@blankdotpage/cake 0.1.4 → 0.1.6

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.
@@ -3,6 +3,7 @@ import { type CakeExtension, type EditCommand } from "../core/runtime";
3
3
  import type { SelectionRect } from "./selection/selection-geometry";
4
4
  type EngineOptions = {
5
5
  container: HTMLElement;
6
+ contentRoot?: HTMLElement;
6
7
  value: string;
7
8
  selection?: Selection;
8
9
  extensions?: CakeExtension[];
@@ -24,8 +25,6 @@ export declare class CakeEngine {
24
25
  private runtime;
25
26
  private extensions;
26
27
  private _state;
27
- private originalCaretRangeFromPoint;
28
- private patchedCaretRangeFromPoint;
29
28
  private get state();
30
29
  private set state(value);
31
30
  private contentRoot;
@@ -56,6 +55,7 @@ export declare class CakeEngine {
56
55
  private spellCheckEnabled;
57
56
  private extensionsRoot;
58
57
  private placeholderRoot;
58
+ private resizeObserver;
59
59
  private lastFocusRect;
60
60
  private verticalNavGoalX;
61
61
  private lastRenderPerf;
@@ -89,6 +89,7 @@ export declare class CakeEngine {
89
89
  private pointerDownPosition;
90
90
  private hasMovedSincePointerDown;
91
91
  private lastTouchTime;
92
+ private isTouchDevice;
92
93
  constructor(options: EngineOptions);
93
94
  destroy(): void;
94
95
  setReadOnly(readOnly: boolean): void;
@@ -99,7 +100,7 @@ export declare class CakeEngine {
99
100
  getFocusRect(): SelectionRect | null;
100
101
  getContainer(): HTMLElement;
101
102
  getContentRoot(): HTMLElement | null;
102
- getOverlayRoot(): HTMLDivElement | null;
103
+ getOverlayRoot(): HTMLDivElement;
103
104
  syncPlaceholder(): void;
104
105
  insertText(text: string): void;
105
106
  replaceText(oldText: string, newText: string): void;
@@ -121,12 +122,10 @@ export declare class CakeEngine {
121
122
  private attachDragListeners;
122
123
  private detachDragListeners;
123
124
  private detachListeners;
124
- private installCaretRangeFromPointShim;
125
- private uninstallCaretRangeFromPointShim;
126
125
  private render;
127
126
  private isEmptyParagraphDoc;
128
127
  private updatePlaceholder;
129
- private syncPlaceholderPadding;
128
+ private syncPlaceholderPosition;
130
129
  private updateContentRootAttributes;
131
130
  private applySelection;
132
131
  private handleSelectionChange;
package/dist/index.cjs CHANGED
@@ -2870,13 +2870,11 @@ function renderDocContent(doc, extensions, root) {
2870
2870
  return "unknown";
2871
2871
  }
2872
2872
  function getElementKey(element) {
2873
- if (element.hasAttribute("data-block")) {
2874
- const blockType = element.getAttribute("data-block") ?? "unknown";
2875
- const lineKind = element instanceof HTMLElement ? element.dataset.lineKind : null;
2876
- if (lineKind && lineKind !== blockType) {
2877
- return lineKind;
2873
+ if (element.classList.contains("cake-line")) {
2874
+ if (element.hasAttribute("data-block-atom")) {
2875
+ return `block-atom:${element.getAttribute("data-block-atom")}`;
2878
2876
  }
2879
- return blockType;
2877
+ return "paragraph";
2880
2878
  }
2881
2879
  if (element.hasAttribute("data-block-wrapper")) {
2882
2880
  return `block-wrapper:${element.getAttribute("data-block-wrapper")}`;
@@ -2902,11 +2900,13 @@ function renderDocContent(doc, extensions, root) {
2902
2900
  if (element.classList.contains("cake-text")) {
2903
2901
  return "text";
2904
2902
  }
2905
- if (element.hasAttribute("data-inline")) {
2906
- return `inline-wrapper:${element.getAttribute("data-inline")}`;
2907
- }
2908
- if (element.hasAttribute("data-inline-atom")) {
2909
- return `inline-atom:${element.getAttribute("data-inline-atom")}`;
2903
+ for (const cls of Array.from(element.classList)) {
2904
+ if (cls.startsWith("cake-inline--")) {
2905
+ return `inline-wrapper:${cls.slice("cake-inline--".length)}`;
2906
+ }
2907
+ if (cls.startsWith("cake-inline-atom--")) {
2908
+ return `inline-atom:${cls.slice("cake-inline-atom--".length)}`;
2909
+ }
2910
2910
  }
2911
2911
  return "unknown";
2912
2912
  }
@@ -2943,11 +2943,13 @@ function renderDocContent(doc, extensions, root) {
2943
2943
  if (inline.type === "inline-wrapper") {
2944
2944
  const canReuse = existing && existing instanceof HTMLSpanElement && getInlineElementKey(existing) === getInlineKey(inline);
2945
2945
  if (canReuse) {
2946
+ existing.removeAttribute("data-inline");
2947
+ existing.classList.add("cake-inline", `cake-inline--${inline.kind}`);
2946
2948
  reconcileInlineChildren(existing, inline.children);
2947
2949
  return [existing];
2948
2950
  }
2949
2951
  const element = document.createElement("span");
2950
- element.setAttribute("data-inline", inline.kind);
2952
+ element.classList.add("cake-inline", `cake-inline--${inline.kind}`);
2951
2953
  for (const child of inline.children) {
2952
2954
  for (const node of reconcileInline(child, null)) {
2953
2955
  element.append(node);
@@ -2958,6 +2960,11 @@ function renderDocContent(doc, extensions, root) {
2958
2960
  if (inline.type === "inline-atom") {
2959
2961
  const canReuse = existing && existing instanceof HTMLSpanElement && getInlineElementKey(existing) === getInlineKey(inline);
2960
2962
  if (canReuse) {
2963
+ existing.removeAttribute("data-inline-atom");
2964
+ existing.classList.add(
2965
+ "cake-inline-atom",
2966
+ `cake-inline-atom--${inline.kind}`
2967
+ );
2961
2968
  const textNode = existing.firstChild;
2962
2969
  if (textNode instanceof Text) {
2963
2970
  createTextRun$1(textNode);
@@ -2965,7 +2972,10 @@ function renderDocContent(doc, extensions, root) {
2965
2972
  }
2966
2973
  }
2967
2974
  const element = document.createElement("span");
2968
- element.setAttribute("data-inline-atom", inline.kind);
2975
+ element.classList.add(
2976
+ "cake-inline-atom",
2977
+ `cake-inline-atom--${inline.kind}`
2978
+ );
2969
2979
  const node = document.createTextNode(" ");
2970
2980
  createTextRun$1(node);
2971
2981
  element.append(node);
@@ -3008,6 +3018,13 @@ function renderDocContent(doc, extensions, root) {
3008
3018
  context.incrementLineIndex();
3009
3019
  if (canReuse) {
3010
3020
  existing.setAttribute("data-line-index", String(currentLineIndex));
3021
+ existing.removeAttribute("data-block");
3022
+ delete existing.dataset.lineKind;
3023
+ delete existing.dataset.headingLevel;
3024
+ delete existing.dataset.headingPlaceholder;
3025
+ existing.removeAttribute("aria-placeholder");
3026
+ existing.className = "cake-line";
3027
+ existing.removeAttribute("style");
3011
3028
  if (block.content.length === 0) {
3012
3029
  const firstChild = existing.firstChild;
3013
3030
  if (firstChild instanceof Text && existing.querySelector("br")) {
@@ -3028,10 +3045,8 @@ function renderDocContent(doc, extensions, root) {
3028
3045
  return [existing];
3029
3046
  }
3030
3047
  const element = document.createElement("div");
3031
- element.setAttribute("data-block", "paragraph");
3032
3048
  element.setAttribute("data-line-index", String(currentLineIndex));
3033
3049
  element.classList.add("cake-line");
3034
- element.dataset.lineKind = "paragraph";
3035
3050
  if (block.content.length === 0) {
3036
3051
  const textNode = document.createTextNode("");
3037
3052
  createTextRun$1(textNode);
@@ -4639,18 +4654,16 @@ const headingExtension = defineExtension({
4639
4654
  const level = typeof ((_a = block.data) == null ? void 0 : _a.level) === "number" ? block.data.level : 1;
4640
4655
  const normalizedLevel = Math.max(1, Math.min(3, level));
4641
4656
  const lineElement = document.createElement("div");
4642
- lineElement.setAttribute("data-block", "paragraph");
4643
4657
  lineElement.setAttribute("data-line-index", String(context.getLineIndex()));
4644
4658
  lineElement.classList.add(
4645
4659
  "cake-line",
4646
4660
  "is-heading",
4647
4661
  `is-heading-${normalizedLevel}`
4648
4662
  );
4649
- lineElement.dataset.lineKind = "heading";
4650
- lineElement.dataset.headingLevel = String(normalizedLevel);
4651
4663
  context.incrementLineIndex();
4652
4664
  const paragraph = block.blocks[0];
4653
4665
  if ((paragraph == null ? void 0 : paragraph.type) === "paragraph" && paragraph.content.length > 0) {
4666
+ lineElement.removeAttribute("aria-placeholder");
4654
4667
  const mergedContent = mergeInlineForRender(paragraph.content);
4655
4668
  for (const inline of mergedContent) {
4656
4669
  for (const node of context.renderInline(inline)) {
@@ -4658,7 +4671,10 @@ const headingExtension = defineExtension({
4658
4671
  }
4659
4672
  }
4660
4673
  } else {
4661
- lineElement.dataset.headingPlaceholder = `Heading ${normalizedLevel}`;
4674
+ lineElement.setAttribute(
4675
+ "aria-placeholder",
4676
+ `Heading ${normalizedLevel}`
4677
+ );
4662
4678
  const node = document.createTextNode("");
4663
4679
  context.createTextRun(node);
4664
4680
  lineElement.append(node);
@@ -5665,7 +5681,6 @@ const listExtension = defineExtension({
5665
5681
  return null;
5666
5682
  }
5667
5683
  const element = document.createElement("div");
5668
- element.setAttribute("data-block", "paragraph");
5669
5684
  element.setAttribute("data-line-index", String(context.getLineIndex()));
5670
5685
  element.classList.add("cake-line", "is-list");
5671
5686
  context.incrementLineIndex();
@@ -7601,8 +7616,6 @@ const HISTORY_GROUPING_INTERVAL_MS = 500;
7601
7616
  const MAX_UNDO_STACK_SIZE = 100;
7602
7617
  class CakeEngine {
7603
7618
  constructor(options) {
7604
- this.originalCaretRangeFromPoint = null;
7605
- this.patchedCaretRangeFromPoint = null;
7606
7619
  this.contentRoot = null;
7607
7620
  this.domMap = null;
7608
7621
  this.isApplyingSelection = false;
@@ -7627,6 +7640,7 @@ class CakeEngine {
7627
7640
  this.lastSelectionRects = null;
7628
7641
  this.extensionsRoot = null;
7629
7642
  this.placeholderRoot = null;
7643
+ this.resizeObserver = null;
7630
7644
  this.lastFocusRect = null;
7631
7645
  this.verticalNavGoalX = null;
7632
7646
  this.lastRenderPerf = null;
@@ -7664,6 +7678,7 @@ class CakeEngine {
7664
7678
  this.hasMovedSincePointerDown = false;
7665
7679
  this.lastTouchTime = 0;
7666
7680
  this.container = options.container;
7681
+ this.contentRoot = options.contentRoot ?? null;
7667
7682
  this.extensions = options.extensions ?? bundledExtensions;
7668
7683
  this.runtime = createRuntime(this.extensions);
7669
7684
  this.state = this.runtime.createState(
@@ -7676,7 +7691,6 @@ class CakeEngine {
7676
7691
  this.spellCheckEnabled = options.spellCheckEnabled ?? true;
7677
7692
  this.render();
7678
7693
  this.attachListeners();
7679
- this.installCaretRangeFromPointShim();
7680
7694
  }
7681
7695
  get state() {
7682
7696
  return this._state;
@@ -7696,9 +7710,13 @@ class CakeEngine {
7696
7710
  }
7697
7711
  return target instanceof Node && (target === this.contentRoot || this.contentRoot.contains(target));
7698
7712
  }
7713
+ // Detect if this is a touch-primary device (mobile/tablet)
7714
+ // We check for touch support AND coarse pointer to exclude laptops with touchscreens
7715
+ isTouchDevice() {
7716
+ return "ontouchstart" in window && window.matchMedia("(pointer: coarse)").matches;
7717
+ }
7699
7718
  destroy() {
7700
7719
  this.detachListeners();
7701
- this.uninstallCaretRangeFromPointShim();
7702
7720
  this.clearCaretBlinkTimer();
7703
7721
  if (this.overlayUpdateId !== null) {
7704
7722
  window.cancelAnimationFrame(this.overlayUpdateId);
@@ -7736,7 +7754,7 @@ class CakeEngine {
7736
7754
  return this.contentRoot;
7737
7755
  }
7738
7756
  getOverlayRoot() {
7739
- return this.extensionsRoot;
7757
+ return this.ensureExtensionsRoot();
7740
7758
  }
7741
7759
  // Placeholder text is provided by the caller via the container's
7742
7760
  // `data-placeholder` attribute (set by the React wrapper).
@@ -7890,6 +7908,11 @@ class CakeEngine {
7890
7908
  );
7891
7909
  this.container.addEventListener("scroll", this.handleScrollBound);
7892
7910
  window.addEventListener("resize", this.handleResizeBound);
7911
+ this.resizeObserver = new ResizeObserver(() => {
7912
+ this.syncPlaceholderPosition();
7913
+ this.scheduleOverlayUpdate();
7914
+ });
7915
+ this.resizeObserver.observe(this.container);
7893
7916
  this.container.addEventListener("click", this.handleClickBound);
7894
7917
  this.container.addEventListener("keydown", this.handleKeyDownBound);
7895
7918
  this.container.addEventListener("paste", this.handlePasteBound);
@@ -7921,6 +7944,7 @@ class CakeEngine {
7921
7944
  this.contentRoot.removeEventListener("dragend", this.handleDragEndBound);
7922
7945
  }
7923
7946
  detachListeners() {
7947
+ var _a;
7924
7948
  this.container.removeEventListener(
7925
7949
  "beforeinput",
7926
7950
  this.handleBeforeInputBound
@@ -7940,6 +7964,8 @@ class CakeEngine {
7940
7964
  );
7941
7965
  this.container.removeEventListener("scroll", this.handleScrollBound);
7942
7966
  window.removeEventListener("resize", this.handleResizeBound);
7967
+ (_a = this.resizeObserver) == null ? void 0 : _a.disconnect();
7968
+ this.resizeObserver = null;
7943
7969
  this.container.removeEventListener("click", this.handleClickBound);
7944
7970
  this.container.removeEventListener("keydown", this.handleKeyDownBound);
7945
7971
  this.container.removeEventListener("paste", this.handlePasteBound);
@@ -7956,80 +7982,6 @@ class CakeEngine {
7956
7982
  this.container.removeEventListener("pointerup", this.handlePointerUpBound);
7957
7983
  this.detachDragListeners();
7958
7984
  }
7959
- installCaretRangeFromPointShim() {
7960
- const doc = document;
7961
- if (typeof doc.caretRangeFromPoint !== "function") {
7962
- return;
7963
- }
7964
- if (this.patchedCaretRangeFromPoint) {
7965
- return;
7966
- }
7967
- this.originalCaretRangeFromPoint = doc.caretRangeFromPoint.bind(document);
7968
- const patched = (x, y) => {
7969
- var _a;
7970
- const original = this.originalCaretRangeFromPoint;
7971
- const range = original ? original(x, y) : null;
7972
- const startNode = (range == null ? void 0 : range.startContainer) ?? null;
7973
- const startLine = startNode instanceof HTMLElement ? startNode.closest("[data-line-index]") : (_a = startNode == null ? void 0 : startNode.parentElement) == null ? void 0 : _a.closest("[data-line-index]");
7974
- if (startLine) {
7975
- return range;
7976
- }
7977
- const rect = this.container.getBoundingClientRect();
7978
- const isInside = x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
7979
- if (!isInside || !this.domMap) {
7980
- return range;
7981
- }
7982
- const docAny = document;
7983
- let node = null;
7984
- let offset = 0;
7985
- if (typeof docAny.caretPositionFromPoint === "function") {
7986
- const pos = docAny.caretPositionFromPoint(x, y);
7987
- node = (pos == null ? void 0 : pos.offsetNode) ?? null;
7988
- offset = (pos == null ? void 0 : pos.offset) ?? 0;
7989
- } else if (original) {
7990
- const fallback = original(x, y);
7991
- node = (fallback == null ? void 0 : fallback.startContainer) ?? null;
7992
- offset = (fallback == null ? void 0 : fallback.startOffset) ?? 0;
7993
- }
7994
- let resolved = null;
7995
- if (node instanceof Text) {
7996
- resolved = { node, offset };
7997
- } else if (node instanceof Element) {
7998
- resolved = resolveTextPoint(node, offset);
7999
- }
8000
- if (!resolved) {
8001
- return range;
8002
- }
8003
- const cursor = this.domMap.cursorAtDom(resolved.node, resolved.offset);
8004
- if (!cursor) {
8005
- return range;
8006
- }
8007
- const domPoint = this.domMap.domAtCursor(
8008
- cursor.cursorOffset,
8009
- cursor.affinity
8010
- );
8011
- if (!domPoint) {
8012
- return range;
8013
- }
8014
- const fixed = document.createRange();
8015
- fixed.setStart(domPoint.node, domPoint.offset);
8016
- fixed.setEnd(domPoint.node, domPoint.offset);
8017
- return fixed;
8018
- };
8019
- this.patchedCaretRangeFromPoint = patched;
8020
- doc.caretRangeFromPoint = patched;
8021
- }
8022
- uninstallCaretRangeFromPointShim() {
8023
- if (!this.originalCaretRangeFromPoint || !this.patchedCaretRangeFromPoint) {
8024
- return;
8025
- }
8026
- const doc = document;
8027
- if (doc.caretRangeFromPoint === this.patchedCaretRangeFromPoint) {
8028
- doc.caretRangeFromPoint = this.originalCaretRangeFromPoint;
8029
- }
8030
- this.originalCaretRangeFromPoint = null;
8031
- this.patchedCaretRangeFromPoint = null;
8032
- }
8033
7985
  render() {
8034
7986
  const perfEnabled = this.container.dataset.cakePerf === "1";
8035
7987
  let perfStart = 0;
@@ -8048,10 +8000,32 @@ class CakeEngine {
8048
8000
  }
8049
8001
  this.contentRoot = document.createElement("div");
8050
8002
  this.contentRoot.className = "cake-content";
8051
- this.updateContentRootAttributes();
8003
+ }
8004
+ if (this.isTouchDevice()) {
8005
+ this.contentRoot.classList.add("cake-touch-mode");
8006
+ }
8007
+ this.updateContentRootAttributes();
8008
+ if (!this.overlayRoot) {
8052
8009
  const overlay = this.ensureOverlayRoot();
8053
- const extensionsRoot = this.ensureExtensionsRoot();
8054
- this.container.replaceChildren(this.contentRoot, overlay, extensionsRoot);
8010
+ const extensionsRoot = this.extensionsRoot;
8011
+ const existingContainerChildren = Array.from(this.container.childNodes);
8012
+ const isCakeManagedContainerChild = (node) => node instanceof Element && (node.classList.contains("cake-content") || node.classList.contains("cake-selection-overlay") || node.classList.contains("cake-extension-overlay") || node.classList.contains("cake-placeholder"));
8013
+ const preservedContainerChildren = existingContainerChildren.filter(
8014
+ (node) => !isCakeManagedContainerChild(node)
8015
+ );
8016
+ if (this.contentRoot.parentElement === this.container) {
8017
+ this.container.append(overlay);
8018
+ if (extensionsRoot && !extensionsRoot.isConnected) {
8019
+ this.container.append(extensionsRoot);
8020
+ }
8021
+ } else {
8022
+ this.container.replaceChildren(
8023
+ this.contentRoot,
8024
+ overlay,
8025
+ ...extensionsRoot ? [extensionsRoot] : [],
8026
+ ...preservedContainerChildren
8027
+ );
8028
+ }
8055
8029
  this.attachDragListeners();
8056
8030
  }
8057
8031
  if (perfEnabled) {
@@ -8063,9 +8037,12 @@ class CakeEngine {
8063
8037
  this.contentRoot
8064
8038
  );
8065
8039
  const existingChildren = Array.from(this.contentRoot.childNodes);
8066
- const needsUpdate = content.length !== existingChildren.length || content.some((node, i) => node !== existingChildren[i]);
8040
+ const isManagedChild = (node) => node instanceof Element && node.hasAttribute("data-line-index");
8041
+ const existingManagedChildren = existingChildren.filter(isManagedChild);
8042
+ const preservedChildren = existingChildren.filter((node) => !isManagedChild(node));
8043
+ const needsUpdate = content.length !== existingManagedChildren.length || content.some((node, i) => node !== existingManagedChildren[i]);
8067
8044
  if (needsUpdate) {
8068
- this.contentRoot.replaceChildren(...content);
8045
+ this.contentRoot.replaceChildren(...content, ...preservedChildren);
8069
8046
  }
8070
8047
  this.domMap = map;
8071
8048
  if (perfEnabled) {
@@ -8118,6 +8095,8 @@ class CakeEngine {
8118
8095
  if (!this.placeholderRoot) {
8119
8096
  this.placeholderRoot = document.createElement("div");
8120
8097
  this.placeholderRoot.className = "cake-placeholder";
8098
+ this.placeholderRoot.style.position = "absolute";
8099
+ this.placeholderRoot.style.pointerEvents = "none";
8121
8100
  }
8122
8101
  if (!shouldShow) {
8123
8102
  if (this.placeholderRoot.isConnected) {
@@ -8127,20 +8106,21 @@ class CakeEngine {
8127
8106
  return;
8128
8107
  }
8129
8108
  this.placeholderRoot.textContent = placeholderText ?? "";
8130
- this.syncPlaceholderPadding();
8131
8109
  if (!this.placeholderRoot.isConnected) {
8132
8110
  this.container.prepend(this.placeholderRoot);
8133
8111
  }
8112
+ this.syncPlaceholderPosition();
8134
8113
  }
8135
- syncPlaceholderPadding() {
8136
- if (!this.placeholderRoot) {
8114
+ syncPlaceholderPosition() {
8115
+ if (!this.placeholderRoot || !this.contentRoot) {
8137
8116
  return;
8138
8117
  }
8139
- const style = window.getComputedStyle(this.container);
8140
- this.placeholderRoot.style.paddingTop = style.paddingTop;
8141
- this.placeholderRoot.style.paddingRight = style.paddingRight;
8142
- this.placeholderRoot.style.paddingBottom = style.paddingBottom;
8143
- this.placeholderRoot.style.paddingLeft = style.paddingLeft;
8118
+ const containerRect = this.container.getBoundingClientRect();
8119
+ const contentRect = this.contentRoot.getBoundingClientRect();
8120
+ this.placeholderRoot.style.top = `${contentRect.top - containerRect.top}px`;
8121
+ this.placeholderRoot.style.left = `${contentRect.left - containerRect.left}px`;
8122
+ this.placeholderRoot.style.width = `${contentRect.width}px`;
8123
+ this.placeholderRoot.style.height = `${contentRect.height}px`;
8144
8124
  }
8145
8125
  updateContentRootAttributes() {
8146
8126
  if (!this.contentRoot) {
@@ -8750,81 +8730,21 @@ class CakeEngine {
8750
8730
  }
8751
8731
  handleBeforeInput(event) {
8752
8732
  if (this.readOnly || this.isComposing || event.isComposing) {
8753
- if (event.inputType === "insertReplacementText" || event.inputType === "insertText") {
8754
- console.log("[SPELLCHECK] beforeinput ignored (readonly/composing)", {
8755
- inputType: event.inputType,
8756
- data: event.data,
8757
- cancelable: event.cancelable,
8758
- isComposing: this.isComposing,
8759
- eventIsComposing: event.isComposing,
8760
- readOnly: this.readOnly
8761
- });
8762
- }
8763
8733
  return;
8764
8734
  }
8765
8735
  if (!this.isEventTargetInContentRoot(event.target)) {
8766
- if (event.inputType === "insertReplacementText" || event.inputType === "insertText") {
8767
- console.log(
8768
- "[SPELLCHECK] beforeinput ignored (target outside editor)",
8769
- {
8770
- inputType: event.inputType,
8771
- data: event.data,
8772
- cancelable: event.cancelable,
8773
- target: event.target
8774
- }
8775
- );
8776
- }
8777
8736
  return;
8778
8737
  }
8779
8738
  if (this.keydownHandledBeforeInput) {
8780
8739
  this.keydownHandledBeforeInput = false;
8781
- if (event.inputType === "insertReplacementText" || event.inputType === "insertText") {
8782
- console.log(
8783
- "[SPELLCHECK] beforeinput skipped (keydownHandledBeforeInput)",
8784
- {
8785
- inputType: event.inputType,
8786
- data: event.data,
8787
- cancelable: event.cancelable
8788
- }
8789
- );
8790
- }
8791
8740
  event.preventDefault();
8792
8741
  return;
8793
8742
  }
8794
8743
  const intent = this.resolveBeforeInputIntent(event);
8795
8744
  if (!intent) {
8796
- if (event.inputType === "insertReplacementText" || event.inputType === "insertText") {
8797
- console.log("[SPELLCHECK] beforeinput: no intent", {
8798
- inputType: event.inputType,
8799
- data: event.data,
8800
- cancelable: event.cancelable
8801
- });
8802
- }
8803
8745
  return;
8804
8746
  }
8805
- if (event.inputType === "insertReplacementText" || event.inputType === "insertText" && intent.type === "replace-text") {
8806
- const selection = this.state.selection;
8807
- const focus = selection.start === selection.end ? selection.start : Math.max(selection.start, selection.end);
8808
- const preview = this.state.source.slice(
8809
- Math.max(0, focus - 24),
8810
- Math.min(this.state.source.length, focus + 24)
8811
- );
8812
- console.log("[SPELLCHECK] beforeinput: resolved intent", {
8813
- inputType: event.inputType,
8814
- data: event.data,
8815
- cancelable: event.cancelable,
8816
- intent,
8817
- currentSelection: this.state.selection,
8818
- sourcePreviewAroundFocus: preview
8819
- });
8820
- }
8821
8747
  event.preventDefault();
8822
- if (event.inputType === "insertReplacementText" || event.inputType === "insertText" && intent.type === "replace-text") {
8823
- console.log("[SPELLCHECK] beforeinput: after preventDefault", {
8824
- inputType: event.inputType,
8825
- defaultPrevented: event.defaultPrevented
8826
- });
8827
- }
8828
8748
  this.markBeforeInputHandled();
8829
8749
  this.suppressSelectionChangeForTick();
8830
8750
  this.applyInputIntent(intent);
@@ -8871,22 +8791,9 @@ class CakeEngine {
8871
8791
  return;
8872
8792
  }
8873
8793
  if (!this.isEventTargetInContentRoot(event.target)) {
8874
- if (event.inputType === "insertReplacementText") {
8875
- console.log("[SPELLCHECK] input ignored (target outside editor)", {
8876
- inputType: event.inputType,
8877
- data: event.data,
8878
- target: event.target
8879
- });
8880
- }
8881
8794
  return;
8882
8795
  }
8883
8796
  if (this.beforeInputHandled) {
8884
- if (event.inputType === "insertReplacementText") {
8885
- console.log("[SPELLCHECK] input ignored (handled via beforeinput)", {
8886
- inputType: event.inputType,
8887
- data: event.data
8888
- });
8889
- }
8890
8797
  return;
8891
8798
  }
8892
8799
  if (this.compositionCommit && event.inputType === "insertText") {
@@ -8998,57 +8905,20 @@ class CakeEngine {
8998
8905
  return null;
8999
8906
  }
9000
8907
  selectionFromTargetRangesWithStatus(event) {
9001
- const debug = event.inputType === "insertReplacementText";
9002
8908
  if (!event.getTargetRanges) {
9003
- if (debug) {
9004
- console.log("[SPELLCHECK][targetRanges] missing getTargetRanges()", {
9005
- inputType: event.inputType,
9006
- data: event.data,
9007
- cancelable: event.cancelable
9008
- });
9009
- }
9010
8909
  return { status: "none" };
9011
8910
  }
9012
8911
  const ranges = event.getTargetRanges();
9013
8912
  if (!ranges || ranges.length === 0) {
9014
- if (debug) {
9015
- console.log("[SPELLCHECK][targetRanges] no ranges", {
9016
- inputType: event.inputType,
9017
- data: event.data,
9018
- cancelable: event.cancelable
9019
- });
9020
- }
9021
8913
  return { status: "none" };
9022
8914
  }
9023
8915
  const range = ranges[0];
9024
- if (debug || event.inputType === "insertText") {
9025
- console.log("[SPELLCHECK][targetRanges] raw range", {
9026
- inputType: event.inputType,
9027
- data: event.data,
9028
- cancelable: event.cancelable,
9029
- startContainer: range.startContainer instanceof Element ? range.startContainer.tagName : range.startContainer.nodeName,
9030
- startOffset: range.startOffset,
9031
- endContainer: range.endContainer instanceof Element ? range.endContainer.tagName : range.endContainer.nodeName,
9032
- endOffset: range.endOffset,
9033
- startContained: this.container.contains(range.startContainer),
9034
- endContained: this.container.contains(range.endContainer)
9035
- });
9036
- }
9037
8916
  if (!this.container.contains(range.startContainer) || !this.container.contains(range.endContainer)) {
9038
8917
  return { status: "invalid" };
9039
8918
  }
9040
8919
  const start = this.cursorFromDom(range.startContainer, range.startOffset);
9041
8920
  const end = this.cursorFromDom(range.endContainer, range.endOffset);
9042
8921
  if (!start || !end) {
9043
- if (debug || event.inputType === "insertText") {
9044
- console.log("[SPELLCHECK][targetRanges] cursorFromDom failed", {
9045
- inputType: event.inputType,
9046
- data: event.data,
9047
- cancelable: event.cancelable,
9048
- start,
9049
- end
9050
- });
9051
- }
9052
8922
  return { status: "invalid" };
9053
8923
  }
9054
8924
  const affinity = start.cursorOffset === end.cursorOffset ? end.affinity : "forward";
@@ -9799,9 +9669,7 @@ class CakeEngine {
9799
9669
  return this.state.source;
9800
9670
  }
9801
9671
  const blocks = Array.from(
9802
- this.contentRoot.querySelectorAll(
9803
- '[data-block="paragraph"]'
9804
- )
9672
+ this.contentRoot.querySelectorAll(".cake-line")
9805
9673
  );
9806
9674
  if (blocks.length === 0) {
9807
9675
  return this.contentRoot.textContent ?? "";
@@ -10054,6 +9922,9 @@ class CakeEngine {
10054
9922
  root.style.zIndex = "50";
10055
9923
  root.style.overflow = "hidden";
10056
9924
  this.extensionsRoot = root;
9925
+ if (this.overlayRoot && !root.isConnected) {
9926
+ this.container.append(root);
9927
+ }
10057
9928
  return root;
10058
9929
  }
10059
9930
  updateExtensionsOverlayPosition() {
@@ -10075,8 +9946,13 @@ class CakeEngine {
10075
9946
  if (!this.overlayRoot || !this.contentRoot) {
10076
9947
  return;
10077
9948
  }
9949
+ if (!this.hasFocus()) {
9950
+ this.updateCaret(null);
9951
+ this.syncSelectionRects([]);
9952
+ return;
9953
+ }
10078
9954
  const isRecentTouch = Date.now() - this.lastTouchTime < 2e3;
10079
- if (isRecentTouch) {
9955
+ if (this.isTouchDevice() || isRecentTouch) {
10080
9956
  this.contentRoot.classList.add("cake-touch-mode");
10081
9957
  this.updateCaret(null);
10082
9958
  this.syncSelectionRects([]);
@@ -11614,13 +11490,15 @@ const CakeEditor = require$$0.forwardRef(
11614
11490
  const onSelectionChangeRef = require$$0.useRef(props.onSelectionChange);
11615
11491
  const lastEmittedValueRef = require$$0.useRef(null);
11616
11492
  const lastEmittedSelectionRef = require$$0.useRef(null);
11617
- const [overlayRoot, setOverlayRoot] = require$$0.useState(null);
11618
11493
  const [contentRoot, setContentRoot] = require$$0.useState(null);
11619
11494
  const baseExtensions = props.disableImageExtension ? bundledExtensionsWithoutImage : bundledExtensions;
11620
11495
  const allExtensionsRef = require$$0.useRef([
11621
11496
  ...baseExtensions,
11622
11497
  ...props.extensions ?? []
11623
11498
  ]);
11499
+ const hasOverlayExtensions = allExtensionsRef.current.some(
11500
+ (ext) => ext.renderOverlay
11501
+ );
11624
11502
  require$$0.useEffect(() => {
11625
11503
  onChangeRef.current = props.onChange;
11626
11504
  onSelectionChangeRef.current = props.onSelectionChange;
@@ -11665,12 +11543,10 @@ const CakeEditor = require$$0.forwardRef(
11665
11543
  }
11666
11544
  });
11667
11545
  engineRef.current = engine;
11668
- setOverlayRoot(engine.getOverlayRoot());
11669
11546
  setContentRoot(engine.getContentRoot());
11670
11547
  return () => {
11671
11548
  engine.destroy();
11672
11549
  engineRef.current = null;
11673
- setOverlayRoot(null);
11674
11550
  setContentRoot(null);
11675
11551
  };
11676
11552
  }, []);
@@ -11798,7 +11674,6 @@ const CakeEditor = require$$0.forwardRef(
11798
11674
  const overlayContext = containerRef.current && contentRoot ? {
11799
11675
  container: containerRef.current,
11800
11676
  contentRoot,
11801
- overlayRoot: overlayRoot ?? void 0,
11802
11677
  toOverlayRect: (rect) => {
11803
11678
  var _a;
11804
11679
  const containerRect = (_a = containerRef.current) == null ? void 0 : _a.getBoundingClientRect();
@@ -11839,9 +11714,6 @@ const CakeEditor = require$$0.forwardRef(
11839
11714
  return ((_a = engineRef.current) == null ? void 0 : _a.executeCommand(command)) ?? false;
11840
11715
  }
11841
11716
  } : null;
11842
- const hasOverlayExtensions = allExtensionsRef.current.some(
11843
- (ext) => ext.renderOverlay
11844
- );
11845
11717
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { position: "relative", height: "100%" }, children: [
11846
11718
  /* @__PURE__ */ jsxRuntimeExports.jsx(
11847
11719
  "div",
package/dist/index.js CHANGED
@@ -2868,13 +2868,11 @@ function renderDocContent(doc, extensions, root) {
2868
2868
  return "unknown";
2869
2869
  }
2870
2870
  function getElementKey(element) {
2871
- if (element.hasAttribute("data-block")) {
2872
- const blockType = element.getAttribute("data-block") ?? "unknown";
2873
- const lineKind = element instanceof HTMLElement ? element.dataset.lineKind : null;
2874
- if (lineKind && lineKind !== blockType) {
2875
- return lineKind;
2871
+ if (element.classList.contains("cake-line")) {
2872
+ if (element.hasAttribute("data-block-atom")) {
2873
+ return `block-atom:${element.getAttribute("data-block-atom")}`;
2876
2874
  }
2877
- return blockType;
2875
+ return "paragraph";
2878
2876
  }
2879
2877
  if (element.hasAttribute("data-block-wrapper")) {
2880
2878
  return `block-wrapper:${element.getAttribute("data-block-wrapper")}`;
@@ -2900,11 +2898,13 @@ function renderDocContent(doc, extensions, root) {
2900
2898
  if (element.classList.contains("cake-text")) {
2901
2899
  return "text";
2902
2900
  }
2903
- if (element.hasAttribute("data-inline")) {
2904
- return `inline-wrapper:${element.getAttribute("data-inline")}`;
2905
- }
2906
- if (element.hasAttribute("data-inline-atom")) {
2907
- return `inline-atom:${element.getAttribute("data-inline-atom")}`;
2901
+ for (const cls of Array.from(element.classList)) {
2902
+ if (cls.startsWith("cake-inline--")) {
2903
+ return `inline-wrapper:${cls.slice("cake-inline--".length)}`;
2904
+ }
2905
+ if (cls.startsWith("cake-inline-atom--")) {
2906
+ return `inline-atom:${cls.slice("cake-inline-atom--".length)}`;
2907
+ }
2908
2908
  }
2909
2909
  return "unknown";
2910
2910
  }
@@ -2941,11 +2941,13 @@ function renderDocContent(doc, extensions, root) {
2941
2941
  if (inline.type === "inline-wrapper") {
2942
2942
  const canReuse = existing && existing instanceof HTMLSpanElement && getInlineElementKey(existing) === getInlineKey(inline);
2943
2943
  if (canReuse) {
2944
+ existing.removeAttribute("data-inline");
2945
+ existing.classList.add("cake-inline", `cake-inline--${inline.kind}`);
2944
2946
  reconcileInlineChildren(existing, inline.children);
2945
2947
  return [existing];
2946
2948
  }
2947
2949
  const element = document.createElement("span");
2948
- element.setAttribute("data-inline", inline.kind);
2950
+ element.classList.add("cake-inline", `cake-inline--${inline.kind}`);
2949
2951
  for (const child of inline.children) {
2950
2952
  for (const node of reconcileInline(child, null)) {
2951
2953
  element.append(node);
@@ -2956,6 +2958,11 @@ function renderDocContent(doc, extensions, root) {
2956
2958
  if (inline.type === "inline-atom") {
2957
2959
  const canReuse = existing && existing instanceof HTMLSpanElement && getInlineElementKey(existing) === getInlineKey(inline);
2958
2960
  if (canReuse) {
2961
+ existing.removeAttribute("data-inline-atom");
2962
+ existing.classList.add(
2963
+ "cake-inline-atom",
2964
+ `cake-inline-atom--${inline.kind}`
2965
+ );
2959
2966
  const textNode = existing.firstChild;
2960
2967
  if (textNode instanceof Text) {
2961
2968
  createTextRun$1(textNode);
@@ -2963,7 +2970,10 @@ function renderDocContent(doc, extensions, root) {
2963
2970
  }
2964
2971
  }
2965
2972
  const element = document.createElement("span");
2966
- element.setAttribute("data-inline-atom", inline.kind);
2973
+ element.classList.add(
2974
+ "cake-inline-atom",
2975
+ `cake-inline-atom--${inline.kind}`
2976
+ );
2967
2977
  const node = document.createTextNode(" ");
2968
2978
  createTextRun$1(node);
2969
2979
  element.append(node);
@@ -3006,6 +3016,13 @@ function renderDocContent(doc, extensions, root) {
3006
3016
  context.incrementLineIndex();
3007
3017
  if (canReuse) {
3008
3018
  existing.setAttribute("data-line-index", String(currentLineIndex));
3019
+ existing.removeAttribute("data-block");
3020
+ delete existing.dataset.lineKind;
3021
+ delete existing.dataset.headingLevel;
3022
+ delete existing.dataset.headingPlaceholder;
3023
+ existing.removeAttribute("aria-placeholder");
3024
+ existing.className = "cake-line";
3025
+ existing.removeAttribute("style");
3009
3026
  if (block.content.length === 0) {
3010
3027
  const firstChild = existing.firstChild;
3011
3028
  if (firstChild instanceof Text && existing.querySelector("br")) {
@@ -3026,10 +3043,8 @@ function renderDocContent(doc, extensions, root) {
3026
3043
  return [existing];
3027
3044
  }
3028
3045
  const element = document.createElement("div");
3029
- element.setAttribute("data-block", "paragraph");
3030
3046
  element.setAttribute("data-line-index", String(currentLineIndex));
3031
3047
  element.classList.add("cake-line");
3032
- element.dataset.lineKind = "paragraph";
3033
3048
  if (block.content.length === 0) {
3034
3049
  const textNode = document.createTextNode("");
3035
3050
  createTextRun$1(textNode);
@@ -4637,18 +4652,16 @@ const headingExtension = defineExtension({
4637
4652
  const level = typeof ((_a = block.data) == null ? void 0 : _a.level) === "number" ? block.data.level : 1;
4638
4653
  const normalizedLevel = Math.max(1, Math.min(3, level));
4639
4654
  const lineElement = document.createElement("div");
4640
- lineElement.setAttribute("data-block", "paragraph");
4641
4655
  lineElement.setAttribute("data-line-index", String(context.getLineIndex()));
4642
4656
  lineElement.classList.add(
4643
4657
  "cake-line",
4644
4658
  "is-heading",
4645
4659
  `is-heading-${normalizedLevel}`
4646
4660
  );
4647
- lineElement.dataset.lineKind = "heading";
4648
- lineElement.dataset.headingLevel = String(normalizedLevel);
4649
4661
  context.incrementLineIndex();
4650
4662
  const paragraph = block.blocks[0];
4651
4663
  if ((paragraph == null ? void 0 : paragraph.type) === "paragraph" && paragraph.content.length > 0) {
4664
+ lineElement.removeAttribute("aria-placeholder");
4652
4665
  const mergedContent = mergeInlineForRender(paragraph.content);
4653
4666
  for (const inline of mergedContent) {
4654
4667
  for (const node of context.renderInline(inline)) {
@@ -4656,7 +4669,10 @@ const headingExtension = defineExtension({
4656
4669
  }
4657
4670
  }
4658
4671
  } else {
4659
- lineElement.dataset.headingPlaceholder = `Heading ${normalizedLevel}`;
4672
+ lineElement.setAttribute(
4673
+ "aria-placeholder",
4674
+ `Heading ${normalizedLevel}`
4675
+ );
4660
4676
  const node = document.createTextNode("");
4661
4677
  context.createTextRun(node);
4662
4678
  lineElement.append(node);
@@ -5663,7 +5679,6 @@ const listExtension = defineExtension({
5663
5679
  return null;
5664
5680
  }
5665
5681
  const element = document.createElement("div");
5666
- element.setAttribute("data-block", "paragraph");
5667
5682
  element.setAttribute("data-line-index", String(context.getLineIndex()));
5668
5683
  element.classList.add("cake-line", "is-list");
5669
5684
  context.incrementLineIndex();
@@ -7599,8 +7614,6 @@ const HISTORY_GROUPING_INTERVAL_MS = 500;
7599
7614
  const MAX_UNDO_STACK_SIZE = 100;
7600
7615
  class CakeEngine {
7601
7616
  constructor(options) {
7602
- this.originalCaretRangeFromPoint = null;
7603
- this.patchedCaretRangeFromPoint = null;
7604
7617
  this.contentRoot = null;
7605
7618
  this.domMap = null;
7606
7619
  this.isApplyingSelection = false;
@@ -7625,6 +7638,7 @@ class CakeEngine {
7625
7638
  this.lastSelectionRects = null;
7626
7639
  this.extensionsRoot = null;
7627
7640
  this.placeholderRoot = null;
7641
+ this.resizeObserver = null;
7628
7642
  this.lastFocusRect = null;
7629
7643
  this.verticalNavGoalX = null;
7630
7644
  this.lastRenderPerf = null;
@@ -7662,6 +7676,7 @@ class CakeEngine {
7662
7676
  this.hasMovedSincePointerDown = false;
7663
7677
  this.lastTouchTime = 0;
7664
7678
  this.container = options.container;
7679
+ this.contentRoot = options.contentRoot ?? null;
7665
7680
  this.extensions = options.extensions ?? bundledExtensions;
7666
7681
  this.runtime = createRuntime(this.extensions);
7667
7682
  this.state = this.runtime.createState(
@@ -7674,7 +7689,6 @@ class CakeEngine {
7674
7689
  this.spellCheckEnabled = options.spellCheckEnabled ?? true;
7675
7690
  this.render();
7676
7691
  this.attachListeners();
7677
- this.installCaretRangeFromPointShim();
7678
7692
  }
7679
7693
  get state() {
7680
7694
  return this._state;
@@ -7694,9 +7708,13 @@ class CakeEngine {
7694
7708
  }
7695
7709
  return target instanceof Node && (target === this.contentRoot || this.contentRoot.contains(target));
7696
7710
  }
7711
+ // Detect if this is a touch-primary device (mobile/tablet)
7712
+ // We check for touch support AND coarse pointer to exclude laptops with touchscreens
7713
+ isTouchDevice() {
7714
+ return "ontouchstart" in window && window.matchMedia("(pointer: coarse)").matches;
7715
+ }
7697
7716
  destroy() {
7698
7717
  this.detachListeners();
7699
- this.uninstallCaretRangeFromPointShim();
7700
7718
  this.clearCaretBlinkTimer();
7701
7719
  if (this.overlayUpdateId !== null) {
7702
7720
  window.cancelAnimationFrame(this.overlayUpdateId);
@@ -7734,7 +7752,7 @@ class CakeEngine {
7734
7752
  return this.contentRoot;
7735
7753
  }
7736
7754
  getOverlayRoot() {
7737
- return this.extensionsRoot;
7755
+ return this.ensureExtensionsRoot();
7738
7756
  }
7739
7757
  // Placeholder text is provided by the caller via the container's
7740
7758
  // `data-placeholder` attribute (set by the React wrapper).
@@ -7888,6 +7906,11 @@ class CakeEngine {
7888
7906
  );
7889
7907
  this.container.addEventListener("scroll", this.handleScrollBound);
7890
7908
  window.addEventListener("resize", this.handleResizeBound);
7909
+ this.resizeObserver = new ResizeObserver(() => {
7910
+ this.syncPlaceholderPosition();
7911
+ this.scheduleOverlayUpdate();
7912
+ });
7913
+ this.resizeObserver.observe(this.container);
7891
7914
  this.container.addEventListener("click", this.handleClickBound);
7892
7915
  this.container.addEventListener("keydown", this.handleKeyDownBound);
7893
7916
  this.container.addEventListener("paste", this.handlePasteBound);
@@ -7919,6 +7942,7 @@ class CakeEngine {
7919
7942
  this.contentRoot.removeEventListener("dragend", this.handleDragEndBound);
7920
7943
  }
7921
7944
  detachListeners() {
7945
+ var _a;
7922
7946
  this.container.removeEventListener(
7923
7947
  "beforeinput",
7924
7948
  this.handleBeforeInputBound
@@ -7938,6 +7962,8 @@ class CakeEngine {
7938
7962
  );
7939
7963
  this.container.removeEventListener("scroll", this.handleScrollBound);
7940
7964
  window.removeEventListener("resize", this.handleResizeBound);
7965
+ (_a = this.resizeObserver) == null ? void 0 : _a.disconnect();
7966
+ this.resizeObserver = null;
7941
7967
  this.container.removeEventListener("click", this.handleClickBound);
7942
7968
  this.container.removeEventListener("keydown", this.handleKeyDownBound);
7943
7969
  this.container.removeEventListener("paste", this.handlePasteBound);
@@ -7954,80 +7980,6 @@ class CakeEngine {
7954
7980
  this.container.removeEventListener("pointerup", this.handlePointerUpBound);
7955
7981
  this.detachDragListeners();
7956
7982
  }
7957
- installCaretRangeFromPointShim() {
7958
- const doc = document;
7959
- if (typeof doc.caretRangeFromPoint !== "function") {
7960
- return;
7961
- }
7962
- if (this.patchedCaretRangeFromPoint) {
7963
- return;
7964
- }
7965
- this.originalCaretRangeFromPoint = doc.caretRangeFromPoint.bind(document);
7966
- const patched = (x, y) => {
7967
- var _a;
7968
- const original = this.originalCaretRangeFromPoint;
7969
- const range = original ? original(x, y) : null;
7970
- const startNode = (range == null ? void 0 : range.startContainer) ?? null;
7971
- const startLine = startNode instanceof HTMLElement ? startNode.closest("[data-line-index]") : (_a = startNode == null ? void 0 : startNode.parentElement) == null ? void 0 : _a.closest("[data-line-index]");
7972
- if (startLine) {
7973
- return range;
7974
- }
7975
- const rect = this.container.getBoundingClientRect();
7976
- const isInside = x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
7977
- if (!isInside || !this.domMap) {
7978
- return range;
7979
- }
7980
- const docAny = document;
7981
- let node = null;
7982
- let offset = 0;
7983
- if (typeof docAny.caretPositionFromPoint === "function") {
7984
- const pos = docAny.caretPositionFromPoint(x, y);
7985
- node = (pos == null ? void 0 : pos.offsetNode) ?? null;
7986
- offset = (pos == null ? void 0 : pos.offset) ?? 0;
7987
- } else if (original) {
7988
- const fallback = original(x, y);
7989
- node = (fallback == null ? void 0 : fallback.startContainer) ?? null;
7990
- offset = (fallback == null ? void 0 : fallback.startOffset) ?? 0;
7991
- }
7992
- let resolved = null;
7993
- if (node instanceof Text) {
7994
- resolved = { node, offset };
7995
- } else if (node instanceof Element) {
7996
- resolved = resolveTextPoint(node, offset);
7997
- }
7998
- if (!resolved) {
7999
- return range;
8000
- }
8001
- const cursor = this.domMap.cursorAtDom(resolved.node, resolved.offset);
8002
- if (!cursor) {
8003
- return range;
8004
- }
8005
- const domPoint = this.domMap.domAtCursor(
8006
- cursor.cursorOffset,
8007
- cursor.affinity
8008
- );
8009
- if (!domPoint) {
8010
- return range;
8011
- }
8012
- const fixed = document.createRange();
8013
- fixed.setStart(domPoint.node, domPoint.offset);
8014
- fixed.setEnd(domPoint.node, domPoint.offset);
8015
- return fixed;
8016
- };
8017
- this.patchedCaretRangeFromPoint = patched;
8018
- doc.caretRangeFromPoint = patched;
8019
- }
8020
- uninstallCaretRangeFromPointShim() {
8021
- if (!this.originalCaretRangeFromPoint || !this.patchedCaretRangeFromPoint) {
8022
- return;
8023
- }
8024
- const doc = document;
8025
- if (doc.caretRangeFromPoint === this.patchedCaretRangeFromPoint) {
8026
- doc.caretRangeFromPoint = this.originalCaretRangeFromPoint;
8027
- }
8028
- this.originalCaretRangeFromPoint = null;
8029
- this.patchedCaretRangeFromPoint = null;
8030
- }
8031
7983
  render() {
8032
7984
  const perfEnabled = this.container.dataset.cakePerf === "1";
8033
7985
  let perfStart = 0;
@@ -8046,10 +7998,32 @@ class CakeEngine {
8046
7998
  }
8047
7999
  this.contentRoot = document.createElement("div");
8048
8000
  this.contentRoot.className = "cake-content";
8049
- this.updateContentRootAttributes();
8001
+ }
8002
+ if (this.isTouchDevice()) {
8003
+ this.contentRoot.classList.add("cake-touch-mode");
8004
+ }
8005
+ this.updateContentRootAttributes();
8006
+ if (!this.overlayRoot) {
8050
8007
  const overlay = this.ensureOverlayRoot();
8051
- const extensionsRoot = this.ensureExtensionsRoot();
8052
- this.container.replaceChildren(this.contentRoot, overlay, extensionsRoot);
8008
+ const extensionsRoot = this.extensionsRoot;
8009
+ const existingContainerChildren = Array.from(this.container.childNodes);
8010
+ const isCakeManagedContainerChild = (node) => node instanceof Element && (node.classList.contains("cake-content") || node.classList.contains("cake-selection-overlay") || node.classList.contains("cake-extension-overlay") || node.classList.contains("cake-placeholder"));
8011
+ const preservedContainerChildren = existingContainerChildren.filter(
8012
+ (node) => !isCakeManagedContainerChild(node)
8013
+ );
8014
+ if (this.contentRoot.parentElement === this.container) {
8015
+ this.container.append(overlay);
8016
+ if (extensionsRoot && !extensionsRoot.isConnected) {
8017
+ this.container.append(extensionsRoot);
8018
+ }
8019
+ } else {
8020
+ this.container.replaceChildren(
8021
+ this.contentRoot,
8022
+ overlay,
8023
+ ...extensionsRoot ? [extensionsRoot] : [],
8024
+ ...preservedContainerChildren
8025
+ );
8026
+ }
8053
8027
  this.attachDragListeners();
8054
8028
  }
8055
8029
  if (perfEnabled) {
@@ -8061,9 +8035,12 @@ class CakeEngine {
8061
8035
  this.contentRoot
8062
8036
  );
8063
8037
  const existingChildren = Array.from(this.contentRoot.childNodes);
8064
- const needsUpdate = content.length !== existingChildren.length || content.some((node, i) => node !== existingChildren[i]);
8038
+ const isManagedChild = (node) => node instanceof Element && node.hasAttribute("data-line-index");
8039
+ const existingManagedChildren = existingChildren.filter(isManagedChild);
8040
+ const preservedChildren = existingChildren.filter((node) => !isManagedChild(node));
8041
+ const needsUpdate = content.length !== existingManagedChildren.length || content.some((node, i) => node !== existingManagedChildren[i]);
8065
8042
  if (needsUpdate) {
8066
- this.contentRoot.replaceChildren(...content);
8043
+ this.contentRoot.replaceChildren(...content, ...preservedChildren);
8067
8044
  }
8068
8045
  this.domMap = map;
8069
8046
  if (perfEnabled) {
@@ -8116,6 +8093,8 @@ class CakeEngine {
8116
8093
  if (!this.placeholderRoot) {
8117
8094
  this.placeholderRoot = document.createElement("div");
8118
8095
  this.placeholderRoot.className = "cake-placeholder";
8096
+ this.placeholderRoot.style.position = "absolute";
8097
+ this.placeholderRoot.style.pointerEvents = "none";
8119
8098
  }
8120
8099
  if (!shouldShow) {
8121
8100
  if (this.placeholderRoot.isConnected) {
@@ -8125,20 +8104,21 @@ class CakeEngine {
8125
8104
  return;
8126
8105
  }
8127
8106
  this.placeholderRoot.textContent = placeholderText ?? "";
8128
- this.syncPlaceholderPadding();
8129
8107
  if (!this.placeholderRoot.isConnected) {
8130
8108
  this.container.prepend(this.placeholderRoot);
8131
8109
  }
8110
+ this.syncPlaceholderPosition();
8132
8111
  }
8133
- syncPlaceholderPadding() {
8134
- if (!this.placeholderRoot) {
8112
+ syncPlaceholderPosition() {
8113
+ if (!this.placeholderRoot || !this.contentRoot) {
8135
8114
  return;
8136
8115
  }
8137
- const style = window.getComputedStyle(this.container);
8138
- this.placeholderRoot.style.paddingTop = style.paddingTop;
8139
- this.placeholderRoot.style.paddingRight = style.paddingRight;
8140
- this.placeholderRoot.style.paddingBottom = style.paddingBottom;
8141
- this.placeholderRoot.style.paddingLeft = style.paddingLeft;
8116
+ const containerRect = this.container.getBoundingClientRect();
8117
+ const contentRect = this.contentRoot.getBoundingClientRect();
8118
+ this.placeholderRoot.style.top = `${contentRect.top - containerRect.top}px`;
8119
+ this.placeholderRoot.style.left = `${contentRect.left - containerRect.left}px`;
8120
+ this.placeholderRoot.style.width = `${contentRect.width}px`;
8121
+ this.placeholderRoot.style.height = `${contentRect.height}px`;
8142
8122
  }
8143
8123
  updateContentRootAttributes() {
8144
8124
  if (!this.contentRoot) {
@@ -8748,81 +8728,21 @@ class CakeEngine {
8748
8728
  }
8749
8729
  handleBeforeInput(event) {
8750
8730
  if (this.readOnly || this.isComposing || event.isComposing) {
8751
- if (event.inputType === "insertReplacementText" || event.inputType === "insertText") {
8752
- console.log("[SPELLCHECK] beforeinput ignored (readonly/composing)", {
8753
- inputType: event.inputType,
8754
- data: event.data,
8755
- cancelable: event.cancelable,
8756
- isComposing: this.isComposing,
8757
- eventIsComposing: event.isComposing,
8758
- readOnly: this.readOnly
8759
- });
8760
- }
8761
8731
  return;
8762
8732
  }
8763
8733
  if (!this.isEventTargetInContentRoot(event.target)) {
8764
- if (event.inputType === "insertReplacementText" || event.inputType === "insertText") {
8765
- console.log(
8766
- "[SPELLCHECK] beforeinput ignored (target outside editor)",
8767
- {
8768
- inputType: event.inputType,
8769
- data: event.data,
8770
- cancelable: event.cancelable,
8771
- target: event.target
8772
- }
8773
- );
8774
- }
8775
8734
  return;
8776
8735
  }
8777
8736
  if (this.keydownHandledBeforeInput) {
8778
8737
  this.keydownHandledBeforeInput = false;
8779
- if (event.inputType === "insertReplacementText" || event.inputType === "insertText") {
8780
- console.log(
8781
- "[SPELLCHECK] beforeinput skipped (keydownHandledBeforeInput)",
8782
- {
8783
- inputType: event.inputType,
8784
- data: event.data,
8785
- cancelable: event.cancelable
8786
- }
8787
- );
8788
- }
8789
8738
  event.preventDefault();
8790
8739
  return;
8791
8740
  }
8792
8741
  const intent = this.resolveBeforeInputIntent(event);
8793
8742
  if (!intent) {
8794
- if (event.inputType === "insertReplacementText" || event.inputType === "insertText") {
8795
- console.log("[SPELLCHECK] beforeinput: no intent", {
8796
- inputType: event.inputType,
8797
- data: event.data,
8798
- cancelable: event.cancelable
8799
- });
8800
- }
8801
8743
  return;
8802
8744
  }
8803
- if (event.inputType === "insertReplacementText" || event.inputType === "insertText" && intent.type === "replace-text") {
8804
- const selection = this.state.selection;
8805
- const focus = selection.start === selection.end ? selection.start : Math.max(selection.start, selection.end);
8806
- const preview = this.state.source.slice(
8807
- Math.max(0, focus - 24),
8808
- Math.min(this.state.source.length, focus + 24)
8809
- );
8810
- console.log("[SPELLCHECK] beforeinput: resolved intent", {
8811
- inputType: event.inputType,
8812
- data: event.data,
8813
- cancelable: event.cancelable,
8814
- intent,
8815
- currentSelection: this.state.selection,
8816
- sourcePreviewAroundFocus: preview
8817
- });
8818
- }
8819
8745
  event.preventDefault();
8820
- if (event.inputType === "insertReplacementText" || event.inputType === "insertText" && intent.type === "replace-text") {
8821
- console.log("[SPELLCHECK] beforeinput: after preventDefault", {
8822
- inputType: event.inputType,
8823
- defaultPrevented: event.defaultPrevented
8824
- });
8825
- }
8826
8746
  this.markBeforeInputHandled();
8827
8747
  this.suppressSelectionChangeForTick();
8828
8748
  this.applyInputIntent(intent);
@@ -8869,22 +8789,9 @@ class CakeEngine {
8869
8789
  return;
8870
8790
  }
8871
8791
  if (!this.isEventTargetInContentRoot(event.target)) {
8872
- if (event.inputType === "insertReplacementText") {
8873
- console.log("[SPELLCHECK] input ignored (target outside editor)", {
8874
- inputType: event.inputType,
8875
- data: event.data,
8876
- target: event.target
8877
- });
8878
- }
8879
8792
  return;
8880
8793
  }
8881
8794
  if (this.beforeInputHandled) {
8882
- if (event.inputType === "insertReplacementText") {
8883
- console.log("[SPELLCHECK] input ignored (handled via beforeinput)", {
8884
- inputType: event.inputType,
8885
- data: event.data
8886
- });
8887
- }
8888
8795
  return;
8889
8796
  }
8890
8797
  if (this.compositionCommit && event.inputType === "insertText") {
@@ -8996,57 +8903,20 @@ class CakeEngine {
8996
8903
  return null;
8997
8904
  }
8998
8905
  selectionFromTargetRangesWithStatus(event) {
8999
- const debug = event.inputType === "insertReplacementText";
9000
8906
  if (!event.getTargetRanges) {
9001
- if (debug) {
9002
- console.log("[SPELLCHECK][targetRanges] missing getTargetRanges()", {
9003
- inputType: event.inputType,
9004
- data: event.data,
9005
- cancelable: event.cancelable
9006
- });
9007
- }
9008
8907
  return { status: "none" };
9009
8908
  }
9010
8909
  const ranges = event.getTargetRanges();
9011
8910
  if (!ranges || ranges.length === 0) {
9012
- if (debug) {
9013
- console.log("[SPELLCHECK][targetRanges] no ranges", {
9014
- inputType: event.inputType,
9015
- data: event.data,
9016
- cancelable: event.cancelable
9017
- });
9018
- }
9019
8911
  return { status: "none" };
9020
8912
  }
9021
8913
  const range = ranges[0];
9022
- if (debug || event.inputType === "insertText") {
9023
- console.log("[SPELLCHECK][targetRanges] raw range", {
9024
- inputType: event.inputType,
9025
- data: event.data,
9026
- cancelable: event.cancelable,
9027
- startContainer: range.startContainer instanceof Element ? range.startContainer.tagName : range.startContainer.nodeName,
9028
- startOffset: range.startOffset,
9029
- endContainer: range.endContainer instanceof Element ? range.endContainer.tagName : range.endContainer.nodeName,
9030
- endOffset: range.endOffset,
9031
- startContained: this.container.contains(range.startContainer),
9032
- endContained: this.container.contains(range.endContainer)
9033
- });
9034
- }
9035
8914
  if (!this.container.contains(range.startContainer) || !this.container.contains(range.endContainer)) {
9036
8915
  return { status: "invalid" };
9037
8916
  }
9038
8917
  const start = this.cursorFromDom(range.startContainer, range.startOffset);
9039
8918
  const end = this.cursorFromDom(range.endContainer, range.endOffset);
9040
8919
  if (!start || !end) {
9041
- if (debug || event.inputType === "insertText") {
9042
- console.log("[SPELLCHECK][targetRanges] cursorFromDom failed", {
9043
- inputType: event.inputType,
9044
- data: event.data,
9045
- cancelable: event.cancelable,
9046
- start,
9047
- end
9048
- });
9049
- }
9050
8920
  return { status: "invalid" };
9051
8921
  }
9052
8922
  const affinity = start.cursorOffset === end.cursorOffset ? end.affinity : "forward";
@@ -9797,9 +9667,7 @@ class CakeEngine {
9797
9667
  return this.state.source;
9798
9668
  }
9799
9669
  const blocks = Array.from(
9800
- this.contentRoot.querySelectorAll(
9801
- '[data-block="paragraph"]'
9802
- )
9670
+ this.contentRoot.querySelectorAll(".cake-line")
9803
9671
  );
9804
9672
  if (blocks.length === 0) {
9805
9673
  return this.contentRoot.textContent ?? "";
@@ -10052,6 +9920,9 @@ class CakeEngine {
10052
9920
  root.style.zIndex = "50";
10053
9921
  root.style.overflow = "hidden";
10054
9922
  this.extensionsRoot = root;
9923
+ if (this.overlayRoot && !root.isConnected) {
9924
+ this.container.append(root);
9925
+ }
10055
9926
  return root;
10056
9927
  }
10057
9928
  updateExtensionsOverlayPosition() {
@@ -10073,8 +9944,13 @@ class CakeEngine {
10073
9944
  if (!this.overlayRoot || !this.contentRoot) {
10074
9945
  return;
10075
9946
  }
9947
+ if (!this.hasFocus()) {
9948
+ this.updateCaret(null);
9949
+ this.syncSelectionRects([]);
9950
+ return;
9951
+ }
10076
9952
  const isRecentTouch = Date.now() - this.lastTouchTime < 2e3;
10077
- if (isRecentTouch) {
9953
+ if (this.isTouchDevice() || isRecentTouch) {
10078
9954
  this.contentRoot.classList.add("cake-touch-mode");
10079
9955
  this.updateCaret(null);
10080
9956
  this.syncSelectionRects([]);
@@ -11612,13 +11488,15 @@ const CakeEditor = forwardRef(
11612
11488
  const onSelectionChangeRef = useRef(props.onSelectionChange);
11613
11489
  const lastEmittedValueRef = useRef(null);
11614
11490
  const lastEmittedSelectionRef = useRef(null);
11615
- const [overlayRoot, setOverlayRoot] = useState(null);
11616
11491
  const [contentRoot, setContentRoot] = useState(null);
11617
11492
  const baseExtensions = props.disableImageExtension ? bundledExtensionsWithoutImage : bundledExtensions;
11618
11493
  const allExtensionsRef = useRef([
11619
11494
  ...baseExtensions,
11620
11495
  ...props.extensions ?? []
11621
11496
  ]);
11497
+ const hasOverlayExtensions = allExtensionsRef.current.some(
11498
+ (ext) => ext.renderOverlay
11499
+ );
11622
11500
  useEffect(() => {
11623
11501
  onChangeRef.current = props.onChange;
11624
11502
  onSelectionChangeRef.current = props.onSelectionChange;
@@ -11663,12 +11541,10 @@ const CakeEditor = forwardRef(
11663
11541
  }
11664
11542
  });
11665
11543
  engineRef.current = engine;
11666
- setOverlayRoot(engine.getOverlayRoot());
11667
11544
  setContentRoot(engine.getContentRoot());
11668
11545
  return () => {
11669
11546
  engine.destroy();
11670
11547
  engineRef.current = null;
11671
- setOverlayRoot(null);
11672
11548
  setContentRoot(null);
11673
11549
  };
11674
11550
  }, []);
@@ -11796,7 +11672,6 @@ const CakeEditor = forwardRef(
11796
11672
  const overlayContext = containerRef.current && contentRoot ? {
11797
11673
  container: containerRef.current,
11798
11674
  contentRoot,
11799
- overlayRoot: overlayRoot ?? void 0,
11800
11675
  toOverlayRect: (rect) => {
11801
11676
  var _a;
11802
11677
  const containerRect = (_a = containerRef.current) == null ? void 0 : _a.getBoundingClientRect();
@@ -11837,9 +11712,6 @@ const CakeEditor = forwardRef(
11837
11712
  return ((_a = engineRef.current) == null ? void 0 : _a.executeCommand(command)) ?? false;
11838
11713
  }
11839
11714
  } : null;
11840
- const hasOverlayExtensions = allExtensionsRef.current.some(
11841
- (ext) => ext.renderOverlay
11842
- );
11843
11715
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { position: "relative", height: "100%" }, children: [
11844
11716
  /* @__PURE__ */ jsxRuntimeExports.jsx(
11845
11717
  "div",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blankdotpage/cake",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",