autumnnote 1.0.6 → 1.0.7

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.
@@ -325,14 +325,42 @@
325
325
  * @param {HTMLElement} [editable] - scoping element to avoid touching nodes outside this editor
326
326
  */
327
327
  function fontSize(size, editable = document) {
328
+ const sel = window.getSelection();
329
+ const wasCollapsed = !sel || !sel.rangeCount || sel.getRangeAt(0).collapsed;
328
330
  execCommand("fontSize", "7");
329
- editable.querySelectorAll("font[size=\"7\"]").forEach((el) => {
331
+ const scope = editable instanceof HTMLElement ? editable : document;
332
+ const newSpans = [];
333
+ scope.querySelectorAll("font[size=\"7\"]").forEach((el) => {
330
334
  const span = document.createElement("span");
331
335
  span.style.fontSize = size;
332
336
  el.parentNode.insertBefore(span, el);
333
337
  while (el.firstChild) span.appendChild(el.firstChild);
334
338
  el.parentNode.removeChild(el);
339
+ newSpans.push(span);
335
340
  });
341
+ if (sel && newSpans.length > 0) {
342
+ const first = newSpans[0];
343
+ const last = newSpans[newSpans.length - 1];
344
+ try {
345
+ if (wasCollapsed) {
346
+ if (!first.firstChild) first.appendChild(document.createTextNode("​"));
347
+ const nr = document.createRange();
348
+ const anchor = first.firstChild;
349
+ nr.setStart(anchor, anchor.textContent.length);
350
+ nr.collapse(true);
351
+ sel.removeAllRanges();
352
+ sel.addRange(nr);
353
+ } else {
354
+ const nr = document.createRange();
355
+ const startNode = first.firstChild || first;
356
+ const endNode = last.lastChild || last;
357
+ nr.setStart(startNode, 0);
358
+ nr.setEnd(endNode, endNode.nodeType === Node.TEXT_NODE ? endNode.textContent.length : endNode.childNodes.length);
359
+ sel.removeAllRanges();
360
+ sel.addRange(nr);
361
+ }
362
+ } catch (_) {}
363
+ }
336
364
  }
337
365
  /**
338
366
  * Wraps the selection in the given block tag (p, h1-h6, blockquote, pre).
@@ -437,9 +465,24 @@
437
465
  const codeEl = container && container.closest ? container.closest("code") : null;
438
466
  if (codeEl && !codeEl.closest("pre")) {
439
467
  const parent = codeEl.parentNode;
468
+ const prevSibling = codeEl.previousSibling;
469
+ const movedChildren = Array.from(codeEl.childNodes);
440
470
  while (codeEl.firstChild) parent.insertBefore(codeEl.firstChild, codeEl);
441
471
  parent.removeChild(codeEl);
442
- if (editable) editable.normalize();
472
+ if (parent && parent.normalize) parent.normalize();
473
+ if (movedChildren.length > 0) try {
474
+ const firstMoved = movedChildren[0];
475
+ const lastMoved = movedChildren[movedChildren.length - 1];
476
+ const nr = document.createRange();
477
+ const anchorNode = firstMoved.parentNode === parent ? firstMoved : prevSibling ? prevSibling.nextSibling : parent.firstChild;
478
+ if (anchorNode) {
479
+ nr.setStart(anchorNode, 0);
480
+ const endAnchor = lastMoved.parentNode === parent ? lastMoved : anchorNode;
481
+ nr.setEnd(endAnchor, endAnchor.nodeType === Node.TEXT_NODE ? endAnchor.textContent.length : endAnchor.childNodes.length);
482
+ sel.removeAllRanges();
483
+ sel.addRange(nr);
484
+ }
485
+ } catch (_) {}
443
486
  } else {
444
487
  if (range.collapsed) return;
445
488
  try {
@@ -464,14 +507,17 @@
464
507
  /**
465
508
  * Returns true when the cursor / selection is inside an inline <code>
466
509
  * (not nested in a <pre>).
510
+ * Uses startContainer for reliable cross-browser detection regardless of
511
+ * whether the selection is collapsed or a range (commonAncestorContainer
512
+ * can behave inconsistently for range selections on some browsers).
467
513
  * @returns {boolean}
468
514
  */
469
515
  function isInlineCode() {
470
516
  const sel = window.getSelection();
471
517
  if (!sel || !sel.rangeCount) return false;
472
- let container = sel.getRangeAt(0).commonAncestorContainer;
473
- if (container.nodeType === 3) container = container.parentElement;
474
- const code = container && container.closest ? container.closest("code") : null;
518
+ let sc = sel.getRangeAt(0).startContainer;
519
+ if (sc.nodeType === 3) sc = sc.parentElement;
520
+ const code = sc && sc.closest ? sc.closest("code") : null;
475
521
  return !!(code && !code.closest("pre"));
476
522
  }
477
523
  /**
@@ -482,7 +528,8 @@
482
528
  function toggleChecklist() {
483
529
  const sel = window.getSelection();
484
530
  if (!sel || !sel.rangeCount) return;
485
- let container = sel.getRangeAt(0).commonAncestorContainer;
531
+ const range = sel.getRangeAt(0);
532
+ let container = range.commonAncestorContainer;
486
533
  if (container.nodeType === 3) container = container.parentElement;
487
534
  const ul = container.closest && container.closest(".an-checklist");
488
535
  if (ul) {
@@ -508,6 +555,45 @@
508
555
  return;
509
556
  }
510
557
  }
558
+ if (range.collapsed) {
559
+ const BLOCK_TAGS = new Set([
560
+ "P",
561
+ "DIV",
562
+ "H1",
563
+ "H2",
564
+ "H3",
565
+ "H4",
566
+ "H5",
567
+ "H6",
568
+ "BLOCKQUOTE",
569
+ "LI"
570
+ ]);
571
+ let block = container;
572
+ while (block && block.parentNode && !BLOCK_TAGS.has(block.tagName)) block = block.parentNode;
573
+ const itemText = block && BLOCK_TAGS.has(block.tagName) ? Array.from(block.childNodes).map((n) => n.textContent).join("").replace(/\u00a0/g, " ") : "";
574
+ const ul = document.createElement("ul");
575
+ ul.className = "an-checklist";
576
+ const li = document.createElement("li");
577
+ const checkbox = document.createElement("input");
578
+ checkbox.type = "checkbox";
579
+ checkbox.contentEditable = "false";
580
+ li.appendChild(checkbox);
581
+ li.appendChild(document.createTextNode(itemText || "​"));
582
+ ul.appendChild(li);
583
+ if (block && BLOCK_TAGS.has(block.tagName)) block.parentNode.replaceChild(ul, block);
584
+ else {
585
+ document.execCommand("insertHTML", false, ul.outerHTML);
586
+ return;
587
+ }
588
+ const textNode = li.lastChild;
589
+ const nr = document.createRange();
590
+ const offset = textNode.nodeType === Node.TEXT_NODE ? textNode.textContent.length : 0;
591
+ nr.setStart(textNode, offset);
592
+ nr.collapse(true);
593
+ sel.removeAllRanges();
594
+ sel.addRange(nr);
595
+ return;
596
+ }
511
597
  const lines = sel.toString().split(/\r?\n/).filter((l) => l.trim().length > 0);
512
598
  if (lines.length === 0) return;
513
599
  const items = lines.map((l) => `<li><input type="checkbox" contenteditable="false">${l || "​"}</li>`).join("");
@@ -576,9 +662,9 @@
576
662
  if (document.queryCommandState("underline")) return true;
577
663
  const sel = window.getSelection();
578
664
  if (!sel || !sel.rangeCount) return false;
579
- let container = sel.getRangeAt(0).commonAncestorContainer;
580
- if (container.nodeType === 3) container = container.parentElement;
581
- return !!(container && container.closest && container.closest("u"));
665
+ let sc = sel.getRangeAt(0).startContainer;
666
+ if (sc.nodeType === 3) sc = sc.parentElement;
667
+ return !!(sc && sc.closest && sc.closest("u"));
582
668
  });
583
669
  var strikeBtn = btn("strikethrough", "strikethrough", "Strikethrough", () => strikethrough(), () => document.queryCommandState("strikeThrough"));
584
670
  var superscriptBtn = btn("superscript", "superscript", "Superscript", () => superscript(), () => document.queryCommandState("superscript"));
@@ -972,7 +1058,6 @@
972
1058
  "object",
973
1059
  "embed",
974
1060
  "form",
975
- "input",
976
1061
  "button"
977
1062
  ];
978
1063
  /** Attributes whose values must be sanitised as URLs. */
@@ -996,7 +1081,8 @@
996
1081
  * Uses DOMParser so the sanitisation follows normal browser parsing rules —
997
1082
  * no regex shortcuts that can be bypassed by encoding tricks.
998
1083
  *
999
- * - Strips PROHIBITED_TAGS (script, style, iframe, object, embed, form, input, button)
1084
+ * - Strips PROHIBITED_TAGS (script, style, iframe, object, embed, form, button)
1085
+ * - Allows input[type="checkbox"] only inside ul.an-checklist li; removes all other <input>
1000
1086
  * - Removes all on* event-handler attributes
1001
1087
  * - Rejects javascript: and vbscript: URLs in URL attributes
1002
1088
  * - Rejects data: URIs everywhere except img[src] (base64 uploads)
@@ -1035,6 +1121,16 @@
1035
1121
  }
1036
1122
  });
1037
1123
  });
1124
+ doc.querySelectorAll("input").forEach((el) => {
1125
+ if (!(el.closest("ul.an-checklist") !== null && el.closest("li") !== null) || el.getAttribute("type") !== "checkbox") el.remove();
1126
+ else Array.from(el.attributes).forEach((attr) => {
1127
+ if (![
1128
+ "type",
1129
+ "checked",
1130
+ "contenteditable"
1131
+ ].includes(attr.name)) el.removeAttribute(attr.name);
1132
+ });
1133
+ });
1038
1134
  return doc.body.innerHTML;
1039
1135
  }
1040
1136
  /**
@@ -1115,7 +1211,12 @@
1115
1211
  if (options.maxHeight) editable.style.maxHeight = `${options.maxHeight}px`;
1116
1212
  container.appendChild(editable);
1117
1213
  if (options.theme === "dark") container.classList.add("an-theme-dark");
1118
- if (options.readOnly) container.classList.add("an-disabled");
1214
+ if (options.readOnly) {
1215
+ container.classList.add("an-disabled");
1216
+ editable.querySelectorAll("ul.an-checklist input[type=\"checkbox\"]").forEach((cb) => {
1217
+ cb.setAttribute("disabled", "");
1218
+ });
1219
+ }
1119
1220
  if (options.direction === "rtl") {
1120
1221
  editable.setAttribute("dir", "rtl");
1121
1222
  container.classList.add("an-dir-rtl");
@@ -1994,7 +2095,37 @@
1994
2095
  sel.removeAllRanges();
1995
2096
  sel.addRange(nr);
1996
2097
  };
1997
- this._disposers.push(on(editable, "keydown", onKeydown), on(editable, "beforeinput", onBeforeInput), on(editable, "input", onInput), on(document, "selectionchange", onSelChange), on(editable, "click", onCheckboxClick), on(editable, "mouseup", fixChecklistCursor), on(editable, "keyup", fixChecklistCursor));
2098
+ const isReadOnly = () => this.context.layoutInfo.container.classList.contains("an-disabled");
2099
+ this._disposers.push(on(editable, "keydown", onKeydown), on(editable, "beforeinput", onBeforeInput), on(editable, "input", onInput), on(document, "selectionchange", onSelChange), on(editable, "click", onCheckboxClick), on(editable, "mouseup", fixChecklistCursor), on(editable, "keyup", fixChecklistCursor), on(editable, "dragstart", (e) => {
2100
+ if (isReadOnly()) e.preventDefault();
2101
+ }), on(editable, "drop", (e) => {
2102
+ if (isReadOnly()) e.preventDefault();
2103
+ }));
2104
+ /** @type {string|null} 'superscript' | 'subscript' | null */
2105
+ let _compositionSupSub = null;
2106
+ const onCompositionStart = () => {
2107
+ const sel = window.getSelection();
2108
+ if (!sel || !sel.rangeCount) {
2109
+ _compositionSupSub = null;
2110
+ return;
2111
+ }
2112
+ let node = sel.getRangeAt(0).startContainer;
2113
+ if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
2114
+ if (node && node.closest) if (node.closest("sup")) _compositionSupSub = "superscript";
2115
+ else if (node.closest("sub")) _compositionSupSub = "subscript";
2116
+ else _compositionSupSub = null;
2117
+ };
2118
+ const onCompositionEnd = () => {
2119
+ const tag = _compositionSupSub;
2120
+ _compositionSupSub = null;
2121
+ if (!tag) return;
2122
+ const sel = window.getSelection();
2123
+ if (!sel || !sel.rangeCount) return;
2124
+ let node = sel.getRangeAt(0).startContainer;
2125
+ if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
2126
+ if (!(node && node.closest && (tag === "superscript" ? node.closest("sup") : node.closest("sub")))) document.execCommand(tag);
2127
+ };
2128
+ this._disposers.push(on(editable, "compositionstart", onCompositionStart), on(editable, "compositionend", onCompositionEnd));
1998
2129
  }
1999
2130
  _onKeydown(event) {
2000
2131
  const editable = this.context.layoutInfo.editable;
@@ -2080,6 +2211,7 @@
2080
2211
  }
2081
2212
  }
2082
2213
  afterCommand() {
2214
+ this._cleanOrphanedFigures();
2083
2215
  this.context.invoke("toolbar.refresh");
2084
2216
  this.context.invoke("statusbar.update");
2085
2217
  this._scheduleSnapshot();
@@ -2096,6 +2228,16 @@
2096
2228
  this.context.triggerEvent("change", this.getHTML());
2097
2229
  }, 400);
2098
2230
  }
2231
+ /**
2232
+ * C4: Removes figure.an-figure elements that no longer contain an <img>.
2233
+ * This happens when a user selects only the image (not the whole figure)
2234
+ * and deletes or replaces it, leaving a dangling figcaption.
2235
+ */
2236
+ _cleanOrphanedFigures() {
2237
+ this.context.layoutInfo.editable.querySelectorAll("figure.an-figure").forEach((fig) => {
2238
+ if (!fig.querySelector("img")) fig.parentNode.removeChild(fig);
2239
+ });
2240
+ }
2099
2241
  focus() {
2100
2242
  this.context.layoutInfo.editable.focus();
2101
2243
  }
@@ -2827,15 +2969,28 @@
2827
2969
  if (def.name === "fontFamily" && !isHeader) opt.style.fontFamily = value;
2828
2970
  select.appendChild(opt);
2829
2971
  });
2972
+ /** @type {Range|null} */
2973
+ let _savedRange = null;
2974
+ const dMousedown = on(select, "mousedown", () => {
2975
+ const sel = window.getSelection();
2976
+ _savedRange = sel && sel.rangeCount ? sel.getRangeAt(0).cloneRange() : null;
2977
+ });
2830
2978
  const disposer = on(select, "change", (e) => {
2831
2979
  const value = e.target.value;
2832
2980
  const selectedOpt = e.target.options[e.target.selectedIndex];
2833
2981
  if (!value || selectedOpt.disabled) return;
2834
2982
  this.context.invoke("editor.focus");
2983
+ if (_savedRange) try {
2984
+ const sel = window.getSelection();
2985
+ if (sel) {
2986
+ sel.removeAllRanges();
2987
+ sel.addRange(_savedRange);
2988
+ }
2989
+ } catch (_) {}
2835
2990
  def.action(this.context, value);
2836
2991
  this.context.invoke("editor.afterCommand");
2837
2992
  });
2838
- this._disposers.push(disposer);
2993
+ this._disposers.push(dMousedown, disposer);
2839
2994
  return select;
2840
2995
  }
2841
2996
  /**
@@ -3292,9 +3447,25 @@
3292
3447
  this.options.onImageUpload(files);
3293
3448
  return;
3294
3449
  }
3450
+ const UNSUPPORTED = [
3451
+ "image/tiff",
3452
+ "image/x-tiff",
3453
+ "image/bmp",
3454
+ "image/x-bmp",
3455
+ "image/x-ms-bmp"
3456
+ ];
3295
3457
  const maxBytes = (this.options.maxImageSize || 5) * 1024 * 1024;
3296
3458
  files.forEach((file) => {
3297
3459
  if (!file || !file.type.startsWith("image/")) return;
3460
+ if (UNSUPPORTED.includes(file.type)) {
3461
+ const message = `Image format "${file.type}" is not supported for display in web browsers. Please convert to PNG, JPEG, or WebP first.`;
3462
+ this.context.triggerEvent("imageError", {
3463
+ file,
3464
+ message
3465
+ });
3466
+ console.warn("[AutumnNote]", message);
3467
+ return;
3468
+ }
3298
3469
  if (file.size > maxBytes) {
3299
3470
  const message = `Image "${file.name}" exceeds the ${this.options.maxImageSize || 5} MB size limit.`;
3300
3471
  this.context.triggerEvent("imageError", {
@@ -3458,7 +3629,7 @@
3458
3629
  _update() {
3459
3630
  const editable = this.context.layoutInfo.editable;
3460
3631
  const isFocused = document.activeElement === editable;
3461
- const isEmpty = !editable.textContent.trim() && !editable.querySelector("img, table, hr");
3632
+ const isEmpty = !editable.textContent.trim() && !editable.querySelector("img, table, hr, .an-video-wrapper");
3462
3633
  editable.classList.toggle("an-placeholder", isEmpty && !isFocused);
3463
3634
  }
3464
3635
  };
@@ -3896,9 +4067,11 @@
3896
4067
  accept: "image/*"
3897
4068
  });
3898
4069
  this._fileInput = fileInput;
4070
+ const fileHint = createElement("p", { class: "an-dialog-hint" });
4071
+ this._fileHint = fileHint;
3899
4072
  const d = on(fileInput, "change", () => this._onFileChange());
3900
4073
  this._disposers.push(d);
3901
- box.append(fileLabel, fileInput);
4074
+ box.append(fileLabel, fileInput, fileHint);
3902
4075
  }
3903
4076
  const btnRow = createElement("div", { class: "an-dialog-actions" });
3904
4077
  const insertBtn = createElement("button", {
@@ -3938,9 +4111,27 @@
3938
4111
  _onFileChange() {
3939
4112
  const file = this._fileInput && this._fileInput.files && this._fileInput.files[0];
3940
4113
  if (!file || !file.type.startsWith("image/")) return;
4114
+ if ([
4115
+ "image/tiff",
4116
+ "image/x-tiff",
4117
+ "image/bmp",
4118
+ "image/x-bmp",
4119
+ "image/x-ms-bmp"
4120
+ ].includes(file.type)) {
4121
+ const message = `Format "${file.type}" is not supported for display in web browsers. Please convert to PNG, JPEG, or WebP first.`;
4122
+ if (this._fileHint) this._fileHint.textContent = message;
4123
+ this.context.triggerEvent("imageError", {
4124
+ file,
4125
+ message
4126
+ });
4127
+ this._fileInput.value = "";
4128
+ return;
4129
+ }
4130
+ if (this._fileHint) this._fileHint.textContent = "";
3941
4131
  const maxSize = (this.options.maxImageSize || 5) * 1024 * 1024;
3942
4132
  if (file.size > maxSize) {
3943
4133
  const message = `Image file is too large. Maximum allowed size is ${this.options.maxImageSize || 5} MB.`;
4134
+ if (this._fileHint) this._fileHint.textContent = message;
3944
4135
  console.warn("[AutumnNote] ImageDialog:", message);
3945
4136
  this.context.triggerEvent("imageError", {
3946
4137
  file,
@@ -4252,6 +4443,7 @@
4252
4443
  _resizeDebounce = setTimeout(() => this._updateOverlayPosition(), 100);
4253
4444
  };
4254
4445
  this._disposers.push(on(editable, "click", (e) => this._onEditorClick(e)), on(editable, "contextmenu", (e) => {
4446
+ if (this.context.layoutInfo.container.classList.contains("an-disabled")) return;
4255
4447
  const img = e.target.closest("img");
4256
4448
  if (img) this._select(img);
4257
4449
  }), on(document, "click", (e) => this._onDocClick(e)), on(window, "scroll", () => this._updateOverlayPosition(), { passive: true }), on(window, "resize", onWindowResize, { passive: true }), on(editable, "scroll", () => this._updateOverlayPosition(), { passive: true }));
@@ -4302,6 +4494,7 @@
4302
4494
  return overlay;
4303
4495
  }
4304
4496
  _onEditorClick(e) {
4497
+ if (this.context.layoutInfo.container.classList.contains("an-disabled")) return;
4305
4498
  const img = e.target.closest("img");
4306
4499
  if (img) {
4307
4500
  e.preventDefault();
@@ -4458,9 +4651,12 @@
4458
4651
  _resizeDebounce = setTimeout(() => this._updateOverlayPosition(), 100);
4459
4652
  };
4460
4653
  this._disposers.push(on(editable, "click", (e) => this._onEditorClick(e)), on(editable, "contextmenu", (e) => {
4654
+ if (this.context.layoutInfo.container.classList.contains("an-disabled")) return;
4461
4655
  const wrapper = this._findWrapper(e.target);
4462
4656
  if (wrapper) this._select(wrapper);
4463
- }), on(document, "click", (e) => this._onDocClick(e)), on(window, "scroll", () => this._updateOverlayPosition(), { passive: true }), on(window, "resize", onWindowResize), on(editable, "scroll", () => this._updateOverlayPosition(), { passive: true }));
4657
+ }), on(document, "click", (e) => this._onDocClick(e)), on(window, "scroll", () => this._updateOverlayPosition(), { passive: true }), on(window, "resize", onWindowResize), on(editable, "scroll", () => this._updateOverlayPosition(), { passive: true }), on(editable, "dragstart", (e) => {
4658
+ if (e.target instanceof Element && e.target.closest(".an-video-wrapper")) e.preventDefault();
4659
+ }));
4464
4660
  return this;
4465
4661
  }
4466
4662
  destroy() {
@@ -4519,6 +4715,7 @@
4519
4715
  return overlay;
4520
4716
  }
4521
4717
  _onEditorClick(e) {
4718
+ if (this.context.layoutInfo.container.classList.contains("an-disabled")) return;
4522
4719
  const wrapper = this._findWrapper(e.target);
4523
4720
  if (wrapper) {
4524
4721
  e.preventDefault();
@@ -4717,9 +4914,12 @@
4717
4914
  }, HIDE_DELAY$4);
4718
4915
  }
4719
4916
  _show(anchor) {
4917
+ const isReadOnly = this.context.layoutInfo.container.classList.contains("an-disabled");
4720
4918
  const url = anchor.getAttribute("href") || "";
4721
4919
  this._urlLabel.textContent = this._truncateUrl(url);
4722
4920
  this._urlLabel.title = url;
4921
+ this._editBtn.style.display = isReadOnly ? "none" : "";
4922
+ this._unlinkBtn.style.display = isReadOnly ? "none" : "";
4723
4923
  this._el.style.display = "flex";
4724
4924
  this._positionNear(anchor);
4725
4925
  }
@@ -4833,6 +5033,7 @@
4833
5033
  document.body.appendChild(this._el);
4834
5034
  const editable = this.context.layoutInfo.editable;
4835
5035
  this._disposers.push(on(editable, "mouseover", (e) => {
5036
+ if (this.context.layoutInfo.container.classList.contains("an-disabled")) return;
4836
5037
  const img = e.target.closest("img");
4837
5038
  if (img && editable.contains(img) && !img.closest("a[href]")) this._scheduleShow(img);
4838
5039
  }, { passive: true }), on(editable, "mouseout", (e) => {
@@ -4959,6 +5160,7 @@
4959
5160
  const target = img.closest("figure.an-figure") || img;
4960
5161
  target.style.float = value;
4961
5162
  target.style.display = "";
5163
+ if (target !== img) target.style.width = "";
4962
5164
  target.style.marginLeft = value === "right" ? "12px" : "";
4963
5165
  target.style.marginRight = value === "left" ? "12px" : "";
4964
5166
  this.context.invoke("editor.afterCommand");
@@ -4973,6 +5175,7 @@
4973
5175
  const target = img.closest("figure.an-figure") || img;
4974
5176
  target.style.float = "";
4975
5177
  target.style.display = "block";
5178
+ if (target !== img) target.style.width = "fit-content";
4976
5179
  target.style.marginLeft = "auto";
4977
5180
  target.style.marginRight = "auto";
4978
5181
  this.context.invoke("editor.afterCommand");
@@ -5057,6 +5260,7 @@
5057
5260
  img.style.marginRight = "";
5058
5261
  } else if (img.style.display === "block" && img.style.marginLeft === "auto") {
5059
5262
  figure.style.display = "block";
5263
+ figure.style.width = "fit-content";
5060
5264
  figure.style.marginLeft = "auto";
5061
5265
  figure.style.marginRight = "auto";
5062
5266
  img.style.display = "";
@@ -5111,6 +5315,7 @@
5111
5315
  document.body.appendChild(this._el);
5112
5316
  const editable = this.context.layoutInfo.editable;
5113
5317
  this._disposers.push(on(editable, "mouseover", (e) => {
5318
+ if (this.context.layoutInfo.container.classList.contains("an-disabled")) return;
5114
5319
  const wrapper = e.target.closest(".an-video-wrapper");
5115
5320
  if (wrapper && editable.contains(wrapper)) this._scheduleShow(wrapper);
5116
5321
  }, { passive: true }), on(editable, "mouseout", (e) => {
@@ -5402,6 +5607,7 @@
5402
5607
  document.body.appendChild(this._sizePopover);
5403
5608
  const editable = this.context.layoutInfo.editable;
5404
5609
  this._disposers.push(on(editable, "mouseover", (e) => {
5610
+ if (this.context.layoutInfo.container.classList.contains("an-disabled")) return;
5405
5611
  const table = e.target.closest("table");
5406
5612
  if (table && editable.contains(table)) {
5407
5613
  const cell = e.target.closest("td, th");
@@ -5443,6 +5649,10 @@
5443
5649
  };
5444
5650
  const onEditorMove = (e) => {
5445
5651
  if (_resizing) return;
5652
+ if (this.context.layoutInfo.container.classList.contains("an-disabled")) {
5653
+ clearHover();
5654
+ return;
5655
+ }
5446
5656
  if (_rafEditorMove !== null) return;
5447
5657
  const target = e.target;
5448
5658
  const clientX = e.clientX;
@@ -5470,6 +5680,7 @@
5470
5680
  });
5471
5681
  };
5472
5682
  const onEditorDown = (e) => {
5683
+ if (this.context.layoutInfo.container.classList.contains("an-disabled")) return;
5473
5684
  if (!_nearCell || !_nearEdge) return;
5474
5685
  _resizing = true;
5475
5686
  _edge = _nearEdge;
@@ -5919,6 +6130,7 @@
5919
6130
  this._ensurePrism();
5920
6131
  const editable = this.context.layoutInfo.editable;
5921
6132
  this._disposers.push(on(editable, "mouseover", (e) => {
6133
+ if (this.context.layoutInfo.container.classList.contains("an-disabled")) return;
5922
6134
  const pre = e.target.closest("pre");
5923
6135
  if (pre && editable.contains(pre)) this._scheduleShow(pre);
5924
6136
  }), on(editable, "mouseout", (e) => {
@@ -9349,22 +9561,50 @@
9349
9561
  copyFormat: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`,
9350
9562
  pasteFormat: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1"/><path d="m9 14 2 2 4-4"/></svg>`,
9351
9563
  removeFormat: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/><path d="M22 21H7"/><path d="m5 11 9 9"/></svg>`,
9352
- table: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/></svg>`
9564
+ table: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/></svg>`,
9565
+ textColor: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20L12 4L20 20"/><line x1="7.5" y1="14" x2="16.5" y2="14"/></svg>`,
9566
+ highlightColor: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21v-4l9-9 4 4-9 9z"/><path d="M12 8l4 4"/><line x1="3" y1="21" x2="21" y2="21"/></svg>`,
9567
+ noColor: `<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="4" y1="4" x2="20" y2="20"/><line x1="20" y1="4" x2="4" y2="20"/></svg>`,
9568
+ back: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>`
9353
9569
  };
9570
+ var COLOR_PRESETS = [
9571
+ "#000000",
9572
+ "#434343",
9573
+ "#666666",
9574
+ "#999999",
9575
+ "#b7b7b7",
9576
+ "#cccccc",
9577
+ "#efefef",
9578
+ "#ffffff",
9579
+ "#ff0000",
9580
+ "#ff9900",
9581
+ "#ffff00",
9582
+ "#00ff00",
9583
+ "#00ffff",
9584
+ "#4a86e8",
9585
+ "#9900ff",
9586
+ "#ff00ff",
9587
+ "#f4cccc",
9588
+ "#fce5cd",
9589
+ "#fff2cc",
9590
+ "#d9ead3",
9591
+ "#d0e0e3",
9592
+ "#c9daf8",
9593
+ "#d9d2e9",
9594
+ "#ead1dc"
9595
+ ];
9596
+ function makeColorSubItems(colorType) {
9597
+ const label = colorType === "foreColor" ? "Text Color" : "Highlight Color";
9598
+ return () => [{
9599
+ back: true,
9600
+ label,
9601
+ navigate: () => defaultItems
9602
+ }, {
9603
+ colorPalette: true,
9604
+ colorType
9605
+ }];
9606
+ }
9354
9607
  var defaultItems = [
9355
- {
9356
- name: "undo",
9357
- label: "Undo",
9358
- icon: ICONS.undo,
9359
- action: (ctx) => ctx.invoke("editor.undo")
9360
- },
9361
- {
9362
- name: "redo",
9363
- label: "Redo",
9364
- icon: ICONS.redo,
9365
- action: (ctx) => ctx.invoke("editor.redo")
9366
- },
9367
- { separator: true },
9368
9608
  {
9369
9609
  name: "cut",
9370
9610
  label: "Cut",
@@ -9425,6 +9665,21 @@
9425
9665
  action: (ctx) => ctx.invoke("editor.underline")
9426
9666
  },
9427
9667
  { separator: true },
9668
+ {
9669
+ name: "textColor",
9670
+ label: "Text Color",
9671
+ icon: ICONS.textColor,
9672
+ colorStrip: "foreColor",
9673
+ navigate: makeColorSubItems("foreColor")
9674
+ },
9675
+ {
9676
+ name: "highlightColor",
9677
+ label: "Highlight Color",
9678
+ icon: ICONS.highlightColor,
9679
+ colorStrip: "hiliteColor",
9680
+ navigate: makeColorSubItems("hiliteColor")
9681
+ },
9682
+ { separator: true },
9428
9683
  {
9429
9684
  name: "copyFormat",
9430
9685
  label: "Copy Format",
@@ -9542,8 +9797,10 @@
9542
9797
  backBtn.appendChild(createElement("span", { class: "an-context-label" }, [it.label || "Back"]));
9543
9798
  const off = on(backBtn, "click", (e) => {
9544
9799
  e.stopPropagation();
9800
+ const curLeft = parseFloat(this.el.style.left);
9801
+ const curTop = parseFloat(this.el.style.top);
9545
9802
  this._renderItems(it.navigate());
9546
- this._reposition();
9803
+ this._reposition(curLeft, curTop);
9547
9804
  });
9548
9805
  this._menuDisposers.push(off);
9549
9806
  this.el.appendChild(backBtn);
@@ -9555,7 +9812,19 @@
9555
9812
  class: "an-context-item an-context-submenu",
9556
9813
  "data-name": it.name || ""
9557
9814
  });
9558
- if (it.icon) {
9815
+ if (it.icon) if (it.colorStrip) {
9816
+ const iconWrap = createElement("span", {
9817
+ class: "an-context-icon an-context-icon--color",
9818
+ "aria-hidden": "true"
9819
+ });
9820
+ const svgSpan = createElement("span", { class: "an-context-icon-svg" });
9821
+ svgSpan.innerHTML = it.icon;
9822
+ const strip = createElement("span", { class: "an-context-color-strip" });
9823
+ strip.style.background = this._getSelectionColor(it.colorStrip);
9824
+ iconWrap.appendChild(svgSpan);
9825
+ iconWrap.appendChild(strip);
9826
+ btn.appendChild(iconWrap);
9827
+ } else {
9559
9828
  const iconSpan = createElement("span", {
9560
9829
  class: "an-context-icon",
9561
9830
  "aria-hidden": "true"
@@ -9572,13 +9841,63 @@
9572
9841
  btn.appendChild(chevron);
9573
9842
  const off = on(btn, "click", (e) => {
9574
9843
  e.stopPropagation();
9844
+ const curLeft = parseFloat(this.el.style.left);
9845
+ const curTop = parseFloat(this.el.style.top);
9575
9846
  this._renderItems(it.navigate());
9576
- this._reposition();
9847
+ this._reposition(curLeft, curTop);
9577
9848
  });
9578
9849
  this._menuDisposers.push(off);
9579
9850
  this.el.appendChild(btn);
9580
9851
  return;
9581
9852
  }
9853
+ if (it.colorPalette) {
9854
+ const palette = createElement("div", { class: "an-context-color-palette" });
9855
+ COLOR_PRESETS.forEach((color) => {
9856
+ const sw = createElement("div", {
9857
+ class: "an-context-color-swatch",
9858
+ title: color,
9859
+ role: "button",
9860
+ "aria-label": color
9861
+ });
9862
+ sw.style.background = color;
9863
+ const offSw = on(sw, "click", (e) => {
9864
+ e.stopPropagation();
9865
+ this._applyColor(it.colorType, color);
9866
+ });
9867
+ this._menuDisposers.push(offSw);
9868
+ palette.appendChild(sw);
9869
+ });
9870
+ if (it.colorType === "hiliteColor") {
9871
+ const noColor = createElement("div", {
9872
+ class: "an-context-color-swatch an-context-color-none",
9873
+ title: "No highlight",
9874
+ role: "button",
9875
+ "aria-label": "No highlight"
9876
+ });
9877
+ noColor.innerHTML = ICONS.noColor;
9878
+ const offNo = on(noColor, "click", (e) => {
9879
+ e.stopPropagation();
9880
+ this._applyColor("hiliteColor", "transparent");
9881
+ });
9882
+ this._menuDisposers.push(offNo);
9883
+ palette.appendChild(noColor);
9884
+ }
9885
+ this.el.appendChild(palette);
9886
+ const customRow = createElement("div", { class: "an-context-color-custom" });
9887
+ const colorInput = createElement("input", {
9888
+ type: "color",
9889
+ value: it.colorType === "foreColor" ? "#000000" : "#ffff00",
9890
+ title: "Custom color",
9891
+ "aria-label": "Custom color"
9892
+ });
9893
+ const customLabel = createElement("span", {}, ["Custom…"]);
9894
+ const offCustom = on(colorInput, "change", () => this._applyColor(it.colorType, colorInput.value));
9895
+ this._menuDisposers.push(offCustom);
9896
+ customRow.appendChild(colorInput);
9897
+ customRow.appendChild(customLabel);
9898
+ this.el.appendChild(customRow);
9899
+ return;
9900
+ }
9582
9901
  if (it.tableGrid) {
9583
9902
  const GRID_ROWS = 8, GRID_COLS = 8;
9584
9903
  const wrapper = createElement("div", { class: "an-context-table-wrap" });
@@ -9696,6 +10015,7 @@
9696
10015
  const editable = this.context.layoutInfo && this.context.layoutInfo.editable;
9697
10016
  if (!editable) return;
9698
10017
  if (!editable.contains(event.target)) return;
10018
+ if (this.context.layoutInfo.container.classList.contains("an-disabled")) return;
9699
10019
  event.preventDefault();
9700
10020
  this._lastX = event.clientX;
9701
10021
  this._lastY = event.clientY;
@@ -9722,7 +10042,19 @@
9722
10042
  let left = rx;
9723
10043
  let top = ry;
9724
10044
  if (left + rect.width > window.innerWidth) left = window.innerWidth - rect.width - 8;
10045
+ if (left < 8) left = 8;
9725
10046
  if (top + rect.height > window.innerHeight) top = window.innerHeight - rect.height - 8;
10047
+ if (top < 8) top = 8;
10048
+ if (this._savedRange) try {
10049
+ const sel = this._savedRange.getBoundingClientRect();
10050
+ if (sel.width > 0 || sel.height > 0) {
10051
+ if (top < sel.bottom && top + rect.height > sel.top && left < sel.right && left + rect.width > sel.left) {
10052
+ const belowTop = sel.bottom + 6;
10053
+ if (belowTop + rect.height <= window.innerHeight - 8) top = belowTop;
10054
+ else top = Math.max(8, sel.top - rect.height - 6);
10055
+ }
10056
+ }
10057
+ } catch (_) {}
9726
10058
  this.el.style.left = `${left}px`;
9727
10059
  this.el.style.top = `${top}px`;
9728
10060
  }
@@ -9731,6 +10063,33 @@
9731
10063
  this.el.style.display = "none";
9732
10064
  this.el.setAttribute("aria-hidden", "true");
9733
10065
  }
10066
+ /** Read the current selection's text or highlight color for the strip.
10067
+ * @param {'foreColor'|'hiliteColor'} type
10068
+ * @returns {string} CSS color string
10069
+ */
10070
+ _getSelectionColor(type) {
10071
+ const range = this._savedRange;
10072
+ if (!range) return type === "foreColor" ? "#000000" : "transparent";
10073
+ let node = range.startContainer;
10074
+ if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
10075
+ if (!node) return type === "foreColor" ? "#000000" : "transparent";
10076
+ const cs = window.getComputedStyle(node);
10077
+ if (type === "foreColor") return cs.color || "#000000";
10078
+ const bg = cs.backgroundColor;
10079
+ return !bg || bg === "rgba(0, 0, 0, 0)" || bg === "transparent" ? "transparent" : bg;
10080
+ }
10081
+ /** Restore selection, apply a color command, then hide the menu. */
10082
+ _applyColor(type, color) {
10083
+ const editable = this.context.layoutInfo && this.context.layoutInfo.editable;
10084
+ if (!editable || !this._savedRange) return;
10085
+ editable.focus();
10086
+ const sel = window.getSelection();
10087
+ sel.removeAllRanges();
10088
+ sel.addRange(this._savedRange.cloneRange());
10089
+ document.execCommand(type, false, color);
10090
+ this.context.invoke("editor.afterCommand");
10091
+ this.hide();
10092
+ }
9734
10093
  /** Returns true if a format has been copied — used to disable Paste Format. */
9735
10094
  hasCopiedFormat() {
9736
10095
  return !!this._copiedFormat;
@@ -10398,7 +10757,7 @@
10398
10757
  resolve(null);
10399
10758
  }
10400
10759
  };
10401
- if (img.src.startsWith("data:") || img.src.startsWith("blob:") || img.src.startsWith(location.origin)) {
10760
+ if (img.src.startsWith("data:") || img.src.startsWith("blob:") || img.src.startsWith(window.location.origin)) {
10402
10761
  tryDraw(img);
10403
10762
  return;
10404
10763
  }
@@ -10774,7 +11133,7 @@
10774
11133
  height: natH
10775
11134
  }, w, h);
10776
11135
  if (!canvas) {
10777
- alert("Cannot crop this image: the image server does not allow cross-origin access.\nUpload the image directly to use the crop tool.");
11136
+ window.alert("Cannot crop this image: the image server does not allow cross-origin access.\nUpload the image directly to use the crop tool.");
10778
11137
  this._close(false);
10779
11138
  return;
10780
11139
  }
@@ -11135,9 +11494,15 @@
11135
11494
  if (disabled) {
11136
11495
  editable.setAttribute("contenteditable", "false");
11137
11496
  this.layoutInfo.container.classList.add("an-disabled");
11497
+ editable.querySelectorAll("ul.an-checklist input[type=\"checkbox\"]").forEach((cb) => {
11498
+ cb.setAttribute("disabled", "");
11499
+ });
11138
11500
  } else {
11139
11501
  editable.setAttribute("contenteditable", "true");
11140
11502
  this.layoutInfo.container.classList.remove("an-disabled");
11503
+ editable.querySelectorAll("ul.an-checklist input[type=\"checkbox\"]").forEach((cb) => {
11504
+ cb.removeAttribute("disabled");
11505
+ });
11141
11506
  }
11142
11507
  }
11143
11508
  /**