@docmentis/udoc-editor 0.0.1 → 0.8.1
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-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/README.md +48 -0
- package/dist/arrange.d.ts +37 -0
- package/dist/arrange.d.ts.map +1 -0
- package/dist/arrange.js +431 -0
- package/dist/arrange.js.map +1 -0
- package/dist/clipboard.d.ts +12 -0
- package/dist/clipboard.d.ts.map +1 -0
- package/dist/clipboard.js +101 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/commands.d.ts +12 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +225 -0
- package/dist/commands.js.map +1 -0
- package/dist/components/ComboBox.d.ts +38 -0
- package/dist/components/ComboBox.d.ts.map +1 -0
- package/dist/components/ComboBox.js +57 -0
- package/dist/components/ComboBox.js.map +1 -0
- package/dist/components/ModeSwitch.d.ts +15 -0
- package/dist/components/ModeSwitch.d.ts.map +1 -0
- package/dist/components/ModeSwitch.js +94 -0
- package/dist/components/ModeSwitch.js.map +1 -0
- package/dist/components/NewSlideModal.d.ts +41 -0
- package/dist/components/NewSlideModal.d.ts.map +1 -0
- package/dist/components/NewSlideModal.js +136 -0
- package/dist/components/NewSlideModal.js.map +1 -0
- package/dist/components/PageControls.d.ts +78 -0
- package/dist/components/PageControls.d.ts.map +1 -0
- package/dist/components/PageControls.js +201 -0
- package/dist/components/PageControls.js.map +1 -0
- package/dist/components/PageSettingsModal.d.ts +27 -0
- package/dist/components/PageSettingsModal.d.ts.map +1 -0
- package/dist/components/PageSettingsModal.js +273 -0
- package/dist/components/PageSettingsModal.js.map +1 -0
- package/dist/components/PaintOptionsModal.d.ts +19 -0
- package/dist/components/PaintOptionsModal.d.ts.map +1 -0
- package/dist/components/PaintOptionsModal.js +103 -0
- package/dist/components/PaintOptionsModal.js.map +1 -0
- package/dist/components/TextControls.d.ts +49 -0
- package/dist/components/TextControls.d.ts.map +1 -0
- package/dist/components/TextControls.js +452 -0
- package/dist/components/TextControls.js.map +1 -0
- package/dist/context-menu.d.ts +46 -0
- package/dist/context-menu.d.ts.map +1 -0
- package/dist/context-menu.js +144 -0
- package/dist/context-menu.js.map +1 -0
- package/dist/edit-client.d.ts +121 -0
- package/dist/edit-client.d.ts.map +1 -0
- package/dist/edit-client.js +154 -0
- package/dist/edit-client.js.map +1 -0
- package/dist/edit-command.d.ts +7 -0
- package/dist/edit-command.d.ts.map +1 -0
- package/dist/edit-command.js +18 -0
- package/dist/edit-command.js.map +1 -0
- package/dist/edit-mode.d.ts +19 -0
- package/dist/edit-mode.d.ts.map +1 -0
- package/dist/edit-mode.js +214 -0
- package/dist/edit-mode.js.map +1 -0
- package/dist/editor.d.ts +101 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +101 -0
- package/dist/editor.js.map +1 -0
- package/dist/fonts/faces.d.ts +28 -0
- package/dist/fonts/faces.d.ts.map +1 -0
- package/dist/fonts/faces.js +38 -0
- package/dist/fonts/faces.js.map +1 -0
- package/dist/fonts/preview.d.ts +98 -0
- package/dist/fonts/preview.d.ts.map +1 -0
- package/dist/fonts/preview.js +149 -0
- package/dist/fonts/preview.js.map +1 -0
- package/dist/formatting-marks.d.ts +23 -0
- package/dist/formatting-marks.d.ts.map +1 -0
- package/dist/formatting-marks.js +55 -0
- package/dist/formatting-marks.js.map +1 -0
- package/dist/grips.d.ts +14 -0
- package/dist/grips.d.ts.map +1 -0
- package/dist/grips.js +459 -0
- package/dist/grips.js.map +1 -0
- package/dist/i18n/ar.d.ts +4 -0
- package/dist/i18n/ar.d.ts.map +1 -0
- package/dist/i18n/ar.js +175 -0
- package/dist/i18n/ar.js.map +1 -0
- package/dist/i18n/de.d.ts +4 -0
- package/dist/i18n/de.d.ts.map +1 -0
- package/dist/i18n/de.js +175 -0
- package/dist/i18n/de.js.map +1 -0
- package/dist/i18n/en.d.ts +5 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/en.js +176 -0
- package/dist/i18n/en.js.map +1 -0
- package/dist/i18n/es.d.ts +4 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +175 -0
- package/dist/i18n/es.js.map +1 -0
- package/dist/i18n/fr.d.ts +4 -0
- package/dist/i18n/fr.d.ts.map +1 -0
- package/dist/i18n/fr.js +175 -0
- package/dist/i18n/fr.js.map +1 -0
- package/dist/i18n/index.d.ts +21 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +42 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/ja.d.ts +4 -0
- package/dist/i18n/ja.d.ts.map +1 -0
- package/dist/i18n/ja.js +175 -0
- package/dist/i18n/ja.js.map +1 -0
- package/dist/i18n/ko.d.ts +4 -0
- package/dist/i18n/ko.d.ts.map +1 -0
- package/dist/i18n/ko.js +175 -0
- package/dist/i18n/ko.js.map +1 -0
- package/dist/i18n/messages.d.ts +316 -0
- package/dist/i18n/messages.d.ts.map +1 -0
- package/dist/i18n/messages.js +2 -0
- package/dist/i18n/messages.js.map +1 -0
- package/dist/i18n/pt-BR.d.ts +4 -0
- package/dist/i18n/pt-BR.d.ts.map +1 -0
- package/dist/i18n/pt-BR.js +175 -0
- package/dist/i18n/pt-BR.js.map +1 -0
- package/dist/i18n/ru.d.ts +4 -0
- package/dist/i18n/ru.d.ts.map +1 -0
- package/dist/i18n/ru.js +175 -0
- package/dist/i18n/ru.js.map +1 -0
- package/dist/i18n/zh-CN.d.ts +4 -0
- package/dist/i18n/zh-CN.d.ts.map +1 -0
- package/dist/i18n/zh-CN.js +175 -0
- package/dist/i18n/zh-CN.js.map +1 -0
- package/dist/i18n/zh-TW.d.ts +4 -0
- package/dist/i18n/zh-TW.d.ts.map +1 -0
- package/dist/i18n/zh-TW.js +175 -0
- package/dist/i18n/zh-TW.js.map +1 -0
- package/dist/icons.d.ts +97 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +103 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/insert.d.ts +21 -0
- package/dist/insert.d.ts.map +1 -0
- package/dist/insert.js +129 -0
- package/dist/insert.js.map +1 -0
- package/dist/menu-action.d.ts +73 -0
- package/dist/menu-action.d.ts.map +1 -0
- package/dist/menu-action.js +89 -0
- package/dist/menu-action.js.map +1 -0
- package/dist/mode.d.ts +21 -0
- package/dist/mode.d.ts.map +1 -0
- package/dist/mode.js +17 -0
- package/dist/mode.js.map +1 -0
- package/dist/new-slide.d.ts +21 -0
- package/dist/new-slide.d.ts.map +1 -0
- package/dist/new-slide.js +63 -0
- package/dist/new-slide.js.map +1 -0
- package/dist/page-controls.d.ts +20 -0
- package/dist/page-controls.d.ts.map +1 -0
- package/dist/page-controls.js +99 -0
- package/dist/page-controls.js.map +1 -0
- package/dist/paint.d.ts +52 -0
- package/dist/paint.d.ts.map +1 -0
- package/dist/paint.js +262 -0
- package/dist/paint.js.map +1 -0
- package/dist/ruler.d.ts +10 -0
- package/dist/ruler.d.ts.map +1 -0
- package/dist/ruler.js +97 -0
- package/dist/ruler.js.map +1 -0
- package/dist/state.d.ts +85 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +28 -0
- package/dist/state.js.map +1 -0
- package/dist/styles.d.ts +2 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +758 -0
- package/dist/styles.js.map +1 -0
- package/dist/tabs.d.ts +11 -0
- package/dist/tabs.d.ts.map +1 -0
- package/dist/tabs.js +153 -0
- package/dist/tabs.js.map +1 -0
- package/dist/text-format.d.ts +114 -0
- package/dist/text-format.d.ts.map +1 -0
- package/dist/text-format.js +315 -0
- package/dist/text-format.js.map +1 -0
- package/dist/tools.d.ts +35 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +41 -0
- package/dist/tools.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/dist/worker/handlers.d.ts +11 -0
- package/dist/worker/handlers.d.ts.map +1 -0
- package/dist/worker/handlers.js +127 -0
- package/dist/worker/handlers.js.map +1 -0
- package/dist/worker/protocol.d.ts +190 -0
- package/dist/worker/protocol.d.ts.map +1 -0
- package/dist/worker/protocol.js +2 -0
- package/dist/worker/protocol.js.map +1 -0
- package/dist/worker/worker-source.d.ts +3 -0
- package/dist/worker/worker-source.d.ts.map +1 -0
- package/dist/worker/worker-source.js +11 -0
- package/dist/worker/worker.d.ts +2 -0
- package/dist/worker/worker.d.ts.map +1 -0
- package/dist/worker/worker.js +6 -0
- package/dist/worker/worker.js.map +1 -0
- package/package.json +68 -5
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PaintChannel } from "@docmentis/udoc-ui/internal";
|
|
2
|
+
import type { I18n } from "../i18n/index.js";
|
|
3
|
+
/** What `open` needs — which channel's options to show. */
|
|
4
|
+
export interface PaintOptionsContext {
|
|
5
|
+
channel: PaintChannel;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Build the paint-options modal. The returned handle is mounted once by the
|
|
9
|
+
* shell; `open` re-seeds it for whichever channel's row was clicked.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createPaintOptionsModal(i18n: I18n): {
|
|
12
|
+
el: HTMLDivElement;
|
|
13
|
+
mount: (parent: HTMLElement) => void;
|
|
14
|
+
open: (ctx: PaintOptionsContext) => void;
|
|
15
|
+
close: () => void;
|
|
16
|
+
destroy: () => void;
|
|
17
|
+
};
|
|
18
|
+
export type PaintOptionsModal = ReturnType<typeof createPaintOptionsModal>;
|
|
19
|
+
//# sourceMappingURL=PaintOptionsModal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PaintOptionsModal.d.ts","sourceRoot":"","sources":["../../src/components/PaintOptionsModal.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,IAAI,EAAc,MAAM,kBAAkB,CAAC;AAEzD,2DAA2D;AAC3D,MAAM,WAAW,mBAAmB;IAChC,OAAO,EAAE,YAAY,CAAC;CACzB;AAgBD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,IAAI;;oBAmEvB,WAAW,KAAG,IAAI;gBAftB,mBAAmB,KAAG,IAAI;iBAS3B,IAAI;mBAkBF,IAAI;EAM3B;AAED,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// The advanced Fill / Line options dialog, opened from a paint panel's "More
|
|
2
|
+
// Fill/Line Options…" row. A modal — backdrop + centered dialog — that will
|
|
3
|
+
// host the richer authoring surface the dropdown can't: a custom colour, and
|
|
4
|
+
// the non-solid fills (gradient / pattern / picture) for Fill; compound lines,
|
|
5
|
+
// caps / joins, and arrow ends for Stroke.
|
|
6
|
+
//
|
|
7
|
+
// Storeless and imperative, matching {@link createPageSettingsModal}: mounted
|
|
8
|
+
// once by the shell and reused for whichever channel's row was clicked.
|
|
9
|
+
// `open({ channel })` seeds the title; the body is a placeholder until the
|
|
10
|
+
// surface is designed — the goal here is the dialog + its seam, not its content.
|
|
11
|
+
import { ICON_CLEAR, on } from "@docmentis/udoc-ui/internal";
|
|
12
|
+
const TITLE = {
|
|
13
|
+
fill: "paintOptions.fillTitle",
|
|
14
|
+
stroke: "paintOptions.lineTitle",
|
|
15
|
+
text: "paintOptions.textColorTitle",
|
|
16
|
+
highlight: "paintOptions.highlightTitle",
|
|
17
|
+
};
|
|
18
|
+
const BLURB = {
|
|
19
|
+
fill: "paintOptions.fillBlurb",
|
|
20
|
+
stroke: "paintOptions.lineBlurb",
|
|
21
|
+
text: "paintOptions.textColorBlurb",
|
|
22
|
+
highlight: "paintOptions.highlightBlurb",
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Build the paint-options modal. The returned handle is mounted once by the
|
|
26
|
+
* shell; `open` re-seeds it for whichever channel's row was clicked.
|
|
27
|
+
*/
|
|
28
|
+
export function createPaintOptionsModal(i18n) {
|
|
29
|
+
const el = document.createElement("div");
|
|
30
|
+
el.className = "udoc-paint-modal";
|
|
31
|
+
el.style.display = "none";
|
|
32
|
+
const backdrop = document.createElement("div");
|
|
33
|
+
backdrop.className = "udoc-paint-modal__backdrop";
|
|
34
|
+
const dialog = document.createElement("div");
|
|
35
|
+
dialog.className = "udoc-paint-modal__dialog";
|
|
36
|
+
dialog.setAttribute("role", "dialog");
|
|
37
|
+
dialog.setAttribute("aria-modal", "true");
|
|
38
|
+
// --- header ---
|
|
39
|
+
const header = document.createElement("div");
|
|
40
|
+
header.className = "udoc-paint-modal__header";
|
|
41
|
+
const heading = document.createElement("h2");
|
|
42
|
+
heading.className = "udoc-paint-modal__heading";
|
|
43
|
+
const closeBtn = document.createElement("button");
|
|
44
|
+
closeBtn.type = "button";
|
|
45
|
+
closeBtn.className = "udoc-paint-modal__close";
|
|
46
|
+
closeBtn.setAttribute("aria-label", i18n.t("common.close"));
|
|
47
|
+
closeBtn.title = i18n.t("common.close");
|
|
48
|
+
closeBtn.innerHTML = ICON_CLEAR;
|
|
49
|
+
header.append(heading, closeBtn);
|
|
50
|
+
// --- body (placeholder until the authoring surface is designed) ---
|
|
51
|
+
const body = document.createElement("div");
|
|
52
|
+
body.className = "udoc-paint-modal__body";
|
|
53
|
+
const placeholder = document.createElement("p");
|
|
54
|
+
placeholder.className = "udoc-paint-modal__placeholder";
|
|
55
|
+
body.appendChild(placeholder);
|
|
56
|
+
// --- footer ---
|
|
57
|
+
const footer = document.createElement("div");
|
|
58
|
+
footer.className = "udoc-paint-modal__footer";
|
|
59
|
+
const doneBtn = document.createElement("button");
|
|
60
|
+
doneBtn.type = "button";
|
|
61
|
+
doneBtn.className = "udoc-paint-modal__done";
|
|
62
|
+
doneBtn.textContent = i18n.t("common.done");
|
|
63
|
+
footer.appendChild(doneBtn);
|
|
64
|
+
dialog.append(header, body, footer);
|
|
65
|
+
el.append(backdrop, dialog);
|
|
66
|
+
let isOpen = false;
|
|
67
|
+
const unsubEvents = [];
|
|
68
|
+
on(closeBtn, "click", () => close());
|
|
69
|
+
on(doneBtn, "click", () => close());
|
|
70
|
+
on(backdrop, "click", () => close());
|
|
71
|
+
function open(ctx) {
|
|
72
|
+
heading.textContent = i18n.t(TITLE[ctx.channel]);
|
|
73
|
+
dialog.setAttribute("aria-label", i18n.t(TITLE[ctx.channel]));
|
|
74
|
+
placeholder.textContent = i18n.t(BLURB[ctx.channel]);
|
|
75
|
+
isOpen = true;
|
|
76
|
+
el.style.display = "";
|
|
77
|
+
doneBtn.focus();
|
|
78
|
+
}
|
|
79
|
+
function close() {
|
|
80
|
+
if (!isOpen)
|
|
81
|
+
return;
|
|
82
|
+
isOpen = false;
|
|
83
|
+
el.style.display = "none";
|
|
84
|
+
}
|
|
85
|
+
function mount(parent) {
|
|
86
|
+
parent.appendChild(el);
|
|
87
|
+
const onKeydown = (e) => {
|
|
88
|
+
if (isOpen && e.key === "Escape") {
|
|
89
|
+
e.stopPropagation();
|
|
90
|
+
close();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
document.addEventListener("keydown", onKeydown);
|
|
94
|
+
unsubEvents.push(() => document.removeEventListener("keydown", onKeydown));
|
|
95
|
+
}
|
|
96
|
+
function destroy() {
|
|
97
|
+
for (const off of unsubEvents)
|
|
98
|
+
off();
|
|
99
|
+
el.remove();
|
|
100
|
+
}
|
|
101
|
+
return { el, mount, open, close, destroy };
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=PaintOptionsModal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PaintOptionsModal.js","sourceRoot":"","sources":["../../src/components/PaintOptionsModal.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { RibbonControl, RibbonControlEnv } from "@docmentis/udoc-ui/internal";
|
|
2
|
+
import type { ListKind, ListStyle, TextFormatProperty } from "../text-format.js";
|
|
3
|
+
import type { EditTools } from "../tools.js";
|
|
4
|
+
/** The em an engine-drawn specimen is rasterized at, in CSS px — matched to the
|
|
5
|
+
* row's own text size so a masked row and a live-text row read at the same
|
|
6
|
+
* weight beside each other. */
|
|
7
|
+
export declare const FONT_SPECIMEN_EM = 14;
|
|
8
|
+
/**
|
|
9
|
+
* The Text tab's font picker: a typeable combobox showing the *selection's*
|
|
10
|
+
* resolved font. The list offers the catalog (opened full, virtualized), Enter
|
|
11
|
+
* applies any name — one outside the list included — and, while the list is
|
|
12
|
+
* open, typing filters it (the input doubles as a search field). The input
|
|
13
|
+
* mirrors the selection; each render keeps it in sync.
|
|
14
|
+
*/
|
|
15
|
+
export declare function fontFamilyControl(env: RibbonControlEnv, id: string, tools: EditTools): RibbonControl;
|
|
16
|
+
/**
|
|
17
|
+
* The Text tab's size field: a typeable combobox showing the selection's
|
|
18
|
+
* resolved point size, its list the preset sizes (rebuilt on open so the check
|
|
19
|
+
* tracks the selection). Enter applies any point size in range — one outside
|
|
20
|
+
* the presets included. Mirror-only like the font picker.
|
|
21
|
+
*/
|
|
22
|
+
export declare function fontSizeControl(env: RibbonControlEnv, id: string, tools: EditTools): RibbonControl;
|
|
23
|
+
/**
|
|
24
|
+
* A character toggle (Bold / Italic / Underline / …): an icon button
|
|
25
|
+
* reflecting whether the selection carries that format (on / off / mixed). A
|
|
26
|
+
* mixed / off selection turns the format on, a fully-on one turns it off (the
|
|
27
|
+
* Office gesture).
|
|
28
|
+
*/
|
|
29
|
+
export declare function textFormatControl(env: RibbonControlEnv, command: {
|
|
30
|
+
id: string;
|
|
31
|
+
icon: string;
|
|
32
|
+
property: TextFormatProperty;
|
|
33
|
+
}, tools: EditTools): RibbonControl;
|
|
34
|
+
/**
|
|
35
|
+
* A Bullets / Numbering split button: the main region toggles the selection's
|
|
36
|
+
* paragraphs into the last-picked style (or out of the list when it's already
|
|
37
|
+
* lit), the caret opens the style gallery. A mirror like the character toggles
|
|
38
|
+
* — the lit state re-derives from the selection each render, not from the click.
|
|
39
|
+
*/
|
|
40
|
+
export declare function listControl(env: RibbonControlEnv, command: {
|
|
41
|
+
id: string;
|
|
42
|
+
icon: string;
|
|
43
|
+
list: ListKind;
|
|
44
|
+
}, tools: EditTools): RibbonControl;
|
|
45
|
+
/** The style each list button applies when its main region is clicked — the
|
|
46
|
+
* gallery's first entry until one is picked, that pick from then on (the
|
|
47
|
+
* split-button contract: the caret chooses, the main repeats). */
|
|
48
|
+
export declare function createLastListStyle(): Record<ListKind, ListStyle>;
|
|
49
|
+
//# sourceMappingURL=TextControls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TextControls.d.ts","sourceRoot":"","sources":["../../src/components/TextControls.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAmB,MAAM,6BAA6B,CAAC;AAKpG,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAiB7C;;gCAEgC;AAChC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAqHnC;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,gBAAgB,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,aAAa,CAkLpG;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,gBAAgB,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,aAAa,CAiDlG;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC7B,GAAG,EAAE,gBAAgB,EACrB,OAAO,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,kBAAkB,CAAA;CAAE,EACnE,KAAK,EAAE,SAAS,GACjB,aAAa,CAcf;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACvB,GAAG,EAAE,gBAAgB,EACrB,OAAO,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,EACrD,KAAK,EAAE,SAAS,GACjB,aAAa,CA8Df;AAED;;mEAEmE;AACnE,wBAAgB,mBAAmB,IAAI,MAAM,CAAC,QAAQ,EAAE,SAAS,CAAC,CAEjE"}
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { createButton, createSplitButton } from "@docmentis/udoc-ui/internal";
|
|
2
|
+
import { cssFontFamily, isEngineDrawn } from "../fonts/preview.js";
|
|
3
|
+
import { editState } from "../state.js";
|
|
4
|
+
import { listStyles } from "../text-format.js";
|
|
5
|
+
import { createComboBox } from "./ComboBox.js";
|
|
6
|
+
/** The point sizes the Text tab's size dropdown offers — the common Office set.
|
|
7
|
+
* The selection's own size still shows on the trigger even when it isn't one of
|
|
8
|
+
* these (a document can carry any size); the list is just the quick picks. */
|
|
9
|
+
const SIZE_PRESETS = [8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 32, 36, 40, 48, 60, 72];
|
|
10
|
+
// Font dropdown virtualization knobs. FONT_ROW_H must match the fixed
|
|
11
|
+
// `.udoc-font-item` height in styles.ts; FONT_MENU_ROWS is how many rows
|
|
12
|
+
// the menu's (height-capped) max-height shows. The number of selectable
|
|
13
|
+
// fonts is unbounded — the list scrolls the whole catalog.
|
|
14
|
+
const FONT_ROW_H = 30;
|
|
15
|
+
const FONT_MENU_ROWS = 10;
|
|
16
|
+
const FONT_OVERSCAN = 4;
|
|
17
|
+
/** The em an engine-drawn specimen is rasterized at, in CSS px — matched to the
|
|
18
|
+
* row's own text size so a masked row and a live-text row read at the same
|
|
19
|
+
* weight beside each other. */
|
|
20
|
+
export const FONT_SPECIMEN_EM = 14;
|
|
21
|
+
/** A text-format toggle's `aria-pressed` for the tri-state selection format —
|
|
22
|
+
* `"mixed"` for a split selection, else the plain boolean string. The active
|
|
23
|
+
* tint lights for both `"on"` and `"mixed"` (the format is present); the aria
|
|
24
|
+
* value is what distinguishes them for assistive tech. */
|
|
25
|
+
function textFormatAria(state) {
|
|
26
|
+
return state === "on" ? "true" : state === "mixed" ? "mixed" : "false";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Whether a family answers what has been typed into the font box.
|
|
30
|
+
*
|
|
31
|
+
* **Prefix, not substring.** Across ~2000 families a match anywhere in the name
|
|
32
|
+
* buries what you meant: `ar` used to return 227 rows — Akshar, Almarai and
|
|
33
|
+
* Amaranth ahead of Arimo — where it now returns 31, and `oto` matched Roboto
|
|
34
|
+
* without you having typed its start. Type-ahead is for a name you already know,
|
|
35
|
+
* so the match has to begin where the name does.
|
|
36
|
+
*
|
|
37
|
+
* Every **word** is a starting point, not just the first, because a family's
|
|
38
|
+
* distinguishing word is often not the one it leads with — `sans` has to keep
|
|
39
|
+
* finding Open Sans, Noto Sans and PT Sans, and `mono` Roboto Mono. That is
|
|
40
|
+
* still a prefix rule: it drops the mid-word matches and keeps the ones a reader
|
|
41
|
+
* would call "starts with".
|
|
42
|
+
*
|
|
43
|
+
* Splitting on spaces is exhaustive for this catalog — every name in it is
|
|
44
|
+
* letters, digits and spaces, with no hyphens or punctuation to miss.
|
|
45
|
+
*/
|
|
46
|
+
function matchesFontQuery(name, query) {
|
|
47
|
+
const lower = name.toLowerCase();
|
|
48
|
+
return lower.startsWith(query) || lower.split(" ").some((word) => word.startsWith(query));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Put a font-picker row's family into its label — as its own typeface where we
|
|
52
|
+
* can, and as plain text where we can't yet.
|
|
53
|
+
*
|
|
54
|
+
* Three states, and the row is legible in all of them. A family the browser can
|
|
55
|
+
* load is **live text** with `font-family` set to it: the name shows in the UI
|
|
56
|
+
* face until the web font arrives and in its own from then on, with no work here
|
|
57
|
+
* to make the swap happen. A family only the engine can draw shows the plain
|
|
58
|
+
* name until its specimen lands, then the mask replaces it. A specimen the
|
|
59
|
+
* engine had nothing to draw for stays plain text, which is exactly what the
|
|
60
|
+
* picker looked like before specimens existed.
|
|
61
|
+
*
|
|
62
|
+
* The engine specimen is a `background: currentColor` box with the PNG as its
|
|
63
|
+
* mask, so it takes the row's text color: the hovered row, the dark theme and
|
|
64
|
+
* the forced-colors mode all come out right without a second raster. `width`
|
|
65
|
+
* and `height` are the CSS box the engine sized it for, and `bleed` the
|
|
66
|
+
* transparent margin inside it that a negative offset takes back out.
|
|
67
|
+
*/
|
|
68
|
+
function fillFontLabel(label, family, loader) {
|
|
69
|
+
const specimen = isEngineDrawn(family.sourceType) ? loader.specimen(family.name) : undefined;
|
|
70
|
+
if (specimen) {
|
|
71
|
+
const ink = document.createElement("span");
|
|
72
|
+
ink.className = "udoc-font-specimen";
|
|
73
|
+
ink.style.setProperty("--udoc-font-specimen", `url("${specimen.url}")`);
|
|
74
|
+
ink.style.width = `${specimen.width}px`;
|
|
75
|
+
ink.style.height = `${specimen.height}px`;
|
|
76
|
+
// Pull the raster back by its bleed so the specimen's pen origin lands
|
|
77
|
+
// where a live-text row's does; without it every engine row sits an
|
|
78
|
+
// eighth of an em right of its neighbours.
|
|
79
|
+
ink.style.marginLeft = `${-specimen.bleed}px`;
|
|
80
|
+
label.replaceChildren(ink);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
label.textContent = family.name;
|
|
84
|
+
// An engine-drawn family keeps the UI face while it waits: naming it to CSS
|
|
85
|
+
// would let a same-named OS font answer for a face the document isn't using.
|
|
86
|
+
if (!isEngineDrawn(family.sourceType))
|
|
87
|
+
label.style.fontFamily = cssFontFamily(family.name);
|
|
88
|
+
}
|
|
89
|
+
// Reflect a value in a combobox's input. Skipped while the input is focused so
|
|
90
|
+
// it never stomps a value the user is typing (the incoming selection report
|
|
91
|
+
// re-mirrors on blur).
|
|
92
|
+
//
|
|
93
|
+
// A font name shows in the UI font, not its own typeface. We deliberately don't
|
|
94
|
+
// preview: the engine shapes font formats the browser can't load as web fonts
|
|
95
|
+
// (Type1, ...), and bundled / embedded faces never reach the DOM at all — an
|
|
96
|
+
// honest name beats a preview that only works for the subset a `font-family`
|
|
97
|
+
// CSS lookup happens to resolve.
|
|
98
|
+
function mirrorField(field, value) {
|
|
99
|
+
if (field === document.activeElement)
|
|
100
|
+
return;
|
|
101
|
+
field.value = value;
|
|
102
|
+
}
|
|
103
|
+
// Commit a typed value on Enter, abandon it on Escape / blur (the mirror then
|
|
104
|
+
// restores the selection's value), open the list on ArrowDown, and keep the
|
|
105
|
+
// caret-movement keys for in-field editing — stopping them from reaching the
|
|
106
|
+
// ribbon's roving handler, which would otherwise jump focus to an adjacent
|
|
107
|
+
// tool. `mirror` re-reads the live selection each blur.
|
|
108
|
+
function wireComboInput(combo, commit, mirror) {
|
|
109
|
+
combo.input.addEventListener("keydown", (e) => {
|
|
110
|
+
switch (e.key) {
|
|
111
|
+
case "Enter":
|
|
112
|
+
commit(combo.input.value);
|
|
113
|
+
combo.input.blur();
|
|
114
|
+
break;
|
|
115
|
+
case "Escape":
|
|
116
|
+
combo.input.blur();
|
|
117
|
+
break;
|
|
118
|
+
case "ArrowDown":
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
combo.controller.open();
|
|
121
|
+
break;
|
|
122
|
+
case "ArrowLeft":
|
|
123
|
+
case "ArrowRight":
|
|
124
|
+
case "Home":
|
|
125
|
+
case "End":
|
|
126
|
+
e.stopPropagation();
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
combo.input.addEventListener("focus", () => combo.input.select());
|
|
131
|
+
combo.input.addEventListener("blur", mirror);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The Text tab's font picker: a typeable combobox showing the *selection's*
|
|
135
|
+
* resolved font. The list offers the catalog (opened full, virtualized), Enter
|
|
136
|
+
* applies any name — one outside the list included — and, while the list is
|
|
137
|
+
* open, typing filters it (the input doubles as a search field). The input
|
|
138
|
+
* mirrors the selection; each render keeps it in sync.
|
|
139
|
+
*/
|
|
140
|
+
export function fontFamilyControl(env, id, tools) {
|
|
141
|
+
const { store, label } = env;
|
|
142
|
+
const { i18n } = tools;
|
|
143
|
+
const selectedFont = () => editState(store.getState()).selectionText.font;
|
|
144
|
+
// Re-render the open font list. Set while a list is open, so a specimen
|
|
145
|
+
// landing from the engine can put itself on screen; cleared on close, so a
|
|
146
|
+
// late arrival for a closed list does nothing.
|
|
147
|
+
let refreshFontMenu = null;
|
|
148
|
+
const unlisten = tools.onSpecimens(() => refreshFontMenu?.());
|
|
149
|
+
// Render the font combobox's list (a listbox) — one row per family name,
|
|
150
|
+
// the current pick checked. Every listed family is pickable whether or not
|
|
151
|
+
// its bytes are here yet: the engine shapes a still-downloading family in a
|
|
152
|
+
// stand-in and re-shapes when it lands, so the row has no load state to
|
|
153
|
+
// show. The catalog can run to ~2000 families, so the list is **filtered by
|
|
154
|
+
// `query` and virtualized**: a full-height sizer the menu scrolls, with only
|
|
155
|
+
// the visible rows (plus overscan) mounted, each absolutely positioned.
|
|
156
|
+
//
|
|
157
|
+
// The list is in **two sections**: the families already loaded, then a
|
|
158
|
+
// divider, then the rest of the catalog. Both sections are filtered by the
|
|
159
|
+
// same query, so a section can empty out and the divider goes with it. The
|
|
160
|
+
// divider is a border on the first row below it rather than a row of its
|
|
161
|
+
// own — a row of its own would have to carry a height through the virtual
|
|
162
|
+
// list's index-to-offset arithmetic, and this needs nothing from it.
|
|
163
|
+
const renderFontMenu = (dd, query, keepScroll = false) => {
|
|
164
|
+
const state = editState(store.getState());
|
|
165
|
+
// A full rebuild replaces the scroll viewport, so where the list was has
|
|
166
|
+
// to be carried across by hand. `keepScroll` marks the renders that are
|
|
167
|
+
// *not* a new list — a specimen arriving mid-scroll — where jumping back
|
|
168
|
+
// to the selection would yank the rows out from under the pointer.
|
|
169
|
+
const wasAt = dd.menu.querySelector(".udoc-font-scroll")?.scrollTop ?? 0;
|
|
170
|
+
const q = query.trim().toLowerCase();
|
|
171
|
+
const matches = q
|
|
172
|
+
? state.fontFamilies.filter((family) => matchesFontQuery(family.name, q))
|
|
173
|
+
: state.fontFamilies;
|
|
174
|
+
// A Set because this runs over the whole ~2000-family list.
|
|
175
|
+
const here = new Set(state.loadedFontFamilyNames);
|
|
176
|
+
const loaded = matches.filter((family) => here.has(family.name));
|
|
177
|
+
const rest = matches.filter((family) => !here.has(family.name));
|
|
178
|
+
// Every match — the virtual list mounts only the visible rows, so the
|
|
179
|
+
// whole (unfiltered) ~2000-family catalog scrolls just as cheaply.
|
|
180
|
+
const shown = [...loaded, ...rest];
|
|
181
|
+
// Where the second section starts, so its first row draws the divider.
|
|
182
|
+
// -1 when either section is empty: nothing to divide.
|
|
183
|
+
const dividerAt = loaded.length > 0 && rest.length > 0 ? loaded.length : -1;
|
|
184
|
+
const selected = state.selectionText.font;
|
|
185
|
+
dd.menu.classList.add("udoc-font-menu");
|
|
186
|
+
dd.menu.innerHTML = "";
|
|
187
|
+
if (shown.length === 0) {
|
|
188
|
+
const empty = document.createElement("div");
|
|
189
|
+
empty.className = "udoc-menu__empty";
|
|
190
|
+
empty.textContent = i18n.t("ribbon.noMatchingFonts");
|
|
191
|
+
dd.menu.appendChild(empty);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// The scroll viewport is a child of the panel, not the panel — the
|
|
195
|
+
// panel's rounded corners have to clip the native scrollbar. See
|
|
196
|
+
// udoc-font-scroll in styles.ts.
|
|
197
|
+
const viewport = document.createElement("div");
|
|
198
|
+
viewport.className = "udoc-font-scroll";
|
|
199
|
+
const sizer = document.createElement("div");
|
|
200
|
+
sizer.className = "udoc-font-sizer";
|
|
201
|
+
sizer.style.height = `${shown.length * FONT_ROW_H}px`;
|
|
202
|
+
viewport.appendChild(sizer);
|
|
203
|
+
dd.menu.appendChild(viewport);
|
|
204
|
+
// Mount only the rows in view (+ overscan); recycle on scroll.
|
|
205
|
+
const loader = tools.fontPreviews;
|
|
206
|
+
const renderWindow = () => {
|
|
207
|
+
const scrollTop = viewport.scrollTop;
|
|
208
|
+
const viewH = viewport.clientHeight || FONT_ROW_H * FONT_MENU_ROWS;
|
|
209
|
+
const first = Math.max(0, Math.floor(scrollTop / FONT_ROW_H) - FONT_OVERSCAN);
|
|
210
|
+
const last = Math.min(shown.length, Math.ceil((scrollTop + viewH) / FONT_ROW_H) + FONT_OVERSCAN);
|
|
211
|
+
sizer.replaceChildren();
|
|
212
|
+
for (let i = first; i < last; i++) {
|
|
213
|
+
const family = shown[i];
|
|
214
|
+
const name = family.name;
|
|
215
|
+
const item = document.createElement("button");
|
|
216
|
+
item.className =
|
|
217
|
+
i === dividerAt
|
|
218
|
+
? "udoc-menu__item udoc-font-item udoc-font-item--section"
|
|
219
|
+
: "udoc-menu__item udoc-font-item";
|
|
220
|
+
item.setAttribute("role", "option");
|
|
221
|
+
// The label is the family drawn in its own face, so the row's
|
|
222
|
+
// accessible name has to be stated rather than read off the text
|
|
223
|
+
// — for an engine specimen there is no text to read.
|
|
224
|
+
item.setAttribute("aria-label", name);
|
|
225
|
+
item.title = name;
|
|
226
|
+
item.style.top = `${i * FONT_ROW_H}px`;
|
|
227
|
+
const isSelected = name === selected;
|
|
228
|
+
item.classList.toggle("udoc-menu__item--selected", isSelected);
|
|
229
|
+
item.setAttribute("aria-selected", String(isSelected));
|
|
230
|
+
item.innerHTML = `<span class="udoc-menu__item-label"></span>`;
|
|
231
|
+
fillFontLabel(item.lastElementChild, family, loader);
|
|
232
|
+
item.addEventListener("click", (e) => {
|
|
233
|
+
e.stopPropagation();
|
|
234
|
+
// The trigger mirrors the selection, so it updates when the
|
|
235
|
+
// resulting report lands.
|
|
236
|
+
void tools.text.fontFamilySelection(name);
|
|
237
|
+
dd.controller.close();
|
|
238
|
+
});
|
|
239
|
+
sizer.appendChild(item);
|
|
240
|
+
}
|
|
241
|
+
// Ask for the window's specimens once the rows are up, so the list
|
|
242
|
+
// is on screen before any of this starts. Families already worked
|
|
243
|
+
// on are skipped inside, which is what makes calling this on every
|
|
244
|
+
// scroll step cheap.
|
|
245
|
+
loader.request(shown.slice(first, last).map((f) => ({ family: f.name, sourceType: f.sourceType })));
|
|
246
|
+
};
|
|
247
|
+
viewport.onscroll = renderWindow;
|
|
248
|
+
// On open (no filter), reveal the current selection; on a filter, top.
|
|
249
|
+
const selectedIndex = q ? -1 : shown.findIndex((family) => family.name === selected);
|
|
250
|
+
viewport.scrollTop = keepScroll
|
|
251
|
+
? wasAt
|
|
252
|
+
: selectedIndex > 0
|
|
253
|
+
? Math.max(0, selectedIndex * FONT_ROW_H - (FONT_ROW_H * FONT_MENU_ROWS) / 2)
|
|
254
|
+
: 0;
|
|
255
|
+
renderWindow();
|
|
256
|
+
};
|
|
257
|
+
// What the open list is filtered by — deliberately *not* the input's value.
|
|
258
|
+
// The input mirrors the selection, so on open it holds the font already
|
|
259
|
+
// applied; filtering by that would narrow the list to the one row you
|
|
260
|
+
// started from. It becomes a query only once typed into, and opening clears
|
|
261
|
+
// it again.
|
|
262
|
+
let fontQuery = "";
|
|
263
|
+
const combo = createComboBox({ label, className: "udoc-font-select" }, {
|
|
264
|
+
menuLabel: label,
|
|
265
|
+
onOpen: () => {
|
|
266
|
+
fontQuery = "";
|
|
267
|
+
// Held while the list is up so a specimen arriving from the
|
|
268
|
+
// engine can put itself on screen (it lands a round trip after
|
|
269
|
+
// the row was mounted).
|
|
270
|
+
refreshFontMenu = () => renderFontMenu(combo, fontQuery, true);
|
|
271
|
+
// Render from what the store already holds, then again when the
|
|
272
|
+
// refreshed loaded set lands — rendering only on the reply would
|
|
273
|
+
// blank the list for a round trip, and never re-rendering would
|
|
274
|
+
// leave a font loaded since the last open below the divider.
|
|
275
|
+
renderFontMenu(combo, fontQuery);
|
|
276
|
+
void tools.refreshLoadedFontFamilies().then(() => {
|
|
277
|
+
if (combo.controller.isOpen)
|
|
278
|
+
renderFontMenu(combo, fontQuery);
|
|
279
|
+
});
|
|
280
|
+
},
|
|
281
|
+
onClose: () => {
|
|
282
|
+
refreshFontMenu = null;
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
combo.input.dataset.command = id;
|
|
286
|
+
mirrorField(combo.input, selectedFont());
|
|
287
|
+
wireComboInput(combo, (raw) => {
|
|
288
|
+
const name = raw.trim();
|
|
289
|
+
if (name)
|
|
290
|
+
void tools.text.fontFamilySelection(name);
|
|
291
|
+
}, () => mirrorField(combo.input, selectedFont()));
|
|
292
|
+
// Type-to-filter: narrow the open list as the user types (closed, typing
|
|
293
|
+
// just edits the value the mirror will restore).
|
|
294
|
+
combo.input.addEventListener("input", () => {
|
|
295
|
+
if (!combo.controller.isOpen)
|
|
296
|
+
return;
|
|
297
|
+
fontQuery = combo.input.value;
|
|
298
|
+
renderFontMenu(combo, fontQuery);
|
|
299
|
+
});
|
|
300
|
+
return {
|
|
301
|
+
el: combo.el,
|
|
302
|
+
sync: (state) => mirrorField(combo.input, editState(state).selectionText.font),
|
|
303
|
+
close: () => combo.controller.close(),
|
|
304
|
+
destroy: () => {
|
|
305
|
+
unlisten();
|
|
306
|
+
combo.destroy();
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* The Text tab's size field: a typeable combobox showing the selection's
|
|
312
|
+
* resolved point size, its list the preset sizes (rebuilt on open so the check
|
|
313
|
+
* tracks the selection). Enter applies any point size in range — one outside
|
|
314
|
+
* the presets included. Mirror-only like the font picker.
|
|
315
|
+
*/
|
|
316
|
+
export function fontSizeControl(env, id, tools) {
|
|
317
|
+
const { store, label } = env;
|
|
318
|
+
const selectedSize = () => editState(store.getState()).selectionText.size;
|
|
319
|
+
// Fill the list (a listbox) — one option per preset point size, the one
|
|
320
|
+
// matching the selection marked selected. Like the font picker the input is
|
|
321
|
+
// a mirror, so it updates when the resulting selection report lands, not on
|
|
322
|
+
// the click.
|
|
323
|
+
const fillSizes = (dd) => {
|
|
324
|
+
dd.menu.innerHTML = "";
|
|
325
|
+
for (const size of SIZE_PRESETS) {
|
|
326
|
+
const item = document.createElement("button");
|
|
327
|
+
item.className = "udoc-menu__item udoc-size-item";
|
|
328
|
+
item.setAttribute("role", "option");
|
|
329
|
+
const selected = String(size) === selectedSize();
|
|
330
|
+
item.classList.toggle("udoc-menu__item--selected", selected);
|
|
331
|
+
item.setAttribute("aria-selected", String(selected));
|
|
332
|
+
item.innerHTML = `<span class="udoc-menu__item-label">${size}</span>`;
|
|
333
|
+
item.addEventListener("click", (e) => {
|
|
334
|
+
e.stopPropagation();
|
|
335
|
+
void tools.text.fontSizeSelection(size);
|
|
336
|
+
dd.controller.close();
|
|
337
|
+
});
|
|
338
|
+
dd.menu.appendChild(item);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
const combo = createComboBox({ label, className: "udoc-size-select", inputMode: "decimal" }, { menuLabel: label, onOpen: () => fillSizes(combo) });
|
|
342
|
+
combo.input.dataset.command = id;
|
|
343
|
+
mirrorField(combo.input, selectedSize());
|
|
344
|
+
wireComboInput(combo, (raw) => {
|
|
345
|
+
const size = parseFloat(raw);
|
|
346
|
+
// Office's point-size range; an out-of-range or non-numeric entry is
|
|
347
|
+
// dropped and the mirror restores the real value.
|
|
348
|
+
if (!isNaN(size) && size >= 1 && size <= 4000)
|
|
349
|
+
void tools.text.fontSizeSelection(size);
|
|
350
|
+
}, () => mirrorField(combo.input, selectedSize()));
|
|
351
|
+
return {
|
|
352
|
+
el: combo.el,
|
|
353
|
+
sync: (state) => mirrorField(combo.input, editState(state).selectionText.size),
|
|
354
|
+
close: () => combo.controller.close(),
|
|
355
|
+
destroy: () => combo.destroy(),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* A character toggle (Bold / Italic / Underline / …): an icon button
|
|
360
|
+
* reflecting whether the selection carries that format (on / off / mixed). A
|
|
361
|
+
* mixed / off selection turns the format on, a fully-on one turns it off (the
|
|
362
|
+
* Office gesture).
|
|
363
|
+
*/
|
|
364
|
+
export function textFormatControl(env, command, tools) {
|
|
365
|
+
const { property } = command;
|
|
366
|
+
const btn = createButton({ icon: command.icon, label: env.label });
|
|
367
|
+
btn.dataset.command = command.id;
|
|
368
|
+
const sync = (state) => {
|
|
369
|
+
const format = editState(state).selectionText[property];
|
|
370
|
+
btn.classList.toggle("udoc-btn--selected", format !== "off");
|
|
371
|
+
btn.setAttribute("aria-pressed", textFormatAria(format));
|
|
372
|
+
};
|
|
373
|
+
sync(env.store.getState());
|
|
374
|
+
btn.addEventListener("click", () => {
|
|
375
|
+
void tools.text.formatTextSelection(property, editState(env.store.getState()).selectionText[property] !== "on");
|
|
376
|
+
});
|
|
377
|
+
return { el: btn, sync };
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* A Bullets / Numbering split button: the main region toggles the selection's
|
|
381
|
+
* paragraphs into the last-picked style (or out of the list when it's already
|
|
382
|
+
* lit), the caret opens the style gallery. A mirror like the character toggles
|
|
383
|
+
* — the lit state re-derives from the selection each render, not from the click.
|
|
384
|
+
*/
|
|
385
|
+
export function listControl(env, command, tools) {
|
|
386
|
+
const { i18n, lastListStyle } = tools;
|
|
387
|
+
const { list } = command;
|
|
388
|
+
const listed = () => editState(env.store.getState()).selectionText[list];
|
|
389
|
+
// Fill the gallery — one row per marker style. No row is marked: the
|
|
390
|
+
// selection reports only whether its paragraphs are in a list, not which
|
|
391
|
+
// style, and the last pick the main region re-applies is not a value the
|
|
392
|
+
// selection holds. The row's sample is the markers themselves ("1. 2. 3."),
|
|
393
|
+
// not a glyph: the gallery's whole question is what the marker *looks
|
|
394
|
+
// like*, which a drawn icon would only paraphrase. Picking applies the
|
|
395
|
+
// style and remembers it for the main region; the button's lit state still
|
|
396
|
+
// comes from the selection report that follows.
|
|
397
|
+
const fillListStyles = () => {
|
|
398
|
+
split.menu.innerHTML = "";
|
|
399
|
+
for (const style of listStyles(list)) {
|
|
400
|
+
const item = document.createElement("button");
|
|
401
|
+
item.className = "udoc-menu__item udoc-list-item";
|
|
402
|
+
item.setAttribute("role", "menuitem");
|
|
403
|
+
const label = i18n.t(style.label);
|
|
404
|
+
item.setAttribute("aria-label", label);
|
|
405
|
+
item.title = label;
|
|
406
|
+
item.innerHTML = `<span class="udoc-menu__item-label udoc-list-sample">${style.preview}</span>`;
|
|
407
|
+
item.addEventListener("click", (e) => {
|
|
408
|
+
e.stopPropagation();
|
|
409
|
+
lastListStyle[list] = style;
|
|
410
|
+
void tools.text.listSelection(style);
|
|
411
|
+
split.controller.close();
|
|
412
|
+
});
|
|
413
|
+
split.menu.appendChild(item);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
const split = createSplitButton({
|
|
417
|
+
mainLabel: env.label,
|
|
418
|
+
mainIcon: command.icon,
|
|
419
|
+
caretLabel: i18n.t("ribbon.listOptions", { label: env.label }),
|
|
420
|
+
menuLabel: i18n.t("ribbon.listStyles", { label: env.label }),
|
|
421
|
+
onOpen: fillListStyles,
|
|
422
|
+
});
|
|
423
|
+
split.main.dataset.command = command.id;
|
|
424
|
+
split.caret.dataset.command = command.id;
|
|
425
|
+
split.setActive(listed() !== "off");
|
|
426
|
+
split.main.addEventListener("click", () => {
|
|
427
|
+
// Lit means every touched paragraph is already in this kind of list, so
|
|
428
|
+
// the button's job is to take them out; mixed and off both put them in
|
|
429
|
+
// (the Office gesture, matching Bold).
|
|
430
|
+
const on = listed() === "on";
|
|
431
|
+
void tools.text.listSelection(on ? null : lastListStyle[list]);
|
|
432
|
+
});
|
|
433
|
+
return {
|
|
434
|
+
el: split.el,
|
|
435
|
+
// Both halves carry the command id (so each greys with it), but the lit
|
|
436
|
+
// state belongs to the pair — set it on the wrapper they share.
|
|
437
|
+
sync: (state) => {
|
|
438
|
+
const format = editState(state).selectionText[list];
|
|
439
|
+
split.el.classList.toggle("udoc-split--selected", format !== "off");
|
|
440
|
+
split.main.setAttribute("aria-pressed", textFormatAria(format));
|
|
441
|
+
},
|
|
442
|
+
close: () => split.controller.close(),
|
|
443
|
+
destroy: () => split.destroy(),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
/** The style each list button applies when its main region is clicked — the
|
|
447
|
+
* gallery's first entry until one is picked, that pick from then on (the
|
|
448
|
+
* split-button contract: the caret chooses, the main repeats). */
|
|
449
|
+
export function createLastListStyle() {
|
|
450
|
+
return { bulleted: listStyles("bulleted")[0], numbered: listStyles("numbered")[0] };
|
|
451
|
+
}
|
|
452
|
+
//# sourceMappingURL=TextControls.js.map
|