@rathkit/rte-ui 0.1.0-alpha.0 → 0.1.0-alpha.4
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 +1 -1
- package/css/base.css +9 -1
- package/dist/element-define.d.ts +1 -0
- package/dist/element-define.js +2 -0
- package/dist/element.d.ts +23 -0
- package/dist/element.js +62 -0
- package/dist/floating.d.ts +24 -0
- package/dist/floating.js +286 -0
- package/dist/messages.js +2 -0
- package/dist/types.d.ts +13 -2
- package/dist/ui.js +12 -0
- package/package.json +20 -6
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **Alpha** (0.1.0-alpha.0). Not for production: the APIs will change between releases. Published to the `next` tag only.
|
|
4
4
|
|
|
5
|
-
The styled interface: a toolbar, menus, dialogs, find bar, status line and tooltips on the headless layer, themeable by preset, tokens, icons, classes and messages.
|
|
5
|
+
The styled interface: a toolbar, menus, dialogs, find bar, status line and tooltips on the headless layer, with an optional selection toolbar and context menu, themeable by preset, tokens, icons, classes and messages.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
package/css/base.css
CHANGED
|
@@ -107,6 +107,14 @@
|
|
|
107
107
|
[data-rk-part="menu-keys"] { color: var(--rk-fg-muted); font-size: 0.85em; }
|
|
108
108
|
[data-rk-part="menu-separator"] { height: var(--rk-border-width); margin: var(--rk-space-1) var(--rk-space-2); background: var(--rk-border-subtle); }
|
|
109
109
|
|
|
110
|
+
/* ---- the selection toolbar: a short row of controls over selected text ---- */
|
|
111
|
+
[data-rk-part="floating"] {
|
|
112
|
+
position: absolute; z-index: 15; display: flex; align-items: center; gap: var(--rk-space-1); padding: var(--rk-space-1);
|
|
113
|
+
background: var(--rk-surface-raised); border: var(--rk-border-width) solid var(--rk-border); border-radius: var(--rk-radius-m); box-shadow: var(--rk-shadow-popover);
|
|
114
|
+
}
|
|
115
|
+
[data-rk-part="floating"][hidden] { display: none; }
|
|
116
|
+
[data-rk-part="floating"] [data-rk-part="separator"] { align-self: stretch; margin-block: var(--rk-space-1); }
|
|
117
|
+
|
|
110
118
|
/* ---- tooltip: the control's name and key, shown near it on hover and keyboard focus ---- */
|
|
111
119
|
[data-rk-part="tooltip"] {
|
|
112
120
|
position: absolute; z-index: 20; max-width: 20rem; padding: var(--rk-space-1) var(--rk-space-2); font-size: 0.85em; line-height: 1.35;
|
|
@@ -252,7 +260,7 @@
|
|
|
252
260
|
[data-rk-part="content"] img[data-rk-image-selected] { outline-color: Highlight; }
|
|
253
261
|
.rk-root :focus-visible, .rk-root:focus-within { outline-color: Highlight; }
|
|
254
262
|
[data-rk-part="separator"], [data-rk-part="menu-separator"] { background: CanvasText; }
|
|
255
|
-
[data-rk-part="menu"] { border-color: CanvasText; background: Canvas; }
|
|
263
|
+
[data-rk-part="menu"], [data-rk-part="floating"] { border-color: CanvasText; background: Canvas; }
|
|
256
264
|
::highlight(rk-find) { background-color: Mark; color: MarkText; }
|
|
257
265
|
::highlight(rk-find-current) { background-color: Highlight; color: HighlightText; }
|
|
258
266
|
[data-rk-part="find-field"] { border-color: ButtonText; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { StyleProvider, Ui } from "./types.ts";
|
|
2
|
+
export declare class RkUi extends HTMLElement {
|
|
3
|
+
static observedAttributes: string[];
|
|
4
|
+
private current;
|
|
5
|
+
private extra;
|
|
6
|
+
connectedCallback(): void;
|
|
7
|
+
disconnectedCallback(): void;
|
|
8
|
+
attributeChangedCallback(name: string): void;
|
|
9
|
+
/** The interface, once it is built. */
|
|
10
|
+
get ui(): Ui | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Everything about the look that attributes cannot say: icons, messages, tokens, classes. The `theme`, `scheme` and `density`
|
|
13
|
+
* attributes win over the same names here.
|
|
14
|
+
*/
|
|
15
|
+
get styleProvider(): StyleProvider;
|
|
16
|
+
set styleProvider(style: StyleProvider);
|
|
17
|
+
private readonly onReady;
|
|
18
|
+
private look;
|
|
19
|
+
private attach;
|
|
20
|
+
private detach;
|
|
21
|
+
}
|
|
22
|
+
/** Registers <rk-ui>. Safe to call more than once. */
|
|
23
|
+
export declare function define(name?: string): void;
|
package/dist/element.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createStandardUi } from "./standard.js";
|
|
2
|
+
const STYLE_ATTRIBUTES = ["theme", "scheme", "density"];
|
|
3
|
+
export class RkUi extends HTMLElement {
|
|
4
|
+
static observedAttributes = [...STYLE_ATTRIBUTES, "selection-toolbar", "context-menu", "status"];
|
|
5
|
+
current;
|
|
6
|
+
extra = {};
|
|
7
|
+
connectedCallback() {
|
|
8
|
+
this.addEventListener("rk-ready", this.onReady);
|
|
9
|
+
this.attach();
|
|
10
|
+
}
|
|
11
|
+
disconnectedCallback() {
|
|
12
|
+
this.removeEventListener("rk-ready", this.onReady);
|
|
13
|
+
this.detach();
|
|
14
|
+
}
|
|
15
|
+
attributeChangedCallback(name) {
|
|
16
|
+
if (!this.current)
|
|
17
|
+
return;
|
|
18
|
+
if (STYLE_ATTRIBUTES.includes(name))
|
|
19
|
+
this.current.setStyle(this.look());
|
|
20
|
+
else {
|
|
21
|
+
this.detach();
|
|
22
|
+
this.attach();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
get ui() { return this.current; }
|
|
26
|
+
get styleProvider() { return this.extra; }
|
|
27
|
+
set styleProvider(style) {
|
|
28
|
+
this.extra = style;
|
|
29
|
+
this.current?.setStyle(this.look());
|
|
30
|
+
}
|
|
31
|
+
onReady = () => { if (!this.current)
|
|
32
|
+
this.attach(); };
|
|
33
|
+
look() {
|
|
34
|
+
const given = {};
|
|
35
|
+
for (const name of STYLE_ATTRIBUTES) {
|
|
36
|
+
const value = this.getAttribute(name);
|
|
37
|
+
if (value)
|
|
38
|
+
given[name] = value;
|
|
39
|
+
}
|
|
40
|
+
return { ...this.extra, ...given };
|
|
41
|
+
}
|
|
42
|
+
attach() {
|
|
43
|
+
const host = this.querySelector("rk-editor");
|
|
44
|
+
const editor = host?.editor;
|
|
45
|
+
if (!host || !editor || this.current)
|
|
46
|
+
return;
|
|
47
|
+
this.current = createStandardUi(editor, {
|
|
48
|
+
root: host, style: this.look(),
|
|
49
|
+
selectionToolbar: this.hasAttribute("selection-toolbar"),
|
|
50
|
+
contextMenu: this.hasAttribute("context-menu"),
|
|
51
|
+
status: this.getAttribute("status") !== "false",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
detach() {
|
|
55
|
+
this.current?.destroy();
|
|
56
|
+
this.current = undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function define(name = "rk-ui") {
|
|
60
|
+
if (!customElements.get(name))
|
|
61
|
+
customElements.define(name, RkUi);
|
|
62
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { UiItem } from "@rathkit/rte-core";
|
|
2
|
+
import type { UiEditor } from "./types.ts";
|
|
3
|
+
export declare const FLOATING_SLOT = "toolbar.floating";
|
|
4
|
+
export declare const CONTEXT_SLOT = "menu.context";
|
|
5
|
+
type Dialog = Extract<UiItem, {
|
|
6
|
+
kind: "dialog";
|
|
7
|
+
}>;
|
|
8
|
+
export interface FloatingOptions {
|
|
9
|
+
readonly editor: UiEditor;
|
|
10
|
+
readonly root: HTMLElement;
|
|
11
|
+
readonly selectionToolbar: boolean;
|
|
12
|
+
readonly contextMenu: boolean;
|
|
13
|
+
readonly part: <E extends HTMLElement>(el: E, name: string) => E;
|
|
14
|
+
readonly message: (key: string, fallback?: string) => string;
|
|
15
|
+
readonly icon: (name: string | undefined) => string | (() => Node) | undefined;
|
|
16
|
+
readonly openDialog: (item: Dialog, opener: HTMLElement) => void;
|
|
17
|
+
}
|
|
18
|
+
export interface Floating {
|
|
19
|
+
/** Words or icons may have changed: draw again. */
|
|
20
|
+
refresh(): void;
|
|
21
|
+
destroy(): void;
|
|
22
|
+
}
|
|
23
|
+
export declare function createFloating(options: FloatingOptions): Floating;
|
|
24
|
+
export {};
|
package/dist/floating.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { createMenu, createToolbar } from "@rathkit/rte-headless";
|
|
2
|
+
import { setIcon } from "./icon.js";
|
|
3
|
+
export const FLOATING_SLOT = "toolbar.floating";
|
|
4
|
+
export const CONTEXT_SLOT = "menu.context";
|
|
5
|
+
function copyItems(editor, ids) {
|
|
6
|
+
const main = editor.ui.items("toolbar.main");
|
|
7
|
+
const out = [];
|
|
8
|
+
ids.forEach(([id, group], index) => {
|
|
9
|
+
const found = main.find((i) => i.id === id);
|
|
10
|
+
if (found && found.kind !== "menu" && found.kind !== "separator" && found.kind !== "select" && found.kind !== "panel")
|
|
11
|
+
out.push({ ...found, group, order: index });
|
|
12
|
+
});
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
const SELECTION_DEFAULTS = [["bold", "text"], ["italic", "text"], ["underline", "text"], ["strike", "text"], ["code", "text"], ["link", "link"], ["unlink", "link"], ["clear-format", "clear"]];
|
|
16
|
+
const CONTEXT_DEFAULTS = [["undo", "history"], ["redo", "history"], ["bold", "text"], ["italic", "text"], ["underline", "text"], ["strike", "text"], ["code", "text"], ["link", "link"], ["unlink", "link"], ["clear-format", "clear"]];
|
|
17
|
+
let counter = 0;
|
|
18
|
+
export function createFloating(options) {
|
|
19
|
+
const { editor, root, part, message } = options;
|
|
20
|
+
const stops = [];
|
|
21
|
+
const redraws = [];
|
|
22
|
+
const seed = (slot, ids) => {
|
|
23
|
+
if (editor.ui.items(slot).length > 0)
|
|
24
|
+
return;
|
|
25
|
+
const removers = copyItems(editor, ids).map((item) => editor.ui.add(slot, item));
|
|
26
|
+
stops.push(() => removers.forEach((remove) => remove()));
|
|
27
|
+
};
|
|
28
|
+
const returnTo = document.createElement("button");
|
|
29
|
+
returnTo.type = "button";
|
|
30
|
+
returnTo.hidden = true;
|
|
31
|
+
returnTo.focus = () => editor.focus();
|
|
32
|
+
root.appendChild(returnTo);
|
|
33
|
+
stops.push(() => returnTo.remove());
|
|
34
|
+
const iconInto = (el, name) => {
|
|
35
|
+
const holder = part(document.createElement("span"), "icon");
|
|
36
|
+
holder.setAttribute("aria-hidden", "true");
|
|
37
|
+
if (!setIcon(holder, options.icon(name)))
|
|
38
|
+
return false;
|
|
39
|
+
el.appendChild(holder);
|
|
40
|
+
return true;
|
|
41
|
+
};
|
|
42
|
+
if (options.selectionToolbar) {
|
|
43
|
+
seed(FLOATING_SLOT, SELECTION_DEFAULTS);
|
|
44
|
+
const toolbar = createToolbar(editor, { slot: FLOATING_SLOT, label: message("floating.label") });
|
|
45
|
+
const bar = part(document.createElement("div"), "floating");
|
|
46
|
+
bar.hidden = true;
|
|
47
|
+
root.appendChild(bar);
|
|
48
|
+
const draw = () => {
|
|
49
|
+
const controls = [];
|
|
50
|
+
let group;
|
|
51
|
+
for (const item of toolbar.items) {
|
|
52
|
+
if (item.kind !== "button" && item.kind !== "toggle" && item.kind !== "dialog")
|
|
53
|
+
continue;
|
|
54
|
+
const next = item.group ?? "";
|
|
55
|
+
if (group !== undefined && next !== group)
|
|
56
|
+
controls.push(part(document.createElement("span"), "separator"));
|
|
57
|
+
group = next;
|
|
58
|
+
const button = part(document.createElement("button"), "button");
|
|
59
|
+
button.type = "button";
|
|
60
|
+
button.setAttribute("data-rk-item", item.id);
|
|
61
|
+
const state = toolbar.state(item.id);
|
|
62
|
+
const label = message(`item.${item.id}`, state?.label ?? item.id);
|
|
63
|
+
button.setAttribute("aria-label", label);
|
|
64
|
+
button.setAttribute("data-rk-tip", state?.shortcutLabel ? `${label} (${state.shortcutLabel})` : label);
|
|
65
|
+
if (!iconInto(button, state?.icon)) {
|
|
66
|
+
const words = part(document.createElement("span"), "label");
|
|
67
|
+
words.textContent = label;
|
|
68
|
+
button.appendChild(words);
|
|
69
|
+
}
|
|
70
|
+
controls.push(button);
|
|
71
|
+
}
|
|
72
|
+
bar.replaceChildren(...controls);
|
|
73
|
+
unbind?.();
|
|
74
|
+
unbind = toolbar.bind(bar, { onOpen: (item) => { if (item.kind === "dialog")
|
|
75
|
+
options.openDialog(item, returnTo); } });
|
|
76
|
+
};
|
|
77
|
+
let unbind;
|
|
78
|
+
draw();
|
|
79
|
+
const stopItems = toolbar.subscribe(() => {
|
|
80
|
+
const drawn = [...bar.querySelectorAll("[data-rk-item]")].map((el) => el.getAttribute("data-rk-item")).join("\u0000");
|
|
81
|
+
if (drawn !== toolbar.items.filter((i) => i.kind === "button" || i.kind === "toggle" || i.kind === "dialog").map((i) => i.id).join("\u0000"))
|
|
82
|
+
draw();
|
|
83
|
+
});
|
|
84
|
+
let pointerDown = false;
|
|
85
|
+
let touched = false;
|
|
86
|
+
let dismissedFor = "";
|
|
87
|
+
let timer;
|
|
88
|
+
const focused = () => {
|
|
89
|
+
const active = root.getRootNode().activeElement;
|
|
90
|
+
return !!active && (editor.element.contains(active) || bar.contains(active));
|
|
91
|
+
};
|
|
92
|
+
const selectionKey = () => {
|
|
93
|
+
const { anchor, focus } = editor.snapshot.selection;
|
|
94
|
+
return `${anchor.path.join()}:${anchor.offset}-${focus.path.join()}:${focus.offset}`;
|
|
95
|
+
};
|
|
96
|
+
const hasSelection = () => {
|
|
97
|
+
const s = editor.snapshot.selection;
|
|
98
|
+
return s.anchor.offset !== s.focus.offset || s.anchor.path.join() !== s.focus.path.join();
|
|
99
|
+
};
|
|
100
|
+
const place = () => {
|
|
101
|
+
const selection = root.ownerDocument.getSelection();
|
|
102
|
+
const range = selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
|
|
103
|
+
const rect = range?.getBoundingClientRect();
|
|
104
|
+
if (!rect || (rect.width === 0 && rect.height === 0)) {
|
|
105
|
+
bar.hidden = true;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const box = root.getBoundingClientRect();
|
|
109
|
+
const left0 = box.left + root.clientLeft;
|
|
110
|
+
const top0 = box.top + root.clientTop;
|
|
111
|
+
bar.hidden = false;
|
|
112
|
+
const above = rect.top - top0 - bar.offsetHeight - 8;
|
|
113
|
+
const top = above >= 0 ? above : rect.bottom - top0 + 8;
|
|
114
|
+
const left = Math.max(0, Math.min(rect.left + rect.width / 2 - left0 - bar.offsetWidth / 2, root.clientWidth - bar.offsetWidth));
|
|
115
|
+
bar.style.top = `${Math.round(top)}px`;
|
|
116
|
+
bar.style.left = `${Math.round(left)}px`;
|
|
117
|
+
};
|
|
118
|
+
const update = () => {
|
|
119
|
+
timer = undefined;
|
|
120
|
+
const show = hasSelection() && !pointerDown && !touched && dismissedFor !== selectionKey() && focused();
|
|
121
|
+
if (show)
|
|
122
|
+
place();
|
|
123
|
+
else if (!bar.contains(root.getRootNode().activeElement))
|
|
124
|
+
bar.hidden = true;
|
|
125
|
+
};
|
|
126
|
+
const schedule = () => { timer ??= setTimeout(update, 80); };
|
|
127
|
+
stops.push(editor.subscribe(schedule));
|
|
128
|
+
const on = (target, type, handler, capture = false) => {
|
|
129
|
+
target.addEventListener(type, handler, capture);
|
|
130
|
+
stops.push(() => target.removeEventListener(type, handler, capture));
|
|
131
|
+
};
|
|
132
|
+
on(editor.element, "pointerdown", (e) => { pointerDown = true; touched = e.pointerType === "touch"; if (!bar.hidden && !bar.contains(e.target))
|
|
133
|
+
bar.hidden = true; });
|
|
134
|
+
on(root.ownerDocument, "pointerup", () => { if (pointerDown) {
|
|
135
|
+
pointerDown = false;
|
|
136
|
+
schedule();
|
|
137
|
+
} }, true);
|
|
138
|
+
on(root.ownerDocument, "pointercancel", () => { pointerDown = false; }, true);
|
|
139
|
+
on(editor.element, "focusout", (e) => { if (!bar.contains(e.relatedTarget))
|
|
140
|
+
schedule(); });
|
|
141
|
+
on(bar, "focusout", (e) => { if (!editor.element.contains(e.relatedTarget) && !bar.contains(e.relatedTarget))
|
|
142
|
+
schedule(); });
|
|
143
|
+
on(root.ownerDocument.defaultView ?? window, "scroll", () => { if (!bar.hidden)
|
|
144
|
+
place(); }, true);
|
|
145
|
+
on(root.ownerDocument.defaultView ?? window, "resize", () => { if (!bar.hidden)
|
|
146
|
+
place(); });
|
|
147
|
+
on(editor.element, "keydown", (e) => {
|
|
148
|
+
touched = false;
|
|
149
|
+
if (e.key === "F10" && e.altKey && !bar.hidden) {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
bar.querySelector("[data-rk-item][tabindex='0']")?.focus();
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
on(bar, "keydown", (e) => { if (e.key === "Escape") {
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
dismissedFor = selectionKey();
|
|
157
|
+
bar.hidden = true;
|
|
158
|
+
editor.focus();
|
|
159
|
+
} });
|
|
160
|
+
redraws.push(draw);
|
|
161
|
+
stops.push(() => { stopItems(); unbind?.(); toolbar.destroy(); if (timer)
|
|
162
|
+
clearTimeout(timer); bar.remove(); });
|
|
163
|
+
}
|
|
164
|
+
if (options.contextMenu) {
|
|
165
|
+
seed(CONTEXT_SLOT, CONTEXT_DEFAULTS);
|
|
166
|
+
const toolbar = createToolbar(editor, { slot: CONTEXT_SLOT });
|
|
167
|
+
const popup = part(document.createElement("div"), "menu");
|
|
168
|
+
popup.setAttribute("data-rk-context", "");
|
|
169
|
+
const anchor = document.createElement("button");
|
|
170
|
+
anchor.type = "button";
|
|
171
|
+
anchor.hidden = true;
|
|
172
|
+
anchor.setAttribute("aria-label", message("context.label"));
|
|
173
|
+
anchor.id = `rk-context-anchor-${++counter}`;
|
|
174
|
+
anchor.focus = () => editor.focus();
|
|
175
|
+
root.append(anchor, popup);
|
|
176
|
+
const entries = new Map();
|
|
177
|
+
let menu;
|
|
178
|
+
let at = { x: 0, y: 0 };
|
|
179
|
+
const draw = () => {
|
|
180
|
+
menu?.destroy();
|
|
181
|
+
entries.clear();
|
|
182
|
+
const children = [];
|
|
183
|
+
let group;
|
|
184
|
+
for (const item of toolbar.items) {
|
|
185
|
+
if (item.kind !== "button" && item.kind !== "toggle" && item.kind !== "dialog")
|
|
186
|
+
continue;
|
|
187
|
+
const next = item.group ?? "";
|
|
188
|
+
if (group !== undefined && next !== group) {
|
|
189
|
+
const rule = part(document.createElement("div"), "menu-separator");
|
|
190
|
+
rule.setAttribute("role", "separator");
|
|
191
|
+
children.push(rule);
|
|
192
|
+
}
|
|
193
|
+
group = next;
|
|
194
|
+
entries.set(item.id, item);
|
|
195
|
+
const entry = part(document.createElement("button"), "menu-item");
|
|
196
|
+
entry.type = "button";
|
|
197
|
+
entry.setAttribute("data-rk-menuitem", "");
|
|
198
|
+
entry.setAttribute("data-rk-menu-item", item.id);
|
|
199
|
+
if (item.kind === "toggle")
|
|
200
|
+
entry.setAttribute("role", "menuitemcheckbox");
|
|
201
|
+
entry.append(part(document.createElement("span"), "label"), part(document.createElement("span"), "menu-keys"));
|
|
202
|
+
children.push(entry);
|
|
203
|
+
}
|
|
204
|
+
popup.replaceChildren(...children);
|
|
205
|
+
menu = createMenu({
|
|
206
|
+
trigger: anchor, menu: popup,
|
|
207
|
+
onOpen: () => {
|
|
208
|
+
popup.style.position = "fixed";
|
|
209
|
+
popup.style.insetBlockStart = popup.style.insetInlineStart = "auto";
|
|
210
|
+
const w = popup.offsetWidth;
|
|
211
|
+
const h = popup.offsetHeight;
|
|
212
|
+
const view = root.ownerDocument.documentElement;
|
|
213
|
+
popup.style.left = `${Math.max(4, Math.min(at.x, view.clientWidth - w - 4))}px`;
|
|
214
|
+
popup.style.top = `${Math.max(4, Math.min(at.y, view.clientHeight - h - 4))}px`;
|
|
215
|
+
},
|
|
216
|
+
onSelect: (entry) => {
|
|
217
|
+
const id = entry.getAttribute("data-rk-menu-item") ?? "";
|
|
218
|
+
const item = entries.get(id);
|
|
219
|
+
if (item?.kind === "dialog") {
|
|
220
|
+
options.openDialog(item, returnTo);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
toolbar.activate(id);
|
|
224
|
+
editor.focus();
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
words();
|
|
228
|
+
};
|
|
229
|
+
const words = () => {
|
|
230
|
+
anchor.setAttribute("aria-label", message("context.label"));
|
|
231
|
+
for (const entry of popup.querySelectorAll("[data-rk-menu-item]")) {
|
|
232
|
+
const id = entry.getAttribute("data-rk-menu-item") ?? "";
|
|
233
|
+
const item = entries.get(id);
|
|
234
|
+
const state = toolbar.state(id);
|
|
235
|
+
if (!item || !state)
|
|
236
|
+
continue;
|
|
237
|
+
const label = message(`item.${id}`, state.label);
|
|
238
|
+
const name = entry.querySelector('[data-rk-part="label"]');
|
|
239
|
+
if (name)
|
|
240
|
+
name.textContent = label;
|
|
241
|
+
const keys = entry.querySelector('[data-rk-part="menu-keys"]');
|
|
242
|
+
if (keys)
|
|
243
|
+
keys.textContent = state.shortcutLabel ?? "";
|
|
244
|
+
entry.setAttribute("aria-disabled", String(state.disabled));
|
|
245
|
+
if (item.kind === "toggle")
|
|
246
|
+
entry.setAttribute("aria-checked", String(state.pressed === true));
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
draw();
|
|
250
|
+
const stopState = toolbar.subscribe(() => {
|
|
251
|
+
const drawn = [...popup.querySelectorAll("[data-rk-menu-item]")].map((el) => el.getAttribute("data-rk-menu-item")).join("\u0000");
|
|
252
|
+
if (drawn !== toolbar.items.filter((i) => i.kind === "button" || i.kind === "toggle" || i.kind === "dialog").map((i) => i.id).join("\u0000"))
|
|
253
|
+
draw();
|
|
254
|
+
else
|
|
255
|
+
words();
|
|
256
|
+
});
|
|
257
|
+
const onContextMenu = (e) => {
|
|
258
|
+
if ((e.shiftKey && e.button === 2) || e.pointerType === "touch")
|
|
259
|
+
return;
|
|
260
|
+
if (toolbar.items.length === 0)
|
|
261
|
+
return;
|
|
262
|
+
e.preventDefault();
|
|
263
|
+
let x = e.clientX;
|
|
264
|
+
let y = e.clientY;
|
|
265
|
+
if (x === 0 && y === 0) {
|
|
266
|
+
const selection = root.ownerDocument.getSelection();
|
|
267
|
+
const rect = selection && selection.rangeCount > 0 ? selection.getRangeAt(0).getBoundingClientRect() : editor.element.getBoundingClientRect();
|
|
268
|
+
x = rect.left;
|
|
269
|
+
y = rect.bottom;
|
|
270
|
+
}
|
|
271
|
+
at = { x, y };
|
|
272
|
+
words();
|
|
273
|
+
menu?.close(false);
|
|
274
|
+
menu?.open();
|
|
275
|
+
};
|
|
276
|
+
editor.element.addEventListener("contextmenu", onContextMenu);
|
|
277
|
+
redraws.push(draw);
|
|
278
|
+
stops.push(() => { editor.element.removeEventListener("contextmenu", onContextMenu); stopState(); menu?.destroy(); toolbar.destroy(); anchor.remove(); popup.remove(); });
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
refresh: () => { for (const redraw of redraws)
|
|
282
|
+
redraw(); },
|
|
283
|
+
destroy: () => { for (const stop of stops.splice(0).reverse())
|
|
284
|
+
stop(); },
|
|
285
|
+
};
|
|
286
|
+
}
|
package/dist/messages.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import type { Toolbar, ItemState, HeadlessEditor } from "@rathkit/rte-headless";
|
|
|
2
2
|
import type { Counts, UiItem } from "@rathkit/rte-core";
|
|
3
3
|
import type { TokenName } from "@rathkit/rte-themes";
|
|
4
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";
|
|
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" | "floating" | "findbar" | "find-line" | "find-row" | "find-label" | "find-field" | "find-count" | "dialog" | "dialog-title" | "field-label" | "field-check" | "field" | "field-error" | "actions" | "action";
|
|
6
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"
|
|
7
|
+
export type MessageKey = "toolbar.label" | "floating.label" | "context.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
8
|
/** Rename any item: `item.bold`, `item.link`, and so on. */
|
|
9
9
|
| `item.${string}`;
|
|
10
10
|
/** What a replacement part gets: the same headless state the default part uses, and a way to run the item. */
|
|
@@ -59,6 +59,17 @@ export interface UiOptions {
|
|
|
59
59
|
readonly status?: boolean;
|
|
60
60
|
/** Which set of interface items to draw. Default: the main toolbar. */
|
|
61
61
|
readonly slot?: string;
|
|
62
|
+
/**
|
|
63
|
+
* A short row of formatting controls over selected text (the items of the `toolbar.floating` slot; when the slot is empty it gets the
|
|
64
|
+
* common formatting controls). Default: off. It stays out of the way of touch selection, and Alt+F10 moves the focus into it.
|
|
65
|
+
*/
|
|
66
|
+
readonly selectionToolbar?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* A menu of commands at the pointer on a right click (the items of the `menu.context` slot; when empty it gets undo, redo and the common
|
|
69
|
+
* formatting controls). Default: off. It replaces the browser's menu, spelling suggestions included, so Shift + right click still opens the
|
|
70
|
+
* browser's own, and a touch long-press is left to the browser.
|
|
71
|
+
*/
|
|
72
|
+
readonly contextMenu?: boolean;
|
|
62
73
|
/**
|
|
63
74
|
* Style that every `setStyle` is layered on top of: icons and messages given here stay unless the new style gives the same name.
|
|
64
75
|
* This is how the standard interface keeps its default icons when the theme is switched.
|
package/dist/ui.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { checkFields, createAnnouncer, createDialog, createMenu, createToolbar, createTooltip } from "@rathkit/rte-headless";
|
|
2
2
|
import { createFindBar } from "./findbar.js";
|
|
3
|
+
import { createFloating } from "./floating.js";
|
|
3
4
|
import { setIcon } from "./icon.js";
|
|
4
5
|
import { createShortcuts } from "./shortcuts.js";
|
|
5
6
|
import { createSourceView } from "./source.js";
|
|
@@ -107,6 +108,7 @@ export function createUi(editor, options) {
|
|
|
107
108
|
draw();
|
|
108
109
|
findBar?.refresh();
|
|
109
110
|
shortcuts.refresh();
|
|
111
|
+
floating?.refresh();
|
|
110
112
|
}
|
|
111
113
|
syncDialogs();
|
|
112
114
|
}
|
|
@@ -114,6 +116,7 @@ export function createUi(editor, options) {
|
|
|
114
116
|
return name === undefined ? undefined : style.icons?.[name];
|
|
115
117
|
}
|
|
116
118
|
let unbind;
|
|
119
|
+
let floating;
|
|
117
120
|
function labelOf(item) {
|
|
118
121
|
return message(`item.${item.id}`, toolbar.state(item.id)?.label ?? item.id);
|
|
119
122
|
}
|
|
@@ -442,6 +445,13 @@ export function createUi(editor, options) {
|
|
|
442
445
|
}
|
|
443
446
|
drawn = toolbar.items.map((i) => i.id).join("\u0000");
|
|
444
447
|
applyStyle(options.style ?? {});
|
|
448
|
+
floating = options.selectionToolbar || options.contextMenu
|
|
449
|
+
? createFloating({
|
|
450
|
+
editor, root, selectionToolbar: options.selectionToolbar === true, contextMenu: options.contextMenu === true,
|
|
451
|
+
part: (el, name) => part(el, name), message: (key, fallback) => message(key, fallback), icon: (name) => iconFor(name),
|
|
452
|
+
openDialog,
|
|
453
|
+
})
|
|
454
|
+
: undefined;
|
|
445
455
|
if (stopStatus)
|
|
446
456
|
showStatus();
|
|
447
457
|
return {
|
|
@@ -456,6 +466,7 @@ export function createUi(editor, options) {
|
|
|
456
466
|
tipEl.remove();
|
|
457
467
|
stopStatus?.();
|
|
458
468
|
findBar?.destroy();
|
|
469
|
+
floating?.destroy();
|
|
459
470
|
sourceView?.destroy();
|
|
460
471
|
removeSourceItem?.();
|
|
461
472
|
shortcuts.destroy();
|
|
@@ -472,6 +483,7 @@ export function createUi(editor, options) {
|
|
|
472
483
|
announcer.destroy();
|
|
473
484
|
toolbar.destroy();
|
|
474
485
|
toolbarEl.remove();
|
|
486
|
+
contentEl.replaceWith(...contentEl.childNodes);
|
|
475
487
|
root.classList.remove("rk-root");
|
|
476
488
|
for (const name of ["data-rk-theme", "data-rk-scheme", "data-rk-density", "data-rk-part"])
|
|
477
489
|
root.removeAttribute(name);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rathkit/rte-ui",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.4",
|
|
4
4
|
"description": "The styled editor interface: a toolbar and dialogs on the headless layer, themeable with tokens, presets, icons and messages.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rich-text",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
},
|
|
25
25
|
"type": "module",
|
|
26
26
|
"sideEffects": [
|
|
27
|
-
"*.css"
|
|
27
|
+
"*.css",
|
|
28
|
+
"./dist/element-define.js"
|
|
28
29
|
],
|
|
29
30
|
"engines": {
|
|
30
31
|
"node": ">=22.14.0"
|
|
@@ -49,13 +50,21 @@
|
|
|
49
50
|
"types": "./dist/standard.d.ts",
|
|
50
51
|
"default": "./dist/standard.js"
|
|
51
52
|
},
|
|
53
|
+
"./element": {
|
|
54
|
+
"types": "./dist/element.d.ts",
|
|
55
|
+
"default": "./dist/element.js"
|
|
56
|
+
},
|
|
57
|
+
"./element/define": {
|
|
58
|
+
"types": "./dist/element-define.d.ts",
|
|
59
|
+
"default": "./dist/element-define.js"
|
|
60
|
+
},
|
|
52
61
|
"./base.css": "./css/base.css"
|
|
53
62
|
},
|
|
54
63
|
"peerDependencies": {
|
|
55
|
-
"@rathkit/rte-core": ">=0.1.0-alpha.
|
|
56
|
-
"@rathkit/rte-headless": ">=0.1.0-alpha.
|
|
57
|
-
"@rathkit/rte-themes": ">=0.1.0-alpha.
|
|
58
|
-
"@rathkit/rte-icons": ">=0.1.0-alpha.
|
|
64
|
+
"@rathkit/rte-core": ">=0.1.0-alpha.4 <0.2.0",
|
|
65
|
+
"@rathkit/rte-headless": ">=0.1.0-alpha.4 <0.2.0",
|
|
66
|
+
"@rathkit/rte-themes": ">=0.1.0-alpha.4 <0.2.0",
|
|
67
|
+
"@rathkit/rte-icons": ">=0.1.0-alpha.4 <0.2.0"
|
|
59
68
|
},
|
|
60
69
|
"peerDependenciesMeta": {
|
|
61
70
|
"@rathkit/rte-core": {
|
|
@@ -93,6 +102,11 @@
|
|
|
93
102
|
"name": "ui with default icons",
|
|
94
103
|
"entry": "dist/standard.js",
|
|
95
104
|
"gzipKB": 30
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"name": "ui element",
|
|
108
|
+
"entry": "dist/element.js",
|
|
109
|
+
"gzipKB": 30
|
|
96
110
|
}
|
|
97
111
|
]
|
|
98
112
|
}
|