tetrons 2.4.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,10 @@ interface TetronsEditorProps {
5
5
  config?: TetronsConfig;
6
6
  onError?: (msg: string) => void;
7
7
  onSuccess?: (details: ValidationResponse) => void;
8
+ onChange?: (payload: {
9
+ html: string;
10
+ json: unknown;
11
+ }) => void;
8
12
  }
9
- export default function TetronsEditor({ apiKey, config, onError, onSuccess, }: TetronsEditorProps): import("react/jsx-runtime").JSX.Element | null;
13
+ export default function TetronsEditor({ apiKey, config, onError, onSuccess, onChange, }: TetronsEditorProps): import("react/jsx-runtime").JSX.Element | null;
10
14
  export {};
@@ -86,6 +86,10 @@ export interface EditorRuntimeProps {
86
86
  showParagraphDropdown?: boolean;
87
87
  showFontDropdown?: boolean;
88
88
  showFontSizeDropdown?: boolean;
89
+ onChange?: (payload: {
90
+ html: string;
91
+ json: unknown;
92
+ }) => void;
89
93
  }
90
94
  interface BaseProps {
91
95
  apiKey: string;
package/dist/index.css CHANGED
@@ -4886,3 +4886,69 @@ textarea#email-body {
4886
4886
  z-index: 9999999999 !important;
4887
4887
  position: relative;
4888
4888
  }
4889
+ .ProseMirror table {
4890
+ border-collapse: collapse;
4891
+ table-layout: fixed;
4892
+ width: 100%;
4893
+ max-width: 100%;
4894
+ margin: 0.75em 0;
4895
+ background: #fff;
4896
+ }
4897
+ .ProseMirror th,
4898
+ .ProseMirror td {
4899
+ border: 1px solid #d0d0d0;
4900
+ padding: 6px 8px;
4901
+ vertical-align: top;
4902
+ word-break: break-word;
4903
+ overflow-wrap: anywhere;
4904
+ }
4905
+ .ProseMirror th {
4906
+ background-color: #f5f5f5;
4907
+ font-weight: 600;
4908
+ text-align: left;
4909
+ }
4910
+ .ProseMirror .selectedCell {
4911
+ background: rgba(200, 200, 255, 0.4);
4912
+ }
4913
+ .ProseMirror table p {
4914
+ margin: 0;
4915
+ }
4916
+ .ProseMirror {
4917
+ overflow-x: auto;
4918
+ }
4919
+ @media (max-width: 768px) {
4920
+ .ProseMirror table {
4921
+ display: block;
4922
+ width: max-content;
4923
+ max-width: 100%;
4924
+ }
4925
+ .ProseMirror th,
4926
+ .ProseMirror td {
4927
+ min-width: 120px;
4928
+ }
4929
+ }
4930
+ @media (max-width: 480px) {
4931
+ .ProseMirror th,
4932
+ .ProseMirror td {
4933
+ min-width: 100px;
4934
+ padding: 6px;
4935
+ font-size: 13px;
4936
+ }
4937
+ }
4938
+ .ProseMirror .column-resize-handle {
4939
+ position: absolute;
4940
+ right: -2px;
4941
+ top: 0;
4942
+ bottom: 0;
4943
+ width: 4px;
4944
+ background-color: rgba(0, 0, 0, 0.15);
4945
+ pointer-events: none;
4946
+ }
4947
+ .ProseMirror th:hover .column-resize-handle,
4948
+ .ProseMirror td:hover .column-resize-handle {
4949
+ pointer-events: auto;
4950
+ }
4951
+ .ProseMirror table:focus-within {
4952
+ outline: 2px solid rgba(100, 150, 255, 0.35);
4953
+ outline-offset: 2px;
4954
+ }
package/dist/index.mjs CHANGED
@@ -10940,85 +10940,6 @@ function InsertGroup({
10940
10940
  };
10941
10941
  const handleTableInsert = (rows, cols) => {
10942
10942
  editor.chain().focus().insertTable({ rows, cols, withHeaderRow: true }).run();
10943
- setTimeout(() => {
10944
- var _a, _b;
10945
- const table = editor.view.dom.querySelector("table");
10946
- if (table && !((_a = table.parentElement) == null ? void 0 : _a.classList.contains("resizable-table-wrapper"))) {
10947
- const wrapper = document.createElement("div");
10948
- wrapper.className = "resizable-table-wrapper";
10949
- wrapper.style.position = "relative";
10950
- wrapper.style.display = "inline-block";
10951
- wrapper.style.width = "600px";
10952
- wrapper.style.maxWidth = "100%";
10953
- (_b = table.parentNode) == null ? void 0 : _b.insertBefore(wrapper, table);
10954
- wrapper.appendChild(table);
10955
- const resizeHandle = document.createElement("div");
10956
- resizeHandle.className = "resize-handle";
10957
- resizeHandle.style.position = "absolute";
10958
- resizeHandle.style.right = "0";
10959
- resizeHandle.style.bottom = "0";
10960
- resizeHandle.style.width = "12px";
10961
- resizeHandle.style.height = "12px";
10962
- resizeHandle.style.cursor = "se-resize";
10963
- resizeHandle.style.background = "transparent";
10964
- wrapper.appendChild(resizeHandle);
10965
- const dragHandle = document.createElement("div");
10966
- dragHandle.className = "drag-handle";
10967
- dragHandle.style.position = "absolute";
10968
- dragHandle.style.left = "0";
10969
- dragHandle.style.top = "0";
10970
- dragHandle.style.width = "16px";
10971
- dragHandle.style.height = "16px";
10972
- dragHandle.style.cursor = "move";
10973
- dragHandle.style.background = "transparent";
10974
- dragHandle.style.display = "flex";
10975
- dragHandle.style.justifyContent = "center";
10976
- dragHandle.style.alignItems = "center";
10977
- dragHandle.innerHTML = "\u2630";
10978
- dragHandle.style.color = "#fff";
10979
- dragHandle.style.fontSize = "12px";
10980
- wrapper.appendChild(dragHandle);
10981
- let startWidth = 0;
10982
- let startHeight = 0;
10983
- let startX = 0;
10984
- let startY = 0;
10985
- resizeHandle.addEventListener("mousedown", (e) => {
10986
- e.preventDefault();
10987
- startX = e.clientX;
10988
- startY = e.clientY;
10989
- startWidth = wrapper.offsetWidth;
10990
- startHeight = wrapper.offsetHeight;
10991
- const onMouseMove = (e2) => {
10992
- const dx = e2.clientX - startX;
10993
- const dy = e2.clientY - startY;
10994
- wrapper.style.width = Math.max(100, startWidth + dx) + "px";
10995
- wrapper.style.height = Math.max(50, startHeight + dy) + "px";
10996
- };
10997
- const onMouseUp = () => {
10998
- document.removeEventListener("mousemove", onMouseMove);
10999
- document.removeEventListener("mouseup", onMouseUp);
11000
- };
11001
- document.addEventListener("mousemove", onMouseMove);
11002
- document.addEventListener("mouseup", onMouseUp);
11003
- });
11004
- dragHandle.addEventListener("mousedown", (e) => {
11005
- e.preventDefault();
11006
- startX = e.clientX;
11007
- startY = e.clientY;
11008
- const onMouseMove = (e2) => {
11009
- const dx = e2.clientX - startX;
11010
- const dy = e2.clientY - startY;
11011
- wrapper.style.transform = `translate(${dx}px, ${dy}px)`;
11012
- };
11013
- const onMouseUp = () => {
11014
- document.removeEventListener("mousemove", onMouseMove);
11015
- document.removeEventListener("mouseup", onMouseUp);
11016
- };
11017
- document.addEventListener("mousemove", onMouseMove);
11018
- document.addEventListener("mouseup", onMouseUp);
11019
- });
11020
- }
11021
- }, 0);
11022
10943
  setShowTableGrid(false);
11023
10944
  setSelectedRows(1);
11024
10945
  setSelectedCols(1);
@@ -12142,6 +12063,7 @@ var SpellCheck = Extension.create({
12142
12063
  let spell = null;
12143
12064
  let activePopup = null;
12144
12065
  let lastHoveredWord = null;
12066
+ const key = new PluginKey("spellcheck");
12145
12067
  function closePopup() {
12146
12068
  if (activePopup) {
12147
12069
  activePopup.remove();
@@ -12150,25 +12072,22 @@ var SpellCheck = Extension.create({
12150
12072
  }
12151
12073
  return [
12152
12074
  new Plugin({
12153
- key: new PluginKey("spellcheck"),
12154
- view: (editorView) => {
12155
- fetch("https://tetrons.com/api/dictionary").then((r) => r.json()).then((dict) => spell = nspell(dict)).catch(
12156
- (e) => console.error("Spellcheck dictionary load failed:", e)
12157
- );
12158
- const handleMouseMove2 = (event) => {
12159
- if (!spell) return;
12160
- if (activePopup) {
12161
- const rect = activePopup.getBoundingClientRect();
12162
- if (event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom) {
12163
- return;
12164
- }
12075
+ key,
12076
+ view(view) {
12077
+ let destroyed = false;
12078
+ fetch("https://tetrons.com/api/dictionary").then((r) => r.json()).then((dict) => {
12079
+ if (!destroyed) {
12080
+ spell = nspell(dict);
12165
12081
  }
12166
- const pos = editorView.posAtCoords({
12167
- left: event.clientX,
12168
- top: event.clientY
12082
+ }).catch(console.error);
12083
+ const onMouseMove = (e) => {
12084
+ if (!spell) return;
12085
+ const coords = view.posAtCoords({
12086
+ left: e.clientX,
12087
+ top: e.clientY
12169
12088
  });
12170
- if (!pos) return;
12171
- const word = getWordAtPosition(editorView, pos.pos);
12089
+ if (!coords) return;
12090
+ const word = getWordAtPosition(view, coords.pos);
12172
12091
  if (!word) return;
12173
12092
  if (!spell.correct(word.text)) {
12174
12093
  if (lastHoveredWord && lastHoveredWord.start === word.start && lastHoveredWord.end === word.end) {
@@ -12176,57 +12095,54 @@ var SpellCheck = Extension.create({
12176
12095
  }
12177
12096
  lastHoveredWord = word;
12178
12097
  closePopup();
12179
- const suggestions = spell.suggest(word.text);
12180
12098
  activePopup = showSuggestionsPopup(
12181
- editorView,
12182
- pos.pos,
12099
+ view,
12100
+ coords.pos,
12183
12101
  word,
12184
- suggestions,
12102
+ spell.suggest(word.text),
12185
12103
  closePopup
12186
12104
  );
12187
- }
12188
- };
12189
- const handleOutsideClick = (event) => {
12190
- if (activePopup && !activePopup.contains(event.target)) {
12105
+ } else {
12191
12106
  closePopup();
12192
12107
  }
12193
12108
  };
12194
- document.addEventListener("mousemove", handleMouseMove2);
12195
- document.addEventListener("mousedown", handleOutsideClick);
12109
+ document.addEventListener("mousemove", onMouseMove);
12110
+ document.addEventListener("mousedown", closePopup);
12196
12111
  return {
12197
12112
  destroy() {
12198
- document.removeEventListener("mousemove", handleMouseMove2);
12199
- document.removeEventListener("mousedown", handleOutsideClick);
12113
+ destroyed = true;
12114
+ closePopup();
12115
+ document.removeEventListener("mousemove", onMouseMove);
12116
+ document.removeEventListener("mousedown", closePopup);
12200
12117
  }
12201
12118
  };
12202
12119
  },
12203
- state: {
12204
- init() {
12205
- return DecorationSet.empty;
12206
- },
12207
- apply(tr, old) {
12208
- if (!spell) return old;
12209
- const text = tr.doc.textBetween(0, tr.doc.content.size, " ");
12210
- const words = text.split(/\s+/);
12211
- const decorations = [];
12212
- let pos = 0;
12213
- for (const word of words) {
12214
- const clean = word.replace(/[.,!?;:()"\']/g, "");
12215
- if (clean.length > 0 && !spell.correct(clean)) {
12216
- decorations.push(
12217
- Decoration.inline(pos, pos + word.length, {
12218
- class: "spell-wrong"
12219
- })
12220
- );
12221
- }
12222
- pos += word.length + 1;
12223
- }
12224
- return DecorationSet.create(tr.doc, decorations);
12225
- }
12226
- },
12227
12120
  props: {
12228
12121
  decorations(state) {
12229
- return this.getState(state);
12122
+ const sp = spell;
12123
+ if (!sp) return DecorationSet.empty;
12124
+ const decorations = [];
12125
+ state.doc.descendants((node, pos) => {
12126
+ if (!node.isText || !node.text) return;
12127
+ const text = node.text;
12128
+ const wordRegex = /\b[^\s.,!?;:()"'`]+\b/g;
12129
+ let match;
12130
+ while (match = wordRegex.exec(text)) {
12131
+ const word = match[0];
12132
+ const from = pos + match.index;
12133
+ const to = from + word.length;
12134
+ if (from < 0) continue;
12135
+ if (to > state.doc.content.size) continue;
12136
+ if (!sp.correct(word)) {
12137
+ decorations.push(
12138
+ Decoration.inline(from, to, {
12139
+ class: "spell-wrong"
12140
+ })
12141
+ );
12142
+ }
12143
+ }
12144
+ });
12145
+ return DecorationSet.create(state.doc, decorations);
12230
12146
  }
12231
12147
  }
12232
12148
  })
@@ -12235,41 +12151,39 @@ var SpellCheck = Extension.create({
12235
12151
  });
12236
12152
  function getWordAtPosition(view, pos) {
12237
12153
  const $pos = view.state.doc.resolve(pos);
12238
- const parent = $pos.parent;
12239
- const offset = $pos.parentOffset;
12240
- const text = parent.textContent;
12241
- let start = offset;
12242
- let end = offset;
12243
- while (start > 0 && !/\s/.test(text[start - 1])) {
12244
- start--;
12245
- }
12246
- while (end < text.length && !/\s/.test(text[end])) {
12247
- end++;
12248
- }
12154
+ if (!$pos.parent.isTextblock) return null;
12155
+ const text = $pos.parent.textContent;
12156
+ let start = $pos.parentOffset;
12157
+ let end = start;
12158
+ while (start > 0 && !/\s/.test(text[start - 1])) start--;
12159
+ while (end < text.length && !/\s/.test(text[end])) end++;
12249
12160
  const word = text.slice(start, end);
12250
12161
  if (!word.trim()) return null;
12251
- const absStart = $pos.start() + start;
12252
- const absEnd = $pos.start() + end;
12253
- return { text: word, start: absStart, end: absEnd };
12162
+ return {
12163
+ text: word,
12164
+ start: $pos.start() + start,
12165
+ end: $pos.start() + end
12166
+ };
12254
12167
  }
12255
- function showSuggestionsPopup(view, pos, word, suggestions, closePopup) {
12256
- const dom = document.createElement("div");
12257
- dom.className = "spell-suggestions-popup";
12168
+ function showSuggestionsPopup(view, pos, word, suggestions, close2) {
12169
+ const el = document.createElement("div");
12170
+ el.className = "spell-suggestions-popup";
12171
+ el.style.position = "absolute";
12258
12172
  suggestions.forEach((s) => {
12259
12173
  const item = document.createElement("div");
12260
12174
  item.className = "suggestion-item";
12261
12175
  item.textContent = s;
12262
12176
  item.onclick = () => {
12263
12177
  view.dispatch(view.state.tr.insertText(s, word.start, word.end));
12264
- closePopup();
12178
+ close2();
12265
12179
  };
12266
- dom.appendChild(item);
12180
+ el.appendChild(item);
12267
12181
  });
12268
- document.body.appendChild(dom);
12182
+ document.body.appendChild(el);
12269
12183
  const coords = view.coordsAtPos(pos);
12270
- dom.style.left = coords.left + "px";
12271
- dom.style.top = coords.bottom + "px";
12272
- return dom;
12184
+ el.style.left = `${coords.left}px`;
12185
+ el.style.top = `${coords.bottom}px`;
12186
+ return el;
12273
12187
  }
12274
12188
  function AIModal({
12275
12189
  open,
@@ -16588,9 +16502,8 @@ function Editor2(props) {
16588
16502
  }, [versions]);
16589
16503
  useEffect(() => {
16590
16504
  if (!editor) return;
16591
- const current = editor.getHTML();
16592
- editor.commands.setContent(current);
16593
- }, [editor, spellCheckEnabled]);
16505
+ editor.view.dispatch(editor.state.tr);
16506
+ }, [spellCheckEnabled]);
16594
16507
  const showThirdRow = props.showParagraphDropdown || props.showFontDropdown || props.showFontSizeDropdown;
16595
16508
  const handleAISubmit = async (prompt) => {
16596
16509
  try {
@@ -16820,16 +16733,6 @@ function Editor2(props) {
16820
16733
  if (!editor || !editorRight) return;
16821
16734
  editorRight.commands.setContent(editor.getJSON());
16822
16735
  }, [editor, editorRight]);
16823
- useEffect(() => {
16824
- if (!editor || !editorRight) return;
16825
- const handler = () => {
16826
- editorRight.commands.setContent(editor.getJSON());
16827
- };
16828
- editor.on("update", handler);
16829
- return () => {
16830
- editor.off("update", handler);
16831
- };
16832
- }, [editor, editorRight]);
16833
16736
  function dispatchTetronsEvent2(name, detail) {
16834
16737
  if (typeof window !== "undefined") {
16835
16738
  window.dispatchEvent(new CustomEvent(name, { detail }));
@@ -16979,6 +16882,20 @@ function Editor2(props) {
16979
16882
  onHelpOpen
16980
16883
  );
16981
16884
  }, []);
16885
+ useEffect(() => {
16886
+ if (!editor || !props.onChange) return;
16887
+ const handler = () => {
16888
+ var _a2;
16889
+ (_a2 = props.onChange) == null ? void 0 : _a2.call(props, {
16890
+ html: editor.getHTML(),
16891
+ json: editor.getJSON()
16892
+ });
16893
+ };
16894
+ editor.on("update", handler);
16895
+ return () => {
16896
+ editor.off("update", handler);
16897
+ };
16898
+ }, [editor, props.onChange]);
16982
16899
  if (!editor) return null;
16983
16900
  return /* @__PURE__ */ jsxs("div", { className: "tetrons-editor__container", children: [
16984
16901
  props.topMenu && /* @__PURE__ */ jsx(
@@ -17322,7 +17239,8 @@ function TetronsEditor({
17322
17239
  apiKey,
17323
17240
  config,
17324
17241
  onError,
17325
- onSuccess
17242
+ onSuccess,
17243
+ onChange
17326
17244
  }) {
17327
17245
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
17328
17246
  const [loading, setLoading] = useState(true);
@@ -17410,7 +17328,8 @@ function TetronsEditor({
17410
17328
  topMenu: (_d = (_c = config == null ? void 0 : config.ui) == null ? void 0 : _c.topMenu) != null ? _d : true,
17411
17329
  showParagraphDropdown: (_f = (_e = config == null ? void 0 : config.ui) == null ? void 0 : _e.paragraphDropdown) != null ? _f : true,
17412
17330
  showFontDropdown: (_h = (_g = config == null ? void 0 : config.ui) == null ? void 0 : _g.fontDropdown) != null ? _h : true,
17413
- showFontSizeDropdown: (_j = (_i = config == null ? void 0 : config.ui) == null ? void 0 : _i.fontSizeDropdown) != null ? _j : true
17331
+ showFontSizeDropdown: (_j = (_i = config == null ? void 0 : config.ui) == null ? void 0 : _i.fontSizeDropdown) != null ? _j : true,
17332
+ onChange
17414
17333
  }
17415
17334
  );
17416
17335
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tetrons",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "Tetrons is a fully-featured, modern WYSIWYG rich text editor built on TipTap.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.mjs",
@@ -68,7 +68,7 @@
68
68
  "nspell": "^2.1.5",
69
69
  "re-resizable": "^6.11.2",
70
70
  "react-icons": "^5.5.0",
71
- "tesseract.js": "^6.0.1"
71
+ "tesseract.js": "^7.0.0"
72
72
  },
73
73
  "peerDependencies": {
74
74
  "react": ">=18",
@@ -93,7 +93,7 @@
93
93
  "optionalDependencies": {
94
94
  "mongoose": "^9.0.1",
95
95
  "nodemailer": "^7.0.11",
96
- "openai": "^6.10.0"
96
+ "openai": "^6.13.0"
97
97
  },
98
98
  "exports": {
99
99
  ".": {