インデント毎に可視/不可視を切り替える
【バグ内容】クローム時で実行時、下記の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 => {
※報告方法・報告内容に誤りがありましたら、たいへんすみません
色つきボタンは可視、灰色ボタンは不可視
https://gyazo.com/fc90f858b083821ef752aaeb9c38c15e
お試し方法
以下を自分のページに書き込んでください
code:js
import '/api/code/customize/インデント毎に可視/不可視を切り替える/script.js';
UserCSSの設定は不要です
インデントなし(レベル0)を不可視にするのは却下
ページタイトルも消えるので危険
コードブロックや表は、タイトル行(code:... や table:...) と中味とのインデントレベルがずれる(かもしれない)ので、同時に可視・不可視を制御したいなら工夫が必要
インデントブロックごとに表示非表示を切り替えるのもよさそう
そういう使い方も「あり」ですね
インデントブロック毎に切り替えボタンを用意する
.indent-class-n の存在で、ボタンを付ける位置を特定できるか?
参考にしたモノ
ページ <head>内に <style>要素を作っておき、css を追加する。
どんどんたまる(たまに、ページ再読み込みがよいかも)
ボタンを押すたびにクリア→書き換えするといいかも
解消しました
Web Componentで<style>をまとめるとかするともう少しスッキリするかもしれませんtakker.icon
Shadow DOM よいですね。初めて知りました (Web Front-end 全然知らないので) tenfu2tea.icon
添削ありがとうございます > takker.icon
いえこちらこそtakker.icon
丁度こういうUserScript作りたいなーっと思っていたので、とても助かります
19:19:54 Web Componentにしました
最初のクリックだけ、何故か表示非表示が切り替わらないバグがあります。
直しました。
相当大胆に書き換えた影響で、元のコードを一部消してしまいました。申し訳ございませんtakker.icon
すっきりして、すばらしいです tenfu2tea.icon
既知の問題
カーソルをおいた行からハイライトが消える
行の属性変更に対する監視を行っていないのが原因
記法が展開される行をクリックすれば直る
記法が展開されると、DOMが変更されるので、監視に引っかかる
すでにここで言及されている:
トップページに移動してから別のページに移ると、切り替えボタンが消える
ページ遷移も監視して、ボタンを再生成する必要がある
ページ中の最大インデント数から動的にボタンを生成するように動作を変えれば、ページ遷移を監視せずにボタンを再生成できそう
一瞬だけ素のチェックボックスが表示される
DOMの追加とstyleの適用にタイムラグがあるせいだと思われる
対策
styleの読み込みをPromiseで待つ
cssを文字列として直接埋め込む
code: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();
code:script.js
const linesDOM = document.getElementsByClassName('lines')?.0; インデントレベルを取得する方法
span.indent-markに含まれるspan[class^="c-"]の数を数える方法
任意のインデントに対してスタイルを設定できる
この方法では、ブロック(コードブロックや表ブロック)のインデントレベルが取得できない
class^="c-" は、素のテキストにのみ付くから
code:script-indent-mark.js
function addIndentLevels() {
linesDOM.children.forEach(line => {
// indent levelを計算する
const indentLevel = line
.querySelectorAll('.indent-mark spanclass^="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ヘッドと連動して不可視にできる
動作状況
https://gyazo.com/ed9b8eb0e952071cbec853c5c886160d
個人的に使っている別のCSSが適用されてたtakker.icon
行番号とcodeヘッダから伸びている変な棒は本CSSと無関係なので無視してください
code: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(result1)/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が変化すると、全ての .line 要素を処理するが
変化した要素のみ処理すれば、早く終わるはず
変化した要素を含む親 .line 要素を、すぐに見つける方法はあるか?
code:script.js
// DOM読み込み後の処理
addIndentLevels();
// DOM監視
(function () {
const observer = new MutationObserver(() => addIndentLevels() )
observer.observe(linesDOM, {
childList: true,
subtree: true
})
})()
行の背景色を設定する
code: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.icon
行の表示を切り替えることを「テキストを変え」ると表現しているのだと読んでいたのですが、違うっぽいのでお聞きしました
チェックボックス内に表示されるテキストです。
わかりましたtakker.icon
というか「ボックス内に表示するテキスト」と書いてありましたね。ものすんごい誤読をしてました……すみません。
ボックス内に表示するテキストを、ON/OFFで変えられるようにした
createCheckboxes 内。コメントアウトされた on-label と off-label への setAttribute を参照
code: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() {
}
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;
}
});
表示切り替えボタンのスタイル
code: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;
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.icon