generated at
Scrapboxで暗記シートを作る

更新履歴
2019/8/26
公開
2019/8/31
[! 文字] が途中で折り返されているとクリックによる表示・非表示がおかしくなる不具合を修正

----------

ScrapBoxで暗記カード - ボルゾイとネコとITエンジニアで公開されているスクリプトを元にして、いくつか機能を追加したUserScriptです。
デスクトップとモバイルの両方で同じように使えます。

UserScriptは自分だけにしか適用されないため、問題文を作成して他の人に配布する目的では利用できません。そういった用途にはScrapboxで暗記シートを作る (CSSだけ使ったバージョン)をお勧めします。


[! ] で囲まれた文字列を非表示にする
クリックで隠された文字が表示される

表示、非表示を一括で切り替えるPage Menu
初期状態で非表示になっていますので、シートを作成する時は表示に切り替えてから行って下さい

[! ] を付け外しするPopup Menu
選択されている文字列に [! ] を付ける
選択範囲に [! ] が含まれる場合は一括で外す


スクリプトの先頭にある decoChar が装飾に使用する文字 ( ! ) になっていますので、適宜変更して下さい。
装飾に使用できる文字についてはこちら。

js
(() => { const decoChar = '!'; const displayColor = '#3cb371'; const showCssString = ` .deco-\\${decoChar} { color: ${displayColor}; border-bottom: 2px solid ${displayColor}; } `; const hideCssString = ` .deco-\\${decoChar} { color: transparent; border-bottom: 2px solid ${displayColor}; } `; const showStyleElm = document.createElement('style'); document.head.appendChild(showStyleElm); showStyleElm.sheet.insertRule(showCssString); const hideStyleElm = document.createElement('style'); document.head.appendChild(hideStyleElm); hideStyleElm.sheet.insertRule(hideCssString); // カーソルの移動を検出して文字を表示する const cursorObserver = new MutationObserver(records => { const decoElmArray = Array.from(document.getElementsByClassName(`deco-${decoChar}`)); if (!decoElmArray || hideStyleElm.disabled) return; for (const record of records) { const cursorRect = record.target.getBoundingClientRect(); let isDecoAtCursorPositionProcessed = false; for (const decoElm of decoElmArray) { if (isDecoAtCursorPositionProcessed) { decoElm.style.color = ''; continue; } const decoElmRect = decoElm.getBoundingClientRect(); const openingBracketRect = decoElm.firstElementChild.getBoundingClientRect(); const closingBracketRect = decoElm.lastElementChild.getBoundingClientRect(); const isWordWrapped = closingBracketRect.left < openingBracketRect.left; if (isWordWrapped) { // [! 文字] が途中で折り返されている時 const decoCharRectArray = Array.from(decoElm.children, value => value.getBoundingClientRect()); const secondLineFirstCharIndex = decoCharRectArray.map(rect => { return rect.left; }).reduce((minimumIndex, currentValue, currentIndex, arr) => { return currentValue < arr[minimumIndex] ? currentIndex : minimumIndex; }, 0); const firstLineLastCharRect = decoCharRectArray[secondLineFirstCharIndex - 1]; const secondLineFirstCharRect = decoCharRectArray[secondLineFirstCharIndex]; const isCursorInsideFirstLine = openingBracketRect.left <= cursorRect.left && cursorRect.right <= firstLineLastCharRect.right && openingBracketRect.top <= cursorRect.top && cursorRect.bottom <= openingBracketRect.bottom; const isCursorInsideSecondLine = secondLineFirstCharRect.left <= cursorRect.left && cursorRect.right <= closingBracketRect.right && closingBracketRect.top <= cursorRect.top && cursorRect.bottom <= closingBracketRect.bottom; if (isCursorInsideFirstLine || isCursorInsideSecondLine) { decoElm.style.color = displayColor; isDecoAtCursorPositionProcessed = true; } else { decoElm.style.color = ''; } } else { // [! 文字] が途中で折り返されていない時 const isCursorInside = decoElmRect.left <= cursorRect.left && cursorRect.right <= decoElmRect.right && decoElmRect.top <= cursorRect.top && cursorRect.bottom <= decoElmRect.bottom; if (isCursorInside) { decoElm.style.color = displayColor; isDecoAtCursorPositionProcessed = true; } else { decoElm.style.color = ''; } } } } }); cursorObserver.observe(document.getElementsByClassName('cursor')[0], {attributes: true, attributeFilter: ['style']}); // 表示、非表示を切り替えるPageMenu scrapbox.PageMenu.addMenu({ title: '暗記シート', image: 'https://i.gyazo.com/55485bbf213f80632f7dee725e74a910.png', /* アイコンは https://scrapbox.io/nyaagoo/ScrapBox%E3%81%A7%E6%9A%97%E8%A8%98%E3%82%AB%E3%83%BC%E3%83%89 のものをそのまま使用させていただいています。リンクが無効になっている場合は適当な画像に差し替えて下さい。 */ onClick: () => hideStyleElm.disabled = !hideStyleElm.disabled }); // [! ]を付け外しするPopupMenu scrapbox.PopupMenu.addButton({ title: text => { const reg = new RegExp(`\\[${decoChar} (.+?)\\]`, 'g'); return reg.test(text) ? `remove [${decoChar}]` : `add [${decoChar}]`; }, onClick: text => { const reg = new RegExp(`\\[${decoChar} (.+?)\\]`, 'g'); if (reg.test(text)) { return text.replace(reg, '$1'); } else { if (!/\n/.test(text)) { return `[${decoChar} ${text}]`; } } } }); // 他のプロジェクトに移動したら終了する const targetProject = scrapbox.Project.name; observeUrlChange((newUrl, oldUrl, observer) => { if (scrapbox.Project.name !== targetProject) { showStyleElm.parentNode.removeChild(showStyleElm); hideStyleElm.parentNode.removeChild(hideStyleElm); cursorObserver.disconnect(); observer.disconnect(); } }); function observeUrlChange(handler) { let lastUrl = location.href; const titleObserver = new MutationObserver((records, observer) => { for (const record of records) { if (lastUrl !== location.href) { const newUrl = location.href; const oldUrl = lastUrl; lastUrl = location.href; handler(newUrl, oldUrl, observer); } } }); const title = document.querySelector('head title'); titleObserver.observe(title, {childList: true}); } })();

UserScript