@rathkit/rte-ui 0.0.0 → 0.1.0-alpha.3
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/README.md +32 -4
- 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 +84 -3
|
@@ -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