generated at
インデント毎に可視/不可視を切り替える
2022-04-11 バグレポートです(どこに書いて良いか分からないのでここに書きます)
【バグ内容】クローム時で実行時、下記のforEachの部分でエラーが生じ、動作が中断しました
const linesDOM = document.getElementsByClassName('lines')?.0;
function addIndentLevels() {
linesDOM.children.forEach(line => {
【修正案】htmlcollectionだと foreachにならないので、下記のように変換したところ動作しました
const linesDOM = document.getElementsByClassName('lines')?.0;
function addIndentLevels() {
let elements = Array.from( linesDOM.children ) ;//htmlcollectionだと foreachにならないので変換
elements.forEach(line => {
※報告方法・報告内容に誤りがありましたら、たいへんすみません


2020-12-30 公開
インデント毎に背景色を変える に加えて、インデント毎の可視/不可視を切り替えられるようにした

色つきボタンは可視、灰色ボタンは不可視



お試し方法
以下を自分のページに書き込んでください
js
import '/api/code/customize/インデント毎に可視/不可視を切り替える/script.js';
UserCSSの設定は不要です


インデントなし(レベル0)を不可視にするのは却下
ページタイトルも消えるので危険
コードブロックや表は、タイトル行( code:... table:... ) と中味とのインデントレベルがずれる(かもしれない)ので、同時に可視・不可視を制御したいなら工夫が必要
インデントブロックごとに表示非表示を切り替えるのもよさそう
よりアウトライナーっぽくなる
そういう使い方も「あり」ですね
インデントブロック毎に切り替えボタンを用意する
.indent-class-n の存在で、ボタンを付ける位置を特定できるか?

参考にしたモノ
インデント毎に隠す方法 → Hierarchy Extension
ページ <head>内に <style>要素を作っておき、css を追加する。
どんどんたまる(たまに、ページ再読み込みがよいかも)
ボタンを押すたびにクリア→書き換えするといいかも
解消しました
インデント背景やボタンの色→ テーブルを派手めに表示するUserCSS
Web Componentで<style>をまとめるとかするともう少しスッキリするかもしれませんtakker
/takker/Shadow DOMの練習にあるコードをベースにすればボタンを作れそう
Shadow DOM よいですね。初めて知りました (Web Front-end 全然知らないので) tenfu2tea
添削ありがとうございます > takker
いえこちらこそtakker
丁度こういうUserScript作りたいなーっと思っていたので、とても助かります
19:19:54 Web Componentにしました
最初のクリックだけ、何故か表示非表示が切り替わらないバグがあります。
直しました。
相当大胆に書き換えた影響で、元のコードを一部消してしまいました。申し訳ございませんtakker
すっきりして、すばらしいです tenfu2tea


既知の問題
カーソルをおいた行からハイライトが消える
行の属性変更に対する監視を行っていないのが原因
記法が展開される行をクリックすれば直る
記法が展開されると、DOMが変更されるので、監視に引っかかる
すでにここで言及されている:
トップページに移動してから別のページに移ると、切り替えボタンが消える
ページ遷移も監視して、ボタンを再生成する必要がある
ページ中の最大インデント数から動的にボタンを生成するように動作を変えれば、ページ遷移を監視せずにボタンを再生成できそう
一瞬だけ素のチェックボックスが表示される
DOMの追加とstyleの適用にタイムラグがあるせいだと思われる
対策
styleの読み込みをPromiseで待つ
cssを文字列として直接埋め込む

script.js
import '/api/code/customize/インデント毎に可視/不可視を切り替える/button.js'; // 行用styleを入れる document.body.insertAdjacentHTML('beforeend', `<style id="indent-level-style"> @import '/api/code/customize/インデント毎に可視/不可視を切り替える/style.css'; </style>`); // checkboxを1から MAX_LEVELS まで作成する const MAX_LEVELS = 10; // 1から10までを設定。 11以上はスタイルを用意していない const createCheckboxes = () => { const root = document.getElementsByClassName('page-menu')?.[0]; for (let i = 1; i <= MAX_LEVELS; i++) { const toggleButton = document.createElement('outline-toggle-button'); toggleButton.setAttribute('target-indent-level', i); toggleButton.setAttribute( 'on-label', i); // toggleButton.setAttribute( 'on-label', i+' on'); toggleButton.setAttribute('off-label', i); // toggleButton.setAttribute('off-label', i+' off'); root.appendChild(toggleButton); } }; createCheckboxes();


script.js
const linesDOM = document.getElementsByClassName('lines')?.[0];

以下は、「インデント毎に背景色を変える」と同じインデントにレベルのクラスを付与する

インデントレベルを取得する方法
span.indent-mark に含まれる span[class^="c-"] の数を数える方法
任意のインデントに対してスタイルを設定できる
2020-12-30 18:17:32 ↑に変更しました
この方法では、ブロック(コードブロックや表ブロック)のインデントレベルが取得できない
class^="c-" は、素のテキストにのみ付くから

script-indent-mark.js
function addIndentLevels() { linesDOM.children.forEach(line => { // indent levelを計算する const indentLevel = line .querySelectorAll('.indent-mark span[class^="c-"]').length; const newIndentClass = `indent-level-${indentLevel}`; // すでにindent levelが付与されていたら消す if (/indent-level-\d+/.test(line.className)) { const indentClass = line.className.match(/indent-level-\d+/)?.[0]; if (indentClass === newIndentClass) return; line.classList.replace(indentClass, newIndentClass); } else { line.classList.add(newIndentClass); } }); }

別の方法
span.indent-mark style 属性にある width: ○○em; の数字○○を取得して、1.5で割り、整数にする
span.indent-mark が見つからなければ indent-level-0 とみなす
直下のコードが動作したので、上と交換してみた
動作不良でしたら、上を script-indent-mark.js を採用してください
tableヘッダとtable中味は、同じ width が付いているので、同じインデントレベルになる
code中味は、codeヘッダに比べて width が一つ分 (1.5em)増える。
ヘッダと中味が同じレベルとなるように、ヘッダのレベルを中味のレベルと一致させてみた
そうしないと、インデントなしのコードブロックが、うまく扱えない
ただし、地テキストのインデントレベルとずれるのは気持ち悪い
codeヘッダの .line 要素は、 .code-start の存在で判定できる
code中味の .line 要素は、 .code-block の存在で判定できる
codeのヘッダと中味を、連動して可視/不可視できるようになった(ように見える)
code中味 を含む .line 要素に .indent-level-n が追加されない、ようだ?
なぜか code中味もcodeヘッドと連動して不可視にできる
動作状況
個人的に使っている別のCSSが適用されてたtakker
行番号とcodeヘッダから伸びている変な棒は本CSSと無関係なので無視してください


script.js
function addIndentLevels() { linesDOM.children.forEach(line => { // indent levelを計算する let indentLevel = 0; const indentMarkNode = line.querySelector('.indent-mark'); const isCodeStart = line.querySelector('.code-start'); // const isCodeBlock = line.querySelector('.code-block'); if (indentMarkNode) { const result = /width: ([0-9\.]+)em;/.exec(indentMarkNode.getAttribute('style')); indentLevel = Math.ceil(parseFloat(result[1])/1.5); if (isCodeStart) { indentLevel += 1; } } // indentLevel が取得できたら,後は上と同じ const newIndentClass = `indent-level-${indentLevel}`; // すでにindent levelが付与されていたら消す if (/indent-level-\d+/.test(line.className)) { const indentClass = line.className.match(/indent-level-\d+/)?.[0]; if (indentClass === newIndentClass) return; line.classList.replace(indentClass, newIndentClass); } else { line.classList.add(newIndentClass); } }); }

.indent-level-n を付与・修正する
ページ読み込み時
DOM変更を監視 MutationObserver
下のプログラムでは、DOMが変化すると、全ての .line 要素を処理するが
変化した要素のみ処理すれば、早く終わるはず
変化した要素を含む親 .line 要素を、すぐに見つける方法はあるか?
script.js
// DOM読み込み後の処理 addIndentLevels(); // DOM監視 (function () { const observer = new MutationObserver(() => addIndentLevels() ) observer.observe(linesDOM, { childList: true, subtree: true }) })()

行の背景色を設定する
style.css
.indent-level-0 { } .indent-level-1 { background-color: hsla( 30,100%,90%,0.6); } .indent-level-2 { background-color: hsla( 60,100%,90%,0.6); } .indent-level-3 { background-color: hsla( 90,100%,90%,0.6); } .indent-level-4 { background-color: hsla(120,100%,90%,0.6); } .indent-level-5 { background-color: hsla(150,100%,90%,0.6); } .indent-level-6 { background-color: hsla(180,100%,90%,0.6); } .indent-level-7 { background-color: hsla(210,100%,90%,0.6); } .indent-level-8 { background-color: hsla(240,100%,90%,0.6); } .indent-level-9 { background-color: hsla(270,100%,90%,0.6); } .indent-level-10 { background-color: hsla(300,100%,90%,0.6); }

Web Componentで表示切り替えボタンを作る
チェックボックスのON/OFFで、ボックス内に表示するテキストを変えられる
「テキストを変えられる」はどのテキストのことを示していますか?takker
行の表示を切り替えることを「テキストを変え」ると表現しているのだと読んでいたのですが、違うっぽいのでお聞きしました
チェックボックス内に表示されるテキストです。
わかりましたtakker
というか「ボックス内に表示するテキスト」と書いてありましたね。ものすんごい誤読をしてました……すみません。
ボックス内に表示するテキストを、ON/OFFで変えられるようにした
createCheckboxes 内。コメントアウトされた on-label off-label への setAttribute を参照
button.js
customElements.define('outline-toggle-button', class extends HTMLElement { connectedCallback() { if (this.rendered) return; this.render(); this.rendered = true; } render() { const shadow = this.attachShadow({mode: 'open'}); const number = this.getAttribute('target-indent-level'); const onLabel = this.getAttribute('on-label'); const offLabel = this.getAttribute('off-label'); // DOMを作る shadow.innerHTML = ` <style> @import '/api/code/customize/インデント毎に可視/不可視を切り替える/button.css'; </style> <input class="level-${number}" type="checkbox" data-off-label="${offLabel}" data-on-label="${onLabel}"></input>`; this._createLineStyle(number); // event listenerの登録 const input = this.shadowRoot.lastElementChild; input.checked = true; input.addEventListener('change', () => { // スタイルを入れる要素 const level = this.getAttribute('target-indent-level'); const style = document.getElementById(`indent-level-styles-${level}`) ?? this._createLineStyle(level); // なければ作る style.textContent = `.indent-level-${level} { display: ${input.checked ? 'block' : 'none'}; }`; return false; }); } // DOMの更新処理 update() { if (!this.rendered) { this.render(); this.rendered = true; return; } const input = this.shadowRoot.lastElementChild; const number = this.getAttribute('target-indent-level'); input.className = `level-${number}`; input.dataset.onLabel = this.getAttribute('on-label'); input.dataset.offLabel = this.getAttribute('off-label'); } static get observedAttributes() { return [ 'target-indent-level', 'on-label', 'off-label' ]; } attributeChangedCallback(name, oldValue, newValue) { this.update(); } // 対応する行のstyleを生成する _createLineStyle(number) { const id = `indent-level-styles-${number}`; if (document.getElementById(id)) return; // すでにあるなら何もしない const style = document.createElement('style'); style.id = id; style.textContent = `.indent-level-${number} { display: block; }`; document.head.appendChild(style); return style; } });

表示切り替えボタンのスタイル
button.css
.level-0:checked { } .level-1:checked { background-color: hsla( 30,100%,90%,0.6); } .level-2:checked { background-color: hsla( 60,100%,90%,0.6); } .level-3:checked { background-color: hsla( 90,100%,90%,0.6); } .level-4:checked { background-color: hsla(120,100%,90%,0.6); } .level-5:checked { background-color: hsla(150,100%,90%,0.6); } .level-6:checked { background-color: hsla(180,100%,90%,0.6); } .level-7:checked { background-color: hsla(210,100%,90%,0.6); } .level-8:checked { background-color: hsla(240,100%,90%,0.6); } .level-9:checked { background-color: hsla(270,100%,90%,0.6); } .level-10:checked { background-color: hsla(300,100%,90%,0.6); } input { appearance: none; -webkit-appearance: none; -ms-appearance: none; -moz-appearance: none; display: inline-block; vertical-align: top; width: 48px; height: 48px; margin: 0; border-radius: 8px; border: 1px solid #aaa; text-align: center; line-height: 44px; font-size: 18px; cursor: pointer; transition: 50ms; } input:active{ height: 48px; margin-top: 2px; transition: none; } input:not(:checked)::after{ display: inline; content: attr(data-off-label); transition: none; } input:checked::after{ display: inline; content: attr(data-on-label); transition: 50ms; } input:active::after{ transition: none; }

UserScript