@caretcms/core 0.2.0 → 0.4.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 +195 -12
- package/dist/index.d.ts +26 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +113 -12
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +35 -3
- package/dist/loader.js.map +1 -1
- package/dist/markdown/paragraphs.d.ts +14 -0
- package/dist/markdown/paragraphs.d.ts.map +1 -0
- package/dist/markdown/paragraphs.js +61 -0
- package/dist/markdown/paragraphs.js.map +1 -0
- package/dist/markdown/remark.d.ts.map +1 -1
- package/dist/markdown/remark.js +1 -0
- package/dist/markdown/remark.js.map +1 -1
- package/dist/markdown/satteri.d.ts.map +1 -1
- package/dist/markdown/satteri.js +35 -9
- package/dist/markdown/satteri.js.map +1 -1
- package/dist/markdown/stamp.d.ts +0 -9
- package/dist/markdown/stamp.d.ts.map +1 -1
- package/dist/markdown/stamp.js +6 -0
- package/dist/markdown/stamp.js.map +1 -1
- package/dist/providers/deployment/github.d.ts +27 -0
- package/dist/providers/deployment/github.d.ts.map +1 -0
- package/dist/providers/deployment/github.js +180 -0
- package/dist/providers/deployment/github.js.map +1 -0
- package/dist/providers/deployment/simulated.d.ts +10 -0
- package/dist/providers/deployment/simulated.d.ts.map +1 -0
- package/dist/providers/deployment/simulated.js +29 -0
- package/dist/providers/deployment/simulated.js.map +1 -0
- package/dist/runtime/auth/session.d.ts +1 -0
- package/dist/runtime/auth/session.d.ts.map +1 -1
- package/dist/runtime/auth/session.js +16 -0
- package/dist/runtime/auth/session.js.map +1 -1
- package/dist/runtime/authorization.d.ts +11 -0
- package/dist/runtime/authorization.d.ts.map +1 -0
- package/dist/runtime/authorization.js +29 -0
- package/dist/runtime/authorization.js.map +1 -0
- package/dist/runtime/collection-policy.d.ts +11 -0
- package/dist/runtime/collection-policy.d.ts.map +1 -0
- package/dist/runtime/collection-policy.js +44 -0
- package/dist/runtime/collection-policy.js.map +1 -0
- package/dist/runtime/config.d.ts +3 -0
- package/dist/runtime/config.d.ts.map +1 -1
- package/dist/runtime/config.js +4 -1
- package/dist/runtime/config.js.map +1 -1
- package/dist/runtime/content-errors.d.ts +9 -0
- package/dist/runtime/content-errors.d.ts.map +1 -0
- package/dist/runtime/content-errors.js +17 -0
- package/dist/runtime/content-errors.js.map +1 -0
- package/dist/runtime/content.d.ts.map +1 -1
- package/dist/runtime/content.js +13 -4
- package/dist/runtime/content.js.map +1 -1
- package/dist/runtime/deployment-status.d.ts +14 -0
- package/dist/runtime/deployment-status.d.ts.map +1 -0
- package/dist/runtime/deployment-status.js +57 -0
- package/dist/runtime/deployment-status.js.map +1 -0
- package/dist/runtime/draft-state.d.ts +16 -0
- package/dist/runtime/draft-state.d.ts.map +1 -0
- package/dist/runtime/draft-state.js +29 -0
- package/dist/runtime/draft-state.js.map +1 -0
- package/dist/runtime/i18n.d.ts +97 -0
- package/dist/runtime/i18n.d.ts.map +1 -0
- package/dist/runtime/i18n.js +190 -0
- package/dist/runtime/i18n.js.map +1 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/middleware.d.ts.map +1 -1
- package/dist/runtime/middleware.js +61 -4
- package/dist/runtime/middleware.js.map +1 -1
- package/dist/runtime/mutations/contracts.d.ts +12 -0
- package/dist/runtime/mutations/contracts.d.ts.map +1 -1
- package/dist/runtime/mutations/contracts.js +54 -4
- package/dist/runtime/mutations/contracts.js.map +1 -1
- package/dist/runtime/mutations/engine.d.ts +2 -0
- package/dist/runtime/mutations/engine.d.ts.map +1 -1
- package/dist/runtime/mutations/engine.js +220 -57
- package/dist/runtime/mutations/engine.js.map +1 -1
- package/dist/runtime/providers.d.ts +4 -1
- package/dist/runtime/providers.d.ts.map +1 -1
- package/dist/runtime/providers.js +18 -1
- package/dist/runtime/providers.js.map +1 -1
- package/dist/runtime/publish-recovery.d.ts +32 -0
- package/dist/runtime/publish-recovery.d.ts.map +1 -0
- package/dist/runtime/publish-recovery.js +98 -0
- package/dist/runtime/publish-recovery.js.map +1 -0
- package/dist/runtime/publish.d.ts +10 -4
- package/dist/runtime/publish.d.ts.map +1 -1
- package/dist/runtime/publish.js +105 -63
- package/dist/runtime/publish.js.map +1 -1
- package/dist/runtime/rebuild-webhook.d.ts +4 -0
- package/dist/runtime/rebuild-webhook.d.ts.map +1 -1
- package/dist/runtime/rebuild-webhook.js +12 -2
- package/dist/runtime/rebuild-webhook.js.map +1 -1
- package/dist/runtime/request-context.d.ts +7 -1
- package/dist/runtime/request-context.d.ts.map +1 -1
- package/dist/runtime/request-context.js +7 -1
- package/dist/runtime/request-context.js.map +1 -1
- package/dist/runtime/rewrite.d.ts.map +1 -1
- package/dist/runtime/rewrite.js +28 -2
- package/dist/runtime/rewrite.js.map +1 -1
- package/dist/runtime/rich-allowlist.js +1 -1
- package/dist/runtime/rich-allowlist.js.map +1 -1
- package/dist/runtime/routes/admin-entry.d.ts.map +1 -1
- package/dist/runtime/routes/admin-entry.js +129 -21
- package/dist/runtime/routes/admin-entry.js.map +1 -1
- package/dist/runtime/routes/admin.d.ts.map +1 -1
- package/dist/runtime/routes/admin.js +28 -14
- package/dist/runtime/routes/admin.js.map +1 -1
- package/dist/runtime/routes/auth-login.d.ts.map +1 -1
- package/dist/runtime/routes/auth-login.js +4 -0
- package/dist/runtime/routes/auth-login.js.map +1 -1
- package/dist/runtime/routes/auth-logout.d.ts.map +1 -1
- package/dist/runtime/routes/auth-logout.js +7 -2
- package/dist/runtime/routes/auth-logout.js.map +1 -1
- package/dist/runtime/routes/auth-session.d.ts.map +1 -1
- package/dist/runtime/routes/auth-session.js +10 -2
- package/dist/runtime/routes/auth-session.js.map +1 -1
- package/dist/runtime/routes/collection-list.d.ts.map +1 -1
- package/dist/runtime/routes/collection-list.js +213 -66
- package/dist/runtime/routes/collection-list.js.map +1 -1
- package/dist/runtime/routes/deployment.d.ts +5 -0
- package/dist/runtime/routes/deployment.d.ts.map +1 -0
- package/dist/runtime/routes/deployment.js +38 -0
- package/dist/runtime/routes/deployment.js.map +1 -0
- package/dist/runtime/routes/draft.d.ts.map +1 -1
- package/dist/runtime/routes/draft.js +10 -3
- package/dist/runtime/routes/draft.js.map +1 -1
- package/dist/runtime/routes/entries.d.ts.map +1 -1
- package/dist/runtime/routes/entries.js +87 -43
- package/dist/runtime/routes/entries.js.map +1 -1
- package/dist/runtime/routes/history.d.ts.map +1 -1
- package/dist/runtime/routes/history.js +40 -21
- package/dist/runtime/routes/history.js.map +1 -1
- package/dist/runtime/routes/mutate.d.ts.map +1 -1
- package/dist/runtime/routes/mutate.js +4 -1
- package/dist/runtime/routes/mutate.js.map +1 -1
- package/dist/runtime/routes/publish.d.ts.map +1 -1
- package/dist/runtime/routes/publish.js +127 -33
- package/dist/runtime/routes/publish.js.map +1 -1
- package/dist/runtime/routes/schema.d.ts.map +1 -1
- package/dist/runtime/routes/schema.js +11 -2
- package/dist/runtime/routes/schema.js.map +1 -1
- package/dist/runtime/routes/studio-home.d.ts.map +1 -1
- package/dist/runtime/routes/studio-home.js +41 -20
- package/dist/runtime/routes/studio-home.js.map +1 -1
- package/dist/runtime/routes/upload.d.ts.map +1 -1
- package/dist/runtime/routes/upload.js +3 -0
- package/dist/runtime/routes/upload.js.map +1 -1
- package/dist/runtime/schema-registry.d.ts +8 -1
- package/dist/runtime/schema-registry.d.ts.map +1 -1
- package/dist/runtime/schema-registry.js +49 -3
- package/dist/runtime/schema-registry.js.map +1 -1
- package/dist/runtime/storage/entry-commit.d.ts +8 -0
- package/dist/runtime/storage/entry-commit.d.ts.map +1 -0
- package/dist/runtime/storage/entry-commit.js +45 -0
- package/dist/runtime/storage/entry-commit.js.map +1 -0
- package/dist/runtime/storage/filesystem-adapter.d.ts +5 -0
- package/dist/runtime/storage/filesystem-adapter.d.ts.map +1 -1
- package/dist/runtime/storage/filesystem-adapter.js +12 -3
- package/dist/runtime/storage/filesystem-adapter.js.map +1 -1
- package/dist/runtime/storage/frontmatter-codec.d.ts +5 -23
- package/dist/runtime/storage/frontmatter-codec.d.ts.map +1 -1
- package/dist/runtime/storage/frontmatter-codec.js +156 -13
- package/dist/runtime/storage/frontmatter-codec.js.map +1 -1
- package/dist/runtime/storage/in-memory-adapter.d.ts +7 -0
- package/dist/runtime/storage/in-memory-adapter.d.ts.map +1 -1
- package/dist/runtime/storage/in-memory-adapter.js +6 -0
- package/dist/runtime/storage/in-memory-adapter.js.map +1 -1
- package/dist/runtime/storage/markdown-adapter.d.ts.map +1 -1
- package/dist/runtime/storage/markdown-adapter.js +18 -20
- package/dist/runtime/storage/markdown-adapter.js.map +1 -1
- package/dist/runtime/storage/session-overlay-adapter.d.ts +12 -4
- package/dist/runtime/storage/session-overlay-adapter.d.ts.map +1 -1
- package/dist/runtime/storage/session-overlay-adapter.js +100 -13
- package/dist/runtime/storage/session-overlay-adapter.js.map +1 -1
- package/dist/runtime/storage/sidecar-meta-store.d.ts +5 -0
- package/dist/runtime/storage/sidecar-meta-store.d.ts.map +1 -1
- package/dist/runtime/storage/sidecar-meta-store.js +48 -5
- package/dist/runtime/storage/sidecar-meta-store.js.map +1 -1
- package/dist/runtime/themes/preset-tokens.js +4 -4
- package/dist/runtime/themes/preset-tokens.js.map +1 -1
- package/dist/runtime/utils.d.ts.map +1 -1
- package/dist/runtime/utils.js +3 -4
- package/dist/runtime/utils.js.map +1 -1
- package/dist/runtime/views/studio-layout.d.ts +3 -0
- package/dist/runtime/views/studio-layout.d.ts.map +1 -1
- package/dist/runtime/views/studio-layout.js +36 -13
- package/dist/runtime/views/studio-layout.js.map +1 -1
- package/dist/schema-utils.d.ts +7 -0
- package/dist/schema-utils.d.ts.map +1 -1
- package/dist/schema-utils.js +143 -0
- package/dist/schema-utils.js.map +1 -1
- package/dist/types.d.ts +165 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +12 -3
- package/static/cms/admin-entry.css +144 -13
- package/static/cms/admin-entry.js +539 -586
- package/static/cms/dev-toolbar/app.js +78 -19
- package/static/cms/editor/config.js +35 -15
- package/static/cms/editor/content-map.css +8 -1
- package/static/cms/editor/content-map.js +62 -23
- package/static/cms/editor/deployment-status.js +66 -0
- package/static/cms/editor/draft-client.js +87 -0
- package/static/cms/editor/guards.js +20 -0
- package/static/cms/editor/helpers.js +30 -1
- package/static/cms/editor/highlight.js +1 -0
- package/static/cms/editor/image-edit.js +44 -1
- package/static/cms/editor/image-utils.js +23 -7
- package/static/cms/editor/link-follow.js +122 -41
- package/static/cms/editor/link-ui.css +10 -1
- package/static/cms/editor/linkify.js +2 -0
- package/static/cms/editor/md-block-edit.js +140 -37
- package/static/cms/editor/md-paragraphs.js +98 -0
- package/static/cms/editor/md-serialize.js +1 -1
- package/static/cms/editor/panel.js +53 -7
- package/static/cms/editor/preview-mode.js +44 -0
- package/static/cms/editor/publish-result.js +100 -0
- package/static/cms/editor/rich-toolbar.js +58 -15
- package/static/cms/editor/sanitize.js +6 -2
- package/static/cms/editor/save-queue.js +75 -17
- package/static/cms/editor/section-controls/activation.js +3 -0
- package/static/cms/editor/section-controls/api.js +25 -7
- package/static/cms/editor/section-controls/index.js +60 -8
- package/static/cms/editor/section-controls/model.js +39 -4
- package/static/cms/editor/section-controls/mutations.js +15 -0
- package/static/cms/editor/section-controls/page-context.js +9 -0
- package/static/cms/editor/section-controls/pickers.js +53 -2
- package/static/cms/editor/section-controls/reorder.js +23 -0
- package/static/cms/editor/section-controls/selection.js +6 -0
- package/static/cms/editor/section-controls/spacing-drag.js +16 -0
- package/static/cms/editor/section-controls/ui-elements.js +7 -0
- package/static/cms/editor/section-controls/utils.js +7 -0
- package/static/cms/editor/section-controls.css +18 -3
- package/static/cms/editor/security.js +13 -2
- package/static/cms/editor/stega-hydrate.js +17 -4
- package/static/cms/editor/sync.js +475 -56
- package/static/cms/editor/text-edit.js +37 -3
- package/static/cms/editor/text-link-interactions.js +59 -17
- package/static/cms/editor/toast.js +23 -9
- package/static/cms/editor/tokens-and-text.css +9 -0
- package/static/cms/editor/toolbar.css +5 -0
- package/static/cms/editor/toolbar.js +91 -101
- package/static/cms/editor.css +1 -1
- package/static/cms/editor.js +1 -0
- package/static/cms/studio/entry-loader.js +102 -0
- package/static/cms/studio/field-model.js +110 -0
- package/static/cms/studio/fields.js +319 -0
- package/static/cms/studio/history-client.js +74 -0
- package/static/cms/studio/mutation-client.js +74 -0
- package/static/cms/studio/sync-client.js +107 -0
- package/static/cms/studio/upload-client.js +109 -0
- package/static/cms/studio-tokens.css +4 -4
- package/static/cms/studio.css +24 -0
|
@@ -1,72 +1,491 @@
|
|
|
1
1
|
import { sanitizeHtml } from './sanitize.js';
|
|
2
|
+
import { parseCaretAttr, resolveBinding } from './helpers.js';
|
|
3
|
+
import { STUDIO_PATH } from './config.js';
|
|
2
4
|
|
|
5
|
+
const SYNC_CHANNEL = 'caretcms:content';
|
|
6
|
+
const PENDING_SELECTION_KEY = 'caretcms:pending-preview-selection';
|
|
7
|
+
|
|
8
|
+
/** @typedef {{ dirtyEls: Set<Element> }} InlineEditorState */
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {object} SelectionMessage
|
|
11
|
+
* @property {'cms:field-selected'} type
|
|
12
|
+
* @property {string} collection
|
|
13
|
+
* @property {string} id
|
|
14
|
+
* @property {string} field
|
|
15
|
+
* @property {string} [previewPath]
|
|
16
|
+
* @property {boolean} [embedded]
|
|
17
|
+
* @property {string} [source]
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} EntryReadyMessage
|
|
21
|
+
* @property {'cms:entry-ready'} type
|
|
22
|
+
* @property {string} collection
|
|
23
|
+
* @property {string} id
|
|
24
|
+
* @property {boolean} [embedded]
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {object} ChangeMessage
|
|
28
|
+
* @property {'cms:preview' | 'cms:saved' | 'cms:created' | 'cms:deleted'} type
|
|
29
|
+
* @property {string} collection
|
|
30
|
+
* @property {string} id
|
|
31
|
+
* @property {Record<string, unknown> | null} [data]
|
|
32
|
+
* @property {string} [studioPath]
|
|
33
|
+
* @property {boolean} [embedded]
|
|
34
|
+
*/
|
|
35
|
+
/** @typedef {SelectionMessage | EntryReadyMessage | ChangeMessage} SyncMessage */
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {object} PendingSelection
|
|
38
|
+
* @property {string} collection
|
|
39
|
+
* @property {string} id
|
|
40
|
+
* @property {string} field
|
|
41
|
+
* @property {number} savedAt
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {unknown} value
|
|
46
|
+
* @returns {value is Record<string, unknown>}
|
|
47
|
+
*/
|
|
48
|
+
function isRecord(value) {
|
|
49
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @param {unknown} value @returns {value is string} */
|
|
53
|
+
function isNonEmptyString(value) {
|
|
54
|
+
return typeof value === 'string' && value.length > 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Normalize messages before they reach selectors, navigation, or DOM updates.
|
|
59
|
+
* @param {unknown} value
|
|
60
|
+
* @returns {SyncMessage | null}
|
|
61
|
+
*/
|
|
62
|
+
export function normalizeSyncMessage(value) {
|
|
63
|
+
if (!isRecord(value) || typeof value.type !== 'string') return null;
|
|
64
|
+
if (!isNonEmptyString(value.collection) || !isNonEmptyString(value.id)) return null;
|
|
65
|
+
|
|
66
|
+
const common = {
|
|
67
|
+
collection: value.collection,
|
|
68
|
+
id: value.id,
|
|
69
|
+
...(value.embedded === true ? { embedded: true } : {}),
|
|
70
|
+
};
|
|
71
|
+
if (value.type === 'cms:field-selected') {
|
|
72
|
+
if (!isNonEmptyString(value.field)) return null;
|
|
73
|
+
return {
|
|
74
|
+
...common,
|
|
75
|
+
type: value.type,
|
|
76
|
+
field: value.field,
|
|
77
|
+
...(typeof value.previewPath === 'string' ? { previewPath: value.previewPath } : {}),
|
|
78
|
+
...(typeof value.source === 'string' ? { source: value.source } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (value.type === 'cms:entry-ready') return { ...common, type: value.type };
|
|
82
|
+
if (!['cms:preview', 'cms:saved', 'cms:created', 'cms:deleted'].includes(value.type)) return null;
|
|
83
|
+
if (value.data !== undefined && value.data !== null && !isRecord(value.data)) return null;
|
|
84
|
+
return {
|
|
85
|
+
...common,
|
|
86
|
+
type: /** @type {ChangeMessage['type']} */ (value.type),
|
|
87
|
+
...(value.data !== undefined ? { data: value.data } : {}),
|
|
88
|
+
...(typeof value.studioPath === 'string' ? { studioPath: value.studioPath } : {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {unknown} value
|
|
94
|
+
* @returns {PendingSelection | null}
|
|
95
|
+
*/
|
|
96
|
+
export function normalizePendingSelection(value) {
|
|
97
|
+
if (!isRecord(value)) return null;
|
|
98
|
+
if (
|
|
99
|
+
!isNonEmptyString(value.collection)
|
|
100
|
+
|| !isNonEmptyString(value.id)
|
|
101
|
+
|| !isNonEmptyString(value.field)
|
|
102
|
+
|| typeof value.savedAt !== 'number'
|
|
103
|
+
|| !Number.isFinite(value.savedAt)
|
|
104
|
+
) return null;
|
|
105
|
+
return {
|
|
106
|
+
collection: value.collection,
|
|
107
|
+
id: value.id,
|
|
108
|
+
field: value.field,
|
|
109
|
+
savedAt: value.savedAt,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {object} options
|
|
115
|
+
* @param {HTMLIFrameElement | null} options.studioIframe
|
|
116
|
+
* @param {InlineEditorState} options.state
|
|
117
|
+
* @param {(message: string, kind: 'success' | 'error') => void} options.showToast
|
|
118
|
+
* @param {(text: string) => string} options.clientLinkify
|
|
119
|
+
*/
|
|
3
120
|
export function mountStudioSync({ studioIframe, state, showToast, clientLinkify }) {
|
|
4
|
-
|
|
121
|
+
let refreshTimer = 0;
|
|
122
|
+
/** @type {Element | null} */
|
|
123
|
+
let selectedInlineEl = null;
|
|
124
|
+
let linkedSelectionTimer = 0;
|
|
125
|
+
/** @type {SelectionMessage | null} */
|
|
126
|
+
let pendingStudioSelection = null;
|
|
127
|
+
/** @type {SelectionMessage | null} */
|
|
128
|
+
let activeStudioSelection = null;
|
|
5
129
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
130
|
+
/**
|
|
131
|
+
* @param {string} collection
|
|
132
|
+
* @param {string} id
|
|
133
|
+
* @param {string} field
|
|
134
|
+
* @returns {Element[]}
|
|
135
|
+
*/
|
|
136
|
+
function matchingElements(collection, id, field) {
|
|
137
|
+
const fullKey = `${collection}::${id}::${field}`;
|
|
138
|
+
const matches = Array.from(document.querySelectorAll(`[data-caret="${CSS.escape(fullKey)}"]`));
|
|
139
|
+
document.querySelectorAll(`[data-caret="${CSS.escape(field)}"]`).forEach((el) => {
|
|
140
|
+
const scope = el.closest('[data-caret-scope]')?.getAttribute('data-caret-scope');
|
|
141
|
+
if (scope === `${collection}::${id}` && !matches.includes(el)) matches.push(el);
|
|
142
|
+
});
|
|
143
|
+
return matches;
|
|
144
|
+
}
|
|
11
145
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
146
|
+
/**
|
|
147
|
+
* @param {string} collection
|
|
148
|
+
* @param {string} id
|
|
149
|
+
* @param {string} field
|
|
150
|
+
* @param {boolean} shouldScroll
|
|
151
|
+
*/
|
|
152
|
+
function selectInlineField(collection, id, field, shouldScroll) {
|
|
153
|
+
const target = matchingElements(collection, id, field)[0];
|
|
154
|
+
if (!target) return false;
|
|
155
|
+
|
|
156
|
+
window.clearTimeout(linkedSelectionTimer);
|
|
157
|
+
if (selectedInlineEl && selectedInlineEl !== target) {
|
|
158
|
+
selectedInlineEl.classList.remove('cms-linked-selection');
|
|
159
|
+
}
|
|
160
|
+
selectedInlineEl = target;
|
|
161
|
+
target.classList.add('cms-linked-selection');
|
|
162
|
+
if (shouldScroll) {
|
|
163
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Blue confirms that the paired field was located. Green remains the
|
|
167
|
+
// distinct confirmation that an edit was actually saved.
|
|
168
|
+
linkedSelectionTimer = window.setTimeout(() => {
|
|
169
|
+
if (selectedInlineEl !== target) return;
|
|
170
|
+
target.classList.remove('cms-linked-selection');
|
|
171
|
+
selectedInlineEl = null;
|
|
172
|
+
}, 1500);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** @param {unknown} path */
|
|
177
|
+
function previewTarget(path) {
|
|
178
|
+
if (typeof path !== 'string' || !path.startsWith('/') || path.startsWith('//')) return null;
|
|
179
|
+
try {
|
|
180
|
+
const target = new URL(path, window.location.origin);
|
|
181
|
+
return target.origin === window.location.origin ? target : null;
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** @param {SelectionMessage} message */
|
|
188
|
+
function selectOrNavigate(message) {
|
|
189
|
+
if (selectInlineField(message.collection, message.id, message.field, true)) return;
|
|
190
|
+
|
|
191
|
+
const target = previewTarget(message.previewPath);
|
|
192
|
+
if (!target || target.href === window.location.href) return;
|
|
193
|
+
if (state.dirtyEls.size > 0) {
|
|
194
|
+
showToast('Finish or cancel your inline edit before opening another page', 'error');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
sessionStorage.setItem(PENDING_SELECTION_KEY, JSON.stringify({
|
|
200
|
+
collection: message.collection,
|
|
201
|
+
id: message.id,
|
|
202
|
+
field: message.field,
|
|
203
|
+
savedAt: Date.now(),
|
|
204
|
+
}));
|
|
205
|
+
} catch {
|
|
206
|
+
// Navigation still works without the post-load highlight handoff.
|
|
207
|
+
}
|
|
208
|
+
window.location.assign(target.href);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** @param {SelectionMessage} message */
|
|
212
|
+
function publishSelection(message) {
|
|
213
|
+
if (!('BroadcastChannel' in window)) return;
|
|
214
|
+
try {
|
|
215
|
+
const channel = new BroadcastChannel(SYNC_CHANNEL);
|
|
216
|
+
channel.postMessage(message);
|
|
217
|
+
window.setTimeout(() => channel.close(), 0);
|
|
218
|
+
} catch {
|
|
219
|
+
// Cross-tab selection linking is a progressive enhancement.
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** @param {SelectionMessage} message */
|
|
224
|
+
function studioEntryPath(message) {
|
|
225
|
+
if (!STUDIO_PATH) return null;
|
|
226
|
+
return `${STUDIO_PATH}/${encodeURIComponent(message.collection)}/${encodeURIComponent(message.id)}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** @param {SelectionMessage} message */
|
|
230
|
+
function postEmbeddedSelection(message) {
|
|
231
|
+
if (!studioIframe?.contentWindow) return;
|
|
232
|
+
try {
|
|
233
|
+
studioIframe.contentWindow.postMessage({ ...message, embedded: true }, window.location.origin);
|
|
234
|
+
} catch {
|
|
235
|
+
// Embedded field linking is a progressive enhancement.
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** @param {SelectionMessage} message */
|
|
240
|
+
function deliverEmbeddedSelection(message) {
|
|
241
|
+
if (!studioIframe?.contentWindow) return;
|
|
242
|
+
const targetPath = studioEntryPath(message);
|
|
243
|
+
if (!targetPath) return;
|
|
244
|
+
|
|
245
|
+
// A normal mouse interaction can emit pointerdown, focusin, and click.
|
|
246
|
+
// Keep the latest field without restarting the same iframe navigation for
|
|
247
|
+
// each event in that sequence.
|
|
248
|
+
if (pendingStudioSelection && studioIframe.getAttribute('src') === targetPath) {
|
|
249
|
+
pendingStudioSelection = message;
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
if (studioIframe.contentWindow.location.pathname !== targetPath) {
|
|
255
|
+
pendingStudioSelection = message;
|
|
256
|
+
studioIframe.src = targetPath;
|
|
257
|
+
try {
|
|
258
|
+
sessionStorage.setItem('cms-panel-path', targetPath);
|
|
259
|
+
} catch {
|
|
260
|
+
// Remembering the route is optional; the current navigation is not.
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
} catch {
|
|
265
|
+
// Cross-origin Studio panels cannot be inspected or navigated safely;
|
|
266
|
+
// preserve the existing postMessage-only behavior for those setups.
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
postEmbeddedSelection(message);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
studioIframe?.addEventListener('load', () => {
|
|
273
|
+
if (!pendingStudioSelection) return;
|
|
274
|
+
const message = pendingStudioSelection;
|
|
275
|
+
pendingStudioSelection = null;
|
|
276
|
+
window.requestAnimationFrame(() => postEmbeddedSelection(message));
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
/** @param {Element} el */
|
|
280
|
+
function selectStudioField(el) {
|
|
281
|
+
const resolved = resolveBinding(el, parseCaretAttr(el.getAttribute('data-caret') || ''));
|
|
282
|
+
if (!resolved) return;
|
|
283
|
+
|
|
284
|
+
selectInlineField(resolved.collection, resolved.id, resolved.field, false);
|
|
285
|
+
/** @type {SelectionMessage} */
|
|
286
|
+
const message = {
|
|
287
|
+
type: 'cms:field-selected',
|
|
288
|
+
collection: resolved.collection,
|
|
289
|
+
id: resolved.id,
|
|
290
|
+
field: resolved.field,
|
|
291
|
+
source: 'inline',
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
activeStudioSelection = message;
|
|
295
|
+
deliverEmbeddedSelection(message);
|
|
296
|
+
publishSelection(message);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Pointerdown covers image-like bindings; focusin covers keyboard navigation
|
|
300
|
+
// and text editing. Click catches non-focusable bound elements.
|
|
301
|
+
document.addEventListener('pointerdown', (event) => {
|
|
302
|
+
const el = event.target instanceof Element ? event.target.closest('[data-caret]') : null;
|
|
303
|
+
if (el) selectStudioField(el);
|
|
304
|
+
});
|
|
305
|
+
document.addEventListener('click', (event) => {
|
|
306
|
+
const el = event.target instanceof Element ? event.target.closest('[data-caret]') : null;
|
|
307
|
+
if (el) selectStudioField(el);
|
|
308
|
+
});
|
|
309
|
+
document.addEventListener('focusin', (event) => {
|
|
310
|
+
const el = event.target instanceof Element ? event.target.closest('[data-caret]') : null;
|
|
311
|
+
if (el) selectStudioField(el);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
/** @param {SyncMessage} msg */
|
|
315
|
+
function rememberStudioPath(msg) {
|
|
316
|
+
if (!('studioPath' in msg) || typeof msg.studioPath !== 'string') return;
|
|
317
|
+
try {
|
|
318
|
+
sessionStorage.setItem('cms-panel-path', msg.studioPath);
|
|
319
|
+
} catch {
|
|
320
|
+
// Remembering the current Studio route is a progressive enhancement.
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** @param {SyncMessage} msg */
|
|
325
|
+
function scheduleEmbeddedRefresh(msg) {
|
|
326
|
+
rememberStudioPath(msg);
|
|
327
|
+
|
|
328
|
+
if (state.dirtyEls.size > 0) {
|
|
329
|
+
showToast('Saved in Studio — finish your inline edit before refreshing', 'success');
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
showToast('Saved — refreshing visual preview…', 'success');
|
|
334
|
+
window.clearTimeout(refreshTimer);
|
|
335
|
+
refreshTimer = window.setTimeout(() => window.location.reload(), 280);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** @param {ChangeMessage} msg */
|
|
339
|
+
function applySavedMessage(msg) {
|
|
340
|
+
const { collection, id, data } = msg;
|
|
341
|
+
if (!data || typeof data !== 'object') return 0;
|
|
342
|
+
|
|
343
|
+
let updated = 0;
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* @param {string} selector
|
|
347
|
+
* @param {string} path
|
|
348
|
+
* @returns {Element[]}
|
|
349
|
+
*/
|
|
350
|
+
function boundElements(selector, path) {
|
|
351
|
+
const matches = Array.from(document.querySelectorAll(`[data-caret="${CSS.escape(selector)}"]`));
|
|
352
|
+
document.querySelectorAll(`[data-caret="${CSS.escape(path)}"]`).forEach((el) => {
|
|
353
|
+
const scope = el.closest('[data-caret-scope]')?.getAttribute('data-caret-scope');
|
|
354
|
+
if (scope === `${collection}::${id}` && !matches.includes(el)) matches.push(el);
|
|
355
|
+
});
|
|
356
|
+
return matches;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* @param {Record<string, unknown>} obj
|
|
361
|
+
* @param {string} prefix
|
|
362
|
+
*/
|
|
363
|
+
function walkData(obj, prefix) {
|
|
364
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
365
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
366
|
+
const selector = `${collection}::${id}::${path}`;
|
|
367
|
+
|
|
368
|
+
if (typeof value === 'string') {
|
|
369
|
+
const els = boundElements(selector, path);
|
|
370
|
+
els.forEach((el) => {
|
|
371
|
+
// Skip elements being actively edited to prevent overwriting unsaved work
|
|
372
|
+
if (state.dirtyEls.has(el) || el === document.activeElement) return;
|
|
373
|
+
|
|
374
|
+
if (el instanceof HTMLImageElement) {
|
|
375
|
+
if (el.src !== value && el.getAttribute('src') !== value) {
|
|
376
|
+
el.src = value;
|
|
50
377
|
updated++;
|
|
51
378
|
}
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
379
|
+
} else if (el.hasAttribute('data-caret-rich')) {
|
|
380
|
+
const sanitized = sanitizeHtml(value);
|
|
381
|
+
if (el.innerHTML !== sanitized) {
|
|
382
|
+
el.innerHTML = sanitized;
|
|
383
|
+
updated++;
|
|
384
|
+
}
|
|
385
|
+
} else if (el.hasAttribute('data-caret-raw')) {
|
|
386
|
+
if (el.getAttribute('data-caret-raw') !== value) {
|
|
387
|
+
el.setAttribute('data-caret-raw', value);
|
|
388
|
+
el.innerHTML = clientLinkify(value);
|
|
389
|
+
updated++;
|
|
390
|
+
}
|
|
391
|
+
} else if (el.textContent !== value) {
|
|
392
|
+
el.textContent = value;
|
|
393
|
+
updated++;
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
} else if (isRecord(value)) {
|
|
397
|
+
walkData(value, path);
|
|
56
398
|
}
|
|
57
399
|
}
|
|
400
|
+
}
|
|
58
401
|
|
|
59
|
-
|
|
402
|
+
walkData(data, '');
|
|
403
|
+
return updated;
|
|
404
|
+
}
|
|
60
405
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
406
|
+
window.addEventListener('message', (e) => {
|
|
407
|
+
// Only accept messages from our iframe
|
|
408
|
+
if (!studioIframe || e.source !== studioIframe.contentWindow) return;
|
|
409
|
+
const msg = normalizeSyncMessage(e.data);
|
|
410
|
+
if (!msg) return;
|
|
411
|
+
|
|
412
|
+
if (
|
|
413
|
+
msg.type === 'cms:entry-ready'
|
|
414
|
+
&& activeStudioSelection
|
|
415
|
+
&& msg.collection === activeStudioSelection.collection
|
|
416
|
+
&& msg.id === activeStudioSelection.id
|
|
417
|
+
) {
|
|
418
|
+
postEmbeddedSelection(activeStudioSelection);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (msg.type === 'cms:preview') {
|
|
423
|
+
applySavedMessage(msg);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (msg.type === 'cms:field-selected') {
|
|
428
|
+
selectOrNavigate(msg);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (msg.type === 'cms:saved') {
|
|
433
|
+
applySavedMessage(msg);
|
|
434
|
+
scheduleEmbeddedRefresh(msg);
|
|
66
435
|
}
|
|
67
436
|
|
|
68
|
-
if (msg.type === 'cms:created') {
|
|
69
|
-
|
|
437
|
+
if (msg.type === 'cms:created' || msg.type === 'cms:deleted') {
|
|
438
|
+
scheduleEmbeddedRefresh(msg);
|
|
70
439
|
}
|
|
71
440
|
});
|
|
441
|
+
|
|
442
|
+
// Standalone Studio pages live in a separate tab, so postMessage cannot
|
|
443
|
+
// reach the site preview. BroadcastChannel provides a same-origin bridge.
|
|
444
|
+
// Reload only after a confirmed mutation. The server remains the source of
|
|
445
|
+
// truth for lists, routing, and derived UI; sites can make the refresh feel
|
|
446
|
+
// continuous with their own cross-document view transitions.
|
|
447
|
+
if ('BroadcastChannel' in window) {
|
|
448
|
+
try {
|
|
449
|
+
const channel = new BroadcastChannel(SYNC_CHANNEL);
|
|
450
|
+
channel.addEventListener('message', (event) => {
|
|
451
|
+
const msg = normalizeSyncMessage(event.data);
|
|
452
|
+
if (!msg || msg.embedded === true) return;
|
|
453
|
+
|
|
454
|
+
if (msg.type === 'cms:field-selected') {
|
|
455
|
+
selectOrNavigate(msg);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (!['cms:saved', 'cms:created', 'cms:deleted'].includes(msg.type)) return;
|
|
459
|
+
|
|
460
|
+
if (state.dirtyEls.size > 0) {
|
|
461
|
+
showToast('Saved in Studio — finish your inline edit before refreshing', 'success');
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (msg.type === 'cms:saved') applySavedMessage(msg);
|
|
466
|
+
showToast('Saved — refreshing preview…', 'success');
|
|
467
|
+
window.clearTimeout(refreshTimer);
|
|
468
|
+
refreshTimer = window.setTimeout(() => window.location.reload(), 280);
|
|
469
|
+
});
|
|
470
|
+
window.addEventListener('pagehide', () => channel.close(), { once: true });
|
|
471
|
+
} catch {
|
|
472
|
+
// Cross-tab preview sync is progressive enhancement; iframe sync above
|
|
473
|
+
// remains available in browsers without BroadcastChannel.
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
const rawPending = sessionStorage.getItem(PENDING_SELECTION_KEY);
|
|
479
|
+
if (rawPending) {
|
|
480
|
+
sessionStorage.removeItem(PENDING_SELECTION_KEY);
|
|
481
|
+
const pending = normalizePendingSelection(JSON.parse(rawPending));
|
|
482
|
+
if (pending && Date.now() - pending.savedAt < 30_000) {
|
|
483
|
+
window.requestAnimationFrame(() => {
|
|
484
|
+
selectInlineField(pending.collection, pending.id, pending.field, true);
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
} catch {
|
|
489
|
+
// Pending selection recovery is a progressive enhancement.
|
|
490
|
+
}
|
|
72
491
|
}
|
|
@@ -2,6 +2,22 @@ import { createTextLinkInteractions } from './text-link-interactions.js';
|
|
|
2
2
|
import { resolveBinding } from './helpers.js';
|
|
3
3
|
import { sanitizeHtml } from './sanitize.js';
|
|
4
4
|
|
|
5
|
+
/** @typedef {HTMLElement & { _caretSkipSave?: boolean }} EditableElement */
|
|
6
|
+
/** @typedef {{ collection: string, id: string, field: string }} ResolvedCaretBinding */
|
|
7
|
+
/** @typedef {{ collection: string | null, id: string | null, field: string }} ParsedCaretBinding */
|
|
8
|
+
/** @typedef {{ dirtyEls: Set<Element>, linkPopupEl: HTMLElement | null }} TextEditorState */
|
|
9
|
+
/** @typedef {{ ok: true, revision?: number } | { ok: false, reason: 'conflict' | 'unauthorized' | 'error', latestValue?: unknown }} SaveResult */
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {((message: string, kind: 'success' | 'error') => unknown) & {
|
|
12
|
+
* conflict: (options: {
|
|
13
|
+
* message: string,
|
|
14
|
+
* onKeepMine: () => void | Promise<void>,
|
|
15
|
+
* onLoadLatest: () => void,
|
|
16
|
+
* }) => unknown,
|
|
17
|
+
* }} ConflictToast
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** @type {WeakMap<HTMLElement, string>} */
|
|
5
21
|
const snapshots = new WeakMap();
|
|
6
22
|
let keydownBound = false;
|
|
7
23
|
|
|
@@ -19,6 +35,15 @@ const PLAINTEXT_ONLY_SUPPORTED = (() => {
|
|
|
19
35
|
}
|
|
20
36
|
})();
|
|
21
37
|
|
|
38
|
+
/**
|
|
39
|
+
* @param {object} options
|
|
40
|
+
* @param {TextEditorState} options.state
|
|
41
|
+
* @param {(attr: string) => ParsedCaretBinding | null} options.parseCaretAttr
|
|
42
|
+
* @param {(text: string) => string} options.clientLinkify
|
|
43
|
+
* @param {(collection: string, id: string, field: string, value: string) => Promise<SaveResult>} options.saveField
|
|
44
|
+
* @param {(element: Element, success: boolean) => void} options.flash
|
|
45
|
+
* @param {ConflictToast} options.showToast
|
|
46
|
+
*/
|
|
22
47
|
export function mountTextEditors({
|
|
23
48
|
state,
|
|
24
49
|
parseCaretAttr,
|
|
@@ -27,6 +52,7 @@ export function mountTextEditors({
|
|
|
27
52
|
flash,
|
|
28
53
|
showToast,
|
|
29
54
|
}) {
|
|
55
|
+
/** @param {HTMLElement} target @param {string} value */
|
|
30
56
|
const applyTextValue = (target, value) => {
|
|
31
57
|
if (target.hasAttribute('data-caret-rich')) {
|
|
32
58
|
target.innerHTML = sanitizeHtml(value);
|
|
@@ -37,6 +63,7 @@ export function mountTextEditors({
|
|
|
37
63
|
target.textContent = value;
|
|
38
64
|
}
|
|
39
65
|
};
|
|
66
|
+
/** @param {string} attr @param {string} value */
|
|
40
67
|
const syncBoundText = (attr, value) => {
|
|
41
68
|
document.querySelectorAll('[data-caret]').forEach((node) => {
|
|
42
69
|
if (!(node instanceof HTMLElement)) return;
|
|
@@ -58,11 +85,13 @@ export function mountTextEditors({
|
|
|
58
85
|
}
|
|
59
86
|
|
|
60
87
|
document.querySelectorAll('[data-caret]').forEach((el) => {
|
|
61
|
-
if (el instanceof HTMLImageElement) return;
|
|
88
|
+
if (!(el instanceof HTMLElement) || el instanceof HTMLImageElement) return;
|
|
89
|
+
const editableEl = /** @type {EditableElement} */ (el);
|
|
62
90
|
if (el.dataset.caretTextMounted === 'true') return;
|
|
63
91
|
el.dataset.caretTextMounted = 'true';
|
|
64
92
|
|
|
65
93
|
const attr = el.getAttribute('data-caret');
|
|
94
|
+
if (!attr) return;
|
|
66
95
|
const isRich = el.hasAttribute('data-caret-rich');
|
|
67
96
|
const isLinkable = !isRich && el.hasAttribute('data-caret-raw');
|
|
68
97
|
|
|
@@ -121,6 +150,11 @@ export function mountTextEditors({
|
|
|
121
150
|
// (the save queue refreshed the cached revision on the 409, so the retry
|
|
122
151
|
// targets the current revision and overwrites); "Load latest" replaces the
|
|
123
152
|
// field with the server value. The element stays dirty until resolved.
|
|
153
|
+
/**
|
|
154
|
+
* @param {ResolvedCaretBinding} resolved
|
|
155
|
+
* @param {string} mine
|
|
156
|
+
* @param {unknown} latestValue
|
|
157
|
+
*/
|
|
124
158
|
const runConflict = (resolved, mine, latestValue) => {
|
|
125
159
|
state.dirtyEls.add(el);
|
|
126
160
|
el.classList.add('cms-conflict');
|
|
@@ -161,8 +195,8 @@ export function mountTextEditors({
|
|
|
161
195
|
if (state.linkPopupEl === el) return;
|
|
162
196
|
|
|
163
197
|
// Skip save if Escape was pressed
|
|
164
|
-
if (
|
|
165
|
-
|
|
198
|
+
if (editableEl._caretSkipSave) {
|
|
199
|
+
editableEl._caretSkipSave = false;
|
|
166
200
|
state.dirtyEls.delete(el);
|
|
167
201
|
if (isRich) {
|
|
168
202
|
el.innerHTML = snapshots.get(el) || '';
|