@markput/react 0.14.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -26,6 +26,12 @@ function merge(...objects) {
26
26
  return Object.keys(result).length > 0 ? result : void 0;
27
27
  }
28
28
  //#endregion
29
+ //#region ../../core/src/shared/utils/replaceInString.ts
30
+ function replaceInString(current, range, replacement) {
31
+ if (range.start < 0 || range.end < range.start || range.end > current.length) return void 0;
32
+ return current.slice(0, range.start) + replacement + current.slice(range.end);
33
+ }
34
+ //#endregion
29
35
  //#region ../../core/src/shared/constants.ts
30
36
  const KEYBOARD = {
31
37
  UP: "ArrowUp",
@@ -54,18 +60,20 @@ const DEFAULT_OPTIONS = [{
54
60
  //#endregion
55
61
  //#region ../../core/src/shared/classes/MarkputHandler.ts
56
62
  var MarkputHandler = class {
57
- constructor(store) {
58
- this.store = store;
63
+ constructor(dom, overlayFeature, parsing) {
64
+ this.dom = dom;
65
+ this.overlayFeature = overlayFeature;
66
+ this.parsing = parsing;
59
67
  }
60
68
  get container() {
61
- return this.store.dom.container();
69
+ return this.dom.container();
62
70
  }
63
71
  get overlay() {
64
- return this.store.overlay.element();
72
+ return this.overlayFeature.element();
65
73
  }
66
74
  focus() {
67
- const firstAddress = this.store.parsing.index().addressFor([0]);
68
- if (firstAddress && this.store.dom.focusAddress(firstAddress).ok) return;
75
+ const firstAddress = this.parsing.index().addressFor([0]);
76
+ if (firstAddress && this.dom.focusAddress(firstAddress).ok) return;
69
77
  this.container?.focus();
70
78
  }
71
79
  };
@@ -482,7 +490,8 @@ function signal(initial, opts) {
482
490
  };
483
491
  return signalOper.bind(node);
484
492
  }
485
- function computed(getter, opts) {
493
+ function computed(getterOrOpts, opts) {
494
+ const isWritable = typeof getterOrOpts !== "function";
486
495
  const node = {
487
496
  value: void 0,
488
497
  subs: void 0,
@@ -490,10 +499,19 @@ function computed(getter, opts) {
490
499
  deps: void 0,
491
500
  depsTail: void 0,
492
501
  flags: ReactiveFlags.None,
493
- getter,
494
- equalsFn: opts?.equals ?? void 0
502
+ getter: isWritable ? getterOrOpts.get : getterOrOpts,
503
+ equalsFn: (isWritable ? getterOrOpts.equals : opts?.equals) ?? void 0
495
504
  };
496
- return computedOper.bind(node);
505
+ const readFn = computedOper.bind(node);
506
+ if (!isWritable) return readFn;
507
+ const writableComputed = function writableComputedOper(...args) {
508
+ if (args.length === 0) return readFn();
509
+ const next = args[0];
510
+ if (next === void 0) return;
511
+ getterOrOpts.set(next);
512
+ };
513
+ Object.defineProperty(writableComputed, "name", { value: "bound " + computedOper.name });
514
+ return writableComputed;
497
515
  }
498
516
  function event() {
499
517
  const node = {
@@ -516,7 +534,7 @@ function event() {
516
534
  callable.read = eventReadOper.bind(node);
517
535
  return callable;
518
536
  }
519
- function alienEffect(fn) {
537
+ function effect(fn) {
520
538
  const e = {
521
539
  fn,
522
540
  cleanup: void 0,
@@ -557,7 +575,7 @@ function effectScope(fn) {
557
575
  function watch(dep, fn) {
558
576
  let initialized = false;
559
577
  let oldValue;
560
- return alienEffect(() => {
578
+ return effect(() => {
561
579
  const newValue = "read" in dep ? dep.read() : dep();
562
580
  if (!initialized) {
563
581
  initialized = true;
@@ -588,8 +606,26 @@ function untracked(fn) {
588
606
  setActiveSub(prev);
589
607
  }
590
608
  }
609
+ function model(opts) {
610
+ let internal;
611
+ const ensureInternal = () => {
612
+ if (internal !== void 0) return internal;
613
+ internal = signal(opts.default !== void 0 ? untracked(opts.default) : void 0, { equals: opts.equals });
614
+ return internal;
615
+ };
616
+ const getFn = opts.get ?? ((value) => value);
617
+ const setFn = opts.set ?? ((next, previous) => next ?? previous);
618
+ const reader = computed(() => getFn(ensureInternal()()));
619
+ const callable = function modelOper(...args) {
620
+ if (args.length === 0) return reader();
621
+ const sig = ensureInternal();
622
+ sig(setFn(args[0], sig()));
623
+ };
624
+ Object.defineProperty(callable, "name", { value: "bound " + computedOper.name });
625
+ return callable;
626
+ }
591
627
  function listen(target, event, handler, options) {
592
- return alienEffect(() => {
628
+ return effect(() => {
593
629
  target.addEventListener(event, handler, options);
594
630
  return () => target.removeEventListener(event, handler, options);
595
631
  });
@@ -1727,20 +1763,6 @@ function findToken(tokens, target, depth = 0, parent) {
1727
1763
  }
1728
1764
  }
1729
1765
  //#endregion
1730
- //#region ../../core/src/features/parsing/utils/valueParser.ts
1731
- function parseWithParser(store, value) {
1732
- const parser = store.parsing.parser();
1733
- if (!parser) return [{
1734
- type: "text",
1735
- content: value,
1736
- position: {
1737
- start: 0,
1738
- end: value.length
1739
- }
1740
- }];
1741
- return parser.parse(value);
1742
- }
1743
- //#endregion
1744
1766
  //#region ../../core/src/features/parsing/tokenIndex.ts
1745
1767
  function pathEquals(a, b) {
1746
1768
  return a.length === b.length && a.every((part, index) => part === b[index]);
@@ -1813,24 +1835,41 @@ function createTokenIndex(tokens, generation) {
1813
1835
  };
1814
1836
  }
1815
1837
  //#endregion
1816
- //#region ../../core/src/features/parsing/ParseFeature.ts
1817
- var ParsingFeature = class {
1838
+ //#region ../../core/src/features/parsing/ParseController.ts
1839
+ var ParseController = class {
1818
1840
  #generation = signal(0);
1819
1841
  #scope;
1820
- constructor(_store) {
1821
- this._store = _store;
1842
+ constructor(lifecycle, value, mark, props, slots) {
1843
+ this.lifecycle = lifecycle;
1844
+ this.value = value;
1845
+ this.mark = mark;
1846
+ this.props = props;
1847
+ this.slots = slots;
1822
1848
  this.tokens = signal([]);
1823
1849
  this.index = computed(() => createTokenIndex(this.tokens(), this.#generation()));
1824
1850
  this.parser = computed(() => {
1825
- if (!this._store.mark.enabled()) return;
1826
- const markups = this._store.props.options().map((opt) => opt.markup);
1851
+ if (!this.mark.enabled()) return;
1852
+ const markups = this.props.options().map((opt) => opt.markup);
1827
1853
  if (!markups.some(Boolean)) return;
1828
- return new Parser(markups, this._store.slots.isBlock() ? { skipEmptyText: true } : void 0);
1854
+ return new Parser(markups, this.slots.isBlock() ? { skipEmptyText: true } : void 0);
1829
1855
  });
1830
1856
  this.reparse = event();
1831
- }
1832
- parseValue(value) {
1833
- return parseWithParser(this._store, value);
1857
+ lifecycle.onMounted(() => {
1858
+ this.acceptTokens(this.#parseValue(value.current()));
1859
+ this.#subscribeValue();
1860
+ });
1861
+ const toggle = (enabled) => {
1862
+ if (enabled && !this.#scope) this.#scope = effectScope(() => {
1863
+ this.#subscribeReactiveParse();
1864
+ this.#subscribeReparse();
1865
+ });
1866
+ if (!enabled && this.#scope) {
1867
+ this.#scope();
1868
+ this.#scope = void 0;
1869
+ }
1870
+ };
1871
+ watch(this.mark.enabled, toggle);
1872
+ toggle(this.mark.enabled());
1834
1873
  }
1835
1874
  acceptTokens(tokens) {
1836
1875
  batch(() => {
@@ -1838,34 +1877,31 @@ var ParsingFeature = class {
1838
1877
  this.#generation(this.#generation() + 1);
1839
1878
  }, { mutable: true });
1840
1879
  }
1841
- enable() {
1842
- if (this.#scope) return;
1843
- this.sync();
1844
- this.#scope = effectScope(() => {
1845
- this.#subscribeParse();
1846
- this.#subscribeReactiveParse();
1847
- });
1848
- }
1849
- disable() {
1850
- this.#scope?.();
1851
- this.#scope = void 0;
1852
- }
1853
- sync(value = this._store.value.current()) {
1854
- this.acceptTokens(this.parseValue(value));
1855
- }
1856
- #subscribeParse() {
1857
- watch(this.reparse, () => {
1858
- if (this._store.caret.recovery()) {
1859
- const text = toString(this.tokens());
1860
- this.acceptTokens(this.parseValue(text));
1861
- return;
1880
+ #parseValue(value) {
1881
+ const parser = this.parser();
1882
+ if (!parser) return [{
1883
+ type: "text",
1884
+ content: value,
1885
+ position: {
1886
+ start: 0,
1887
+ end: value.length
1862
1888
  }
1863
- this.sync();
1889
+ }];
1890
+ return parser.parse(value);
1891
+ }
1892
+ #subscribeValue() {
1893
+ watch(this.value.current, (v) => {
1894
+ this.acceptTokens(this.#parseValue(v));
1864
1895
  });
1865
1896
  }
1866
1897
  #subscribeReactiveParse() {
1867
1898
  watch(computed(() => this.parser()), () => {
1868
- if (!this._store.caret.recovery()) this.sync(this._store.value.current());
1899
+ this.acceptTokens(this.#parseValue(this.value.current()));
1900
+ });
1901
+ }
1902
+ #subscribeReparse() {
1903
+ watch(this.reparse, () => {
1904
+ this.acceptTokens(this.#parseValue(this.value.current()));
1869
1905
  });
1870
1906
  }
1871
1907
  };
@@ -1896,304 +1932,144 @@ function nextText(walker) {
1896
1932
  return node?.nodeType === 3 ? node : null;
1897
1933
  }
1898
1934
  //#endregion
1899
- //#region ../../core/src/features/caret/Caret.ts
1900
- var Caret = class {
1901
- static get isSelectedPosition() {
1902
- const selection = window.getSelection();
1903
- if (!selection) return;
1904
- return selection.isCollapsed;
1905
- }
1906
- static getCurrentPosition() {
1907
- return window.getSelection()?.anchorOffset ?? 0;
1908
- }
1909
- static getFocusedSpan() {
1910
- return window.getSelection()?.anchorNode?.textContent ?? "";
1911
- }
1912
- static getSelectedNode() {
1913
- const node = window.getSelection()?.anchorNode;
1914
- if (node && document.contains(node)) return node;
1915
- throw new Error("Anchor node of selection is not exists!");
1916
- }
1917
- static getAbsolutePosition() {
1918
- const rect = window.getSelection()?.getRangeAt(0).getBoundingClientRect();
1919
- if (rect) return {
1920
- left: rect.left,
1921
- top: rect.top + rect.height + 1
1922
- };
1923
- return {
1924
- left: 0,
1925
- top: 0
1926
- };
1927
- }
1928
- /** Returns the raw DOMRect of the current caret position, or null if unavailable. */
1929
- static getCaretRect() {
1930
- try {
1931
- return (window.getSelection()?.getRangeAt(0))?.getBoundingClientRect() ?? null;
1932
- } catch {
1933
- return null;
1934
- }
1935
- }
1936
- /**
1937
- * Returns true if the caret is on the first visual line of the element.
1938
- */
1939
- static isCaretOnFirstLine(element) {
1940
- const caretRect = this.getCaretRect();
1941
- if (!caretRect || caretRect.height === 0) return true;
1942
- const elRect = element.getBoundingClientRect();
1943
- return caretRect.top < elRect.top + caretRect.height + 2;
1944
- }
1945
- /**
1946
- * Returns true if the caret is on the last visual line of the element.
1947
- */
1948
- static isCaretOnLastLine(element) {
1949
- const caretRect = this.getCaretRect();
1950
- if (!caretRect || caretRect.height === 0) return true;
1951
- const elRect = element.getBoundingClientRect();
1952
- return caretRect.bottom > elRect.bottom - caretRect.height - 2;
1953
- }
1954
- /**
1955
- * Positions the caret in `element` at the character closest to the given x coordinate.
1956
- * `y` defaults to the vertical center of the element.
1957
- */
1958
- static setAtX(element, x, y) {
1959
- const elRect = element.getBoundingClientRect();
1960
- const targetY = y ?? elRect.top + elRect.height / 2;
1961
- const caretDoc = document;
1962
- const caretPos = caretDoc.caretRangeFromPoint?.(x, targetY) ?? caretDoc.caretPositionFromPoint?.(x, targetY);
1963
- if (!caretPos) return;
1964
- const sel = window.getSelection();
1965
- if (!sel) return;
1966
- let domRange;
1967
- if (caretPos instanceof Range) domRange = caretPos;
1968
- else if ("offsetNode" in caretPos) {
1969
- domRange = document.createRange();
1970
- domRange.setStart(caretPos.offsetNode, caretPos.offset);
1971
- domRange.collapse(true);
1972
- } else return;
1973
- if (!element.contains(domRange.startContainer)) {
1974
- this.setIndex(element, Infinity);
1975
- return;
1976
- }
1977
- sel.removeAllRanges();
1978
- sel.addRange(domRange);
1979
- }
1980
- static trySetIndex(element, offset) {
1981
- try {
1982
- this.setIndex(element, offset);
1983
- } catch (e) {
1984
- console.error(e);
1985
- }
1986
- }
1987
- /**
1988
- * Sets the caret at character `offset` within `element` by walking text nodes.
1989
- * Use Infinity to position at the very end of all text.
1990
- */
1991
- static setIndex(element, offset) {
1992
- const selection = window.getSelection();
1993
- if (!selection) return;
1994
- const walker = document.createTreeWalker(element, 4);
1995
- let node = nextText(walker);
1996
- if (!node) return;
1997
- let remaining = isFinite(offset) ? Math.max(0, offset) : Infinity;
1998
- for (;;) {
1999
- const next = nextText(walker);
2000
- if (!next || remaining <= node.length) {
2001
- const charOffset = isFinite(remaining) ? Math.min(remaining, node.length) : node.length;
2002
- const range = document.createRange();
2003
- range.setStart(node, charOffset);
2004
- range.collapse(true);
2005
- selection.removeAllRanges();
2006
- selection.addRange(range);
2007
- return;
2008
- }
2009
- remaining -= node.length;
2010
- node = next;
2011
- }
2012
- }
2013
- static getCaretIndex(element) {
2014
- let position = 0;
2015
- const selection = window.getSelection();
2016
- if (!selection?.rangeCount) return position;
2017
- const range = selection.getRangeAt(0);
2018
- const preCaretRange = range.cloneRange();
2019
- preCaretRange.selectNodeContents(element);
2020
- preCaretRange.setEnd(range.endContainer, range.endOffset);
2021
- position = preCaretRange.toString().length;
2022
- return position;
2023
- }
2024
- static setCaretToEnd(element) {
2025
- if (!element) return;
2026
- this.setIndex(element, Infinity);
2027
- }
2028
- static getIndex() {
2029
- return window.getSelection()?.anchorOffset ?? NaN;
2030
- }
2031
- static setIndex1(offset) {
2032
- const selection = window.getSelection();
2033
- if (!selection?.anchorNode || !selection.rangeCount) return;
2034
- const range = selection.getRangeAt(0);
2035
- range.setStart(range.startContainer.firstChild ?? range.startContainer, offset);
2036
- range.setEnd(range.startContainer.firstChild ?? range.startContainer, offset);
1935
+ //#region ../../core/src/features/caret/CaretModel.ts
1936
+ var CaretModel = class {
1937
+ constructor(lifecycle, dom, value) {
1938
+ this.lifecycle = lifecycle;
1939
+ this.dom = dom;
1940
+ this.value = value;
1941
+ this.selection = signal(void 0, { equals: shallow });
1942
+ this.position = computed({
1943
+ get: () => this.selection()?.start,
1944
+ set: (value) => this.selection(value !== void 0 ? {
1945
+ start: value,
1946
+ end: value
1947
+ } : void 0)
1948
+ });
1949
+ this.isUserSelecting = signal(false);
1950
+ this.isAllSelected = computed(() => {
1951
+ const s = this.selection();
1952
+ const v = this.value.current();
1953
+ return s?.start === 0 && s.end === v.length && v.length > 0;
1954
+ });
1955
+ lifecycle.onMounted(() => {
1956
+ this.#enableFocusTracking();
1957
+ this.#enableSelectionTracking();
1958
+ watch(dom.indexed, () => {
1959
+ dom.reconcile({ isUserSelecting: this.isUserSelecting() });
1960
+ this.#applyRangeToDOM();
1961
+ });
1962
+ effect(() => {
1963
+ const isUserSelecting = this.isUserSelecting();
1964
+ dom.readOnly();
1965
+ dom.reconcile({ isUserSelecting });
1966
+ });
1967
+ });
2037
1968
  }
2038
- setCaretRightTo(element, offset) {
2039
- const range = window.getSelection()?.getRangeAt(0);
2040
- range?.setStart(range.endContainer, offset);
2041
- range?.setEnd(range.endContainer, offset);
1969
+ selectAll() {
1970
+ this.selection({
1971
+ start: 0,
1972
+ end: this.value.current().length
1973
+ });
1974
+ this.#applyRangeToDOM();
2042
1975
  }
2043
- };
2044
- //#endregion
2045
- //#region ../../core/src/features/caret/focus.ts
2046
- function enableFocus(store) {
2047
- const container = store.dom.container();
2048
- if (!container) return () => {};
2049
- const scope = effectScope(() => {
1976
+ #enableFocusTracking() {
1977
+ const container = this.dom.container();
1978
+ if (!container) return;
2050
1979
  listen(container, "focusin", (e) => {
2051
- const target = isHtmlElement(e.target) ? e.target : void 0;
1980
+ const target = e.target instanceof HTMLElement ? e.target : void 0;
2052
1981
  if (!target) {
2053
- store.caret.location(void 0);
1982
+ this.selection(void 0);
2054
1983
  return;
2055
1984
  }
2056
- const result = store.dom.locateNode(target);
1985
+ const result = this.dom.locateNode(target);
2057
1986
  if (!result.ok) {
2058
1987
  if (result.reason === "control") return;
2059
- store.caret.location(void 0);
1988
+ this.selection(void 0);
2060
1989
  return;
2061
1990
  }
2062
- const role = result.value.textElement?.contains(target) ? "text" : "markDescendant";
2063
- store.caret.location({
2064
- address: result.value.address,
2065
- role
2066
- });
1991
+ const rawSel = this.dom.readRawSelection();
1992
+ if (rawSel.ok) this.selection(rawSel.value.range);
2067
1993
  });
2068
1994
  listen(container, "focusout", () => {
2069
- store.caret.location(void 0);
2070
- });
2071
- listen(container, "click", () => {
2072
- const tokens = store.parsing.tokens();
2073
- if (tokens.length === 1 && tokens[0].type === "text" && tokens[0].content === "") {
2074
- const container = store.dom.container();
2075
- (container ? firstHtmlChild(container) : null)?.focus();
2076
- }
1995
+ queueMicrotask(() => {
1996
+ if (!container.contains(document.activeElement)) this.selection(void 0);
1997
+ });
2077
1998
  });
2078
- });
2079
- return () => scope();
2080
- }
2081
- //#endregion
2082
- //#region ../../core/src/features/caret/selection.ts
2083
- function enableSelection(store) {
2084
- let pressedNode = null;
2085
- let isPressed = false;
2086
- const scope = effectScope(() => {
1999
+ }
2000
+ #enableSelectionTracking() {
2001
+ let pressedAt = null;
2087
2002
  listen(document, "mousedown", (e) => {
2088
- pressedNode = nodeTarget(e);
2089
- isPressed = true;
2003
+ pressedAt = nodeTarget(e);
2090
2004
  });
2091
2005
  listen(document, "mousemove", (e) => {
2092
- const container = store.dom.container();
2006
+ if (pressedAt === null) return;
2007
+ const container = this.dom.container();
2093
2008
  if (!container) return;
2094
- const currentIsPressed = isPressed;
2095
- const isNotInnerSome = !container.contains(pressedNode) || pressedNode !== e.target;
2096
- const isInside = window.getSelection()?.containsNode(container, true);
2097
- if (currentIsPressed && isNotInnerSome && isInside) {
2098
- if (store.caret.selecting() !== "drag") store.caret.selecting("drag");
2099
- }
2009
+ const startedOutsideEditor = !container.contains(pressedAt);
2010
+ const sweepingAcrossNodes = pressedAt !== e.target;
2011
+ const selectionIntersectsEditor = window.getSelection()?.containsNode(container, true) ?? false;
2012
+ if ((startedOutsideEditor || sweepingAcrossNodes) && selectionIntersectsEditor) this.isUserSelecting(true);
2100
2013
  });
2101
2014
  listen(document, "mouseup", () => {
2102
- isPressed = false;
2103
- pressedNode = null;
2104
- if (store.caret.selecting() === "drag") {
2105
- const sel = window.getSelection();
2106
- if (!sel || sel.isCollapsed) store.caret.selecting(void 0);
2107
- }
2015
+ pressedAt = null;
2016
+ if (!this.isUserSelecting()) return;
2017
+ const sel = window.getSelection();
2018
+ if (!sel || sel.isCollapsed) this.isUserSelecting(false);
2108
2019
  });
2109
2020
  listen(document, "selectionchange", () => {
2110
2021
  const sel = window.getSelection();
2111
- if (store.caret.selecting() === "drag" && (!sel || sel.isCollapsed)) store.caret.selecting(void 0);
2022
+ if (this.isUserSelecting() && (!sel || sel.isCollapsed)) this.isUserSelecting(false);
2112
2023
  if (!sel?.focusNode) return;
2113
- const result = store.dom.locateNode(sel.focusNode);
2024
+ const result = this.dom.locateNode(sel.focusNode);
2114
2025
  if (!result.ok) {
2115
2026
  if (result.reason === "control") return;
2116
- store.caret.location(void 0);
2027
+ this.selection(void 0);
2117
2028
  return;
2118
2029
  }
2119
- const role = result.value.textElement?.contains(sel.focusNode) ? "text" : "markDescendant";
2120
- store.caret.location({
2121
- address: result.value.address,
2122
- role
2123
- });
2124
- });
2125
- alienEffect(() => {
2126
- if (store.caret.selecting() === "drag") store.dom.reconcile();
2030
+ const rawSel = this.dom.readRawSelection();
2031
+ if (rawSel.ok) this.selection(rawSel.value.range);
2032
+ else this.selection(void 0);
2127
2033
  });
2128
- });
2129
- return () => {
2130
- if (store.caret.selecting() === "drag") store.caret.selecting(void 0);
2131
- scope();
2132
- pressedNode = null;
2133
- isPressed = false;
2134
- };
2135
- }
2136
- //#endregion
2137
- //#region ../../core/src/features/caret/CaretFeature.ts
2138
- var CaretFeature = class {
2139
- #disposers = [];
2140
- constructor(_store) {
2141
- this._store = _store;
2142
- this.recovery = signal(void 0);
2143
- this.location = signal(void 0);
2144
- this.selecting = signal(void 0);
2145
- }
2146
- enable() {
2147
- if (this.#disposers.length) return;
2148
- this.#disposers = [enableFocus(this._store), enableSelection(this._store)];
2149
- }
2150
- disable() {
2151
- this.#disposers.forEach((d) => d());
2152
- this.#disposers = [];
2153
2034
  }
2154
- placeAt(rawPosition, affinity = "after") {
2155
- return this._store.dom.placeCaretAtRawPosition(rawPosition, affinity);
2156
- }
2157
- focus(address, boundary = "start") {
2158
- return this._store.dom.focusAddress(address, boundary);
2035
+ #applyRangeToDOM() {
2036
+ if (this.isUserSelecting()) return;
2037
+ const sel = this.selection();
2038
+ if (sel === void 0) return;
2039
+ if (sel.start === sel.end) {
2040
+ const result = this.dom.placeAt(sel.start);
2041
+ if (!result.ok) {
2042
+ this.selection(void 0);
2043
+ return;
2044
+ }
2045
+ const applied = result.value.applied;
2046
+ if (applied !== sel.start) this.selection({
2047
+ start: applied,
2048
+ end: applied
2049
+ });
2050
+ return;
2051
+ }
2052
+ const result = this.dom.placeRange(sel);
2053
+ if (!result.ok) {
2054
+ this.selection(void 0);
2055
+ return;
2056
+ }
2057
+ this.selection(result.value.applied);
2159
2058
  }
2160
2059
  };
2161
2060
  //#endregion
2162
- //#region ../../core/src/features/caret/selectionHelpers.ts
2163
- function isFullSelection(store) {
2164
- const sel = window.getSelection();
2165
- const container = store.dom.container();
2166
- if (!sel?.rangeCount || !container?.firstChild || !container.lastChild) return false;
2167
- try {
2168
- const range = sel.getRangeAt(0);
2169
- return container.contains(range.startContainer) && container.contains(range.endContainer) && range.toString().length > 0;
2170
- } catch {
2171
- return false;
2172
- }
2173
- }
2174
- function selectAllText(store, event) {
2175
- if ((event.ctrlKey || event.metaKey) && event.code === "KeyA") {
2176
- if (store.slots.isBlock()) return;
2177
- event.preventDefault();
2178
- const selection = window.getSelection();
2179
- const anchorNode = store.dom.container()?.firstChild;
2180
- const focusNode = store.dom.container()?.lastChild;
2181
- if (!selection || !anchorNode || !focusNode) return;
2182
- selection.setBaseAndExtent(anchorNode, 0, focusNode, 1);
2183
- store.caret.selecting("all");
2184
- }
2185
- }
2186
- //#endregion
2187
2061
  //#region ../../core/src/features/caret/TriggerFinder.ts
2188
2062
  /** Regex to match word characters from the start of a string */
2189
2063
  const wordRegex = /* @__PURE__ */ new RegExp(/^\w*/);
2190
2064
  var TriggerFinder = class TriggerFinder {
2191
- constructor(store) {
2192
- this.store = store;
2193
- const caretPosition = Caret.getCurrentPosition();
2194
- this.node = Caret.getSelectedNode();
2195
- this.span = Caret.getFocusedSpan();
2196
- this.dividedText = this.getDividedTextBy(caretPosition);
2065
+ constructor(dom) {
2066
+ this.dom = dom;
2067
+ const sel = window.getSelection();
2068
+ const node = sel?.anchorNode;
2069
+ if (!sel || !node || !document.contains(node)) throw new Error("Anchor node of selection is not exists!");
2070
+ this.node = node;
2071
+ this.span = node.textContent ?? "";
2072
+ this.dividedText = this.getDividedTextBy(sel.anchorOffset);
2197
2073
  }
2198
2074
  /**
2199
2075
  * Find overlay match in text using provided options and trigger extractor.
@@ -2210,11 +2086,11 @@ var TriggerFinder = class TriggerFinder {
2210
2086
  * // Other framework usage
2211
2087
  * TriggerFinder.find(vueOptions, (opt) => opt.overlay?.trigger ?? '@')
2212
2088
  */
2213
- static find(options, getTrigger, store) {
2089
+ static find(options, getTrigger, dom) {
2214
2090
  if (!options) return;
2215
- if (!Caret.isSelectedPosition) return;
2091
+ if (!window.getSelection()?.isCollapsed) return;
2216
2092
  try {
2217
- return new TriggerFinder(store).find(options, getTrigger);
2093
+ return new TriggerFinder(dom).find(options, getTrigger);
2218
2094
  } catch {
2219
2095
  return;
2220
2096
  }
@@ -2252,11 +2128,11 @@ var TriggerFinder = class TriggerFinder {
2252
2128
  }
2253
2129
  }
2254
2130
  #rawRangeForMatch(source, index) {
2255
- if (!this.store) return {
2131
+ if (!this.dom) return {
2256
2132
  start: index,
2257
2133
  end: index + source.length
2258
2134
  };
2259
- const boundary = this.store.dom.rawPositionFromBoundary(this.node, index + source.length, "after");
2135
+ const boundary = this.dom.rawPositionFromBoundary(this.node, index + source.length, "after");
2260
2136
  if (!boundary.ok) return void 0;
2261
2137
  return {
2262
2138
  start: boundary.value - source.length,
@@ -2294,6 +2170,86 @@ var TriggerFinder = class TriggerFinder {
2294
2170
  }
2295
2171
  };
2296
2172
  //#endregion
2173
+ //#region ../../core/src/features/caret/caretDom.ts
2174
+ function getCaretIndex(element) {
2175
+ let position = 0;
2176
+ const selection = window.getSelection();
2177
+ if (!selection?.rangeCount) return position;
2178
+ const range = selection.getRangeAt(0);
2179
+ const preCaretRange = range.cloneRange();
2180
+ preCaretRange.selectNodeContents(element);
2181
+ preCaretRange.setEnd(range.endContainer, range.endOffset);
2182
+ position = preCaretRange.toString().length;
2183
+ return position;
2184
+ }
2185
+ function getRect() {
2186
+ try {
2187
+ return (window.getSelection()?.getRangeAt(0))?.getBoundingClientRect() ?? null;
2188
+ } catch {
2189
+ return null;
2190
+ }
2191
+ }
2192
+ function isOnFirstLine(element) {
2193
+ const caretRect = getRect();
2194
+ if (!caretRect || caretRect.height === 0) return true;
2195
+ const elRect = element.getBoundingClientRect();
2196
+ return caretRect.top < elRect.top + caretRect.height + 2;
2197
+ }
2198
+ function isOnLastLine(element) {
2199
+ const caretRect = getRect();
2200
+ if (!caretRect || caretRect.height === 0) return true;
2201
+ const elRect = element.getBoundingClientRect();
2202
+ return caretRect.bottom > elRect.bottom - caretRect.height - 2;
2203
+ }
2204
+ function setAtElement(element, offset) {
2205
+ try {
2206
+ const selection = window.getSelection();
2207
+ if (!selection) return;
2208
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
2209
+ let node = nextText(walker);
2210
+ if (!node) return;
2211
+ let remaining = isFinite(offset) ? Math.max(0, offset) : Infinity;
2212
+ for (;;) {
2213
+ const next = nextText(walker);
2214
+ if (!next || remaining <= node.length) {
2215
+ const charOffset = isFinite(remaining) ? Math.min(remaining, node.length) : node.length;
2216
+ const range = document.createRange();
2217
+ range.setStart(node, charOffset);
2218
+ range.collapse(true);
2219
+ selection.removeAllRanges();
2220
+ selection.addRange(range);
2221
+ return;
2222
+ }
2223
+ remaining -= node.length;
2224
+ node = next;
2225
+ }
2226
+ } catch (e) {
2227
+ console.error(e);
2228
+ }
2229
+ }
2230
+ function setAtX(element, x, y) {
2231
+ const elRect = element.getBoundingClientRect();
2232
+ const targetY = y ?? elRect.top + elRect.height / 2;
2233
+ const caretDoc = document;
2234
+ const caretPos = caretDoc.caretRangeFromPoint?.(x, targetY) ?? caretDoc.caretPositionFromPoint?.(x, targetY);
2235
+ if (!caretPos) return;
2236
+ const sel = window.getSelection();
2237
+ if (!sel) return;
2238
+ let domRange;
2239
+ if (caretPos instanceof Range) domRange = caretPos;
2240
+ else if ("offsetNode" in caretPos) {
2241
+ domRange = document.createRange();
2242
+ domRange.setStart(caretPos.offsetNode, caretPos.offset);
2243
+ domRange.collapse(true);
2244
+ } else return;
2245
+ if (!element.contains(domRange.startContainer)) {
2246
+ setAtElement(element, Infinity);
2247
+ return;
2248
+ }
2249
+ sel.removeAllRanges();
2250
+ sel.addRange(domRange);
2251
+ }
2252
+ //#endregion
2297
2253
  //#region ../../core/src/features/clipboard/pasteMarkup.ts
2298
2254
  /** Custom MIME type for markput markup syntax. */
2299
2255
  const MARKPUT_MIME = "application/x-markput";
@@ -2321,7 +2277,7 @@ function consumeMarkupPaste(container) {
2321
2277
  return markup;
2322
2278
  }
2323
2279
  //#endregion
2324
- //#region ../../core/src/features/clipboard/ClipboardFeature.ts
2280
+ //#region ../../core/src/features/clipboard/ClipboardController.ts
2325
2281
  function htmlFromRange(range) {
2326
2282
  const fragment = range.cloneContents();
2327
2283
  const div = document.createElement("div");
@@ -2342,47 +2298,36 @@ function trimTokensForRawRange(tokens, range) {
2342
2298
  return Object.assign({}, token, { children: trimTokensForRawRange(token.children, range) });
2343
2299
  });
2344
2300
  }
2345
- var ClipboardFeature = class {
2346
- #scope;
2347
- constructor(store) {
2348
- this.store = store;
2349
- }
2350
- enable() {
2351
- if (this.#scope) return;
2352
- const container = this.store.dom.container();
2353
- if (!container) return;
2354
- this.#scope = effectScope(() => {
2301
+ var ClipboardController = class {
2302
+ constructor(lifecycle, edit, dom, parsing) {
2303
+ this.lifecycle = lifecycle;
2304
+ this.edit = edit;
2305
+ this.dom = dom;
2306
+ this.parsing = parsing;
2307
+ lifecycle.onMounted(() => {
2308
+ const container = dom.container();
2309
+ if (!container) return;
2355
2310
  listen(container, "copy", (e) => {
2356
2311
  this.#handleCopy(e);
2357
2312
  });
2358
2313
  listen(container, "cut", (e) => {
2359
2314
  if (!this.#handleCopy(e)) return;
2360
- const raw = this.store.dom.readRawSelection();
2315
+ const raw = dom.readRawSelection();
2361
2316
  if (!raw.ok || raw.value.range.start === raw.value.range.end) return;
2362
- this.store.value.replaceRange(raw.value.range, "", {
2363
- source: "cut",
2364
- recover: {
2365
- kind: "caret",
2366
- rawPosition: raw.value.range.start
2367
- }
2368
- });
2317
+ edit.replace(raw.value.range, "");
2369
2318
  });
2370
2319
  });
2371
2320
  }
2372
- disable() {
2373
- this.#scope?.();
2374
- this.#scope = void 0;
2375
- }
2376
2321
  #handleCopy(e) {
2377
- if (!this.store.dom.container()) return false;
2378
- const raw = this.store.dom.readRawSelection();
2322
+ if (!this.dom.container()) return false;
2323
+ const raw = this.dom.readRawSelection();
2379
2324
  if (!raw.ok || raw.value.range.start === raw.value.range.end) return false;
2380
2325
  const sel = window.getSelection();
2381
2326
  const range = sel?.rangeCount ? sel.getRangeAt(0) : void 0;
2382
2327
  if (!range) return false;
2383
2328
  const plainText = range.toString();
2384
2329
  const html = htmlFromRange(range);
2385
- const markup = serializeRawRange(this.store.parsing.tokens(), raw.value.range);
2330
+ const markup = serializeRawRange(this.parsing.tokens(), raw.value.range);
2386
2331
  e.preventDefault();
2387
2332
  e.clipboardData?.setData("text/plain", plainText);
2388
2333
  e.clipboardData?.setData("text/html", html);
@@ -2391,7 +2336,7 @@ var ClipboardFeature = class {
2391
2336
  }
2392
2337
  };
2393
2338
  //#endregion
2394
- //#region ../../core/src/features/dom/DomFeature.ts
2339
+ //#region ../../core/src/features/dom/DomController.ts
2395
2340
  function nextTextNode(walker) {
2396
2341
  const node = walker.nextNode();
2397
2342
  return node instanceof Text ? node : null;
@@ -2451,7 +2396,7 @@ function hasEditableAncestorBefore(node, boundary) {
2451
2396
  }
2452
2397
  return false;
2453
2398
  }
2454
- var DomFeature = class {
2399
+ var DomController = class {
2455
2400
  #domIndex = signal(void 0, { readonly: true });
2456
2401
  #pendingControls = /* @__PURE__ */ new Map();
2457
2402
  #pendingChildSequences = /* @__PURE__ */ new Map();
@@ -2463,29 +2408,31 @@ var DomFeature = class {
2463
2408
  #rendering = false;
2464
2409
  #isComposing = false;
2465
2410
  #queuedRender = false;
2466
- #scope;
2467
- constructor(_store) {
2468
- this._store = _store;
2411
+ constructor(lifecycle, props, parsing, value) {
2412
+ this.lifecycle = lifecycle;
2413
+ this.props = props;
2414
+ this.parsing = parsing;
2415
+ this.value = value;
2469
2416
  this.index = computed(() => this.#domIndex());
2470
2417
  this.container = signal(null);
2471
2418
  this.diagnostics = event();
2472
- }
2473
- enable() {
2474
- if (this.#scope) return;
2475
- this.#scope = effectScope(() => {
2476
- watch(this._store.lifecycle.rendered, () => {
2419
+ this.indexed = event();
2420
+ this.readOnly = computed(() => this.props.readOnly());
2421
+ lifecycle.onMounted(() => {
2422
+ const container = this.container();
2423
+ if (container) listen(container, "click", () => {
2424
+ const tokens = this.parsing.tokens();
2425
+ if (tokens.length === 1 && tokens[0].type === "text" && tokens[0].content === "") {
2426
+ const c = this.container();
2427
+ (c ? firstHtmlChild(c) : null)?.focus();
2428
+ }
2429
+ });
2430
+ watch(lifecycle.rendered, () => {
2477
2431
  this.#handleRendered();
2478
2432
  });
2479
- watch(computed(() => ({
2480
- readOnly: this._store.props.readOnly(),
2481
- selecting: this._store.caret.selecting()
2482
- })), () => this.reconcile());
2433
+ watch(computed(() => props.readOnly()), () => this.reconcile());
2483
2434
  });
2484
2435
  }
2485
- disable() {
2486
- this.#scope?.();
2487
- this.#scope = void 0;
2488
- }
2489
2436
  compositionStarted() {
2490
2437
  this.#isComposing = true;
2491
2438
  }
@@ -2515,8 +2462,8 @@ var DomFeature = class {
2515
2462
  };
2516
2463
  return callback;
2517
2464
  }
2518
- reconcile() {
2519
- this.#reconcileStructuralTextSurfaces();
2465
+ reconcile(opts) {
2466
+ this.#reconcileStructuralTextSurfaces(opts?.isUserSelecting);
2520
2467
  }
2521
2468
  locateNode(node) {
2522
2469
  if (!this.index()) return {
@@ -2561,18 +2508,43 @@ var DomFeature = class {
2561
2508
  reason: "outsideEditor"
2562
2509
  };
2563
2510
  }
2564
- placeCaretAtRawPosition(rawPosition, affinity = "after") {
2511
+ placeAt(rawPosition, affinity = "after") {
2565
2512
  if (!this.index()) return {
2566
2513
  ok: false,
2567
2514
  reason: "notIndexed"
2568
2515
  };
2569
- const target = this.#findTextTargetForRawPosition(rawPosition, affinity);
2570
- if (!target) return this.#focusMarkBoundaryForRawPosition(rawPosition);
2516
+ const maxPos = this.value.current().length;
2517
+ const clamped = Math.min(rawPosition, maxPos);
2518
+ const target = this.#findTextTargetForRawPosition(clamped, affinity);
2519
+ if (!target) {
2520
+ const boundary = this.#focusMarkBoundaryForRawPosition(clamped);
2521
+ if (!boundary.ok) return boundary;
2522
+ return {
2523
+ ok: true,
2524
+ value: { applied: clamped }
2525
+ };
2526
+ }
2571
2527
  target.element.focus();
2572
- this.#placeCaretInTextSurface(target.element, rawPosition - target.start);
2528
+ this.#placeCaretInTextSurface(target.element, clamped - target.start);
2573
2529
  return {
2574
2530
  ok: true,
2575
- value: void 0
2531
+ value: { applied: clamped }
2532
+ };
2533
+ }
2534
+ placeRange(range) {
2535
+ const maxPos = this.value.current().length;
2536
+ const clamped = {
2537
+ start: Math.min(range.start, maxPos),
2538
+ end: Math.min(range.end, maxPos)
2539
+ };
2540
+ const result = this.#placeSelection({
2541
+ range: clamped,
2542
+ direction: void 0
2543
+ });
2544
+ if (!result.ok) return result;
2545
+ return {
2546
+ ok: true,
2547
+ value: { applied: clamped }
2576
2548
  };
2577
2549
  }
2578
2550
  focusAddress(address, boundary = "start") {
@@ -2580,7 +2552,7 @@ var DomFeature = class {
2580
2552
  ok: false,
2581
2553
  reason: "notIndexed"
2582
2554
  };
2583
- if (!this._store.parsing.index().resolveAddress(address).ok) return {
2555
+ if (!this.parsing.index().resolveAddress(address).ok) return {
2584
2556
  ok: false,
2585
2557
  reason: "stale"
2586
2558
  };
@@ -2591,12 +2563,7 @@ var DomFeature = class {
2591
2563
  reason: "notIndexed"
2592
2564
  };
2593
2565
  target.focus();
2594
- const role = target === elements?.textElement ? "text" : target === elements?.rowElement ? "row" : "markDescendant";
2595
- if (role === "markDescendant") this.#placeCollapsedBoundary(target, boundary === "end" ? target.childNodes.length : 0);
2596
- this._store.caret.location({
2597
- address,
2598
- role
2599
- });
2566
+ if ((target === elements?.textElement ? "text" : target === elements?.rowElement ? "row" : "markDescendant") === "markDescendant") this.#placeCollapsedBoundary(target, boundary === "end" ? target.childNodes.length : 0);
2600
2567
  return {
2601
2568
  ok: true,
2602
2569
  value: void 0
@@ -2618,7 +2585,7 @@ var DomFeature = class {
2618
2585
  ok: false,
2619
2586
  reason: "control"
2620
2587
  } : location;
2621
- const token = this._store.parsing.index().resolveAddress(location.value.address);
2588
+ const token = this.parsing.index().resolveAddress(location.value.address);
2622
2589
  if (!token.ok) return {
2623
2590
  ok: false,
2624
2591
  reason: "notIndexed"
@@ -2751,7 +2718,7 @@ var DomFeature = class {
2751
2718
  });
2752
2719
  return;
2753
2720
  }
2754
- const tokenIndex = this._store.parsing.index();
2721
+ const tokenIndex = this.parsing.index();
2755
2722
  const pathElements = /* @__PURE__ */ new Map();
2756
2723
  const elementRoles = /* @__PURE__ */ new WeakMap();
2757
2724
  const controlElements = /* @__PURE__ */ new Set();
@@ -2759,15 +2726,14 @@ var DomFeature = class {
2759
2726
  controlElements.add(element);
2760
2727
  elementRoles.set(element, { role: "control" });
2761
2728
  }
2762
- const tokens = this._store.parsing.tokens();
2763
- if (this._store.props.layout() === "block") this.#indexBlockTokens(container, tokens, tokenIndex, controlElements, pathElements, elementRoles);
2729
+ const tokens = this.parsing.tokens();
2730
+ if (this.props.layout() === "block") this.#indexBlockTokens(container, tokens, tokenIndex, controlElements, pathElements, elementRoles);
2764
2731
  else this.#indexTokenSequence(container, tokens, [], void 0, tokenIndex, controlElements, pathElements, elementRoles);
2765
2732
  this.#pathElements = pathElements;
2766
2733
  this.#elementRoles = elementRoles;
2767
2734
  this.#reconcileStructuralTextSurfaces();
2768
2735
  batch(() => this.#domIndex({ generation: ++this.#generation }), { mutable: true });
2769
- this.#clearStaleCaretLocation();
2770
- this.#applyPendingRecovery();
2736
+ this.indexed();
2771
2737
  }
2772
2738
  #elementChildren(element) {
2773
2739
  return Array.from(element.children).filter((child) => child instanceof HTMLElement);
@@ -2881,9 +2847,9 @@ var DomFeature = class {
2881
2847
  });
2882
2848
  this.#indexNestedTokenSequence(token, path, address, element, rowElement, tokenIndex, controlElements, pathElements, elementRoles);
2883
2849
  }
2884
- #reconcileStructuralTextSurfaces() {
2885
- const tokenIndex = this._store.parsing.index();
2886
- const editable = this._store.props.readOnly() || this._store.caret.selecting() ? "false" : "true";
2850
+ #reconcileStructuralTextSurfaces(isUserSelecting) {
2851
+ const tokenIndex = this.parsing.index();
2852
+ const editable = this.props.readOnly() || isUserSelecting ? "false" : "true";
2887
2853
  for (const record of this.#pathElements.values()) {
2888
2854
  const resolved = tokenIndex.resolveAddress(record.address);
2889
2855
  if (!resolved.ok) {
@@ -2907,12 +2873,12 @@ var DomFeature = class {
2907
2873
  record.textElement.contentEditable = editable;
2908
2874
  continue;
2909
2875
  }
2910
- if (resolved.value.type === "mark") if (this._store.props.readOnly()) record.tokenElement.removeAttribute("tabindex");
2876
+ if (resolved.value.type === "mark") if (this.props.readOnly()) record.tokenElement.removeAttribute("tabindex");
2911
2877
  else record.tokenElement.tabIndex = 0;
2912
2878
  }
2913
2879
  }
2914
2880
  #rawPositionFromContainerBoundary(offset, affinity) {
2915
- const tokens = this._store.parsing.tokens();
2881
+ const tokens = this.parsing.tokens();
2916
2882
  if (tokens.length === 0) return {
2917
2883
  ok: true,
2918
2884
  value: 0
@@ -2934,7 +2900,7 @@ var DomFeature = class {
2934
2900
  }
2935
2901
  #rawPositionFromTokenChildBoundary(tokenElement, offset, token, affinity) {
2936
2902
  if (token.type === "text") {
2937
- const textElement = this.#pathElements.get(pathKey(this._store.parsing.index().pathFor(token) ?? []))?.textElement;
2903
+ const textElement = this.#pathElements.get(pathKey(this.parsing.index().pathFor(token) ?? []))?.textElement;
2938
2904
  if (!textElement || textLength(textElement) === 0) return {
2939
2905
  ok: true,
2940
2906
  value: token.position.start
@@ -2943,8 +2909,8 @@ var DomFeature = class {
2943
2909
  const before = this.#locateRegisteredDescendant(tokenElement.childNodes.item(offset - 1));
2944
2910
  const after = this.#locateRegisteredDescendant(tokenElement.childNodes.item(offset));
2945
2911
  if (before?.ok && after?.ok) {
2946
- const beforeToken = this._store.parsing.index().resolveAddress(before.value.address);
2947
- const afterToken = this._store.parsing.index().resolveAddress(after.value.address);
2912
+ const beforeToken = this.parsing.index().resolveAddress(before.value.address);
2913
+ const afterToken = this.parsing.index().resolveAddress(after.value.address);
2948
2914
  if (beforeToken.ok && afterToken.ok) return {
2949
2915
  ok: true,
2950
2916
  value: affinity === "before" ? beforeToken.value.position.end : afterToken.value.position.start
@@ -2961,7 +2927,7 @@ var DomFeature = class {
2961
2927
  }
2962
2928
  #findTextTargetForRawPosition(rawPosition, affinity) {
2963
2929
  const candidates = [];
2964
- const tokenIndex = this._store.parsing.index();
2930
+ const tokenIndex = this.parsing.index();
2965
2931
  for (const record of this.#pathElements.values()) {
2966
2932
  if (!record.textElement) continue;
2967
2933
  const resolved = tokenIndex.resolveAddress(record.address);
@@ -2979,7 +2945,7 @@ var DomFeature = class {
2979
2945
  return candidates.find((candidate) => candidate.start >= rawPosition);
2980
2946
  }
2981
2947
  #focusMarkBoundaryForRawPosition(rawPosition) {
2982
- const tokenIndex = this._store.parsing.index();
2948
+ const tokenIndex = this.parsing.index();
2983
2949
  for (const record of this.#pathElements.values()) {
2984
2950
  const resolved = tokenIndex.resolveAddress(record.address);
2985
2951
  if (!resolved.ok || resolved.value.type !== "mark") continue;
@@ -2987,10 +2953,6 @@ var DomFeature = class {
2987
2953
  const boundary = rawPosition === resolved.value.position.end ? "end" : "start";
2988
2954
  record.tokenElement.focus();
2989
2955
  this.#placeCollapsedBoundary(record.tokenElement, boundary === "end" ? record.tokenElement.childNodes.length : 0);
2990
- this._store.caret.location({
2991
- address: record.address,
2992
- role: "markDescendant"
2993
- });
2994
2956
  return {
2995
2957
  ok: true,
2996
2958
  value: void 0
@@ -3021,25 +2983,6 @@ var DomFeature = class {
3021
2983
  selection.removeAllRanges();
3022
2984
  selection.addRange(range);
3023
2985
  }
3024
- #applyPendingRecovery() {
3025
- const recovery = this._store.caret.recovery();
3026
- if (!recovery) return;
3027
- if (recovery.kind === "caret") {
3028
- const result = this._store.caret.placeAt(recovery.rawPosition, recovery.affinity);
3029
- this._store.caret.recovery(void 0);
3030
- if (!result.ok) this.diagnostics({
3031
- kind: "recoveryFailed",
3032
- reason: `pending caret recovery could not be applied: ${result.reason}`
3033
- });
3034
- return;
3035
- }
3036
- const result = this.#placeSelection(recovery.selection);
3037
- this._store.caret.recovery(void 0);
3038
- if (!result.ok) this.diagnostics({
3039
- kind: "recoveryFailed",
3040
- reason: `pending selection recovery could not be applied: ${result.reason}`
3041
- });
3042
- }
3043
2986
  #placeSelection(selection) {
3044
2987
  const start = this.#findTextTargetForRawPosition(selection.range.start, "after");
3045
2988
  const end = this.#findTextTargetForRawPosition(selection.range.end, "before");
@@ -3083,11 +3026,6 @@ var DomFeature = class {
3083
3026
  offset: text.length
3084
3027
  };
3085
3028
  }
3086
- #clearStaleCaretLocation() {
3087
- const location = this._store.caret.location();
3088
- if (!location) return;
3089
- if (!this._store.parsing.index().resolveAddress(location.address).ok || !this.#pathElements.has(pathKey(location.address.path))) this._store.caret.location(void 0);
3090
- }
3091
3029
  };
3092
3030
  //#endregion
3093
3031
  //#region ../../core/src/features/editing/createRowContent.ts
@@ -3198,103 +3136,95 @@ const EMPTY_TEXT_TOKEN = {
3198
3136
  }
3199
3137
  };
3200
3138
  //#endregion
3201
- //#region ../../core/src/features/drag/DragFeature.ts
3202
- var DragFeature = class {
3203
- constructor(store) {
3204
- this.store = store;
3205
- this.action = event();
3206
- }
3139
+ //#region ../../core/src/features/drag/DragController.ts
3140
+ var DragController = class {
3207
3141
  #unsub;
3208
- enable() {
3209
- if (this.#unsub) return;
3210
- this.#unsub = watch(this.action, (action) => {
3211
- switch (action.type) {
3212
- case "reorder":
3213
- this.#reorder(action);
3214
- break;
3215
- case "add":
3216
- this.#add(action);
3217
- break;
3218
- case "delete":
3219
- this.#delete(action);
3220
- break;
3221
- case "duplicate":
3222
- this.#duplicate(action);
3223
- break;
3142
+ constructor(props, value, parsing, caret) {
3143
+ this.props = props;
3144
+ this.value = value;
3145
+ this.parsing = parsing;
3146
+ this.caret = caret;
3147
+ this.action = event();
3148
+ const isDragEnabled = computed(() => this.props.layout() === "block" && !!this.props.draggable());
3149
+ const toggle = (enabled) => {
3150
+ if (enabled && !this.#unsub) this.#unsub = watch(this.action, (action) => {
3151
+ switch (action.type) {
3152
+ case "reorder":
3153
+ this.#reorder(action);
3154
+ break;
3155
+ case "add":
3156
+ this.#add(action);
3157
+ break;
3158
+ case "delete":
3159
+ this.#delete(action);
3160
+ break;
3161
+ case "duplicate":
3162
+ this.#duplicate(action);
3163
+ break;
3164
+ }
3165
+ });
3166
+ if (!enabled && this.#unsub) {
3167
+ this.#unsub();
3168
+ this.#unsub = void 0;
3224
3169
  }
3225
- });
3226
- }
3227
- disable() {
3228
- this.#unsub?.();
3229
- this.#unsub = void 0;
3170
+ };
3171
+ watch(isDragEnabled, toggle);
3172
+ toggle(isDragEnabled());
3230
3173
  }
3231
3174
  #reorder(action) {
3232
- const value = this.store.value.current();
3233
- const rows = this.store.parsing.tokens();
3175
+ const value = this.value.current();
3176
+ const rows = this.parsing.tokens();
3234
3177
  const newValue = reorderDragRows(value, rows, action.source, action.target);
3235
- if (newValue !== value) this.store.value.replaceAll(newValue, {
3236
- source: "drag",
3237
- recover: this.#recoverAfterDrag(action, rows, newValue)
3238
- });
3178
+ if (newValue !== value) {
3179
+ const range = this.#rangeAfterDrag(action, rows, newValue);
3180
+ if (range) this.caret.selection(range);
3181
+ this.value.current(newValue);
3182
+ }
3239
3183
  }
3240
3184
  #add(action) {
3241
- const value = this.store.value.current();
3242
- const rawRows = this.store.parsing.tokens();
3185
+ const value = this.value.current();
3186
+ const rawRows = this.parsing.tokens();
3243
3187
  const rows = rawRows.length > 0 ? rawRows : [EMPTY_TEXT_TOKEN];
3244
- const newRowContent = createRowContent(this.store.props.options());
3188
+ const newRowContent = createRowContent(this.props.options());
3245
3189
  const newValue = addDragRow(value, rows, action.afterIndex, newRowContent);
3246
- this.store.value.replaceAll(newValue, {
3247
- source: "drag",
3248
- recover: this.#recoverAfterDrag(action, rows, newValue)
3249
- });
3190
+ const range = this.#rangeAfterDrag(action, rows, newValue);
3191
+ if (range) this.caret.selection(range);
3192
+ this.value.current(newValue);
3250
3193
  }
3251
3194
  #delete(action) {
3252
- const value = this.store.value.current();
3253
- const rows = this.store.parsing.tokens();
3195
+ const value = this.value.current();
3196
+ const rows = this.parsing.tokens();
3254
3197
  const newValue = deleteDragRow(value, rows, action.index);
3255
- this.store.value.replaceAll(newValue, {
3256
- source: "drag",
3257
- recover: this.#recoverAfterDrag(action, rows, newValue)
3258
- });
3198
+ const range = this.#rangeAfterDrag(action, rows, newValue);
3199
+ if (range) this.caret.selection(range);
3200
+ this.value.current(newValue);
3259
3201
  }
3260
3202
  #duplicate(action) {
3261
- const value = this.store.value.current();
3262
- const rows = this.store.parsing.tokens();
3203
+ const value = this.value.current();
3204
+ const rows = this.parsing.tokens();
3263
3205
  const newValue = duplicateDragRow(value, rows, action.index);
3264
- this.store.value.replaceAll(newValue, {
3265
- source: "drag",
3266
- recover: this.#recoverAfterDrag(action, rows, newValue)
3267
- });
3206
+ const range = this.#rangeAfterDrag(action, rows, newValue);
3207
+ if (range) this.caret.selection(range);
3208
+ this.value.current(newValue);
3268
3209
  }
3269
- #recoverAfterDrag(action, previousRows, nextValue) {
3210
+ #rangeAfterDrag(action, previousRows, nextValue) {
3211
+ let rawPosition;
3270
3212
  if (action.type === "add") {
3271
3213
  const after = previousRows.at(action.afterIndex);
3272
- return {
3273
- kind: "caret",
3274
- rawPosition: after ? after.position.end : nextValue.length
3275
- };
3276
- }
3277
- if (action.type === "duplicate") {
3214
+ rawPosition = after ? after.position.end : nextValue.length;
3215
+ } else if (action.type === "duplicate") {
3278
3216
  const row = previousRows.at(action.index);
3279
- return row ? {
3280
- kind: "caret",
3281
- rawPosition: row.position.end
3282
- } : void 0;
3283
- }
3284
- if (action.type === "delete") {
3217
+ rawPosition = row ? row.position.end : void 0;
3218
+ } else if (action.type === "delete") {
3285
3219
  const next = previousRows.at(action.index + 1) ?? (action.index > 0 ? previousRows.at(action.index - 1) : void 0);
3286
- return next ? {
3287
- kind: "caret",
3288
- rawPosition: Math.min(next.position.start, nextValue.length)
3289
- } : {
3290
- kind: "caret",
3291
- rawPosition: 0
3292
- };
3220
+ rawPosition = next ? Math.min(next.position.start, nextValue.length) : 0;
3221
+ } else {
3222
+ const moved = previousRows.at(action.source);
3223
+ rawPosition = moved ? Math.min(moved.position.start, nextValue.length) : void 0;
3293
3224
  }
3294
- const moved = previousRows.at(action.source);
3295
- return moved ? {
3296
- kind: "caret",
3297
- rawPosition: Math.min(moved.position.start, nextValue.length)
3225
+ return rawPosition !== void 0 ? {
3226
+ start: rawPosition,
3227
+ end: rawPosition
3298
3228
  } : void 0;
3299
3229
  }
3300
3230
  };
@@ -3316,26 +3246,49 @@ function getAlwaysShowHandle(draggable) {
3316
3246
  return typeof draggable === "object" && !!draggable.alwaysShowHandle;
3317
3247
  }
3318
3248
  //#endregion
3249
+ //#region ../../core/src/features/edit/EditController.ts
3250
+ /**
3251
+ * Single write path for text edits — delegates gating to {@link ValueModel.replace}
3252
+ * and only moves the caret when the edit is accepted. Wrapped in {@link batch}
3253
+ * so subscribers observe a consistent value/selection pair on one tick.
3254
+ */
3255
+ var EditController = class {
3256
+ constructor(value, caret) {
3257
+ this.value = value;
3258
+ this.caret = caret;
3259
+ }
3260
+ replace(range, replacement) {
3261
+ batch(() => {
3262
+ if (!this.value.replace(range, replacement)) return;
3263
+ this.caret.position(range.start + replacement.length);
3264
+ });
3265
+ }
3266
+ };
3267
+ //#endregion
3319
3268
  //#region ../../core/src/features/keyboard/arrowNav.ts
3320
3269
  function enableArrowNav(store) {
3321
3270
  const container = store.dom.container();
3322
- if (!container) return () => {};
3323
- const scope = effectScope(() => {
3324
- listen(container, "keydown", (e) => {
3271
+ if (!container) return;
3272
+ listen(container, "keydown", (e) => {
3273
+ if (store.slots.isBlock()) return;
3274
+ if (e.key === KEYBOARD.LEFT) shiftFocus(store, e, "prev");
3275
+ else if (e.key === KEYBOARD.RIGHT) shiftFocus(store, e, "next");
3276
+ if ((e.ctrlKey || e.metaKey) && e.code === "KeyA") {
3325
3277
  if (store.slots.isBlock()) return;
3326
- if (e.key === KEYBOARD.LEFT) shiftFocus(store, e, "prev");
3327
- else if (e.key === KEYBOARD.RIGHT) shiftFocus(store, e, "next");
3328
- selectAllText(store, e);
3329
- });
3278
+ e.preventDefault();
3279
+ store.caret.selectAll();
3280
+ }
3330
3281
  });
3331
- return () => scope();
3332
3282
  }
3333
3283
  function shiftFocus(store, event, direction) {
3334
- const location = store.caret.location();
3335
- if (!location) return false;
3336
- const token = store.parsing.index().resolveAddress(location.address);
3284
+ const active = document.activeElement instanceof HTMLElement ? document.activeElement : void 0;
3285
+ const located = active ? store.dom.locateNode(active) : void 0;
3286
+ if (!located?.ok) return false;
3287
+ const isFocusedOnMarkElement = active === located.value.tokenElement && !located.value.textElement;
3288
+ const address = located.value.address;
3289
+ const token = store.parsing.index().resolveAddress(address);
3337
3290
  if (!token.ok) return false;
3338
- if (!(token.value.type === "mark" && location.role !== "text")) {
3291
+ if (!isFocusedOnMarkElement) {
3339
3292
  const selection = store.dom.readRawSelection();
3340
3293
  if (!selection.ok || selection.value.range.start !== selection.value.range.end) return false;
3341
3294
  const atStart = selection.value.range.start <= token.value.position.start;
@@ -3343,20 +3296,20 @@ function shiftFocus(store, event, direction) {
3343
3296
  if (direction === "prev" && !atStart) return false;
3344
3297
  if (direction === "next" && !atEnd) return false;
3345
3298
  }
3346
- const path = location.address.path;
3299
+ const path = address.path;
3347
3300
  const siblingIndex = direction === "prev" ? path[path.length - 1] - 1 : path[path.length - 1] + 1;
3348
3301
  const siblingPath = [...path.slice(0, -1), siblingIndex];
3349
3302
  const siblingAddress = store.parsing.index().addressFor(siblingPath);
3350
3303
  if (!siblingAddress) return false;
3351
3304
  event.preventDefault();
3352
- if (!store.caret.focus(siblingAddress, direction === "prev" ? "end" : "start").ok) return false;
3305
+ if (!store.dom.focusAddress(siblingAddress, direction === "prev" ? "end" : "start").ok) return false;
3353
3306
  const sibling = store.parsing.index().resolve(siblingPath);
3354
3307
  if (sibling?.type === "mark") return true;
3355
3308
  if (direction === "prev") {
3356
- store.caret.placeAt(sibling?.position.end ?? 0, "before");
3309
+ store.dom.placeAt(sibling?.position.end ?? 0, "before");
3357
3310
  return true;
3358
3311
  }
3359
- store.caret.placeAt(sibling?.position.start ?? 0, "after");
3312
+ store.dom.placeAt(sibling?.position.start ?? 0, "after");
3360
3313
  return true;
3361
3314
  }
3362
3315
  //#endregion
@@ -3367,22 +3320,19 @@ function isTextLikeRow(token) {
3367
3320
  }
3368
3321
  function enableBlockEdit(store) {
3369
3322
  const container = store.dom.container();
3370
- if (!container) return () => {};
3371
- const scope = effectScope(() => {
3372
- listen(container, "keydown", (e) => {
3373
- if (!store.slots.isBlock()) return;
3374
- if (e.key === KEYBOARD.LEFT || e.key === KEYBOARD.RIGHT) handleBlockArrowLeftRight(store, e, e.key === KEYBOARD.LEFT ? "left" : "right");
3375
- else if (e.key === KEYBOARD.UP || e.key === KEYBOARD.DOWN) handleArrowUpDown(store, e);
3376
- handleDelete(store, e);
3377
- handleEnter(store, e);
3378
- });
3379
- listen(container, "beforeinput", (e) => {
3380
- if (!store.slots.isBlock()) return;
3381
- if (e.defaultPrevented) return;
3382
- handleBlockBeforeInput(store, e);
3383
- }, true);
3323
+ if (!container) return;
3324
+ listen(container, "keydown", (e) => {
3325
+ if (!store.slots.isBlock()) return;
3326
+ if (e.key === KEYBOARD.LEFT || e.key === KEYBOARD.RIGHT) handleBlockArrowLeftRight(store, e, e.key === KEYBOARD.LEFT ? "left" : "right");
3327
+ else if (e.key === KEYBOARD.UP || e.key === KEYBOARD.DOWN) handleArrowUpDown(store, e);
3328
+ handleDelete(store, e);
3329
+ handleEnter(store, e);
3384
3330
  });
3385
- return () => scope();
3331
+ listen(container, "beforeinput", (e) => {
3332
+ if (!store.slots.isBlock()) return;
3333
+ if (e.defaultPrevented) return;
3334
+ handleBlockBeforeInput(store, e);
3335
+ }, true);
3386
3336
  }
3387
3337
  function handleDelete(store, event) {
3388
3338
  const container = store.dom.container();
@@ -3396,7 +3346,7 @@ function handleDelete(store, event) {
3396
3346
  const value = store.value.current();
3397
3347
  if (event.key === KEYBOARD.BACKSPACE) {
3398
3348
  const blockDiv = blockDivs[blockIndex];
3399
- const caretAtStart = Caret.getCaretIndex(blockDiv) === 0;
3349
+ const caretAtStart = getCaretIndex(blockDiv) === 0;
3400
3350
  if (("content" in token ? token.content : "") === "") {
3401
3351
  event.preventDefault();
3402
3352
  const newValue = rows.length <= 1 ? "" : (() => {
@@ -3404,13 +3354,9 @@ function handleDelete(store, event) {
3404
3354
  return value.slice(0, rows[blockIndex].position.start) + value.slice(rows[blockIndex + 1].position.start);
3405
3355
  })();
3406
3356
  const previous = rows.at(Math.max(0, blockIndex - 1));
3407
- store.value.replaceAll(newValue, {
3408
- source: "block",
3409
- recover: {
3410
- kind: "caret",
3411
- rawPosition: previous ? previous.position.end : 0
3412
- }
3413
- });
3357
+ const pos = previous ? previous.position.end : 0;
3358
+ store.caret.position(pos);
3359
+ store.value.current(newValue);
3414
3360
  return;
3415
3361
  }
3416
3362
  if (caretAtStart && blockIndex > 0) {
@@ -3420,26 +3366,18 @@ function handleDelete(store, event) {
3420
3366
  event.preventDefault();
3421
3367
  const joinPos = getMergeDragRowJoinPos(rows, blockIndex);
3422
3368
  const newValue = mergeDragRows(value, rows, blockIndex);
3423
- store.value.replaceAll(newValue, {
3424
- source: "block",
3425
- recover: {
3426
- kind: "caret",
3427
- rawPosition: joinPos
3428
- }
3429
- });
3369
+ store.caret.position(joinPos);
3370
+ store.value.current(newValue);
3430
3371
  return;
3431
3372
  }
3432
3373
  event.preventDefault();
3433
- queueMicrotask(() => {
3434
- const target = blockDivs[blockIndex - 1];
3435
- focusRow(store, prevToken, target, "end");
3436
- });
3374
+ focusRow(store, prevToken, blockDivs[blockIndex - 1], "end");
3437
3375
  return;
3438
3376
  }
3439
3377
  }
3440
3378
  if (event.key === KEYBOARD.DELETE) {
3441
3379
  const blockDiv = blockDivs[blockIndex];
3442
- const caretIndex = Caret.getCaretIndex(blockDiv);
3380
+ const caretIndex = getCaretIndex(blockDiv);
3443
3381
  const caretAtEnd = caretIndex === blockDiv.textContent.length;
3444
3382
  if (caretIndex === 0 && blockIndex > 0) {
3445
3383
  const prevToken = rows[blockIndex - 1];
@@ -3448,20 +3386,12 @@ function handleDelete(store, event) {
3448
3386
  event.preventDefault();
3449
3387
  const joinPos = getMergeDragRowJoinPos(rows, blockIndex);
3450
3388
  const newValue = mergeDragRows(value, rows, blockIndex);
3451
- store.value.replaceAll(newValue, {
3452
- source: "block",
3453
- recover: {
3454
- kind: "caret",
3455
- rawPosition: joinPos
3456
- }
3457
- });
3389
+ store.caret.position(joinPos);
3390
+ store.value.current(newValue);
3458
3391
  return;
3459
3392
  }
3460
3393
  event.preventDefault();
3461
- queueMicrotask(() => {
3462
- const target = blockDivs[blockIndex - 1];
3463
- focusRow(store, prevToken, target, "end");
3464
- });
3394
+ focusRow(store, prevToken, blockDivs[blockIndex - 1], "end");
3465
3395
  return;
3466
3396
  }
3467
3397
  if (caretAtEnd && blockIndex < rows.length - 1) {
@@ -3471,20 +3401,12 @@ function handleDelete(store, event) {
3471
3401
  event.preventDefault();
3472
3402
  const joinPos = getMergeDragRowJoinPos(rows, blockIndex + 1);
3473
3403
  const newValue = mergeDragRows(value, rows, blockIndex + 1);
3474
- store.value.replaceAll(newValue, {
3475
- source: "block",
3476
- recover: {
3477
- kind: "caret",
3478
- rawPosition: joinPos
3479
- }
3480
- });
3404
+ store.caret.position(joinPos);
3405
+ store.value.current(newValue);
3481
3406
  return;
3482
3407
  }
3483
3408
  event.preventDefault();
3484
- queueMicrotask(() => {
3485
- const target = blockDivs[blockIndex + 1];
3486
- focusRow(store, nextToken, target, "start");
3487
- });
3409
+ focusRow(store, nextToken, blockDivs[blockIndex + 1], "start");
3488
3410
  return;
3489
3411
  }
3490
3412
  }
@@ -3510,40 +3432,30 @@ function handleEnter(store, event) {
3510
3432
  const newRowContent = createRowContent(store.props.options());
3511
3433
  if (!isTextLikeRow(token)) {
3512
3434
  const newValue = addDragRow(value, rows, blockIndex, newRowContent);
3513
- store.value.replaceAll(newValue, {
3514
- source: "block",
3515
- recover: {
3516
- kind: "caret",
3517
- rawPosition: token.position.end + newRowContent.length
3518
- }
3519
- });
3435
+ const pos = token.position.end + newRowContent.length;
3436
+ store.caret.position(pos);
3437
+ store.value.current(newValue);
3520
3438
  return;
3521
3439
  }
3522
3440
  const raw = store.dom.readRawSelection();
3523
3441
  const absolutePos = raw.ok ? raw.value.range.start : token.position.end;
3524
- store.value.replaceRange({
3442
+ store.edit.replace({
3525
3443
  start: absolutePos,
3526
3444
  end: absolutePos
3527
- }, newRowContent, {
3528
- source: "block",
3529
- recover: {
3530
- kind: "caret",
3531
- rawPosition: absolutePos + newRowContent.length
3532
- }
3533
- });
3445
+ }, newRowContent);
3534
3446
  }
3535
3447
  function focusRow(store, token, row, caret) {
3536
3448
  if (token.type === "mark") {
3537
3449
  const path = store.parsing.index().pathFor(token);
3538
3450
  const address = path ? store.parsing.index().addressFor(path) : void 0;
3539
- if (address && store.caret.focus(address).ok) return;
3451
+ if (address && store.dom.focusAddress(address).ok) return;
3540
3452
  }
3541
3453
  row.focus();
3542
3454
  if (caret === "start") {
3543
- Caret.trySetIndex(row, 0);
3455
+ setAtElement(row, 0);
3544
3456
  return;
3545
3457
  }
3546
- Caret.setCaretToEnd(row);
3458
+ setAtElement(row, Infinity);
3547
3459
  }
3548
3460
  function handleBlockArrowLeftRight(store, event, direction) {
3549
3461
  const container = store.dom.container();
@@ -3555,20 +3467,20 @@ function handleBlockArrowLeftRight(store, event, direction) {
3555
3467
  if (blockIndex === -1) return false;
3556
3468
  const blockDiv = blockDivs[blockIndex];
3557
3469
  if (direction === "left") {
3558
- if (Caret.getCaretIndex(blockDiv) !== 0) return false;
3470
+ if (getCaretIndex(blockDiv) !== 0) return false;
3559
3471
  if (blockIndex === 0) return true;
3560
3472
  event.preventDefault();
3561
3473
  const prevBlock = blockDivs[blockIndex - 1];
3562
3474
  prevBlock.focus();
3563
- Caret.setCaretToEnd(prevBlock);
3475
+ setAtElement(prevBlock, Infinity);
3564
3476
  return true;
3565
3477
  }
3566
- if (Caret.getCaretIndex(blockDiv) !== blockDiv.textContent.length) return false;
3478
+ if (getCaretIndex(blockDiv) !== blockDiv.textContent.length) return false;
3567
3479
  if (blockIndex >= blockDivs.length - 1) return true;
3568
3480
  event.preventDefault();
3569
3481
  const nextBlock = blockDivs[blockIndex + 1];
3570
3482
  nextBlock.focus();
3571
- Caret.trySetIndex(nextBlock, 0);
3483
+ setAtElement(nextBlock, 0);
3572
3484
  return true;
3573
3485
  }
3574
3486
  function handleArrowUpDown(store, event) {
@@ -3581,23 +3493,21 @@ function handleArrowUpDown(store, event) {
3581
3493
  if (blockIndex === -1) return;
3582
3494
  const blockDiv = blockDivs[blockIndex];
3583
3495
  if (event.key === KEYBOARD.UP) {
3584
- if (!Caret.isCaretOnFirstLine(blockDiv)) return;
3496
+ if (!isOnFirstLine(blockDiv)) return;
3585
3497
  if (blockIndex === 0) return;
3586
3498
  event.preventDefault();
3587
- const caretX = Caret.getCaretRect()?.left ?? blockDiv.getBoundingClientRect().left;
3499
+ const caretX = getRect()?.left ?? blockDiv.getBoundingClientRect().left;
3588
3500
  const prevBlockDiv = blockDivs[blockIndex - 1];
3589
3501
  prevBlockDiv.focus();
3590
- const prevRect = prevBlockDiv.getBoundingClientRect();
3591
- Caret.setAtX(prevBlockDiv, caretX, prevRect.bottom - 4);
3502
+ setAtX(prevBlockDiv, caretX, prevBlockDiv.getBoundingClientRect().bottom - 4);
3592
3503
  } else if (event.key === KEYBOARD.DOWN) {
3593
- if (!Caret.isCaretOnLastLine(blockDiv)) return;
3504
+ if (!isOnLastLine(blockDiv)) return;
3594
3505
  if (blockIndex >= blockDivs.length - 1) return;
3595
3506
  event.preventDefault();
3596
- const caretX = Caret.getCaretRect()?.left ?? blockDiv.getBoundingClientRect().left;
3507
+ const caretX = getRect()?.left ?? blockDiv.getBoundingClientRect().left;
3597
3508
  const nextBlockDiv = blockDivs[blockIndex + 1];
3598
3509
  nextBlockDiv.focus();
3599
- const nextRect = nextBlockDiv.getBoundingClientRect();
3600
- Caret.setAtX(nextBlockDiv, caretX, nextRect.top + 4);
3510
+ setAtX(nextBlockDiv, caretX, nextBlockDiv.getBoundingClientRect().top + 4);
3601
3511
  }
3602
3512
  }
3603
3513
  function handleBlockBeforeInput(store, event) {
@@ -3632,13 +3542,7 @@ function replaceBlockRange(store, event, replacement) {
3632
3542
  const range = rangeForBlockInput(store, event, raw.value.range);
3633
3543
  if (!range) return;
3634
3544
  event.preventDefault();
3635
- store.value.replaceRange(range, replacement, {
3636
- source: "block",
3637
- recover: {
3638
- kind: "caret",
3639
- rawPosition: range.start + replacement.length
3640
- }
3641
- });
3545
+ store.edit.replace(range, replacement);
3642
3546
  }
3643
3547
  function rawRangeFromInputEvent$1(store, event) {
3644
3548
  const ranges = event.getTargetRanges();
@@ -3688,68 +3592,51 @@ function rangeForBlockInput(store, event, range) {
3688
3592
  //#region ../../core/src/features/keyboard/input.ts
3689
3593
  function enableInput(store) {
3690
3594
  const container = store.dom.container();
3691
- if (!container) return () => {};
3595
+ if (!container) return;
3692
3596
  let compositionRange;
3693
- const scope = effectScope(() => {
3694
- listen(container, "paste", (e) => {
3695
- const c = store.dom.container();
3696
- if (c) captureMarkupPaste(e, c);
3697
- handlePaste(store, e);
3698
- });
3699
- listen(container, "compositionstart", () => {
3700
- const selection = store.dom.readRawSelection();
3701
- compositionRange = selection.ok ? selection.value.range : void 0;
3702
- store.dom.compositionStarted();
3703
- });
3704
- listen(container, "compositionend", (e) => {
3705
- const range = compositionRange;
3706
- compositionRange = void 0;
3707
- store.dom.compositionEnded();
3708
- if (store.slots.isBlock()) return;
3709
- if (!range) return;
3710
- const data = e.data;
3711
- store.value.replaceRange(range, data, {
3712
- source: "input",
3713
- recover: {
3714
- kind: "caret",
3715
- rawPosition: range.start + data.length
3716
- }
3717
- });
3718
- });
3719
- listen(container, "beforeinput", (e) => {
3720
- handleBeforeInput(store, e);
3721
- }, true);
3722
- listen(container, "keydown", (e) => {
3723
- handleDeleteKey(store, e);
3724
- });
3597
+ listen(container, "paste", (e) => {
3598
+ const c = store.dom.container();
3599
+ if (c) captureMarkupPaste(e, c);
3600
+ handlePaste(store, e);
3601
+ });
3602
+ listen(container, "compositionstart", () => {
3603
+ const selection = store.dom.readRawSelection();
3604
+ compositionRange = selection.ok ? selection.value.range : void 0;
3605
+ store.dom.compositionStarted();
3606
+ });
3607
+ listen(container, "compositionend", (e) => {
3608
+ const range = compositionRange;
3609
+ compositionRange = void 0;
3610
+ store.dom.compositionEnded();
3611
+ if (store.slots.isBlock()) return;
3612
+ if (!range) return;
3613
+ const data = e.data;
3614
+ store.edit.replace(range, data);
3615
+ });
3616
+ listen(container, "beforeinput", (e) => {
3617
+ handleBeforeInput(store, e);
3618
+ }, true);
3619
+ listen(container, "keydown", (e) => {
3620
+ handleDeleteKey(store, e);
3725
3621
  });
3726
- return () => scope();
3727
3622
  }
3728
3623
  function handleDeleteKey(store, event) {
3729
3624
  if (store.slots.isBlock()) return;
3730
3625
  if (event.key !== KEYBOARD.BACKSPACE && event.key !== KEYBOARD.DELETE) return;
3731
- if (store.caret.selecting() === "all" && isFullSelection(store)) {
3626
+ if (store.caret.isAllSelected()) {
3732
3627
  event.preventDefault();
3733
3628
  replaceAllContentWith(store, "");
3734
3629
  return;
3735
3630
  }
3736
- if (store.caret.selecting() === "all") store.caret.selecting(void 0);
3737
3631
  const raw = store.dom.readRawSelection();
3738
3632
  if (!raw.ok) return;
3739
3633
  const range = rangeForDelete(store, event.key === KEYBOARD.BACKSPACE ? "deleteContentBackward" : "deleteContentForward", raw.value.range);
3740
3634
  if (!range) return;
3741
3635
  event.preventDefault();
3742
- store.value.replaceRange(range, "", {
3743
- source: "input",
3744
- recover: {
3745
- kind: "caret",
3746
- rawPosition: range.start
3747
- }
3748
- });
3636
+ store.edit.replace(range, "");
3749
3637
  }
3750
3638
  function handleBeforeInput(store, event) {
3751
- const selecting = store.caret.selecting();
3752
- if (selecting === "all" && isFullSelection(store)) {
3639
+ if (store.caret.isAllSelected()) {
3753
3640
  if (event.inputType === "insertFromPaste") {
3754
3641
  event.preventDefault();
3755
3642
  return;
@@ -3758,7 +3645,6 @@ function handleBeforeInput(store, event) {
3758
3645
  replaceAllContentWith(store, event.inputType.startsWith("delete") ? "" : event.data ?? "");
3759
3646
  return;
3760
3647
  }
3761
- if (selecting === "all") store.caret.selecting(void 0);
3762
3648
  if (store.slots.isBlock()) return;
3763
3649
  const raw = rawRangeFromInputEvent(store, event);
3764
3650
  if (!raw.ok) return;
@@ -3767,13 +3653,7 @@ function handleBeforeInput(store, event) {
3767
3653
  const range = rangeForInput(store, event, raw.value.range);
3768
3654
  if (!range) return;
3769
3655
  event.preventDefault();
3770
- store.value.replaceRange(range, replacement, {
3771
- source: "input",
3772
- recover: {
3773
- kind: "caret",
3774
- rawPosition: range.start + replacement.length
3775
- }
3776
- });
3656
+ store.edit.replace(range, replacement);
3777
3657
  }
3778
3658
  function rawRangeFromInputEvent(store, event) {
3779
3659
  const ranges = getTargetRanges(event);
@@ -3843,55 +3723,60 @@ function adjacentMarkRange(tokens, position, backward) {
3843
3723
  }
3844
3724
  }
3845
3725
  function handlePaste(store, event) {
3846
- const selecting = store.caret.selecting();
3847
- if (selecting !== "all" || !isFullSelection(store)) {
3848
- if (selecting === "all") store.caret.selecting(void 0);
3849
- return;
3850
- }
3726
+ if (!store.caret.isAllSelected()) return;
3851
3727
  event.preventDefault();
3852
3728
  const c = store.dom.container();
3853
3729
  replaceAllContentWith(store, (c ? consumeMarkupPaste(c) : void 0) ?? event.clipboardData?.getData("text/plain") ?? "");
3854
3730
  }
3855
3731
  function replaceAllContentWith(store, newContent) {
3856
- store.caret.selecting(void 0);
3857
- store.value.replaceAll(newContent, {
3858
- source: "input",
3859
- recover: {
3860
- kind: "caret",
3861
- rawPosition: newContent.length
3862
- }
3863
- });
3732
+ store.caret.position(newContent.length);
3733
+ store.value.current(newContent);
3864
3734
  }
3865
3735
  //#endregion
3866
- //#region ../../core/src/features/keyboard/KeyboardFeature.ts
3867
- var KeyboardFeature = class {
3868
- #disposers = [];
3869
- constructor(_store) {
3870
- this._store = _store;
3871
- }
3872
- enable() {
3873
- if (this.#disposers.length) return;
3874
- this.#disposers = [
3875
- enableInput(this._store),
3876
- enableBlockEdit(this._store),
3877
- enableArrowNav(this._store)
3878
- ];
3879
- }
3880
- disable() {
3881
- this.#disposers.forEach((d) => d());
3882
- this.#disposers = [];
3736
+ //#region ../../core/src/features/keyboard/KeyboardController.ts
3737
+ var KeyboardController = class {
3738
+ constructor(lifecycle, dom, value, caret, edit, slots, parsing, props) {
3739
+ const ctx = {
3740
+ dom,
3741
+ value,
3742
+ caret,
3743
+ edit,
3744
+ slots,
3745
+ parsing,
3746
+ props
3747
+ };
3748
+ lifecycle.onMounted(() => {
3749
+ enableInput(ctx);
3750
+ enableBlockEdit(ctx);
3751
+ enableArrowNav(ctx);
3752
+ });
3883
3753
  }
3884
3754
  };
3885
3755
  //#endregion
3886
- //#region ../../core/src/features/lifecycle/LifecycleFeature.ts
3887
- var LifecycleFeature = class {
3756
+ //#region ../../core/src/features/lifecycle/Lifecycle.ts
3757
+ var Lifecycle = class {
3888
3758
  constructor() {
3889
3759
  this.mounted = event();
3890
3760
  this.unmounted = event();
3891
3761
  this.rendered = event();
3892
3762
  }
3893
- enable() {}
3894
- disable() {}
3763
+ /**
3764
+ * Run `setup` when the editor is mounted. Any reactive subscription
3765
+ * created inside `setup` (`watch`, `listen`, `effect`, nested
3766
+ * `effectScope`) is automatically disposed on `unmounted` and re-created
3767
+ * on the next `mounted`.
3768
+ */
3769
+ onMounted(setup) {
3770
+ let scope;
3771
+ watch(this.mounted, () => {
3772
+ if (scope) return;
3773
+ scope = effectScope(setup);
3774
+ });
3775
+ watch(this.unmounted, () => {
3776
+ scope?.();
3777
+ scope = void 0;
3778
+ });
3779
+ }
3895
3780
  };
3896
3781
  //#endregion
3897
3782
  //#region ../../core/src/features/mark/MarkController.ts
@@ -3930,13 +3815,13 @@ var MarkController = class MarkController {
3930
3815
  }
3931
3816
  remove() {
3932
3817
  const resolved = this.#resolve();
3933
- if (!resolved.ok) return resolved;
3934
- return this.store.value.replaceRange(resolved.value.position, "", { source: "mark" });
3818
+ if (!resolved) return;
3819
+ this.store.value.replace(resolved.position, "");
3935
3820
  }
3936
3821
  update(patch) {
3937
3822
  const resolved = this.#resolve();
3938
- if (!resolved.ok) return resolved;
3939
- const token = resolved.value;
3823
+ if (!resolved) return;
3824
+ const token = resolved;
3940
3825
  const value = patch.value ?? token.value;
3941
3826
  const meta = patch.meta?.kind === "clear" ? void 0 : patch.meta?.kind === "set" ? patch.meta.value : token.meta;
3942
3827
  const slot = patch.slot?.kind === "clear" ? void 0 : patch.slot?.kind === "set" ? patch.slot.value : token.slot?.content;
@@ -3945,7 +3830,7 @@ var MarkController = class MarkController {
3945
3830
  meta,
3946
3831
  slot
3947
3832
  });
3948
- return this.store.value.replaceRange(token.position, serialized, { source: "mark" });
3833
+ this.store.value.replace(token.position, serialized);
3949
3834
  }
3950
3835
  #serialize(token, fields) {
3951
3836
  return annotate(token.descriptor.markup, {
@@ -3955,19 +3840,10 @@ var MarkController = class MarkController {
3955
3840
  });
3956
3841
  }
3957
3842
  #resolve() {
3958
- if (this.store.props.readOnly()) return {
3959
- ok: false,
3960
- reason: "readOnly"
3961
- };
3843
+ if (this.store.props.readOnly()) return void 0;
3962
3844
  const resolved = this.store.parsing.index().resolveAddress(this.address, this.#shape);
3963
- if (!resolved.ok || resolved.value.type !== "mark") return {
3964
- ok: false,
3965
- reason: "stale"
3966
- };
3967
- return {
3968
- ok: true,
3969
- value: resolved.value
3970
- };
3845
+ if (!resolved.ok || resolved.value.type !== "mark") return void 0;
3846
+ return resolved.value;
3971
3847
  }
3972
3848
  };
3973
3849
  //#endregion
@@ -4050,38 +3926,34 @@ function buildContainerProps(isDraggableBlock, readOnly, className, style, slotP
4050
3926
  };
4051
3927
  }
4052
3928
  var SlotsFeature = class {
4053
- constructor(_store) {
4054
- this._store = _store;
4055
- this.isBlock = computed(() => this._store.props.layout() === "block");
4056
- this.isDraggable = computed(() => !!this._store.props.draggable());
4057
- this.containerComponent = computed(() => resolveSlot("container", this._store.props.slots()));
4058
- this.containerProps = computed(() => buildContainerProps(this.isDraggable() && this.isBlock(), this._store.props.readOnly(), this._store.props.className(), this._store.props.style(), this._store.props.slotProps()), { equals: shallow });
4059
- this.blockComponent = computed(() => resolveSlot("block", this._store.props.slots()));
4060
- this.blockProps = computed(() => resolveSlotProps("block", this._store.props.slotProps()));
4061
- this.spanComponent = computed(() => resolveSlot("span", this._store.props.slots()));
4062
- this.spanProps = computed(() => resolveSlotProps("span", this._store.props.slotProps()));
4063
- }
4064
- enable() {}
4065
- disable() {}
3929
+ constructor(props) {
3930
+ this.props = props;
3931
+ this.isBlock = computed(() => this.props.layout() === "block");
3932
+ this.isDraggable = computed(() => !!this.props.draggable());
3933
+ this.containerComponent = computed(() => resolveSlot("container", this.props.slots()));
3934
+ this.containerProps = computed(() => buildContainerProps(this.isDraggable() && this.isBlock(), this.props.readOnly(), this.props.className(), this.props.style(), this.props.slotProps()), { equals: shallow });
3935
+ this.blockComponent = computed(() => resolveSlot("block", this.props.slots()));
3936
+ this.blockProps = computed(() => resolveSlotProps("block", this.props.slotProps()));
3937
+ this.spanComponent = computed(() => resolveSlot("span", this.props.slots()));
3938
+ this.spanProps = computed(() => resolveSlotProps("span", this.props.slotProps()));
3939
+ }
4066
3940
  };
4067
3941
  //#endregion
4068
3942
  //#region ../../core/src/features/mark/MarkFeature.ts
4069
3943
  var MarkFeature = class {
4070
- constructor(_store) {
4071
- this._store = _store;
3944
+ constructor(props) {
3945
+ this.props = props;
4072
3946
  this.enabled = computed(() => {
4073
- if (this._store.props.Mark()) return true;
4074
- return this._store.props.options().some((opt) => "Mark" in opt && opt.Mark != null);
3947
+ if (this.props.Mark()) return true;
3948
+ return this.props.options().some((opt) => "Mark" in opt && opt.Mark != null);
4075
3949
  });
4076
3950
  this.slot = computed(() => {
4077
- const options = this._store.props.options();
4078
- const Mark = this._store.props.Mark();
4079
- const Span = this._store.props.Span();
3951
+ const options = this.props.options();
3952
+ const Mark = this.props.Mark();
3953
+ const Span = this.props.Span();
4080
3954
  return (token) => resolveMarkSlot(token, options, Mark, Span);
4081
3955
  });
4082
3956
  }
4083
- enable() {}
4084
- disable() {}
4085
3957
  };
4086
3958
  //#endregion
4087
3959
  //#region ../../core/src/features/overlay/filterSuggestions.ts
@@ -4117,32 +3989,105 @@ function createMarkFromOverlay(match, value, meta) {
4117
3989
  };
4118
3990
  }
4119
3991
  //#endregion
4120
- //#region ../../core/src/features/overlay/OverlayFeature.ts
4121
- var OverlayFeature = class {
3992
+ //#region ../../core/src/features/overlay/OverlayController.ts
3993
+ var OverlayController = class {
4122
3994
  #scope;
4123
- constructor(_store) {
4124
- this._store = _store;
3995
+ constructor(lifecycle, props, value, dom, caret, edit, parsing) {
3996
+ this.lifecycle = lifecycle;
3997
+ this.props = props;
3998
+ this.value = value;
3999
+ this.dom = dom;
4000
+ this.caret = caret;
4001
+ this.edit = edit;
4002
+ this.parsing = parsing;
4125
4003
  this.match = signal(void 0);
4126
4004
  this.element = signal(null);
4127
4005
  this.slot = computed(() => {
4128
- const Overlay = this._store.props.Overlay();
4006
+ const Overlay = this.props.Overlay();
4129
4007
  return (option, defaultComponent) => resolveOverlaySlot(Overlay, option, defaultComponent);
4130
4008
  });
4131
4009
  this.select = event();
4132
4010
  this.close = event();
4011
+ this.position = computed(() => {
4012
+ if (!this.match()) return {
4013
+ left: 0,
4014
+ top: 0
4015
+ };
4016
+ const rect = getRect();
4017
+ if (!rect) return {
4018
+ left: 0,
4019
+ top: 0
4020
+ };
4021
+ return {
4022
+ left: rect.left,
4023
+ top: rect.top + rect.height + 1
4024
+ };
4025
+ });
4026
+ const hasOverlayTrigger = computed(() => this.props.options().some((opt) => opt.overlay?.trigger != null));
4027
+ const toggle = (enabled) => {
4028
+ if (enabled && !this.#scope) this.#scope = effectScope(() => {
4029
+ watch(this.close, () => {
4030
+ this.match(void 0);
4031
+ });
4032
+ watch(this.value.current, () => {
4033
+ const showOverlayOn = this.props.showOverlayOn();
4034
+ const type = "change";
4035
+ if (showOverlayOn === type || Array.isArray(showOverlayOn) && showOverlayOn.includes(type)) this.#probeTrigger();
4036
+ });
4037
+ effect(() => {
4038
+ if (this.match()) {
4039
+ listen(window, "keydown", (e) => {
4040
+ if (e.key === KEYBOARD.ESC) this.close();
4041
+ });
4042
+ listen(document, "click", (e) => {
4043
+ const target = e.target instanceof HTMLElement ? e.target : null;
4044
+ if (this.element()?.contains(target)) return;
4045
+ if (this.dom.container()?.contains(target)) return;
4046
+ this.close();
4047
+ }, true);
4048
+ }
4049
+ });
4050
+ const selectionChangeHandler = () => {
4051
+ if (!this.dom.container()?.contains(document.activeElement)) return;
4052
+ const showOverlayOn = this.props.showOverlayOn();
4053
+ const type = "selectionChange";
4054
+ if (showOverlayOn === type || Array.isArray(showOverlayOn) && showOverlayOn.includes(type)) this.#probeTrigger();
4055
+ };
4056
+ listen(document, "selectionchange", selectionChangeHandler);
4057
+ watch(this.select, (overlayEvent) => {
4058
+ const { mark, match: { option, range } } = overlayEvent;
4059
+ const markup = option.markup;
4060
+ if (!markup) return;
4061
+ const annotation = mark.type === "mark" ? annotate(markup, {
4062
+ value: mark.value,
4063
+ meta: mark.meta
4064
+ }) : annotate(markup, { value: mark.content });
4065
+ this.edit.replace(range, annotation);
4066
+ this.match(void 0);
4067
+ });
4068
+ });
4069
+ if (!enabled && this.#scope) {
4070
+ this.#scope();
4071
+ this.#scope = void 0;
4072
+ }
4073
+ };
4074
+ this.lifecycle.onMounted(() => {
4075
+ watch(hasOverlayTrigger, toggle);
4076
+ toggle(hasOverlayTrigger());
4077
+ });
4133
4078
  }
4134
4079
  #probeTrigger() {
4135
- const match = TriggerFinder.find(this._store.props.options(), (option) => option.overlay?.trigger, this._store) ?? this.#probeTriggerFromRecovery();
4080
+ const match = TriggerFinder.find(this.props.options(), (option) => option.overlay?.trigger, this.dom) ?? this.#probeTriggerFromCaretRange();
4136
4081
  this.match(match);
4137
4082
  }
4138
- #probeTriggerFromRecovery() {
4139
- const recovery = this._store.caret.recovery();
4140
- if (recovery?.kind !== "caret") return;
4141
- const value = this._store.value.current();
4142
- const cursor = recovery.rawPosition;
4083
+ #probeTriggerFromCaretRange() {
4084
+ const sel = this.caret.selection();
4085
+ if (!sel || sel.start !== sel.end) return;
4086
+ const cursor = sel.start;
4087
+ const value = this.value.current();
4143
4088
  const left = value.slice(0, cursor);
4144
4089
  const rightWord = value.slice(cursor).match(/^\w*/)?.[0] ?? "";
4145
- for (const option of this._store.props.options()) {
4090
+ for (const option of this.props.options()) {
4146
4091
  const trigger = option.overlay?.trigger;
4147
4092
  if (!trigger) continue;
4148
4093
  const match = left.match(new RegExp(`${escape(trigger)}(\\w*)$`));
@@ -4158,65 +4103,11 @@ var OverlayFeature = class {
4158
4103
  end: start + source.length
4159
4104
  },
4160
4105
  span: value,
4161
- node: window.getSelection()?.anchorNode ?? this._store.dom.container() ?? document.body,
4106
+ node: window.getSelection()?.anchorNode ?? this.dom.container() ?? document.body,
4162
4107
  option
4163
4108
  };
4164
4109
  }
4165
4110
  }
4166
- enable() {
4167
- if (this.#scope) return;
4168
- this.#scope = effectScope(() => {
4169
- watch(this.close, () => {
4170
- this.match(void 0);
4171
- });
4172
- watch(this._store.value.change, () => {
4173
- const showOverlayOn = this._store.props.showOverlayOn();
4174
- const type = "change";
4175
- if (showOverlayOn === type || Array.isArray(showOverlayOn) && showOverlayOn.includes(type)) this.#probeTrigger();
4176
- });
4177
- alienEffect(() => {
4178
- if (this.match()) {
4179
- listen(window, "keydown", (e) => {
4180
- if (e.key === KEYBOARD.ESC) this.close();
4181
- });
4182
- listen(document, "click", (e) => {
4183
- const target = e.target instanceof HTMLElement ? e.target : null;
4184
- if (this.element()?.contains(target)) return;
4185
- if (this._store.dom.container()?.contains(target)) return;
4186
- this.close();
4187
- }, true);
4188
- }
4189
- });
4190
- const selectionChangeHandler = () => {
4191
- if (!this._store.dom.container()?.contains(document.activeElement)) return;
4192
- const showOverlayOn = this._store.props.showOverlayOn();
4193
- const type = "selectionChange";
4194
- if (showOverlayOn === type || Array.isArray(showOverlayOn) && showOverlayOn.includes(type)) this.#probeTrigger();
4195
- };
4196
- listen(document, "selectionchange", selectionChangeHandler);
4197
- watch(this.select, (overlayEvent) => {
4198
- const { mark, match: { option, range } } = overlayEvent;
4199
- const markup = option.markup;
4200
- if (!markup) return;
4201
- const annotation = mark.type === "mark" ? annotate(markup, {
4202
- value: mark.value,
4203
- meta: mark.meta
4204
- }) : annotate(markup, { value: mark.content });
4205
- this._store.value.replaceRange(range, annotation, {
4206
- source: "overlay",
4207
- recover: {
4208
- kind: "caret",
4209
- rawPosition: range.start + annotation.length
4210
- }
4211
- });
4212
- this.match(void 0);
4213
- });
4214
- });
4215
- }
4216
- disable() {
4217
- this.#scope?.();
4218
- this.#scope = void 0;
4219
- }
4220
4111
  };
4221
4112
  //#endregion
4222
4113
  //#region ../../core/src/features/overlay/suggestionNavigation.ts
@@ -4249,10 +4140,9 @@ function navigateSuggestions(key, activeIndex, length) {
4249
4140
  }
4250
4141
  }
4251
4142
  //#endregion
4252
- //#region ../../core/src/features/props/PropsFeature.ts
4253
- var PropsFeature = class {
4254
- constructor(_store) {
4255
- this._store = _store;
4143
+ //#region ../../core/src/features/props/PropsModel.ts
4144
+ var PropsModel = class {
4145
+ constructor() {
4256
4146
  this.value = signal(void 0, { readonly: true });
4257
4147
  this.defaultValue = signal(void 0, { readonly: true });
4258
4148
  this.onChange = signal(void 0, { readonly: true });
@@ -4282,99 +4172,34 @@ var PropsFeature = class {
4282
4172
  }
4283
4173
  };
4284
4174
  //#endregion
4285
- //#region ../../core/src/features/value/ControlledEcho.ts
4286
- var ControlledEcho = class {
4287
- #pending;
4288
- propose(candidate, recovery) {
4289
- this.#pending = {
4290
- candidate,
4291
- recovery
4292
- };
4293
- }
4294
- onEcho(value) {
4295
- const pending = this.#pending;
4296
- if (!pending) return void 0;
4297
- this.#pending = void 0;
4298
- return pending.candidate === value ? pending.recovery : void 0;
4299
- }
4300
- supersede() {
4301
- this.#pending = void 0;
4302
- }
4303
- };
4304
- //#endregion
4305
- //#region ../../core/src/features/value/ValueFeature.ts
4306
- var ValueFeature = class {
4307
- #controlledEcho = new ControlledEcho();
4308
- #scope;
4309
- constructor(_store) {
4310
- this._store = _store;
4311
- this.current = signal("");
4312
- this.isControlledMode = computed(() => this._store.props.value() !== void 0);
4313
- this.change = event();
4314
- }
4315
- enable() {
4316
- if (this.#scope) return;
4317
- this.#commitAccepted(this._store.props.value() ?? this._store.props.defaultValue() ?? "");
4318
- this.#scope = effectScope(() => {
4319
- watch(this._store.props.value, (value) => {
4320
- if (value === void 0) return;
4321
- if (value === this.current()) return;
4322
- const recovery = this.#controlledEcho.onEcho(value);
4323
- this.#commitAccepted(value);
4324
- if (recovery) this._store.caret.recovery(recovery);
4325
- this.change();
4326
- });
4175
+ //#region ../../core/src/features/value/ValueModel.ts
4176
+ var ValueModel = class {
4177
+ constructor(props) {
4178
+ this.props = props;
4179
+ this.isControlledMode = computed(() => this.props.value() !== void 0);
4180
+ this.current = model({
4181
+ default: () => this.props.defaultValue() ?? "",
4182
+ get: (value) => this.isControlledMode() ? this.props.value() ?? "" : value,
4183
+ set: (next, previous) => {
4184
+ if (next === void 0) return previous;
4185
+ if (this.props.readOnly()) return previous;
4186
+ this.props.onChange()?.(next);
4187
+ return this.isControlledMode() ? previous : next;
4188
+ }
4327
4189
  });
4328
4190
  }
4329
- disable() {
4330
- this.#scope?.();
4331
- this.#scope = void 0;
4332
- }
4333
- replaceRange(range, replacement, options) {
4334
- const current = this.current();
4335
- if (this._store.props.readOnly()) return {
4336
- ok: false,
4337
- reason: "readOnly"
4338
- };
4339
- if (range.start < 0 || range.end < range.start || range.end > current.length) return {
4340
- ok: false,
4341
- reason: "invalidRange"
4342
- };
4343
- const candidate = current.slice(0, range.start) + replacement + current.slice(range.end);
4344
- return this.#commitCandidate(candidate, options?.recover);
4345
- }
4346
- replaceAll(next, options) {
4347
- return this.replaceRange({
4348
- start: 0,
4349
- end: this.current().length
4350
- }, next, options);
4351
- }
4352
- #commitCandidate(candidate, recovery) {
4353
- if (this.isControlledMode()) {
4354
- this.#controlledEcho.propose(candidate, recovery);
4355
- this._store.props.onChange()?.(candidate);
4356
- return {
4357
- ok: true,
4358
- accepted: "pendingControlledEcho",
4359
- value: candidate
4360
- };
4361
- }
4362
- this._store.props.onChange()?.(candidate);
4363
- this.#commitAccepted(candidate);
4364
- this._store.caret.recovery(recovery);
4365
- this.change();
4366
- return {
4367
- ok: true,
4368
- accepted: "immediate",
4369
- value: candidate
4370
- };
4371
- }
4372
- #commitAccepted(value) {
4373
- const tokens = this._store.parsing.parseValue(value);
4374
- batch(() => {
4375
- this._store.parsing.acceptTokens(tokens);
4376
- this.current(value);
4377
- });
4191
+ /**
4192
+ * Attempts to replace `range` with `replacement`. Returns `true` when the
4193
+ * edit was accepted (range valid and not read-only), `false` otherwise.
4194
+ * Callers use the return value to gate downstream side effects such as
4195
+ * caret placement.
4196
+ */
4197
+ replace(range, replacement) {
4198
+ if (this.props.readOnly()) return false;
4199
+ const next = replaceInString(this.current(), range, replacement);
4200
+ if (next === void 0) return false;
4201
+ this.current(next);
4202
+ return true;
4378
4203
  }
4379
4204
  };
4380
4205
  //#endregion
@@ -4554,34 +4379,20 @@ var Store = class {
4554
4379
  constructor() {
4555
4380
  this.key = new KeyGenerator();
4556
4381
  this.blocks = new BlockRegistry();
4557
- this.props = new PropsFeature(this);
4558
- this.handler = new MarkputHandler(this);
4559
- this.lifecycle = new LifecycleFeature();
4560
- this.value = new ValueFeature(this);
4561
- this.mark = new MarkFeature(this);
4562
- this.overlay = new OverlayFeature(this);
4563
- this.slots = new SlotsFeature(this);
4564
- this.caret = new CaretFeature(this);
4565
- this.keyboard = new KeyboardFeature(this);
4566
- this.dom = new DomFeature(this);
4567
- this.drag = new DragFeature(this);
4568
- this.clipboard = new ClipboardFeature(this);
4569
- this.parsing = new ParsingFeature(this);
4570
- const features = [
4571
- this.lifecycle,
4572
- this.value,
4573
- this.mark,
4574
- this.overlay,
4575
- this.slots,
4576
- this.caret,
4577
- this.keyboard,
4578
- this.dom,
4579
- this.drag,
4580
- this.clipboard,
4581
- this.parsing
4582
- ];
4583
- watch(this.lifecycle.mounted, () => features.forEach((f) => f.enable()));
4584
- watch(this.lifecycle.unmounted, () => features.forEach((f) => f.disable()));
4382
+ this.lifecycle = new Lifecycle();
4383
+ this.props = new PropsModel();
4384
+ this.value = new ValueModel(this.props);
4385
+ this.mark = new MarkFeature(this.props);
4386
+ this.slots = new SlotsFeature(this.props);
4387
+ this.parsing = new ParseController(this.lifecycle, this.value, this.mark, this.props, this.slots);
4388
+ this.dom = new DomController(this.lifecycle, this.props, this.parsing, this.value);
4389
+ this.caret = new CaretModel(this.lifecycle, this.dom, this.value);
4390
+ this.edit = new EditController(this.value, this.caret);
4391
+ this.overlay = new OverlayController(this.lifecycle, this.props, this.value, this.dom, this.caret, this.edit, this.parsing);
4392
+ this.keyboard = new KeyboardController(this.lifecycle, this.dom, this.value, this.caret, this.edit, this.slots, this.parsing, this.props);
4393
+ this.drag = new DragController(this.props, this.value, this.parsing, this.caret);
4394
+ this.clipboard = new ClipboardController(this.lifecycle, this.edit, this.dom, this.parsing);
4395
+ this.handler = new MarkputHandler(this.dom, this.overlay, this.parsing);
4585
4396
  }
4586
4397
  };
4587
4398
  //#endregion
@@ -4865,13 +4676,7 @@ function useOverlay() {
4865
4676
  match: s.overlay.match,
4866
4677
  overlay: s.overlay
4867
4678
  }));
4868
- const style = useMemo(() => {
4869
- if (!match) return {
4870
- left: 0,
4871
- top: 0
4872
- };
4873
- return Caret.getAbsolutePosition();
4874
- }, [match]);
4679
+ const style = useMarkput((s) => s.overlay.position());
4875
4680
  const close = useCallback(() => overlay.close(), [overlay]);
4876
4681
  return {
4877
4682
  match,