@collidecreatives/edit-mode 0.1.0 → 0.3.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 +12 -0
- package/dist/browser.global.js +17 -2
- package/dist/browser.global.js.map +1 -1
- package/dist/index.cjs +402 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +402 -72
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -24,8 +24,26 @@ __export(src_exports, {
|
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(src_exports);
|
|
26
26
|
var DEFAULT_EDITABLE_SELECTOR = "h1,h2,h3,h4,h5,h6,p,li,blockquote,figcaption,label,legend,dt,dd,th,td,a,button";
|
|
27
|
-
var DEFAULT_SKIP_SELECTOR = "[data-no-edit],[data-edit-mode-skip],form,input,textarea,select,option,script,style,svg,canvas,iframe,.cem-edit-banner,.cem-
|
|
27
|
+
var DEFAULT_SKIP_SELECTOR = "[data-no-edit],[data-edit-mode-skip],form,input,textarea,select,option,script,style,svg,canvas,iframe,.cem-edit-banner,.cem-panel,.cem-toast";
|
|
28
28
|
var SKIP_CHILD_TAGS = /* @__PURE__ */ new Set(["IMG", "PICTURE", "SVG", "VIDEO", "CANVAS", "IFRAME", "SCRIPT", "STYLE"]);
|
|
29
|
+
var SINGLE_LINE_TAGS = /* @__PURE__ */ new Set([
|
|
30
|
+
"H1",
|
|
31
|
+
"H2",
|
|
32
|
+
"H3",
|
|
33
|
+
"H4",
|
|
34
|
+
"H5",
|
|
35
|
+
"H6",
|
|
36
|
+
"BUTTON",
|
|
37
|
+
"LABEL",
|
|
38
|
+
"LEGEND",
|
|
39
|
+
"DT",
|
|
40
|
+
"TH",
|
|
41
|
+
"TD",
|
|
42
|
+
"FIGCAPTION",
|
|
43
|
+
"A"
|
|
44
|
+
]);
|
|
45
|
+
var DRAFT_VERSION = 1;
|
|
46
|
+
var AUTOSAVE_DEBOUNCE_MS = 400;
|
|
29
47
|
var defaults = {
|
|
30
48
|
queryParam: "edit",
|
|
31
49
|
queryValue: "true",
|
|
@@ -33,7 +51,8 @@ var defaults = {
|
|
|
33
51
|
brandName: "Edit Mode",
|
|
34
52
|
accentColour: "#1e40af",
|
|
35
53
|
editableSelector: DEFAULT_EDITABLE_SELECTOR,
|
|
36
|
-
skipSelector: DEFAULT_SKIP_SELECTOR
|
|
54
|
+
skipSelector: DEFAULT_SKIP_SELECTOR,
|
|
55
|
+
autoSave: true
|
|
37
56
|
};
|
|
38
57
|
function hasDom() {
|
|
39
58
|
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
@@ -49,18 +68,34 @@ function addStyles(accentColour) {
|
|
|
49
68
|
const style = document.createElement("style");
|
|
50
69
|
style.dataset.editModeStyle = "true";
|
|
51
70
|
style.textContent = [
|
|
52
|
-
`.cem-edit-banner{position:fixed;top:0;left:0;right:0;z-index:2147483640;background:${accentColour};color:#fff;
|
|
53
|
-
".cem-edit-banner kbd{background:rgba(255,255,255
|
|
54
|
-
".cem-exit-btn{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:rgba(255,255,255
|
|
55
|
-
".cem-exit-btn:hover{background:rgba(255,255,255
|
|
71
|
+
`.cem-edit-banner{position:fixed;top:0;left:0;right:0;z-index:2147483640;background:${accentColour};color:#fff;display:flex;align-items:center;justify-content:center;gap:8px;padding:8px 92px 8px 16px;font:13px/1.4 system-ui,-apple-system,sans-serif;box-shadow:0 1px 8px rgba(0,0,0,.18)}`,
|
|
72
|
+
".cem-edit-banner kbd{background:rgba(255,255,255,.18);padding:0 4px;border-radius:3px;font-size:11px}",
|
|
73
|
+
".cem-exit-btn{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:rgba(255,255,255,.15);border:1px solid rgba(255,255,255,.25);color:#fff;padding:4px 10px;border-radius:5px;font:12px system-ui,-apple-system,sans-serif;cursor:pointer;white-space:nowrap}",
|
|
74
|
+
".cem-exit-btn:hover,.cem-exit-btn:focus-visible{background:rgba(255,255,255,.25);outline:none}",
|
|
56
75
|
"[contenteditable=true]{cursor:text!important}",
|
|
57
|
-
"[contenteditable=true]:hover{outline:2px dashed rgba(59,130,246
|
|
58
|
-
"[contenteditable=true]:focus{outline:2px solid #3b82f6!important;outline-offset:2px;background:rgba(59,130,246
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
".cem-
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
"[contenteditable=true]:hover{outline:2px dashed rgba(59,130,246,.5)!important;outline-offset:2px}",
|
|
77
|
+
"[contenteditable=true]:focus{outline:2px solid #3b82f6!important;outline-offset:2px;background:rgba(59,130,246,.04)}",
|
|
78
|
+
"[contenteditable=true].cem-changed{box-shadow:inset 3px 0 0 #10b981}",
|
|
79
|
+
`.cem-panel{position:fixed;right:16px;bottom:16px;z-index:2147483640;background:#fff;color:#111827;border:1px solid #e5e7eb;border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,.22);padding:10px;display:grid;gap:8px;width:min(320px,calc(100vw - 32px));font:13px/1.4 system-ui,-apple-system,sans-serif}`,
|
|
80
|
+
".cem-panel-actions{display:flex;gap:8px;flex-wrap:wrap}",
|
|
81
|
+
`.cem-copy-btn,.cem-review-btn,.cem-download-btn,.cem-open-link-btn{border:0;border-radius:8px;padding:9px 12px;font:600 13px system-ui,-apple-system,sans-serif;cursor:pointer;white-space:nowrap}`,
|
|
82
|
+
`.cem-copy-btn{background:${accentColour};color:#fff;flex:1}`,
|
|
83
|
+
".cem-review-btn,.cem-download-btn,.cem-open-link-btn{background:#f3f4f6;color:#111827;border:1px solid #e5e7eb}",
|
|
84
|
+
".cem-open-link-btn[hidden]{display:none}",
|
|
85
|
+
".cem-copy-btn:hover,.cem-review-btn:hover,.cem-download-btn:hover,.cem-open-link-btn:hover{filter:brightness(.97)}",
|
|
86
|
+
".cem-status{color:#4b5563;font-size:12px;min-height:17px}",
|
|
87
|
+
".cem-review-list{max-height:220px;overflow-y:auto;display:grid;gap:6px;border-top:1px solid #e5e7eb;padding-top:8px}",
|
|
88
|
+
".cem-review-list[hidden]{display:none}",
|
|
89
|
+
".cem-review-empty{color:#6b7280;font-size:12px;margin:0;padding:4px 0}",
|
|
90
|
+
".cem-review-item{display:grid;gap:4px;padding:8px;background:#f9fafb;border-radius:8px;border:1px solid #e5e7eb}",
|
|
91
|
+
".cem-review-path{font-size:11px;color:#6b7280;font-weight:600;text-transform:uppercase;letter-spacing:.02em}",
|
|
92
|
+
".cem-review-diff{font-size:12px;word-break:break-word}",
|
|
93
|
+
".cem-review-diff del{color:#b91c1c;text-decoration:line-through;opacity:.75;display:block}",
|
|
94
|
+
".cem-review-diff ins{color:#065f46;text-decoration:none;display:block}",
|
|
95
|
+
".cem-revert-btn{justify-self:start;background:transparent;border:1px solid #e5e7eb;border-radius:6px;padding:3px 8px;font:12px system-ui,-apple-system,sans-serif;cursor:pointer;color:#374151}",
|
|
96
|
+
".cem-revert-btn:hover{background:#fff}",
|
|
97
|
+
".cem-toast{position:fixed;bottom:116px;right:16px;z-index:2147483641;background:#065f46;color:#fff;padding:10px 18px;border-radius:8px;font:14px system-ui,-apple-system,sans-serif;box-shadow:0 2px 12px rgba(0,0,0,.3);transition:opacity .3s}",
|
|
98
|
+
"@media(max-width:520px){.cem-edit-banner{justify-content:flex-start;text-align:left}.cem-panel{left:16px;right:16px;width:auto}}"
|
|
64
99
|
].join("\n");
|
|
65
100
|
document.head.appendChild(style);
|
|
66
101
|
return style;
|
|
@@ -90,6 +125,40 @@ function fallbackCopy(text, label) {
|
|
|
90
125
|
document.body.removeChild(textarea);
|
|
91
126
|
showToast(label);
|
|
92
127
|
}
|
|
128
|
+
function getStorage() {
|
|
129
|
+
try {
|
|
130
|
+
const testKey = "__cem_storage_test__";
|
|
131
|
+
window.localStorage.setItem(testKey, "1");
|
|
132
|
+
window.localStorage.removeItem(testKey);
|
|
133
|
+
return window.localStorage;
|
|
134
|
+
} catch {
|
|
135
|
+
try {
|
|
136
|
+
return window.sessionStorage;
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function getDraftKey(config) {
|
|
143
|
+
const base = config.storageKey || `${config.sessionKey}:draft`;
|
|
144
|
+
return `${base}:${window.location.origin}${window.location.pathname}`;
|
|
145
|
+
}
|
|
146
|
+
function escapeHtml(text) {
|
|
147
|
+
return text.replace(/[&<>"']/g, (char) => {
|
|
148
|
+
switch (char) {
|
|
149
|
+
case "&":
|
|
150
|
+
return "&";
|
|
151
|
+
case "<":
|
|
152
|
+
return "<";
|
|
153
|
+
case ">":
|
|
154
|
+
return ">";
|
|
155
|
+
case '"':
|
|
156
|
+
return """;
|
|
157
|
+
default:
|
|
158
|
+
return "'";
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
93
162
|
function isEditableElement(el, skipSelector) {
|
|
94
163
|
if (el.closest(skipSelector)) return false;
|
|
95
164
|
const text = el.textContent?.trim() ?? "";
|
|
@@ -100,6 +169,22 @@ function isEditableElement(el, skipSelector) {
|
|
|
100
169
|
}
|
|
101
170
|
return true;
|
|
102
171
|
}
|
|
172
|
+
function getElementKey(el) {
|
|
173
|
+
if (el.dataset.editId) return `data-edit-id:${el.dataset.editId}`;
|
|
174
|
+
if (el.id) return `id:${el.id}`;
|
|
175
|
+
const parts = [];
|
|
176
|
+
let current = el;
|
|
177
|
+
while (current && current !== document.body && current !== document.documentElement) {
|
|
178
|
+
const parentElement = current.parentElement;
|
|
179
|
+
if (!parentElement) break;
|
|
180
|
+
const currentTag = current.tagName;
|
|
181
|
+
const siblings = Array.from(parentElement.children).filter((child) => child.tagName === currentTag);
|
|
182
|
+
const index = siblings.indexOf(current) + 1;
|
|
183
|
+
parts.unshift(`${current.tagName.toLowerCase()}:nth-of-type(${index})`);
|
|
184
|
+
current = parentElement;
|
|
185
|
+
}
|
|
186
|
+
return parts.join(" > ");
|
|
187
|
+
}
|
|
103
188
|
function getEditPath(el) {
|
|
104
189
|
let context = "";
|
|
105
190
|
let current = el.parentElement;
|
|
@@ -146,6 +231,114 @@ function rewriteLinks(queryParam, queryValue) {
|
|
|
146
231
|
}
|
|
147
232
|
});
|
|
148
233
|
}
|
|
234
|
+
function buildPayload(changes) {
|
|
235
|
+
return {
|
|
236
|
+
site: window.location.hostname,
|
|
237
|
+
page: window.location.pathname,
|
|
238
|
+
pageTitle: document.title,
|
|
239
|
+
url: window.location.href,
|
|
240
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
241
|
+
changes
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function loadDraft(storage, key) {
|
|
245
|
+
if (!storage) return null;
|
|
246
|
+
try {
|
|
247
|
+
const raw = storage.getItem(key);
|
|
248
|
+
if (!raw) return null;
|
|
249
|
+
const draft = JSON.parse(raw);
|
|
250
|
+
return draft?.version === DRAFT_VERSION && Array.isArray(draft.changes) ? draft : null;
|
|
251
|
+
} catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function saveDraft(storage, key, draft) {
|
|
256
|
+
if (!storage) return false;
|
|
257
|
+
try {
|
|
258
|
+
storage.setItem(key, JSON.stringify(draft));
|
|
259
|
+
return true;
|
|
260
|
+
} catch {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function downloadText(filename, text) {
|
|
265
|
+
const blob = new Blob([text], { type: "application/json" });
|
|
266
|
+
const url = URL.createObjectURL(blob);
|
|
267
|
+
const anchor = document.createElement("a");
|
|
268
|
+
anchor.href = url;
|
|
269
|
+
anchor.download = filename;
|
|
270
|
+
anchor.style.display = "none";
|
|
271
|
+
document.body.appendChild(anchor);
|
|
272
|
+
anchor.click();
|
|
273
|
+
anchor.remove();
|
|
274
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
275
|
+
}
|
|
276
|
+
function formatTime(date = /* @__PURE__ */ new Date()) {
|
|
277
|
+
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
278
|
+
}
|
|
279
|
+
function initEditableElements(config) {
|
|
280
|
+
document.querySelectorAll(config.editableSelector).forEach((el) => {
|
|
281
|
+
if (!isEditableElement(el, config.skipSelector)) return;
|
|
282
|
+
if (el.closest("[data-edit-key]")) return;
|
|
283
|
+
const text = el.textContent?.trim() ?? "";
|
|
284
|
+
el.contentEditable = "true";
|
|
285
|
+
if (!el.dataset.editOriginal) el.dataset.editOriginal = text;
|
|
286
|
+
if (!el.dataset.editKey) el.dataset.editKey = getElementKey(el);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
function findEditableByKey(editableSelector, key) {
|
|
290
|
+
const elements = document.querySelectorAll(editableSelector);
|
|
291
|
+
for (const el of Array.from(elements)) {
|
|
292
|
+
if (el.dataset.editKey === key) return el;
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
function applyDraft(draft, editableSelector) {
|
|
297
|
+
if (!draft) return 0;
|
|
298
|
+
const edits = new Map(draft.changes.map((change) => [change.key, change]));
|
|
299
|
+
let restored = 0;
|
|
300
|
+
document.querySelectorAll(editableSelector).forEach((el) => {
|
|
301
|
+
const key = el.dataset.editKey;
|
|
302
|
+
if (!key) return;
|
|
303
|
+
const saved = edits.get(key);
|
|
304
|
+
if (!saved) return;
|
|
305
|
+
if (el.dataset.editOriginal !== saved.original) return;
|
|
306
|
+
el.textContent = saved.new;
|
|
307
|
+
restored += 1;
|
|
308
|
+
});
|
|
309
|
+
return restored;
|
|
310
|
+
}
|
|
311
|
+
function markChanged(el) {
|
|
312
|
+
const original = el.dataset.editOriginal;
|
|
313
|
+
if (original === void 0) return;
|
|
314
|
+
const current = el.textContent?.trim() ?? "";
|
|
315
|
+
el.classList.toggle("cem-changed", original !== current);
|
|
316
|
+
}
|
|
317
|
+
function collectDraftChanges(editableSelector) {
|
|
318
|
+
const changes = [];
|
|
319
|
+
document.querySelectorAll(editableSelector).forEach((el) => {
|
|
320
|
+
if (!el.isContentEditable && el.contentEditable !== "true") return;
|
|
321
|
+
const original = el.dataset.editOriginal;
|
|
322
|
+
const key = el.dataset.editKey;
|
|
323
|
+
if (original === void 0 || !key) return;
|
|
324
|
+
const current = el.textContent?.trim() ?? "";
|
|
325
|
+
const changed = original !== current;
|
|
326
|
+
el.classList.toggle("cem-changed", changed);
|
|
327
|
+
if (changed) {
|
|
328
|
+
changes.push({
|
|
329
|
+
key,
|
|
330
|
+
path: getEditPath(el),
|
|
331
|
+
tag: el.tagName,
|
|
332
|
+
original,
|
|
333
|
+
new: current
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
return changes;
|
|
338
|
+
}
|
|
339
|
+
function collectChanges(editableSelector) {
|
|
340
|
+
return collectDraftChanges(editableSelector).map(({ key: _key, ...change }) => change);
|
|
341
|
+
}
|
|
149
342
|
function initClientEditMode(options = {}) {
|
|
150
343
|
if (!hasDom()) {
|
|
151
344
|
return { active: false, getChanges: () => [], destroy: () => void 0 };
|
|
@@ -159,108 +352,245 @@ function initClientEditMode(options = {}) {
|
|
|
159
352
|
}
|
|
160
353
|
window.__COLLIDE_EDIT_MODE_ACTIVE = true;
|
|
161
354
|
window.sessionStorage.setItem(config.sessionKey, "1");
|
|
355
|
+
const storage = getStorage();
|
|
356
|
+
const draftKey = getDraftKey(config);
|
|
162
357
|
const style = addStyles(config.accentColour);
|
|
163
358
|
const banner = document.createElement("div");
|
|
164
359
|
banner.className = "cem-edit-banner";
|
|
165
|
-
banner.innerHTML = `\u270F\uFE0F <strong>${config.brandName}</strong>
|
|
360
|
+
banner.innerHTML = `\u270F\uFE0F <strong>${config.brandName}</strong> <span>Click text to edit. Press <kbd>Esc</kbd> when done.</span>`;
|
|
166
361
|
const exitButton = document.createElement("button");
|
|
167
362
|
exitButton.className = "cem-exit-btn";
|
|
168
|
-
exitButton.
|
|
169
|
-
exitButton.
|
|
170
|
-
window.sessionStorage.removeItem(config.sessionKey);
|
|
171
|
-
const url = new URL(window.location.href);
|
|
172
|
-
url.searchParams.delete(config.queryParam);
|
|
173
|
-
window.location.href = url.toString();
|
|
174
|
-
});
|
|
363
|
+
exitButton.type = "button";
|
|
364
|
+
exitButton.textContent = "Exit";
|
|
175
365
|
banner.appendChild(exitButton);
|
|
176
366
|
document.body.prepend(banner);
|
|
367
|
+
const panel = document.createElement("div");
|
|
368
|
+
panel.className = "cem-panel";
|
|
369
|
+
panel.innerHTML = `
|
|
370
|
+
<div class="cem-panel-actions">
|
|
371
|
+
<button class="cem-copy-btn" type="button">\u{1F4CB} Copy Changes</button>
|
|
372
|
+
<button class="cem-review-btn" type="button" aria-expanded="false">Review</button>
|
|
373
|
+
<button class="cem-download-btn" type="button">Download backup</button>
|
|
374
|
+
<button class="cem-open-link-btn" type="button" hidden>Open link</button>
|
|
375
|
+
</div>
|
|
376
|
+
<div class="cem-review-list" hidden></div>
|
|
377
|
+
<div class="cem-status" aria-live="polite">Auto-save ready</div>
|
|
378
|
+
`;
|
|
379
|
+
document.body.appendChild(panel);
|
|
380
|
+
const copyButton = panel.querySelector(".cem-copy-btn");
|
|
381
|
+
const reviewButton = panel.querySelector(".cem-review-btn");
|
|
382
|
+
const downloadButton = panel.querySelector(".cem-download-btn");
|
|
383
|
+
const openLinkButton = panel.querySelector(".cem-open-link-btn");
|
|
384
|
+
const reviewList = panel.querySelector(".cem-review-list");
|
|
385
|
+
const status = panel.querySelector(".cem-status");
|
|
386
|
+
let activeLink = null;
|
|
387
|
+
let reviewOpen = false;
|
|
388
|
+
let currentDraftChanges = [];
|
|
177
389
|
document.querySelectorAll("[data-reveal]").forEach((el) => el.classList.add("is-visible"));
|
|
178
390
|
rewriteLinks(config.queryParam, config.queryValue);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
391
|
+
initEditableElements(config);
|
|
392
|
+
const restoredCount = applyDraft(loadDraft(storage, draftKey), config.editableSelector);
|
|
393
|
+
const makeDraft = () => ({
|
|
394
|
+
version: DRAFT_VERSION,
|
|
395
|
+
page: window.location.pathname,
|
|
396
|
+
url: window.location.href,
|
|
397
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
398
|
+
changes: collectDraftChanges(config.editableSelector)
|
|
399
|
+
});
|
|
400
|
+
const renderReviewList = () => {
|
|
401
|
+
if (!reviewList) return;
|
|
402
|
+
if (currentDraftChanges.length === 0) {
|
|
403
|
+
reviewList.innerHTML = '<p class="cem-review-empty">No changes yet.</p>';
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
reviewList.innerHTML = currentDraftChanges.map(
|
|
407
|
+
(change, index) => `
|
|
408
|
+
<div class="cem-review-item">
|
|
409
|
+
<div class="cem-review-path">${escapeHtml(change.path)}</div>
|
|
410
|
+
<div class="cem-review-diff"><del>${escapeHtml(change.original)}</del><ins>${escapeHtml(change.new)}</ins></div>
|
|
411
|
+
<button class="cem-revert-btn" type="button" data-index="${index}">Revert</button>
|
|
412
|
+
</div>
|
|
413
|
+
`
|
|
414
|
+
).join("");
|
|
415
|
+
};
|
|
416
|
+
const renderStatus = (count, message) => {
|
|
417
|
+
if (copyButton) copyButton.textContent = count > 0 ? `\u{1F4CB} Copy Changes (${count})` : "\u{1F4CB} Copy Changes";
|
|
418
|
+
if (openLinkButton) {
|
|
419
|
+
openLinkButton.hidden = !activeLink;
|
|
420
|
+
openLinkButton.textContent = activeLink ? `Open ${activeLink.hostname || "link"}` : "Open link";
|
|
421
|
+
}
|
|
422
|
+
if (status) {
|
|
423
|
+
status.textContent = message || `Auto-saved ${formatTime()} \u2022 ${count} change${count === 1 ? "" : "s"} \u2022 Links: edit text or Ctrl/\u2318-click to open`;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
const updateUi = (message) => {
|
|
427
|
+
renderStatus(collectChanges(config.editableSelector).length, message);
|
|
428
|
+
};
|
|
429
|
+
const persistDraft = () => {
|
|
430
|
+
const draft = makeDraft();
|
|
431
|
+
currentDraftChanges = draft.changes;
|
|
432
|
+
const saved = config.autoSave !== false && saveDraft(storage, draftKey, draft);
|
|
433
|
+
renderStatus(draft.changes.length, saved || config.autoSave === false ? void 0 : "\u26A0\uFE0F Auto-save unavailable \u2014 use Download backup");
|
|
434
|
+
if (reviewOpen) renderReviewList();
|
|
435
|
+
return draft;
|
|
436
|
+
};
|
|
437
|
+
let saveTimeout = null;
|
|
438
|
+
const flushDraftSave = () => {
|
|
439
|
+
if (saveTimeout !== null) {
|
|
440
|
+
window.clearTimeout(saveTimeout);
|
|
441
|
+
saveTimeout = null;
|
|
442
|
+
}
|
|
443
|
+
return persistDraft();
|
|
444
|
+
};
|
|
445
|
+
const scheduleDraftSave = () => {
|
|
446
|
+
if (saveTimeout !== null) window.clearTimeout(saveTimeout);
|
|
447
|
+
saveTimeout = window.setTimeout(() => {
|
|
448
|
+
saveTimeout = null;
|
|
449
|
+
persistDraft();
|
|
450
|
+
}, AUTOSAVE_DEBOUNCE_MS);
|
|
186
451
|
};
|
|
187
|
-
|
|
188
|
-
|
|
452
|
+
const finishEditing = () => {
|
|
453
|
+
const activeElement = document.activeElement;
|
|
454
|
+
if (!(activeElement instanceof HTMLElement)) return false;
|
|
455
|
+
if (!activeElement.isContentEditable && activeElement.contentEditable !== "true") return false;
|
|
456
|
+
activeElement.blur();
|
|
457
|
+
window.getSelection()?.removeAllRanges();
|
|
458
|
+
flushDraftSave();
|
|
459
|
+
return true;
|
|
460
|
+
};
|
|
461
|
+
const handleEditingKeydown = (event) => {
|
|
462
|
+
if (event.key === "Escape") {
|
|
463
|
+
if (!finishEditing()) return;
|
|
464
|
+
event.preventDefault();
|
|
465
|
+
event.stopPropagation();
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
469
|
+
const target = event.target;
|
|
470
|
+
if (target instanceof HTMLElement && SINGLE_LINE_TAGS.has(target.tagName) && (target.isContentEditable || target.contentEditable === "true")) {
|
|
471
|
+
event.preventDefault();
|
|
472
|
+
finishEditing();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
const handleDocumentClick = (event) => {
|
|
189
477
|
const target = event.target;
|
|
190
478
|
if (!(target instanceof Element)) return;
|
|
479
|
+
const anchor = target.closest("a[href]");
|
|
480
|
+
if (anchor && !anchor.closest(".cem-panel") && !anchor.closest(".cem-edit-banner")) {
|
|
481
|
+
activeLink = anchor;
|
|
482
|
+
updateUi("Link selected \u2014 edit the text, use Open link, or Ctrl/\u2318-click to visit it");
|
|
483
|
+
if (event.metaKey || event.ctrlKey || event.altKey) {
|
|
484
|
+
flushDraftSave();
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
event.preventDefault();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
191
490
|
const button = target.closest("button");
|
|
192
|
-
if (button && !button.
|
|
491
|
+
if (button && !button.closest(".cem-panel") && !button.closest(".cem-edit-banner")) {
|
|
193
492
|
event.preventDefault();
|
|
194
493
|
event.stopImmediatePropagation();
|
|
195
494
|
}
|
|
196
495
|
};
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
496
|
+
const handleFocusIn = (event) => {
|
|
497
|
+
const target = event.target;
|
|
498
|
+
if (!(target instanceof Element)) return;
|
|
499
|
+
activeLink = target.closest("a[href]");
|
|
500
|
+
updateUi(activeLink ? "Link selected \u2014 edit the text or use Open link to navigate" : void 0);
|
|
501
|
+
};
|
|
502
|
+
const handleInput = (event) => {
|
|
503
|
+
if (event.target instanceof HTMLElement) markChanged(event.target);
|
|
504
|
+
scheduleDraftSave();
|
|
505
|
+
};
|
|
506
|
+
const handlePageHide = () => flushDraftSave();
|
|
507
|
+
const handleVisibilityChange = () => {
|
|
508
|
+
if (document.visibilityState === "hidden") flushDraftSave();
|
|
204
509
|
};
|
|
205
|
-
document.addEventListener("
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
510
|
+
document.addEventListener("keydown", handleEditingKeydown);
|
|
511
|
+
document.addEventListener("click", handleDocumentClick, true);
|
|
512
|
+
document.addEventListener("focusin", handleFocusIn);
|
|
513
|
+
document.addEventListener("input", handleInput);
|
|
514
|
+
window.addEventListener("pagehide", handlePageHide);
|
|
515
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
516
|
+
reviewButton?.addEventListener("click", () => {
|
|
517
|
+
reviewOpen = !reviewOpen;
|
|
518
|
+
if (reviewList) reviewList.hidden = !reviewOpen;
|
|
519
|
+
reviewButton.textContent = reviewOpen ? "Hide review" : "Review";
|
|
520
|
+
reviewButton.setAttribute("aria-expanded", String(reviewOpen));
|
|
521
|
+
if (reviewOpen) renderReviewList();
|
|
522
|
+
});
|
|
523
|
+
reviewList?.addEventListener("click", (event) => {
|
|
524
|
+
const target = event.target;
|
|
525
|
+
if (!(target instanceof HTMLElement)) return;
|
|
526
|
+
const revertButton = target.closest(".cem-revert-btn");
|
|
527
|
+
if (!revertButton) return;
|
|
528
|
+
const change = currentDraftChanges[Number(revertButton.dataset.index)];
|
|
529
|
+
if (!change) return;
|
|
530
|
+
const el = findEditableByKey(config.editableSelector, change.key);
|
|
531
|
+
if (el) el.textContent = change.original;
|
|
532
|
+
flushDraftSave();
|
|
533
|
+
});
|
|
534
|
+
copyButton?.addEventListener("click", () => {
|
|
535
|
+
const draft = flushDraftSave();
|
|
536
|
+
const payload = buildPayload(draft.changes);
|
|
217
537
|
options.onCopy?.(payload);
|
|
218
538
|
const copyPayload = options.mapPayload ? options.mapPayload(payload) : payload;
|
|
219
539
|
const json = JSON.stringify(copyPayload, null, 2);
|
|
220
|
-
const label = `\u2713 Copied ${changes.length} change${changes.length !== 1 ? "s" : ""} to clipboard`;
|
|
540
|
+
const label = `\u2713 Copied ${draft.changes.length} change${draft.changes.length !== 1 ? "s" : ""} to clipboard`;
|
|
221
541
|
if (navigator.clipboard?.writeText) {
|
|
222
542
|
navigator.clipboard.writeText(json).then(() => showToast(label)).catch(() => fallbackCopy(json, label));
|
|
223
543
|
} else {
|
|
224
544
|
fallbackCopy(json, label);
|
|
225
545
|
}
|
|
226
546
|
});
|
|
547
|
+
downloadButton?.addEventListener("click", () => {
|
|
548
|
+
const draft = flushDraftSave();
|
|
549
|
+
const payload = buildPayload(draft.changes);
|
|
550
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
551
|
+
downloadText(`edit-mode-${window.location.hostname}-${date}.json`, JSON.stringify(payload, null, 2));
|
|
552
|
+
showToast("\u2713 Backup downloaded");
|
|
553
|
+
});
|
|
554
|
+
openLinkButton?.addEventListener("click", () => {
|
|
555
|
+
if (!activeLink?.href) return;
|
|
556
|
+
flushDraftSave();
|
|
557
|
+
window.location.href = activeLink.href;
|
|
558
|
+
});
|
|
559
|
+
exitButton.addEventListener("click", () => {
|
|
560
|
+
flushDraftSave();
|
|
561
|
+
window.sessionStorage.removeItem(config.sessionKey);
|
|
562
|
+
const url = new URL(window.location.href);
|
|
563
|
+
url.searchParams.delete(config.queryParam);
|
|
564
|
+
window.location.href = url.toString();
|
|
565
|
+
});
|
|
566
|
+
persistDraft();
|
|
567
|
+
if (restoredCount > 0) updateUi(`Restored ${restoredCount} saved edit${restoredCount === 1 ? "" : "s"} \u2022 Auto-save on`);
|
|
227
568
|
return {
|
|
228
569
|
active: true,
|
|
229
570
|
getChanges: () => collectChanges(config.editableSelector),
|
|
230
571
|
destroy: () => {
|
|
231
|
-
|
|
232
|
-
document.removeEventListener("
|
|
572
|
+
flushDraftSave();
|
|
573
|
+
document.removeEventListener("keydown", handleEditingKeydown);
|
|
574
|
+
document.removeEventListener("click", handleDocumentClick, true);
|
|
575
|
+
document.removeEventListener("focusin", handleFocusIn);
|
|
576
|
+
document.removeEventListener("input", handleInput);
|
|
577
|
+
window.removeEventListener("pagehide", handlePageHide);
|
|
578
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
233
579
|
banner.remove();
|
|
234
|
-
|
|
580
|
+
panel.remove();
|
|
235
581
|
style.remove();
|
|
236
582
|
document.querySelectorAll(config.editableSelector).forEach((el) => {
|
|
237
583
|
if (el.dataset.editOriginal !== void 0) {
|
|
238
584
|
el.contentEditable = "false";
|
|
585
|
+
el.classList.remove("cem-changed");
|
|
239
586
|
delete el.dataset.editOriginal;
|
|
587
|
+
delete el.dataset.editKey;
|
|
240
588
|
}
|
|
241
589
|
});
|
|
242
590
|
window.__COLLIDE_EDIT_MODE_ACTIVE = false;
|
|
243
591
|
}
|
|
244
592
|
};
|
|
245
593
|
}
|
|
246
|
-
function collectChanges(editableSelector) {
|
|
247
|
-
const changes = [];
|
|
248
|
-
document.querySelectorAll(editableSelector).forEach((el) => {
|
|
249
|
-
if (!el.isContentEditable && el.contentEditable !== "true") return;
|
|
250
|
-
const original = el.dataset.editOriginal;
|
|
251
|
-
if (original === void 0) return;
|
|
252
|
-
const current = el.textContent?.trim() ?? "";
|
|
253
|
-
if (original !== current) {
|
|
254
|
-
changes.push({
|
|
255
|
-
path: getEditPath(el),
|
|
256
|
-
tag: el.tagName,
|
|
257
|
-
original,
|
|
258
|
-
new: current
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
return changes;
|
|
263
|
-
}
|
|
264
594
|
// Annotate the CommonJS export names for ESM import in node:
|
|
265
595
|
0 && (module.exports = {
|
|
266
596
|
initClientEditMode
|