概要
ちょっとしたメモや下書き用途として、ブラウザで動く簡易スクラッチパッドを作ってみました。
単一のHTMLファイルで動作も軽く、ローカルで利用できます。
周りに共有したところ意外と反応が良かったので、簡単に紹介してみます。
経緯
以前から、ブラウザのアドレスバーに次の文字列を入力して簡易メモとして使っていました。
data:text/html,<html contenteditable>
これだけでブラウザ上で編集可能なページが開くので、どんな環境でも使えるローカルメモとして結構便利です。
ただし、この方法には一つ大きな不満がありました。
Tabキーがインデントではなく要素フォーカス移動になってしまう
ちょっとしたメモを書くときでも、Tabが使えないのは意外とストレスだったので、
- Tab / Shift+Tab のインデント操作
- 自由なキー割り当て(カーソル移動・削除に対して)
- 単一HTMLファイル
- ブラウザだけで動く
という条件で、簡易的なスクラッチパッドを試作してみました。
実装
以下のHTMLファイルで動作します。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ScratchPad</title>
<style>
:root{color-scheme:light dark}
html,body{height:100%;margin:0;overflow:hidden}
#editor{height:100%;padding:12px;box-sizing:border-box;outline:none;font:14px/1.5 monospace;white-space:pre-wrap;tab-size:4}
</style>
</head>
<body>
<div id="editor" contenteditable spellcheck="false"><div><br></div></div>
<script>
(() => {
const e = document.getElementById("editor"), TAB = "\t";
const blk = n => (n && n !== e) ? (n.parentNode === e ? n : blk(n.parentNode)) : null;
e.addEventListener("keydown", ev => {
if (ev.isComposing) return;
const s = getSelection(), r = s.getRangeAt(0), k = ev.key.toLowerCase();
const mod = (ev.ctrlKey && ev.altKey), shift = ev.shiftKey;
// カーソル移動 (Ctrl+Alt+H/J/K/L)
if (mod && "hjkl".includes(k)) {
ev.preventDefault();
s.modify(shift ? "extend" : "move", "hk".includes(k) ? "backward" : "forward", "hl".includes(k) ? "character" : "line");
}
// 削除 (Alt+Shift+D:前 / Ctrl+Alt+D:次)
else if (k === "d" && ev.altKey && (shift || ev.ctrlKey)) {
ev.preventDefault();
s.modify("extend", shift ? "backward" : "forward", "character");
document.execCommand("delete");
}
// Tab / Shift+Tab // 複数行選択にも対応
else if (k === "tab") {
ev.preventDefault();
if (s.isCollapsed) {
if (!shift) document.execCommand("insertText", false, TAB);
else {
const n = r.startContainer, o = r.startOffset;
if (n.nodeType === 3 && o > 0 && n.nodeValue[o-1] === TAB) {
n.nodeValue = n.nodeValue.slice(0, o-1) + n.nodeValue.slice(o);
r.setStart(n, o-1); s.removeAllRanges(); s.addRange(r);
}
}
} else {
let b = [], start = blk(r.startContainer), end = blk(r.endContainer);
for (let c = start; c; c = c.nextElementSibling) { b.push(c); if (c === end) break; }
b.forEach(x => {
const f = x.firstChild, isTxt = f?.nodeType === 3;
if (!shift) isTxt ? f.nodeValue = TAB + f.nodeValue : x.insertBefore(document.createTextNode(TAB), f);
else if (isTxt && f.nodeValue.startsWith(TAB)) {
f.nodeValue = f.nodeValue.slice(1);
if (!f.nodeValue) x.removeChild(f);
}
});
const nr = document.createRange();
nr.setStart(b[0], 0); nr.setEnd(b.at(-1), b.at(-1).childNodes.length);
s.removeAllRanges(); s.addRange(nr);
}
}
});
e.focus();
})();
</script>
</body>
</html>
基本は contenteditable をベースにして、必要なキー操作だけ上書きしています。
現在のキー設定はこんな感じですが、HTMLの記述を変更すれば自由に割り当てを変更できます。
| 操作 | キー |
|---|---|
| カーソル移動 | Ctrl+Alt+H / J / K / L |
| インデント | Tab / Shift+Tab |
| 削除 | Alt+Shift+D / Ctrl+Alt+D |
data URLとして使う
実用を考えて、このHTMLを data URL にして、ブラウザのブックマークから直接起動することにしました。
data URL とは、ファイルの内容をそのままURLとして埋め込む仕組みです。
手順はこんな感じです。
- HTMLファイルの内容をdata URLに変換
- そのURLをブックマーク
- クリックするとスクラッチパッドが開く
変換はオンラインツールでもできますし、ブラウザの開発者ツールのConsoleでも簡単に作れます。
const html = `HTMLの内容コピペ`;
const dataUrl = "data:text/html;charset=utf-8," + encodeURIComponent(html);
console.log(dataUrl);
感想
現在、私はこんな用途で使っています。
・ブラウザの新規タブ1枚でテキストメモ
・書き終わったらタブを閉じるだけ
・プレーンテキスト化したいときの一時置き場
・画像をそのまま貼り付け
・エディタやビューアを起動するほどでもないとき
ブラウザの標準機能も基本そのまま使えます。
・Undo / Redo
・拡大縮小
・ページ内検索
・HTML保存
・スクリーンショット
手元のChromeやEdgeでは問題なく動作していますが、環境によっては動かない可能性があります。
execCommandなど非推奨なAPIの利用は避けるべきだと思います。
また、contenteditable ベースで必要な編集挙動のみを上書きしているため、
- DOM構造に依存した挙動
- 貼り付け時の構造崩れ
などの不安定さは多少残っています。
とはいえ、
「すぐ起動して書き捨てるメモ」
としては結構気に入っています。
まだ改善の余地はあると思うので、今後も試していこうと思います。