slate-angular 20.2.0-next.2 → 20.2.0-next.20

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.
@@ -353,6 +353,18 @@ const CustomDOMEditor = {
353
353
  }
354
354
  };
355
355
 
356
+ /**
357
+ * Symbols.
358
+ */
359
+ const PLACEHOLDER_SYMBOL = Symbol('placeholder');
360
+ /**
361
+ * Weak map for associating the html element with the component.
362
+ */
363
+ const ELEMENT_TO_COMPONENT = new WeakMap();
364
+ const IS_ENABLED_VIRTUAL_SCROLL = new WeakMap();
365
+ const EDITOR_TO_VIRTUAL_SCROLL_SELECTION = new WeakMap();
366
+ const EDITOR_TO_AFTER_VIEW_INIT_QUEUE = new WeakMap();
367
+
356
368
  const AngularEditor = {
357
369
  ...CustomDOMEditor,
358
370
  /**
@@ -424,19 +436,12 @@ const AngularEditor = {
424
436
  // FocusedContext is updated to the correct value
425
437
  el.focus({ preventScroll: true });
426
438
  }
439
+ },
440
+ isEnabledVirtualScroll(editor) {
441
+ return IS_ENABLED_VIRTUAL_SCROLL.get(editor);
427
442
  }
428
443
  };
429
444
 
430
- /**
431
- * Symbols.
432
- */
433
- const PLACEHOLDER_SYMBOL = Symbol('placeholder');
434
- /**
435
- * Weak map for associating the html element with the component.
436
- */
437
- const ELEMENT_TO_COMPONENT = new WeakMap();
438
- const EDITOR_TO_AFTER_VIEW_INIT_QUEUE = new WeakMap();
439
-
440
445
  const IS_IOS = typeof navigator !== 'undefined' &&
441
446
  typeof window !== 'undefined' &&
442
447
  /iPad|iPhone|iPod/.test(navigator.userAgent) &&
@@ -470,8 +475,7 @@ const HAS_BEFORE_INPUT_SUPPORT = !IS_CHROME_LEGACY &&
470
475
  globalThis.InputEvent &&
471
476
  // @ts-ignore The `getTargetRanges` property isn't recognized.
472
477
  typeof globalThis.InputEvent.prototype.getTargetRanges === 'function';
473
- const VIRTUAL_SCROLL_DEFAULT_BUFFER_COUNT = 3;
474
- const VIRTUAL_SCROLL_DEFAULT_BLOCK_HEIGHT = 40;
478
+ const VIRTUAL_SCROLL_DEFAULT_BLOCK_HEIGHT = 30;
475
479
  const SLATE_DEBUG_KEY = '__SLATE_DEBUG__';
476
480
 
477
481
  /**
@@ -950,6 +954,55 @@ const fallbackCopyText = async (text) => {
950
954
  });
951
955
  };
952
956
 
957
+ const ELEMENT_KEY_TO_HEIGHTS = new WeakMap();
958
+ const EDITOR_TO_BUSINESS_TOP = new WeakMap();
959
+ const getBusinessTop = (editor) => {
960
+ return EDITOR_TO_BUSINESS_TOP.get(editor) ?? 0;
961
+ };
962
+ const getRealHeightByElement = (editor, element, defaultHeight = VIRTUAL_SCROLL_DEFAULT_BLOCK_HEIGHT) => {
963
+ const isVisible = editor.isVisible(element);
964
+ if (!isVisible) {
965
+ return 0;
966
+ }
967
+ if (!element) {
968
+ return defaultHeight;
969
+ }
970
+ const heights = ELEMENT_KEY_TO_HEIGHTS.get(editor);
971
+ const key = AngularEditor.findKey(editor, element);
972
+ const height = heights?.get(key.id);
973
+ if (typeof height === 'number') {
974
+ return height;
975
+ }
976
+ if (heights?.has(key.id)) {
977
+ console.error('getBlockHeight: invalid height value', key.id, height);
978
+ }
979
+ return defaultHeight;
980
+ };
981
+ const buildHeightsAndAccumulatedHeights = (editor) => {
982
+ const children = (editor.children || []);
983
+ const heights = new Array(children.length);
984
+ const accumulatedHeights = new Array(children.length + 1);
985
+ accumulatedHeights[0] = 0;
986
+ for (let i = 0; i < children.length; i++) {
987
+ const height = getRealHeightByElement(editor, children[i]);
988
+ heights[i] = height;
989
+ accumulatedHeights[i + 1] = accumulatedHeights[i] + height;
990
+ }
991
+ return { heights, accumulatedHeights };
992
+ };
993
+ const scrollToElement = (editor, element, scrollTo) => {
994
+ const children = editor.children;
995
+ if (!children.length) {
996
+ return;
997
+ }
998
+ const anchorIndex = children.findIndex(item => item === element);
999
+ if (anchorIndex < 0) {
1000
+ return;
1001
+ }
1002
+ const { accumulatedHeights } = buildHeightsAndAccumulatedHeights(editor);
1003
+ scrollTo((accumulatedHeights[anchorIndex] ?? 0) + getBusinessTop(editor));
1004
+ };
1005
+
953
1006
  const withAngular = (editor, clipboardFormatKey = 'x-slate-fragment') => {
954
1007
  let e = editor;
955
1008
  let { apply } = e;
@@ -967,7 +1020,14 @@ const withAngular = (editor, clipboardFormatKey = 'x-slate-fragment') => {
967
1020
  }
968
1021
  // Create a fake selection so that we can add a Base64-encoded copy of the
969
1022
  // fragment to the HTML, to decode on future pastes.
970
- const domRange = AngularEditor.toDOMRange(e, selection);
1023
+ let domRange;
1024
+ if (AngularEditor.isEnabledVirtualScroll(e)) {
1025
+ const virtualScrollSelection = EDITOR_TO_VIRTUAL_SCROLL_SELECTION.get(e);
1026
+ if (virtualScrollSelection) {
1027
+ domRange = AngularEditor.toDOMRange(e, virtualScrollSelection);
1028
+ }
1029
+ }
1030
+ domRange = domRange ?? AngularEditor.toDOMRange(e, selection);
971
1031
  let contents = domRange.cloneContents();
972
1032
  let attach = contents.childNodes[0];
973
1033
  // Make sure attach is non-empty, since empty nodes will not get copied.
@@ -1135,6 +1195,12 @@ const withAngular = (editor, clipboardFormatKey = 'x-slate-fragment') => {
1135
1195
  NODE_TO_KEY.set(node, key);
1136
1196
  }
1137
1197
  };
1198
+ e.selectAll = () => {
1199
+ Transforms.select(e, []);
1200
+ };
1201
+ e.isVisible = element => {
1202
+ return true;
1203
+ };
1138
1204
  return e;
1139
1205
  };
1140
1206
 
@@ -1739,6 +1805,7 @@ class BaseElementFlavour extends BaseFlavour {
1739
1805
  this.getOutletElement = () => {
1740
1806
  return this.nativeElement.querySelector('.children-outlet');
1741
1807
  };
1808
+ this.stableHeight = null;
1742
1809
  }
1743
1810
  get element() {
1744
1811
  return this._context && this._context.element;
@@ -1794,6 +1861,7 @@ class BaseElementFlavour extends BaseFlavour {
1794
1861
  if (ELEMENT_TO_COMPONENT.get(this.element) === this) {
1795
1862
  ELEMENT_TO_COMPONENT.delete(this.element);
1796
1863
  }
1864
+ this.listRender.destroy();
1797
1865
  this.nativeElement?.remove();
1798
1866
  }
1799
1867
  onContextChange() {
@@ -1824,11 +1892,21 @@ class BaseElementFlavour extends BaseFlavour {
1824
1892
  readonly: this._context.readonly
1825
1893
  };
1826
1894
  }
1895
+ isStableHeight() {
1896
+ return false;
1897
+ }
1827
1898
  getRealHeight() {
1899
+ if (this.isStableHeight() && this.stableHeight !== null) {
1900
+ return this.stableHeight;
1901
+ }
1828
1902
  const blockCard = getBlockCardByNativeElement(this.nativeElement);
1829
1903
  const target = blockCard || this.nativeElement;
1830
1904
  const computedStyle = getComputedStyle(target);
1831
- return Promise.resolve(target.offsetHeight + parseFloat(computedStyle.marginTop) + parseFloat(computedStyle.marginBottom));
1905
+ const height = Math.ceil(target.getBoundingClientRect().height) + parseFloat(computedStyle.marginTop) + parseFloat(computedStyle.marginBottom);
1906
+ if (this.isStableHeight()) {
1907
+ this.stableHeight = height;
1908
+ }
1909
+ return height;
1832
1910
  }
1833
1911
  }
1834
1912
 
@@ -2180,6 +2258,9 @@ class LeavesRender {
2180
2258
  });
2181
2259
  return { decoratedLeaves, contexts };
2182
2260
  }
2261
+ destroy() {
2262
+ this.views.forEach(view => view.destroy());
2263
+ }
2183
2264
  }
2184
2265
  function getContext$1(index, leafContexts) {
2185
2266
  return leafContexts[index];
@@ -2222,6 +2303,7 @@ class BaseTextFlavour extends BaseFlavour {
2222
2303
  NODE_TO_ELEMENT.delete(this.text);
2223
2304
  }
2224
2305
  ELEMENT_TO_NODE.delete(this.nativeElement);
2306
+ this.leavesRender.destroy();
2225
2307
  this.nativeElement?.remove();
2226
2308
  }
2227
2309
  onContextChange() {
@@ -2262,6 +2344,7 @@ class ListRender {
2262
2344
  this.viewContainerRef = viewContainerRef;
2263
2345
  this.getOutletParent = getOutletParent;
2264
2346
  this.getOutletElement = getOutletElement;
2347
+ this.children = [];
2265
2348
  this.views = [];
2266
2349
  this.blockCards = [];
2267
2350
  this.contexts = [];
@@ -2411,6 +2494,7 @@ class ListRender {
2411
2494
  this.blockCards[index].destroy();
2412
2495
  }
2413
2496
  });
2497
+ this.children = [];
2414
2498
  this.views = [];
2415
2499
  this.blockCards = [];
2416
2500
  this.contexts = [];
@@ -2553,39 +2637,514 @@ function executeAfterViewInit(editor) {
2553
2637
  clearAfterViewInitQueue(editor);
2554
2638
  }
2555
2639
 
2556
- const JUST_NOW_UPDATED_VIRTUAL_VIEW = new WeakMap();
2640
+ class VirtualScrollDebugOverlay {
2641
+ static { this.storageKey = 'slate_virtual_scroll_debug_overlay_state'; }
2642
+ static { this.minWidth = 320; }
2643
+ static { this.minHeight = 240; }
2644
+ static { this.defaultWidth = 410; }
2645
+ static { this.defaultHeight = 480; }
2646
+ static getInstance(doc) {
2647
+ if (!this.instance) {
2648
+ this.instance = new VirtualScrollDebugOverlay(doc);
2649
+ }
2650
+ this.instance.init();
2651
+ return this.instance;
2652
+ }
2653
+ static log(doc, type, ...args) {
2654
+ this.getInstance(doc).log(type, ...args);
2655
+ }
2656
+ static syncScrollTop(doc, value) {
2657
+ const instance = this.getInstance(doc);
2658
+ instance.setScrollTopValue(value);
2659
+ }
2660
+ constructor(doc) {
2661
+ this.doc = doc;
2662
+ this.state = {
2663
+ left: 16,
2664
+ top: 16,
2665
+ collapsed: false,
2666
+ width: VirtualScrollDebugOverlay.defaultWidth,
2667
+ height: VirtualScrollDebugOverlay.defaultHeight
2668
+ };
2669
+ this.originalConsoleLog = console.log.bind(console);
2670
+ this.originalConsoleWarn = console.warn.bind(console);
2671
+ this.dragOffsetX = 0;
2672
+ this.dragOffsetY = 0;
2673
+ this.isDragging = false;
2674
+ this.isResizing = false;
2675
+ this.resizeStartX = 0;
2676
+ this.resizeStartY = 0;
2677
+ this.resizeStartWidth = 0;
2678
+ this.resizeStartHeight = 0;
2679
+ this.dragMoved = false;
2680
+ this.wasDragged = false;
2681
+ this.onDragging = (event) => {
2682
+ if (!this.isDragging || !this.container) {
2683
+ return;
2684
+ }
2685
+ this.dragMoved = true;
2686
+ const nextLeft = event.clientX - this.dragOffsetX;
2687
+ const nextTop = event.clientY - this.dragOffsetY;
2688
+ this.container.style.left = `${nextLeft}px`;
2689
+ this.container.style.top = `${nextTop}px`;
2690
+ this.container.style.right = 'auto';
2691
+ this.container.style.bottom = 'auto';
2692
+ };
2693
+ this.onDragEnd = () => {
2694
+ if (!this.isDragging) {
2695
+ return;
2696
+ }
2697
+ this.isDragging = false;
2698
+ this.wasDragged = this.dragMoved;
2699
+ this.dragMoved = false;
2700
+ this.doc.removeEventListener('mousemove', this.onDragging);
2701
+ this.doc.removeEventListener('mouseup', this.onDragEnd);
2702
+ if (this.container) {
2703
+ const rect = this.container.getBoundingClientRect();
2704
+ this.state.left = rect.left;
2705
+ this.state.top = rect.top;
2706
+ this.persistState();
2707
+ this.container.style.transition = '';
2708
+ }
2709
+ };
2710
+ this.onResizing = (event) => {
2711
+ if (!this.isResizing || !this.container) {
2712
+ return;
2713
+ }
2714
+ const deltaX = event.clientX - this.resizeStartX;
2715
+ const deltaY = event.clientY - this.resizeStartY;
2716
+ const nextWidth = Math.max(VirtualScrollDebugOverlay.minWidth, this.resizeStartWidth + deltaX);
2717
+ const nextHeight = Math.max(VirtualScrollDebugOverlay.minHeight, this.resizeStartHeight + deltaY);
2718
+ this.state.width = nextWidth;
2719
+ this.state.height = nextHeight;
2720
+ this.applySize();
2721
+ };
2722
+ this.onResizeEnd = () => {
2723
+ if (!this.isResizing) {
2724
+ return;
2725
+ }
2726
+ this.isResizing = false;
2727
+ this.doc.removeEventListener('mousemove', this.onResizing);
2728
+ this.doc.removeEventListener('mouseup', this.onResizeEnd);
2729
+ this.persistState();
2730
+ };
2731
+ }
2732
+ init() {
2733
+ if (!this.container) {
2734
+ this.createContainer();
2735
+ }
2736
+ else {
2737
+ this.applyState();
2738
+ }
2739
+ }
2740
+ log(type, ...args) {
2741
+ this.init();
2742
+ if (type === 'warn') {
2743
+ this.originalConsoleWarn(...args);
2744
+ }
2745
+ else {
2746
+ this.originalConsoleLog(...args);
2747
+ }
2748
+ this.appendLog(type, ...args);
2749
+ }
2750
+ dispose() {
2751
+ this.container?.remove();
2752
+ this.container = undefined;
2753
+ this.contentWrapper = undefined;
2754
+ this.bubble = undefined;
2755
+ this.logList = undefined;
2756
+ this.selectorInput = undefined;
2757
+ this.distanceInput = undefined;
2758
+ this.collapseToggle = undefined;
2759
+ this.resizeHandle = undefined;
2760
+ this.doc.removeEventListener('mousemove', this.onDragging);
2761
+ this.doc.removeEventListener('mouseup', this.onDragEnd);
2762
+ this.doc.removeEventListener('mousemove', this.onResizing);
2763
+ this.doc.removeEventListener('mouseup', this.onResizeEnd);
2764
+ this.isDragging = false;
2765
+ this.isResizing = false;
2766
+ }
2767
+ createContainer() {
2768
+ this.loadState();
2769
+ const doc = this.doc;
2770
+ const container = doc.createElement('div');
2771
+ container.style.position = 'fixed';
2772
+ container.style.right = 'auto';
2773
+ container.style.bottom = 'auto';
2774
+ container.style.boxSizing = 'border-box';
2775
+ container.style.background = 'rgba(17, 24, 39, 0.95)';
2776
+ container.style.color = '#e5e7eb';
2777
+ container.style.fontSize = '12px';
2778
+ container.style.border = '1px solid #1f2937';
2779
+ container.style.borderRadius = '10px';
2780
+ container.style.fontFamily = 'Menlo, Consolas, monospace';
2781
+ container.style.boxShadow = '0 10px 30px rgba(0, 0, 0, 0.35)';
2782
+ container.style.zIndex = '9999';
2783
+ container.style.display = 'flex';
2784
+ container.style.flexDirection = 'column';
2785
+ container.style.gap = '10px';
2786
+ container.style.minWidth = `${VirtualScrollDebugOverlay.minWidth}px`;
2787
+ container.style.minHeight = `${VirtualScrollDebugOverlay.minHeight}px`;
2788
+ container.style.maxHeight = '80vh';
2789
+ container.style.cursor = 'default';
2790
+ container.addEventListener('mousedown', event => {
2791
+ if (this.state.collapsed) {
2792
+ this.startDrag(event);
2793
+ }
2794
+ });
2795
+ const header = doc.createElement('div');
2796
+ header.style.display = 'flex';
2797
+ header.style.alignItems = 'center';
2798
+ header.style.justifyContent = 'space-between';
2799
+ header.style.cursor = 'move';
2800
+ header.addEventListener('mousedown', event => {
2801
+ this.startDrag(event);
2802
+ });
2803
+ const title = doc.createElement('div');
2804
+ title.textContent = 'Virtual Scroll Debug';
2805
+ title.style.fontWeight = '600';
2806
+ title.style.letterSpacing = '0.3px';
2807
+ const actions = doc.createElement('div');
2808
+ actions.style.display = 'flex';
2809
+ actions.style.gap = '6px';
2810
+ const collapseButton = doc.createElement('button');
2811
+ collapseButton.type = 'button';
2812
+ collapseButton.textContent = '折叠';
2813
+ collapseButton.style.background = '#1f2937';
2814
+ collapseButton.style.color = '#e5e7eb';
2815
+ collapseButton.style.border = '1px solid #374151';
2816
+ collapseButton.style.borderRadius = '6px';
2817
+ collapseButton.style.padding = '4px 8px';
2818
+ collapseButton.style.cursor = 'pointer';
2819
+ collapseButton.addEventListener('click', () => {
2820
+ this.setCollapsed(!this.state.collapsed);
2821
+ });
2822
+ const clearButton = doc.createElement('button');
2823
+ clearButton.type = 'button';
2824
+ clearButton.textContent = '清除日志';
2825
+ clearButton.style.background = '#374151';
2826
+ clearButton.style.color = '#e5e7eb';
2827
+ clearButton.style.border = '1px solid #4b5563';
2828
+ clearButton.style.borderRadius = '6px';
2829
+ clearButton.style.padding = '4px 10px';
2830
+ clearButton.style.cursor = 'pointer';
2831
+ clearButton.addEventListener('click', () => {
2832
+ if (this.logList) {
2833
+ this.logList.innerHTML = '';
2834
+ }
2835
+ });
2836
+ actions.appendChild(collapseButton);
2837
+ actions.appendChild(clearButton);
2838
+ header.appendChild(title);
2839
+ header.appendChild(actions);
2840
+ const scrollForm = doc.createElement('div');
2841
+ scrollForm.style.display = 'grid';
2842
+ scrollForm.style.gridTemplateColumns = '1fr 110px 50px';
2843
+ scrollForm.style.gap = '6px';
2844
+ scrollForm.style.alignItems = 'center';
2845
+ const selectorInput = doc.createElement('input');
2846
+ selectorInput.placeholder = '滚动容器 selector';
2847
+ selectorInput.style.padding = '6px 8px';
2848
+ selectorInput.style.borderRadius = '6px';
2849
+ selectorInput.style.border = '1px solid #4b5563';
2850
+ selectorInput.style.background = '#111827';
2851
+ selectorInput.style.color = '#e5e7eb';
2852
+ selectorInput.autocomplete = 'off';
2853
+ const distanceInput = doc.createElement('input');
2854
+ distanceInput.placeholder = '滚动距离(px)';
2855
+ distanceInput.type = 'number';
2856
+ distanceInput.style.padding = '6px 8px';
2857
+ distanceInput.style.borderRadius = '6px';
2858
+ distanceInput.style.border = '1px solid #4b5563';
2859
+ distanceInput.style.background = '#111827';
2860
+ distanceInput.style.color = '#e5e7eb';
2861
+ const scrollButton = doc.createElement('button');
2862
+ scrollButton.type = 'button';
2863
+ scrollButton.textContent = '滚动';
2864
+ scrollButton.style.background = '#10b981';
2865
+ scrollButton.style.color = '#0b1c15';
2866
+ scrollButton.style.border = 'none';
2867
+ scrollButton.style.borderRadius = '6px';
2868
+ scrollButton.style.padding = '6px 10px';
2869
+ scrollButton.style.cursor = 'pointer';
2870
+ scrollButton.addEventListener('click', () => {
2871
+ const selector = selectorInput.value.trim();
2872
+ const distanceValue = Number(distanceInput.value ?? 0);
2873
+ const distance = Number.isFinite(distanceValue) ? distanceValue : 0;
2874
+ if (!selector) {
2875
+ this.log('warn', '请先填写滚动容器 selector');
2876
+ return;
2877
+ }
2878
+ const target = doc.querySelector(selector);
2879
+ if (!target) {
2880
+ this.log('warn', `未找到滚动容器: ${selector}`);
2881
+ return;
2882
+ }
2883
+ if (typeof target.scrollTo === 'function') {
2884
+ target.scrollTo({ top: distance, behavior: 'auto' });
2885
+ }
2886
+ else if (Object.prototype.hasOwnProperty.call(target, 'scrollTop')) {
2887
+ target.scrollTop = distance;
2888
+ }
2889
+ else {
2890
+ this.log('warn', '目标元素不支持滚动:', selector);
2891
+ return;
2892
+ }
2893
+ this.log('log', `已将 ${selector} 滚动到`, distance);
2894
+ });
2895
+ scrollForm.appendChild(selectorInput);
2896
+ scrollForm.appendChild(distanceInput);
2897
+ scrollForm.appendChild(scrollButton);
2898
+ const logList = doc.createElement('div');
2899
+ logList.style.height = '260px';
2900
+ logList.style.overflowY = 'auto';
2901
+ logList.style.background = '#0b1220';
2902
+ logList.style.border = '1px solid #1f2937';
2903
+ logList.style.borderRadius = '8px';
2904
+ logList.style.padding = '8px';
2905
+ logList.style.display = 'flex';
2906
+ logList.style.flexDirection = 'column';
2907
+ logList.style.gap = '6px';
2908
+ logList.style.flex = '1';
2909
+ logList.style.minHeight = '160px';
2910
+ const bubble = doc.createElement('div');
2911
+ bubble.textContent = 'VS';
2912
+ bubble.style.display = 'none';
2913
+ bubble.style.alignItems = 'center';
2914
+ bubble.style.justifyContent = 'center';
2915
+ bubble.style.fontWeight = '700';
2916
+ bubble.style.fontSize = '14px';
2917
+ bubble.style.letterSpacing = '0.5px';
2918
+ bubble.style.width = '100%';
2919
+ bubble.style.height = '100%';
2920
+ bubble.style.userSelect = 'none';
2921
+ bubble.addEventListener('click', () => {
2922
+ if (this.wasDragged) {
2923
+ this.wasDragged = false;
2924
+ return;
2925
+ }
2926
+ this.setCollapsed(false);
2927
+ });
2928
+ const contentWrapper = doc.createElement('div');
2929
+ contentWrapper.style.display = 'flex';
2930
+ contentWrapper.style.flexDirection = 'column';
2931
+ contentWrapper.style.gap = '10px';
2932
+ contentWrapper.style.width = '100%';
2933
+ contentWrapper.style.height = '100%';
2934
+ contentWrapper.appendChild(header);
2935
+ contentWrapper.appendChild(scrollForm);
2936
+ contentWrapper.appendChild(logList);
2937
+ container.appendChild(contentWrapper);
2938
+ container.appendChild(bubble);
2939
+ const resizeHandle = doc.createElement('div');
2940
+ resizeHandle.style.position = 'absolute';
2941
+ resizeHandle.style.right = '6px';
2942
+ resizeHandle.style.bottom = '6px';
2943
+ resizeHandle.style.width = '14px';
2944
+ resizeHandle.style.height = '14px';
2945
+ resizeHandle.style.cursor = 'nwse-resize';
2946
+ resizeHandle.style.borderRight = '2px solid #4b5563';
2947
+ resizeHandle.style.borderBottom = '2px solid #4b5563';
2948
+ resizeHandle.addEventListener('mousedown', event => {
2949
+ this.startResize(event);
2950
+ });
2951
+ container.appendChild(resizeHandle);
2952
+ doc.body.appendChild(container);
2953
+ this.container = container;
2954
+ this.contentWrapper = contentWrapper;
2955
+ this.bubble = bubble;
2956
+ this.logList = logList;
2957
+ this.selectorInput = selectorInput;
2958
+ this.distanceInput = distanceInput;
2959
+ this.collapseToggle = collapseButton;
2960
+ this.resizeHandle = resizeHandle;
2961
+ this.applyState();
2962
+ }
2963
+ startDrag(event) {
2964
+ if (event.button !== 0) {
2965
+ return;
2966
+ }
2967
+ if (!this.container) {
2968
+ return;
2969
+ }
2970
+ const rect = this.container.getBoundingClientRect();
2971
+ this.isDragging = true;
2972
+ this.wasDragged = false;
2973
+ this.dragMoved = false;
2974
+ this.dragOffsetX = event.clientX - rect.left;
2975
+ this.dragOffsetY = event.clientY - rect.top;
2976
+ this.container.style.transition = 'none';
2977
+ this.doc.addEventListener('mousemove', this.onDragging);
2978
+ this.doc.addEventListener('mouseup', this.onDragEnd);
2979
+ if (!this.state.collapsed) {
2980
+ event.preventDefault();
2981
+ }
2982
+ }
2983
+ startResize(event) {
2984
+ if (event.button !== 0 || this.state.collapsed) {
2985
+ return;
2986
+ }
2987
+ if (!this.container) {
2988
+ return;
2989
+ }
2990
+ const rect = this.container.getBoundingClientRect();
2991
+ this.isResizing = true;
2992
+ this.resizeStartX = event.clientX;
2993
+ this.resizeStartY = event.clientY;
2994
+ this.resizeStartWidth = rect.width;
2995
+ this.resizeStartHeight = rect.height;
2996
+ this.doc.addEventListener('mousemove', this.onResizing);
2997
+ this.doc.addEventListener('mouseup', this.onResizeEnd);
2998
+ event.preventDefault();
2999
+ event.stopPropagation();
3000
+ }
3001
+ applyPosition() {
3002
+ if (!this.container) {
3003
+ return;
3004
+ }
3005
+ this.container.style.left = `${this.state.left}px`;
3006
+ this.container.style.top = `${this.state.top}px`;
3007
+ }
3008
+ applySize() {
3009
+ if (!this.container) {
3010
+ return;
3011
+ }
3012
+ const width = Math.max(VirtualScrollDebugOverlay.minWidth, this.state.width || VirtualScrollDebugOverlay.defaultWidth);
3013
+ const height = Math.max(VirtualScrollDebugOverlay.minHeight, this.state.height || VirtualScrollDebugOverlay.defaultHeight);
3014
+ this.state.width = width;
3015
+ this.state.height = height;
3016
+ this.container.style.width = `${width}px`;
3017
+ this.container.style.height = `${height}px`;
3018
+ }
3019
+ applyCollapsedState() {
3020
+ if (!this.container || !this.contentWrapper || !this.bubble || !this.collapseToggle) {
3021
+ return;
3022
+ }
3023
+ if (this.state.collapsed) {
3024
+ this.container.style.width = '36px';
3025
+ this.container.style.height = '36px';
3026
+ this.container.style.minWidth = '';
3027
+ this.container.style.minHeight = '';
3028
+ this.container.style.padding = '0';
3029
+ this.container.style.borderRadius = '50%';
3030
+ this.container.style.display = 'flex';
3031
+ this.container.style.flexDirection = 'row';
3032
+ this.container.style.alignItems = 'center';
3033
+ this.container.style.justifyContent = 'center';
3034
+ this.container.style.cursor = 'move';
3035
+ this.contentWrapper.style.display = 'none';
3036
+ this.bubble.style.display = 'flex';
3037
+ this.collapseToggle.textContent = '展开';
3038
+ this.resizeHandle.style.display = 'none';
3039
+ }
3040
+ else {
3041
+ this.applySize();
3042
+ this.container.style.padding = '12px';
3043
+ this.container.style.borderRadius = '10px';
3044
+ this.container.style.display = 'flex';
3045
+ this.container.style.flexDirection = 'column';
3046
+ this.container.style.gap = '10px';
3047
+ this.container.style.cursor = 'default';
3048
+ this.contentWrapper.style.display = 'flex';
3049
+ this.bubble.style.display = 'none';
3050
+ this.collapseToggle.textContent = '折叠';
3051
+ this.resizeHandle.style.display = 'block';
3052
+ }
3053
+ }
3054
+ setCollapsed(collapsed) {
3055
+ this.state.collapsed = collapsed;
3056
+ this.applyCollapsedState();
3057
+ this.persistState();
3058
+ }
3059
+ applyState() {
3060
+ this.applyPosition();
3061
+ this.applyCollapsedState();
3062
+ }
3063
+ loadState() {
3064
+ try {
3065
+ const raw = this.doc.defaultView?.localStorage?.getItem(VirtualScrollDebugOverlay.storageKey);
3066
+ if (raw) {
3067
+ const parsed = JSON.parse(raw);
3068
+ if (typeof parsed.left === 'number') {
3069
+ this.state.left = parsed.left;
3070
+ }
3071
+ if (typeof parsed.top === 'number') {
3072
+ this.state.top = parsed.top;
3073
+ }
3074
+ if (typeof parsed.collapsed === 'boolean') {
3075
+ this.state.collapsed = parsed.collapsed;
3076
+ }
3077
+ if (typeof parsed.width === 'number') {
3078
+ this.state.width = parsed.width;
3079
+ }
3080
+ if (typeof parsed.height === 'number') {
3081
+ this.state.height = parsed.height;
3082
+ }
3083
+ }
3084
+ }
3085
+ catch {
3086
+ // ignore storage errors
3087
+ }
3088
+ }
3089
+ persistState() {
3090
+ try {
3091
+ this.doc.defaultView?.localStorage?.setItem(VirtualScrollDebugOverlay.storageKey, JSON.stringify(this.state));
3092
+ }
3093
+ catch {
3094
+ // ignore storage errors
3095
+ }
3096
+ }
3097
+ appendLog(type, ...args) {
3098
+ if (!this.logList) {
3099
+ return;
3100
+ }
3101
+ const item = this.doc.createElement('div');
3102
+ item.style.display = 'flex';
3103
+ item.style.gap = '6px';
3104
+ item.style.alignItems = 'flex-start';
3105
+ item.style.wordBreak = 'break-all';
3106
+ item.style.color = type === 'warn' ? '#fbbf24' : '#9ca3af';
3107
+ const time = this.doc.createElement('span');
3108
+ time.textContent = new Date().toLocaleTimeString();
3109
+ time.style.color = '#6b7280';
3110
+ time.style.flexShrink = '0';
3111
+ time.style.width = '72px';
3112
+ const text = this.doc.createElement('span');
3113
+ text.textContent = `[${type}] ${args.map(arg => this.formatValue(arg)).join(' ')}`;
3114
+ item.appendChild(time);
3115
+ item.appendChild(text);
3116
+ this.logList.appendChild(item);
3117
+ this.logList.scrollTop = this.logList.scrollHeight;
3118
+ }
3119
+ formatValue(value) {
3120
+ if (typeof value === 'string') {
3121
+ return value;
3122
+ }
3123
+ try {
3124
+ return JSON.stringify(value);
3125
+ }
3126
+ catch (error) {
3127
+ return String(value);
3128
+ }
3129
+ }
3130
+ setScrollTopValue(value) {
3131
+ if (this.distanceInput) {
3132
+ this.distanceInput.value = String(value ?? 0);
3133
+ }
3134
+ }
3135
+ }
3136
+
2557
3137
  // not correctly clipboardData on beforeinput
2558
3138
  const forceOnDOMPaste = IS_SAFARI;
2559
3139
  const isDebug = localStorage.getItem(SLATE_DEBUG_KEY) === 'true';
2560
3140
  class SlateEditable {
2561
3141
  set virtualScroll(config) {
2562
- this.virtualConfig = config;
2563
- this.refreshVirtualViewAnimId && cancelAnimationFrame(this.refreshVirtualViewAnimId);
2564
- this.refreshVirtualViewAnimId = requestAnimationFrame(() => {
2565
- const virtualView = this.refreshVirtualView();
2566
- const diff = this.diffVirtualView(virtualView);
2567
- if (diff.isDiff) {
2568
- if (diff.isMissingTop || diff.isMissingBottom) {
2569
- this.measureHeightByIndexes([...diff.diffTopRenderedIndexes, ...diff.diffBottomRenderedIndexes], true).then(result => {
2570
- if (isDebug) {
2571
- console.log('async measureHeightByIndexes:', result);
2572
- }
2573
- this.applyVirtualView(result || virtualView);
2574
- if (this.listRender.initialized) {
2575
- this.listRender.update(this.renderedChildren, this.editor, this.context);
2576
- }
2577
- this.scheduleMeasureVisibleHeights();
2578
- });
2579
- }
2580
- else {
2581
- this.applyVirtualView(virtualView);
2582
- if (this.listRender.initialized) {
2583
- this.listRender.update(virtualView.renderedChildren, this.editor, this.context);
2584
- }
2585
- this.scheduleMeasureVisibleHeights();
2586
- }
2587
- }
2588
- });
3142
+ this.virtualScrollConfig = config;
3143
+ console.log('virtualScrollConfig', config);
3144
+ IS_ENABLED_VIRTUAL_SCROLL.set(this.editor, config.enabled);
3145
+ if (this.isEnabledVirtualScroll()) {
3146
+ this.tryUpdateVirtualViewport();
3147
+ }
2589
3148
  }
2590
3149
  get hasBeforeInputSupport() {
2591
3150
  return HAS_BEFORE_INPUT_SUPPORT;
@@ -2630,15 +3189,15 @@ class SlateEditable {
2630
3189
  return null;
2631
3190
  }
2632
3191
  };
2633
- this.virtualConfig = {
3192
+ this.virtualScrollConfig = {
2634
3193
  enabled: false,
2635
3194
  scrollTop: 0,
2636
- viewportHeight: 0
3195
+ viewportHeight: 0,
3196
+ viewportBoundingTop: 0
2637
3197
  };
2638
- this.renderedChildren = [];
2639
- this.virtualVisibleIndexes = new Set();
2640
- this.measuredHeights = new Map();
2641
- this.measurePending = false;
3198
+ this.inViewportChildren = [];
3199
+ this.inViewportIndics = new Set();
3200
+ this.keyHeightMap = new Map();
2642
3201
  this.virtualScrollInitialized = false;
2643
3202
  }
2644
3203
  ngOnInit() {
@@ -2650,6 +3209,7 @@ class SlateEditable {
2650
3209
  NODE_TO_ELEMENT.set(this.editor, this.elementRef.nativeElement);
2651
3210
  ELEMENT_TO_NODE.set(this.elementRef.nativeElement, this.editor);
2652
3211
  IS_READ_ONLY.set(this.editor, this.readonly);
3212
+ ELEMENT_KEY_TO_HEIGHTS.set(this.editor, this.keyHeightMap);
2653
3213
  EDITOR_TO_ON_CHANGE.set(this.editor, () => {
2654
3214
  this.ngZone.run(() => {
2655
3215
  this.onChange();
@@ -2663,7 +3223,7 @@ class SlateEditable {
2663
3223
  // add browser class
2664
3224
  let browserClass = IS_FIREFOX ? 'firefox' : IS_SAFARI ? 'safari' : '';
2665
3225
  browserClass && this.elementRef.nativeElement.classList.add(browserClass);
2666
- this.initializeVirtualScrolling();
3226
+ this.initializeVirtualScroll();
2667
3227
  this.listRender = new ListRender(this.viewContext, this.viewContainerRef, this.getOutletParent, this.getOutletElement);
2668
3228
  }
2669
3229
  ngOnChanges(simpleChanges) {
@@ -2695,16 +3255,26 @@ class SlateEditable {
2695
3255
  if (value && value.length) {
2696
3256
  this.editor.children = value;
2697
3257
  this.initializeContext();
2698
- const virtualView = this.refreshVirtualView();
2699
- this.applyVirtualView(virtualView);
2700
- const childrenForRender = virtualView.renderedChildren;
2701
- if (!this.listRender.initialized) {
2702
- this.listRender.initialize(childrenForRender, this.editor, this.context);
3258
+ if (this.isEnabledVirtualScroll()) {
3259
+ const virtualView = this.calculateVirtualViewport();
3260
+ this.applyVirtualView(virtualView);
3261
+ const childrenForRender = virtualView.inViewportChildren;
3262
+ if (!this.listRender.initialized) {
3263
+ this.listRender.initialize(childrenForRender, this.editor, this.context);
3264
+ }
3265
+ else {
3266
+ this.listRender.update(childrenForRender, this.editor, this.context);
3267
+ }
3268
+ this.tryMeasureInViewportChildrenHeights();
2703
3269
  }
2704
3270
  else {
2705
- this.listRender.update(childrenForRender, this.editor, this.context);
3271
+ if (!this.listRender.initialized) {
3272
+ this.listRender.initialize(this.editor.children, this.editor, this.context);
3273
+ }
3274
+ else {
3275
+ this.listRender.update(this.editor.children, this.editor, this.context);
3276
+ }
2706
3277
  }
2707
- this.scheduleMeasureVisibleHeights();
2708
3278
  this.cdr.markForCheck();
2709
3279
  }
2710
3280
  }
@@ -2735,9 +3305,36 @@ class SlateEditable {
2735
3305
  this.addEventListener(event.name, () => { });
2736
3306
  });
2737
3307
  }
3308
+ calculateVirtualScrollSelection(selection) {
3309
+ if (selection) {
3310
+ const indics = Array.from(this.inViewportIndics.values());
3311
+ if (indics.length > 0) {
3312
+ const currentVisibleRange = {
3313
+ anchor: Editor.start(this.editor, [indics[0]]),
3314
+ focus: Editor.end(this.editor, [indics[indics.length - 1]])
3315
+ };
3316
+ const [start, end] = Range.edges(selection);
3317
+ const forwardSelection = { anchor: start, focus: end };
3318
+ const intersectedSelection = Range.intersection(forwardSelection, currentVisibleRange);
3319
+ EDITOR_TO_VIRTUAL_SCROLL_SELECTION.set(this.editor, intersectedSelection);
3320
+ if (!intersectedSelection || !Range.equals(intersectedSelection, forwardSelection)) {
3321
+ if (isDebug) {
3322
+ this.debugLog('log', `selection is not in visible range, selection: ${JSON.stringify(selection)}, intersectedSelection: ${JSON.stringify(intersectedSelection)}`);
3323
+ }
3324
+ return intersectedSelection;
3325
+ }
3326
+ return selection;
3327
+ }
3328
+ }
3329
+ EDITOR_TO_VIRTUAL_SCROLL_SELECTION.set(this.editor, null);
3330
+ return selection;
3331
+ }
2738
3332
  toNativeSelection() {
2739
3333
  try {
2740
- const { selection } = this.editor;
3334
+ let { selection } = this.editor;
3335
+ if (this.isEnabledVirtualScroll()) {
3336
+ selection = this.calculateVirtualScrollSelection(selection);
3337
+ }
2741
3338
  const root = AngularEditor.findDocumentOrShadowRoot(this.editor);
2742
3339
  const { activeElement } = root;
2743
3340
  const domSelection = root.getSelection();
@@ -2825,10 +3422,12 @@ class SlateEditable {
2825
3422
  ngDoCheck() { }
2826
3423
  forceRender() {
2827
3424
  this.updateContext();
2828
- const virtualView = this.refreshVirtualView();
2829
- this.applyVirtualView(virtualView);
2830
- this.listRender.update(virtualView.renderedChildren, this.editor, this.context);
2831
- this.scheduleMeasureVisibleHeights();
3425
+ if (this.isEnabledVirtualScroll()) {
3426
+ this.updateListRenderAndRemeasureHeights();
3427
+ }
3428
+ else {
3429
+ this.listRender.update(this.editor.children, this.editor, this.context);
3430
+ }
2832
3431
  // repair collaborative editing when Chinese input is interrupted by other users' cursors
2833
3432
  // when the DOMElement where the selection is located is removed
2834
3433
  // the compositionupdate and compositionend events will no longer be fired
@@ -2867,12 +3466,32 @@ class SlateEditable {
2867
3466
  render() {
2868
3467
  const changed = this.updateContext();
2869
3468
  if (changed) {
2870
- const virtualView = this.refreshVirtualView();
2871
- this.applyVirtualView(virtualView);
2872
- this.listRender.update(virtualView.renderedChildren, this.editor, this.context);
2873
- this.scheduleMeasureVisibleHeights();
3469
+ if (this.isEnabledVirtualScroll()) {
3470
+ this.updateListRenderAndRemeasureHeights();
3471
+ }
3472
+ else {
3473
+ this.listRender.update(this.editor.children, this.editor, this.context);
3474
+ }
2874
3475
  }
2875
3476
  }
3477
+ updateListRenderAndRemeasureHeights() {
3478
+ const virtualView = this.calculateVirtualViewport();
3479
+ const oldInViewportChildren = this.inViewportChildren;
3480
+ this.applyVirtualView(virtualView);
3481
+ this.listRender.update(this.inViewportChildren, this.editor, this.context);
3482
+ // 新增或者修改的才需要重算,计算出这个结果
3483
+ const remeasureIndics = [];
3484
+ const newInViewportIndics = Array.from(this.inViewportIndics);
3485
+ this.inViewportChildren.forEach((child, index) => {
3486
+ if (oldInViewportChildren.indexOf(child) === -1) {
3487
+ remeasureIndics.push(newInViewportIndics[index]);
3488
+ }
3489
+ });
3490
+ if (isDebug && remeasureIndics.length > 0) {
3491
+ console.log('remeasure height by indics: ', remeasureIndics);
3492
+ }
3493
+ this.remeasureHeightByIndics(remeasureIndics);
3494
+ }
2876
3495
  updateContext() {
2877
3496
  const decorations = this.generateDecorations();
2878
3497
  if (this.context.selection !== this.editor.selection ||
@@ -2933,105 +3552,174 @@ class SlateEditable {
2933
3552
  decorations.push(...placeholderDecorations);
2934
3553
  return decorations;
2935
3554
  }
2936
- shouldUseVirtual() {
2937
- return !!(this.virtualConfig && this.virtualConfig.enabled);
3555
+ isEnabledVirtualScroll() {
3556
+ return !!(this.virtualScrollConfig && this.virtualScrollConfig.enabled);
2938
3557
  }
2939
- initializeVirtualScrolling() {
3558
+ initializeVirtualScroll() {
2940
3559
  if (this.virtualScrollInitialized) {
2941
3560
  return;
2942
3561
  }
2943
- if (this.virtualConfig && this.virtualConfig.enabled) {
3562
+ if (this.isEnabledVirtualScroll()) {
2944
3563
  this.virtualScrollInitialized = true;
2945
3564
  this.virtualTopHeightElement = document.createElement('div');
2946
3565
  this.virtualTopHeightElement.classList.add('virtual-top-height');
3566
+ this.virtualTopHeightElement.contentEditable = 'false';
2947
3567
  this.virtualBottomHeightElement = document.createElement('div');
2948
3568
  this.virtualBottomHeightElement.classList.add('virtual-bottom-height');
3569
+ this.virtualBottomHeightElement.contentEditable = 'false';
2949
3570
  this.virtualCenterOutlet = document.createElement('div');
2950
3571
  this.virtualCenterOutlet.classList.add('virtual-center-outlet');
2951
3572
  this.elementRef.nativeElement.appendChild(this.virtualTopHeightElement);
2952
3573
  this.elementRef.nativeElement.appendChild(this.virtualCenterOutlet);
2953
3574
  this.elementRef.nativeElement.appendChild(this.virtualBottomHeightElement);
3575
+ let editorResizeObserverRectWidth = this.elementRef.nativeElement.getBoundingClientRect()?.width ?? 0;
3576
+ this.editorResizeObserver = new ResizeObserver(entries => {
3577
+ if (entries.length > 0 && entries[0].contentRect.width !== editorResizeObserverRectWidth) {
3578
+ editorResizeObserverRectWidth = entries[0].contentRect.width;
3579
+ const remeasureIndics = Array.from(this.inViewportIndics);
3580
+ this.remeasureHeightByIndics(remeasureIndics);
3581
+ }
3582
+ });
3583
+ this.editorResizeObserver.observe(this.elementRef.nativeElement);
3584
+ if (isDebug) {
3585
+ const doc = this.elementRef?.nativeElement?.ownerDocument ?? document;
3586
+ VirtualScrollDebugOverlay.getInstance(doc);
3587
+ }
2954
3588
  }
2955
3589
  }
2956
- changeVirtualHeight(topHeight, bottomHeight) {
3590
+ setVirtualSpaceHeight(topHeight, bottomHeight) {
2957
3591
  if (!this.virtualScrollInitialized) {
2958
3592
  return;
2959
3593
  }
2960
3594
  this.virtualTopHeightElement.style.height = `${topHeight}px`;
2961
3595
  this.virtualBottomHeightElement.style.height = `${bottomHeight}px`;
2962
3596
  }
2963
- refreshVirtualView() {
3597
+ debugLog(type, ...args) {
3598
+ const doc = this.elementRef?.nativeElement?.ownerDocument ?? document;
3599
+ VirtualScrollDebugOverlay.log(doc, type, ...args);
3600
+ }
3601
+ tryUpdateVirtualViewport() {
3602
+ this.tryUpdateVirtualViewportAnimId && cancelAnimationFrame(this.tryUpdateVirtualViewportAnimId);
3603
+ this.tryUpdateVirtualViewportAnimId = requestAnimationFrame(() => {
3604
+ let virtualView = this.calculateVirtualViewport();
3605
+ let diff = this.diffVirtualViewport(virtualView);
3606
+ if (!diff.isDiff) {
3607
+ return;
3608
+ }
3609
+ // diff.isAddedTop
3610
+ if (diff.isMissingTop) {
3611
+ const remeasureIndics = diff.diffTopRenderedIndexes;
3612
+ const result = this.remeasureHeightByIndics(remeasureIndics);
3613
+ if (result) {
3614
+ virtualView = this.calculateVirtualViewport();
3615
+ diff = this.diffVirtualViewport(virtualView, 'second');
3616
+ if (!diff.isDiff) {
3617
+ return;
3618
+ }
3619
+ }
3620
+ }
3621
+ this.applyVirtualView(virtualView);
3622
+ if (this.listRender.initialized) {
3623
+ this.listRender.update(virtualView.inViewportChildren, this.editor, this.context);
3624
+ if (!AngularEditor.isReadOnly(this.editor) && this.editor.selection) {
3625
+ this.toNativeSelection();
3626
+ }
3627
+ }
3628
+ this.tryMeasureInViewportChildrenHeights();
3629
+ });
3630
+ }
3631
+ calculateVirtualViewport() {
2964
3632
  const children = (this.editor.children || []);
2965
- if (!children.length || !this.shouldUseVirtual()) {
3633
+ if (!children.length || !this.isEnabledVirtualScroll()) {
2966
3634
  return {
2967
- renderedChildren: children,
3635
+ inViewportChildren: children,
2968
3636
  visibleIndexes: new Set(),
2969
3637
  top: 0,
2970
3638
  bottom: 0,
2971
3639
  heights: []
2972
3640
  };
2973
3641
  }
2974
- const scrollTop = this.virtualConfig.scrollTop ?? 0;
2975
- const viewportHeight = this.virtualConfig.viewportHeight ?? 0;
3642
+ const scrollTop = this.virtualScrollConfig.scrollTop;
3643
+ if (isDebug) {
3644
+ const doc = this.elementRef?.nativeElement?.ownerDocument ?? document;
3645
+ VirtualScrollDebugOverlay.syncScrollTop(doc, Number.isFinite(scrollTop) ? scrollTop : 0);
3646
+ }
3647
+ const viewportHeight = this.virtualScrollConfig.viewportHeight ?? 0;
2976
3648
  if (!viewportHeight) {
2977
- // 已经启用虚拟滚动,但可视区域高度还未获取到,先置空不渲染
2978
3649
  return {
2979
- renderedChildren: [],
3650
+ inViewportChildren: [],
2980
3651
  visibleIndexes: new Set(),
2981
3652
  top: 0,
2982
3653
  bottom: 0,
2983
3654
  heights: []
2984
3655
  };
2985
3656
  }
2986
- const bufferCount = this.virtualConfig.bufferCount ?? VIRTUAL_SCROLL_DEFAULT_BUFFER_COUNT;
2987
- const heights = children.map((_, idx) => this.getBlockHeight(idx));
2988
- const accumulatedHeights = this.buildAccumulatedHeight(heights);
2989
- let visibleStart = 0;
2990
- // 按真实或估算高度往后累加,找到滚动起点所在块
2991
- while (visibleStart < heights.length && accumulatedHeights[visibleStart + 1] <= scrollTop) {
2992
- visibleStart++;
2993
- }
2994
- // 向上预留 bufferCount 块
2995
- const startIndex = Math.max(0, visibleStart - bufferCount);
2996
- const top = accumulatedHeights[startIndex];
2997
- const bufferBelowHeight = this.getBufferBelowHeight(viewportHeight, visibleStart, bufferCount);
2998
- const targetHeight = accumulatedHeights[visibleStart] - top + viewportHeight + bufferBelowHeight;
3657
+ const elementLength = children.length;
3658
+ if (!EDITOR_TO_BUSINESS_TOP.has(this.editor)) {
3659
+ EDITOR_TO_BUSINESS_TOP.set(this.editor, 0);
3660
+ setTimeout(() => {
3661
+ const virtualTopBoundingTop = this.virtualTopHeightElement.getBoundingClientRect()?.top ?? 0;
3662
+ const businessTop = Math.ceil(virtualTopBoundingTop) +
3663
+ Math.ceil(this.virtualScrollConfig.scrollTop) -
3664
+ Math.floor(this.virtualScrollConfig.viewportBoundingTop);
3665
+ EDITOR_TO_BUSINESS_TOP.set(this.editor, businessTop);
3666
+ if (isDebug) {
3667
+ this.debugLog('log', 'businessTop', businessTop);
3668
+ }
3669
+ }, 100);
3670
+ }
3671
+ const adjustedScrollTop = Math.max(0, scrollTop - getBusinessTop(this.editor));
3672
+ const { heights, accumulatedHeights } = buildHeightsAndAccumulatedHeights(this.editor);
3673
+ const totalHeight = accumulatedHeights[elementLength];
3674
+ const maxScrollTop = Math.max(0, totalHeight - viewportHeight);
3675
+ const limitedScrollTop = Math.min(adjustedScrollTop, maxScrollTop);
3676
+ const viewBottom = limitedScrollTop + viewportHeight + getBusinessTop(this.editor);
3677
+ let accumulatedOffset = 0;
3678
+ let visibleStartIndex = -1;
2999
3679
  const visible = [];
3000
3680
  const visibleIndexes = [];
3001
- let accumulated = 0;
3002
- let cursor = startIndex;
3003
- // 循环累计高度超出目标高度(可视高度 + 上下 buffer)
3004
- while (cursor < children.length && accumulated < targetHeight) {
3005
- visible.push(children[cursor]);
3006
- visibleIndexes.push(cursor);
3007
- accumulated += this.getBlockHeight(cursor);
3008
- cursor++;
3009
- }
3010
- const bottom = heights.slice(cursor).reduce((acc, height) => acc + height, 0);
3011
- const renderedChildren = visible.length ? visible : children;
3012
- const visibleIndexesSet = new Set(visibleIndexes);
3681
+ for (let i = 0; i < elementLength && accumulatedOffset < viewBottom; i++) {
3682
+ const currentHeight = heights[i];
3683
+ const nextOffset = accumulatedOffset + currentHeight;
3684
+ // 可视区域有交集,加入渲染
3685
+ if (nextOffset > limitedScrollTop && accumulatedOffset < viewBottom) {
3686
+ if (visibleStartIndex === -1)
3687
+ visibleStartIndex = i; // 第一个相交起始位置
3688
+ visible.push(children[i]);
3689
+ visibleIndexes.push(i);
3690
+ }
3691
+ accumulatedOffset = nextOffset;
3692
+ }
3693
+ if (visibleStartIndex === -1 && elementLength) {
3694
+ visibleStartIndex = elementLength - 1;
3695
+ visible.push(children[visibleStartIndex]);
3696
+ visibleIndexes.push(visibleStartIndex);
3697
+ }
3698
+ const visibleEndIndex = visibleStartIndex === -1 ? elementLength - 1 : (visibleIndexes[visibleIndexes.length - 1] ?? visibleStartIndex);
3699
+ const top = visibleStartIndex === -1 ? 0 : accumulatedHeights[visibleStartIndex];
3700
+ const bottom = totalHeight - accumulatedHeights[visibleEndIndex + 1];
3013
3701
  return {
3014
- renderedChildren,
3015
- visibleIndexes: visibleIndexesSet,
3702
+ inViewportChildren: visible.length ? visible : children,
3703
+ visibleIndexes: new Set(visibleIndexes),
3016
3704
  top,
3017
3705
  bottom,
3018
3706
  heights
3019
3707
  };
3020
3708
  }
3021
3709
  applyVirtualView(virtualView) {
3022
- this.renderedChildren = virtualView.renderedChildren;
3023
- this.changeVirtualHeight(virtualView.top, virtualView.bottom);
3024
- this.virtualVisibleIndexes = virtualView.visibleIndexes;
3710
+ this.inViewportChildren = virtualView.inViewportChildren;
3711
+ this.setVirtualSpaceHeight(virtualView.top, virtualView.bottom);
3712
+ this.inViewportIndics = virtualView.visibleIndexes;
3025
3713
  }
3026
- diffVirtualView(virtualView) {
3027
- if (!this.renderedChildren.length) {
3714
+ diffVirtualViewport(virtualView, stage = 'first') {
3715
+ if (!this.inViewportChildren.length) {
3028
3716
  return {
3029
3717
  isDiff: true,
3030
3718
  diffTopRenderedIndexes: [],
3031
3719
  diffBottomRenderedIndexes: []
3032
3720
  };
3033
3721
  }
3034
- const oldVisibleIndexes = [...this.virtualVisibleIndexes];
3722
+ const oldVisibleIndexes = [...this.inViewportIndics];
3035
3723
  const newVisibleIndexes = [...virtualView.visibleIndexes];
3036
3724
  const firstNewIndex = newVisibleIndexes[0];
3037
3725
  const lastNewIndex = newVisibleIndexes[newVisibleIndexes.length - 1];
@@ -3087,17 +3775,18 @@ class SlateEditable {
3087
3775
  }
3088
3776
  }
3089
3777
  if (isDebug) {
3090
- console.log('oldVisibleIndexes:', oldVisibleIndexes);
3091
- console.log('newVisibleIndexes:', newVisibleIndexes);
3092
- console.log('diffTopRenderedIndexes:', isMissingTop ? '-' : isAddedTop ? '+' : '-', diffTopRenderedIndexes, diffTopRenderedIndexes.map(index => this.getBlockHeight(index, 0)));
3093
- console.log('diffBottomRenderedIndexes:', isAddedBottom ? '+' : isMissingBottom ? '-' : '+', diffBottomRenderedIndexes, diffBottomRenderedIndexes.map(index => this.getBlockHeight(index, 0)));
3778
+ this.debugLog('log', `====== diffVirtualViewport stage: ${stage} ======`);
3779
+ this.debugLog('log', 'oldVisibleIndexes:', oldVisibleIndexes);
3780
+ this.debugLog('log', 'newVisibleIndexes:', newVisibleIndexes);
3781
+ this.debugLog('log', 'diffTopRenderedIndexes:', isMissingTop ? '-' : isAddedTop ? '+' : '-', diffTopRenderedIndexes, diffTopRenderedIndexes.map(index => getRealHeightByElement(this.editor, this.editor.children[index], 0)));
3782
+ this.debugLog('log', 'diffBottomRenderedIndexes:', isAddedBottom ? '+' : isMissingBottom ? '-' : '+', diffBottomRenderedIndexes, diffBottomRenderedIndexes.map(index => getRealHeightByElement(this.editor, this.editor.children[index], 0)));
3094
3783
  const needTop = virtualView.heights.slice(0, newVisibleIndexes[0]).reduce((acc, height) => acc + height, 0);
3095
3784
  const needBottom = virtualView.heights
3096
3785
  .slice(newVisibleIndexes[newVisibleIndexes.length - 1] + 1)
3097
3786
  .reduce((acc, height) => acc + height, 0);
3098
- console.log('newTopHeight:', needTop, 'prevTopHeight:', parseFloat(this.virtualTopHeightElement.style.height));
3099
- console.log('newBottomHeight:', needBottom, 'prevBottomHeight:', parseFloat(this.virtualBottomHeightElement.style.height));
3100
- console.warn('=========== Dividing line ===========');
3787
+ this.debugLog('log', 'newTopHeight:', needTop, 'prevTopHeight:', parseFloat(this.virtualTopHeightElement.style.height));
3788
+ this.debugLog('log', 'newBottomHeight:', needBottom, 'prevBottomHeight:', parseFloat(this.virtualBottomHeightElement.style.height));
3789
+ this.debugLog('warn', '=========== Dividing line ===========');
3101
3790
  }
3102
3791
  return {
3103
3792
  isDiff: true,
@@ -3115,76 +3804,46 @@ class SlateEditable {
3115
3804
  diffBottomRenderedIndexes: []
3116
3805
  };
3117
3806
  }
3118
- getBlockHeight(index, defaultHeight = VIRTUAL_SCROLL_DEFAULT_BLOCK_HEIGHT) {
3119
- const node = this.editor.children[index];
3120
- if (!node) {
3121
- return defaultHeight;
3122
- }
3123
- const key = AngularEditor.findKey(this.editor, node);
3124
- return this.measuredHeights.get(key.id) ?? defaultHeight;
3125
- }
3126
- buildAccumulatedHeight(heights) {
3127
- const accumulatedHeights = new Array(heights.length + 1).fill(0);
3128
- for (let i = 0; i < heights.length; i++) {
3129
- // 存储前 i 个的累计高度
3130
- accumulatedHeights[i + 1] = accumulatedHeights[i] + heights[i];
3131
- }
3132
- return accumulatedHeights;
3133
- }
3134
- getBufferBelowHeight(viewportHeight, visibleStart, bufferCount) {
3135
- let blockHeight = 0;
3136
- let start = visibleStart;
3137
- // 循环累计高度超出视图高度代表找到向下缓冲区的起始位置
3138
- while (blockHeight < viewportHeight) {
3139
- blockHeight += this.getBlockHeight(start);
3140
- start++;
3141
- }
3142
- let bufferHeight = 0;
3143
- for (let i = start; i < start + bufferCount; i++) {
3144
- bufferHeight += this.getBlockHeight(i);
3145
- }
3146
- return bufferHeight;
3147
- }
3148
- scheduleMeasureVisibleHeights() {
3149
- if (!this.shouldUseVirtual()) {
3807
+ tryMeasureInViewportChildrenHeights() {
3808
+ if (!this.isEnabledVirtualScroll()) {
3150
3809
  return;
3151
3810
  }
3152
- if (this.measurePending) {
3153
- return;
3154
- }
3155
- this.measurePending = true;
3156
- this.measureVisibleHeightsAnimId && cancelAnimationFrame(this.measureVisibleHeightsAnimId);
3157
- this.measureVisibleHeightsAnimId = requestAnimationFrame(() => {
3811
+ this.tryMeasureInViewportChildrenHeightsAnimId && cancelAnimationFrame(this.tryMeasureInViewportChildrenHeightsAnimId);
3812
+ this.tryMeasureInViewportChildrenHeightsAnimId = requestAnimationFrame(() => {
3158
3813
  this.measureVisibleHeights();
3159
- this.measurePending = false;
3160
3814
  });
3161
3815
  }
3162
3816
  measureVisibleHeights() {
3163
3817
  const children = (this.editor.children || []);
3164
- this.virtualVisibleIndexes.forEach(index => {
3818
+ this.inViewportIndics.forEach(index => {
3165
3819
  const node = children[index];
3166
3820
  if (!node) {
3167
3821
  return;
3168
3822
  }
3169
3823
  const key = AngularEditor.findKey(this.editor, node);
3170
- // 跳过已测过的块
3171
- if (this.measuredHeights.has(key.id)) {
3824
+ // 跳过已测过的块,除非强制测量
3825
+ if (this.keyHeightMap.has(key.id)) {
3172
3826
  return;
3173
3827
  }
3174
3828
  const view = ELEMENT_TO_COMPONENT.get(node);
3175
3829
  if (!view) {
3176
3830
  return;
3177
3831
  }
3178
- view.getRealHeight()?.then(height => {
3179
- this.measuredHeights.set(key.id, height);
3180
- });
3832
+ const ret = view.getRealHeight();
3833
+ if (ret instanceof Promise) {
3834
+ ret.then(height => {
3835
+ this.keyHeightMap.set(key.id, height);
3836
+ });
3837
+ }
3838
+ else {
3839
+ this.keyHeightMap.set(key.id, ret);
3840
+ }
3181
3841
  });
3182
3842
  }
3183
- async measureHeightByIndexes(indexes, isRefresh = false) {
3843
+ remeasureHeightByIndics(indics) {
3184
3844
  const children = (this.editor.children || []);
3185
3845
  let isHeightChanged = false;
3186
- const promises = [];
3187
- indexes.forEach(index => {
3846
+ indics.forEach((index, i) => {
3188
3847
  const node = children[index];
3189
3848
  if (!node) {
3190
3849
  return;
@@ -3194,27 +3853,30 @@ class SlateEditable {
3194
3853
  if (!view) {
3195
3854
  return;
3196
3855
  }
3197
- const promise = view.getRealHeight()?.then(height => {
3198
- const prevHeight = this.measuredHeights.get(key.id);
3199
- if (isDebug) {
3200
- console.log('measureHeightByIndexes: get index:', index, 'prevHeight:', prevHeight, 'newHeight:', height);
3201
- }
3202
- if (prevHeight && height !== prevHeight) {
3203
- this.measuredHeights.set(key.id, height);
3856
+ const prevHeight = this.keyHeightMap.get(key.id);
3857
+ const ret = view.getRealHeight();
3858
+ if (ret instanceof Promise) {
3859
+ ret.then(height => {
3860
+ this.keyHeightMap.set(key.id, height);
3861
+ if (height !== prevHeight) {
3862
+ isHeightChanged = true;
3863
+ if (isDebug) {
3864
+ this.debugLog('log', `remeasure element height, index: ${index} prevHeight: ${prevHeight} newHeight: ${height}`);
3865
+ }
3866
+ }
3867
+ });
3868
+ }
3869
+ else {
3870
+ this.keyHeightMap.set(key.id, ret);
3871
+ if (ret !== prevHeight) {
3204
3872
  isHeightChanged = true;
3873
+ if (isDebug) {
3874
+ this.debugLog('log', `remeasure element height, index: ${index} prevHeight: ${prevHeight} newHeight: ${ret}`);
3875
+ }
3205
3876
  }
3206
- });
3207
- if (promise) {
3208
- promises.push(promise);
3209
3877
  }
3210
3878
  });
3211
- if (promises.length > 0) {
3212
- await Promise.all(promises);
3213
- if (isHeightChanged && isRefresh) {
3214
- return this.refreshVirtualView();
3215
- }
3216
- }
3217
- return null;
3879
+ return isHeightChanged;
3218
3880
  }
3219
3881
  //#region event proxy
3220
3882
  addEventListener(eventName, listener, target = this.elementRef.nativeElement) {
@@ -3229,6 +3891,9 @@ class SlateEditable {
3229
3891
  toSlateSelection() {
3230
3892
  if ((!this.isComposing || IS_ANDROID) && !this.isUpdatingSelection && !this.isDraggingInternally) {
3231
3893
  try {
3894
+ if (isDebug) {
3895
+ console.log('toSlateSelection');
3896
+ }
3232
3897
  const root = AngularEditor.findDocumentOrShadowRoot(this.editor);
3233
3898
  const { activeElement } = root;
3234
3899
  const el = AngularEditor.toDOMNode(this.editor, this.editor);
@@ -3740,6 +4405,11 @@ class SlateEditable {
3740
4405
  Transforms.move(editor, { unit: 'word', reverse: isRTL });
3741
4406
  return;
3742
4407
  }
4408
+ if (isKeyHotkey('mod+a', event)) {
4409
+ this.editor.selectAll();
4410
+ event.preventDefault();
4411
+ return;
4412
+ }
3743
4413
  // COMPAT: Certain browsers don't support the `beforeinput` event, so we
3744
4414
  // fall back to guessing at the input intention for hotkeys.
3745
4415
  // COMPAT: In iOS, some of these hotkeys are handled in the
@@ -3907,6 +4577,7 @@ class SlateEditable {
3907
4577
  }
3908
4578
  //#endregion
3909
4579
  ngOnDestroy() {
4580
+ this.editorResizeObserver?.disconnect();
3910
4581
  NODE_TO_ELEMENT.delete(this.editor);
3911
4582
  this.manualListeners.forEach(manualListener => {
3912
4583
  manualListener();
@@ -4166,6 +4837,7 @@ class BaseElementComponent extends BaseComponent {
4166
4837
  }
4167
4838
  return null;
4168
4839
  };
4840
+ this.stableHeight = null;
4169
4841
  }
4170
4842
  get element() {
4171
4843
  return this._context && this._context.element;
@@ -4219,6 +4891,7 @@ class BaseElementComponent extends BaseComponent {
4219
4891
  if (ELEMENT_TO_COMPONENT.get(this.element) === this) {
4220
4892
  ELEMENT_TO_COMPONENT.delete(this.element);
4221
4893
  }
4894
+ this.listRender.destroy();
4222
4895
  }
4223
4896
  onContextChange() {
4224
4897
  this.childrenContext = this.getChildrenContext();
@@ -4247,11 +4920,21 @@ class BaseElementComponent extends BaseComponent {
4247
4920
  readonly: this._context.readonly
4248
4921
  };
4249
4922
  }
4923
+ isStableHeight() {
4924
+ return false;
4925
+ }
4250
4926
  getRealHeight() {
4927
+ if (this.isStableHeight() && this.stableHeight !== null) {
4928
+ return this.stableHeight;
4929
+ }
4251
4930
  const blockCard = getBlockCardByNativeElement(this.nativeElement);
4252
4931
  const target = blockCard || this.nativeElement;
4253
4932
  const computedStyle = getComputedStyle(target);
4254
- return Promise.resolve(target.offsetHeight + parseFloat(computedStyle.marginTop) + parseFloat(computedStyle.marginBottom));
4933
+ const height = Math.ceil(target.getBoundingClientRect().height) + parseFloat(computedStyle.marginTop) + parseFloat(computedStyle.marginBottom);
4934
+ if (this.isStableHeight()) {
4935
+ this.stableHeight = height;
4936
+ }
4937
+ return height;
4255
4938
  }
4256
4939
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.12", ngImport: i0, type: BaseElementComponent, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
4257
4940
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.12", type: BaseElementComponent, isStandalone: true, viewQueries: [{ propertyName: "childrenOutletInstance", first: true, predicate: SlateChildrenOutlet, descendants: true, static: true }], usesInheritance: true, ngImport: i0 }); }
@@ -4296,6 +4979,7 @@ class BaseTextComponent extends BaseComponent {
4296
4979
  NODE_TO_ELEMENT.delete(this.text);
4297
4980
  }
4298
4981
  ELEMENT_TO_NODE.delete(this.nativeElement);
4982
+ this.leavesRender.destroy();
4299
4983
  }
4300
4984
  onContextChange() {
4301
4985
  this.updateWeakMap();
@@ -4407,5 +5091,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.12", ngImpo
4407
5091
  * Generated bundle index. Do not edit.
4408
5092
  */
4409
5093
 
4410
- export { AngularEditor, BaseComponent, BaseElementComponent, BaseElementFlavour, BaseFlavour, BaseLeafComponent, BaseLeafFlavour, BaseTextComponent, BaseTextFlavour, BlockCardRef, DEFAULT_ELEMENT_HEIGHT, DefaultTextFlavour, EDITOR_TO_AFTER_VIEW_INIT_QUEUE, ELEMENT_TO_COMPONENT, FAKE_LEFT_BLOCK_CARD_OFFSET, FAKE_RIGHT_BLOCK_CARD_OFFSET, FlavourRef, HAS_BEFORE_INPUT_SUPPORT, IS_ANDROID, IS_APPLE, IS_CHROME, IS_CHROME_LEGACY, IS_EDGE_LEGACY, IS_FIREFOX, IS_FIREFOX_LEGACY, IS_IOS, IS_QQBROWSER, IS_SAFARI, IS_UC_MOBILE, IS_WECHATBROWSER, JUST_NOW_UPDATED_VIRTUAL_VIEW, PLACEHOLDER_SYMBOL, SLATE_BLOCK_CARD_CLASS_NAME, SLATE_DEBUG_KEY, SlateBlockCard, SlateChildrenOutlet, SlateEditable, SlateErrorCode, SlateFragmentAttributeKey, SlateModule, VIRTUAL_SCROLL_DEFAULT_BLOCK_HEIGHT, VIRTUAL_SCROLL_DEFAULT_BUFFER_COUNT, VoidTextFlavour, blobAsString, buildHTMLText, check, completeTable, createClipboardData, createText, createThrottleRAF, defaultScrollSelectionIntoView, fallbackCopyText, getBlockCardByNativeElement, getCardTargetAttribute, getClipboardData, getClipboardFromHTMLText, getContentHeight, getDataTransferClipboard, getDataTransferClipboardText, getNavigatorClipboard, getPlainText, getSelection, getSlateFragmentAttribute, getZeroTextNode, hasAfterContextChange, hasBeforeContextChange, hasBlockCard, hasBlockCardWithNode, hotkeys, isCardCenterByTargetAttr, isCardLeft, isCardLeftByTargetAttr, isCardRightByTargetAttr, isClipboardFile, isClipboardReadSupported, isClipboardWriteSupported, isClipboardWriteTextSupported, isComponentType, isDOMText, isDecoratorRangeListEqual, isFlavourType, isInvalidTable, isTemplateRef, isValid, normalize, setClipboardData, setDataTransferClipboard, setDataTransferClipboardText, setNavigatorClipboard, shallowCompare, stripHtml, withAngular };
5094
+ export { AngularEditor, BaseComponent, BaseElementComponent, BaseElementFlavour, BaseFlavour, BaseLeafComponent, BaseLeafFlavour, BaseTextComponent, BaseTextFlavour, BlockCardRef, DEFAULT_ELEMENT_HEIGHT, DefaultTextFlavour, EDITOR_TO_AFTER_VIEW_INIT_QUEUE, EDITOR_TO_BUSINESS_TOP, EDITOR_TO_VIRTUAL_SCROLL_SELECTION, ELEMENT_KEY_TO_HEIGHTS, ELEMENT_TO_COMPONENT, FAKE_LEFT_BLOCK_CARD_OFFSET, FAKE_RIGHT_BLOCK_CARD_OFFSET, FlavourRef, HAS_BEFORE_INPUT_SUPPORT, IS_ANDROID, IS_APPLE, IS_CHROME, IS_CHROME_LEGACY, IS_EDGE_LEGACY, IS_ENABLED_VIRTUAL_SCROLL, IS_FIREFOX, IS_FIREFOX_LEGACY, IS_IOS, IS_QQBROWSER, IS_SAFARI, IS_UC_MOBILE, IS_WECHATBROWSER, PLACEHOLDER_SYMBOL, SLATE_BLOCK_CARD_CLASS_NAME, SLATE_DEBUG_KEY, SlateBlockCard, SlateChildrenOutlet, SlateEditable, SlateErrorCode, SlateFragmentAttributeKey, SlateModule, VIRTUAL_SCROLL_DEFAULT_BLOCK_HEIGHT, VoidTextFlavour, blobAsString, buildHTMLText, buildHeightsAndAccumulatedHeights, check, completeTable, createClipboardData, createText, createThrottleRAF, defaultScrollSelectionIntoView, fallbackCopyText, getBlockCardByNativeElement, getBusinessTop, getCardTargetAttribute, getClipboardData, getClipboardFromHTMLText, getContentHeight, getDataTransferClipboard, getDataTransferClipboardText, getNavigatorClipboard, getPlainText, getRealHeightByElement, getSelection, getSlateFragmentAttribute, getZeroTextNode, hasAfterContextChange, hasBeforeContextChange, hasBlockCard, hasBlockCardWithNode, hotkeys, isCardCenterByTargetAttr, isCardLeft, isCardLeftByTargetAttr, isCardRightByTargetAttr, isClipboardFile, isClipboardReadSupported, isClipboardWriteSupported, isClipboardWriteTextSupported, isComponentType, isDOMText, isDecoratorRangeListEqual, isFlavourType, isInvalidTable, isTemplateRef, isValid, normalize, scrollToElement, setClipboardData, setDataTransferClipboard, setDataTransferClipboardText, setNavigatorClipboard, shallowCompare, stripHtml, withAngular };
4411
5095
  //# sourceMappingURL=slate-angular.mjs.map