@pie-lib/editable-html-tip-tap 2.1.7 → 2.1.9

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.
@@ -169,14 +169,27 @@ export const CSSMark = Mark.create({
169
169
  },
170
170
 
171
171
  parseHTML() {
172
- // Any span with a class that matches one of allowed classes
172
+ // Any span with a class that matches one of allowed classes any span that carries a class attribute
173
+ // so that pre-existing spans are preserved when loading content
173
174
  return [
174
175
  {
175
176
  tag: 'span[class]',
176
177
  getAttrs: (el) => {
177
178
  const cls = el.getAttribute('class') || '';
178
- const match = this.options.classes.find((name) => cls.includes(name));
179
- return match ? { class: match } : false;
179
+
180
+ if (!cls) {
181
+ return false;
182
+ }
183
+
184
+ const allowedClasses = (this.options && this.options.classes) || [];
185
+
186
+ if (allowedClasses.length > 0) {
187
+ const match = this.options.classes.find((name) => cls.includes(name));
188
+
189
+ return match ? { class: match } : false;
190
+ }
191
+
192
+ return { class: cls };
180
193
  },
181
194
  },
182
195
  ];
@@ -23,7 +23,9 @@ export const EnsureTextAfterMathPlugin = (mathNodeName) =>
23
23
  key: ensureTextAfterMathPluginKey,
24
24
  appendTransaction: (transactions, oldState, newState) => {
25
25
  // Only act when the doc actually changed
26
- if (!transactions.some((tr) => tr.docChanged)) return null;
26
+ if (!transactions.some((tr) => tr.docChanged)) {
27
+ return null;
28
+ }
27
29
 
28
30
  const tr = newState.tr;
29
31
  let changed = false;
@@ -202,6 +204,7 @@ export const MathNodeView = (props) => {
202
204
  const { node, updateAttributes, editor, selected, options } = props;
203
205
  const [showToolbar, setShowToolbar] = useState(selected);
204
206
  const toolbarRef = useRef(null);
207
+ const nodeRef = useRef(null);
205
208
  const timestamp = useRef(Date.now());
206
209
  const [position, setPosition] = useState({ top: 0, left: 0 });
207
210
  const { math: mathOptions = {} } = options || {};
@@ -232,28 +235,102 @@ export const MathNodeView = (props) => {
232
235
  editor.commands.focus();
233
236
  };
234
237
 
238
+ // Only open the toolbar when this node is *explicitly* selected
239
+ // via a NodeSelection — not when it's merely included in a broader
240
+ // TextSelection or AllSelection (e.g. click-drag across math, or Cmd+A).
235
241
  useEffect(() => {
236
- if (selected) {
242
+ if (!selected) return;
243
+
244
+ const { selection } = editor.state;
245
+ const isNodeSelected = selection.node?.type?.name === 'math';
246
+
247
+ if (isNodeSelected) {
237
248
  setShowToolbar(true);
238
249
  }
239
- }, [selected]);
250
+ }, [selected, editor]);
240
251
 
241
252
  useEffect(() => {
242
253
  setToolbarOpened(editor, selected || showToolbar);
243
254
  }, [editor, showToolbar, selected]);
244
255
 
245
256
  useEffect(() => {
246
- // Calculate position relative to selection
247
- const { from } = editor.state.selection;
248
- const start = editor.view.coordsAtPos(from);
249
- const editorDOM = editor.options.element;
250
- const editorRect = editorDOM.getBoundingClientRect();
251
-
252
- setPosition({
253
- top: start.top - editorRect.top + 40, // shift above
254
- left: start.left - editorRect.left,
257
+ if (!editor || !showToolbar) {
258
+ setPosition({ top: 0, left: 0 });
259
+ return;
260
+ }
261
+
262
+ // Clamp in viewport coordinates, then convert to portal-container-relative values
263
+ // for position: absolute (toolbar is portaled into _tiptapContainerEl or document.body).
264
+ const updatePosition = () => {
265
+ if (!toolbarRef.current) {
266
+ return;
267
+ }
268
+
269
+ const { from } = editor.state.selection;
270
+ const start = editor.view.coordsAtPos(from);
271
+ const nodeRect = nodeRef.current?.getBoundingClientRect?.();
272
+
273
+ // Anchor to the math node element when available; fall back to selection coords.
274
+ const anchorTop = nodeRect?.height ? nodeRect.top : start.top;
275
+ const anchorLeft = nodeRect?.width ? nodeRect.left : start.left;
276
+ const anchorBottom = nodeRect?.height ? nodeRect.bottom : (start.bottom ?? start.top);
277
+
278
+ const toolbarHeight = toolbarRef.current.offsetHeight;
279
+ const toolbarWidth = toolbarRef.current.offsetWidth;
280
+
281
+ const gap = 0;
282
+ const spaceBelow = window.innerHeight - (anchorBottom + gap);
283
+
284
+ // Place the toolbar's top-left corner directly below the anchor; flip above when needed.
285
+ let top = spaceBelow >= toolbarHeight ? anchorBottom + gap : anchorTop - toolbarHeight - gap;
286
+ let left = anchorLeft;
287
+
288
+ const margin = 8;
289
+ top = Math.max(margin, Math.min(top, window.innerHeight - toolbarHeight - margin));
290
+ left = Math.max(margin, Math.min(left, window.innerWidth - toolbarWidth - margin));
291
+
292
+ const portalEl = editor._tiptapContainerEl || document.body;
293
+ const containerRect = portalEl.getBoundingClientRect();
294
+
295
+ setPosition({
296
+ top: top - containerRect.top,
297
+ left: left - containerRect.left,
298
+ });
299
+ };
300
+
301
+ updatePosition();
302
+
303
+ let frame = null;
304
+ const scheduleUpdate = () => {
305
+ if (frame !== null) {
306
+ return;
307
+ }
308
+
309
+ frame = requestAnimationFrame(() => {
310
+ frame = null;
311
+ updatePosition();
312
+ });
313
+ };
314
+
315
+ frame = requestAnimationFrame(() => {
316
+ frame = null;
317
+ updatePosition();
255
318
  });
256
319
 
320
+ window.addEventListener('scroll', scheduleUpdate, true);
321
+ window.addEventListener('resize', scheduleUpdate);
322
+
323
+ return () => {
324
+ if (frame !== null) {
325
+ cancelAnimationFrame(frame);
326
+ }
327
+
328
+ window.removeEventListener('scroll', scheduleUpdate, true);
329
+ window.removeEventListener('resize', scheduleUpdate);
330
+ };
331
+ }, [editor, showToolbar]);
332
+
333
+ useEffect(() => {
257
334
  const handleClickOutside = (event) => {
258
335
  const target = event?.target;
259
336
 
@@ -298,7 +375,7 @@ export const MathNodeView = (props) => {
298
375
  }
299
376
 
300
377
  return () => document.removeEventListener('click', handleClickOutside);
301
- }, [editor, showToolbar]);
378
+ }, [editor, showToolbar, node]);
302
379
 
303
380
  return (
304
381
  <NodeViewWrapper
@@ -310,7 +387,7 @@ export const MathNodeView = (props) => {
310
387
  }}
311
388
  data-selected={selected}
312
389
  >
313
- <div onClick={() => setShowToolbar(true)} contentEditable={false}>
390
+ <div ref={nodeRef} onClick={() => setShowToolbar(true)} contentEditable={false}>
314
391
  <MathPreview latex={latex} />
315
392
  </div>
316
393
  {showToolbar &&
@@ -322,7 +399,7 @@ export const MathNodeView = (props) => {
322
399
  position: 'absolute',
323
400
  top: `${position.top}px`,
324
401
  left: `${position.left}px`,
325
- zIndex: 20,
402
+ zIndex: 1000,
326
403
  background: 'var(--editable-html-toolbar-bg, #efefef)',
327
404
  boxShadow:
328
405
  '0px 1px 5px 0px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.12)',