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.
Files changed (146) hide show
  1. package/el/ico/ico.js +5 -5
  2. package/el/rte/README.md +53 -13
  3. package/el/rte/manifest.json +19 -4
  4. package/el/rte/rte.css +31 -6
  5. package/el/rte/rte.js +174 -44
  6. package/el/rte0/README.md +37 -0
  7. package/el/rte0/manifest.json +33 -0
  8. package/el/rte0/rte0.css +9 -0
  9. package/el/rte0/rte0.js +56 -0
  10. package/el/system/styler.js +2 -0
  11. package/el/tooltip/tooltip.css +2 -2
  12. package/js/Placer/Placer.js +2 -1
  13. package/js/rte/AGENTS.md +142 -0
  14. package/js/rte/PLAN.md +487 -0
  15. package/js/rte/README.md +319 -25
  16. package/js/rte/blocks.js +7 -0
  17. package/js/rte/breaks.js +6 -0
  18. package/js/rte/classes.js +7 -0
  19. package/js/rte/docs/usage.md +845 -0
  20. package/js/rte/images.js +7 -0
  21. package/js/rte/link.js +8 -0
  22. package/js/rte/playground/README.md +95 -0
  23. package/js/rte/playground/hosts.html +553 -0
  24. package/js/rte/playground/index.html +835 -0
  25. package/js/rte/rte.js +56 -5
  26. package/js/rte/run.out +17 -0
  27. package/js/rte/source.js +12 -0
  28. package/js/rte/src/browser/README.md +40 -0
  29. package/js/rte/src/browser/interactive.js +17 -0
  30. package/js/rte/src/browser/range-rect.js +82 -0
  31. package/js/rte/src/client/README.md +385 -0
  32. package/js/rte/src/client/blocks.js +38 -0
  33. package/js/rte/src/client/breaks.js +212 -0
  34. package/js/rte/src/client/classes.js +48 -0
  35. package/js/rte/src/client/editor.js +472 -0
  36. package/js/rte/src/client/external-input.js +47 -0
  37. package/js/rte/src/client/history.js +13 -0
  38. package/js/rte/src/client/images.js +285 -0
  39. package/js/rte/src/client/link.js +567 -0
  40. package/js/rte/src/client/marks.js +25 -0
  41. package/js/rte/src/client/source.js +182 -0
  42. package/js/rte/src/client/structure.js +27 -0
  43. package/js/rte/src/client/tables.js +165 -0
  44. package/js/rte/src/client/unstyle.js +32 -0
  45. package/js/rte/src/command/README.md +520 -0
  46. package/js/rte/src/command/block-boundary.js +43 -0
  47. package/js/rte/src/command/block-style.js +120 -0
  48. package/js/rte/src/command/commands.js +159 -0
  49. package/js/rte/src/command/delete.js +119 -0
  50. package/js/rte/src/command/edit.js +78 -0
  51. package/js/rte/src/command/element.js +45 -0
  52. package/js/rte/src/command/enter.js +175 -0
  53. package/js/rte/src/command/fragment.js +76 -0
  54. package/js/rte/src/command/insert.js +52 -0
  55. package/js/rte/src/command/list.js +284 -0
  56. package/js/rte/src/command/mark.js +583 -0
  57. package/js/rte/src/command/pending-marks.js +216 -0
  58. package/js/rte/src/command/table.js +207 -0
  59. package/js/rte/src/command/unstyle.js +248 -0
  60. package/js/rte/src/config/README.md +96 -0
  61. package/js/rte/src/config/config.js +134 -0
  62. package/js/rte/src/core/README.md +63 -0
  63. package/js/rte/src/core/core.js +247 -0
  64. package/js/rte/src/history/README.md +83 -0
  65. package/js/rte/src/history/history.js +283 -0
  66. package/js/rte/src/input/README.md +173 -0
  67. package/js/rte/src/input/external-input.js +130 -0
  68. package/js/rte/src/input/input-pipeline.js +459 -0
  69. package/js/rte/src/mark/README.md +219 -0
  70. package/js/rte/src/mark/dom-adapter.js +100 -0
  71. package/js/rte/src/mark/mark.js +133 -0
  72. package/js/rte/src/mark/standard.js +76 -0
  73. package/js/rte/src/model/README.md +71 -0
  74. package/js/rte/src/model/content-model.js +213 -0
  75. package/js/rte/src/model/html/README.md +51 -0
  76. package/js/rte/src/model/html/html-model.js +90 -0
  77. package/js/rte/src/normalize/execute/README.md +33 -0
  78. package/js/rte/src/normalize/execute/repair-executor.js +129 -0
  79. package/js/rte/src/normalize/normalizer/README.md +48 -0
  80. package/js/rte/src/normalize/normalizer/normalizer.js +141 -0
  81. package/js/rte/src/normalize/repair/README.md +70 -0
  82. package/js/rte/src/normalize/repair/repair-planner.js +208 -0
  83. package/js/rte/src/sanitize/README.md +108 -0
  84. package/js/rte/src/sanitize/native.js +43 -0
  85. package/js/rte/src/sanitize/policy.js +206 -0
  86. package/js/rte/src/selection/README.md +39 -0
  87. package/js/rte/src/selection/map/README.md +53 -0
  88. package/js/rte/src/selection/map/point-map.js +307 -0
  89. package/js/rte/src/selection/ownership/README.md +32 -0
  90. package/js/rte/src/selection/ownership/ownership.js +42 -0
  91. package/js/rte/src/selection/point/README.md +34 -0
  92. package/js/rte/src/selection/point/point.js +83 -0
  93. package/js/rte/src/selection/range/README.md +38 -0
  94. package/js/rte/src/selection/range/edit-range.js +223 -0
  95. package/js/rte/src/selection/snapshot.js +64 -0
  96. package/js/rte/src/source/README.md +64 -0
  97. package/js/rte/src/source/source.js +193 -0
  98. package/js/rte/src/surface/README.md +36 -0
  99. package/js/rte/src/surface/content.js +16 -0
  100. package/js/rte/src/surface/surface.js +87 -0
  101. package/js/rte/src/transaction/README.md +35 -0
  102. package/js/rte/src/transaction/transaction.js +56 -0
  103. package/js/rte/src/ui/README.md +271 -0
  104. package/js/rte/src/ui/caret.js +17 -0
  105. package/js/rte/src/ui/chrome.js +214 -0
  106. package/js/rte/src/ui/handles.js +179 -0
  107. package/js/rte/src/ui/highlight.js +86 -0
  108. package/js/rte/src/ui/place.js +25 -0
  109. package/js/rte/src/ui/toolbar.js +258 -0
  110. package/js/rte/src/unstyle/README.md +103 -0
  111. package/js/rte/src/unstyle/unstyle.js +140 -0
  112. package/js/rte/tables.js +7 -0
  113. package/js/rte/unstyle.js +8 -0
  114. package/js/rte0/README.md +37 -0
  115. package/js/rte0/rte.js +7 -0
  116. package/js/{rte → rte0}/src/behavior.js +0 -1
  117. package/js/{rte → rte0}/src/events.js +0 -1
  118. package/js/{rte → rte0}/src/range.js +0 -2
  119. package/js/{rte → rte0}/src/ui.items.js +0 -2
  120. package/js/{rte → rte0}/src/ui.js +0 -1
  121. package/package.json +1 -1
  122. package/u2/Element.js +2 -2
  123. package/u2/plan-scoped-refactoring.md +133 -303
  124. package/u2/projects.json +18 -2
  125. package/u2/tools/update.repos.json.js +1 -1
  126. package/el/qrcode/package.json +0 -22
  127. /package/js/{rte → rte0}/old/NodeCleaner.js +0 -0
  128. /package/js/{rte → rte0}/old/Rte/Rte.mjs +0 -0
  129. /package/js/{rte → rte0}/old/Rte/Rte.ui.items.mjs +0 -0
  130. /package/js/{rte → rte0}/old/Rte/Rte.ui.mjs +0 -0
  131. /package/js/{rte → rte0}/old/Rte/crossbrowser.mjs +0 -0
  132. /package/js/{rte → rte0}/old/Rte/font/tinymce.svg +0 -0
  133. /package/js/{rte → rte0}/old/Rte/font/tinymce.woff +0 -0
  134. /package/js/{rte → rte0}/old/Rte/helpers.mjs +0 -0
  135. /package/js/{rte → rte0}/old/Rte/htmlparser.mjs +0 -0
  136. /package/js/{rte → rte0}/old/Rte/index.mjs +0 -0
  137. /package/js/{rte → rte0}/old/Rte/main.css +0 -0
  138. /package/js/{rte → rte0}/old/old.rte.js +0 -0
  139. /package/js/{rte → rte0}/old/range-selection.old.js +0 -0
  140. /package/js/{rte → rte0}/src/fixes.js +0 -0
  141. /package/js/{rte → rte0}/src/rte.js +0 -0
  142. /package/js/{rte → rte0}/src/ui/items/ai.js +0 -0
  143. /package/js/{rte → rte0}/src/ui/items/code.js +0 -0
  144. /package/js/{rte → rte0}/src/ui/items/undo.js +0 -0
  145. /package/js/{rte → rte0}/src/util/htmlparser.js +0 -0
  146. /package/js/{rte → rte0}/src/util/indentHtml.js +0 -0
@@ -0,0 +1,182 @@
1
+ import {Source} from '../source/source.js';
2
+
3
+ const STYLE = `
4
+ #source {
5
+ pointer-events: auto;
6
+ block-size: min(70vh, 45.71em);
7
+ border: 1px solid;
8
+ inline-size: min(90vw, 68.57em);
9
+ max-block-size: none;
10
+ max-inline-size: none;
11
+ padding: 0;
12
+ }
13
+ #source form {
14
+ block-size: 100%;
15
+ box-sizing: border-box;
16
+ display: flex;
17
+ flex-direction: column;
18
+ gap: .57em;
19
+ margin: 0;
20
+ padding: .57em;
21
+ }
22
+ #source u2-code {
23
+ display: flex;
24
+ flex: 1;
25
+ flex-direction: column;
26
+ font-family: monospace;
27
+ min-block-size: 0;
28
+ overflow: auto;
29
+ }
30
+ #source textarea {
31
+ box-sizing: border-box;
32
+ flex: 1;
33
+ font-family: monospace;
34
+ inline-size: 100%;
35
+ resize: none;
36
+ white-space: pre;
37
+ }
38
+ #source menu {
39
+ display: flex;
40
+ gap: .57em;
41
+ justify-content: flex-end;
42
+ margin: 0;
43
+ padding: 0;
44
+ }
45
+ `;
46
+
47
+ // Optional source view. One dialog per editor is shared by every surface: it is
48
+ // modal, so only one can be open, and its lifetime belongs to the editor rather
49
+ // than to a surface that may be replaced while it is open.
50
+ //
51
+ // The text area is always wrapped in `<u2-code>`. Where that element is defined
52
+ // it takes the textarea over and highlights it; where it is not, the textarea
53
+ // renders on its own. Highlighting is therefore an enhancement, never a
54
+ // dependency. `highlight` is an optional loader called before the first open.
55
+ export function sourceView({highlight = null, ...options} = {}) {
56
+ if (highlight !== null && typeof highlight !== 'function') {
57
+ throw new TypeError('A source highlighter must be a loader function');
58
+ }
59
+ const editors = new WeakMap();
60
+ return Object.freeze({
61
+ name: 'source',
62
+ setup({editor, root, chrome}) {
63
+ const state = {
64
+ root,
65
+ chrome,
66
+ document: root.nodeType === Node.DOCUMENT_NODE ? root : root.ownerDocument,
67
+ options,
68
+ highlight,
69
+ loaded: null,
70
+ dialog: null,
71
+ pending: null,
72
+ };
73
+ editors.set(editor, state);
74
+ let connected = true;
75
+ return {dispose() {
76
+ if (!connected) return;
77
+ state.dialog?.remove();
78
+ editors.delete(editor);
79
+ connected = false;
80
+ }};
81
+ },
82
+ commands({editor, surface}) {
83
+ const state = editors.get(editor);
84
+ if (!state) throw new DOMException('The source extension is not set up', 'InvalidStateError');
85
+ return {source: {
86
+ // No convention has settled here: the browsers' own "view
87
+ // source" is ctrl+u, which an editor owes to underline. H reads
88
+ // as html and is what the editor this replaces used.
89
+ shortcut: 'ctrl+h',
90
+ // Editing happens in the dialog, so opening it changes nothing
91
+ // and the write it may cause runs as its own transaction.
92
+ transaction: false,
93
+ enabled: edit => !!edit.range,
94
+ run: () => open(state, surface),
95
+ }};
96
+ },
97
+ toolbar: Object.freeze([Object.freeze({
98
+ command: 'source', label: 'HTML source', text: '</>', shortcut: 'ctrl+h',
99
+ })]),
100
+ });
101
+ }
102
+
103
+ // A loader is awaited so the first open is already highlighted; without one the
104
+ // dialog opens synchronously.
105
+ function open(state, surface) {
106
+ if (!state.highlight) return show(state, surface);
107
+ state.loaded ??= Promise.resolve().then(state.highlight).catch(() => null);
108
+ return state.loaded.then(() => show(state, surface));
109
+ }
110
+
111
+ function show(state, surface) {
112
+ const view = new Source(surface, state.options);
113
+ const dialog = build(state);
114
+ const code = dialog.querySelector('u2-code');
115
+ const area = dialog.querySelector('textarea');
116
+ const {html, start, end} = view.read();
117
+ // The light textarea always carries the value: a late upgrade reads it, and
118
+ // an upgraded element writes every edit back into it.
119
+ area.value = html;
120
+ const input = upgraded(code) ? code : area;
121
+ // The value has to be set before focus; an upgraded element defers a write
122
+ // to a focused field.
123
+ if (input === code) code.value = html;
124
+ state.pending = {view, surface, code, area};
125
+ dialog.showModal();
126
+ input.focus();
127
+ input.setSelectionRange(start ?? 0, end ?? start ?? 0);
128
+ // A long document opens scrolled to the selection instead of at the top.
129
+ const lines = html.slice(0, start ?? 0).split('\n').length - 1;
130
+ const height = parseFloat(state.document.defaultView.getComputedStyle(area).lineHeight);
131
+ if (lines && height) (input === code ? code : area).scrollTop = lines * height;
132
+ return view;
133
+ }
134
+
135
+ function upgraded(element) {
136
+ return typeof element.value === 'string' && typeof element.setSelectionRange === 'function';
137
+ }
138
+
139
+ function build(state) {
140
+ if (state.dialog) return state.dialog;
141
+ const dialog = state.chrome.part('source', STYLE, 'dialog');
142
+ dialog.setAttribute('aria-label', 'HTML source');
143
+ const form = state.document.createElement('form');
144
+ form.method = 'dialog';
145
+ const area = state.document.createElement('textarea');
146
+ area.spellcheck = false;
147
+ area.setAttribute('aria-label', 'HTML source');
148
+ const code = state.document.createElement('u2-code');
149
+ code.setAttribute('editable', '');
150
+ code.setAttribute('language', 'html');
151
+ code.append(area);
152
+ const menu = state.document.createElement('menu');
153
+ for (const [value, label] of [['cancel', 'Cancel'], ['apply', 'Apply']]) {
154
+ const button = state.document.createElement('button');
155
+ button.type = 'submit';
156
+ button.value = value;
157
+ button.textContent = label;
158
+ menu.append(button);
159
+ }
160
+ form.append(code, menu);
161
+ dialog.append(form);
162
+ dialog.addEventListener('close', () => apply(state, dialog.returnValue));
163
+ state.chrome.root.append(dialog);
164
+ state.dialog = dialog;
165
+ return dialog;
166
+ }
167
+
168
+ function apply(state, result) {
169
+ const pending = state.pending;
170
+ state.pending = null;
171
+ if (!pending?.surface.connected) return;
172
+ // Focus returns before writing so the surface owns the selection the
173
+ // resulting transaction captures.
174
+ pending.surface.element.focus();
175
+ if (result !== 'apply') {
176
+ // Closing without applying leaves the surface exactly as it was,
177
+ // including the caret the view was opened on.
178
+ pending.surface.restore();
179
+ return;
180
+ }
181
+ pending.view.write(upgraded(pending.code) ? pending.code.value : pending.area.value);
182
+ }
@@ -0,0 +1,27 @@
1
+ import {insertNode} from '../command/insert.js';
2
+ import {Lists} from '../command/list.js';
3
+
4
+ // Block structure as one convention module: lists, their nesting level, and a
5
+ // separator. All of them consult the content model, so a host that does not
6
+ // allow a list or a rule simply keeps the control disabled.
7
+ const lists = new Lists(['ul', 'ol']);
8
+
9
+ export const structure = Object.freeze({
10
+ name: 'structure',
11
+ // Tab and Shift+Tab only ever reach the nesting commands where those are
12
+ // available, so outside a list they keep moving focus as they should.
13
+ commands: () => ({
14
+ bullets: {...lists.toggle('ul'), shortcut: 'ctrl+shift+8'},
15
+ numbers: {...lists.toggle('ol'), shortcut: 'ctrl+shift+7'},
16
+ indent: {...lists.indent, shortcut: 'tab'},
17
+ outdent: {...lists.outdent, shortcut: 'shift+tab'},
18
+ rule: insertNode(document => document.createElement('hr')),
19
+ }),
20
+ toolbar: Object.freeze([
21
+ Object.freeze({command: 'bullets', label: 'Bulleted list', text: '•–', state: true, shortcut: 'ctrl+shift+8'}),
22
+ Object.freeze({command: 'numbers', label: 'Numbered list', text: '1–', state: true, shortcut: 'ctrl+shift+7'}),
23
+ Object.freeze({command: 'outdent', label: 'Decrease level', text: '⇤', shortcut: 'shift+tab'}),
24
+ Object.freeze({command: 'indent', label: 'Increase level', text: '⇥', shortcut: 'tab'}),
25
+ Object.freeze({command: 'rule', label: 'Separator', text: '―'}),
26
+ ]),
27
+ });
@@ -0,0 +1,165 @@
1
+ import {Tables} from '../command/table.js';
2
+ import {inlineUi} from '../config/config.js';
3
+ import {elementOf} from '../selection/ownership/ownership.js';
4
+ import {Handles} from '../ui/handles.js';
5
+
6
+
7
+ // Where each handle sits: `axis` picks the edge it lines up with, `at` the
8
+ // fraction along the cell it is anchored to.
9
+ const HANDLES = [
10
+ {name: 'rowBefore', axis: 'row', at: 0, label: 'Row above', text: '+'},
11
+ {name: 'rowDelete', axis: 'row', at: 0.5, label: 'Delete row', text: '×'},
12
+ {name: 'rowAfter', axis: 'row', at: 1, label: 'Row below', text: '+'},
13
+ {name: 'columnBefore', axis: 'column', at: 0, label: 'Column before', text: '+'},
14
+ {name: 'columnDelete', axis: 'column', at: 0.5, label: 'Delete column', text: '×'},
15
+ {name: 'columnAfter', axis: 'column', at: 1, label: 'Column after', text: '+'},
16
+ ];
17
+
18
+ // Optional table module. Inserting a table is a toolbar action because it
19
+ // applies where no table is; everything else belongs to a table that exists, so
20
+ // it appears on that table — row handles down its left edge, column handles
21
+ // along its top, lined up with the cell the caret is in.
22
+ export function tableTools({tag = 'table'} = {}) {
23
+ const tables = new Tables(tag);
24
+ const editors = new WeakMap();
25
+ return Object.freeze({
26
+ name: 'tables',
27
+ setup({editor, root, chrome}) {
28
+ const state = {
29
+ root,
30
+ chrome,
31
+ document: root.nodeType === Node.DOCUMENT_NODE ? root : root.ownerDocument,
32
+ views: new WeakMap(),
33
+ active: null,
34
+ handles: null,
35
+ controller: null,
36
+ frame: 0,
37
+ };
38
+ editors.set(editor, state);
39
+ let connected = true;
40
+ return {dispose() {
41
+ if (!connected) return;
42
+ release(state);
43
+ editors.delete(editor);
44
+ connected = false;
45
+ }};
46
+ },
47
+ commands({editor, surface, commands}) {
48
+ const state = editors.get(editor);
49
+ if (!state) throw new DOMException('The table extension is not set up', 'InvalidStateError');
50
+ state.views.set(surface, {surface, commands});
51
+ return tables.commands;
52
+ },
53
+ attach({editor, surface}) {
54
+ const state = editors.get(editor);
55
+ const view = state?.views.get(surface);
56
+ if (!view) throw new DOMException('The table surface is not set up', 'InvalidStateError');
57
+ const controller = new state.document.defaultView.AbortController();
58
+ const listen = {signal: controller.signal};
59
+ const sync = () => track(state, view);
60
+ surface.addEventListener('u2-rte-selectionchange', sync, listen);
61
+ surface.addEventListener('u2-rte-change', sync, listen);
62
+ // What a session's end takes away, its beginning brings back: coming
63
+ // back to the same selection is no selection change to hear about.
64
+ surface.addEventListener('u2-rte-activate', sync, listen);
65
+ surface.addEventListener('u2-rte-deactivate', () => close(state, surface), listen);
66
+ surface.element.addEventListener('input', sync, listen);
67
+ return {dispose() {
68
+ controller.abort();
69
+ close(state, surface);
70
+ state.views.delete(surface);
71
+ }};
72
+ },
73
+ toolbar: Object.freeze([Object.freeze({command: 'insertTable', label: 'Insert table', text: '⊞'})]),
74
+ });
75
+ }
76
+
77
+ export const tables = tableTools();
78
+
79
+ function track(state, view) {
80
+ if (!inlineUi(view.surface.config, 'table')) return close(state, view.surface);
81
+ const cell = cellAt(view.surface);
82
+ if (!cell) return close(state, view.surface);
83
+ build(state);
84
+ state.active = {view, cell};
85
+ state.handles.show();
86
+ for (const handle of HANDLES) state.handles.disable(handle.name, !view.commands.enabled(handle.name));
87
+ schedule(state);
88
+ }
89
+
90
+ function close(state, surface) {
91
+ if (surface && state.active && state.active.view.surface !== surface) return;
92
+ state.active = null;
93
+ state.handles?.show(false);
94
+ }
95
+
96
+ // The cell the surface's saved selection sits in, if any.
97
+ function cellAt(surface) {
98
+ const range = surface.selection?.range();
99
+ if (!range) return null;
100
+ for (let element = elementOf(range.startContainer); element && element !== surface.element;
101
+ element = element.parentElement) {
102
+ if (element.matches('td, th') && surface.element.contains(element)) return element;
103
+ }
104
+ return null;
105
+ }
106
+
107
+ function schedule(state) {
108
+ if (state.frame || !state.handles) return;
109
+ state.frame = state.document.defaultView.requestAnimationFrame(() => {
110
+ state.frame = 0;
111
+ position(state);
112
+ });
113
+ }
114
+
115
+ function position(state) {
116
+ const cell = state.active?.cell;
117
+ const table = cell?.isConnected && cell.closest('table');
118
+ if (!table) return close(state);
119
+ const box = cell.getBoundingClientRect();
120
+ const bounds = table.getBoundingClientRect();
121
+ const edge = 12;
122
+ // One axis follows the cell, the other sits just outside the table's edge.
123
+ for (const {name, axis, at} of HANDLES) {
124
+ const centre = axis === 'row'
125
+ ? {x: bounds.left - edge, y: box.top + box.height * at}
126
+ : {x: box.left + box.width * at, y: bounds.top - edge};
127
+ state.handles.place(name, centre.x, centre.y);
128
+ }
129
+ }
130
+
131
+ function build(state) {
132
+ if (state.handles) return state.handles;
133
+ state.handles = new Handles(state.chrome.root, {
134
+ name: 'tables',
135
+ handles: HANDLES,
136
+ action: name => run(state, name),
137
+ });
138
+ const controller = new state.document.defaultView.AbortController();
139
+ const reposition = () => state.active && schedule(state);
140
+ state.root.addEventListener('scroll', reposition,
141
+ {capture: true, passive: true, signal: controller.signal});
142
+ state.document.defaultView.addEventListener('resize', reposition, {signal: controller.signal});
143
+ state.controller = controller;
144
+ return state.handles;
145
+ }
146
+
147
+ function run(state, name) {
148
+ const active = state.active;
149
+ if (!active?.view.surface.connected) return;
150
+ const {view} = active;
151
+ if (!view.surface.capture()) view.surface.restore();
152
+ view.commands.run(name);
153
+ view.surface.element.focus();
154
+ track(state, view);
155
+ }
156
+
157
+ function release(state) {
158
+ state.controller?.abort();
159
+ if (state.frame) state.document.defaultView.cancelAnimationFrame(state.frame);
160
+ state.handles?.dispose();
161
+ state.active = null;
162
+ state.frame = 0;
163
+ state.handles = null;
164
+ state.controller = null;
165
+ }
@@ -0,0 +1,32 @@
1
+ import {unstyleCommand} from '../command/unstyle.js';
2
+ import {Unstyle, defaultUnstyleLevels} from '../unstyle/unstyle.js';
3
+
4
+ export function unstyles(levels = defaultUnstyleLevels, {
5
+ name = 'unstyle',
6
+ control = 'unstyle',
7
+ command = 'unstyle',
8
+ label = 'Remove formatting',
9
+ text = 'T×',
10
+ shortcut = 'ctrl+\\',
11
+ } = {}) {
12
+ for (const [property, value] of Object.entries({name, control, command, label, text})) {
13
+ if (typeof value !== 'string' || !value.trim()) {
14
+ throw new TypeError(`Unstyle module ${property} must be a non-empty string`);
15
+ }
16
+ }
17
+ const policy = new Unstyle(levels);
18
+ const commands = Object.freeze({[command]: {...unstyleCommand(policy), shortcut}});
19
+ return Object.freeze({
20
+ name: name.trim(),
21
+ commands: () => commands,
22
+ toolbar: Object.freeze([Object.freeze({
23
+ name: control.trim(),
24
+ command: command.trim(),
25
+ label: label.trim(),
26
+ text: text.trim(),
27
+ shortcut,
28
+ })]),
29
+ });
30
+ }
31
+
32
+ export const unstyle = unstyles();