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