u2ui 1.5.3 → 1.5.7
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/el/ico/ico.js +5 -5
- package/el/rte/README.md +53 -13
- package/el/rte/manifest.json +19 -4
- package/el/rte/rte.css +31 -6
- package/el/rte/rte.js +174 -44
- package/el/rte0/README.md +37 -0
- package/el/rte0/manifest.json +33 -0
- package/el/rte0/rte0.css +9 -0
- package/el/rte0/rte0.js +56 -0
- package/el/system/styler.js +2 -0
- package/el/tooltip/tooltip.css +2 -2
- package/js/Placer/Placer.js +2 -1
- package/js/rte/AGENTS.md +142 -0
- package/js/rte/PLAN.md +487 -0
- package/js/rte/README.md +319 -25
- package/js/rte/blocks.js +7 -0
- package/js/rte/breaks.js +6 -0
- package/js/rte/classes.js +7 -0
- package/js/rte/docs/usage.md +845 -0
- package/js/rte/images.js +7 -0
- package/js/rte/link.js +8 -0
- package/js/rte/playground/README.md +95 -0
- package/js/rte/playground/hosts.html +553 -0
- package/js/rte/playground/index.html +835 -0
- package/js/rte/rte.js +56 -5
- package/js/rte/run.out +17 -0
- package/js/rte/source.js +12 -0
- package/js/rte/src/browser/README.md +40 -0
- package/js/rte/src/browser/interactive.js +17 -0
- package/js/rte/src/browser/range-rect.js +82 -0
- package/js/rte/src/client/README.md +385 -0
- package/js/rte/src/client/blocks.js +38 -0
- package/js/rte/src/client/breaks.js +212 -0
- package/js/rte/src/client/classes.js +48 -0
- package/js/rte/src/client/editor.js +472 -0
- package/js/rte/src/client/external-input.js +47 -0
- package/js/rte/src/client/history.js +13 -0
- package/js/rte/src/client/images.js +285 -0
- package/js/rte/src/client/link.js +567 -0
- package/js/rte/src/client/marks.js +25 -0
- package/js/rte/src/client/source.js +182 -0
- package/js/rte/src/client/structure.js +27 -0
- package/js/rte/src/client/tables.js +165 -0
- package/js/rte/src/client/unstyle.js +32 -0
- package/js/rte/src/command/README.md +520 -0
- package/js/rte/src/command/block-boundary.js +43 -0
- package/js/rte/src/command/block-style.js +120 -0
- package/js/rte/src/command/commands.js +159 -0
- package/js/rte/src/command/delete.js +119 -0
- package/js/rte/src/command/edit.js +78 -0
- package/js/rte/src/command/element.js +45 -0
- package/js/rte/src/command/enter.js +175 -0
- package/js/rte/src/command/fragment.js +76 -0
- package/js/rte/src/command/insert.js +52 -0
- package/js/rte/src/command/list.js +284 -0
- package/js/rte/src/command/mark.js +583 -0
- package/js/rte/src/command/pending-marks.js +216 -0
- package/js/rte/src/command/table.js +207 -0
- package/js/rte/src/command/unstyle.js +248 -0
- package/js/rte/src/config/README.md +96 -0
- package/js/rte/src/config/config.js +134 -0
- package/js/rte/src/core/README.md +63 -0
- package/js/rte/src/core/core.js +247 -0
- package/js/rte/src/history/README.md +83 -0
- package/js/rte/src/history/history.js +283 -0
- package/js/rte/src/input/README.md +173 -0
- package/js/rte/src/input/external-input.js +130 -0
- package/js/rte/src/input/input-pipeline.js +459 -0
- package/js/rte/src/mark/README.md +219 -0
- package/js/rte/src/mark/dom-adapter.js +100 -0
- package/js/rte/src/mark/mark.js +133 -0
- package/js/rte/src/mark/standard.js +76 -0
- package/js/rte/src/model/README.md +71 -0
- package/js/rte/src/model/content-model.js +213 -0
- package/js/rte/src/model/html/README.md +51 -0
- package/js/rte/src/model/html/html-model.js +90 -0
- package/js/rte/src/normalize/execute/README.md +33 -0
- package/js/rte/src/normalize/execute/repair-executor.js +129 -0
- package/js/rte/src/normalize/normalizer/README.md +48 -0
- package/js/rte/src/normalize/normalizer/normalizer.js +141 -0
- package/js/rte/src/normalize/repair/README.md +70 -0
- package/js/rte/src/normalize/repair/repair-planner.js +208 -0
- package/js/rte/src/sanitize/README.md +108 -0
- package/js/rte/src/sanitize/native.js +43 -0
- package/js/rte/src/sanitize/policy.js +206 -0
- package/js/rte/src/selection/README.md +39 -0
- package/js/rte/src/selection/map/README.md +53 -0
- package/js/rte/src/selection/map/point-map.js +307 -0
- package/js/rte/src/selection/ownership/README.md +32 -0
- package/js/rte/src/selection/ownership/ownership.js +42 -0
- package/js/rte/src/selection/point/README.md +34 -0
- package/js/rte/src/selection/point/point.js +83 -0
- package/js/rte/src/selection/range/README.md +38 -0
- package/js/rte/src/selection/range/edit-range.js +223 -0
- package/js/rte/src/selection/snapshot.js +64 -0
- package/js/rte/src/source/README.md +64 -0
- package/js/rte/src/source/source.js +193 -0
- package/js/rte/src/surface/README.md +36 -0
- package/js/rte/src/surface/content.js +16 -0
- package/js/rte/src/surface/surface.js +87 -0
- package/js/rte/src/transaction/README.md +35 -0
- package/js/rte/src/transaction/transaction.js +56 -0
- package/js/rte/src/ui/README.md +271 -0
- package/js/rte/src/ui/caret.js +17 -0
- package/js/rte/src/ui/chrome.js +214 -0
- package/js/rte/src/ui/handles.js +179 -0
- package/js/rte/src/ui/highlight.js +86 -0
- package/js/rte/src/ui/place.js +25 -0
- package/js/rte/src/ui/toolbar.js +258 -0
- package/js/rte/src/unstyle/README.md +103 -0
- package/js/rte/src/unstyle/unstyle.js +140 -0
- package/js/rte/tables.js +7 -0
- package/js/rte/unstyle.js +8 -0
- package/js/rte0/README.md +37 -0
- package/js/rte0/rte.js +7 -0
- package/js/{rte → rte0}/src/behavior.js +0 -1
- package/js/{rte → rte0}/src/events.js +0 -1
- package/js/{rte → rte0}/src/range.js +0 -2
- package/js/{rte → rte0}/src/ui.items.js +0 -2
- package/js/{rte → rte0}/src/ui.js +0 -1
- package/package.json +1 -1
- package/u2/Element.js +2 -2
- package/u2/plan-scoped-refactoring.md +133 -303
- package/u2/projects.json +18 -2
- package/u2/tools/update.repos.json.js +1 -1
- package/el/qrcode/package.json +0 -22
- /package/js/{rte → rte0}/old/NodeCleaner.js +0 -0
- /package/js/{rte → rte0}/old/Rte/Rte.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/Rte.ui.items.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/Rte.ui.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/crossbrowser.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/font/tinymce.svg +0 -0
- /package/js/{rte → rte0}/old/Rte/font/tinymce.woff +0 -0
- /package/js/{rte → rte0}/old/Rte/helpers.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/htmlparser.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/index.mjs +0 -0
- /package/js/{rte → rte0}/old/Rte/main.css +0 -0
- /package/js/{rte → rte0}/old/old.rte.js +0 -0
- /package/js/{rte → rte0}/old/range-selection.old.js +0 -0
- /package/js/{rte → rte0}/src/fixes.js +0 -0
- /package/js/{rte → rte0}/src/rte.js +0 -0
- /package/js/{rte → rte0}/src/ui/items/ai.js +0 -0
- /package/js/{rte → rte0}/src/ui/items/code.js +0 -0
- /package/js/{rte → rte0}/src/ui/items/undo.js +0 -0
- /package/js/{rte → rte0}/src/util/htmlparser.js +0 -0
- /package/js/{rte → rte0}/src/util/indentHtml.js +0 -0
package/js/rte/rte.js
CHANGED
|
@@ -1,7 +1,58 @@
|
|
|
1
|
+
import {Editor} from './src/client/editor.js';
|
|
2
|
+
import {Rte} from './src/core/core.js';
|
|
1
3
|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
export {rangeRect} from './src/browser/range-rect.js';
|
|
5
|
+
export {Commands} from './src/command/commands.js';
|
|
6
|
+
export {BlockStyles} from './src/command/block-style.js';
|
|
7
|
+
export {deleteBackward, deleteForward} from './src/command/delete.js';
|
|
8
|
+
export {Edit} from './src/command/edit.js';
|
|
9
|
+
export {elementAttributes, selectedElement} from './src/command/element.js';
|
|
10
|
+
export {enter, lineBreak} from './src/command/enter.js';
|
|
11
|
+
export {blank, blockEdge, emptyBlock, fill} from './src/command/block-boundary.js';
|
|
12
|
+
export {insertFragment} from './src/command/fragment.js';
|
|
13
|
+
export {insertNode} from './src/command/insert.js';
|
|
14
|
+
export {Lists} from './src/command/list.js';
|
|
15
|
+
export {Tables} from './src/command/table.js';
|
|
16
|
+
export {applyMark, removeMark, setMarks, toggleMark, valueMark} from './src/command/mark.js';
|
|
17
|
+
export {PendingMarks} from './src/command/pending-marks.js';
|
|
18
|
+
export {unstyleCommand} from './src/command/unstyle.js';
|
|
19
|
+
export {config, elementPresets, enabled, hostDefaults, inlineUi} from './src/config/config.js';
|
|
20
|
+
export {Rte} from './src/core/core.js';
|
|
21
|
+
export {History} from './src/history/history.js';
|
|
22
|
+
export {ExternalInput} from './src/input/external-input.js';
|
|
23
|
+
export {InputPipeline, inputRange, inputTrigger} from './src/input/input-pipeline.js';
|
|
24
|
+
export {Editor} from './src/client/editor.js';
|
|
25
|
+
export {externalInputs, importLevel} from './src/client/external-input.js';
|
|
26
|
+
export {history} from './src/client/history.js';
|
|
27
|
+
export {marks} from './src/client/marks.js';
|
|
28
|
+
export {structure} from './src/client/structure.js';
|
|
29
|
+
export {classMark} from './src/mark/standard.js';
|
|
30
|
+
export {
|
|
31
|
+
bold, boldHtml,
|
|
32
|
+
code, codeHtml,
|
|
33
|
+
italic, italicHtml,
|
|
34
|
+
link, linkHtml,
|
|
35
|
+
strike, strikeHtml,
|
|
36
|
+
underline, underlineHtml,
|
|
37
|
+
} from './src/mark/standard.js';
|
|
38
|
+
export {MarkAdapter} from './src/mark/dom-adapter.js';
|
|
39
|
+
export {Mark, MarkType, markSet} from './src/mark/mark.js';
|
|
40
|
+
export {ContentModel} from './src/model/content-model.js';
|
|
41
|
+
export {createHtmlModel, htmlModel} from './src/model/html/html-model.js';
|
|
42
|
+
export {RepairPlanner} from './src/normalize/repair/repair-planner.js';
|
|
43
|
+
export {RepairExecutor} from './src/normalize/execute/repair-executor.js';
|
|
44
|
+
export {Normalizer} from './src/normalize/normalizer/normalizer.js';
|
|
45
|
+
export {EditRange} from './src/selection/range/edit-range.js';
|
|
46
|
+
export {PointMap} from './src/selection/map/point-map.js';
|
|
47
|
+
export {SelectionSnapshot} from './src/selection/snapshot.js';
|
|
48
|
+
export {Point} from './src/selection/point/point.js';
|
|
49
|
+
export {NativeSanitizer} from './src/sanitize/native.js';
|
|
50
|
+
export {SanitizePolicy, sanitizeDefaults, sanitizePolicy} from './src/sanitize/policy.js';
|
|
51
|
+
export {Source} from './src/source/source.js';
|
|
52
|
+
export {Surface} from './src/surface/surface.js';
|
|
53
|
+
export {Transaction} from './src/transaction/transaction.js';
|
|
54
|
+
export {Toolbar} from './src/ui/toolbar.js';
|
|
55
|
+
export {Unstyle, defaultUnstyle, defaultUnstyleLevels} from './src/unstyle/unstyle.js';
|
|
7
56
|
|
|
57
|
+
export const rte = new Rte(document);
|
|
58
|
+
export const editor = new Editor(rte);
|
package/js/rte/run.out
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
node:internal/modules/cjs/loader:1433
|
|
2
|
+
throw err;
|
|
3
|
+
^
|
|
4
|
+
|
|
5
|
+
Error: Cannot find module '/var/www/workplace/u2/js/rte/run.mjs'
|
|
6
|
+
at Function._resolveFilename (node:internal/modules/cjs/loader:1430:15)
|
|
7
|
+
at defaultResolveImpl (node:internal/modules/cjs/loader:1040:19)
|
|
8
|
+
at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1045:22)
|
|
9
|
+
at Function._load (node:internal/modules/cjs/loader:1216:25)
|
|
10
|
+
at wrapModuleLoad (node:internal/modules/cjs/loader:254:19)
|
|
11
|
+
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)
|
|
12
|
+
at node:internal/main/run_main_module:36:49 {
|
|
13
|
+
code: 'MODULE_NOT_FOUND',
|
|
14
|
+
requireStack: []
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Node.js v22.23.2
|
package/js/rte/source.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {editor} from './rte.js';
|
|
2
|
+
import {sourceView} from './src/client/source.js';
|
|
3
|
+
|
|
4
|
+
export {sourceView} from './src/client/source.js';
|
|
5
|
+
export {Source} from './src/source/source.js';
|
|
6
|
+
|
|
7
|
+
// Syntax highlighting is an enhancement: the dialog wraps its text area in
|
|
8
|
+
// <u2-code> and works without it. This entry loads that element on first use,
|
|
9
|
+
// so the composable `sourceView()` stays free of any dependency.
|
|
10
|
+
export const source = sourceView({highlight: () => import('../../el/code/code.js')});
|
|
11
|
+
|
|
12
|
+
editor.add(source);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Browser policies
|
|
2
|
+
|
|
3
|
+
Browser policies compensate for observable differences in native editing,
|
|
4
|
+
selection, and layout primitives. This directory is not a general `fixes.js`:
|
|
5
|
+
each policy has one named responsibility, no hidden global state, and a focused
|
|
6
|
+
regression suite. Semantic editing behavior remains in commands, normalization,
|
|
7
|
+
or UI rather than being mislabeled as a browser fix.
|
|
8
|
+
|
|
9
|
+
## Range geometry
|
|
10
|
+
|
|
11
|
+
`range-rect.js` exports `rangeRect(range, {root})`. A native rectangle with a
|
|
12
|
+
rendered size or a non-zero position is returned unchanged. When a collapsed
|
|
13
|
+
range reports an entirely zero rectangle, the
|
|
14
|
+
function derives its caret edge from following content, preceding content, or
|
|
15
|
+
the nearest rendered ancestor inside `root`, in that order.
|
|
16
|
+
|
|
17
|
+
- The DOM and live selection are never mutated; no temporary marker is inserted.
|
|
18
|
+
- Text is measured with a temporary native range over one adjacent character.
|
|
19
|
+
- Element boundaries use descendant content first and the element box second.
|
|
20
|
+
- Returned fallbacks have zero width at the inferred caret edge and retain the
|
|
21
|
+
measured line or element height.
|
|
22
|
+
- `root` is optional, but when supplied it both validates ownership and bounds
|
|
23
|
+
ancestor fallback.
|
|
24
|
+
- The implementation uses observed geometry, not browser names or user-agent
|
|
25
|
+
branches.
|
|
26
|
+
|
|
27
|
+
## Placement of future policies
|
|
28
|
+
|
|
29
|
+
A new file belongs here only when current engines expose different or unusable
|
|
30
|
+
native behavior below editor semantics. Content structure belongs to the model
|
|
31
|
+
and normalizer; key meaning belongs to commands/input; focus visibility belongs
|
|
32
|
+
to UI. Prefer feature detection and a narrow primitive that higher layers can
|
|
33
|
+
replace or omit.
|
|
34
|
+
|
|
35
|
+
## TODO
|
|
36
|
+
|
|
37
|
+
- Verify collapsed text, filler, replaced-element, vertical-writing, zoom, and
|
|
38
|
+
Shadow DOM geometry in current Chromium, Firefox, and WebKit.
|
|
39
|
+
- Add policy composition only after two independent policies need shared
|
|
40
|
+
installation or teardown; do not introduce a global fix registry in advance.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// A link, a button, or a label around an editing host — `<a><span
|
|
2
|
+
// contenteditable>` — is a host shape engines answer with the element instead of
|
|
3
|
+
// the text: the link is dragged rather than giving the text a caret and followed
|
|
4
|
+
// when the press ends, and the label hands the press to the control it names.
|
|
5
|
+
// All of it belongs to what surrounds the text, not to someone editing it.
|
|
6
|
+
//
|
|
7
|
+
// Only what surrounds the host is asked for: a link the content itself holds is
|
|
8
|
+
// content, and what a press does to that one is the editor's own business.
|
|
9
|
+
export function interactiveAround(host) {
|
|
10
|
+
return host?.closest('a[href], button, label') || null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// And a button answers Space and Enter by activating itself, whatever it holds:
|
|
14
|
+
// the text inside never sees those keys, and no `beforeinput` arrives at all.
|
|
15
|
+
export function activatingAround(host) {
|
|
16
|
+
return host?.closest('button') || null;
|
|
17
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Native collapsed ranges may report an empty rectangle. Derive the same caret
|
|
2
|
+
// boundary from adjacent rendered content without mutating DOM or selection.
|
|
3
|
+
export function rangeRect(range, {root = null} = {}) {
|
|
4
|
+
if (!range?.getBoundingClientRect || !range?.cloneRange) throw new TypeError('Range geometry requires a native Range');
|
|
5
|
+
if (root !== null && (typeof root?.contains !== 'function'
|
|
6
|
+
|| !contains(root, range.startContainer) || !contains(root, range.endContainer))) {
|
|
7
|
+
throw new RangeError('Range geometry root must contain the range');
|
|
8
|
+
}
|
|
9
|
+
const native = range.getBoundingClientRect();
|
|
10
|
+
if (!range.collapsed || located(native)) return native;
|
|
11
|
+
return after(range.startContainer, range.startOffset)
|
|
12
|
+
|| before(range.startContainer, range.startOffset)
|
|
13
|
+
|| ancestor(range.startContainer, root)
|
|
14
|
+
|| native;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function after(container, offset) {
|
|
18
|
+
if (container.nodeType === Node.TEXT_NODE) {
|
|
19
|
+
return offset < container.length ? textEdge(container, offset, false) : null;
|
|
20
|
+
}
|
|
21
|
+
const child = container.childNodes[offset];
|
|
22
|
+
return child ? nodeEdge(child, false) : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function before(container, offset) {
|
|
26
|
+
if (container.nodeType === Node.TEXT_NODE) {
|
|
27
|
+
return offset > 0 ? textEdge(container, offset - 1, true) : null;
|
|
28
|
+
}
|
|
29
|
+
const child = container.childNodes[offset - 1];
|
|
30
|
+
return child ? nodeEdge(child, true) : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function nodeEdge(node, end) {
|
|
34
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
35
|
+
if (!node.length) return null;
|
|
36
|
+
return textEdge(node, end ? node.length - 1 : 0, end);
|
|
37
|
+
}
|
|
38
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return null;
|
|
39
|
+
const child = node.childNodes[end ? node.childNodes.length - 1 : 0];
|
|
40
|
+
const nested = child && nodeEdge(child, end);
|
|
41
|
+
if (nested) return nested;
|
|
42
|
+
const rect = node.getBoundingClientRect();
|
|
43
|
+
return located(rect) ? edge(rect, end) : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function textEdge(node, offset, end) {
|
|
47
|
+
const range = node.ownerDocument.createRange();
|
|
48
|
+
range.setStart(node, offset);
|
|
49
|
+
range.setEnd(node, offset + 1);
|
|
50
|
+
const rect = range.getBoundingClientRect();
|
|
51
|
+
return located(rect) ? edge(rect, end) : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ancestor(node, root) {
|
|
55
|
+
let element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
|
56
|
+
while (element) {
|
|
57
|
+
const rect = element.getBoundingClientRect();
|
|
58
|
+
if (located(rect)) return edge(rect, false);
|
|
59
|
+
if (element === root) break;
|
|
60
|
+
element = element.parentElement;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function edge(rect, end) {
|
|
66
|
+
const left = end ? rect.right : rect.left;
|
|
67
|
+
const DOMRect = rect.constructor;
|
|
68
|
+
if (typeof DOMRect?.fromRect === 'function') {
|
|
69
|
+
return DOMRect.fromRect({x: left, y: rect.top, width: 0, height: rect.height});
|
|
70
|
+
}
|
|
71
|
+
return Object.freeze({x: left, y: rect.top, top: rect.top, right: left,
|
|
72
|
+
bottom: rect.bottom, left, width: 0, height: rect.height});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function located(rect) {
|
|
76
|
+
return !!rect && (rect.left !== 0 || rect.top !== 0 || rect.right !== 0 || rect.bottom !== 0
|
|
77
|
+
|| rect.width !== 0 || rect.height !== 0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function contains(root, node) {
|
|
81
|
+
return node === root || root.contains(node);
|
|
82
|
+
}
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
# Convention editor client
|
|
2
|
+
|
|
3
|
+
This is the deliberately small batteries-included layer above the RTE engine.
|
|
4
|
+
It proves the desired one-import setup without making its provisional module and
|
|
5
|
+
default-UI decisions part of the core.
|
|
6
|
+
|
|
7
|
+
The public root `rte.js` creates one `Editor` for the default document core:
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import './rte.js';
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```css
|
|
14
|
+
[contenteditable] {
|
|
15
|
+
--u2-rte: true;
|
|
16
|
+
--u2-rte-toolbar: bold;
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
No surface is discovered by observation. The core registers an opted-in host
|
|
21
|
+
when focus reaches it; the client then installs its standard command registry
|
|
22
|
+
and input pipeline. A default roaming toolbar is allocated only when the first
|
|
23
|
+
rich-text surface becomes active. `plaintext-only` hosts stay completely native.
|
|
24
|
+
|
|
25
|
+
The optional `classes.js` entry adds a content-class control. Its choices come
|
|
26
|
+
from the host's `--u2-rte-classes`, so its toolbar select is filled per surface
|
|
27
|
+
rather than at registration: a module control may declare `options` as a
|
|
28
|
+
function of the surface, and the client refills that select before the toolbar
|
|
29
|
+
is shown. The same declaration is what the sanitizer keeps and what
|
|
30
|
+
presentation cleanup leaves alone.
|
|
31
|
+
|
|
32
|
+
The optional `images.js` entry frames a selected image, with handles on its
|
|
33
|
+
trailing edges and its alt text in a field below it. Both come and go with the
|
|
34
|
+
selection and neither takes the focus by appearing, so naming an image is one
|
|
35
|
+
click away and nothing has to be opened. The name is written as it is typed, so
|
|
36
|
+
Enter has nothing to confirm: it hands the caret back to the text, after the
|
|
37
|
+
image.
|
|
38
|
+
|
|
39
|
+
The optional `link.js` entry adds a contextual link editor. Its `link` command
|
|
40
|
+
is an ordinary `valueMark`, so any other UI can drive the same create, change,
|
|
41
|
+
and remove path; the module supplies one form.
|
|
42
|
+
|
|
43
|
+
That form is where the caret is: it appears on its own when the caret enters a
|
|
44
|
+
link, anchored on the link rather than on the selection, and goes when the caret
|
|
45
|
+
leaves — the same rule the table and image handles follow. Appearing therefore
|
|
46
|
+
never takes the focus, because someone is typing. What is left for the toolbar
|
|
47
|
+
control is the one thing that is a decision: turning a selection into a link, and
|
|
48
|
+
at an existing link handing it the keyboard.
|
|
49
|
+
|
|
50
|
+
The form has no Apply and no Remove: what its fields say is what the link is, as
|
|
51
|
+
it is typed, and an emptied address says there is no link — the form stays on the
|
|
52
|
+
text, so the same words can be linked again.
|
|
53
|
+
Each edit runs the command marked as ongoing input, so history keeps a whole
|
|
54
|
+
address as one step rather than one per keystroke. Marking the link moves the
|
|
55
|
+
document selection into it and an engine follows that with focus, so the field
|
|
56
|
+
being typed into gets focus back with its caret intact — otherwise the second
|
|
57
|
+
character would land in the editor. What it edits is one link, decided when it
|
|
58
|
+
appeared — not wherever the caret has moved on to, or a pending edit would land
|
|
59
|
+
somewhere else the moment the caret did. Enter and Escape hand the caret back to
|
|
60
|
+
the text — after the link, not inside it, because whoever just made one wants to
|
|
61
|
+
keep writing without it. Neither dismisses the form: it goes because the caret
|
|
62
|
+
has left the link, and with every edit already applied one key has nothing left
|
|
63
|
+
to confirm and the other nothing to undo.
|
|
64
|
+
|
|
65
|
+
Beside the address sits a way to open it in a new tab, shown only while the
|
|
66
|
+
address is one the browser can follow: an application scheme — a page id, a
|
|
67
|
+
record reference — is a link the editor understands and the browser does not. The address is a plain text field on
|
|
68
|
+
purpose: native url validation rejects relative paths, fragments, and
|
|
69
|
+
application schemes and would silently block the form, while which protocols are
|
|
70
|
+
acceptable is the sanitizer's decision.
|
|
71
|
+
|
|
72
|
+
What an address means is the application's decision, and `linkEditor()` takes
|
|
73
|
+
two hooks for it. `normalize(value, surface)` receives the finished value
|
|
74
|
+
once, when the form is done, and may complete a bare domain, map an address into
|
|
75
|
+
a scheme of its own, or derive `rel` and `target` from it; what it returns is
|
|
76
|
+
what gets written and shown. Not per keystroke, which would rewrite a half-typed
|
|
77
|
+
address under the caret — and not when the field is left either: marking the link
|
|
78
|
+
takes the focus away and hands it back on every keystroke, so leaving a field is
|
|
79
|
+
not an event this form can wait for. `suggest(text, surface)` is asked, possibly
|
|
80
|
+
asynchronously, what a *new* link should point at, given the text it is being put
|
|
81
|
+
on; its answer is used only while the form is still open on the same link and
|
|
82
|
+
the address is still empty. It is offered rather than applied — it stands in the
|
|
83
|
+
field, selected so one keystroke replaces it, and becomes the link when the form
|
|
84
|
+
is done. Applying it at once would mark the link, and marking moves the selection
|
|
85
|
+
into it and the focus after that, throwing whoever asked for a link back into the
|
|
86
|
+
text before they had seen the answer.
|
|
87
|
+
|
|
88
|
+
`complete(text, surface)` is asked what addresses go with what is being typed and
|
|
89
|
+
answers with entries `{value, label}` — or `{value, html}` for a row with markup,
|
|
90
|
+
which the form writes through `setHTML()`, so the platform sanitizes it; where an
|
|
91
|
+
engine does not ship that yet, the entry shows its address instead. The form
|
|
92
|
+
renders its own list rather than a native datalist, which cannot show markup: the
|
|
93
|
+
arrow keys move through it, Enter takes the current entry, Escape closes the list
|
|
94
|
+
before it closes the form, and a pointer does the same. One question is in flight
|
|
95
|
+
at a time, and a late answer to a word that has since changed is dropped.
|
|
96
|
+
|
|
97
|
+
Contextual UI goes with the editing session and comes back with it: a module
|
|
98
|
+
draws on `u2-rte-activate` as well as on selection and content changes, because
|
|
99
|
+
returning to the field a caret was already in is no selection change and would
|
|
100
|
+
otherwise leave its handles or its form behind.
|
|
101
|
+
|
|
102
|
+
Everything this client draws goes into one `Chrome`: its toolbar, the contextual
|
|
103
|
+
handles, the link form, the source dialog. A module receives it as `chrome` in
|
|
104
|
+
its setup context and puts its UI in `chrome.root`, so no module owns a layer, a
|
|
105
|
+
top-layer element, or a place in the application's DOM. The chrome is made on
|
|
106
|
+
first use, so an editor that never shows anything adds nothing to the document,
|
|
107
|
+
and disposing the client takes all of it away at once.
|
|
108
|
+
When the active surface sits in a modal dialog, open popover, or fullscreen
|
|
109
|
+
element, the same chrome follows it into that native top-layer boundary and
|
|
110
|
+
returns afterward. The client registers the chrome as retained core UI, so its
|
|
111
|
+
controls keep activation and direct top-layer hosts keep it out of Source and
|
|
112
|
+
History content.
|
|
113
|
+
|
|
114
|
+
The optional `images.js` entry frames a selected image with three handles on its
|
|
115
|
+
trailing edges: the bottom-right corner keeps the proportion, the right edge
|
|
116
|
+
changes only the width and the bottom edge only the height. An image sits in a
|
|
117
|
+
text flow with its top and start edges held in place, so dragging the other side
|
|
118
|
+
is the only direction that grows it where the eye expects — a handle on a
|
|
119
|
+
leading edge would move the picture rather than resize it. A drag moves the
|
|
120
|
+
frame only and the size is written once when it is released, so a resize is one
|
|
121
|
+
undo step rather than a trail of them. Selecting the image that was clicked is
|
|
122
|
+
not its job: the input pipeline selects any atomic element a click lands on.
|
|
123
|
+
What is resizable is a selector, so any atomic element can be made sizeable.
|
|
124
|
+
|
|
125
|
+
The optional `tables.js` entry puts its actions on the table itself. Row handles
|
|
126
|
+
run down the table's left edge and column handles along its top, each lined up
|
|
127
|
+
with the cell the caret is in: add before, delete, add after. A handle whose
|
|
128
|
+
command cannot run is disabled rather than hidden, so the row of three keeps its
|
|
129
|
+
shape. Only `insertTable` is a toolbar control, because it applies where no
|
|
130
|
+
table is.
|
|
131
|
+
|
|
132
|
+
The layer is one per editor, lives in the browser top layer, and is repositioned
|
|
133
|
+
from selection, change, input, scroll, and resize — never from a timer.
|
|
134
|
+
|
|
135
|
+
The optional `source.js` entry adds a modal HTML source view. Its dialog wraps
|
|
136
|
+
its text area in `<u2-code>`: where that element is defined it takes the area
|
|
137
|
+
over and highlights it, and where it is not the area renders on its own.
|
|
138
|
+
Highlighting is therefore an enhancement, never a dependency — `sourceView()`
|
|
139
|
+
alone imports nothing, and the root entry passes a `highlight` loader that pulls
|
|
140
|
+
`u2/el/code` before the first open. Closing without applying restores the caret
|
|
141
|
+
the view was opened on.
|
|
142
|
+
|
|
143
|
+
## Prototype contract
|
|
144
|
+
|
|
145
|
+
`new Editor(core)` currently installs these defaults per rich-text surface:
|
|
146
|
+
|
|
147
|
+
- `enter` and `lineBreak` structural commands;
|
|
148
|
+
- `deleteBackward` and `deleteForward` for content-model-approved block joins
|
|
149
|
+
while ordinary character deletion stays native;
|
|
150
|
+
- one `PendingMarks` instance;
|
|
151
|
+
- `insertText` routing and post-composition formatting while a pending mark exists;
|
|
152
|
+
- one `History` instance with `undo` and `redo`, including Ctrl/Command+Z,
|
|
153
|
+
Ctrl/Command+Shift+Z, and Ctrl/Command+Y;
|
|
154
|
+
- the ready-made inline marks `bold`, `italic`, `underline`, `strike`, and
|
|
155
|
+
`code`, with Ctrl/Command+B, +I, and +U;
|
|
156
|
+
- the block structure commands `bullets`, `numbers`, `indent`, `outdent`, and
|
|
157
|
+
`rule`;
|
|
158
|
+
- one `InputPipeline` using the normal host configuration.
|
|
159
|
+
|
|
160
|
+
`commands(surface)` and `history(surface)` expose the per-surface registry and
|
|
161
|
+
history to an application; `controls` lists what the registered modules offer by
|
|
162
|
+
name, so a host can build its own list of what `--u2-rte-toolbar` may choose
|
|
163
|
+
from without guessing.
|
|
164
|
+
|
|
165
|
+
Those defaults arrive as three ordinary modules — `history`, `marks`, and
|
|
166
|
+
`structure`, one file each — registered before any surface exists. They use exactly the module
|
|
167
|
+
contract below and can be removed with `editor.delete('structure')`, so nothing
|
|
168
|
+
about them is privileged. Every control they contribute is gated by
|
|
169
|
+
`--u2-rte-toolbar`, so a host that lists only `bold` shows only Bold while the
|
|
170
|
+
commands and their keyboard shortcuts stay available.
|
|
171
|
+
|
|
172
|
+
`add(module)` installs an optional extension into already registered and future
|
|
173
|
+
rich-text surfaces. A module has one client-wide name and may provide a command
|
|
174
|
+
factory, lifecycle hooks, and declarative toolbar controls:
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
const highlight = {
|
|
178
|
+
name: 'highlight',
|
|
179
|
+
commands({pending}) {
|
|
180
|
+
return {highlight: pending.toggle(highlightHtml)};
|
|
181
|
+
},
|
|
182
|
+
toolbar: [{
|
|
183
|
+
command: 'highlight',
|
|
184
|
+
label: 'Highlight',
|
|
185
|
+
text: 'H',
|
|
186
|
+
state: true,
|
|
187
|
+
}],
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
editor.add(highlight);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The factory returns a plain object whose keys are command names and whose values
|
|
194
|
+
follow the normal `Commands` contract. `surface` is the target surface,
|
|
195
|
+
`pending` is its shared `PendingMarks` instance, so formatting modules compose
|
|
196
|
+
with the one `insertText` route rather than installing competing pipelines, and
|
|
197
|
+
`history` is its `History`.
|
|
198
|
+
`toolbar` is optional; its controls require non-empty `command`, `label`, and
|
|
199
|
+
`text`, with optional `state` and `shortcut`.
|
|
200
|
+
|
|
201
|
+
A module may instead contribute a value-bound select. Its `name` is the CSS
|
|
202
|
+
toolbar token, `command` is one normal command, and every option maps a label to
|
|
203
|
+
an `edit.value`:
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
toolbar: [{
|
|
207
|
+
type: 'select',
|
|
208
|
+
name: 'block',
|
|
209
|
+
command: 'blockStyle',
|
|
210
|
+
label: 'Block style',
|
|
211
|
+
options: [
|
|
212
|
+
{value: 'paragraph', label: 'Paragraph'},
|
|
213
|
+
{value: 'h1', label: 'Heading 1'},
|
|
214
|
+
],
|
|
215
|
+
}]
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The optional root `blocks.js` entry uses this contract for Paragraph, H1, H2,
|
|
219
|
+
and H3. `blockStyles(styles, options)` creates a replacement module from custom
|
|
220
|
+
definitions, including class-based styles such as `p.lead`. Importing
|
|
221
|
+
`blocks.js` also imports the convention editor and registers the default module.
|
|
222
|
+
|
|
223
|
+
Module names, command names, and routed `inputType` ownership are exclusive.
|
|
224
|
+
Adding the same module object twice is idempotent; a different module with the
|
|
225
|
+
same name or a command/input route already owned by the client throws. The
|
|
226
|
+
declaration and controls are snapshotted at registration, so later caller
|
|
227
|
+
mutation cannot change future surfaces. Installation into existing surfaces is
|
|
228
|
+
atomic. `delete()` accepts the registered module object or its name, removes its
|
|
229
|
+
commands and controls everywhere, and returns whether it existed. Changing
|
|
230
|
+
module topology clears transient pending caret marks so a removed formatter
|
|
231
|
+
cannot affect later input. A disposed client rejects new modules.
|
|
232
|
+
|
|
233
|
+
`setup({editor, core, root})` runs once when an extension is added.
|
|
234
|
+
`attach({editor, surface, pending, commands})` runs once for every current and
|
|
235
|
+
future rich-text surface, after that extension's commands are registered. Each
|
|
236
|
+
hook may return nothing or one object with `dispose()`. Surface disposal,
|
|
237
|
+
`delete()`, failed atomic installation, and editor disposal release those
|
|
238
|
+
objects in the reverse scope. There is deliberately no second `destroy()` or
|
|
239
|
+
cleanup-function convention.
|
|
240
|
+
|
|
241
|
+
This remains a narrow extension contract rather than a general plugin
|
|
242
|
+
framework: setup and attachment are synchronous, command/input ownership stays
|
|
243
|
+
exclusive, and an extension cannot reach private client state.
|
|
244
|
+
`commands(surface)` exposes that surface's registry or `null` for direct
|
|
245
|
+
inspection. `refresh()` re-reads the active surface's CSS and toolbar state.
|
|
246
|
+
|
|
247
|
+
The first active roaming surface creates one application-owned default toolbar
|
|
248
|
+
element and one small style element in the core root. `--u2-rte-toolbar` filters
|
|
249
|
+
its available controls through the normal `Toolbar` contract. Only `bold` is
|
|
250
|
+
shipped here yet; an unknown name such as `code` remains invisible rather than
|
|
251
|
+
pretending that its command exists. `--u2-rte-ui: none` prevents allocation
|
|
252
|
+
until another roaming surface becomes active. Leaving both the active surface
|
|
253
|
+
and toolbar hides it. Default placement uses the isolated `rangeRect()` browser
|
|
254
|
+
policy, so an empty native caret rectangle is derived from adjacent rendered
|
|
255
|
+
content without inserting a marker into the editor.
|
|
256
|
+
`--u2-rte-toolbar-when: selection` additionally keeps the toolbar hidden at a
|
|
257
|
+
caret and opens it only for a non-collapsed saved selection.
|
|
258
|
+
When Popover is available, the convention toolbar is a manual popover in the
|
|
259
|
+
browser top layer; its fixed range placement and focus-preserving behavior stay
|
|
260
|
+
the same inside a Document or ShadowRoot.
|
|
261
|
+
|
|
262
|
+
### Visible line breaks
|
|
263
|
+
|
|
264
|
+
The optional root `breaks.js` entry is the first lifecycle extension:
|
|
265
|
+
|
|
266
|
+
```js
|
|
267
|
+
import './breaks.js';
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
```css
|
|
271
|
+
[contenteditable] {
|
|
272
|
+
--u2-rte-show-breaks: true;
|
|
273
|
+
--u2-rte-toolbar: bold breaks;
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The CSS property chooses the initial view state when a surface connects.
|
|
278
|
+
Leaving `breaks` out of the toolbar list gives an always-on display with no
|
|
279
|
+
toggle. The `showBreaks` command runs outside an editing transaction, so it
|
|
280
|
+
emits a command event but no content-change event and never enters history.
|
|
281
|
+
|
|
282
|
+
The extension does not insert markers beside `<br>` nodes. One extension-owned
|
|
283
|
+
top-layer overlay contains presentation-only marks and is allocated only while
|
|
284
|
+
at least one surface shows them. A single root scroll listener, resize listener,
|
|
285
|
+
and `ResizeObserver` schedule all visible surfaces together; input and editor
|
|
286
|
+
changes schedule their own surface. Removing the extension restores its host
|
|
287
|
+
attributes, overlay, styles, observers, and listeners. Document and ShadowRoot
|
|
288
|
+
cores keep all of those resources in their own root. Nested editable surfaces
|
|
289
|
+
own their own `<br>` markers, so an outer surface never duplicates them.
|
|
290
|
+
|
|
291
|
+
### Optional Unstyle
|
|
292
|
+
|
|
293
|
+
The optional root `unstyle.js` entry adds a selection-only `unstyle` action:
|
|
294
|
+
|
|
295
|
+
```js
|
|
296
|
+
import './unstyle.js';
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
```css
|
|
300
|
+
[contenteditable] { --u2-rte-toolbar: bold unstyle; }
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Each click applies the first remaining presentation level. The button disables
|
|
304
|
+
when the selection has nothing covered by the policy. `unstyles(levels,
|
|
305
|
+
options)` creates a replacement module with application levels, command/control
|
|
306
|
+
names, label, and text. The underlying policy also cleans safely parsed
|
|
307
|
+
external fragments; the toolbar contains no paste-specific logic.
|
|
308
|
+
|
|
309
|
+
### Optional rich external input
|
|
310
|
+
|
|
311
|
+
`externalInputs(options)` adapts the low-level `ExternalInput` boundary to the
|
|
312
|
+
convention client's normal extension lifecycle. It is needed only when an
|
|
313
|
+
application chooses to read and replace rich HTML before native insertion. A
|
|
314
|
+
sanitizer is mandatory and is never selected implicitly:
|
|
315
|
+
|
|
316
|
+
```js
|
|
317
|
+
import {editor, externalInputs, NativeSanitizer} from '../../rte.js';
|
|
318
|
+
|
|
319
|
+
editor.add(externalInputs({sanitizer: new NativeSanitizer()}));
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
The module registers mapped `insertFragment` and attaches one input boundary to
|
|
323
|
+
every current and future rich surface. Removing it disposes the listeners and
|
|
324
|
+
command everywhere. It contributes no toolbar control.
|
|
325
|
+
|
|
326
|
+
With the default Unstyle policy, an inherited CSS property chooses cumulative
|
|
327
|
+
presentation cleanup for each import:
|
|
328
|
+
|
|
329
|
+
```css
|
|
330
|
+
[contenteditable] { --u2-rte-import-unstyle: styles; }
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
The value may be `none`, `classes`, `styles`, `attributes`, or `formatting`.
|
|
334
|
+
Custom `Unstyle` policies may use their own level names. Pass `unstyle: null`
|
|
335
|
+
to disable this stage, or pass `through` as a fixed name or resolver function
|
|
336
|
+
to replace CSS resolution. The security sanitizer still always runs.
|
|
337
|
+
|
|
338
|
+
`rte.js` exports the factory but does not install it by default. Its ordinary
|
|
339
|
+
input pipeline leaves paste/drop insertion to the browser, removes classes and
|
|
340
|
+
inline styles from only the added elements, and then normalizes their structure.
|
|
341
|
+
That path needs no HTML parser. Applications importing arbitrary HTML strings
|
|
342
|
+
may still provide any safe adapter through the same `sanitize()` contract.
|
|
343
|
+
|
|
344
|
+
`dispose()` removes every installed input pipeline, listener, toolbar node, and
|
|
345
|
+
style node without disposing the supplied core. It is idempotent and is also
|
|
346
|
+
available through `[Symbol.dispose]()`; core disposal disposes the client. One
|
|
347
|
+
core accepts only one convention client at a time, preventing duplicate input
|
|
348
|
+
pipelines. Disposal releases that ownership so a new client can take over.
|
|
349
|
+
|
|
350
|
+
## Invariants
|
|
351
|
+
|
|
352
|
+
- Importing the convention client adds only core listeners. It scans no DOM,
|
|
353
|
+
installs no observer, and creates no UI before a qualifying surface is active.
|
|
354
|
+
- Every mutable registry and pipeline belongs to one `Editor` instance and one
|
|
355
|
+
surface; different cores cannot share state.
|
|
356
|
+
- A module is installed once per rich surface, owns all command names and
|
|
357
|
+
controls it contributes, and is removed from all of them as one operation.
|
|
358
|
+
- Module installation either succeeds for every existing surface or leaves
|
|
359
|
+
every registry and input route unchanged.
|
|
360
|
+
- All editing still runs through public commands, transactions, point mapping,
|
|
361
|
+
normalization, and `Toolbar`. The client contains no second editor algorithm.
|
|
362
|
+
- The engine entry `rte.js` remains usable without importing this layer.
|
|
363
|
+
|
|
364
|
+
## Browser considerations
|
|
365
|
+
|
|
366
|
+
Lazy registration uses the core's existing capture-phase focus route. The UI
|
|
367
|
+
uses a manual top-layer popover with fixed positioning around the saved range,
|
|
368
|
+
with non-mutating
|
|
369
|
+
collapsed-range geometry fallback, and intentionally does not claim final
|
|
370
|
+
viewport, zoom, touch, writing-mode, or virtual-keyboard policy.
|
|
371
|
+
The real-browser suite covers lazy focus setup, Enter routing, toolbar commands,
|
|
372
|
+
late module installation, future surfaces, rollback, removal, plain-text
|
|
373
|
+
exclusion, rich external-input composition, and complete teardown.
|
|
374
|
+
|
|
375
|
+
## TODO exposed by the prototype
|
|
376
|
+
|
|
377
|
+
- Exercise lifecycle failure and coexistence further before adding dependency,
|
|
378
|
+
ordering, or asynchronous setup concepts.
|
|
379
|
+
- Decide whether the default UI becomes application DOM, a custom element, or
|
|
380
|
+
an optional UI module before stabilizing its styling hooks.
|
|
381
|
+
- Generalize placement for viewport edges, writing modes, zoom, touch handles,
|
|
382
|
+
and virtual keyboards.
|
|
383
|
+
- Decide separately whether `code` means inline `<code>` formatting or the old
|
|
384
|
+
editor's HTML-source UI; block styles now provide the first substantial
|
|
385
|
+
optional capability.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {BlockStyles} from '../command/block-style.js';
|
|
2
|
+
|
|
3
|
+
export const defaultBlockStyles = Object.freeze([
|
|
4
|
+
Object.freeze({name: 'paragraph', label: 'Paragraph', selector: 'p', tag: 'p'}),
|
|
5
|
+
Object.freeze({name: 'h1', label: 'Heading 1', selector: 'h1', tag: 'h1'}),
|
|
6
|
+
Object.freeze({name: 'h2', label: 'Heading 2', selector: 'h2', tag: 'h2'}),
|
|
7
|
+
Object.freeze({name: 'h3', label: 'Heading 3', selector: 'h3', tag: 'h3'}),
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
export function blockStyles(styles = defaultBlockStyles, {
|
|
11
|
+
name = 'blocks',
|
|
12
|
+
control = 'block',
|
|
13
|
+
command = 'blockStyle',
|
|
14
|
+
label = 'Block style',
|
|
15
|
+
} = {}) {
|
|
16
|
+
for (const [property, value] of Object.entries({name, control, command, label})) {
|
|
17
|
+
if (typeof value !== 'string' || !value.trim()) throw new TypeError(`Block module ${property} must be a non-empty string`);
|
|
18
|
+
}
|
|
19
|
+
const policy = new BlockStyles(styles);
|
|
20
|
+
const commands = Object.freeze({[command]: policy.command()});
|
|
21
|
+
const options = Object.freeze(policy.styles.map(style => Object.freeze({
|
|
22
|
+
value: style.name,
|
|
23
|
+
label: style.label,
|
|
24
|
+
})));
|
|
25
|
+
return Object.freeze({
|
|
26
|
+
name: name.trim(),
|
|
27
|
+
commands: () => commands,
|
|
28
|
+
toolbar: Object.freeze([Object.freeze({
|
|
29
|
+
type: 'select',
|
|
30
|
+
name: control.trim(),
|
|
31
|
+
command: command.trim(),
|
|
32
|
+
label,
|
|
33
|
+
options,
|
|
34
|
+
})]),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const blocks = blockStyles();
|