u2ui 1.5.3 → 1.5.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/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 +488 -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 +518 -0
- package/js/rte/playground/index.html +824 -0
- package/js/rte/rte.js +56 -5
- package/js/rte/source.js +12 -0
- package/js/rte/src/browser/README.md +40 -0
- package/js/rte/src/browser/range-rect.js +82 -0
- package/js/rte/src/client/README.md +378 -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 +461 -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 +275 -0
- package/js/rte/src/client/link.js +572 -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 +162 -0
- package/js/rte/src/client/unstyle.js +32 -0
- package/js/rte/src/command/README.md +517 -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 +158 -0
- package/js/rte/src/command/delete.js +112 -0
- package/js/rte/src/command/edit.js +72 -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 +127 -0
- package/js/rte/src/core/README.md +41 -0
- package/js/rte/src/core/core.js +156 -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 +155 -0
- package/js/rte/src/input/external-input.js +130 -0
- package/js/rte/src/input/input-pipeline.js +414 -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 +105 -0
- package/js/rte/src/sanitize/native.js +43 -0
- package/js/rte/src/sanitize/policy.js +202 -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 +255 -0
- package/js/rte/src/ui/chrome.js +214 -0
- package/js/rte/src/ui/handles.js +179 -0
- package/js/rte/src/ui/place.js +25 -0
- package/js/rte/src/ui/toolbar.js +283 -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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
`config.js` resolves inheritable CSS custom properties for one editable host.
|
|
4
|
+
Absent values and `auto` use semantic defaults derived from the host tag.
|
|
5
|
+
|
|
6
|
+
## Contract
|
|
7
|
+
|
|
8
|
+
- `enabled(host)` detects the opt-in property `--u2-rte`.
|
|
9
|
+
- `hostDefaults(host)` returns a shared immutable default for block and Enter
|
|
10
|
+
behavior.
|
|
11
|
+
- `config(host)` returns an immutable snapshot of the current computed settings.
|
|
12
|
+
- CSS is read on demand, so class changes and media/container queries can alter
|
|
13
|
+
behavior without recreating a surface.
|
|
14
|
+
- Unknown enum values fall back safely instead of becoming implicit modes.
|
|
15
|
+
- A block value is used only when it is a usable lowercase tag name, so a typo,
|
|
16
|
+
quoted value, or value list cannot reach `createElement()`.
|
|
17
|
+
|
|
18
|
+
Current properties:
|
|
19
|
+
|
|
20
|
+
| Property | Values | Default |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| `--u2-rte` | any truthy token | disabled |
|
|
23
|
+
| `--u2-rte-block` | tag name, `none`, `auto` | host-specific |
|
|
24
|
+
| `--u2-rte-enter` | `break`, `block`, `item`, `row`, `cell`, `auto` | host-specific |
|
|
25
|
+
| `--u2-rte-cleanup` | `none`, `minimal`, `structural`, `canonical` | `structural` |
|
|
26
|
+
| `--u2-rte-clean-on` | space- or comma-separated triggers | `input paste drop command` |
|
|
27
|
+
| `--u2-rte-elements` | tag list, `@basic`, `@article`, `@document`, `all` | `all` |
|
|
28
|
+
| `--u2-rte-import-unstyle` | `none` or an installed Unstyle level | `styles` |
|
|
29
|
+
| `--u2-rte-ui` | `none`, `roaming`, `static` | `roaming` |
|
|
30
|
+
| `--u2-rte-toolbar` | control names separated by spaces or commas | every represented control |
|
|
31
|
+
| `--u2-rte-toolbar-when` | `always`, `selection` | `always` |
|
|
32
|
+
|
|
33
|
+
`--u2-rte-enter` is consumed by the `enter` command: `break` inserts a line
|
|
34
|
+
break, every other value names the element Enter splits. `--u2-rte-ui: none`
|
|
35
|
+
hides a roaming `Toolbar`; `static` is reserved for the future static binding.
|
|
36
|
+
The convention client in `rte.js` currently represents only `bold`, so additional
|
|
37
|
+
toolbar names stay invisible until an optional module supplies both command and
|
|
38
|
+
control.
|
|
39
|
+
See [`../command/README.md`](../command/README.md) and
|
|
40
|
+
[`../ui/README.md`](../ui/README.md).
|
|
41
|
+
|
|
42
|
+
Semantic block defaults preserve native container shape: generic block hosts
|
|
43
|
+
such as `div` use `p`, lists use `li`, and table structure uses `tbody`, `tr`,
|
|
44
|
+
and `td`. A directly editable `li`, `caption`, `th`, or `td` keeps phrasing
|
|
45
|
+
content unwrapped and Enter inserts `br`. Any host can override this, including
|
|
46
|
+
opting those containers back into paragraphs with `--u2-rte-block: p` and
|
|
47
|
+
`--u2-rte-enter: block`.
|
|
48
|
+
|
|
49
|
+
Functions, command implementations, and policy objects are intentionally not
|
|
50
|
+
encoded as CSS strings. They enter through module registration.
|
|
51
|
+
|
|
52
|
+
`--u2-rte-elements` is a structural output policy shared by cleanup and
|
|
53
|
+
commands. An explicit list is application-specific; the frozen `basic`,
|
|
54
|
+
`article`, and `document` presets are exported as `elementPresets` and selected
|
|
55
|
+
in CSS with an `@` prefix:
|
|
56
|
+
|
|
57
|
+
```css
|
|
58
|
+
.article-editor {
|
|
59
|
+
--u2-rte-elements: @article;
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
An unknown preset or malformed list resolves to an empty allowlist rather than
|
|
64
|
+
silently broadening content. `all` retains the complete supplied content model.
|
|
65
|
+
The list controls elements only; it is deliberately not an attribute or URL
|
|
66
|
+
sanitizer.
|
|
67
|
+
|
|
68
|
+
`--u2-rte-import-unstyle` controls presentation cleanup for native paste and
|
|
69
|
+
drop. The default removes classes and inline styles only from elements added by
|
|
70
|
+
that input; `none` keeps native presentation. A custom `Unstyle` policy may
|
|
71
|
+
provide other level names.
|
|
72
|
+
|
|
73
|
+
`--u2-rte-classes` lists the class names the host treats as content. It is one
|
|
74
|
+
declaration with three consumers: the style control offers exactly those names,
|
|
75
|
+
the sanitizer keeps only those from external HTML, and presentation cleanup
|
|
76
|
+
leaves them and their wrappers alone. A host that declares none behaves exactly
|
|
77
|
+
as before.
|
|
78
|
+
|
|
79
|
+
`--u2-rte-import-elements` is what may *arrive*, a narrower question than
|
|
80
|
+
`--u2-rte-elements`, which is what a host tolerates in content it already owns.
|
|
81
|
+
It defaults to the `@content` preset: headings, text, lists, tables, media, and
|
|
82
|
+
the text-level semantics that carry meaning — no layout, no embeds, no forms.
|
|
83
|
+
The host list still bounds it; neither can widen the sanitize policy.
|
|
84
|
+
|
|
85
|
+
`--u2-rte-import-sanitize` switches that whole stage off with `none`, leaving
|
|
86
|
+
what the browser inserted. Which attributes and protocols are acceptable stays
|
|
87
|
+
in JavaScript with the policy — that is a security decision, not a presentation
|
|
88
|
+
one, and not something CSS should be carrying.
|
|
89
|
+
|
|
90
|
+
## TODO
|
|
91
|
+
|
|
92
|
+
- Add named sanitize-policy references when applications need CSS selection
|
|
93
|
+
among several registered JavaScript policies.
|
|
94
|
+
- Decide whether typed `@property` declarations improve debugging without
|
|
95
|
+
restricting inheritance or future values.
|
|
96
|
+
- Test container-query and adopted-style-sheet changes in integration suites.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const INLINE_HOSTS = new Set([
|
|
2
|
+
'A', 'ABBR', 'B', 'BUTTON', 'CODE', 'EM', 'H1', 'H2', 'H3', 'H4', 'H5',
|
|
3
|
+
'H6', 'I', 'KBD', 'LABEL', 'P', 'PRE', 'Q', 'SAMP', 'SMALL', 'SPAN',
|
|
4
|
+
'STRONG', 'SUB', 'SUP', 'TIME', 'U', 'VAR',
|
|
5
|
+
]);
|
|
6
|
+
const UNWRAPPED_HOSTS = new Set(['LI', 'CAPTION', 'TH', 'TD']);
|
|
7
|
+
const CLEANUP = new Set(['none', 'minimal', 'structural', 'canonical']);
|
|
8
|
+
const ENTER = new Set(['break', 'block', 'item', 'row', 'cell']);
|
|
9
|
+
const UI = new Set(['none', 'roaming', 'static']);
|
|
10
|
+
const IMPORT_SANITIZE = new Set(['policy', 'none']);
|
|
11
|
+
const TAG = /^[a-z][a-z\d-]*$/;
|
|
12
|
+
const CLASS = /^-?[_a-zA-Z][\w-]*$/;
|
|
13
|
+
const FALSE = new Set(['0', 'false', 'none', 'off']);
|
|
14
|
+
const DEFAULT_CLEAN_ON = Object.freeze(['input', 'paste', 'drop', 'command']);
|
|
15
|
+
const EMPTY_ELEMENTS = Object.freeze([]);
|
|
16
|
+
export const elementPresets = Object.freeze({
|
|
17
|
+
basic: elements('p ul ol li a strong b em i code br'),
|
|
18
|
+
// What a content field is about: headings, text, lists, tables, media and
|
|
19
|
+
// the text-level semantics that carry meaning. No layout, no embeds, no
|
|
20
|
+
// forms. This is the import default: what may arrive is a narrower question
|
|
21
|
+
// than what a host tolerates in content it owns.
|
|
22
|
+
content: elements(`
|
|
23
|
+
p h1 h2 h3 h4 h5 h6 blockquote pre ul ol li table caption thead tbody
|
|
24
|
+
tfoot tr th td hr img a strong em code span br
|
|
25
|
+
`),
|
|
26
|
+
article: elements('p h1 h2 h3 h4 h5 h6 blockquote pre ul ol li hr a strong b em i u s del ins code kbd mark q small sub sup span br'),
|
|
27
|
+
document: elements('p h1 h2 h3 h4 h5 h6 blockquote pre ul ol li dl dt dd hr figure figcaption img table caption thead tbody tfoot tr th td a strong b em i u s del ins code kbd mark q small sub sup span br'),
|
|
28
|
+
});
|
|
29
|
+
const DEFAULTS = Object.freeze({
|
|
30
|
+
block: Object.freeze({block: 'p', enter: 'block'}),
|
|
31
|
+
text: Object.freeze({block: null, enter: 'break'}),
|
|
32
|
+
list: Object.freeze({block: 'li', enter: 'item'}),
|
|
33
|
+
table: Object.freeze({block: 'tbody', enter: 'row'}),
|
|
34
|
+
row: Object.freeze({block: 'tr', enter: 'row'}),
|
|
35
|
+
cell: Object.freeze({block: 'td', enter: 'cell'}),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export function enabled(host) {
|
|
39
|
+
const value = getComputedStyle(host).getPropertyValue('--u2-rte').trim().toLowerCase();
|
|
40
|
+
return value !== '' && !FALSE.has(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function hostDefaults(host) {
|
|
44
|
+
const tag = host.tagName;
|
|
45
|
+
if (UNWRAPPED_HOSTS.has(tag)) return DEFAULTS.text;
|
|
46
|
+
if (tag === 'UL' || tag === 'OL') return DEFAULTS.list;
|
|
47
|
+
if (tag === 'TABLE') return DEFAULTS.table;
|
|
48
|
+
if (tag === 'THEAD' || tag === 'TBODY' || tag === 'TFOOT') return DEFAULTS.row;
|
|
49
|
+
if (tag === 'TR') return DEFAULTS.cell;
|
|
50
|
+
if (INLINE_HOSTS.has(tag)) return DEFAULTS.text;
|
|
51
|
+
return DEFAULTS.block;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function config(host) {
|
|
55
|
+
const style = getComputedStyle(host);
|
|
56
|
+
const defaults = hostDefaults(host);
|
|
57
|
+
const cleanOn = value(style, 'clean-on');
|
|
58
|
+
return Object.freeze({
|
|
59
|
+
block: tag(style, 'block', defaults.block),
|
|
60
|
+
enter: choice(style, 'enter', ENTER, defaults.enter),
|
|
61
|
+
cleanup: choice(style, 'cleanup', CLEANUP, 'structural'),
|
|
62
|
+
cleanOn: Object.freeze(cleanOn ? cleanOn.split(/[\s,]+/).filter(Boolean) : [...DEFAULT_CLEAN_ON]),
|
|
63
|
+
elements: allowedElements(style, 'elements', null),
|
|
64
|
+
importElements: allowedElements(style, 'import-elements', elementPresets.content),
|
|
65
|
+
classes: classNames(style),
|
|
66
|
+
// Foreign presentation through the class rung: pasted markup keeps no
|
|
67
|
+
// styles, no presentational attributes, and no undeclared classes.
|
|
68
|
+
importSanitize: choice(style, 'import-sanitize', IMPORT_SANITIZE, 'policy'),
|
|
69
|
+
importUnstyle: value(style, 'import-unstyle') || 'classes',
|
|
70
|
+
ui: choice(style, 'ui', UI, 'roaming'),
|
|
71
|
+
inlineUi: inlineNames(style),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Which contextual UIs a field draws at its content — `table image link`. Unset
|
|
76
|
+
// means every one the loaded modules bring, `none` means the field stays plain
|
|
77
|
+
// while keeping the commands. The same modules then serve a body of text and a
|
|
78
|
+
// bare teaser field without a second editor.
|
|
79
|
+
export function inlineUi(config, name) {
|
|
80
|
+
return !config.inlineUi || config.inlineUi.includes(name);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function inlineNames(style) {
|
|
84
|
+
const result = value(style, 'inline-ui').toLowerCase();
|
|
85
|
+
if (!result) return null;
|
|
86
|
+
if (result === 'none') return EMPTY_ELEMENTS;
|
|
87
|
+
return Object.freeze([...new Set(result.split(/[\s,]+/).filter(Boolean))]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function value(style, name) {
|
|
91
|
+
const value = style.getPropertyValue(`--u2-rte-${name}`).trim();
|
|
92
|
+
return value === 'auto' ? '' : value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function tag(style, name, fallback) {
|
|
96
|
+
const result = value(style, name).toLowerCase();
|
|
97
|
+
if (result === 'none') return null;
|
|
98
|
+
return TAG.test(result) ? result : fallback;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function choice(style, name, choices, fallback) {
|
|
102
|
+
const result = value(style, name);
|
|
103
|
+
return choices.has(result) ? result : fallback;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The class names this host treats as content. One declaration serves the style
|
|
107
|
+
// control, the sanitizer, and presentation cleanup, so a class the host knows is
|
|
108
|
+
// never offered without being allowed, or cleaned away as foreign.
|
|
109
|
+
function classNames(style) {
|
|
110
|
+
const result = value(style, 'classes');
|
|
111
|
+
if (!result) return EMPTY_ELEMENTS;
|
|
112
|
+
const names = result.split(/[\s,]+/).filter(Boolean);
|
|
113
|
+
return names.every(name => CLASS.test(name)) ? Object.freeze([...new Set(names)]) : EMPTY_ELEMENTS;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function allowedElements(style, name, fallback) {
|
|
117
|
+
const result = value(style, name).toLowerCase();
|
|
118
|
+
if (!result) return fallback;
|
|
119
|
+
if (result === 'all') return null;
|
|
120
|
+
if (result.startsWith('@')) return elementPresets[result.slice(1)] || EMPTY_ELEMENTS;
|
|
121
|
+
const names = result.split(/[\s,]+/).filter(Boolean);
|
|
122
|
+
return names.length && names.every(name => TAG.test(name)) ? Object.freeze([...new Set(names)]) : EMPTY_ELEMENTS;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function elements(value) {
|
|
126
|
+
return Object.freeze(value.trim().split(/\s+/));
|
|
127
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Core
|
|
2
|
+
|
|
3
|
+
The core is the shared coordinator for all editable elements in one document or
|
|
4
|
+
ShadowRoot. It listens for focus and selection changes once, knows which editor
|
|
5
|
+
is active, and routes work to that editor. It does not decide formatting,
|
|
6
|
+
document structure, or UI behavior.
|
|
7
|
+
|
|
8
|
+
`core.js` implements this coordinator and registers each editable element as a
|
|
9
|
+
`Surface`.
|
|
10
|
+
|
|
11
|
+
## Contract
|
|
12
|
+
|
|
13
|
+
- One core may coordinate any number of explicit editable surfaces.
|
|
14
|
+
- `add()` is idempotent and rejects non-editable or foreign-root elements.
|
|
15
|
+
- With `auto` enabled, focusing an element opted in through `--u2-rte` registers
|
|
16
|
+
it lazily. Explicit registration does not require that CSS property.
|
|
17
|
+
- Capture-phase `focus` and `focusin` share one idempotent route to tolerate
|
|
18
|
+
browser and document-lifecycle differences without duplicate activation.
|
|
19
|
+
- The nearest explicit nested `contenteditable` is an isolation boundary.
|
|
20
|
+
- `sync()` resolves the current selection to one registered surface and captures
|
|
21
|
+
it without scanning or mutating unrelated editor contents.
|
|
22
|
+
- At most one surface is active per core; active state may outlive DOM focus so
|
|
23
|
+
a roaming UI can safely receive focus.
|
|
24
|
+
- `retain(element)` marks editor-owned UI in the same document. Focus inside it
|
|
25
|
+
keeps the active surface, `retains(node)` identifies its composed subtree,
|
|
26
|
+
and `release(element)` removes the mark. This also distinguishes UI from
|
|
27
|
+
content when a native top-layer boundary requires both to share one host.
|
|
28
|
+
- Focus that lands anywhere else — not a surface, not retained UI — ends the
|
|
29
|
+
session, so everything drawn for it goes at once. Retaining is therefore how a
|
|
30
|
+
UI that takes focus stays alive; `Toolbar` retains its element itself.
|
|
31
|
+
- `dispose()` removes listeners and disconnects every surface. It is
|
|
32
|
+
idempotent; `[Symbol.dispose]()` exposes the same teardown to `using`.
|
|
33
|
+
|
|
34
|
+
Separate cores are supported for iframes, ShadowRoots, tests, and intentionally
|
|
35
|
+
isolated environments. The public entry point creates one default document core.
|
|
36
|
+
|
|
37
|
+
## TODO
|
|
38
|
+
|
|
39
|
+
- Observe removed auto-registered hosts without watching their contents.
|
|
40
|
+
- Complete cross-browser ShadowRoot selection routing.
|
|
41
|
+
- Add module lifecycle registration at the core boundary.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {enabled} from '../config/config.js';
|
|
2
|
+
import {editingHost, isEditableHost, isEditingBoundary} from '../selection/ownership/ownership.js';
|
|
3
|
+
import {Surface} from '../surface/surface.js';
|
|
4
|
+
|
|
5
|
+
export class Rte extends EventTarget {
|
|
6
|
+
#root;
|
|
7
|
+
#document;
|
|
8
|
+
#options;
|
|
9
|
+
#surfaces = new Set();
|
|
10
|
+
#elements = new WeakMap();
|
|
11
|
+
#retained = new Set();
|
|
12
|
+
#active = null;
|
|
13
|
+
#controller;
|
|
14
|
+
|
|
15
|
+
constructor(root = document, options = {}) {
|
|
16
|
+
super();
|
|
17
|
+
const isDocument = root?.nodeType === Node.DOCUMENT_NODE;
|
|
18
|
+
const isShadow = root?.nodeType === Node.DOCUMENT_FRAGMENT_NODE && root.host;
|
|
19
|
+
if (!isDocument && !isShadow) throw new TypeError('RTE root must be a Document or ShadowRoot');
|
|
20
|
+
this.#root = root;
|
|
21
|
+
this.#document = isDocument ? root : root.ownerDocument;
|
|
22
|
+
this.#options = Object.freeze({auto: options.auto !== false});
|
|
23
|
+
this.#controller = new this.#document.defaultView.AbortController();
|
|
24
|
+
const listen = {capture: true, signal: this.#controller.signal};
|
|
25
|
+
root.addEventListener('focus', this.#focus, listen);
|
|
26
|
+
root.addEventListener('focusin', this.#focus, listen);
|
|
27
|
+
this.#document.addEventListener('selectionchange', this.#selectionChange, {signal: this.#controller.signal});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get root() { return this.#root; }
|
|
31
|
+
get options() { return this.#options; }
|
|
32
|
+
get active() { return this.#active; }
|
|
33
|
+
get surfaces() { return [...this.#surfaces]; }
|
|
34
|
+
get selection() { return this.#root.getSelection?.() || this.#document.getSelection(); }
|
|
35
|
+
|
|
36
|
+
// Retained UI may receive focus without deactivating the current surface.
|
|
37
|
+
// When it temporarily has to live inside a top-layer editing host, content
|
|
38
|
+
// readers also use this boundary to leave it out of the document.
|
|
39
|
+
retain(element) {
|
|
40
|
+
if (element?.nodeType !== Node.ELEMENT_NODE) throw new TypeError('Retained UI must be an element');
|
|
41
|
+
if (element.ownerDocument !== this.#document) throw new RangeError('Retained UI must belong to the core document');
|
|
42
|
+
this.#retained.add(element);
|
|
43
|
+
return element;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
release(element) {
|
|
47
|
+
return this.#retained.delete(element);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
retains(node) {
|
|
51
|
+
for (let current = node; current;) {
|
|
52
|
+
if (this.#retained.has(current)) return true;
|
|
53
|
+
const root = current.getRootNode?.();
|
|
54
|
+
current = current.parentNode || root?.host || null;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
add(element, options) {
|
|
60
|
+
if (!isEditableHost(element)) {
|
|
61
|
+
throw new TypeError('RTE surfaces must be explicit contenteditable elements');
|
|
62
|
+
}
|
|
63
|
+
if (element.getRootNode() !== this.#root) throw new RangeError('RTE surface must belong to the core root');
|
|
64
|
+
const current = this.#elements.get(element);
|
|
65
|
+
if (current) return current;
|
|
66
|
+
const surface = new Surface(this, element, options);
|
|
67
|
+
this.#elements.set(element, surface);
|
|
68
|
+
this.#surfaces.add(surface);
|
|
69
|
+
this.#emit('u2-rte-add', {surface});
|
|
70
|
+
return surface;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get(element) {
|
|
74
|
+
return this.#elements.get(element) || null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
delete(target) {
|
|
78
|
+
const surface = target instanceof Surface ? target : this.#elements.get(target);
|
|
79
|
+
if (!surface || !this.#surfaces.has(surface)) return false;
|
|
80
|
+
if (this.#active === surface) this.activate(null);
|
|
81
|
+
this.#surfaces.delete(surface);
|
|
82
|
+
this.#elements.delete(surface.element);
|
|
83
|
+
surface.disconnect();
|
|
84
|
+
this.#emit('u2-rte-delete', {surface});
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
activate(surface) {
|
|
89
|
+
if (surface && !this.#surfaces.has(surface)) throw new RangeError('Active surface must belong to this core');
|
|
90
|
+
if (surface === this.#active) return surface;
|
|
91
|
+
this.#active?.activate(false);
|
|
92
|
+
this.#active = surface;
|
|
93
|
+
surface?.activate();
|
|
94
|
+
this.#emit('u2-rte-activechange', {surface});
|
|
95
|
+
return surface;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
sync() {
|
|
99
|
+
const selection = this.selection;
|
|
100
|
+
const editable = editingHost(selection?.anchorNode);
|
|
101
|
+
if (!editable) return null;
|
|
102
|
+
const surface = this.#elements.get(editable);
|
|
103
|
+
if (!surface || !selection.focusNode || !surface.element.contains(selection.focusNode)) {
|
|
104
|
+
this.activate(null);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
if (!surface.capture(selection)) {
|
|
108
|
+
this.activate(null);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
this.activate(surface);
|
|
112
|
+
return surface;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
dispose() {
|
|
116
|
+
if (this.#controller.signal.aborted) return;
|
|
117
|
+
this.#controller.abort();
|
|
118
|
+
for (const surface of [...this.#surfaces]) this.delete(surface);
|
|
119
|
+
this.#retained.clear();
|
|
120
|
+
this.#emit('u2-rte-dispose');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
[Symbol.dispose]() {
|
|
124
|
+
this.dispose();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#focus = event => {
|
|
128
|
+
if (event.composedPath().some(node => this.#retained.has(node))) return;
|
|
129
|
+
const editable = event.composedPath().find(node =>
|
|
130
|
+
node?.nodeType === Node.ELEMENT_NODE && isEditingBoundary(node)
|
|
131
|
+
);
|
|
132
|
+
// Focus that went somewhere else entirely ends the session: retained UI
|
|
133
|
+
// is already excluded above, so what is left is not the editor's, and
|
|
134
|
+
// everything drawn for it has to go with it.
|
|
135
|
+
if (!editable) {
|
|
136
|
+
this.activate(null);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!isEditableHost(editable)) {
|
|
140
|
+
this.activate(null);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
let surface = this.#elements.get(editable);
|
|
144
|
+
if (!surface && this.#options.auto && enabled(editable)) surface = this.add(editable);
|
|
145
|
+
this.activate(surface || null);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
#selectionChange = () => {
|
|
149
|
+
this.sync();
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
#emit(type, detail = {}) {
|
|
153
|
+
const Event = this.#document.defaultView.CustomEvent;
|
|
154
|
+
return this.dispatchEvent(new Event(type, {detail: {core: this, ...detail}}));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# History
|
|
2
|
+
|
|
3
|
+
Undo and redo for one surface.
|
|
4
|
+
|
|
5
|
+
RTE deliberately leaves ordinary typing and deletion to the browser, so history
|
|
6
|
+
cannot be an operation log: it would only ever see the edits the engine itself
|
|
7
|
+
performs. `history.js` therefore records *states*. A `MutationObserver` reports
|
|
8
|
+
every change regardless of its origin — native input, a command, paste, drop, or
|
|
9
|
+
an unrelated application script — and an entry stores the resulting content as a
|
|
10
|
+
cloned `DocumentFragment` with a path-addressed selection.
|
|
11
|
+
|
|
12
|
+
Because entries are complete states, undo never has to reverse a mutation it did
|
|
13
|
+
not understand. That is what makes a mixed native/engine editing model undoable
|
|
14
|
+
at all.
|
|
15
|
+
|
|
16
|
+
## Contract
|
|
17
|
+
|
|
18
|
+
- `new History(surface, {limit = 100, coalesce = 400})` starts with the current
|
|
19
|
+
content as its baseline entry and observes the surface from that moment.
|
|
20
|
+
- `record()` commits the current content as one entry and reports whether it
|
|
21
|
+
added one. Unchanged content adds nothing, so any boundary may flush safely.
|
|
22
|
+
- `undo()` and `redo()` move one entry and report whether they moved. Both
|
|
23
|
+
commit uncommitted input first, so undo leaves the state the user sees.
|
|
24
|
+
- `canUndo` and `canRedo` account for uncommitted input: pending input is
|
|
25
|
+
undoable, and it invalidates a redo branch before it is recorded.
|
|
26
|
+
- `commands` is the frozen `{undo, redo}` pair for a `Commands` registry. They
|
|
27
|
+
claim the `historyUndo` and `historyRedo` input types and run without a
|
|
28
|
+
transaction, because an entry replaces the content and restores the selection
|
|
29
|
+
itself. Like every other command they require the surface to own the edit's
|
|
30
|
+
range, so a shortcut never reaches an editor the user has left; a toolbar
|
|
31
|
+
that still holds a saved selection passes it and keeps working.
|
|
32
|
+
- The methods are unconditional: `undo()` and `redo()` are the programmatic API
|
|
33
|
+
and do not consult the selection.
|
|
34
|
+
- `clear()` discards every entry and restarts from the current content.
|
|
35
|
+
- Recording a new state drops the redo branch. Exceeding `limit` drops the
|
|
36
|
+
oldest entry.
|
|
37
|
+
- `u2-rte-history` reports every entry and every move on the surface.
|
|
38
|
+
- `dispose()` stops observing and releases every entry. A disconnected surface
|
|
39
|
+
disposes its history.
|
|
40
|
+
- Core-retained UI mutations are not edits. Snapshots omit those subtrees and
|
|
41
|
+
undo/redo preserves them, including when shared chrome is temporarily mounted
|
|
42
|
+
directly in a top-layer editing host.
|
|
43
|
+
|
|
44
|
+
## Grouping
|
|
45
|
+
|
|
46
|
+
Transactions supply the boundaries, so grouping needs no separate timer policy:
|
|
47
|
+
|
|
48
|
+
- A transaction with the `input` trigger is ordinary typing and coalesces.
|
|
49
|
+
- Every other transaction — `command`, `paste`, `drop` — records one entry
|
|
50
|
+
before and one after itself, so a command is always its own undo step.
|
|
51
|
+
- Native input that never reaches a transaction coalesces through the observer.
|
|
52
|
+
- Deactivating the surface flushes.
|
|
53
|
+
|
|
54
|
+
Coalescing keeps one open interval instead of restarting it on every keystroke,
|
|
55
|
+
so continuous typing still becomes several undo steps rather than one.
|
|
56
|
+
|
|
57
|
+
## Selection
|
|
58
|
+
|
|
59
|
+
`SelectionSnapshot` holds live nodes and cannot survive replaced content. An
|
|
60
|
+
entry therefore stores each boundary as a child-index path from the surface
|
|
61
|
+
element plus an offset, and rebuilds a `SelectionSnapshot` on restore so
|
|
62
|
+
backward selections keep their direction. An entry captured before the surface
|
|
63
|
+
had a selection adopts the first one it sees, so undoing back to the baseline
|
|
64
|
+
restores a caret too.
|
|
65
|
+
|
|
66
|
+
## Browser considerations
|
|
67
|
+
|
|
68
|
+
- Ctrl/Command+Z and +Y are routed from `keydown` by the input pipeline, not
|
|
69
|
+
from `beforeinput`. A browser stops reporting `historyUndo` once its own undo
|
|
70
|
+
stack no longer matches content the engine replaced. The `inputTypes` claim
|
|
71
|
+
remains so that a menu- or gesture-triggered undo is captured as well.
|
|
72
|
+
- Restoring content takes the observer's own records so an applied entry can
|
|
73
|
+
never record itself.
|
|
74
|
+
|
|
75
|
+
## TODO
|
|
76
|
+
|
|
77
|
+
- Expose the coalescing interval and an off switch as `--u2-rte-history`.
|
|
78
|
+
- Store diffs instead of whole fragments once the transaction layer records
|
|
79
|
+
reversible operations, and keep this state model as the fallback for
|
|
80
|
+
mutations the engine does not own.
|
|
81
|
+
- Group composition deliberately instead of relying on its mutations alone.
|
|
82
|
+
- Verify entry cost on large documents and decide whether `limit` should be
|
|
83
|
+
measured in bytes rather than entries.
|