generated at
scrapbox-selection-3
Scrapboxの選択範囲の領域と文字列をscriptから取得するAPI
warningWIP

2021-07-08
18:57:59 バグってるぅ……takker
調べる
19:00:54 一行しか選択していないとscrapbox-selection-3#5ffa608d1280f00000e42a45にfallbackされちゃうぜいえーい(白目)takker
まあバグの原因がすぐに分かって何より
2021-06-10
15:41:14 多分動く
バグ取りは一切していない
2021-06-08
23:49:17 scrapbox-access-nodes@0.1.0を使って書き換えるだけにとどめておくことにした
scrapbox-cursor-observerを使うのを止める
scrapbox-char-accessorを使っているから

実装
文字列の取得
#text-inputHTMLTextAreaElement.valueから選択範囲中の文字列を取得できる
選択範囲の位置の取得
行番号
座標計算をして取得する
ここだけscrapbox-selection-2から流用する
列番号
文字列比較で取得する
選択範囲の開始/終了位置の列番号を使う
選択範囲内に改行を含まない場合に、位置を一意に決めることができない
改行コードを含んでいる場合は、1行に一つしか改行コードが含まれないので特定できる
改行コードを含んでいない文字列は、1行に複数回出現することがある
例えば「ああああああああ」のなかで「あ」だけ選択していた場合は、どの「あ」を選択しているのか全く判別できない
おとなしくscrapbox-selection-2のコードをそのまま使うしかなさそう。
2021-07-08
テストコードtakker
js
(async () => { const {selection} = await import('/api/code/takker/scrapbox-selection-3/script.js'); let observer = new MutationObserver(() => { const range = selection.range; if (!range.start || !range.end) { console.log(range); return; } const {start, end} = range; const texts = selection.text; console.log({start, end, 'real texts': texts}); }); observer.observe(document.getElementsByClassName('cursor')?.[0], {attributes: true}); })();

dependencies
script.js
import {scrapboxDOM} from '../scrapbox-dom-accessor/script.js'; import {position} from '../scrapbox-cursor-position-6/script.js'; import { getDOMFromPoint, getLineNo, getIndex, } from '../scrapbox-access-nodes@0.1.0/script.js'; class Selection { constructor() { this._cursorObserver = undefined; // 先に.selectionの監視を開始する this._selectionObserver = new MutationObserver(mutations => { if (mutations.flatMap(mutation => [...mutation.addedNodes]) .some(element =>element.classList.contains('selections'))) { this._selectMode = true; this._recordSelectionEdge(); // 選択範囲の開始位置を記録しておく } if (mutations.flatMap(mutation => [...mutation.removedNodes]) .some(element =>element.classList.contains('selections'))) { this._selectMode = false; } }); this._selectionObserver.observe(scrapboxDOM.editor, { childList: true }); this._cursorObserver = new MutationObserver(mutations => { if (scrapboxDOM.cursor.style.display === 'none') return; const {char, line} = position(); this._position = {index: getIndex(char), lineNo: getLineNo(line)}; }); this._cursorObserver.observe(scrapboxDOM.cursor, {attributes: true}); this._recordedEdge = {}; } // 選択範囲が存在するかどうか get exist() { return this.text !== ''; } // 選択範囲の開始位置と終了位置を返す get range() { //if (!this.exist) return {message: '[range@scrapbox-selection-3] No range found.'}; // 順番を判定する if (this._recordedEdge.lineNo > this._position.lineNo) { return { start: this._position, end: this._recordedEdge, }; } if (this._recordedEdge.lineNo < this._position.lineNo) { return { start: this._recordedEdge, end: this._position, }; } // 行が同じの場合は列で比較する if (this._recordedEdge.index > this._position.index) { return { start: this._position, end: this._recordedEdge, }; } if (this._recordedEdge.index < this._position.index) { return { start: this._recordedEdge, end: this._position, }; } // 完全に位置が一致している場合 return {message: '[range@scrapbox-selection-3] the start position is the same as the end one.'}; } get text() { if (!/mobile/i.test(navigator.userAgent)) return scrapboxDOM.textInput.value; const range = selection.range; if (!range.start || !range.end) return; const {start, end} = range; return [ scrapbox.Page.lines[start.lineNo].text.substring(start.index), ...scrapbox.Page.lines.map(line => line.text).slice(start.lineNo + 1, end.lineNo), scrapbox.Page.lines[end.lineNo].text.substring(0, end.index + 1), ].join('\n'); } //cursorがない方の選択範囲の端を取得する // この関数を呼び出すタイミングではまだ_cursorObserverが呼び出されていないので、独自にcursorの位置を取得する _recordSelectionEdge() { if (!this.exist) return undefined; const selections = scrapboxDOM.selections?.getElementsByClassName('selection'); // 一瞬で選択範囲が消えるとselectionsがundefindeになる場合がある if (!selections || selections?.length === 0) { this._selectMode = false; return undefined; } // 選択範囲の端の座標 const startRect = selections[0].getBoundingClientRect(); const endRect = (selections[2] ?? selections[0]).getBoundingClientRect(); // 選択範囲の端の文字を取得する const start = getDOMFromPoint(startRect.left, startRect.top); const end = getDOMFromPoint(endRect.right, endRect.bottom); // 行数と列数を計算する const startEdge = { index: getIndex(start.char), // 末尾の場合はundefined lineNo: getLineNo(start.line), }; const endEdge = { index: getIndex(end.char), // 末尾の場合はundefined lineNo: getLineNo(end.line), }; // cursorの位置を取得する const temp = position(); const cursorEdge = { index: getIndex(temp.char), lineNo: getLineNo(temp.line), }; // 選択範囲が一行のときは、選択範囲の端の座標から計算した位置を使う // 1行かつ選択範囲の取得開始直後なら記法が展開されていることが保証されている if (startEdge.lineNo === endEdge.lineNo) { if (cursorEdge.index === startEdge.index && cursorEdge.lineNo === startEdge.lineNo) { this._recordedEdge = endEdge; } else { this._recordedEdge = startEdge; } return; } // 選択範囲が複数行に渡るときは、記録しておいたカーソルの位置を用いる // 記法が隠れている可能性があるので、選択範囲の座標から位置を計算できない this._recordedEdge = this._position; } } export const selection = new Selection();

#2021-07-08 18:58:20
#2021-06-10 15:41:32
#2021-06-09 00:03:28