@mhamz.01/easyflow-texteditor 0.1.150 → 0.1.152

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -2874,6 +2874,115 @@ function useTiptapEditor(providedEditor) {
2874
2874
  return editorState || { editor: null };
2875
2875
  }
2876
2876
 
2877
+ // src/hooks/mark-preservers/mark-preserver.ts
2878
+ var MarkPreserver = class {
2879
+ static preservedStyles = /* @__PURE__ */ new Map();
2880
+ /**
2881
+ * Preserve current font and color before blur or node change
2882
+ */
2883
+ static preserve(editor) {
2884
+ if (!editor) return;
2885
+ try {
2886
+ const { state } = editor;
2887
+ const { $from } = state.selection;
2888
+ const currentNode = $from.parent;
2889
+ let font = currentNode.attrs.fontFamily;
2890
+ let color = currentNode.attrs.color;
2891
+ const marks = state.storedMarks || $from.marks();
2892
+ const textStyleMark = marks.find((m) => m.type.name === "textStyle");
2893
+ if (!font && textStyleMark?.attrs.fontFamily) {
2894
+ font = textStyleMark.attrs.fontFamily;
2895
+ }
2896
+ if (!color && textStyleMark?.attrs.color) {
2897
+ color = textStyleMark.attrs.color;
2898
+ }
2899
+ if (font || color) {
2900
+ this.preservedStyles.set(editor, { font, color });
2901
+ }
2902
+ } catch (error) {
2903
+ console.debug("MarkPreserver.preserve error:", error);
2904
+ }
2905
+ }
2906
+ /**
2907
+ * Restore preserved styles to current node
2908
+ */
2909
+ static restore(editor, delay = 0) {
2910
+ if (!editor) return;
2911
+ const styles = this.preservedStyles.get(editor);
2912
+ if (!styles) return;
2913
+ const restoreFn = () => {
2914
+ if (editor.isDestroyed) {
2915
+ this.preservedStyles.delete(editor);
2916
+ return;
2917
+ }
2918
+ try {
2919
+ const { state } = editor;
2920
+ const { $from } = state.selection;
2921
+ const depth = $from.depth;
2922
+ const pos = $from.before(depth);
2923
+ const node = $from.node(depth);
2924
+ const supportsAttributes = [
2925
+ "paragraph",
2926
+ "heading",
2927
+ "listItem",
2928
+ "taskItem",
2929
+ "blockquote"
2930
+ ].includes(node.type.name);
2931
+ if (!supportsAttributes) {
2932
+ this.preservedStyles.delete(editor);
2933
+ editor.commands.focus();
2934
+ return;
2935
+ }
2936
+ const updates = {};
2937
+ if (styles.font && !node.attrs.fontFamily) {
2938
+ updates.fontFamily = styles.font;
2939
+ }
2940
+ if (styles.color && !node.attrs.color) {
2941
+ updates.color = styles.color;
2942
+ }
2943
+ if (Object.keys(updates).length > 0) {
2944
+ const newAttrs = { ...node.attrs, ...updates };
2945
+ editor.view.dispatch(
2946
+ state.tr.setNodeMarkup(pos, void 0, newAttrs)
2947
+ );
2948
+ }
2949
+ const marks = state.storedMarks || $from.marks();
2950
+ const filteredMarks = marks.filter((mark) => mark.type.name !== "textStyle");
2951
+ const textStyleAttrs = {};
2952
+ if (styles.font) textStyleAttrs.fontFamily = styles.font;
2953
+ if (styles.color) textStyleAttrs.color = styles.color;
2954
+ const newMarks = [
2955
+ ...filteredMarks,
2956
+ state.schema.marks.textStyle.create(textStyleAttrs)
2957
+ ];
2958
+ setTimeout(() => {
2959
+ if (!editor.isDestroyed) {
2960
+ editor.view.dispatch(
2961
+ editor.state.tr.setStoredMarks(newMarks)
2962
+ );
2963
+ editor.commands.focus();
2964
+ }
2965
+ }, 0);
2966
+ } catch (error) {
2967
+ console.debug("MarkPreserver.restore error:", error);
2968
+ } finally {
2969
+ this.preservedStyles.delete(editor);
2970
+ }
2971
+ };
2972
+ if (delay > 0) {
2973
+ setTimeout(restoreFn, delay);
2974
+ } else {
2975
+ restoreFn();
2976
+ }
2977
+ }
2978
+ /**
2979
+ * Clear preserved styles
2980
+ */
2981
+ static clear(editor) {
2982
+ this.preservedStyles.delete(editor);
2983
+ }
2984
+ };
2985
+
2877
2986
  // src/components/tiptap-ui/heading-button/heading-button.tsx
2878
2987
  import { forwardRef as forwardRef6, useCallback as useCallback8 } from "react";
2879
2988
 
@@ -3478,9 +3587,15 @@ var HeadingDropdownMenu = forwardRef9(
3478
3587
  if (!editor || !canToggle2) return;
3479
3588
  setIsOpen(open);
3480
3589
  onOpenChange?.(open);
3590
+ if (!open) {
3591
+ MarkPreserver.restore(editor, 10);
3592
+ }
3481
3593
  },
3482
3594
  [canToggle2, editor, onOpenChange]
3483
3595
  );
3596
+ const handlePointerDown = useCallback10(() => {
3597
+ MarkPreserver.preserve(editor);
3598
+ }, [editor]);
3484
3599
  if (!isVisible) {
3485
3600
  return null;
3486
3601
  }
@@ -3496,6 +3611,7 @@ var HeadingDropdownMenu = forwardRef9(
3496
3611
  "aria-label": "Format text as heading",
3497
3612
  "aria-pressed": isActive,
3498
3613
  tooltip: "Heading",
3614
+ onPointerDown: handlePointerDown,
3499
3615
  ...buttonProps,
3500
3616
  ref,
3501
3617
  children: [
@@ -4383,9 +4499,15 @@ function ListDropdownMenu({
4383
4499
  (open) => {
4384
4500
  setIsOpen(open);
4385
4501
  onOpenChange?.(open);
4502
+ if (!open) {
4503
+ MarkPreserver.restore(editor, 10);
4504
+ }
4386
4505
  },
4387
- [onOpenChange]
4506
+ [onOpenChange, editor]
4388
4507
  );
4508
+ const handlePointerDown = useCallback15(() => {
4509
+ MarkPreserver.preserve(editor);
4510
+ }, [editor]);
4389
4511
  if (!isVisible) {
4390
4512
  return null;
4391
4513
  }
@@ -4402,6 +4524,7 @@ function ListDropdownMenu({
4402
4524
  "data-disabled": !canToggle2,
4403
4525
  "aria-label": "List options",
4404
4526
  tooltip: "List",
4527
+ onPointerDown: handlePointerDown,
4405
4528
  ...props,
4406
4529
  children: [
4407
4530
  /* @__PURE__ */ jsx44(Icon, { className: "tiptap-button-icon" }),
@@ -4423,56 +4546,6 @@ function ListDropdownMenu({
4423
4546
 
4424
4547
  // src/components/tiptap-ui/blockquote-button/blockquote-button.tsx
4425
4548
  import { forwardRef as forwardRef12, useCallback as useCallback16 } from "react";
4426
-
4427
- // src/hooks/mark-preservers/mark-preserver.ts
4428
- var MarkPreserver = class {
4429
- static preservedMarks = /* @__PURE__ */ new Map();
4430
- /**
4431
- * Preserve marks before an action that will cause blur
4432
- * Call this in onMouseDown or onPointerDown events
4433
- */
4434
- static preserve(editor) {
4435
- if (!editor) return;
4436
- const { state } = editor;
4437
- const { $from } = state.selection;
4438
- const currentMarks = state.storedMarks || $from.marks();
4439
- if (currentMarks.length > 0) {
4440
- this.preservedMarks.set(editor, [...currentMarks]);
4441
- }
4442
- }
4443
- /**
4444
- * Restore marks after an action completes
4445
- * Call this after the editor action in onClick or in a setTimeout
4446
- */
4447
- static restore(editor, delay = 0) {
4448
- if (!editor) return;
4449
- const marks = this.preservedMarks.get(editor);
4450
- if (!marks) return;
4451
- const restoreFn = () => {
4452
- if (editor.isDestroyed) {
4453
- this.preservedMarks.delete(editor);
4454
- return;
4455
- }
4456
- const { state } = editor;
4457
- editor.view.dispatch(state.tr.setStoredMarks(marks));
4458
- this.preservedMarks.delete(editor);
4459
- editor.commands.focus();
4460
- };
4461
- if (delay > 0) {
4462
- setTimeout(restoreFn, delay);
4463
- } else {
4464
- restoreFn();
4465
- }
4466
- }
4467
- /**
4468
- * Clear preserved marks for an editor
4469
- */
4470
- static clear(editor) {
4471
- this.preservedMarks.delete(editor);
4472
- }
4473
- };
4474
-
4475
- // src/components/tiptap-ui/blockquote-button/blockquote-button.tsx
4476
4549
  import { Fragment as Fragment7, jsx as jsx45, jsxs as jsxs26 } from "react/jsx-runtime";
4477
4550
  function BlockquoteShortcutBadge({
4478
4551
  shortcutKeys = BLOCKQUOTE_SHORTCUT_KEY
@@ -7622,6 +7695,7 @@ function useCursorVisibility({
7622
7695
 
7623
7696
  // src/components/extensions/font-family-block.ts
7624
7697
  import { Extension as Extension2 } from "@tiptap/core";
7698
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
7625
7699
  var FontFamilyBlock = Extension2.create({
7626
7700
  name: "fontFamilyBlock",
7627
7701
  addGlobalAttributes() {
@@ -7678,150 +7752,174 @@ var ColorBlock = Extension2.create({
7678
7752
  ];
7679
7753
  }
7680
7754
  });
7755
+ var stylePersistenceKey = new PluginKey("stylePersistence");
7681
7756
  var StylePersistence = Extension2.create({
7682
7757
  name: "stylePersistence",
7683
7758
  addOptions() {
7684
7759
  return {
7685
- // Marks that should NOT inherit font family or color
7686
7760
  excludedMarks: ["code", "codeBlock"]
7687
7761
  };
7688
7762
  },
7689
- addStorage() {
7690
- return {
7691
- // Store marks temporarily when editor loses focus
7692
- preservedMarks: null,
7693
- // Store marks from the previous node (for Enter key handling)
7694
- previousNodeMarks: null
7695
- };
7696
- },
7697
- addKeyboardShortcuts() {
7698
- return {
7699
- // Handle Enter key to preserve marks when creating new paragraphs
7700
- "Enter": ({ editor }) => {
7701
- const { state } = editor;
7702
- const { $from } = state.selection;
7703
- const currentNode = $from.parent;
7704
- const blockFont = currentNode.attrs.fontFamily;
7705
- const blockColor = currentNode.attrs.color;
7706
- if (blockFont || blockColor) {
7707
- this.storage.previousNodeMarks = { blockFont, blockColor };
7708
- }
7709
- return false;
7710
- },
7711
- // Handle Shift+Enter (soft break) - same logic
7712
- "Shift-Enter": ({ editor }) => {
7713
- const { state } = editor;
7714
- const { $from } = state.selection;
7715
- const currentNode = $from.parent;
7716
- const blockFont = currentNode.attrs.fontFamily;
7717
- const blockColor = currentNode.attrs.color;
7718
- if (blockFont || blockColor) {
7719
- this.storage.previousNodeMarks = { blockFont, blockColor };
7720
- }
7721
- return false;
7722
- }
7723
- };
7724
- },
7725
- onSelectionUpdate({ editor }) {
7763
+ addProseMirrorPlugins() {
7726
7764
  const excludedMarks = this.options.excludedMarks;
7727
- syncStoredMarks(editor, excludedMarks, this.storage);
7728
- },
7729
- onFocus({ editor }) {
7730
- const excludedMarks = this.options.excludedMarks;
7731
- if (this.storage.preservedMarks) {
7732
- editor.view.dispatch(
7733
- editor.state.tr.setStoredMarks(this.storage.preservedMarks)
7734
- );
7735
- this.storage.preservedMarks = null;
7736
- }
7737
- syncStoredMarks(editor, excludedMarks, this.storage);
7738
- },
7739
- onBlur({ editor }) {
7740
- const { state } = editor;
7741
- const { $from } = state.selection;
7742
- const currentMarks = state.storedMarks || $from.marks();
7743
- const hasTextStyle = currentMarks.some((mark) => mark.type.name === "textStyle");
7744
- if (hasTextStyle) {
7745
- this.storage.preservedMarks = currentMarks;
7746
- }
7747
- },
7748
- onCreate({ editor }) {
7749
- const excludedMarks = this.options.excludedMarks;
7750
- syncStoredMarks(editor, excludedMarks, this.storage);
7751
- },
7752
- onTransaction({ editor, transaction }) {
7753
- if (!transaction.selectionSet && !transaction.docChanged) return;
7754
- const excludedMarks = this.options.excludedMarks;
7755
- requestAnimationFrame(() => {
7756
- if (!editor.isDestroyed) {
7757
- syncStoredMarks(editor, excludedMarks, this.storage);
7758
- }
7759
- });
7765
+ return [
7766
+ new Plugin({
7767
+ key: stylePersistenceKey,
7768
+ state: {
7769
+ init() {
7770
+ return {
7771
+ lastFont: null,
7772
+ lastColor: null
7773
+ };
7774
+ },
7775
+ apply(tr, value, oldState, newState) {
7776
+ const { $from } = newState.selection;
7777
+ const marks = $from.marks();
7778
+ const hasExcludedMarks = marks.some(
7779
+ (mark) => excludedMarks.includes(mark.type.name)
7780
+ );
7781
+ let isInCodeBlock = false;
7782
+ for (let depth = $from.depth; depth > 0; depth--) {
7783
+ const node = $from.node(depth);
7784
+ if (node.type.name === "codeBlock") {
7785
+ isInCodeBlock = true;
7786
+ break;
7787
+ }
7788
+ }
7789
+ if (hasExcludedMarks || isInCodeBlock) {
7790
+ return value;
7791
+ }
7792
+ const parentNode = $from.parent;
7793
+ let font = parentNode.attrs.fontFamily;
7794
+ let color = parentNode.attrs.color;
7795
+ const textStyleMark = marks.find((m) => m.type.name === "textStyle");
7796
+ if (!font && textStyleMark?.attrs.fontFamily) {
7797
+ font = textStyleMark.attrs.fontFamily;
7798
+ }
7799
+ if (!color && textStyleMark?.attrs.color) {
7800
+ color = textStyleMark.attrs.color;
7801
+ }
7802
+ return {
7803
+ lastFont: font || value.lastFont,
7804
+ lastColor: color || value.lastColor
7805
+ };
7806
+ }
7807
+ },
7808
+ appendTransaction(transactions, oldState, newState) {
7809
+ if (!transactions.some((tr2) => tr2.docChanged)) {
7810
+ return null;
7811
+ }
7812
+ const pluginState = stylePersistenceKey.getState(newState);
7813
+ if (!pluginState.lastFont && !pluginState.lastColor) {
7814
+ return null;
7815
+ }
7816
+ const { $from } = newState.selection;
7817
+ const parentNode = $from.parent;
7818
+ const marks = $from.marks();
7819
+ const hasExcludedMarks = marks.some(
7820
+ (mark) => excludedMarks.includes(mark.type.name)
7821
+ );
7822
+ let isInCodeBlock = false;
7823
+ for (let depth = $from.depth; depth > 0; depth--) {
7824
+ const node = $from.node(depth);
7825
+ if (node.type.name === "codeBlock") {
7826
+ isInCodeBlock = true;
7827
+ break;
7828
+ }
7829
+ }
7830
+ if (hasExcludedMarks || isInCodeBlock) {
7831
+ return null;
7832
+ }
7833
+ const supportsAttributes = [
7834
+ "paragraph",
7835
+ "heading",
7836
+ "listItem",
7837
+ "taskItem",
7838
+ "blockquote"
7839
+ ].includes(parentNode.type.name);
7840
+ if (!supportsAttributes) {
7841
+ return null;
7842
+ }
7843
+ const currentFont = parentNode.attrs.fontFamily;
7844
+ const currentColor = parentNode.attrs.color;
7845
+ const needsFont = !currentFont && pluginState.lastFont;
7846
+ const needsColor = !currentColor && pluginState.lastColor;
7847
+ if (!needsFont && !needsColor) {
7848
+ return null;
7849
+ }
7850
+ const tr = newState.tr;
7851
+ try {
7852
+ const depth = $from.depth;
7853
+ const pos = $from.before(depth);
7854
+ const node = $from.node(depth);
7855
+ const newAttrs = { ...node.attrs };
7856
+ if (needsFont) newAttrs.fontFamily = pluginState.lastFont;
7857
+ if (needsColor) newAttrs.color = pluginState.lastColor;
7858
+ tr.setNodeMarkup(pos, void 0, newAttrs);
7859
+ const storedMarks = newState.storedMarks || $from.marks();
7860
+ const filteredMarks = storedMarks.filter(
7861
+ (mark) => mark.type.name !== "textStyle" && !excludedMarks.includes(mark.type.name)
7862
+ );
7863
+ const textStyleAttrs = {};
7864
+ if (pluginState.lastFont) textStyleAttrs.fontFamily = pluginState.lastFont;
7865
+ if (pluginState.lastColor) textStyleAttrs.color = pluginState.lastColor;
7866
+ const newMarks = [
7867
+ ...filteredMarks,
7868
+ newState.schema.marks.textStyle.create(textStyleAttrs)
7869
+ ];
7870
+ tr.setStoredMarks(newMarks);
7871
+ return tr;
7872
+ } catch (error) {
7873
+ console.error("StylePersistence error:", error);
7874
+ return null;
7875
+ }
7876
+ },
7877
+ props: {
7878
+ handleKeyDown(view, event) {
7879
+ if (event.key === "Enter") {
7880
+ const { state } = view;
7881
+ const { $from } = state.selection;
7882
+ const marks = $from.marks();
7883
+ const hasExcludedMarks = marks.some(
7884
+ (mark) => excludedMarks.includes(mark.type.name)
7885
+ );
7886
+ if (hasExcludedMarks) {
7887
+ return false;
7888
+ }
7889
+ let isInCodeBlock = false;
7890
+ for (let depth = $from.depth; depth > 0; depth--) {
7891
+ const node = $from.node(depth);
7892
+ if (node.type.name === "codeBlock") {
7893
+ isInCodeBlock = true;
7894
+ break;
7895
+ }
7896
+ }
7897
+ if (isInCodeBlock) {
7898
+ return false;
7899
+ }
7900
+ const parentNode = $from.parent;
7901
+ let font = parentNode.attrs.fontFamily;
7902
+ let color = parentNode.attrs.color;
7903
+ const textStyleMark = marks.find((m) => m.type.name === "textStyle");
7904
+ if (!font && textStyleMark?.attrs.fontFamily) {
7905
+ font = textStyleMark.attrs.fontFamily;
7906
+ }
7907
+ if (!color && textStyleMark?.attrs.color) {
7908
+ color = textStyleMark.attrs.color;
7909
+ }
7910
+ if (font || color) {
7911
+ const pluginState = stylePersistenceKey.getState(state);
7912
+ pluginState.lastFont = font || pluginState.lastFont;
7913
+ pluginState.lastColor = color || pluginState.lastColor;
7914
+ }
7915
+ }
7916
+ return false;
7917
+ }
7918
+ }
7919
+ })
7920
+ ];
7760
7921
  }
7761
7922
  });
7762
- function syncStoredMarks(editor, excludedMarks, storage) {
7763
- const { state } = editor;
7764
- const { $from } = state.selection;
7765
- const marks = $from.marks();
7766
- const hasExcludedMarks = marks.some(
7767
- (mark) => excludedMarks.includes(mark.type.name)
7768
- );
7769
- let isInCodeBlock = false;
7770
- for (let depth = $from.depth; depth > 0; depth--) {
7771
- const node = $from.node(depth);
7772
- if (node.type.name === "codeBlock") {
7773
- isInCodeBlock = true;
7774
- break;
7775
- }
7776
- }
7777
- if (hasExcludedMarks || isInCodeBlock) {
7778
- return;
7779
- }
7780
- const parentNode = $from.parent;
7781
- let blockFont = parentNode.attrs.fontFamily;
7782
- let blockColor = parentNode.attrs.color;
7783
- if ((!blockFont || !blockColor) && storage?.previousNodeMarks) {
7784
- if (!blockFont && storage.previousNodeMarks.blockFont) {
7785
- blockFont = storage.previousNodeMarks.blockFont;
7786
- }
7787
- if (!blockColor && storage.previousNodeMarks.blockColor) {
7788
- blockColor = storage.previousNodeMarks.blockColor;
7789
- }
7790
- if (blockFont || blockColor) {
7791
- const depth = $from.depth;
7792
- const nodePos = $from.before(depth);
7793
- const node = $from.node(depth);
7794
- const newAttrs = { ...node.attrs };
7795
- if (blockFont) newAttrs.fontFamily = blockFont;
7796
- if (blockColor) newAttrs.color = blockColor;
7797
- const tr = state.tr.setNodeMarkup(nodePos, void 0, newAttrs);
7798
- editor.view.dispatch(tr);
7799
- }
7800
- storage.previousNodeMarks = null;
7801
- }
7802
- if (!blockFont && !blockColor) {
7803
- return;
7804
- }
7805
- const storedMarks = state.storedMarks || $from.marks();
7806
- const existingTextStyle = storedMarks.find((mark) => mark.type.name === "textStyle");
7807
- const needsUpdate = !existingTextStyle || blockFont && existingTextStyle.attrs.fontFamily !== blockFont || blockColor && existingTextStyle.attrs.color !== blockColor;
7808
- if (!needsUpdate) {
7809
- return;
7810
- }
7811
- const filteredMarks = storedMarks.filter(
7812
- (mark) => mark.type.name !== "textStyle" && !excludedMarks.includes(mark.type.name)
7813
- );
7814
- const textStyleAttrs = {};
7815
- if (blockFont) textStyleAttrs.fontFamily = blockFont;
7816
- if (blockColor) textStyleAttrs.color = blockColor;
7817
- const newMarks = [
7818
- ...filteredMarks,
7819
- state.schema.marks.textStyle.create(textStyleAttrs)
7820
- ];
7821
- editor.view.dispatch(
7822
- state.tr.setStoredMarks(newMarks)
7823
- );
7824
- }
7825
7923
 
7826
7924
  // src/components/tiptap-templates/simple/simple-editor.tsx
7827
7925
  import { Fragment as Fragment12, jsx as jsx81, jsxs as jsxs48 } from "react/jsx-runtime";