generated at
Porterっぽい編集バーを生やすUserScript
元のソースコードが載っている
ここにあるソースコードは多少改変してある

script.js
var R=()=>{let e=document.createElement("style");e.textContent=`.status-bar.left { left: 0; right: unset; } .status-bar > div:first-of-type { border-top-left-radius: unset; } .status-bar > div:last-of-type { border-top-right-radius: 3px; }`,document.head.append(e);let t=document.createElement("div");return t.classList.add("status-bar","left"),document.getElementsByClassName("app")[0].append(t),t},_=R(),w=()=>{let e=document.createElement("div");_.append(e);let t;return{render:(n,r)=>{e.textContent="",t&&e.removeEventListener("touchstart",t),t=r;let c=b(...n);c&&(t&&e.addEventListener("touchstart",t),e.append(c))},dispose:()=>e.remove()}},b=(...e)=>{let t=e.flatMap(r=>{switch(r.type){case"spinner":return[m("fa","fa-spinner")];case"check-circle":return[m("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[m("fas",`fa-${r.type}`)];case"copy":case"clipboard":return[m("far",`fa-${r.type}`)];case"text":return[T(r.text)];case"group":{let c=b(...r.items);return c?[c]:[]}}});if(t.length===0)return;if(t.length===1)return t[0];let n=document.createElement("span");return n.classList.add("item-group"),n.append(...t),n},T=e=>{let t=document.createElement("span");return t.classList.add("item"),t.append(e),t},m=(...e)=>{let t=document.createElement("i");return t.classList.add(...e),T(t)};var l=e=>e==null,d=e=>typeof e=="string",p=e=>typeof e=="number";var L=(e,t)=>{if(!Array.isArray(e))throw new TypeError(`"${t}" must be an array but actual is "${e}"`)};var M=(e,t)=>{if(!(e instanceof HTMLTextAreaElement))throw new TypeError(`"${t}" must be HTMLTextAreaElement but actual is "${e}"`)};var a=()=>{let e=document.getElementById("text-input");if(!!e)return M(e,"textarea#text-input"),e};function P(e){if(l(e))return;if(p(e))return f(e)?.id;if(d(e))return e.startsWith("L")?e.slice(1):e;if(e.classList.contains("line"))return e.id.slice(1);let t=e.closest(".line");if(t)return t.id.slice(1)}function f(e){if(l(e))return;if(p(e))return y()[e];let t=P(e);return t?y().find(n=>n.id===t):void 0}function F(e){return e instanceof HTMLDivElement&&e.classList.contains("line")}function y(){return L(scrapbox.Page.lines,"scrapbox.Page.lines"),scrapbox.Page.lines}function g(e){if(l(e))return;if(p(e)||d(e))return f(e)?.text;if(!(e instanceof HTMLElement))return;if(F(e))return f(e)?.text;if(e.classList.contains("char-index"))return e.textContent??void 0;if(e.classList.contains("line")||e.getElementsByClassName("lines")?.[0])return y().map(({text:r})=>r).join(` `);let t=[],n=f(e);if(!l(n)){for(let r of U(e))t.push(Y(r));return n.text.slice(Math.min(...t),Math.max(...t)+1)}}function*U(e){let t=e.getElementsByClassName("char-index");for(let n=0;n<t.length;n++)yield t[0]}function $(e){return e instanceof HTMLSpanElement&&e.classList.contains("char-index")}function Y(e){if(!$(e))throw Error("A char DOM is required.");let t=e.className.match(/c-(\d+)/)?.[1];if(l(t))throw Error('.char-index must have ".c-{\\d}"');return parseInt(t)}function o(e,t){let{noModifiedKeys:n=!1,...r}=t??{},c={bubbles:!0,cancelable:!0,keyCode:X[e],...n?{}:{...r}},E=a();if(!E)throw Error("#text-input must exist.");E.dispatchEvent(new KeyboardEvent("keydown",c)),E.dispatchEvent(new KeyboardEvent("keyup",c))}var X={Backspace:8,Tab:9,Enter:13,Delete:46,Escape:27," ":32,PageUp:33,PageDown:34,End:35,Home:36,ArrowLeft:37,ArrowUp:38,ArrowRight:39,ArrowDown:40,a:65,A:65,b:66,B:66,c:67,C:67,d:68,D:68,e:69,E:69,f:70,F:70,g:71,G:71,h:72,H:72,i:73,I:73,j:74,J:74,k:75,K:75,l:76,L:76,m:77,M:77,n:78,N:78,o:79,O:79,p:80,P:80,q:81,Q:81,r:82,R:82,s:83,S:83,t:84,T:84,u:85,U:85,v:86,V:86,w:87,W:87,x:88,X:88,y:89,Y:89,z:90,Z:90,0:48,1:49,2:50,3:51,4:52,5:53,6:54,7:55,8:56,9:57,F1:113,F2:114,F3:115,F4:116,F5:117,F6:118,F7:119,F8:120,F9:121,F10:122,F11:123,F12:124,":":186,"*":186,";":187,"+":187,"-":189,"=":189,".":190,">":190,"/":191,"?":191,"@":192,"`":192,"[":219,"{":219,"\\":220,"|":220,"]":221,"}":221,"^":222,"~":222,_:226};function u(){let e=a();if(!e)throw Error("#text-input is not found.");let t=Object.keys(e).find(n=>n.startsWith("__reactFiber"));if(!t)throw Error('div.cursor must has the property whose name starts with "__reactFiber"');return e[t].return.return.stateNode.props}function*s(e,t){for(let n=e;n<t;n++)yield n}function k(e=1){for(let t of s(0,e))o("z",{ctrlKey:!0})}function H(e=1){for(let t of s(0,e))o("z",{shiftKey:!0,ctrlKey:!0})}function C(e=1){for(let t of s(0,e))o("ArrowRight",{ctrlKey:!0})}function v(e=1){for(let t of s(0,e))o("ArrowLeft",{ctrlKey:!0})}function D(e=1){for(let t of s(0,e))o("ArrowUp",{ctrlKey:!0})}function B(e=1){for(let t of s(0,e))o("ArrowDown",{ctrlKey:!0})}function A(e=1){for(let t of s(0,e))o("ArrowRight",{altKey:!0})}function I(e=1){for(let t of s(0,e))o("ArrowLeft",{altKey:!0})}function S(e=1){for(let t of s(0,e))o("ArrowUp",{altKey:!0})}function K(e=1){for(let t of s(0,e))o("ArrowDown",{altKey:!0})}var x=()=>{let e=a();if(!e)throw Error("#text-input is not found.");let t=Object.keys(e).find(n=>n.startsWith("__reactFiber"));if(!t)throw Error('#text-input must has the property whose name starts with "__reactFiber"');return e[t].return.return.stateNode._stores};var N=()=>{for(let e of x())if("goByAction"in e)return e;throw Error('#text-input must has a "Cursor" store.')};var O=()=>{for(let e of x())if("hasSelection"in e)return e;throw Error('#text-input must has a "Selection" store.')};var h=O(),i=N(),W=[{type:"caret-left",onClick:()=>{i.focus(),u().selectedText===""?I():v()}},{type:"caret-right",onClick:()=>{i.focus(),u().selectedText===""?A():C()}},{type:"caret-up",onClick:()=>{i.focus(),u().selectedText===""?S():D()}},{type:"caret-down",onClick:()=>{i.focus(),u().selectedText===""?K():B()}},{type:"copy",onClick:async()=>{try{let{position:e,selectedText:t}=u(),n=t||g(e.line);if(!n)return;await navigator.clipboard.writeText(n)}catch(e){console.error(e),alert(`Faild to copy: ${JSON.stringify(e)}`)}}},{type:"cut",onClick:async()=>{try{let e=h.hasSelection(),t=h.getRange().start.line,n=e?h.getSelectedText():g(t);if(!n)return;await navigator.clipboard.writeText(n),e||h.setRange({start:{line:t,char:0},end:{line:t,char:n.length}}),i.focus(),o("Delete")}catch(e){console.error(e),alert(`Faild to cut: ${JSON.stringify(e)}`)}}},{type:"undo",onClick:()=>k()},{type:"redo",onClick:()=>H()},{type:"i-cursor",onClick:()=>{i.getVisible()?i.hide():(i.focus(),i.showEditPopupMenu())}}];if(/mobile/i.test(navigator.userAgent))for(let{type:e,onClick:t}of W){let{render:n,dispose:r}=w();n([{type:e}],t)}

ソース
縦画面だと画面外にはみ出してしまうのでペーストボタンを削除する
ペーストボタンはGboardにあるので必要ない
script.ts
import { Icon, useStatusBar } from "./statusBar.ts"; import { caret, downBlocks, downLines, getText, indentBlocks, indentLines, insertText, outdentBlocks, outdentLines, press, redo, takeCursor, takeSelection, undo, upBlocks, upLines, } from "../../takker/scrapbox-userscript-std/dom.ts"; const selection = takeSelection(); const cursor = takeCursor(); const data: { type: Icon; onClick: () => void }[] = [ { type: "caret-left", onClick: () => {cursor.focus();caret().selectedText === "" ? outdentBlocks() : outdentLines()}, }, { type: "caret-right", onClick: () => {cursor.focus();caret().selectedText === "" ? indentBlocks() : indentLines()}, }, { type: "caret-up", onClick: () => {cursor.focus();caret().selectedText === "" ? upBlocks() : upLines()}, }, { type: "caret-down", onClick: () => {cursor.focus();caret().selectedText === "" ? downBlocks() : downLines()}, }, { type: "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)}`); } }, }, { type: "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)}`); } }, }, { type: "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)}`); } }, }, { type: "undo", onClick: () => undo(), }, { type: "redo", onClick: () => redo(), }, { type: "i-cursor", onClick: () => { if (cursor.getVisible()) { cursor.hide(); } else { cursor.focus(); cursor.showEditPopupMenu(); } }, }, ]; if (/mobile/i.test(navigator.userAgent)) { for (const { type, onClick } of data) { const { render, dispose } = useStatusBar(); render([{ type }], onClick); } }

statusBar.ts
/// <reference lib="esnext" /> /// <reference lib="dom" /> const makeLeftStatusBar = (): HTMLDivElement => { const style = document.createElement("style"); style.textContent = `.status-bar.left { left: 0; right: unset; } .status-bar > div:first-of-type { border-top-left-radius: unset; } .status-bar > div:last-of-type { border-top-right-radius: 3px; }`; document.head.append(style); const statusBar = document.createElement("div"); statusBar.classList.add("status-bar", "left"); const app = document.getElementsByClassName("app")[0]!; app.append(statusBar); return statusBar; }; const bar = makeLeftStatusBar(); export interface UseStatusBarResult { /** 取得した.status-barの領域に情報を表示する */ render: (items: Item[], onClick?: (e: TouchEvent) => void) => void; /** 取得した.statusb-barの領域を削除する */ dispose: () => void; } /** .status-barの一区画を取得し、各種操作函数を返す */ export const useStatusBar = (): UseStatusBarResult => { const status = document.createElement("div"); bar.append(status); let listener: ((e: TouchEvent) => void) | undefined; return { render: (items, onClick) => { status.textContent = ""; if (listener) status.removeEventListener("touchstart", listener); listener = onClick; const child = makeGroup(...items); if (child) { if (listener) status.addEventListener("touchstart", listener); status.append(child); } }, dispose: () => status.remove(), }; }; export interface ItemGroup { type: "group"; items: Item[]; } export type Icon = | "spinner" | "check-circle" | "exclamation-triangle" | `caret-${"up" | "down" | "left" | "right"}` | "copy" | "cut" | "clipboard" | "expand" | "i-cursor" | "undo" | "redo"; export type Item = | { type: Icon; } | { type: "text"; text: string } | ItemGroup; const makeGroup = (...items: Item[]): HTMLSpanElement | undefined => { const nodes = items.flatMap((item) => { switch (item.type) { case "spinner": return [makeIcon("fa", "fa-spinner")]; case "check-circle": return [makeIcon("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 [makeIcon("fas", `fa-${item.type}`)]; case "copy": case "clipboard": return [makeIcon("far", `fa-${item.type}`)]; case "text": return [makeItem(item.text)]; case "group": { const group = makeGroup(...item.items); return group ? [group] : []; } } }); if (nodes.length === 0) return; if (nodes.length === 1) return nodes[0]; const span = document.createElement("span"); span.classList.add("item-group"); span.append(...nodes); return span; }; 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("i"); i.classList.add(...classNames); return makeItem(i); };