rich-html-editor 0.2.1 → 0.2.2

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/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # rich-html-editor
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/rich-html-editor.svg)](https://www.npmjs.com/package/rich-html-editor)
4
+
3
5
  A framework-agnostic, plug-and-play rich HTML editor library for adding WYSIWYG editing capabilities to your web applications.
4
6
 
5
7
  ## Features
@@ -52,9 +54,11 @@ Vanilla JavaScript
52
54
  ```html
53
55
  <iframe id="editor" srcdoc="<body><div class=\"editable\">Edit me</div></body>"></iframe>
54
56
  <script type="module">
55
- import { initRichEditor } from './dist/index.mjs';
57
+ import { initRichEditor, getCleanHTML } from './dist/index.mjs';
56
58
  const iframe = document.getElementById('editor');
57
59
  initRichEditor(iframe);
60
+ // later, retrieve the cleaned HTML from the editor
61
+ const html = getCleanHTML();
58
62
  </script>
59
63
  ```
60
64
 
@@ -62,12 +66,14 @@ React (functional component)
62
66
 
63
67
  ```jsx
64
68
  import { useEffect, useRef } from "react";
65
- import { initRichEditor } from "rich-html-editor";
69
+ import { initRichEditor, getCleanHTML } from "rich-html-editor";
66
70
 
67
71
  export default function Editor() {
68
72
  const iframeRef = useRef(null);
69
73
  useEffect(() => {
70
74
  if (iframeRef.current) initRichEditor(iframeRef.current);
75
+ // later, read cleaned HTML from the editor
76
+ const html = getCleanHTML();
71
77
  }, []);
72
78
  return (
73
79
  <iframe
@@ -86,12 +92,14 @@ Angular (simple component)
86
92
 
87
93
  // in component.ts
88
94
  import { AfterViewInit, ViewChild, ElementRef } from "@angular/core";
89
- import { initRichEditor } from "rich-html-editor";
95
+ import { initRichEditor, getCleanHTML } from "rich-html-editor";
90
96
 
91
97
  export class MyEditorComponent implements AfterViewInit {
92
98
  @ViewChild("editor") editor!: ElementRef<HTMLIFrameElement>;
93
99
  ngAfterViewInit() {
94
100
  initRichEditor(this.editor.nativeElement);
101
+ // later, obtain cleaned HTML
102
+ const html = getCleanHTML();
95
103
  }
96
104
  }
97
105
  ```
@@ -104,12 +112,14 @@ Vue 3 (Composition API)
104
112
  </template>
105
113
  <script setup>
106
114
  import { onMounted, ref } from "vue";
107
- import { initRichEditor } from "rich-html-editor";
115
+ import { initRichEditor, getCleanHTML } from "rich-html-editor";
108
116
 
109
117
  const editor = ref(null);
110
118
  const srcdoc = '<body><div class="editable">Edit me</div></body>';
111
119
  onMounted(() => {
112
120
  if (editor.value) initRichEditor(editor.value);
121
+ // later, read cleaned HTML from the editor
122
+ const html = getCleanHTML();
113
123
  });
114
124
  </script>
115
125
  ```
package/dist/index.js CHANGED
@@ -761,219 +761,76 @@ function injectStyles(doc) {
761
761
  styleEl.textContent = css;
762
762
  }
763
763
 
764
- // src/toolbar/toolbar.ts
764
+ // src/toolbar/render.ts
765
765
  init_constants();
766
- function injectToolbar(doc, options) {
767
- const existing = doc.getElementById(TOOLBAR_ID);
768
- if (existing) existing.remove();
769
- const toolbar = doc.createElement("div");
770
- toolbar.id = TOOLBAR_ID;
771
- toolbar.setAttribute("role", "toolbar");
772
- toolbar.setAttribute("aria-label", "Rich text editor toolbar");
773
- function makeButton(label, title, command, value, isActive, disabled) {
774
- const btn = doc.createElement("button");
775
- btn.type = "button";
776
- if (label && label.trim().startsWith("<")) {
777
- btn.innerHTML = label;
778
- } else {
779
- btn.textContent = label;
766
+
767
+ // src/toolbar/color.ts
768
+ function makeColorInput(doc, options, title, command, initialColor) {
769
+ const input = doc.createElement("input");
770
+ input.type = "color";
771
+ input.className = "toolbar-color-input";
772
+ const wrapper = doc.createElement("label");
773
+ wrapper.className = "color-label";
774
+ wrapper.appendChild(doc.createTextNode(title + " "));
775
+ wrapper.appendChild(input);
776
+ let savedRange = null;
777
+ input.addEventListener("pointerdown", () => {
778
+ const s = doc.getSelection();
779
+ if (s && s.rangeCount) savedRange = s.getRangeAt(0).cloneRange();
780
+ });
781
+ input.onchange = (e) => {
782
+ try {
783
+ const s = doc.getSelection();
784
+ if (savedRange && s) {
785
+ s.removeAllRanges();
786
+ s.addRange(savedRange);
787
+ }
788
+ } catch (err) {
780
789
  }
781
- btn.title = title;
782
- btn.setAttribute("aria-label", title);
783
- if (typeof isActive !== "undefined")
784
- btn.setAttribute("aria-pressed", String(!!isActive));
785
- btn.tabIndex = 0;
786
- if (disabled) btn.disabled = true;
787
- btn.onclick = () => options.onCommand(command, value);
788
- btn.addEventListener("keydown", (ev) => {
789
- if (ev.key === "Enter" || ev.key === " ") {
790
- ev.preventDefault();
791
- btn.click();
790
+ options.onCommand(command, e.target.value);
791
+ savedRange = null;
792
+ };
793
+ function rgbToHex(input2) {
794
+ if (!input2) return null;
795
+ const v = input2.trim();
796
+ if (v.startsWith("#")) {
797
+ if (v.length === 4) {
798
+ return ("#" + v[1] + v[1] + v[2] + v[2] + v[3] + v[3]).toLowerCase();
792
799
  }
793
- });
794
- return btn;
795
- }
796
- function makeSelect(title, command, optionsList, initialValue) {
797
- const select = doc.createElement("select");
798
- select.title = title;
799
- select.setAttribute("aria-label", title);
800
- select.appendChild(new Option(title, "", true, true));
801
- for (const opt of optionsList) {
802
- select.appendChild(new Option(opt.label, opt.value));
800
+ return v.toLowerCase();
803
801
  }
804
- try {
805
- if (initialValue) select.value = initialValue;
806
- } catch (e) {
802
+ const rgbMatch = v.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
803
+ if (rgbMatch) {
804
+ const r = Number(rgbMatch[1]);
805
+ const g = Number(rgbMatch[2]);
806
+ const b = Number(rgbMatch[3]);
807
+ const hex = "#" + [r, g, b].map((n) => n.toString(16).padStart(2, "0")).join("").toLowerCase();
808
+ return hex;
807
809
  }
808
- select.onchange = (e) => {
809
- const val = e.target.value;
810
- options.onCommand(command, val);
811
- select.selectedIndex = 0;
812
- };
813
- return select;
810
+ return null;
814
811
  }
815
- function makeColorInput(title, command, initialColor) {
816
- const input = doc.createElement("input");
817
- input.type = "color";
818
- input.className = "toolbar-color-input";
819
- const wrapper = doc.createElement("label");
820
- wrapper.className = "color-label";
821
- wrapper.appendChild(doc.createTextNode(title + " "));
822
- wrapper.appendChild(input);
823
- let savedRange = null;
824
- input.addEventListener("pointerdown", () => {
825
- const s = doc.getSelection();
826
- if (s && s.rangeCount) savedRange = s.getRangeAt(0).cloneRange();
827
- });
828
- input.onchange = (e) => {
829
- try {
830
- const s = doc.getSelection();
831
- if (savedRange && s) {
832
- s.removeAllRanges();
833
- s.addRange(savedRange);
834
- }
835
- } catch (err) {
836
- }
837
- options.onCommand(command, e.target.value);
838
- savedRange = null;
839
- };
840
- function rgbToHex(input2) {
841
- if (!input2) return null;
842
- const v = input2.trim();
843
- if (v.startsWith("#")) {
844
- if (v.length === 4) {
845
- return ("#" + v[1] + v[1] + v[2] + v[2] + v[3] + v[3]).toLowerCase();
846
- }
847
- return v.toLowerCase();
848
- }
849
- const rgbMatch = v.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
850
- if (rgbMatch) {
851
- const r = Number(rgbMatch[1]);
852
- const g = Number(rgbMatch[2]);
853
- const b = Number(rgbMatch[3]);
854
- const hex = "#" + [r, g, b].map((n) => n.toString(16).padStart(2, "0")).join("").toLowerCase();
855
- return hex;
812
+ const setColor = (val) => {
813
+ if (!val) return;
814
+ const hex = rgbToHex(val) || val;
815
+ try {
816
+ if (hex && hex.startsWith("#") && input.value !== hex) {
817
+ input.value = hex;
856
818
  }
857
- return null;
819
+ } catch (e) {
858
820
  }
859
- const setColor = (val) => {
860
- if (!val) return;
861
- const hex = rgbToHex(val) || val;
862
- try {
863
- if (hex && hex.startsWith("#") && input.value !== hex) {
864
- input.value = hex;
865
- }
866
- } catch (e) {
867
- }
868
- };
869
- if (initialColor) setColor(initialColor);
870
- input.addEventListener("input", (e) => {
871
- const val = e.target.value;
872
- setColor(val);
873
- });
874
- input.title = title;
875
- input.setAttribute("aria-label", title);
876
- return wrapper;
877
- }
878
- const format = options.getFormatState();
879
- function makeGroup() {
880
- const g = doc.createElement("div");
881
- g.className = "toolbar-group";
882
- return g;
883
- }
884
- function makeSep() {
885
- const s = doc.createElement("div");
886
- s.className = "toolbar-sep";
887
- return s;
888
- }
889
- const undoBtn = makeButton(
890
- LABEL_UNDO,
891
- "Undo",
892
- "undo",
893
- void 0,
894
- false,
895
- !options.canUndo()
896
- );
897
- undoBtn.onclick = () => options.onUndo();
898
- const redoBtn = makeButton(
899
- LABEL_REDO,
900
- "Redo",
901
- "redo",
902
- void 0,
903
- false,
904
- !options.canRedo()
905
- );
906
- redoBtn.onclick = () => options.onRedo();
907
- const grp1 = makeGroup();
908
- grp1.appendChild(undoBtn);
909
- grp1.appendChild(redoBtn);
910
- toolbar.appendChild(grp1);
911
- toolbar.appendChild(makeSep());
912
- const grp2 = makeGroup();
913
- grp2.className = "toolbar-group collapse-on-small";
914
- grp2.appendChild(
915
- makeSelect(
916
- "Format",
917
- "formatBlock",
918
- FORMAT_OPTIONS,
919
- format.formatBlock
920
- )
921
- );
922
- grp2.appendChild(
923
- makeSelect("Font", "fontName", FONT_OPTIONS, format.fontName)
924
- );
925
- grp2.appendChild(
926
- makeSelect("Size", "fontSize", SIZE_OPTIONS, format.fontSize)
927
- );
928
- toolbar.appendChild(grp2);
929
- toolbar.appendChild(makeSep());
930
- const grp3 = makeGroup();
931
- grp3.appendChild(
932
- makeButton(LABEL_BOLD, "Bold", "bold", void 0, format.bold)
933
- );
934
- grp3.appendChild(
935
- makeButton(LABEL_ITALIC, "Italic", "italic", void 0, format.italic)
936
- );
937
- grp3.appendChild(
938
- makeButton(
939
- LABEL_UNDERLINE,
940
- "Underline",
941
- "underline",
942
- void 0,
943
- format.underline
944
- )
945
- );
946
- grp3.appendChild(makeButton(LABEL_STRIKETHROUGH, "Strikethrough", "strike"));
947
- toolbar.appendChild(grp3);
948
- toolbar.appendChild(makeSep());
949
- const grp4 = makeGroup();
950
- grp4.appendChild(makeButton(LABEL_ALIGN_LEFT, "Align left", "align", "left"));
951
- grp4.appendChild(
952
- makeButton(LABEL_ALIGN_CENTER, "Align center", "align", "center")
953
- );
954
- grp4.appendChild(
955
- makeButton(LABEL_ALIGN_RIGHT, "Align right", "align", "right")
956
- );
957
- toolbar.appendChild(grp4);
958
- toolbar.appendChild(makeSep());
959
- const grp5 = makeGroup();
960
- grp5.className = "toolbar-group collapse-on-small";
961
- grp5.appendChild(
962
- makeColorInput("Text color", "foreColor", format.foreColor)
963
- );
964
- grp5.appendChild(
965
- makeColorInput(
966
- "Highlight color",
967
- "hiliteColor",
968
- format.hiliteColor
969
- )
970
- );
971
- toolbar.appendChild(grp5);
972
- toolbar.appendChild(makeSep());
973
- const grp6 = makeGroup();
974
- grp6.className = "toolbar-group collapse-on-small";
975
- grp6.appendChild(makeButton(LABEL_LINK, "Insert link", "link"));
976
- toolbar.appendChild(grp6);
821
+ };
822
+ if (initialColor) setColor(initialColor);
823
+ input.addEventListener("input", (e) => {
824
+ const val = e.target.value;
825
+ setColor(val);
826
+ });
827
+ input.title = title;
828
+ input.setAttribute("aria-label", title);
829
+ return wrapper;
830
+ }
831
+
832
+ // src/toolbar/overflow.ts
833
+ function setupOverflow(doc, toolbar, options, format, helpers) {
977
834
  const overflowBtn = doc.createElement("button");
978
835
  overflowBtn.type = "button";
979
836
  overflowBtn.className = "toolbar-overflow-btn";
@@ -1000,7 +857,7 @@ function injectToolbar(doc, options) {
1000
857
  overflowBtn.setAttribute("aria-expanded", "false");
1001
858
  overflowBtn.focus();
1002
859
  }
1003
- overflowBtn.addEventListener("click", (e) => {
860
+ overflowBtn.addEventListener("click", () => {
1004
861
  if (overflowMenu.hidden) openOverflow();
1005
862
  else closeOverflow();
1006
863
  });
@@ -1027,35 +884,105 @@ function injectToolbar(doc, options) {
1027
884
  }
1028
885
  });
1029
886
  overflowMenu.appendChild(
1030
- makeSelect(
887
+ helpers.makeSelect(
1031
888
  "Format",
1032
889
  "formatBlock",
1033
- FORMAT_OPTIONS,
890
+ window.RHE_FORMAT_OPTIONS || [],
1034
891
  format.formatBlock
1035
892
  )
1036
893
  );
1037
894
  overflowMenu.appendChild(
1038
- makeSelect("Font", "fontName", FONT_OPTIONS, format.fontName)
895
+ helpers.makeSelect(
896
+ "Font",
897
+ "fontName",
898
+ window.RHE_FONT_OPTIONS || [],
899
+ format.fontName
900
+ )
1039
901
  );
1040
902
  overflowMenu.appendChild(
1041
- makeSelect("Size", "fontSize", SIZE_OPTIONS, format.fontSize)
903
+ helpers.makeSelect(
904
+ "Size",
905
+ "fontSize",
906
+ window.RHE_SIZE_OPTIONS || [],
907
+ format.fontSize
908
+ )
1042
909
  );
1043
910
  overflowMenu.appendChild(
1044
- makeColorInput("Text color", "foreColor", format.foreColor)
911
+ helpers.makeColorInput("Text color", "foreColor", format.foreColor)
1045
912
  );
1046
913
  overflowMenu.appendChild(
1047
- makeColorInput(
914
+ helpers.makeColorInput(
1048
915
  "Highlight color",
1049
916
  "hiliteColor",
1050
917
  format.hiliteColor
1051
918
  )
1052
919
  );
1053
- overflowMenu.appendChild(makeButton(LABEL_LINK, "Insert link", "link"));
1054
- const overflowWrap = makeGroup();
920
+ overflowMenu.appendChild(helpers.makeButton("Link", "Insert link", "link"));
921
+ const overflowWrap = helpers.makeGroup();
1055
922
  overflowWrap.className = "toolbar-group toolbar-overflow-wrap";
1056
923
  overflowWrap.appendChild(overflowBtn);
1057
924
  overflowWrap.appendChild(overflowMenu);
1058
925
  toolbar.appendChild(overflowWrap);
926
+ }
927
+
928
+ // src/toolbar/buttons.ts
929
+ function makeButton(doc, options, label, title, command, value, isActive, disabled) {
930
+ const btn = doc.createElement("button");
931
+ btn.type = "button";
932
+ if (label && label.trim().startsWith("<")) {
933
+ btn.innerHTML = label;
934
+ } else {
935
+ btn.textContent = label;
936
+ }
937
+ btn.title = title;
938
+ btn.setAttribute("aria-label", title);
939
+ if (typeof isActive !== "undefined")
940
+ btn.setAttribute("aria-pressed", String(!!isActive));
941
+ btn.tabIndex = 0;
942
+ if (disabled) btn.disabled = true;
943
+ btn.onclick = () => options.onCommand(command, value);
944
+ btn.addEventListener("keydown", (ev) => {
945
+ if (ev.key === "Enter" || ev.key === " ") {
946
+ ev.preventDefault();
947
+ btn.click();
948
+ }
949
+ });
950
+ return btn;
951
+ }
952
+ function makeGroup(doc) {
953
+ const g = doc.createElement("div");
954
+ g.className = "toolbar-group";
955
+ return g;
956
+ }
957
+ function makeSep(doc) {
958
+ const s = doc.createElement("div");
959
+ s.className = "toolbar-sep";
960
+ return s;
961
+ }
962
+
963
+ // src/toolbar/selects.ts
964
+ function makeSelect(doc, options, title, command, optionsList, initialValue) {
965
+ const select = doc.createElement("select");
966
+ select.title = title;
967
+ select.setAttribute("aria-label", title);
968
+ select.appendChild(new Option(title, "", true, true));
969
+ for (const opt of optionsList) {
970
+ select.appendChild(new Option(opt.label, opt.value));
971
+ }
972
+ try {
973
+ if (initialValue) select.value = initialValue;
974
+ } catch (e) {
975
+ }
976
+ select.onchange = (e) => {
977
+ const val = e.target.value;
978
+ options.onCommand(command, val);
979
+ select.selectedIndex = 0;
980
+ };
981
+ return select;
982
+ }
983
+
984
+ // src/toolbar/navigation.ts
985
+ function setupNavigation(toolbar) {
1059
986
  toolbar.addEventListener("keydown", (e) => {
1060
987
  const focusable = Array.from(
1061
988
  toolbar.querySelectorAll("button, select, input, [tabindex]")
@@ -1078,6 +1005,140 @@ function injectToolbar(doc, options) {
1078
1005
  focusable[focusable.length - 1].focus();
1079
1006
  }
1080
1007
  });
1008
+ }
1009
+
1010
+ // src/toolbar/render.ts
1011
+ function injectToolbar(doc, options) {
1012
+ const existing = doc.getElementById(TOOLBAR_ID);
1013
+ if (existing) existing.remove();
1014
+ const toolbar = doc.createElement("div");
1015
+ toolbar.id = TOOLBAR_ID;
1016
+ toolbar.setAttribute("role", "toolbar");
1017
+ toolbar.setAttribute("aria-label", "Rich text editor toolbar");
1018
+ const makeButton2 = (label, title, command, value, isActive, disabled) => makeButton(
1019
+ doc,
1020
+ { onCommand: options.onCommand },
1021
+ label,
1022
+ title,
1023
+ command,
1024
+ value,
1025
+ isActive,
1026
+ disabled
1027
+ );
1028
+ const makeSelect2 = (title, command, optionsList, initialValue) => makeSelect(
1029
+ doc,
1030
+ { onCommand: options.onCommand },
1031
+ title,
1032
+ command,
1033
+ optionsList,
1034
+ initialValue
1035
+ );
1036
+ const format = options.getFormatState();
1037
+ const makeGroup2 = () => makeGroup(doc);
1038
+ const makeSep2 = () => makeSep(doc);
1039
+ const undoBtn = makeButton2(
1040
+ LABEL_UNDO,
1041
+ "Undo",
1042
+ "undo",
1043
+ void 0,
1044
+ false,
1045
+ !options.canUndo()
1046
+ );
1047
+ undoBtn.onclick = () => options.onUndo();
1048
+ const redoBtn = makeButton2(
1049
+ LABEL_REDO,
1050
+ "Redo",
1051
+ "redo",
1052
+ void 0,
1053
+ false,
1054
+ !options.canRedo()
1055
+ );
1056
+ redoBtn.onclick = () => options.onRedo();
1057
+ const grp1 = makeGroup2();
1058
+ grp1.appendChild(undoBtn);
1059
+ grp1.appendChild(redoBtn);
1060
+ toolbar.appendChild(grp1);
1061
+ toolbar.appendChild(makeSep2());
1062
+ const grp2 = makeGroup2();
1063
+ grp2.className = "toolbar-group collapse-on-small";
1064
+ grp2.appendChild(
1065
+ makeSelect2(
1066
+ "Format",
1067
+ "formatBlock",
1068
+ FORMAT_OPTIONS,
1069
+ format.formatBlock
1070
+ )
1071
+ );
1072
+ grp2.appendChild(
1073
+ makeSelect2("Font", "fontName", FONT_OPTIONS, format.fontName)
1074
+ );
1075
+ grp2.appendChild(
1076
+ makeSelect2("Size", "fontSize", SIZE_OPTIONS, format.fontSize)
1077
+ );
1078
+ toolbar.appendChild(grp2);
1079
+ toolbar.appendChild(makeSep2());
1080
+ const grp3 = makeGroup2();
1081
+ grp3.appendChild(
1082
+ makeButton2(LABEL_BOLD, "Bold", "bold", void 0, format.bold)
1083
+ );
1084
+ grp3.appendChild(
1085
+ makeButton2(LABEL_ITALIC, "Italic", "italic", void 0, format.italic)
1086
+ );
1087
+ grp3.appendChild(
1088
+ makeButton2(
1089
+ LABEL_UNDERLINE,
1090
+ "Underline",
1091
+ "underline",
1092
+ void 0,
1093
+ format.underline
1094
+ )
1095
+ );
1096
+ grp3.appendChild(makeButton2(LABEL_STRIKETHROUGH, "Strikethrough", "strike"));
1097
+ toolbar.appendChild(grp3);
1098
+ toolbar.appendChild(makeSep2());
1099
+ const grp4 = makeGroup2();
1100
+ grp4.appendChild(makeButton2(LABEL_ALIGN_LEFT, "Align left", "align", "left"));
1101
+ grp4.appendChild(
1102
+ makeButton2(LABEL_ALIGN_CENTER, "Align center", "align", "center")
1103
+ );
1104
+ grp4.appendChild(
1105
+ makeButton2(LABEL_ALIGN_RIGHT, "Align right", "align", "right")
1106
+ );
1107
+ toolbar.appendChild(grp4);
1108
+ toolbar.appendChild(makeSep2());
1109
+ const grp5 = makeGroup2();
1110
+ grp5.className = "toolbar-group collapse-on-small";
1111
+ grp5.appendChild(
1112
+ makeColorInput(
1113
+ doc,
1114
+ options,
1115
+ "Text color",
1116
+ "foreColor",
1117
+ format.foreColor
1118
+ )
1119
+ );
1120
+ grp5.appendChild(
1121
+ makeColorInput(
1122
+ doc,
1123
+ options,
1124
+ "Highlight color",
1125
+ "hiliteColor",
1126
+ format.hiliteColor
1127
+ )
1128
+ );
1129
+ toolbar.appendChild(grp5);
1130
+ toolbar.appendChild(makeSep2());
1131
+ const grp6 = makeGroup2();
1132
+ grp6.className = "toolbar-group collapse-on-small";
1133
+ grp6.appendChild(makeButton2(LABEL_LINK, "Insert link", "link"));
1134
+ toolbar.appendChild(grp6);
1135
+ setupOverflow(doc, toolbar, options, format, {
1136
+ makeSelect: makeSelect2,
1137
+ makeColorInput: (title, command, initial) => makeColorInput(doc, options, title, command, initial),
1138
+ makeButton: makeButton2,
1139
+ makeGroup: makeGroup2
1140
+ });
1141
+ setupNavigation(toolbar);
1081
1142
  doc.body.insertBefore(toolbar, doc.body.firstChild);
1082
1143
  }
1083
1144