@widgetic/editor 4.0.10 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +242 -0
- package/dist/components/WidgetPreview.svelte +123 -86
- package/dist/components/WidgetPreview.svelte.d.ts +8 -0
- package/dist/components/color-select/ColorSelect.svelte +4 -4
- package/dist/components/editors/CompositionEditorApp.svelte +562 -0
- package/dist/components/editors/CompositionEditorApp.svelte.d.ts +95 -0
- package/dist/components/editors/EmbedEditor.svelte +33 -22
- package/dist/components/editors/EmbedEditor.svelte.d.ts +1 -1
- package/dist/components/editors/PropsEditor.svelte +267 -850
- package/dist/components/editors/PropsEditor.svelte.d.ts +10 -3
- package/dist/components/ic/AngleInputController.svelte +1 -1
- package/dist/components/ic/BrowserInputController.svelte +58 -31
- package/dist/components/ic/ColorPickerInputController.svelte +55 -21
- package/dist/components/ic/DateFieldInputController.svelte +34 -14
- package/dist/components/ic/DatePickerInputController.svelte +37 -21
- package/dist/components/ic/DropdownInputController.svelte +21 -9
- package/dist/components/ic/DropdownInputController.svelte.d.ts +1 -1
- package/dist/components/ic/FixedPositionInputController.svelte +12 -6
- package/dist/components/ic/FontInputController.svelte +100 -76
- package/dist/components/ic/InputControllerHeader.svelte +16 -9
- package/dist/components/ic/NumberControls.svelte +4 -4
- package/dist/components/ic/NumberInputController.svelte +6 -4
- package/dist/components/ic/PositionInputController.svelte +23 -15
- package/dist/components/ic/RangeCalendarInputController.svelte +36 -20
- package/dist/components/ic/RoundedCornersInputController.svelte +39 -26
- package/dist/components/ic/SliderInputController.svelte +7 -2
- package/dist/components/ic/SwitchInputController.svelte +12 -3
- package/dist/components/ic/TagsInputController.svelte +19 -4
- package/dist/components/ic/TextAreaInputController.svelte +79 -51
- package/dist/components/ic/TextAreaPreviewInputController.svelte +27 -72
- package/dist/components/ic/TextInputController.svelte +17 -1
- package/dist/components/ic/TimeInputController.svelte +11 -4
- package/dist/components/ic/VectorInputController.svelte +101 -70
- package/dist/components/ic/shared/ItemGrid.svelte +12 -4
- package/dist/components/ic/shared/RecentItemsSection.svelte +16 -11
- package/dist/components/ic/shared/RecentItemsSection.svelte.d.ts +1 -1
- package/dist/composition/compositionApi.d.ts +95 -0
- package/dist/composition/compositionApi.js +289 -0
- package/dist/composition/compositionEditorController.d.ts +263 -0
- package/dist/composition/compositionEditorController.js +746 -0
- package/dist/composition/compositionHelpers.d.ts +74 -0
- package/dist/composition/compositionHelpers.js +405 -0
- package/dist/i18n/index.d.ts +4 -0
- package/dist/i18n/index.js +4 -0
- package/dist/i18n/labels.d.ts +15 -0
- package/dist/i18n/labels.js +57 -0
- package/dist/i18n/language.d.ts +228 -0
- package/dist/i18n/language.js +34 -0
- package/dist/i18n/languageKit.d.ts +54 -0
- package/dist/i18n/languageKit.js +102 -0
- package/dist/i18n/messages.d.ts +241 -0
- package/dist/i18n/messages.js +253 -0
- package/dist/i18n/translate.d.ts +6 -0
- package/dist/i18n/translate.js +8 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +8 -0
- package/dist/mediaLibraryContext.d.ts +10 -0
- package/dist/mediaLibraryContext.js +30 -0
- package/dist/utils/FontManager.d.ts +21 -5
- package/dist/utils/FontManager.js +62 -67
- package/dist/utils/VectorManager.d.ts +0 -1
- package/dist/utils/VectorManager.js +10 -12
- package/dist/utils/dockPlacement.d.ts +12 -0
- package/dist/utils/dockPlacement.js +13 -0
- package/dist/utils/editorStorage.d.ts +23 -0
- package/dist/utils/editorStorage.js +95 -0
- package/dist/utils/fieldDirection.d.ts +13 -0
- package/dist/utils/fieldDirection.js +25 -0
- package/dist/utils/fontUtils.js +5 -3
- package/dist/utils/htmlSafety.d.ts +21 -0
- package/dist/utils/htmlSafety.js +38 -0
- package/dist/utils/itemReorder.d.ts +90 -0
- package/dist/utils/itemReorder.js +354 -0
- package/dist/utils/recentColors.js +4 -2
- package/dist/utils/richTextPreview.d.ts +38 -0
- package/dist/utils/richTextPreview.js +207 -0
- package/dist/utils/scrollActivity.d.ts +14 -0
- package/dist/utils/scrollActivity.js +63 -0
- package/dist/utils/shadowDom.d.ts +67 -0
- package/dist/utils/shadowDom.js +211 -0
- package/dist/utils/vectorUtils.js +5 -3
- package/dist/utils/visibilityCondition.d.ts +40 -0
- package/dist/utils/visibilityCondition.js +535 -0
- package/package.json +7 -6
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The compact preview of a rich text (what the text area control shows before it is opened): the text with its bold,
|
|
3
|
+
* italic, underline, colours and so on, in a few lines.
|
|
4
|
+
*
|
|
5
|
+
* The rich text is HTML that somebody typed or pasted, so it is not trusted. The preview used to be made by copying
|
|
6
|
+
* the HTML it was given, tag by tag, into a string for `{@html}`: a text that said `<img src=x onerror=...>` came
|
|
7
|
+
* out as a real `<img>` that ran its script in the page the editor is on. It is now made from a description of the text
|
|
8
|
+
* (the `PreviewNode`s below), and everything that goes into the string is either escaped or comes from a short list of
|
|
9
|
+
* what is allowed: some tags, a few style properties with plain values, one class per marker.
|
|
10
|
+
*
|
|
11
|
+
* html text --parseRichText (the browser's own parser, which runs nothing)--> PreviewNode[]
|
|
12
|
+
* --renderPreviewNodes (plain string work, no browser needed)--> HTML that is safe for {@html}
|
|
13
|
+
*/
|
|
14
|
+
import { escapeHtml } from './htmlSafety';
|
|
15
|
+
/** Elements that are shown as they are (their formatting is the point of the preview). */
|
|
16
|
+
const KEPT_TAGS = new Set([
|
|
17
|
+
'a',
|
|
18
|
+
'b',
|
|
19
|
+
'code',
|
|
20
|
+
'del',
|
|
21
|
+
'em',
|
|
22
|
+
'i',
|
|
23
|
+
'ins',
|
|
24
|
+
'li',
|
|
25
|
+
'mark',
|
|
26
|
+
'ol',
|
|
27
|
+
's',
|
|
28
|
+
'small',
|
|
29
|
+
'span',
|
|
30
|
+
'strike',
|
|
31
|
+
'strong',
|
|
32
|
+
'sub',
|
|
33
|
+
'sup',
|
|
34
|
+
'u',
|
|
35
|
+
'ul'
|
|
36
|
+
]);
|
|
37
|
+
/** Elements whose content is not text to show at all: nothing of them, not even their text, reaches the preview. */
|
|
38
|
+
const DROPPED_TAGS = new Set([
|
|
39
|
+
'applet',
|
|
40
|
+
'audio',
|
|
41
|
+
'canvas',
|
|
42
|
+
'embed',
|
|
43
|
+
'frame',
|
|
44
|
+
'frameset',
|
|
45
|
+
'iframe',
|
|
46
|
+
'img',
|
|
47
|
+
'link',
|
|
48
|
+
'math',
|
|
49
|
+
'meta',
|
|
50
|
+
'noscript',
|
|
51
|
+
'object',
|
|
52
|
+
'script',
|
|
53
|
+
'style',
|
|
54
|
+
'svg',
|
|
55
|
+
'template',
|
|
56
|
+
'textarea',
|
|
57
|
+
'title',
|
|
58
|
+
'video'
|
|
59
|
+
]);
|
|
60
|
+
/** The style properties a text can carry into the preview. */
|
|
61
|
+
const ALLOWED_STYLE_PROPERTIES = new Set([
|
|
62
|
+
'background-color',
|
|
63
|
+
'color',
|
|
64
|
+
'font-family',
|
|
65
|
+
'font-size',
|
|
66
|
+
'font-style',
|
|
67
|
+
'font-weight',
|
|
68
|
+
'letter-spacing',
|
|
69
|
+
'line-height',
|
|
70
|
+
'text-decoration',
|
|
71
|
+
'text-decoration-color',
|
|
72
|
+
'text-decoration-line',
|
|
73
|
+
'text-transform'
|
|
74
|
+
]);
|
|
75
|
+
/** Functions that may appear in a style value (colours and arithmetic); `url(...)`, `expression(...)`... may not. */
|
|
76
|
+
const ALLOWED_STYLE_FUNCTIONS = new Set([
|
|
77
|
+
'calc',
|
|
78
|
+
'clamp',
|
|
79
|
+
'hsl',
|
|
80
|
+
'hsla',
|
|
81
|
+
'max',
|
|
82
|
+
'min',
|
|
83
|
+
'rgb',
|
|
84
|
+
'rgba',
|
|
85
|
+
'var'
|
|
86
|
+
]);
|
|
87
|
+
const MAX_STYLE_VALUE_LENGTH = 200;
|
|
88
|
+
/** Letters, digits, spaces and the punctuation of colours, sizes and font lists. No `;`, `{`, `}`, `<`, `>`, `\`, `@`. */
|
|
89
|
+
const SAFE_STYLE_CHARACTERS = /^[\p{L}\p{N} #%.,+\-_'"()/*]*$/u;
|
|
90
|
+
/** A value for the preview's `data-text-style` attribute: a short name such as `heading-1`. */
|
|
91
|
+
const TEXT_STYLE_NAME = /^[A-Za-z0-9_-]{1,40}$/;
|
|
92
|
+
/** Whether one style declaration may go into the preview. */
|
|
93
|
+
export function isAllowedDeclaration(property, value) {
|
|
94
|
+
if (!ALLOWED_STYLE_PROPERTIES.has(property))
|
|
95
|
+
return false;
|
|
96
|
+
if (value.length === 0 || value.length > MAX_STYLE_VALUE_LENGTH)
|
|
97
|
+
return false;
|
|
98
|
+
if (!SAFE_STYLE_CHARACTERS.test(value))
|
|
99
|
+
return false;
|
|
100
|
+
// Comments can hide the words below from the checks that follow; an escape can spell them.
|
|
101
|
+
if (value.includes('/*') || value.includes('*/'))
|
|
102
|
+
return false;
|
|
103
|
+
for (const call of value.matchAll(/([A-Za-z-]+)\s*\(/g)) {
|
|
104
|
+
if (!ALLOWED_STYLE_FUNCTIONS.has(call[1].toLowerCase()))
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
/** The `style` attribute value (already escaped for an attribute) for the declarations that are allowed, or ''. */
|
|
110
|
+
function styleAttribute(declarations) {
|
|
111
|
+
const allowed = declarations.filter(([property, value]) => isAllowedDeclaration(property, value));
|
|
112
|
+
if (allowed.length === 0)
|
|
113
|
+
return '';
|
|
114
|
+
const colour = allowed.find(([property]) => property === 'color');
|
|
115
|
+
const parts = allowed.map(([property, value]) => `${property}: ${value}`);
|
|
116
|
+
// The preview's own stylesheet reads the text colour from --color.
|
|
117
|
+
if (colour)
|
|
118
|
+
parts.unshift(`--color: ${colour[1]}`);
|
|
119
|
+
return escapeHtml(`${parts.join('; ')};`);
|
|
120
|
+
}
|
|
121
|
+
function renderNode(node) {
|
|
122
|
+
if (typeof node === 'string')
|
|
123
|
+
return escapeHtml(node);
|
|
124
|
+
const tag = node.tag.toLowerCase();
|
|
125
|
+
if (DROPPED_TAGS.has(tag))
|
|
126
|
+
return '';
|
|
127
|
+
const children = node.children.map(renderNode).join('');
|
|
128
|
+
// Line breaks and paragraphs become spaces: the preview is a few lines of running text.
|
|
129
|
+
if (tag === 'br')
|
|
130
|
+
return ' ';
|
|
131
|
+
if (tag === 'p' || tag === 'div')
|
|
132
|
+
return `${children} `;
|
|
133
|
+
const isHeading = /^h[1-4]$/.test(tag);
|
|
134
|
+
if (!isHeading && !KEPT_TAGS.has(tag))
|
|
135
|
+
return children;
|
|
136
|
+
const classes = [];
|
|
137
|
+
const textStyle = node.textStyle !== null && TEXT_STYLE_NAME.test(node.textStyle) ? node.textStyle : null;
|
|
138
|
+
if (textStyle !== null)
|
|
139
|
+
classes.push('preserve-color');
|
|
140
|
+
if (isHeading)
|
|
141
|
+
classes.push('tiptap-heading');
|
|
142
|
+
const style = styleAttribute(node.declarations);
|
|
143
|
+
const outputTag = isHeading ? 'span' : tag;
|
|
144
|
+
let startTag = `<${outputTag}`;
|
|
145
|
+
if (style !== '')
|
|
146
|
+
startTag += ` style="${style}"`;
|
|
147
|
+
if (textStyle !== null)
|
|
148
|
+
startTag += ` data-text-style="${escapeHtml(textStyle)}"`;
|
|
149
|
+
if (classes.length > 0)
|
|
150
|
+
startTag += ` class="${classes.join(' ')}"`;
|
|
151
|
+
return `${startTag}>${children}</${outputTag}>`;
|
|
152
|
+
}
|
|
153
|
+
/** HTML for `{@html}` from the description of a rich text. Everything in it is escaped or on the lists above. */
|
|
154
|
+
export function renderPreviewNodes(nodes) {
|
|
155
|
+
return nodes.map(renderNode).join('').trim();
|
|
156
|
+
}
|
|
157
|
+
// ---- From the browser ---------------------------------------------------------------------------------------------
|
|
158
|
+
function toPreviewNode(node) {
|
|
159
|
+
if (node.nodeType === Node.TEXT_NODE)
|
|
160
|
+
return node.textContent ?? '';
|
|
161
|
+
if (node.nodeType !== Node.ELEMENT_NODE)
|
|
162
|
+
return null;
|
|
163
|
+
const element = node;
|
|
164
|
+
const declarations = [];
|
|
165
|
+
// The browser reads the style the way it would apply it: what it cannot read is already gone.
|
|
166
|
+
const style = element.style;
|
|
167
|
+
if (style) {
|
|
168
|
+
for (let index = 0; index < style.length; index += 1) {
|
|
169
|
+
const property = style.item(index);
|
|
170
|
+
declarations.push([property, style.getPropertyValue(property)]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
tag: element.tagName.toLowerCase(),
|
|
175
|
+
declarations,
|
|
176
|
+
textStyle: element.getAttribute('data-text-style'),
|
|
177
|
+
children: Array.from(element.childNodes)
|
|
178
|
+
.map(toPreviewNode)
|
|
179
|
+
.filter((child) => child !== null)
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Reads rich text with the browser's HTML parser. A document made by `DOMParser` has no page behind it: scripts do
|
|
184
|
+
* not run, pictures are not fetched, event handlers never fire.
|
|
185
|
+
*/
|
|
186
|
+
export function parseRichText(html) {
|
|
187
|
+
const document = new DOMParser().parseFromString(html, 'text/html');
|
|
188
|
+
return Array.from(document.body.childNodes)
|
|
189
|
+
.map(toPreviewNode)
|
|
190
|
+
.filter((node) => node !== null);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The preview of a rich text, as HTML that is safe for `{@html}`. A text that arrives written out as entities
|
|
194
|
+
* (`<strong>...`) is read as the markup it spells, as it always has been.
|
|
195
|
+
*/
|
|
196
|
+
export function createPreviewHtml(text) {
|
|
197
|
+
if (!text)
|
|
198
|
+
return '';
|
|
199
|
+
let html = text;
|
|
200
|
+
if (!html.includes('<') && html.includes('<')) {
|
|
201
|
+
// A detached <textarea> only decodes entities: nothing in it is parsed as markup or run.
|
|
202
|
+
const decoder = document.createElement('textarea');
|
|
203
|
+
decoder.innerHTML = html;
|
|
204
|
+
html = decoder.value;
|
|
205
|
+
}
|
|
206
|
+
return renderPreviewNodes(parseRichText(html));
|
|
207
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Something that wants to know the user is scrolling (the help tooltips hide while the panel moves under them).
|
|
3
|
+
*
|
|
4
|
+
* Every control used to put its own three listeners on the window and the document, which is hundreds of global
|
|
5
|
+
* listeners on a customer's page and a pass over every control for each wheel tick. The listeners are shared now:
|
|
6
|
+
* one set per tree (the page, or the shadow root of a module), however many controls ask.
|
|
7
|
+
*/
|
|
8
|
+
type ScrollActivityListener = () => void;
|
|
9
|
+
/**
|
|
10
|
+
* Calls `listener` whenever the user scrolls in the tree `anchor` lives in: the page scrolling, a panel scrolling, a
|
|
11
|
+
* wheel turning or a finger moving. Gives back the function that stops it; call that when the control goes away.
|
|
12
|
+
*/
|
|
13
|
+
export declare function onScrollActivity(anchor: Node | null | undefined, listener: ScrollActivityListener): () => void;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { listenForScroll } from './shadowDom';
|
|
2
|
+
/** The shared listeners of each tree, found by the tree's root node (the document or a shadow root). */
|
|
3
|
+
const sharedListenersByTree = new Map();
|
|
4
|
+
/** The node that stands for the tree `anchor` lives in. */
|
|
5
|
+
function treeKeyOf(anchor) {
|
|
6
|
+
if (typeof document === 'undefined')
|
|
7
|
+
return null;
|
|
8
|
+
try {
|
|
9
|
+
const rootNode = anchor?.getRootNode?.();
|
|
10
|
+
// A node that is not in a page (yet) has no tree of its own: the page's listeners are the ones it wants.
|
|
11
|
+
return rootNode && rootNode.nodeType === 11 ? rootNode : document;
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
console.warn('[scrollActivity] Could not tell which tree the control is in:', error);
|
|
15
|
+
return document;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Calls `listener` whenever the user scrolls in the tree `anchor` lives in: the page scrolling, a panel scrolling, a
|
|
20
|
+
* wheel turning or a finger moving. Gives back the function that stops it; call that when the control goes away.
|
|
21
|
+
*/
|
|
22
|
+
export function onScrollActivity(anchor, listener) {
|
|
23
|
+
const treeKey = treeKeyOf(anchor);
|
|
24
|
+
if (!treeKey)
|
|
25
|
+
return () => { };
|
|
26
|
+
let shared = sharedListenersByTree.get(treeKey);
|
|
27
|
+
if (!shared) {
|
|
28
|
+
const listeners = new Set();
|
|
29
|
+
const tellEveryone = () => {
|
|
30
|
+
// Copied, because a listener may stop listening while it is being told.
|
|
31
|
+
for (const interested of [...listeners]) {
|
|
32
|
+
try {
|
|
33
|
+
interested();
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error('[scrollActivity] A scroll listener failed:', error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const stopScroll = listenForScroll(anchor, tellEveryone, { passive: true });
|
|
41
|
+
// The wheel and a finger moving leave a shadow root, so the document hears about them in either tree.
|
|
42
|
+
document.addEventListener('wheel', tellEveryone, { capture: true, passive: true });
|
|
43
|
+
document.addEventListener('touchmove', tellEveryone, { capture: true, passive: true });
|
|
44
|
+
shared = {
|
|
45
|
+
listeners,
|
|
46
|
+
stop: () => {
|
|
47
|
+
stopScroll();
|
|
48
|
+
document.removeEventListener('wheel', tellEveryone, true);
|
|
49
|
+
document.removeEventListener('touchmove', tellEveryone, true);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
sharedListenersByTree.set(treeKey, shared);
|
|
53
|
+
}
|
|
54
|
+
const sharedForThisTree = shared;
|
|
55
|
+
sharedForThisTree.listeners.add(listener);
|
|
56
|
+
return () => {
|
|
57
|
+
sharedForThisTree.listeners.delete(listener);
|
|
58
|
+
if (sharedForThisTree.listeners.size === 0 && sharedListenersByTree.get(treeKey) === sharedForThisTree) {
|
|
59
|
+
sharedForThisTree.stop();
|
|
60
|
+
sharedListenersByTree.delete(treeKey);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding things on the page when the editor can be inside a shadow root.
|
|
3
|
+
*
|
|
4
|
+
* The editor also runs inside a module on a customer's page, in a shadow root. `document.querySelector(...)` and
|
|
5
|
+
* `document.getElementById(...)` only look at the page's own tree and never inside a shadow root, so code that looks
|
|
6
|
+
* something up that way finds nothing there (or, worse, finds something of the customer's page). `node.getRootNode()`
|
|
7
|
+
* is the tree a node lives in: the shadow root inside a module, the document on a normal page. These helpers look
|
|
8
|
+
* there.
|
|
9
|
+
*/
|
|
10
|
+
/** What a document and a shadow root have in common, which is all these helpers need of either. */
|
|
11
|
+
export interface SearchableTree {
|
|
12
|
+
readonly nodeType: number;
|
|
13
|
+
querySelector(selector: string): Element | null;
|
|
14
|
+
querySelectorAll(selector: string): ArrayLike<Element>;
|
|
15
|
+
getElementById(id: string): HTMLElement | null;
|
|
16
|
+
appendChild<NodeType extends Node>(node: NodeType): NodeType;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The tree a node lives in: the shadow root it is inside, or the document. A node that is not on a page yet, or no
|
|
20
|
+
* node at all, gives the document (and nothing when there is no document, as when rendering on a server).
|
|
21
|
+
*/
|
|
22
|
+
export declare function treeOf(anchor: Node | null | undefined): SearchableTree | null;
|
|
23
|
+
/** `document.querySelector(selector)`, but in the tree `anchor` lives in. */
|
|
24
|
+
export declare function queryInTreeOf(anchor: Node | null | undefined, selector: string): Element | null;
|
|
25
|
+
/** `document.querySelectorAll(selector)` as a list, but in the tree `anchor` lives in. */
|
|
26
|
+
export declare function queryAllInTreeOf(anchor: Node | null | undefined, selector: string): Element[];
|
|
27
|
+
/**
|
|
28
|
+
* Whether an event happened inside `element`. Heard on the document or the window, an event that started inside a
|
|
29
|
+
* shadow root has the shadow root's host as its target, so `element.contains(event.target)` is false for everything
|
|
30
|
+
* inside the editor; the event's path still lists the elements it really went through. (Call it while the event is
|
|
31
|
+
* being handled: the path is gone once handling is over.)
|
|
32
|
+
*/
|
|
33
|
+
export declare function isEventInside(event: Event, element: Node | null | undefined): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The element that has the keyboard focus in the tree `anchor` lives in. `document.activeElement` is the module's
|
|
36
|
+
* host element for anything focused inside a shadow root, which is never one of the editor's fields.
|
|
37
|
+
*/
|
|
38
|
+
export declare function activeElementIn(anchor: Node | null | undefined): Element | null;
|
|
39
|
+
/**
|
|
40
|
+
* Calls `handler` when something scrolls and gives back the function that stops it. A scroll is not an event that
|
|
41
|
+
* leaves a shadow root, so a listener on the window only hears about the page being scrolled; to hear about the
|
|
42
|
+
* editor's own panels scrolling the listener goes on the shadow root too. Both are listened to while the event goes
|
|
43
|
+
* down (capture), as a scroll does not bubble.
|
|
44
|
+
*/
|
|
45
|
+
export declare function listenForScroll(anchor: Node | null | undefined, handler: (event: Event) => void, { passive }?: {
|
|
46
|
+
passive?: boolean;
|
|
47
|
+
}): () => void;
|
|
48
|
+
/**
|
|
49
|
+
* The layer a popover is drawn in when it must sit above everything else, found or made in the tree `anchor` lives in
|
|
50
|
+
* and shared by every component that asks for the same `id`. On a page it is a node at the end of the body; inside a
|
|
51
|
+
* module it is a node in the module's portal layer (or, failing that, in the shadow root), never in the customer's
|
|
52
|
+
* body. Give it back with `releasePortalHost` when the component goes away.
|
|
53
|
+
*/
|
|
54
|
+
export declare function acquirePortalHost(anchor: Node | null | undefined, id: string, zIndex?: string): HTMLElement | null;
|
|
55
|
+
/** Gives a layer from `acquirePortalHost` back; the last one to give it back removes it from the page. */
|
|
56
|
+
export declare function releasePortalHost(host: HTMLElement | null | undefined): void;
|
|
57
|
+
/** What `lockPointerStyle` can be asked to do besides showing a cursor. */
|
|
58
|
+
export interface PointerStyleOptions {
|
|
59
|
+
/** Also stop text from being selected as the pointer moves. On unless told otherwise. */
|
|
60
|
+
blockTextSelection?: boolean;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Shows `cursor` for as long as a drag or a resize lasts (and, unless told otherwise, stops text from being selected as
|
|
64
|
+
* the pointer moves). Gives back the function that puts back what was there before; it puts it back once, however
|
|
65
|
+
* often it is called, so it can be called from every way a gesture can end.
|
|
66
|
+
*/
|
|
67
|
+
export declare function lockPointerStyle(anchor: EventTarget | null | undefined, cursor: string, { blockTextSelection }?: PointerStyleOptions): () => void;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding things on the page when the editor can be inside a shadow root.
|
|
3
|
+
*
|
|
4
|
+
* The editor also runs inside a module on a customer's page, in a shadow root. `document.querySelector(...)` and
|
|
5
|
+
* `document.getElementById(...)` only look at the page's own tree and never inside a shadow root, so code that looks
|
|
6
|
+
* something up that way finds nothing there (or, worse, finds something of the customer's page). `node.getRootNode()`
|
|
7
|
+
* is the tree a node lives in: the shadow root inside a module, the document on a normal page. These helpers look
|
|
8
|
+
* there.
|
|
9
|
+
*/
|
|
10
|
+
const DOCUMENT_NODE = 9;
|
|
11
|
+
const DOCUMENT_FRAGMENT_NODE = 11;
|
|
12
|
+
/**
|
|
13
|
+
* The class of the layer a module keeps inside its shadow root for menus, dialogs and popovers. It is set by the
|
|
14
|
+
* module (ui-sdk/modules/src/shared/shadowMount.ts); this file only finds it.
|
|
15
|
+
*/
|
|
16
|
+
const MODULE_PORTAL_LAYER_SELECTOR = '.widgetic-ui-portal';
|
|
17
|
+
/**
|
|
18
|
+
* The tree a node lives in: the shadow root it is inside, or the document. A node that is not on a page yet, or no
|
|
19
|
+
* node at all, gives the document (and nothing when there is no document, as when rendering on a server).
|
|
20
|
+
*/
|
|
21
|
+
export function treeOf(anchor) {
|
|
22
|
+
try {
|
|
23
|
+
const rootNode = anchor?.getRootNode?.();
|
|
24
|
+
if (rootNode &&
|
|
25
|
+
(rootNode.nodeType === DOCUMENT_NODE || rootNode.nodeType === DOCUMENT_FRAGMENT_NODE)) {
|
|
26
|
+
return rootNode;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.warn('[shadowDom] Could not tell which tree a node is in, using the document:', error);
|
|
31
|
+
}
|
|
32
|
+
return typeof document === 'undefined' ? null : document;
|
|
33
|
+
}
|
|
34
|
+
/** `document.querySelector(selector)`, but in the tree `anchor` lives in. */
|
|
35
|
+
export function queryInTreeOf(anchor, selector) {
|
|
36
|
+
try {
|
|
37
|
+
return treeOf(anchor)?.querySelector(selector) ?? null;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.warn(`[shadowDom] Could not search for "${selector}":`, error);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** `document.querySelectorAll(selector)` as a list, but in the tree `anchor` lives in. */
|
|
45
|
+
export function queryAllInTreeOf(anchor, selector) {
|
|
46
|
+
try {
|
|
47
|
+
return Array.from(treeOf(anchor)?.querySelectorAll(selector) ?? []);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.warn(`[shadowDom] Could not search for "${selector}":`, error);
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether an event happened inside `element`. Heard on the document or the window, an event that started inside a
|
|
56
|
+
* shadow root has the shadow root's host as its target, so `element.contains(event.target)` is false for everything
|
|
57
|
+
* inside the editor; the event's path still lists the elements it really went through. (Call it while the event is
|
|
58
|
+
* being handled: the path is gone once handling is over.)
|
|
59
|
+
*/
|
|
60
|
+
export function isEventInside(event, element) {
|
|
61
|
+
if (!element)
|
|
62
|
+
return false;
|
|
63
|
+
try {
|
|
64
|
+
const path = event.composedPath?.();
|
|
65
|
+
if (path && path.length > 0)
|
|
66
|
+
return path.includes(element);
|
|
67
|
+
return element.contains(event.target);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.warn('[shadowDom] Could not tell where an event happened:', error);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The element that has the keyboard focus in the tree `anchor` lives in. `document.activeElement` is the module's
|
|
76
|
+
* host element for anything focused inside a shadow root, which is never one of the editor's fields.
|
|
77
|
+
*/
|
|
78
|
+
export function activeElementIn(anchor) {
|
|
79
|
+
try {
|
|
80
|
+
const rootNode = anchor?.getRootNode?.();
|
|
81
|
+
if (rootNode && (rootNode.nodeType === DOCUMENT_NODE || rootNode.nodeType === DOCUMENT_FRAGMENT_NODE)) {
|
|
82
|
+
return rootNode.activeElement ?? null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.warn('[shadowDom] Could not tell which element has the focus:', error);
|
|
87
|
+
}
|
|
88
|
+
return typeof document === 'undefined' ? null : document.activeElement;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Calls `handler` when something scrolls and gives back the function that stops it. A scroll is not an event that
|
|
92
|
+
* leaves a shadow root, so a listener on the window only hears about the page being scrolled; to hear about the
|
|
93
|
+
* editor's own panels scrolling the listener goes on the shadow root too. Both are listened to while the event goes
|
|
94
|
+
* down (capture), as a scroll does not bubble.
|
|
95
|
+
*/
|
|
96
|
+
export function listenForScroll(anchor, handler, { passive = false } = {}) {
|
|
97
|
+
const targets = [];
|
|
98
|
+
if (typeof window !== 'undefined')
|
|
99
|
+
targets.push(window);
|
|
100
|
+
try {
|
|
101
|
+
const rootNode = anchor?.getRootNode?.();
|
|
102
|
+
if (rootNode?.nodeType === DOCUMENT_FRAGMENT_NODE)
|
|
103
|
+
targets.push(rootNode);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
console.warn('[shadowDom] Could not tell which tree to listen for scrolling in:', error);
|
|
107
|
+
}
|
|
108
|
+
for (const target of targets)
|
|
109
|
+
target.addEventListener('scroll', handler, { capture: true, passive });
|
|
110
|
+
return () => {
|
|
111
|
+
for (const target of targets)
|
|
112
|
+
target.removeEventListener('scroll', handler, true);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** How many components use each popover layer, so the last one to leave can remove it. */
|
|
116
|
+
const portalHostUsers = new WeakMap();
|
|
117
|
+
/** The layers made here (a layer found already in place belongs to whoever made it). */
|
|
118
|
+
const portalHostsMadeHere = new WeakSet();
|
|
119
|
+
/**
|
|
120
|
+
* The layer a popover is drawn in when it must sit above everything else, found or made in the tree `anchor` lives in
|
|
121
|
+
* and shared by every component that asks for the same `id`. On a page it is a node at the end of the body; inside a
|
|
122
|
+
* module it is a node in the module's portal layer (or, failing that, in the shadow root), never in the customer's
|
|
123
|
+
* body. Give it back with `releasePortalHost` when the component goes away.
|
|
124
|
+
*/
|
|
125
|
+
export function acquirePortalHost(anchor, id, zIndex = '100000000') {
|
|
126
|
+
try {
|
|
127
|
+
const tree = treeOf(anchor);
|
|
128
|
+
if (!tree)
|
|
129
|
+
return null;
|
|
130
|
+
let host = tree.getElementById(id);
|
|
131
|
+
if (!host) {
|
|
132
|
+
host = document.createElement('div');
|
|
133
|
+
host.id = id;
|
|
134
|
+
host.style.position = 'relative';
|
|
135
|
+
host.style.zIndex = zIndex;
|
|
136
|
+
const parent = tree.nodeType === DOCUMENT_FRAGMENT_NODE
|
|
137
|
+
? (tree.querySelector(MODULE_PORTAL_LAYER_SELECTOR) ?? tree)
|
|
138
|
+
: document.body;
|
|
139
|
+
parent.appendChild(host);
|
|
140
|
+
portalHostsMadeHere.add(host);
|
|
141
|
+
}
|
|
142
|
+
portalHostUsers.set(host, (portalHostUsers.get(host) ?? 0) + 1);
|
|
143
|
+
return host;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
console.error(`[shadowDom] Could not make the "${id}" layer for popovers:`, error);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** Gives a layer from `acquirePortalHost` back; the last one to give it back removes it from the page. */
|
|
151
|
+
export function releasePortalHost(host) {
|
|
152
|
+
if (!host)
|
|
153
|
+
return;
|
|
154
|
+
try {
|
|
155
|
+
const remaining = (portalHostUsers.get(host) ?? 1) - 1;
|
|
156
|
+
if (remaining > 0) {
|
|
157
|
+
portalHostUsers.set(host, remaining);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
portalHostUsers.delete(host);
|
|
161
|
+
if (portalHostsMadeHere.has(host)) {
|
|
162
|
+
portalHostsMadeHere.delete(host);
|
|
163
|
+
host.remove();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
console.error('[shadowDom] Could not remove the layer for popovers:', error);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The element a drag or a resize shows its cursor on: the host of the shadow root `anchor` lives in (the module the
|
|
172
|
+
* editor is in), or the page's body when it is not in a shadow root. The body of a customer's page is not the editor's
|
|
173
|
+
* to change, and an editor that sets a cursor there and then clears it also deletes the cursor the page had set.
|
|
174
|
+
*/
|
|
175
|
+
function pointerStyleElementOf(anchor) {
|
|
176
|
+
const rootNode = anchor?.getRootNode?.();
|
|
177
|
+
if (rootNode?.nodeType === DOCUMENT_FRAGMENT_NODE && rootNode.host) {
|
|
178
|
+
return rootNode.host;
|
|
179
|
+
}
|
|
180
|
+
return typeof document === 'undefined' ? null : document.body;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Shows `cursor` for as long as a drag or a resize lasts (and, unless told otherwise, stops text from being selected as
|
|
184
|
+
* the pointer moves). Gives back the function that puts back what was there before; it puts it back once, however
|
|
185
|
+
* often it is called, so it can be called from every way a gesture can end.
|
|
186
|
+
*/
|
|
187
|
+
export function lockPointerStyle(anchor, cursor, { blockTextSelection = true } = {}) {
|
|
188
|
+
try {
|
|
189
|
+
const styledElement = pointerStyleElementOf(anchor);
|
|
190
|
+
if (!styledElement)
|
|
191
|
+
return () => { };
|
|
192
|
+
const previousCursor = styledElement.style.cursor;
|
|
193
|
+
const previousUserSelect = styledElement.style.userSelect;
|
|
194
|
+
styledElement.style.cursor = cursor;
|
|
195
|
+
if (blockTextSelection)
|
|
196
|
+
styledElement.style.userSelect = 'none';
|
|
197
|
+
let isRestored = false;
|
|
198
|
+
return () => {
|
|
199
|
+
if (isRestored)
|
|
200
|
+
return;
|
|
201
|
+
isRestored = true;
|
|
202
|
+
styledElement.style.cursor = previousCursor;
|
|
203
|
+
if (blockTextSelection)
|
|
204
|
+
styledElement.style.userSelect = previousUserSelect;
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
console.error('[shadowDom] Could not change the pointer for the drag:', error);
|
|
209
|
+
return () => { };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { writable } from 'svelte/store';
|
|
2
|
+
import { readEditorStorageValue, writeEditorStorageValue } from './editorStorage';
|
|
2
3
|
export const vectorCategories = {
|
|
3
4
|
map: {
|
|
4
5
|
'icons': 'Icons',
|
|
@@ -22,13 +23,14 @@ export const vectorCategories = {
|
|
|
22
23
|
return 'Icons'; // Default category
|
|
23
24
|
}
|
|
24
25
|
};
|
|
26
|
+
// Stored in the editor's own corner of localStorage (see editorStorage.ts), not under a key the customer's page may use.
|
|
25
27
|
const RECENT_VECTORS_KEY = 'recent_vectors';
|
|
26
28
|
const MAX_RECENT_VECTORS = 3;
|
|
27
29
|
export const recentVectorsStore = writable([]);
|
|
28
30
|
export const vectorUtils = {
|
|
29
31
|
getRecentVectors() {
|
|
30
32
|
try {
|
|
31
|
-
const stored =
|
|
33
|
+
const stored = readEditorStorageValue(RECENT_VECTORS_KEY);
|
|
32
34
|
return stored ? JSON.parse(stored) : [];
|
|
33
35
|
}
|
|
34
36
|
catch (error) {
|
|
@@ -47,7 +49,7 @@ export const vectorUtils = {
|
|
|
47
49
|
},
|
|
48
50
|
...filteredVectors
|
|
49
51
|
].slice(0, MAX_RECENT_VECTORS);
|
|
50
|
-
|
|
52
|
+
writeEditorStorageValue(RECENT_VECTORS_KEY, JSON.stringify(newRecentVectors));
|
|
51
53
|
recentVectorsStore.set(newRecentVectors);
|
|
52
54
|
return newRecentVectors;
|
|
53
55
|
}
|
|
@@ -60,7 +62,7 @@ export const vectorUtils = {
|
|
|
60
62
|
try {
|
|
61
63
|
const recentVectors = this.getRecentVectors();
|
|
62
64
|
const filteredVectors = recentVectors.filter(v => v.name !== name);
|
|
63
|
-
|
|
65
|
+
writeEditorStorageValue(RECENT_VECTORS_KEY, JSON.stringify(filteredVectors));
|
|
64
66
|
return filteredVectors;
|
|
65
67
|
}
|
|
66
68
|
catch (error) {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decides, from the condition a widget's schema gives a control (`autoplay === true`), whether the control is shown.
|
|
3
|
+
*
|
|
4
|
+
* The condition is text that comes with a widget, written by whoever made the widget, and the editor can run inside
|
|
5
|
+
* a customer's own page. So it is never handed to `eval` or `new Function`: that would run the widget author's code
|
|
6
|
+
* with the rights of the customer's page (its cookies, its logged-in visitor, its API calls), and it fails outright
|
|
7
|
+
* under a Content-Security-Policy that does not allow `'unsafe-eval'`. It is read here instead, by a small parser for
|
|
8
|
+
* the part of JavaScript a condition is made of:
|
|
9
|
+
*
|
|
10
|
+
* - values: numbers, 'strings' and "strings", true, false, null, undefined, and [lists];
|
|
11
|
+
* - the settings the condition depends on, by name (`autoplay`), with `.name` and `[key]` to look inside them;
|
|
12
|
+
* - comparisons (`===` `!==` `==` `!=` `<` `<=` `>` `>=`), `&&`, `||`, `??`, `!`, `+ - * / %`, `a ? b : c`, brackets;
|
|
13
|
+
* - the methods `includes`, `indexOf`, `startsWith`, `endsWith`, `toLowerCase`, `toUpperCase` and `trim`.
|
|
14
|
+
*
|
|
15
|
+
* It can read the values it is given and nothing else: no globals (`window`, `document`, `Function`), no other call,
|
|
16
|
+
* no assignment, no template string, no `constructor` or `__proto__`.
|
|
17
|
+
*/
|
|
18
|
+
/** The current value of each setting a condition may name. */
|
|
19
|
+
export type ConditionValues = Record<string, unknown>;
|
|
20
|
+
/** A condition that cannot be read or cannot be worked out. */
|
|
21
|
+
export declare class ConditionError extends Error {
|
|
22
|
+
constructor(message: string);
|
|
23
|
+
}
|
|
24
|
+
/** Longer than any real condition; a limit so that a hostile schema cannot make the editor work for long. */
|
|
25
|
+
export declare const MAX_CONDITION_LENGTH = 1000;
|
|
26
|
+
/** How deeply brackets, lists, `!`s and `?:` may be nested; a limit that keeps the parser from running out of stack. */
|
|
27
|
+
export declare const MAX_CONDITION_DEPTH = 40;
|
|
28
|
+
/**
|
|
29
|
+
* Works out a condition from the values of the settings it names, the way JavaScript would.
|
|
30
|
+
*
|
|
31
|
+
* @throws {ConditionError} when the condition cannot be read, names something that is not in `values`, or cannot be
|
|
32
|
+
* worked out (looking inside a setting that has no value, say).
|
|
33
|
+
*/
|
|
34
|
+
export declare function evaluateCondition(condition: string, values: ConditionValues): unknown;
|
|
35
|
+
/**
|
|
36
|
+
* Whether a control whose condition is `condition` is shown. A condition the editor cannot use (it is not valid, or
|
|
37
|
+
* names a setting it does not depend on) keeps the control shown: a mistake in a widget's schema must not hide one of
|
|
38
|
+
* its settings for good. The problem is reported in the console, once per condition.
|
|
39
|
+
*/
|
|
40
|
+
export declare function conditionHolds(condition: string, values: ConditionValues): boolean;
|