@rathkit/rte-ui 0.1.0-alpha.3 → 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 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,2 @@
1
+ import { define } from "./element.js";
2
+ define();
@@ -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;
@@ -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 {};
@@ -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
@@ -1,5 +1,7 @@
1
1
  export const DEFAULT_MESSAGES = {
2
2
  "toolbar.label": "Formatting",
3
+ "floating.label": "Selection formatting",
4
+ "context.label": "Editing commands",
3
5
  "dialog.ok": "OK",
4
6
  "dialog.cancel": "Cancel",
5
7
  "field.required": "{label} is required.",
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",
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.3 <0.2.0",
56
- "@rathkit/rte-headless": ">=0.1.0-alpha.3 <0.2.0",
57
- "@rathkit/rte-themes": ">=0.1.0-alpha.3 <0.2.0",
58
- "@rathkit/rte-icons": ">=0.1.0-alpha.3 <0.2.0"
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
  }