まえがき
まずは写真を。
デモアドレス: web-marker demo
git: github.com/...
npm: www.npmjs.com/package/web...
分析
プラグインは、フレームワークの選択肢はありませんが、rollupでパッケージ化され、typescriptでもう少し厳格な、他のeslintこれらは装備されていない、怠惰なため、理由を聞かないでください。
要件の分析:最も直感的な質問は3つあります。
- ユーザーが選択したテキストの内容を取得します。
- ユーザが選択したテキストノードをマークします。
- ユーザーが選択したテキストノードの親ノードを取得します。
その解決法をひとつずつご紹介しましょう。
I: ユーザーが選択したテキストコンテンツの取得
この関数は、実際には、関連するインターフェイスは、 window.getSelection()、クリアをチェックし、開始位置と位置の終わりを含む、現在選択されているテキストノードの情報に返されます通常のほとんど接触することがあり、次のメソッドの主な用途とテキスト。
const selectedText = window.getSelection()
selectedText.getRangeAt(0) // ノード情報を返すには、0を渡さなければならないようだ。;
selectedText.toString() // 本文内容に戻る;
selectedText.getRangeAt(0).surroundContents(dom) // 現在の選択範囲を渡されたドムで置き換える
selectedText.removeAllRanges() // 選択したコンテンツを手動で削除する、これはわかりやすい。
これは、上記のインターフェイスを介して、基本的に要件を達成するようですが、理想は非常に豊かですが、現実は非常に骨である、最初の問題が発生したノードの位置を取得する方法です、上記のインターフェイスは、視覚的なマーキング機能を実現することができますので、しかし、現在選択されているノードの親ノードなどの情報の場所を保存することはできません、最初の単語から最初の単語の末尾に選択したテキストは、同じノードに複数のマークを選択する方法どのように同じノード内の複数のマーカーを計算するには?
最初に現在のタグ付き親ノードを取得し、操作を容易にするために、私はより暴力的な、新しいクラスを追加するには、ページ上のすべてのノードの初期化は、この利点は、タグ付きデータをロードするページへの位置と後の計算は、はるかに便利ですが、欠点は、それがパフォーマンスホッグのビットであるということですエレガントではありません。
export const setMarkClassName = (dom: HTMLElement, index:string = '1') => {
if(dom === document.body){
dom.className = '_WM-0'
}
if (dom.childNodes) {
for (let i = 0; i < dom.childNodes.length; i++) {
const childNode = dom.childNodes[i] as HTMLElement
if (childNode.nodeType === 1) {
const ingoreNodes = ['BR', 'HR', 'SCRIPT', 'BUTTON']
if (!ingoreNodes.includes(childNode.nodeName)) {
childNode.className = childNode.className ? childNode.className + ` _WM-${index}-${i}` : `_WM-${index}-${i}`
}
if (childNode.childNodes.length > 0) {
setMarkClassName(childNode, index + 1 + `-${i}`)
}
}
}
}
}
handleMouseUp(){
// ...
const text = this.selectedText.toString()
const rang = this.selectedText.getRangeAt(0)
const span = setTextSelected(this.TEMP_MARKED_CLASSNAME, text, this.tempMarkerInfo.id)
rang.surroundContents(span)
// ...
}
// setTextSelected utilsのメソッド
export const setTextSelected = (className: string, text: string, id: string) => {
const span = document.createElement('span')
span.className = className
span.id = id
span.innerHTML = text
return span
}
また、getRangeAt(0)は選択されたノードに関する情報も保存します。
handleMouseUp(){
// ...
const {commonAncestorContainer} = this.selectedText.getRangeAt(0)
const {anchorOffset, focusOffset} = this.selectedText
const startIndex = Math.min(anchorOffset, focusOffset)
const endIndex = Math.max(anchorOffset, focusOffset)
const className = commonAncestorContainer.parentNode.className.split(' ')
let parentClassName = className[className.length - 1]
this.tempMarkerInfo = new Marker(setuuid(), parentClassName, 0, startIndex, endIndex)
// ...
}
// Marker これはクラス,
class Marker implements IMarker {
id : string;
childIndex : number;
start : number;
end : number;
parentClassName?: string;
constructor(id : string, parentClassName : string, childIndex : number, start : number, end : number) {
this.id = id
this.childIndex = childIndex
this.start = start
this.end = end
this.parentClassName = parentClassName || ''
}
}
同時に this.show()を呼び出すと、アクションボックスを表示し、いくつかの判断があるかどうかなど、複数のノードの選択は、上記の要件を取る、 複数のノードにまたがる操作を必要としないときにアクションボックスを表示し、上記の新しいノードの置換に応じて表示位置を制御するために、これらは困難ではありません。
: タグの追加
mark(){
const tempMarkDom = document.getElementsByClassName(this.TEMP_MARKED_CLASSNAME)[0]
tempMarkDom.className = this.MARKED_CLASSNAME
this.currentId = this.tempMarkerInfo.id
this.tempMarkerInfo = null
this.resetMarker(parentClassName)
this.selectedText.removeAllRanges()
this.hide()
}
キーは this.resetMarker(domClassName)で、渡されるparentClassNameはマーカーが配置されている親ノードです。ここではコードはコア部分のみを掲載していますので、詳細はgithub.com/移動してください。...
private resetMarker(domClassName : string) {
const dom = document.getElementsByClassName(domClassName)[0]
const newMarkerArr : Marker[] = []
let preNodeLength = 0
for (let i = 0; i < dom.childNodes.length; i++) {
const node = dom.childNodes[i]
if (node.nodeName === '#text') {
preNodeLength = node.textContent.length
}
// childIndex なぜi - 1なのか?? ノードのインデックスは、例えばポイントの内容が「xxx」である場合など、順序を反転させたときに正しい位置が分かるように記されている。
// <ノードをマーキングする>ooo</ノードをマーキングする>", i それは1であり、実際には逆順の0番に位置する。
const childIndex = i - 1
this.selectedMarkers[domClassName].forEach((marker : Marker) => {
const child = dom.childNodes[i] as HTMLElement
if (child.id == marker.id) {
newMarkerArr.push(new Marker(marker.id, '', childIndex, preNodeLength, preNodeLength + node.textContent.length))
}
})
}
//
this.selectedMarkers[domClassName] = newMarkerArr
}
まず、現在のページは次のようになっているはずです。
ノードは大体こんな感じです。
resetMarkerで処理されるドムは"_wm-11-1-3 "であり、resetMarkerの目的は、このノードのすべてのマークされたノードをマークされるたびに再計算し、次のマーカーの削除を含めて保存することです。
.タグの削除
タグの削除は比較的簡単で、タグの付いたテキストをクリックすると、currentIdがキャッシュされ、アクションボックスが表示されます。
del() {
if (!this.currentId) return
this.tempMarkDom = null
const dom = document.getElementById(this.currentId) as any
// 自分のクラスを取得する
const className = dom.parentNode.className.split(' ')
const parentClassName = className[className.length - 1]
mergeTextNode(dom)
this.resetMarker(parentClassName)
}
削除には、テキスト・コンテンツをマージする mergeTextNode() という重要なステップがあります。
export const mergeTextNode = (dom: HTMLElement) => {
const parentNode = dom.parentNode
if(!parentNode) return
const text = dom.innerText
const replaceTextNode = document.createTextNode(text)
// ここでは、タグ付きスパン・ノードをテキスト・ノードに置き換えているが、前後のテキストとはまだ分離している。,
parentNode.replaceChild(replaceTextNode, dom)
const preDom = replaceTextNode.previousSibling
const nextDom = replaceTextNode.nextSibling
// テキストノードをマージする
if (preDom && preDom.nodeType === 3) {
preDom.textContent = preDom.textContent + text
parentNode.removeChild(replaceTextNode)
if (nextDom && nextDom.nodeType === 3) {
preDom.textContent = preDom.textContent + nextDom.textContent
parentNode.removeChild(nextDom)
}
} else {
if (nextDom && nextDom.nodeType === 3) {
replaceTextNode.textContent = replaceTextNode.textContent + nextDom.textContent
parentNode.removeChild(nextDom)
}
}
}
テキストをマークした後、テキストは3つのノードになります。前方のテキストノード、中央のマークされたノード、後方のテキストノードです。削除後、ここには3つのテキストノードがあり、preDom、nextDomは前後のテキストノードを表しています。
この時点で、基本的にウェブページのマークアップの機能を実装し、コードは最後にいくつかのインターフェースを提供します。
// 現在選択されているタグIDを返す
getCurrentId() {
return this.currentId
}
// 現在のページのタグ付きデータをすべて返す
getAllMarkes() {
return this.selectedMarkers
}
// 百科事典、辞書、コピー、その他の操作で使われる、現在選択されているテキストを返す。
getSelectedText() {
return this.selectedText.toString()
}
複数のマーカーの色やノードをまたいだ操作など、いくつかの新機能は後で暇なときに追加する予定です。