generated at
改造:Porterっぽい編集バーを生やすUserScript
この略し方だと、あたかも本家より良くなったように見えるけれど、そんな事はないです
ぶっちゃけ前から気になっていたけれどいい略し方が思いつかなかった
他にいい案があれば置き換えます
ぶっちゃけ大したことは書いていない

使い方
普通に使う場合
ここからソースコードを取得してUserScriptとして自分のページに貼り付ける。
自分で用意したボタンを追加する場合(上級者向け)
ここの手順を始める前に、addGeneralButtons.tsとeditBar.tsを自分のプロジェクトにコピーしたほうが無難
こんな感じのTypeScriptソースを書いて、takker/scrapbox-bundlerでコンパイルする
ts
import { Item, ItemIcon, ItemText useEditBar } from "./editBar.ts" import "./addGeneralButtons.ts" import { insertTimestamp } from "https://scrapbox.io/api/code/takker/scrapbox-userscript-std/dom.ts" if (/mobile/i.test(navigator.userAgent)) { // ボタンを追加する useEditBar().render({ type: "text", name: "ボタンの名前", text: "表示する文字", onClick: (e:MouseEvent)=>{ // クリック時に実行する処理 } } as ItemText) }
文字を表示するのではなく、Font Awesomeのアイコンを表示する場合
ts
// 前略 // ボタンを追加する useEditBar().render({ type: "icon", name: "ボタンの名前", iconClass: ["fas", "fa-redo"] // これは例(redoアイコンを表示する)。 // Font AwesomeのCSSのセレクタで使われているclassを指定している onClick: (e:MouseEvent)=>{ // クリック時に実行する処理 } } as ItemIcon) // 後略
仕様
元のソースにあったItemGroup型は廃止した
サポートしきれなかった…
にはキャレットを再表示する機能もあったが削除した
意図しない所にキャレットが表示されることもあったので…
なので、キャレットを外す専用のボタンになりました
諸事情(主に製作時期)により本家とは色々と仕様が違います
互換性なんてなかった…
バグ
プロジェクトを移動しても編集バーが消えない
DOMの再生成に巻き込まれて消えるかと思ってた…
トップページでも表示され続けている
色んな理由により多分直しません
個人的に不便を感じていない
ボタンが急に出ると押し間違える危険がある
トップページでも使えるボタンを実装する人がいるかもしれない
ページと被る
被らないようにする方法をあんまりしらないので修正できない
キャレットと被ってしまうのはどうにかしたい
キャレットが出た瞬間にプログラムを実行する手段がないと厳しい気がする
ソースコード
addGeneralButtons.ts
元々のscript.ts
いくつかの基本的なボタンを追加する
ボタンを追加するだけなので、編集バーに置くボタンを1からカスタマイズしたい人は使う必要がない
縦画面だと画面外にはみ出してしまうのでペーストボタンを削除する
ペーストボタンはGboardにあるので必要ない
二段にすることで削除する意味が薄れたので元に戻した
カーソルボタンからカーソルを再表示する機能を
addGeneralButtons.ts
import { useEditBar, ItemIcon } from "./editBar.ts"; import { caret, downBlocks, downLines, getText, indentBlocks, indentLines, insertText, outdentBlocks, outdentLines, press, redo, takeCursor, takeSelection, undo, upBlocks, upLines, } from "https://scrapbox.io/api/code/takker/scrapbox-userscript-std/dom.ts"; function getIconClass(name: string): string[] { switch(name){ case "spinner": return ["fa", "fa-spinner"]; case "check-circle": return ["kamon", "kamon-check-circle"]; case "exclamation-triangle": case "caret-up": case "caret-down": case "caret-left": case "caret-right": case "cut": case "expand": case "i-cursor": case "undo": case "redo": return ["fas", `fa-${name}`]; case "copy": case "clipboard": return ["far", `fa-${name}`]; default: return [""]; } } const selection = takeSelection(); const cursor = takeCursor(); const data: {name: string; onClick: (e:MouseEvent) => void}[] = [ { name: "caret-left", onClick: () => {cursor.focus();caret().selectedText === "" ? outdentBlocks() : outdentLines()}, }, { name: "caret-right", onClick: () => {cursor.focus();caret().selectedText === "" ? indentBlocks() : indentLines()}, }, { name: "caret-up", onClick: () => {cursor.focus();caret().selectedText === "" ? upBlocks() : upLines()}, }, { name: "caret-down", onClick: () => {cursor.focus();caret().selectedText === "" ? downBlocks() : downLines()}, }, { name: "copy", onClick: async () => { try { const { position, selectedText } = caret(); const text = selectedText || getText(position.line); if (!text) return; await navigator.clipboard.writeText(text); } catch (e: unknown) { console.error(e); alert(`Faild to copy:\n${JSON.stringify(e)}`); } }, }, { name: "cut", onClick: async () => { try { const hasSelection = selection.hasSelection(); const start = selection.getRange().start.line; const text = hasSelection ? selection.getSelectedText() : getText(start); if (!text) return; await navigator.clipboard.writeText(text); if (!hasSelection) { selection.setRange({ start: { line: start, char: 0 }, end: { line: start, char: text.length }, }); } cursor.focus(); press("Delete"); } catch (e: unknown) { console.error(e); alert(`Faild to cut:\n${JSON.stringify(e)}`); } }, }, { name: "clipboard", onClick: async () => { try { const text = await navigator.clipboard.readText(); if (!text) return; cursor.focus(); await insertText(text); } catch (e: unknown) { console.error(e); alert(`Faild to paste:\n${JSON.stringify(e)}`); } }, }, { name: "undo", onClick: () => undo(), }, { name: "redo", onClick: () => redo(), }, { name: "i-cursor", onClick: () => { if (cursor.getVisible()) { cursor.hide(); } else { // カーソルを再表示する機能はいらないので無効化した // cursor.focus(); // cursor.showEditPopupMenu(); } }, }, ]; if (/mobile/i.test(navigator.userAgent)) { // dataやtextsの中身を読み取ってボタンを作成する for (const { name, onClick } of data) { const { render } = useEditBar(); render({type:"icon", name, iconClass:getIconClass(name), onClick} as ItemIcon); } }

editBar.ts
編集バーを作成し、ボタンを追加するための基本的な機能が入っている
editBar.ts
/// <reference no-default-lib="true" /> /// <reference lib="esnext" /> /// <reference lib="dom" /> /** CSSでボタンの中身を表示するタイプのボタンの情報を格納しておくための型 */ export type ItemIcon = { type: "icon"; name: string; iconClass: string[]; onClick: (e:MouseEvent)=>void; } /** テキストでボタンの中身を表示するタイプのボタンの情報を格納しておくための型 */ export type ItemText = { type: "text"; name: string; text: string; onClick: (e:MouseEvent)=>void; } /** ボタンの情報を格納しておくための型。こっちは汎用的に使う。 */ export type Item = ItemText | ItemIcon; const makeEditBar = (): HTMLDivElement => { const style = document.createElement("style"); /* ↓editBar.cssを直接埋め込んでいる*/ style.textContent = ".bottom-bar{position:fixed;display:flex;flex-direction:column;align-items:flex-end;z-index:300;bottom:0;width:100%}div.status-bar{position:relative}.edit-bar{position:relative;border-top:var(--edit-bar-border);display:grid;grid-auto-flow:row;grid-template-columns:repeat(auto-fill,48px);grid-template-rows:auto;width:100%;overflow-x:auto;overflow-y:hidden;background-color:#0000001a;--edit-bar-border: 1px solid var(--tool-light-color, #a9aaaf)}.edit-bar>div{border:var(--edit-bar-border);background-color:var(--body-bg, #dcdde0);color:var(--tool-text-color, #666874);font-size:16px;height:34px;line-height:28px;text-align:center;vertical-align:middle;cursor:pointer}.edit-bar>div:first-child{border-top-left-radius:unset}.edit-bar>div:last-child{border-top-right-radius:unset}"; document.head.append(style); const editBar = document.createElement("div"); editBar.classList.add("edit-bar"); const bottomBar = document.createElement("div") bottomBar.classList.add("bottom-bar") const statusBar = document.querySelector(".status-bar")! document.querySelector(".app")!.append(bottomBar) bottomBar.append(statusBar) bottomBar.append(editBar); return editBar; }; const bar = makeEditBar(); export interface UseEditBarResult { /** * 取得した.edit-barの領域にボタンを表示する * @param items ボタンの情報 * @param onClick ボタンを押した時の動作 * */ render: (item: Item) => void; /** 表示したボタンを取得した領域ごと削除する */ dispose: () => void; } /** .edit-barの一区画を取得し、各種操作函数を返す */ export const useEditBar = (): UseEditBarResult => { const status = document.createElement("div"); bar.append(status); let listener: ((e: MouseEvent) => void) | undefined; return { render: (item: Item) => { status.textContent = ""; if (listener) status.removeEventListener("click", listener); listener = item.onClick; status.classList.add("eb-"+item.name) const child = makeGroup(item); if (child) { if (listener) status.addEventListener("click", listener); status.append(child); } }, dispose: () => status.remove(), }; }; const makeGroup = (item: Item): HTMLSpanElement | undefined => { switch (item.type) { case "icon": return makeIcon(...item.iconClass); case "text": return makeItem(item.text); } }; const makeItem = (child: string | Node): HTMLSpanElement => { const span = document.createElement("span"); span.classList.add("item"); span.append(child); return span; }; const makeIcon = (...classNames: string[]): HTMLElement => { const i = document.createElement("span"); if(classNames.join("") != "") i.classList.add(...classNames); return i; };

CSS
TypeScriptのソースコードから直接読み込んではいない
つまりここのソースコード(editBar.css)を変更したところで直には反映されないということ
minifyした後にstatusBar.tsに手動でつっこむ
editBar.css
.bottom-bar { position: fixed; display: flex; flex-direction: column; align-items: flex-end; z-index: 300; bottom: 0; width: 100%; } div.status-bar { position: relative; } .edit-bar { position: relative; border-top: var(--edit-bar-border); display: grid; grid-auto-flow: row; grid-template-columns: repeat(auto-fill, 48px); grid-template-rows: auto; width: 100%; overflow-x: auto; overflow-y: hidden; background-color: rgba( 0, 0, 0, .1); --edit-bar-border: 1px solid var(--tool-light-color, #a9aaaf); } .edit-bar > div { border: var(--edit-bar-border); background-color: var(--body-bg, #dcdde0); color: var(--tool-text-color, #666874); font-size: 16px; height: 34px; line-height: 28px; text-align: center; vertical-align: middle; cursor: pointer; } .edit-bar > div:first-child { border-top-left-radius: unset; } .edit-bar > div:last-child { border-top-right-radius: unset; /*border-right: var(--edit-bar-border);*/ }