@rathkit/rte-ui 0.0.0-stage → 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +38 -2
- package/css/base.css +264 -0
- package/dist/findbar.d.ts +24 -0
- package/dist/findbar.js +187 -0
- package/dist/icon.d.ts +2 -0
- package/dist/icon.js +25 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/messages.d.ts +3 -0
- package/dist/messages.js +34 -0
- package/dist/shortcuts.d.ts +14 -0
- package/dist/shortcuts.js +68 -0
- package/dist/source.d.ts +15 -0
- package/dist/source.js +67 -0
- package/dist/standard.d.ts +5 -0
- package/dist/standard.js +6 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.js +1 -0
- package/dist/ui.d.ts +2 -0
- package/dist/ui.js +482 -0
- package/package.json +97 -4
package/dist/findbar.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { setIcon } from "./icon.js";
|
|
2
|
+
const registry = () => globalThis.CSS?.highlights;
|
|
3
|
+
const HighlightCtor = () => globalThis.Highlight;
|
|
4
|
+
const MOST_PAINTED = 2000;
|
|
5
|
+
export function createFindBar(options) {
|
|
6
|
+
const { editor, part, message } = options;
|
|
7
|
+
let counter = 0;
|
|
8
|
+
const bar = part(document.createElement("div"), "findbar");
|
|
9
|
+
bar.setAttribute("role", "search");
|
|
10
|
+
bar.hidden = true;
|
|
11
|
+
const field = (label, name) => {
|
|
12
|
+
const id = `rk-find-${++counter}`;
|
|
13
|
+
const wrap = part(document.createElement("div"), "find-row");
|
|
14
|
+
const text = part(document.createElement("label"), "find-label");
|
|
15
|
+
text.htmlFor = id;
|
|
16
|
+
text.textContent = label;
|
|
17
|
+
const input = part(document.createElement("input"), "find-field");
|
|
18
|
+
input.id = id;
|
|
19
|
+
input.type = "text";
|
|
20
|
+
input.name = name;
|
|
21
|
+
input.autocomplete = "off";
|
|
22
|
+
input.spellcheck = false;
|
|
23
|
+
wrap.append(text, input);
|
|
24
|
+
return { wrap, input, text };
|
|
25
|
+
};
|
|
26
|
+
const iconButton = (name, label) => {
|
|
27
|
+
const b = part(document.createElement("button"), "button");
|
|
28
|
+
b.type = "button";
|
|
29
|
+
const i = part(document.createElement("span"), "icon");
|
|
30
|
+
i.setAttribute("aria-hidden", "true");
|
|
31
|
+
if (setIcon(i, options.icon(name)))
|
|
32
|
+
b.appendChild(i);
|
|
33
|
+
else
|
|
34
|
+
b.textContent = label;
|
|
35
|
+
b.setAttribute("aria-label", label);
|
|
36
|
+
b.setAttribute("data-rk-tip", label);
|
|
37
|
+
return b;
|
|
38
|
+
};
|
|
39
|
+
const textButton = (label) => {
|
|
40
|
+
const b = part(document.createElement("button"), "action");
|
|
41
|
+
b.type = "button";
|
|
42
|
+
b.textContent = label;
|
|
43
|
+
return b;
|
|
44
|
+
};
|
|
45
|
+
const find = field("", "find");
|
|
46
|
+
const replace = field("", "replace");
|
|
47
|
+
const count = part(document.createElement("span"), "find-count");
|
|
48
|
+
const previous = iconButton("chevron-up", "");
|
|
49
|
+
const next = iconButton("chevron-down", "");
|
|
50
|
+
const matchCase = part(document.createElement("button"), "button");
|
|
51
|
+
matchCase.type = "button";
|
|
52
|
+
matchCase.textContent = "Aa";
|
|
53
|
+
const wholeWord = part(document.createElement("button"), "button");
|
|
54
|
+
wholeWord.type = "button";
|
|
55
|
+
wholeWord.textContent = "ab";
|
|
56
|
+
const replaceOne = textButton("");
|
|
57
|
+
const replaceAll = textButton("");
|
|
58
|
+
const close = iconButton("close", "");
|
|
59
|
+
const first = part(document.createElement("div"), "find-line");
|
|
60
|
+
first.append(find.wrap, previous, next, matchCase, wholeWord, count, close);
|
|
61
|
+
const second = part(document.createElement("div"), "find-line");
|
|
62
|
+
second.append(replace.wrap, replaceOne, replaceAll);
|
|
63
|
+
bar.append(first, second);
|
|
64
|
+
const words = () => {
|
|
65
|
+
find.text.textContent = message("find.find");
|
|
66
|
+
replace.text.textContent = message("find.replaceWith");
|
|
67
|
+
for (const [el, key] of [[previous, "find.previous"], [next, "find.next"], [close, "find.close"], [matchCase, "find.matchCase"], [wholeWord, "find.wholeWord"]]) {
|
|
68
|
+
el.setAttribute("aria-label", message(key));
|
|
69
|
+
el.setAttribute("data-rk-tip", message(key));
|
|
70
|
+
}
|
|
71
|
+
replaceOne.textContent = message("find.replace");
|
|
72
|
+
replaceAll.textContent = message("find.replaceAll");
|
|
73
|
+
};
|
|
74
|
+
words();
|
|
75
|
+
const info = () => editor.commands.state("find.set").value;
|
|
76
|
+
let lastOpens = -1;
|
|
77
|
+
let lastCount = "";
|
|
78
|
+
let announceTimer;
|
|
79
|
+
const paint = (state) => {
|
|
80
|
+
const highlights = registry();
|
|
81
|
+
const Highlight = HighlightCtor();
|
|
82
|
+
if (!highlights || !Highlight || !editor.rangeFor)
|
|
83
|
+
return;
|
|
84
|
+
if (!state.open || state.matches.length === 0) {
|
|
85
|
+
highlights.delete("rk-find");
|
|
86
|
+
highlights.delete("rk-find-current");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const ranges = [];
|
|
90
|
+
let current;
|
|
91
|
+
state.matches.slice(0, MOST_PAINTED).forEach((m, i) => {
|
|
92
|
+
const range = editor.rangeFor?.({ path: [...m.path], offset: m.from }, { path: [...m.path], offset: m.to });
|
|
93
|
+
if (!range)
|
|
94
|
+
return;
|
|
95
|
+
if (i === state.index)
|
|
96
|
+
current = range;
|
|
97
|
+
else
|
|
98
|
+
ranges.push(range);
|
|
99
|
+
});
|
|
100
|
+
highlights.set("rk-find", new Highlight(...ranges));
|
|
101
|
+
if (current)
|
|
102
|
+
highlights.set("rk-find-current", new Highlight(current));
|
|
103
|
+
else
|
|
104
|
+
highlights.delete("rk-find-current");
|
|
105
|
+
};
|
|
106
|
+
const render = () => {
|
|
107
|
+
const state = info();
|
|
108
|
+
bar.hidden = !state.open;
|
|
109
|
+
if (!state.open) {
|
|
110
|
+
paint(state);
|
|
111
|
+
lastOpens = state.opens;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (state.query !== find.input.value)
|
|
115
|
+
find.input.value = state.query;
|
|
116
|
+
matchCase.setAttribute("aria-pressed", String(state.matchCase));
|
|
117
|
+
wholeWord.setAttribute("aria-pressed", String(state.wholeWord));
|
|
118
|
+
const text = state.query === "" ? "" : state.matches.length === 0 ? message("find.none")
|
|
119
|
+
: state.index >= 0 ? message("find.count", "", { current: String(state.index + 1), total: String(state.matches.length) })
|
|
120
|
+
: message("find.total", "", { total: String(state.matches.length) });
|
|
121
|
+
count.textContent = text;
|
|
122
|
+
if (text !== lastCount) {
|
|
123
|
+
lastCount = text;
|
|
124
|
+
if (announceTimer)
|
|
125
|
+
clearTimeout(announceTimer);
|
|
126
|
+
if (text !== "")
|
|
127
|
+
announceTimer = setTimeout(() => options.announce(text), 500);
|
|
128
|
+
}
|
|
129
|
+
for (const b of [previous, next, replaceOne, replaceAll])
|
|
130
|
+
b.setAttribute("aria-disabled", String(state.matches.length === 0));
|
|
131
|
+
paint(state);
|
|
132
|
+
if (state.opens !== lastOpens) {
|
|
133
|
+
lastOpens = state.opens;
|
|
134
|
+
find.input.focus();
|
|
135
|
+
find.input.select();
|
|
136
|
+
}
|
|
137
|
+
if (state.index >= 0 && editor.rangeFor) {
|
|
138
|
+
const m = state.matches[state.index];
|
|
139
|
+
const at = m && editor.rangeFor({ path: [...m.path], offset: m.from }, { path: [...m.path], offset: m.to });
|
|
140
|
+
(at?.startContainer instanceof Element ? at.startContainer : at?.startContainer.parentElement)?.scrollIntoView?.({ block: "nearest", inline: "nearest" });
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const run = (name, arg) => { editor.commands.run(name, arg); };
|
|
144
|
+
const onFind = () => run("find.set", { query: find.input.value });
|
|
145
|
+
find.input.addEventListener("input", onFind);
|
|
146
|
+
find.input.addEventListener("keydown", (e) => {
|
|
147
|
+
if (e.key === "Enter") {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
run(e.shiftKey ? "find.previous" : "find.next");
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
replace.input.addEventListener("keydown", (e) => {
|
|
153
|
+
if (e.key === "Enter") {
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
run("find.replace", { with: replace.input.value });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
bar.addEventListener("keydown", (e) => {
|
|
159
|
+
if (e.key === "Escape") {
|
|
160
|
+
e.preventDefault();
|
|
161
|
+
e.stopPropagation();
|
|
162
|
+
run("find.close");
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
previous.addEventListener("click", () => run("find.previous"));
|
|
166
|
+
next.addEventListener("click", () => run("find.next"));
|
|
167
|
+
matchCase.addEventListener("click", () => run("find.set", { matchCase: matchCase.getAttribute("aria-pressed") !== "true" }));
|
|
168
|
+
wholeWord.addEventListener("click", () => run("find.set", { wholeWord: wholeWord.getAttribute("aria-pressed") !== "true" }));
|
|
169
|
+
replaceOne.addEventListener("click", () => run("find.replace", { with: replace.input.value }));
|
|
170
|
+
replaceAll.addEventListener("click", () => run("find.replaceAll", { with: replace.input.value }));
|
|
171
|
+
close.addEventListener("click", () => run("find.close"));
|
|
172
|
+
const stop = editor.commands.subscribe("find.set", render);
|
|
173
|
+
render();
|
|
174
|
+
return {
|
|
175
|
+
element: bar,
|
|
176
|
+
refresh() { words(); render(); },
|
|
177
|
+
destroy() {
|
|
178
|
+
stop();
|
|
179
|
+
if (announceTimer)
|
|
180
|
+
clearTimeout(announceTimer);
|
|
181
|
+
const highlights = registry();
|
|
182
|
+
highlights?.delete("rk-find");
|
|
183
|
+
highlights?.delete("rk-find-current");
|
|
184
|
+
bar.remove();
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
package/dist/icon.d.ts
ADDED
package/dist/icon.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const cache = new Map();
|
|
2
|
+
function parse(svg) {
|
|
3
|
+
const hit = cache.get(svg);
|
|
4
|
+
if (hit !== undefined)
|
|
5
|
+
return hit;
|
|
6
|
+
const template = document.createElement("template");
|
|
7
|
+
template.innerHTML = svg;
|
|
8
|
+
const ok = template.content.childElementCount === 1 && template.content.firstElementChild?.localName === "svg" && !template.content.querySelector("script, foreignObject, [onload], [onclick], [onerror]");
|
|
9
|
+
const fragment = ok ? template.content : null;
|
|
10
|
+
cache.set(svg, fragment);
|
|
11
|
+
return fragment;
|
|
12
|
+
}
|
|
13
|
+
export function setIcon(target, source) {
|
|
14
|
+
if (source === undefined) {
|
|
15
|
+
target.replaceChildren();
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const node = typeof source === "function" ? source() : parse(source)?.cloneNode(true);
|
|
19
|
+
if (!node) {
|
|
20
|
+
target.replaceChildren();
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
target.replaceChildren(node);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { MessageKey } from "./types.ts";
|
|
2
|
+
export declare const DEFAULT_MESSAGES: Readonly<Record<string, string>>;
|
|
3
|
+
export declare function text(table: Partial<Record<MessageKey, string>> | undefined, key: MessageKey, fallback?: string, values?: Record<string, string>): string;
|
package/dist/messages.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const DEFAULT_MESSAGES = {
|
|
2
|
+
"toolbar.label": "Formatting",
|
|
3
|
+
"dialog.ok": "OK",
|
|
4
|
+
"dialog.cancel": "Cancel",
|
|
5
|
+
"field.required": "{label} is required.",
|
|
6
|
+
"field.url": "{label} must be a web, mail or phone address.",
|
|
7
|
+
"toggle.on": "{label} on",
|
|
8
|
+
"toggle.off": "{label} off",
|
|
9
|
+
"status.word": "{count} word",
|
|
10
|
+
"status.words": "{count} words",
|
|
11
|
+
"status.characters": "{count} characters",
|
|
12
|
+
"status.selection": "{selected} of {total} words",
|
|
13
|
+
"source.label": "HTML source",
|
|
14
|
+
"shortcuts.title": "Keyboard shortcuts",
|
|
15
|
+
"shortcuts.action": "Action",
|
|
16
|
+
"shortcuts.keys": "Keys",
|
|
17
|
+
"shortcuts.close": "Close",
|
|
18
|
+
"find.find": "Find",
|
|
19
|
+
"find.replaceWith": "Replace with",
|
|
20
|
+
"find.previous": "Previous match",
|
|
21
|
+
"find.next": "Next match",
|
|
22
|
+
"find.close": "Close find and replace",
|
|
23
|
+
"find.matchCase": "Match case",
|
|
24
|
+
"find.wholeWord": "Whole word",
|
|
25
|
+
"find.replace": "Replace",
|
|
26
|
+
"find.replaceAll": "Replace all",
|
|
27
|
+
"find.none": "No matches",
|
|
28
|
+
"find.count": "{current} of {total}",
|
|
29
|
+
"find.total": "{total} matches",
|
|
30
|
+
};
|
|
31
|
+
export function text(table, key, fallback = "", values = {}) {
|
|
32
|
+
const raw = table?.[key] ?? DEFAULT_MESSAGES[key] ?? fallback;
|
|
33
|
+
return raw.replace(/\{(\w+)\}/g, (_, name) => values[name] ?? "");
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { UiEditor } from "./types.ts";
|
|
2
|
+
export interface ShortcutsOptions {
|
|
3
|
+
readonly editor: UiEditor;
|
|
4
|
+
readonly root: HTMLElement;
|
|
5
|
+
readonly part: <E extends HTMLElement>(el: E, name: string) => E;
|
|
6
|
+
readonly message: (key: string) => string;
|
|
7
|
+
}
|
|
8
|
+
export interface Shortcuts {
|
|
9
|
+
open(opener: HTMLElement): void;
|
|
10
|
+
/** Words may have changed: write them again. */
|
|
11
|
+
refresh(): void;
|
|
12
|
+
destroy(): void;
|
|
13
|
+
}
|
|
14
|
+
export declare function createShortcuts(options: ShortcutsOptions): Shortcuts;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createDialog } from "@rathkit/rte-headless";
|
|
2
|
+
let counter = 0;
|
|
3
|
+
export function createShortcuts(options) {
|
|
4
|
+
const { editor, root, message } = options;
|
|
5
|
+
const element = options.part(document.createElement("dialog"), "dialog");
|
|
6
|
+
const titleId = `rk-shortcuts-title-${++counter}`;
|
|
7
|
+
element.setAttribute("aria-labelledby", titleId);
|
|
8
|
+
element.setAttribute("data-rk-shortcuts", "");
|
|
9
|
+
const title = options.part(document.createElement("h2"), "dialog-title");
|
|
10
|
+
title.id = titleId;
|
|
11
|
+
const table = options.part(document.createElement("table"), "shortcuts");
|
|
12
|
+
const actions = options.part(document.createElement("div"), "actions");
|
|
13
|
+
const done = options.part(document.createElement("button"), "action");
|
|
14
|
+
done.type = "button";
|
|
15
|
+
done.setAttribute("data-primary", "");
|
|
16
|
+
actions.appendChild(done);
|
|
17
|
+
element.append(title, table, actions);
|
|
18
|
+
root.appendChild(element);
|
|
19
|
+
const from = { opener: null };
|
|
20
|
+
const dialog = createDialog({ dialog: element, editor, get opener() { return from.opener; } });
|
|
21
|
+
done.addEventListener("click", () => dialog.close("cancel"));
|
|
22
|
+
const words = () => {
|
|
23
|
+
title.textContent = message("shortcuts.title");
|
|
24
|
+
done.textContent = message("shortcuts.close");
|
|
25
|
+
};
|
|
26
|
+
words();
|
|
27
|
+
const fill = () => {
|
|
28
|
+
const labels = new Map(editor.commands.list().map((c) => [c.name, c.label]));
|
|
29
|
+
const head = document.createElement("thead");
|
|
30
|
+
const headRow = document.createElement("tr");
|
|
31
|
+
for (const key of ["shortcuts.action", "shortcuts.keys"]) {
|
|
32
|
+
const th = document.createElement("th");
|
|
33
|
+
th.scope = "col";
|
|
34
|
+
th.textContent = message(key);
|
|
35
|
+
headRow.appendChild(th);
|
|
36
|
+
}
|
|
37
|
+
head.appendChild(headRow);
|
|
38
|
+
const body = document.createElement("tbody");
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
for (const binding of editor.keymap.list()) {
|
|
41
|
+
if (seen.has(binding.command))
|
|
42
|
+
continue;
|
|
43
|
+
seen.add(binding.command);
|
|
44
|
+
const keys = editor.keymap.shortcutFor(binding.command);
|
|
45
|
+
if (!keys)
|
|
46
|
+
continue;
|
|
47
|
+
const row = document.createElement("tr");
|
|
48
|
+
const what = document.createElement("td");
|
|
49
|
+
what.textContent = labels.get(binding.command) ?? binding.command;
|
|
50
|
+
const how = document.createElement("td");
|
|
51
|
+
const kbd = document.createElement("kbd");
|
|
52
|
+
kbd.textContent = keys;
|
|
53
|
+
how.appendChild(kbd);
|
|
54
|
+
row.append(what, how);
|
|
55
|
+
body.appendChild(row);
|
|
56
|
+
}
|
|
57
|
+
table.replaceChildren(head, body);
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
open(opener) {
|
|
61
|
+
from.opener = opener;
|
|
62
|
+
fill();
|
|
63
|
+
dialog.open();
|
|
64
|
+
},
|
|
65
|
+
refresh: words,
|
|
66
|
+
destroy() { dialog.destroy(); element.remove(); },
|
|
67
|
+
};
|
|
68
|
+
}
|
package/dist/source.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { UiEditor } from "./types.ts";
|
|
2
|
+
export interface SourceViewOptions {
|
|
3
|
+
readonly editor: UiEditor;
|
|
4
|
+
readonly content: HTMLElement;
|
|
5
|
+
readonly toolbar: HTMLElement;
|
|
6
|
+
readonly part: <E extends HTMLElement>(el: E, name: string) => E;
|
|
7
|
+
readonly message: (key: string) => string;
|
|
8
|
+
readonly announce: (message: string) => void;
|
|
9
|
+
}
|
|
10
|
+
export interface SourceView {
|
|
11
|
+
toggle(opener: HTMLElement): void;
|
|
12
|
+
readonly isOpen: boolean;
|
|
13
|
+
destroy(): void;
|
|
14
|
+
}
|
|
15
|
+
export declare function createSourceView(options: SourceViewOptions): SourceView;
|
package/dist/source.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export function createSourceView(options) {
|
|
2
|
+
const { editor, content, toolbar } = options;
|
|
3
|
+
const area = options.part(document.createElement("textarea"), "source");
|
|
4
|
+
area.hidden = true;
|
|
5
|
+
area.spellcheck = false;
|
|
6
|
+
area.setAttribute("aria-label", options.message("source.label"));
|
|
7
|
+
area.setAttribute("autocapitalize", "off");
|
|
8
|
+
content.appendChild(area);
|
|
9
|
+
let original = "";
|
|
10
|
+
let opener;
|
|
11
|
+
const button = () => toolbar.querySelector('[data-rk-item="source"]');
|
|
12
|
+
const close = (apply) => {
|
|
13
|
+
if (area.hidden)
|
|
14
|
+
return;
|
|
15
|
+
const changed = area.value !== original;
|
|
16
|
+
area.hidden = true;
|
|
17
|
+
content.removeAttribute("data-rk-source");
|
|
18
|
+
button()?.setAttribute("aria-pressed", "false");
|
|
19
|
+
if (apply && changed)
|
|
20
|
+
editor.commands.run("document.setHtml", { html: area.value });
|
|
21
|
+
editor.focus();
|
|
22
|
+
(opener ?? button())?.removeAttribute("data-rk-busy");
|
|
23
|
+
};
|
|
24
|
+
const open = (from) => {
|
|
25
|
+
opener = from;
|
|
26
|
+
original = editor.getHtml?.("\n") ?? "";
|
|
27
|
+
area.value = original;
|
|
28
|
+
area.hidden = false;
|
|
29
|
+
content.setAttribute("data-rk-source", "");
|
|
30
|
+
button()?.setAttribute("aria-pressed", "true");
|
|
31
|
+
area.focus();
|
|
32
|
+
area.setSelectionRange(0, 0);
|
|
33
|
+
};
|
|
34
|
+
const guard = (e) => {
|
|
35
|
+
if (area.hidden)
|
|
36
|
+
return;
|
|
37
|
+
const control = e.target?.closest("[data-rk-item]");
|
|
38
|
+
if (!control || control.getAttribute("data-rk-item") === "source")
|
|
39
|
+
return;
|
|
40
|
+
if (e.type === "keydown" && !["Enter", " "].includes(e.key))
|
|
41
|
+
return;
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
e.stopImmediatePropagation();
|
|
44
|
+
};
|
|
45
|
+
toolbar.addEventListener("click", guard, true);
|
|
46
|
+
toolbar.addEventListener("keydown", guard, true);
|
|
47
|
+
area.addEventListener("keydown", (e) => {
|
|
48
|
+
if (e.key === "Escape") {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
e.stopPropagation();
|
|
51
|
+
close(false);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
toggle(from) { if (area.hidden)
|
|
56
|
+
open(from);
|
|
57
|
+
else
|
|
58
|
+
close(true); },
|
|
59
|
+
get isOpen() { return !area.hidden; },
|
|
60
|
+
destroy() {
|
|
61
|
+
toolbar.removeEventListener("click", guard, true);
|
|
62
|
+
toolbar.removeEventListener("keydown", guard, true);
|
|
63
|
+
area.remove();
|
|
64
|
+
content.removeAttribute("data-rk-source");
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
package/dist/standard.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { defaultIcons } from "@rathkit/rte-icons";
|
|
2
|
+
import { createUi } from "./ui.js";
|
|
3
|
+
export function createStandardUi(editor, options) {
|
|
4
|
+
return createUi(editor, { ...options, defaults: { ...options.defaults, icons: { ...defaultIcons, ...options.defaults?.icons } } });
|
|
5
|
+
}
|
|
6
|
+
export { createUi };
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Toolbar, ItemState, HeadlessEditor } from "@rathkit/rte-headless";
|
|
2
|
+
import type { Counts, UiItem } from "@rathkit/rte-core";
|
|
3
|
+
import type { TokenName } from "@rathkit/rte-themes";
|
|
4
|
+
/** The parts the interface is made of. Selectors and `classes` address them by these names (as `data-rk-part`). */
|
|
5
|
+
export type PartName = "root" | "toolbar" | "button" | "select" | "separator" | "icon" | "label" | "content" | "menu-host" | "menu" | "menu-item" | "menu-separator" | "menu-keys" | "swatch" | "tooltip" | "status" | "source" | "shortcuts" | "findbar" | "find-line" | "find-row" | "find-label" | "find-field" | "find-count" | "dialog" | "dialog-title" | "field-label" | "field-check" | "field" | "field-error" | "actions" | "action";
|
|
6
|
+
/** Words the interface says. Each has a default in English; this table is also the way to translate it. */
|
|
7
|
+
export type MessageKey = "toolbar.label" | "dialog.ok" | "dialog.cancel" | "field.required" | "field.url" | "toggle.on" | "toggle.off" | "status.word" | "status.words" | "status.characters" | "status.selection" | "source.label" | "shortcuts.title" | "shortcuts.action" | "shortcuts.keys" | "shortcuts.close" | "find.find" | "find.replaceWith" | "find.previous" | "find.next" | "find.close" | "find.matchCase" | "find.wholeWord" | "find.replace" | "find.replaceAll" | "find.none" | "find.count" | "find.total"
|
|
8
|
+
/** Rename any item: `item.bold`, `item.link`, and so on. */
|
|
9
|
+
| `item.${string}`;
|
|
10
|
+
/** What a replacement part gets: the same headless state the default part uses, and a way to run the item. */
|
|
11
|
+
export interface PartContext {
|
|
12
|
+
readonly item: UiItem;
|
|
13
|
+
readonly state: ItemState;
|
|
14
|
+
/** Runs the item's command (a toggle or button) or, for a select, with the chosen value. */
|
|
15
|
+
run(value?: unknown): boolean;
|
|
16
|
+
}
|
|
17
|
+
/** Everything about how the interface looks, as plain data. No code is needed for the first four. */
|
|
18
|
+
export interface StyleProvider {
|
|
19
|
+
/** A preset name from `@rathkit/rte-themes` (`light`, `dark`, `auto`, `high-contrast`). Load that theme's CSS file. */
|
|
20
|
+
readonly theme?: string;
|
|
21
|
+
readonly scheme?: "light" | "dark";
|
|
22
|
+
readonly density?: "compact" | "comfortable";
|
|
23
|
+
/** Token values, applied as custom properties on the root: beats the preset. */
|
|
24
|
+
readonly tokens?: Partial<Record<TokenName, string>>;
|
|
25
|
+
/** SVG markup (trusted: it is yours) or a function that makes a node. Missing names fall back to the defaults, if any. */
|
|
26
|
+
readonly icons?: Readonly<Record<string, string | (() => Node)>>;
|
|
27
|
+
/** Extra classes added to every element of a part, for utility-class systems. */
|
|
28
|
+
readonly classes?: Partial<Record<PartName, string>>;
|
|
29
|
+
readonly messages?: Partial<Record<MessageKey, string>>;
|
|
30
|
+
/** The one code hook, a last resort: replace the element of a part. It must follow the same accessibility rules. */
|
|
31
|
+
readonly parts?: {
|
|
32
|
+
readonly button?: (context: PartContext) => HTMLElement;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** What the interface needs of an editor: the headless needs, and the element that is the editing surface. */
|
|
36
|
+
export type UiEditor = HeadlessEditor & {
|
|
37
|
+
readonly element: HTMLElement;
|
|
38
|
+
/** Words and characters, for the status line. Without it there is no status line. */
|
|
39
|
+
counts?(): {
|
|
40
|
+
readonly document: Counts;
|
|
41
|
+
readonly selection: Counts;
|
|
42
|
+
};
|
|
43
|
+
/** The document as HTML, for the source view. Without it there is no source view. */
|
|
44
|
+
getHtml?(separator?: string): string;
|
|
45
|
+
/** The part of the page between two positions, for painting matches. Without it matches are not painted. */
|
|
46
|
+
rangeFor?(from: {
|
|
47
|
+
path: number[];
|
|
48
|
+
offset: number;
|
|
49
|
+
}, to: {
|
|
50
|
+
path: number[];
|
|
51
|
+
offset: number;
|
|
52
|
+
}): Range | null;
|
|
53
|
+
};
|
|
54
|
+
export interface UiOptions {
|
|
55
|
+
/** The element the interface is built in. It gets the `rk-root` class. */
|
|
56
|
+
readonly root: HTMLElement;
|
|
57
|
+
readonly style?: StyleProvider;
|
|
58
|
+
/** A line under the writing area with the number of words and characters. Default: on, when the editor can count. */
|
|
59
|
+
readonly status?: boolean;
|
|
60
|
+
/** Which set of interface items to draw. Default: the main toolbar. */
|
|
61
|
+
readonly slot?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Style that every `setStyle` is layered on top of: icons and messages given here stay unless the new style gives the same name.
|
|
64
|
+
* This is how the standard interface keeps its default icons when the theme is switched.
|
|
65
|
+
*/
|
|
66
|
+
readonly defaults?: Pick<StyleProvider, "icons" | "messages">;
|
|
67
|
+
}
|
|
68
|
+
export interface Ui {
|
|
69
|
+
readonly root: HTMLElement;
|
|
70
|
+
readonly toolbar: Toolbar;
|
|
71
|
+
/** Changes how the interface looks, now: nothing is rebuilt. */
|
|
72
|
+
setStyle(next: StyleProvider): void;
|
|
73
|
+
/** Says something to assistive technology. */
|
|
74
|
+
announce(message: string): void;
|
|
75
|
+
destroy(): void;
|
|
76
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/ui.d.ts
ADDED