@flexiui/svelte-rich-text 0.0.68 → 0.0.70

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.
@@ -100,6 +100,8 @@
100
100
  contentWrapperAs?: T;
101
101
  inlineNodeMode?: boolean;
102
102
  trailingNode?: boolean;
103
+ cleanMode?: boolean;
104
+ [key: string]: any;
103
105
  }
104
106
 
105
107
  type ToolbarButton =
@@ -157,6 +159,8 @@
157
159
  contentWrapperAs = "div" as T,
158
160
  inlineNodeMode = false,
159
161
  trailingNode = true,
162
+ cleanMode = false,
163
+ ...rest
160
164
  }: Props = $props();
161
165
 
162
166
  let editor = $state() as Readable<Editor>;
@@ -391,7 +395,7 @@
391
395
  content,
392
396
  editorProps: {
393
397
  attributes: {
394
- class: "fl-rich-text-doc",
398
+ class: `fl-rich-text-doc ${cleanMode ? "fl-rich-text-doc--clean" : ""}`,
395
399
  },
396
400
  handleKeyDown: (view, event) => {
397
401
  if (event.key === "Enter" && !event.ctrlKey) {
@@ -784,153 +788,171 @@ function onOpenChangeHighlight(open: boolean) {
784
788
  }
785
789
  </script>
786
790
 
787
- <div
788
- class="fl-rich-text {className}"
789
- class:editable
790
- style="
791
- --fl-editor-accent-color: {editorConfig.editorAccentColor};
792
- --fl-editor-radius: {editorConfig.editorRadius};
793
- --fl-editor-bg: {editorConfig.editorBgColor};
794
- --fl-toolbar-sticky-position: {editorConfig.toolbarStickyPosition}px;
795
- --fl-toolbar-z-index: {editorConfig.toolbarZIndex};
796
- --fl-toolbar-padding: {editorConfig.toolbarPadding};
797
- --fl-toolbar-gap: {editorConfig.toolbarGap};
798
- --fl-toolbar-bg: {editorConfig.toolbarBgColor};
799
- --fl-toolbar-justify-content: {editorConfig.toolbarJustifyContent};
800
- --fl-toolbar-text-color: {editorConfig.toolbarTextColor};
801
- --fl-toolbar-btn-padding: {editorConfig.toolbarBtnPadding};
802
- --fl-toolbar-btn-radius: {editorConfig.toolbarBtnRadius};
803
- --fl-toolbar-btn-min-height: {editorConfig.toolbarBtnMinHeight};
804
- --fl-toolbar-btn-min-width: {editorConfig.toolbarBtnMinWidth ||
805
- editorConfig.toolbarBtnMinHeight};
806
- --fl-doc-max-width: {editorConfig.docMaxWidth};
807
- --fl-doc-padding: {editorConfig.docPadding};
808
- --fl-doc-bg: {editorConfig.docBg};
809
- --fl-doc-margin-inline: {editorConfig.docMarginInline};
810
- --fl-doc-margin-block: {editorConfig.docMarginBlock};
811
- --fl-doc-radius: {editorConfig.docRadius};
812
- --fl-doc-text-color: {editorConfig.docTextColor};
813
- "
814
- >
815
- {#if editor && showToolbar}
816
- <header class="fl-rich-text-toolbar">
817
- <div class="fl-rich-text-toolbar-container container">
818
- {#each toolbarGroups as toolbarGroup}
819
- {#if toolbarGroup.length > 0}
820
- <div role="group" class="fl-rich-text-toolbar-group">
821
- {#each toolbarGroup as toolbarItem}
822
- {#if Array.isArray(toolbarItem)}
823
- <!-- Si por alguna razón hay un array anidado, manejarlo -->
824
- <p>Array anidado (no debería pasar)</p>
825
- {:else if typeof toolbarItem === "string"}
826
- <RenderToolbarButton
827
- type={toolbarItem}
828
- {editor}
829
- {nodeCounters}
830
- {nodesLimit}
831
- {currentNodeCount}
832
- accentSoft={isAccentSoft}
833
- {fontSize}
834
- onToggleDropdown={(e: MouseEvent, dropdownName: string) => {
835
- toogleDropdown(
836
- e.currentTarget as HTMLElement,
837
- dropdownName,
838
- );
839
- }}
840
- />
841
- {:else if isButton(toolbarItem)}
842
- <RenderToolbarButton
843
- type={toolbarItem.type}
844
- {editor}
845
- {nodeCounters}
846
- {nodesLimit}
847
- {currentNodeCount}
848
- accentSoft={isAccentSoft}
849
- {fontSize}
850
- onToggleDropdown={(e: MouseEvent, dropdownName: string) => {
851
- toogleDropdown(
852
- e.currentTarget as HTMLElement,
853
- dropdownName,
854
- );
855
- }}
856
- />
857
- {/if}
858
- {/each}
859
- </div>
860
- {/if}
861
- {/each}
862
- </div>
863
- </header>
864
- {/if}
791
+ {#if cleanMode}
865
792
 
866
- <EditorContent
867
- as={contentWrapperAs}
868
- editor={$editor}
869
- class="fl-rich-text-content"
870
- data-fl-editable="true"
871
- />
872
-
873
- <!-- Warning message for node limit -->
874
- {#if showLimitWarning && nodesLimit}
875
- <div class="fl-node-limit-warning">
876
- {limitWarningMessage ||
877
- ` No se pueden añadir más nodos a este editor. Max: ${nodesLimit}`}
878
- </div>
879
- {/if}
880
-
881
- <!-- Bottom bar showing node count -->
882
- {#if showCountersBar || percentage >= 90}
883
- <div class="fl-counters-bar">
884
- <div
885
- class="fl-character-count"
886
- class:fl-character-count--warning={percentage >= 100}
887
- >
888
- {#if charactersLimit}
889
- <svg height="20" width="20" viewBox="0 0 20 20">
890
- <circle r="10" cx="10" cy="10" fill="#ffffff30" />
891
- <circle
892
- r="5"
893
- cx="10"
894
- cy="10"
895
- fill="transparent"
896
- stroke="currentColor"
897
- stroke-width="10"
898
- stroke-dasharray={`calc(${percentage} * 31.4 / 100) 31.4`}
899
- transform="rotate(-90) translate(-20)"
900
- />
901
- <circle r="6" cx="10" cy="10" fill="var(--fl-editor-bg)" />
902
- </svg>
903
- {/if}
904
-
905
- <span>
906
- Characters: {$editor?.storage?.characterCount?.characters()}
907
- {#if charactersLimit}
908
- / {charactersLimit}{/if}
909
- </span>
793
+ <EditorContent
794
+ as={contentWrapperAs}
795
+ editor={$editor}
796
+ class={className}
797
+ {...rest}
798
+ />
799
+
800
+ <!-- Warning message for node limit -->
801
+ {#if showLimitWarning && nodesLimit}
802
+ <div class="fl-node-limit-warning">
803
+ {limitWarningMessage ||
804
+ ` No se pueden añadir más nodos a este editor. Max: ${nodesLimit}`}
805
+ </div>
806
+ {/if}
807
+
808
+ {:else}
809
+ <div
810
+ class="fl-rich-text {className}"
811
+ class:editable
812
+ style="
813
+ --fl-editor-accent-color: {editorConfig.editorAccentColor};
814
+ --fl-editor-radius: {editorConfig.editorRadius};
815
+ --fl-editor-bg: {editorConfig.editorBgColor};
816
+ --fl-toolbar-sticky-position: {editorConfig.toolbarStickyPosition}px;
817
+ --fl-toolbar-z-index: {editorConfig.toolbarZIndex};
818
+ --fl-toolbar-padding: {editorConfig.toolbarPadding};
819
+ --fl-toolbar-gap: {editorConfig.toolbarGap};
820
+ --fl-toolbar-bg: {editorConfig.toolbarBgColor};
821
+ --fl-toolbar-justify-content: {editorConfig.toolbarJustifyContent};
822
+ --fl-toolbar-text-color: {editorConfig.toolbarTextColor};
823
+ --fl-toolbar-btn-padding: {editorConfig.toolbarBtnPadding};
824
+ --fl-toolbar-btn-radius: {editorConfig.toolbarBtnRadius};
825
+ --fl-toolbar-btn-min-height: {editorConfig.toolbarBtnMinHeight};
826
+ --fl-toolbar-btn-min-width: {editorConfig.toolbarBtnMinWidth ||
827
+ editorConfig.toolbarBtnMinHeight};
828
+ --fl-doc-max-width: {editorConfig.docMaxWidth};
829
+ --fl-doc-padding: {editorConfig.docPadding};
830
+ --fl-doc-bg: {editorConfig.docBg};
831
+ --fl-doc-margin-inline: {editorConfig.docMarginInline};
832
+ --fl-doc-margin-block: {editorConfig.docMarginBlock};
833
+ --fl-doc-radius: {editorConfig.docRadius};
834
+ --fl-doc-text-color: {editorConfig.docTextColor};
835
+ "
836
+ >
837
+ {#if editor && showToolbar}
838
+ <header class="fl-rich-text-toolbar">
839
+ <div class="fl-rich-text-toolbar-container container">
840
+ {#each toolbarGroups as toolbarGroup}
841
+ {#if toolbarGroup.length > 0}
842
+ <div role="group" class="fl-rich-text-toolbar-group">
843
+ {#each toolbarGroup as toolbarItem}
844
+ {#if Array.isArray(toolbarItem)}
845
+ <!-- Si por alguna razón hay un array anidado, manejarlo -->
846
+ <p>Array anidado (no debería pasar)</p>
847
+ {:else if typeof toolbarItem === "string"}
848
+ <RenderToolbarButton
849
+ type={toolbarItem}
850
+ {editor}
851
+ {nodeCounters}
852
+ {nodesLimit}
853
+ {currentNodeCount}
854
+ accentSoft={isAccentSoft}
855
+ {fontSize}
856
+ onToggleDropdown={(e: MouseEvent, dropdownName: string) => {
857
+ toogleDropdown(
858
+ e.currentTarget as HTMLElement,
859
+ dropdownName,
860
+ );
861
+ }}
862
+ />
863
+ {:else if isButton(toolbarItem)}
864
+ <RenderToolbarButton
865
+ type={toolbarItem.type}
866
+ {editor}
867
+ {nodeCounters}
868
+ {nodesLimit}
869
+ {currentNodeCount}
870
+ accentSoft={isAccentSoft}
871
+ {fontSize}
872
+ onToggleDropdown={(e: MouseEvent, dropdownName: string) => {
873
+ toogleDropdown(
874
+ e.currentTarget as HTMLElement,
875
+ dropdownName,
876
+ );
877
+ }}
878
+ />
879
+ {/if}
880
+ {/each}
881
+ </div>
882
+ {/if}
883
+ {/each}
884
+ </div>
885
+ </header>
886
+ {/if}
887
+
888
+ <EditorContent
889
+ as={contentWrapperAs}
890
+ editor={$editor}
891
+ class="fl-rich-text-content"
892
+ data-fl-editable="true"
893
+ />
894
+
895
+ <!-- Warning message for node limit -->
896
+ {#if showLimitWarning && nodesLimit}
897
+ <div class="fl-node-limit-warning">
898
+ {limitWarningMessage ||
899
+ ` No se pueden añadir más nodos a este editor. Max: ${nodesLimit}`}
910
900
  </div>
901
+ {/if}
911
902
 
912
- {#if showCountersBar}
913
- <span>
914
- Words: {$editor?.storage?.characterCount?.words()}
915
- </span>
916
- {/if}
903
+ <!-- Bottom bar showing node count -->
904
+ {#if showCountersBar || percentage >= 90}
905
+ <div class="fl-counters-bar">
906
+ <div
907
+ class="fl-character-count"
908
+ class:fl-character-count--warning={percentage >= 100}
909
+ >
910
+ {#if charactersLimit}
911
+ <svg height="20" width="20" viewBox="0 0 20 20">
912
+ <circle r="10" cx="10" cy="10" fill="#ffffff30" />
913
+ <circle
914
+ r="5"
915
+ cx="10"
916
+ cy="10"
917
+ fill="transparent"
918
+ stroke="currentColor"
919
+ stroke-width="10"
920
+ stroke-dasharray={`calc(${percentage} * 31.4 / 100) 31.4`}
921
+ transform="rotate(-90) translate(-20)"
922
+ />
923
+ <circle r="6" cx="10" cy="10" fill="var(--fl-editor-bg)" />
924
+ </svg>
925
+ {/if}
917
926
 
918
- {#if nodesLimit}
919
- <span class="fl-node-count-text">
920
- Nodes: {currentNodeCount} / {nodesLimit}
921
- </span>
922
- {/if}
927
+ <span>
928
+ Characters: {$editor?.storage?.characterCount?.characters()}
929
+ {#if charactersLimit}
930
+ / {charactersLimit}{/if}
931
+ </span>
932
+ </div>
933
+
934
+ {#if showCountersBar}
935
+ <span>
936
+ Words: {$editor?.storage?.characterCount?.words()}
937
+ </span>
938
+ {/if}
923
939
 
924
- <!-- <div class="fl-node-count-progress">
925
- <div
926
- class="fl-node-count-progress-bar"
927
- style="width: {Math.min((currentNodeCount / nodesLimit) * 100, 100)}%"
928
- ></div>
929
- </div> -->
930
- </div>
931
- {/if}
932
- </div>
940
+ {#if nodesLimit}
941
+ <span class="fl-node-count-text">
942
+ Nodes: {currentNodeCount} / {nodesLimit}
943
+ </span>
944
+ {/if}
933
945
 
946
+ <!-- <div class="fl-node-count-progress">
947
+ <div
948
+ class="fl-node-count-progress-bar"
949
+ style="width: {Math.min((currentNodeCount / nodesLimit) * 100, 100)}%"
950
+ ></div>
951
+ </div> -->
952
+ </div>
953
+ {/if}
954
+ </div>
955
+ {/if}
934
956
  <div
935
957
  class="fl-toolbar-dropdown-panel"
936
958
  bind:this={tooltip}
@@ -0,0 +1,7 @@
1
+ import { Extension } from '@tiptap/core';
2
+ export interface ResizableNodesOptions {
3
+ nodes: string[];
4
+ minWidth?: number;
5
+ handleWidth?: number;
6
+ }
7
+ export declare const ResizableNodes: Extension<ResizableNodesOptions, any>;
@@ -0,0 +1,116 @@
1
+ // resizable-nodes.ts (versión completa con Decorations)
2
+ import { Extension } from '@tiptap/core';
3
+ import { Plugin, PluginKey } from '@tiptap/pm/state';
4
+ import { Decoration, DecorationSet } from '@tiptap/pm/view';
5
+ export const ResizableNodes = Extension.create({
6
+ name: 'resizableNodes',
7
+ addOptions() {
8
+ return {
9
+ nodes: [],
10
+ minWidth: 120,
11
+ handleWidth: 8,
12
+ };
13
+ },
14
+ // ✅ Añade maxWidth a los nodos configurados SIN tocar su NodeView
15
+ addGlobalAttributes() {
16
+ return [
17
+ {
18
+ types: this.options.nodes,
19
+ attributes: {
20
+ maxWidth: {
21
+ default: null,
22
+ parseHTML: el => {
23
+ const val = el.style.maxWidth;
24
+ return val || null;
25
+ },
26
+ renderHTML: attrs => {
27
+ if (!attrs.maxWidth)
28
+ return {};
29
+ return {
30
+ style: `max-width: ${attrs.maxWidth}; box-sizing: border-box;`,
31
+ };
32
+ },
33
+ },
34
+ },
35
+ },
36
+ ];
37
+ },
38
+ addProseMirrorPlugins() {
39
+ const extension = this;
40
+ const pluginKey = new PluginKey('resizableNodes');
41
+ return [
42
+ new Plugin({
43
+ key: pluginKey,
44
+ // Decoraciones: añade el handle como widget sin reemplazar el NodeView
45
+ props: {
46
+ decorations(state) {
47
+ const { doc, selection } = state;
48
+ const decorations = [];
49
+ const nodeNames = extension.options.nodes;
50
+ doc.descendants((node, pos) => {
51
+ if (!nodeNames.includes(node.type.name))
52
+ return;
53
+ if (node.attrs.maxWidth === undefined)
54
+ return;
55
+ // Widget al FINAL del nodo (después del contenido)
56
+ const widget = Decoration.widget(pos + node.nodeSize - 1, () => {
57
+ const handle = document.createElement('div');
58
+ handle.className = 'resize-handle';
59
+ handle.contentEditable = 'false';
60
+ handle.dataset.pos = String(pos);
61
+ return handle;
62
+ }, { side: 1, key: `resize-${pos}` });
63
+ decorations.push(widget);
64
+ });
65
+ return DecorationSet.create(doc, decorations);
66
+ },
67
+ handleDOMEvents: {
68
+ mousedown(view, event) {
69
+ const handle = event.target.closest('.resize-handle');
70
+ if (!handle)
71
+ return false;
72
+ event.preventDefault();
73
+ const pos = parseInt(handle.dataset.pos || '0', 10);
74
+ const node = view.state.doc.nodeAt(pos);
75
+ if (!node)
76
+ return false;
77
+ const dom = view.nodeDOM(pos);
78
+ if (!dom)
79
+ return false;
80
+ const startX = event.clientX;
81
+ const startWidth = dom.offsetWidth;
82
+ const minWidth = extension.options.minWidth ?? 120;
83
+ const onMouseMove = (e) => {
84
+ const newWidth = Math.max(minWidth, startWidth + (e.clientX - startX));
85
+ console.log(newWidth);
86
+ dom.style.maxWidth = `${newWidth}px`; // preview live
87
+ // Commit al schema de ProseMirror
88
+ const { tr } = view.state;
89
+ // tr.setNodeMarkup(pos, undefined, {
90
+ // ...node.attrs,
91
+ // maxWidth: `${newWidth}px`,
92
+ // })
93
+ view.dispatch(tr);
94
+ };
95
+ const onMouseUp = (e) => {
96
+ document.removeEventListener('mousemove', onMouseMove);
97
+ document.removeEventListener('mouseup', onMouseUp);
98
+ const finalWidth = Math.max(minWidth, startWidth + (e.clientX - startX));
99
+ // Commit al schema de ProseMirror
100
+ const { tr } = view.state;
101
+ tr.setNodeMarkup(pos, undefined, {
102
+ ...node.attrs,
103
+ maxWidth: `${finalWidth}px`,
104
+ });
105
+ view.dispatch(tr);
106
+ };
107
+ document.addEventListener('mousemove', onMouseMove);
108
+ document.addEventListener('mouseup', onMouseUp);
109
+ return true;
110
+ },
111
+ },
112
+ },
113
+ }),
114
+ ];
115
+ },
116
+ });
package/dist/styles.css CHANGED
@@ -63,6 +63,12 @@
63
63
  box-sizing: border-box;
64
64
  }
65
65
 
66
+ .fl-rich-text-doc--clean {
67
+ padding: 0;
68
+ margin: 0;
69
+ max-width: none;
70
+ }
71
+
66
72
  .fl-toolbar-dropdown-panel {
67
73
  position: absolute;
68
74
  padding: 9px;
@@ -637,3 +643,34 @@ button {
637
643
  }
638
644
  }
639
645
  }
646
+
647
+
648
+ /* El handle flota sobre el borde derecho del nodo */
649
+ [data-resizable], [style*="max-width"] {
650
+ position: relative;
651
+ }
652
+
653
+ .resize-handle {
654
+ position: absolute;
655
+ right: -4px;
656
+ top: 0;
657
+ bottom: 0;
658
+ width: 8px;
659
+ cursor: col-resize;
660
+ opacity: 0;
661
+ background: #3b82f6;
662
+ border-radius: 4px;
663
+ transition: opacity 0.15s;
664
+ z-index: 10;
665
+ }
666
+
667
+ p:hover .resize-handle,
668
+ h1:hover .resize-handle,
669
+ h2:hover .resize-handle,
670
+ blockquote:hover .resize-handle {
671
+ opacity: 0.6;
672
+ }
673
+
674
+ .resize-handle:hover {
675
+ opacity: 1 !important;
676
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flexiui/svelte-rich-text",
3
- "version": "0.0.68",
3
+ "version": "0.0.70",
4
4
  "description": "A lightweight and flexible rich text editor component for Svelte",
5
5
  "keywords": [
6
6
  "svelte",