Porterっぽい編集バーを生やすUserScript@0.1.0
実装
型定義とかは飛ばす
さっさと実装したい
20:14:52 結局型定義書いた
事前にある程度エラーを消せるからこっちのほうが楽
作成する機能
文字列選択なしでAlt+矢印キー
文字列選択ありでCtrl+矢印キー
copy
cut
paste
undo & redo
caret付け外し
バグ
トップページやStreamでも常時表示される
scrapbox.Layoutをみて表示非表示を切り替える必要がある
まあ実害はないと思うので、直したくなるまで放置するtakker.icon
右端にborderがない
あとでCSS直す
不満点
使いにくい
ボタンのアイコンを変更できない
ボタンを削除できない
2022-05-03
08:01:35 ボタンが多いときにはスクロールさせるようにした
2022-05-02
21:20:17 終了
https://gyazo.com/e0e4f35b1cb5924c52094a30941c4d23
21:11:33 event listenerの登録先を.item-groupの一つ上のDOM(div)にすることで、仮想キーボードが出たり隠れたりする問題を解決した これでリリースする
20:37:35 cursorが外れているときは編集できない方がいい
画面外で編集が行われてしまう
もしくはfocusを当て直す?
こっちがよさそう
20:42:57 そうした
20:28:53 あ、caret外しも入れておこう
20:28:18 終了
GIF撮ったらおしまい
20:23:29 テスト中
うまく動いているっぽい!
code: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 "../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);
}
}
status barの実装
code:sh
code:statusBar.ts
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
const makeLeftStatusBar = (): HTMLDivElement => {
const style = document.createElement("style");
style.textContent = `.status-bar.left {
left: 0;
right: unset;
max-width: 80vw;
overflow-x: auto;
}
.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: MouseEvent) => void) => void;
/** 取得した.status-barの領域を削除する */
dispose: () => void;
}
/** .status-barの一区画を取得し、各種操作函数を返す */
export const useStatusBar = (): UseStatusBarResult => {
const status = document.createElement("div");
bar.append(status);
let listener: ((e: MouseEvent) => void) | undefined;
return {
render: (items, onClick) => {
status.textContent = "";
if (listener) status.removeEventListener("click", listener);
listener = onClick;
const child = makeGroup(...items);
if (child) {
if (listener) status.addEventListener("click", 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":
case "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":
case "group": {
const group = makeGroup(...item.items);
return group ? group : []; }
}
});
if (nodes.length === 0) return;
if (nodes.length === 1) return nodes0; 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);
};