@caretcms/core 0.3.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 +116 -11
- package/dist/index.d.ts +15 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +78 -5
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +8 -0
- 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 +3 -1
- 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/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/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 +13 -2
- package/dist/runtime/i18n.d.ts.map +1 -1
- package/dist/runtime/i18n.js +26 -4
- package/dist/runtime/i18n.js.map +1 -1
- 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 +21 -2
- package/dist/runtime/middleware.js.map +1 -1
- package/dist/runtime/mutations/contracts.d.ts +9 -0
- package/dist/runtime/mutations/contracts.d.ts.map +1 -1
- package/dist/runtime/mutations/contracts.js +45 -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 +115 -63
- package/dist/runtime/mutations/engine.js.map +1 -1
- package/dist/runtime/providers.d.ts +3 -1
- package/dist/runtime/providers.d.ts.map +1 -1
- package/dist/runtime/providers.js +12 -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 +104 -67
- 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 +3 -1
- package/dist/runtime/request-context.d.ts.map +1 -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 +24 -12
- package/dist/runtime/routes/admin-entry.js.map +1 -1
- package/dist/runtime/routes/auth-session.d.ts.map +1 -1
- package/dist/runtime/routes/auth-session.js +6 -0
- 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 +164 -40
- 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 +74 -39
- 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 +37 -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 +4 -0
- 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 +3 -1
- 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.map +1 -1
- package/dist/runtime/schema-registry.js +2 -0
- 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/utils.d.ts.map +1 -1
- package/dist/runtime/utils.js +3 -4
- package/dist/runtime/utils.js.map +1 -1
- package/dist/types.d.ts +123 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +12 -3
- package/static/cms/admin-entry.css +9 -0
- package/static/cms/admin-entry.js +251 -687
- 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 +43 -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 +43 -15
- 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 +208 -10
- 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/toolbar.css +5 -0
- package/static/cms/editor/toolbar.js +91 -101
- package/static/cms/editor.js +2 -1
- 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
|
@@ -1,33 +1,40 @@
|
|
|
1
1
|
import { SPACING_Y_VALUES } from './constants.js';
|
|
2
2
|
|
|
3
|
+
/** @param {string} key @returns {string} */
|
|
3
4
|
export function humanizeSectionKey(key) {
|
|
4
5
|
const short = key.startsWith('home.') ? key.slice('home.'.length) : key;
|
|
5
6
|
return short.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
6
7
|
}
|
|
7
8
|
|
|
9
|
+
/** @param {unknown} value @returns {string | undefined} */
|
|
8
10
|
export function normalizeSpacingY(value) {
|
|
9
11
|
if (typeof value !== 'string') return undefined;
|
|
10
12
|
return SPACING_Y_VALUES.includes(value) && value !== 'default' ? value : undefined;
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
/** @param {unknown} value @returns {string} */
|
|
13
16
|
export function spacingToken(value) {
|
|
14
17
|
return normalizeSpacingY(value) || 'default';
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
/** @param {unknown} value @returns {number} */
|
|
17
21
|
export function spacingIndex(value) {
|
|
18
22
|
const idx = SPACING_Y_VALUES.indexOf(spacingToken(value));
|
|
19
23
|
return idx === -1 ? 0 : idx;
|
|
20
24
|
}
|
|
21
25
|
|
|
26
|
+
/** @param {unknown} value @returns {string} */
|
|
22
27
|
export function spacingButtonLabel(value) {
|
|
23
28
|
const token = spacingToken(value);
|
|
24
29
|
return token === 'default' ? 'Space' : `Space:${token}`;
|
|
25
30
|
}
|
|
26
31
|
|
|
32
|
+
/** @param {unknown} value @returns {value is Record<string, unknown>} */
|
|
27
33
|
export function isRecord(value) {
|
|
28
34
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
29
35
|
}
|
|
30
36
|
|
|
37
|
+
/** @param {string} key @returns {string} */
|
|
31
38
|
export function generateSectionId(key) {
|
|
32
39
|
return `sec-${key.replace(/\./g, '-')}-${Date.now().toString(36)}-${Math.random()
|
|
33
40
|
.toString(36)
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
.cms-section-editable.cms-section-disabled-preview .cms-section-chip {
|
|
26
26
|
opacity: 1;
|
|
27
27
|
transform: translateY(0);
|
|
28
|
-
pointer-events: auto;
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
.cms-highlight-all .cms-section-editable {
|
|
@@ -57,7 +56,6 @@
|
|
|
57
56
|
.cms-section-controls:focus-within {
|
|
58
57
|
opacity: 1;
|
|
59
58
|
transform: translateY(0);
|
|
60
|
-
pointer-events: auto;
|
|
61
59
|
}
|
|
62
60
|
|
|
63
61
|
.cms-section-chip {
|
|
@@ -112,6 +110,14 @@
|
|
|
112
110
|
padding: 5px 7px;
|
|
113
111
|
cursor: pointer;
|
|
114
112
|
transition: all 0.16s ease;
|
|
113
|
+
pointer-events: none;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.cms-section-editable:hover .cms-section-btn,
|
|
117
|
+
.cms-section-editable.cms-section-active .cms-section-btn,
|
|
118
|
+
.cms-section-controls:focus-within .cms-section-btn,
|
|
119
|
+
.cms-section-editable.cms-section-disabled-preview .cms-section-btn {
|
|
120
|
+
pointer-events: auto;
|
|
115
121
|
}
|
|
116
122
|
|
|
117
123
|
.cms-section-btn:hover {
|
|
@@ -173,7 +179,6 @@ body.cms-section-dragging .cms-section-controls {
|
|
|
173
179
|
.cms-section-editable.cms-section-active .cms-section-boundary-actions,
|
|
174
180
|
body.cms-spacing-dragging .cms-section-boundary-actions {
|
|
175
181
|
opacity: 1;
|
|
176
|
-
pointer-events: auto;
|
|
177
182
|
}
|
|
178
183
|
|
|
179
184
|
.cms-section-insert-handle {
|
|
@@ -189,6 +194,7 @@ body.cms-spacing-dragging .cms-section-boundary-actions {
|
|
|
189
194
|
cursor: pointer;
|
|
190
195
|
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.26);
|
|
191
196
|
transition: border-color 0.16s ease, transform 0.16s ease, background 0.16s ease;
|
|
197
|
+
pointer-events: none;
|
|
192
198
|
}
|
|
193
199
|
|
|
194
200
|
.cms-section-insert-plus {
|
|
@@ -224,6 +230,15 @@ body.cms-spacing-dragging .cms-section-boundary-actions {
|
|
|
224
230
|
cursor: ns-resize;
|
|
225
231
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28);
|
|
226
232
|
transition: border-color 0.16s ease, color 0.16s ease, background 0.16s ease;
|
|
233
|
+
pointer-events: none;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.cms-section-editable:hover .cms-section-insert-handle,
|
|
237
|
+
.cms-section-editable.cms-section-active .cms-section-insert-handle,
|
|
238
|
+
.cms-section-editable:hover .cms-section-gap-handle,
|
|
239
|
+
.cms-section-editable.cms-section-active .cms-section-gap-handle,
|
|
240
|
+
body.cms-spacing-dragging .cms-section-gap-handle {
|
|
241
|
+
pointer-events: auto;
|
|
227
242
|
}
|
|
228
243
|
|
|
229
244
|
.cms-section-gap-grip {
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { CLOUD, getCloudSessionToken, isCloudMode } from './config.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} action
|
|
5
|
+
* @returns {Promise<string | null>}
|
|
6
|
+
*/
|
|
3
7
|
async function getTurnstileToken(action) {
|
|
4
|
-
|
|
8
|
+
const getToken = /** @type {Window & { cmsGetTurnstileToken?: (action: string) => unknown }} */ (
|
|
9
|
+
window
|
|
10
|
+
).cmsGetTurnstileToken;
|
|
11
|
+
if (typeof getToken !== 'function') return null;
|
|
5
12
|
try {
|
|
6
|
-
|
|
13
|
+
const token = await getToken(action);
|
|
14
|
+
return typeof token === 'string' && token.trim() ? token.trim() : null;
|
|
7
15
|
} catch {
|
|
8
16
|
return null;
|
|
9
17
|
}
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
export async function mutateHeaders() {
|
|
21
|
+
/** @type {Record<string, string>} */
|
|
13
22
|
const headers = { 'Content-Type': 'application/json', 'x-caret-request': '1' };
|
|
14
23
|
if (isCloudMode()) {
|
|
15
24
|
if (CLOUD?.publicToken) headers['x-caret-token'] = CLOUD.publicToken;
|
|
@@ -24,6 +33,7 @@ export async function mutateHeaders() {
|
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
export function readHeaders() {
|
|
36
|
+
/** @type {Record<string, string>} */
|
|
27
37
|
const headers = {};
|
|
28
38
|
if (isCloudMode()) {
|
|
29
39
|
if (CLOUD?.publicToken) headers['x-caret-token'] = CLOUD.publicToken;
|
|
@@ -34,6 +44,7 @@ export function readHeaders() {
|
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
export async function uploadHeaders() {
|
|
47
|
+
/** @type {Record<string, string>} */
|
|
37
48
|
const headers = { 'x-caret-request': '1' };
|
|
38
49
|
if (isCloudMode()) {
|
|
39
50
|
if (CLOUD?.publicToken) headers['x-caret-token'] = CLOUD.publicToken;
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* Decode logic mirrors runtime/stega.ts (the editor bundle is hand-authored JS,
|
|
13
13
|
* separate from the TS runtime, so the few lines are intentionally duplicated).
|
|
14
14
|
*/
|
|
15
|
+
import { parseCaretAttr } from './helpers.js';
|
|
15
16
|
|
|
16
17
|
const TAG_BASE = 0xe0000;
|
|
17
18
|
const TAG_MAX = 0xe007f;
|
|
@@ -19,12 +20,14 @@ const START = String.fromCodePoint(TAG_BASE + 0x00);
|
|
|
19
20
|
const END = String.fromCodePoint(TAG_BASE + 0x01);
|
|
20
21
|
const TAG_RE = /[\u{E0000}-\u{E007F}]/gu;
|
|
21
22
|
|
|
23
|
+
/** @param {string} b64 @returns {string} */
|
|
22
24
|
function fromBase64(b64) {
|
|
23
25
|
const binary = atob(b64);
|
|
24
26
|
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
25
27
|
return new TextDecoder().decode(bytes);
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
/** @param {string} value @returns {unknown} */
|
|
28
31
|
export function stegaDecode(value) {
|
|
29
32
|
const start = value.indexOf(START);
|
|
30
33
|
if (start === -1) return undefined;
|
|
@@ -43,6 +46,7 @@ export function stegaDecode(value) {
|
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
/** @param {string} value @returns {string} */
|
|
46
50
|
export function stegaClean(value) {
|
|
47
51
|
return value.replace(TAG_RE, '');
|
|
48
52
|
}
|
|
@@ -51,28 +55,37 @@ export function stegaClean(value) {
|
|
|
51
55
|
* Find every stega-tagged text node under `root`, decode its key, strip the
|
|
52
56
|
* invisible characters, and tag the containing element with `data-caret`.
|
|
53
57
|
* Returns the number of elements newly promoted.
|
|
58
|
+
* @param {Node | null} [root]
|
|
59
|
+
* @returns {number}
|
|
54
60
|
*/
|
|
55
61
|
export function hydrateStega(root) {
|
|
56
62
|
const scope = root || document.body;
|
|
57
63
|
if (!scope) return 0;
|
|
58
64
|
|
|
59
65
|
const walker = document.createTreeWalker(scope, NodeFilter.SHOW_TEXT);
|
|
66
|
+
/** @type {Text[]} */
|
|
60
67
|
const tagged = [];
|
|
61
68
|
let node = walker.nextNode();
|
|
62
69
|
while (node) {
|
|
63
|
-
if (node.nodeValue && node.nodeValue.indexOf(START) !== -1)
|
|
70
|
+
if (node instanceof Text && node.nodeValue && node.nodeValue.indexOf(START) !== -1) {
|
|
71
|
+
tagged.push(node);
|
|
72
|
+
}
|
|
64
73
|
node = walker.nextNode();
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
let promoted = 0;
|
|
68
77
|
for (const textNode of tagged) {
|
|
69
|
-
const
|
|
78
|
+
const value = textNode.nodeValue || '';
|
|
79
|
+
const key = stegaDecode(value);
|
|
70
80
|
// Always strip the invisible characters so they never affect layout/copy.
|
|
71
|
-
textNode.nodeValue = stegaClean(
|
|
72
|
-
if (typeof key !== 'string'
|
|
81
|
+
textNode.nodeValue = stegaClean(value);
|
|
82
|
+
if (typeof key !== 'string') continue;
|
|
83
|
+
const binding = parseCaretAttr(key);
|
|
84
|
+
if (!binding?.collection || !binding.id) continue;
|
|
73
85
|
|
|
74
86
|
const el = textNode.parentElement;
|
|
75
87
|
if (!el || el.hasAttribute('data-caret')) continue; // explicit attr wins
|
|
88
|
+
if (el.closest('script, style, template, noscript')) continue;
|
|
76
89
|
el.setAttribute('data-caret', key);
|
|
77
90
|
promoted++;
|
|
78
91
|
}
|
|
@@ -3,14 +3,136 @@ import { parseCaretAttr, resolveBinding } from './helpers.js';
|
|
|
3
3
|
import { STUDIO_PATH } from './config.js';
|
|
4
4
|
|
|
5
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
|
+
}
|
|
6
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
|
+
*/
|
|
7
120
|
export function mountStudioSync({ studioIframe, state, showToast, clientLinkify }) {
|
|
8
121
|
let refreshTimer = 0;
|
|
122
|
+
/** @type {Element | null} */
|
|
9
123
|
let selectedInlineEl = null;
|
|
10
124
|
let linkedSelectionTimer = 0;
|
|
125
|
+
/** @type {SelectionMessage | null} */
|
|
11
126
|
let pendingStudioSelection = null;
|
|
127
|
+
/** @type {SelectionMessage | null} */
|
|
12
128
|
let activeStudioSelection = null;
|
|
13
129
|
|
|
130
|
+
/**
|
|
131
|
+
* @param {string} collection
|
|
132
|
+
* @param {string} id
|
|
133
|
+
* @param {string} field
|
|
134
|
+
* @returns {Element[]}
|
|
135
|
+
*/
|
|
14
136
|
function matchingElements(collection, id, field) {
|
|
15
137
|
const fullKey = `${collection}::${id}::${field}`;
|
|
16
138
|
const matches = Array.from(document.querySelectorAll(`[data-caret="${CSS.escape(fullKey)}"]`));
|
|
@@ -21,10 +143,15 @@ export function mountStudioSync({ studioIframe, state, showToast, clientLinkify
|
|
|
21
143
|
return matches;
|
|
22
144
|
}
|
|
23
145
|
|
|
146
|
+
/**
|
|
147
|
+
* @param {string} collection
|
|
148
|
+
* @param {string} id
|
|
149
|
+
* @param {string} field
|
|
150
|
+
* @param {boolean} shouldScroll
|
|
151
|
+
*/
|
|
24
152
|
function selectInlineField(collection, id, field, shouldScroll) {
|
|
25
|
-
if (typeof collection !== 'string' || typeof id !== 'string' || typeof field !== 'string') return;
|
|
26
153
|
const target = matchingElements(collection, id, field)[0];
|
|
27
|
-
if (!target) return;
|
|
154
|
+
if (!target) return false;
|
|
28
155
|
|
|
29
156
|
window.clearTimeout(linkedSelectionTimer);
|
|
30
157
|
if (selectedInlineEl && selectedInlineEl !== target) {
|
|
@@ -43,8 +170,45 @@ export function mountStudioSync({ studioIframe, state, showToast, clientLinkify
|
|
|
43
170
|
target.classList.remove('cms-linked-selection');
|
|
44
171
|
selectedInlineEl = null;
|
|
45
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
|
+
}
|
|
46
185
|
}
|
|
47
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 */
|
|
48
212
|
function publishSelection(message) {
|
|
49
213
|
if (!('BroadcastChannel' in window)) return;
|
|
50
214
|
try {
|
|
@@ -56,10 +220,13 @@ export function mountStudioSync({ studioIframe, state, showToast, clientLinkify
|
|
|
56
220
|
}
|
|
57
221
|
}
|
|
58
222
|
|
|
223
|
+
/** @param {SelectionMessage} message */
|
|
59
224
|
function studioEntryPath(message) {
|
|
225
|
+
if (!STUDIO_PATH) return null;
|
|
60
226
|
return `${STUDIO_PATH}/${encodeURIComponent(message.collection)}/${encodeURIComponent(message.id)}`;
|
|
61
227
|
}
|
|
62
228
|
|
|
229
|
+
/** @param {SelectionMessage} message */
|
|
63
230
|
function postEmbeddedSelection(message) {
|
|
64
231
|
if (!studioIframe?.contentWindow) return;
|
|
65
232
|
try {
|
|
@@ -69,9 +236,11 @@ export function mountStudioSync({ studioIframe, state, showToast, clientLinkify
|
|
|
69
236
|
}
|
|
70
237
|
}
|
|
71
238
|
|
|
239
|
+
/** @param {SelectionMessage} message */
|
|
72
240
|
function deliverEmbeddedSelection(message) {
|
|
73
241
|
if (!studioIframe?.contentWindow) return;
|
|
74
242
|
const targetPath = studioEntryPath(message);
|
|
243
|
+
if (!targetPath) return;
|
|
75
244
|
|
|
76
245
|
// A normal mouse interaction can emit pointerdown, focusin, and click.
|
|
77
246
|
// Keep the latest field without restarting the same iframe navigation for
|
|
@@ -107,11 +276,13 @@ export function mountStudioSync({ studioIframe, state, showToast, clientLinkify
|
|
|
107
276
|
window.requestAnimationFrame(() => postEmbeddedSelection(message));
|
|
108
277
|
});
|
|
109
278
|
|
|
279
|
+
/** @param {Element} el */
|
|
110
280
|
function selectStudioField(el) {
|
|
111
281
|
const resolved = resolveBinding(el, parseCaretAttr(el.getAttribute('data-caret') || ''));
|
|
112
282
|
if (!resolved) return;
|
|
113
283
|
|
|
114
284
|
selectInlineField(resolved.collection, resolved.id, resolved.field, false);
|
|
285
|
+
/** @type {SelectionMessage} */
|
|
115
286
|
const message = {
|
|
116
287
|
type: 'cms:field-selected',
|
|
117
288
|
collection: resolved.collection,
|
|
@@ -140,8 +311,9 @@ export function mountStudioSync({ studioIframe, state, showToast, clientLinkify
|
|
|
140
311
|
if (el) selectStudioField(el);
|
|
141
312
|
});
|
|
142
313
|
|
|
314
|
+
/** @param {SyncMessage} msg */
|
|
143
315
|
function rememberStudioPath(msg) {
|
|
144
|
-
if (typeof msg.studioPath !== 'string') return;
|
|
316
|
+
if (!('studioPath' in msg) || typeof msg.studioPath !== 'string') return;
|
|
145
317
|
try {
|
|
146
318
|
sessionStorage.setItem('cms-panel-path', msg.studioPath);
|
|
147
319
|
} catch {
|
|
@@ -149,6 +321,7 @@ export function mountStudioSync({ studioIframe, state, showToast, clientLinkify
|
|
|
149
321
|
}
|
|
150
322
|
}
|
|
151
323
|
|
|
324
|
+
/** @param {SyncMessage} msg */
|
|
152
325
|
function scheduleEmbeddedRefresh(msg) {
|
|
153
326
|
rememberStudioPath(msg);
|
|
154
327
|
|
|
@@ -162,12 +335,18 @@ export function mountStudioSync({ studioIframe, state, showToast, clientLinkify
|
|
|
162
335
|
refreshTimer = window.setTimeout(() => window.location.reload(), 280);
|
|
163
336
|
}
|
|
164
337
|
|
|
338
|
+
/** @param {ChangeMessage} msg */
|
|
165
339
|
function applySavedMessage(msg) {
|
|
166
340
|
const { collection, id, data } = msg;
|
|
167
341
|
if (!data || typeof data !== 'object') return 0;
|
|
168
342
|
|
|
169
343
|
let updated = 0;
|
|
170
344
|
|
|
345
|
+
/**
|
|
346
|
+
* @param {string} selector
|
|
347
|
+
* @param {string} path
|
|
348
|
+
* @returns {Element[]}
|
|
349
|
+
*/
|
|
171
350
|
function boundElements(selector, path) {
|
|
172
351
|
const matches = Array.from(document.querySelectorAll(`[data-caret="${CSS.escape(selector)}"]`));
|
|
173
352
|
document.querySelectorAll(`[data-caret="${CSS.escape(path)}"]`).forEach((el) => {
|
|
@@ -177,6 +356,10 @@ export function mountStudioSync({ studioIframe, state, showToast, clientLinkify
|
|
|
177
356
|
return matches;
|
|
178
357
|
}
|
|
179
358
|
|
|
359
|
+
/**
|
|
360
|
+
* @param {Record<string, unknown>} obj
|
|
361
|
+
* @param {string} prefix
|
|
362
|
+
*/
|
|
180
363
|
function walkData(obj, prefix) {
|
|
181
364
|
for (const [key, value] of Object.entries(obj)) {
|
|
182
365
|
const path = prefix ? `${prefix}.${key}` : key;
|
|
@@ -210,7 +393,7 @@ export function mountStudioSync({ studioIframe, state, showToast, clientLinkify
|
|
|
210
393
|
updated++;
|
|
211
394
|
}
|
|
212
395
|
});
|
|
213
|
-
} else if (
|
|
396
|
+
} else if (isRecord(value)) {
|
|
214
397
|
walkData(value, path);
|
|
215
398
|
}
|
|
216
399
|
}
|
|
@@ -223,8 +406,8 @@ export function mountStudioSync({ studioIframe, state, showToast, clientLinkify
|
|
|
223
406
|
window.addEventListener('message', (e) => {
|
|
224
407
|
// Only accept messages from our iframe
|
|
225
408
|
if (!studioIframe || e.source !== studioIframe.contentWindow) return;
|
|
226
|
-
const msg = e.data;
|
|
227
|
-
if (!msg
|
|
409
|
+
const msg = normalizeSyncMessage(e.data);
|
|
410
|
+
if (!msg) return;
|
|
228
411
|
|
|
229
412
|
if (
|
|
230
413
|
msg.type === 'cms:entry-ready'
|
|
@@ -242,7 +425,7 @@ export function mountStudioSync({ studioIframe, state, showToast, clientLinkify
|
|
|
242
425
|
}
|
|
243
426
|
|
|
244
427
|
if (msg.type === 'cms:field-selected') {
|
|
245
|
-
|
|
428
|
+
selectOrNavigate(msg);
|
|
246
429
|
return;
|
|
247
430
|
}
|
|
248
431
|
|
|
@@ -265,11 +448,11 @@ export function mountStudioSync({ studioIframe, state, showToast, clientLinkify
|
|
|
265
448
|
try {
|
|
266
449
|
const channel = new BroadcastChannel(SYNC_CHANNEL);
|
|
267
450
|
channel.addEventListener('message', (event) => {
|
|
268
|
-
const msg = event.data;
|
|
269
|
-
if (!msg ||
|
|
451
|
+
const msg = normalizeSyncMessage(event.data);
|
|
452
|
+
if (!msg || msg.embedded === true) return;
|
|
270
453
|
|
|
271
454
|
if (msg.type === 'cms:field-selected') {
|
|
272
|
-
|
|
455
|
+
selectOrNavigate(msg);
|
|
273
456
|
return;
|
|
274
457
|
}
|
|
275
458
|
if (!['cms:saved', 'cms:created', 'cms:deleted'].includes(msg.type)) return;
|
|
@@ -290,4 +473,19 @@ export function mountStudioSync({ studioIframe, state, showToast, clientLinkify
|
|
|
290
473
|
// remains available in browsers without BroadcastChannel.
|
|
291
474
|
}
|
|
292
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
|
+
}
|
|
293
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) || '';
|