@zzish/math-rich-input 0.1.49 → 0.1.51

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/src/standalone.js CHANGED
@@ -10,19 +10,66 @@ import "./Toolbar.css";
10
10
  import "./EquationEditor.css";
11
11
  import "./SymbolButton.css";
12
12
 
13
+ // Global drag state tracking for SortableJS compatibility
14
+ let globalDragState = {
15
+ isDragging: false,
16
+ activeElement: null,
17
+ timeoutId: null,
18
+ dragStartTime: 0,
19
+ maxDragDuration: 2000, // 2 seconds max drag duration (reduced)
20
+ disconnectedElements: new Set(),
21
+ reconnectionTimeouts: new Map(),
22
+ forceRenderQueue: new Set(),
23
+ };
24
+
13
25
  class MathRichInputElement extends HTMLElement {
14
26
  constructor() {
15
27
  super();
16
28
  this.root = null;
29
+ this.renderTimeout = null;
30
+ this.isRendering = false;
31
+ this.lastRenderTime = 0;
32
+ this.renderThrottleMs = 16; // ~60fps throttle
33
+ this.isDuringDrag = false;
34
+
35
+ // Setup global drag event listeners once
36
+ this.setupDragListeners();
17
37
  }
18
38
 
19
39
  connectedCallback() {
20
- this.render();
40
+ // Handle reconnection after disconnect (common in SortableJS)
41
+ this.handleReconnection();
42
+
43
+ // Detect if we're during a SortableJS operation
44
+ this.detectDragOperation();
45
+
46
+ // Use throttled, safe render
47
+ this.safeRender();
21
48
  }
22
49
 
23
50
  disconnectedCallback() {
51
+ // Track this element as disconnected (for potential reconnection)
52
+ globalDragState.disconnectedElements.add(this);
53
+
54
+ // Clear any pending render timeouts
55
+ if (this.renderTimeout) {
56
+ clearTimeout(this.renderTimeout);
57
+ this.renderTimeout = null;
58
+ }
59
+
60
+ // Reset state flags
61
+ this.isRendering = false;
62
+ this.isDuringDrag = false;
63
+
64
+ // Unmount React root
24
65
  if (this.root) {
25
- this.root.unmount();
66
+ try {
67
+ this.root.unmount();
68
+ } catch (error) {
69
+ console.error("❌ MathRichInput: Error unmounting React root:", error);
70
+ } finally {
71
+ this.root = null;
72
+ }
26
73
  }
27
74
  }
28
75
 
@@ -34,73 +81,383 @@ class MathRichInputElement extends HTMLElement {
34
81
  "disabled",
35
82
  "mimetype",
36
83
  "outputtex",
84
+ "disableanimation",
37
85
  ];
38
86
  }
39
87
 
40
88
  attributeChangedCallback(name, oldValue, newValue) {
41
89
  // Avoid infinite re-render loop by checking if value actually changed
42
90
  if (oldValue !== newValue) {
43
- this.render();
91
+ this.safeRender();
44
92
  }
45
93
  }
46
94
 
47
- render() {
48
- const value = this.getAttribute("value") || "";
49
- const placeholder = this.getAttribute("placeholder") || "";
50
- const readonly = this.hasAttribute("readonly");
51
- const disabled = this.hasAttribute("disabled");
52
- const mimeType = this.getAttribute("mimetype") || "";
53
- const outputTexAttr = this.getAttribute("outputtex");
54
- const outputTex =
55
- outputTexAttr !== "false" && this.hasAttribute("outputtex");
56
-
57
- // Create container if it doesn't exist
58
- if (!this.querySelector(".math-rich-input-container")) {
59
- const container = document.createElement("div");
60
- container.className = "math-rich-input-container";
61
- this.appendChild(container);
62
- }
63
-
64
- const container = this.querySelector(".math-rich-input-container");
65
-
66
- if (!this.root) {
67
- this.root = createRoot(container);
68
- }
69
-
70
- this.root.render(
71
- React.createElement(MathRichInput, {
72
- value: value,
73
- placeholder: placeholder,
74
- readOnly: readonly,
75
- disabled: disabled,
76
- mimeType: mimeType,
77
- outputTex: outputTex,
78
- className: this.className || "",
79
- onChange: (data, options) => {
80
- const { value: newValue, mimeType: newMimeType } = data;
81
-
82
- // Only update attributes if they actually changed to avoid infinite re-render
83
- const currentValue = this.getAttribute("value") || "";
84
- const currentMimeType = this.getAttribute("mimetype") || "";
85
-
86
- if (newValue !== currentValue) {
87
- this.setAttribute("value", newValue);
95
+ /**
96
+ * Setup global drag event listeners for SortableJS compatibility
97
+ */
98
+ setupDragListeners() {
99
+ // Only setup once
100
+ if (globalDragState.listenersSetup) return;
101
+ globalDragState.listenersSetup = true;
102
+
103
+ // Listen for SortableJS drag events on document
104
+ document.addEventListener("sortableStart", () => {
105
+ globalDragState.isDragging = true;
106
+ globalDragState.dragStartTime = Date.now();
107
+ });
108
+
109
+ document.addEventListener("sortableEnd", () => {
110
+ // IMMEDIATE state reset - don't wait for any timeouts
111
+ globalDragState.isDragging = false;
112
+ globalDragState.dragStartTime = 0;
113
+
114
+ // Force render all elements after very short delay
115
+ setTimeout(() => {
116
+ MathRichInputElement.forceResetDragState();
117
+ }, 1); // Minimal delay to ensure DOM operations complete
118
+ });
119
+
120
+ // Fallback: Monitor DOM mutations for drag classes
121
+ const observer = new MutationObserver((mutations) => {
122
+ mutations.forEach((mutation) => {
123
+ if (
124
+ mutation.type === "attributes" &&
125
+ mutation.attributeName === "class"
126
+ ) {
127
+ const element = mutation.target;
128
+ const hasGhost = element.classList.contains("sortable-ghost");
129
+ const hasChosen = element.classList.contains("sortable-chosen");
130
+
131
+ if (hasGhost || hasChosen) {
132
+ if (!globalDragState.isDragging) {
133
+ globalDragState.isDragging = true;
134
+ globalDragState.dragStartTime = Date.now();
135
+ globalDragState.activeElement = element;
136
+ }
88
137
  }
89
- if (newMimeType !== currentMimeType) {
90
- this.setAttribute("mimetype", newMimeType);
138
+ }
139
+ });
140
+ });
141
+
142
+ observer.observe(document.body, {
143
+ attributes: true,
144
+ subtree: true,
145
+ attributeFilter: ["class"],
146
+ });
147
+
148
+ // Add drag timeout safety
149
+ setInterval(() => {
150
+ if (globalDragState.isDragging && globalDragState.dragStartTime > 0) {
151
+ const elapsed = Date.now() - globalDragState.dragStartTime;
152
+ if (elapsed > globalDragState.maxDragDuration) {
153
+ MathRichInputElement.forceResetDragState();
154
+ }
155
+ }
156
+ }, 1000);
157
+ }
158
+
159
+ /**
160
+ * Force reset drag state (emergency cleanup)
161
+ */
162
+ static forceResetDragState() {
163
+ globalDragState.isDragging = false;
164
+ globalDragState.dragStartTime = 0;
165
+ globalDragState.activeElement = null;
166
+
167
+ if (globalDragState.timeoutId) {
168
+ clearTimeout(globalDragState.timeoutId);
169
+ globalDragState.timeoutId = null;
170
+ }
171
+
172
+ // Force render ALL elements immediately (aggressive approach)
173
+ const allElements = document.querySelectorAll("math-rich-input");
174
+
175
+ allElements.forEach((element) => {
176
+ if (element.isConnected) {
177
+ // Add to force render queue and trigger immediate render
178
+ globalDragState.forceRenderQueue.add(element);
179
+ setTimeout(() => {
180
+ element.forceRender();
181
+ globalDragState.forceRenderQueue.delete(element);
182
+ }, 10);
183
+ }
184
+ });
185
+
186
+ // Clear all tracking sets and maps
187
+ globalDragState.disconnectedElements.clear();
188
+ globalDragState.reconnectionTimeouts.forEach((timeout) =>
189
+ clearTimeout(timeout)
190
+ );
191
+ globalDragState.reconnectionTimeouts.clear();
192
+ globalDragState.forceRenderQueue.clear();
193
+ }
194
+
195
+ /**
196
+ * Handle element reconnection after disconnect (common in SortableJS DOM manipulation)
197
+ */
198
+ handleReconnection() {
199
+ // Remove from disconnected set
200
+ globalDragState.disconnectedElements.delete(this);
201
+
202
+ // Clear any existing reconnection timeout for this element
203
+ if (globalDragState.reconnectionTimeouts.has(this)) {
204
+ clearTimeout(globalDragState.reconnectionTimeouts.get(this));
205
+ globalDragState.reconnectionTimeouts.delete(this);
206
+ }
207
+
208
+ // Add to force render queue for immediate processing
209
+ globalDragState.forceRenderQueue.add(this);
210
+
211
+ // React StrictMode pattern: Clean setup on reconnect
212
+ // Force immediate render regardless of drag state
213
+ setTimeout(() => {
214
+ if (this.isConnected) {
215
+ this.forceRender();
216
+ globalDragState.forceRenderQueue.delete(this);
217
+ }
218
+ }, 5); // Very short delay to ensure DOM is settled
219
+ }
220
+
221
+ /**
222
+ * Detect if element is being manipulated by SortableJS or similar drag libraries
223
+ */
224
+ detectDragOperation() {
225
+ // Primary check: Use global drag state
226
+ if (globalDragState.isDragging) {
227
+ this.isDuringDrag = true;
228
+ return;
229
+ }
230
+
231
+ // Secondary check: Direct element inspection (more conservative)
232
+ const hasDirectDragClass =
233
+ this.classList.contains("sortable-ghost") ||
234
+ this.classList.contains("sortable-chosen") ||
235
+ this.classList.contains("sortable-drag");
236
+
237
+ // Tertiary check: Check immediate parent only (not deep search)
238
+ const parentHasDragClass =
239
+ this.parentElement &&
240
+ (this.parentElement.classList.contains("sortable-ghost") ||
241
+ this.parentElement.classList.contains("sortable-chosen") ||
242
+ this.parentElement.classList.contains("sortable-drag"));
243
+
244
+ this.isDuringDrag = hasDirectDragClass || parentHasDragClass;
245
+
246
+ if (this.isDuringDrag && !globalDragState.isDragging) {
247
+ // Local drag operation detected
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Force render bypassing all safety checks (for reconnection after SortableJS)
253
+ */
254
+ forceRender() {
255
+ // Reset all flags for clean start
256
+ this.isRendering = false;
257
+ this.isDuringDrag = false;
258
+
259
+ // Clear any pending timeouts
260
+ if (this.renderTimeout) {
261
+ clearTimeout(this.renderTimeout);
262
+ this.renderTimeout = null;
263
+ }
264
+
265
+ // Directly call render without any checks
266
+ try {
267
+ this.render();
268
+ } catch (error) {
269
+ console.error("❌ MathRichInput: Force render failed:", error);
270
+
271
+ // Try one more time after a brief delay
272
+ setTimeout(() => {
273
+ if (this.isConnected) {
274
+ try {
275
+ this.render();
276
+ } catch (retryError) {
277
+ console.error(
278
+ "❌ MathRichInput: Force render retry failed:",
279
+ retryError
280
+ );
91
281
  }
282
+ }
283
+ }, 50);
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Throttled, error-safe rendering method
289
+ */
290
+ safeRender() {
291
+ // Force render for elements in the force render queue
292
+ if (globalDragState.forceRenderQueue.has(this)) {
293
+ this.forceRender();
294
+ return;
295
+ }
296
+
297
+ // Skip rendering during drag operations to prevent React conflicts
298
+ if (this.isDuringDrag) {
299
+ return;
300
+ }
301
+
302
+ // Throttle rapid render calls
303
+ const now = Date.now();
304
+ if (now - this.lastRenderTime < this.renderThrottleMs) {
305
+ // Clear previous timeout and schedule new one
306
+ if (this.renderTimeout) {
307
+ clearTimeout(this.renderTimeout);
308
+ }
309
+
310
+ this.renderTimeout = setTimeout(() => {
311
+ this.safeRender();
312
+ }, this.renderThrottleMs);
313
+ return;
314
+ }
315
+
316
+ // Prevent concurrent renders
317
+ if (this.isRendering) {
318
+ return;
319
+ }
320
+
321
+ this.lastRenderTime = now;
322
+ this.isRendering = true;
323
+
324
+ try {
325
+ this.render();
326
+ } catch (error) {
327
+ console.error("❌ MathRichInput: Render error caught:", error);
328
+
329
+ // Attempt graceful recovery
330
+ setTimeout(() => {
331
+ if (!this.isDuringDrag) {
332
+ this.render();
333
+ }
334
+ }, 100);
335
+ } finally {
336
+ this.isRendering = false;
337
+ }
338
+ }
92
339
 
93
- this.dispatchEvent(
94
- new CustomEvent("change", {
95
- detail: {
96
- value: newValue,
97
- mimeType: newMimeType,
98
- },
99
- })
340
+ render() {
341
+ // Check if this is a force render (bypass all safety checks)
342
+ const isForceRender = globalDragState.forceRenderQueue.has(this);
343
+
344
+ if (!isForceRender) {
345
+ // Normal defensive checks
346
+ if (!this.isConnected) {
347
+ return;
348
+ }
349
+
350
+ // Double check drag state before proceeding
351
+ this.detectDragOperation();
352
+ if (this.isDuringDrag) {
353
+ return;
354
+ }
355
+ }
356
+
357
+ try {
358
+ const value = this.getAttribute("value") || "";
359
+ const placeholder = this.getAttribute("placeholder") || "";
360
+ const readonly = this.hasAttribute("readonly");
361
+ const disabled = this.hasAttribute("disabled");
362
+ const mimeType = this.getAttribute("mimetype") || "";
363
+ const outputTexAttr = this.getAttribute("outputtex");
364
+ const outputTex =
365
+ outputTexAttr !== "false" && this.hasAttribute("outputtex");
366
+ const disableAnimationAttr = this.getAttribute("disableanimation");
367
+ const disableAnimation =
368
+ disableAnimationAttr !== "false" &&
369
+ this.hasAttribute("disableanimation");
370
+
371
+ // Create container if it doesn't exist
372
+ if (!this.querySelector(".math-rich-input-container")) {
373
+ const container = document.createElement("div");
374
+ container.className = "math-rich-input-container";
375
+ this.appendChild(container);
376
+ }
377
+
378
+ const container = this.querySelector(".math-rich-input-container");
379
+
380
+ // Safety check for container
381
+ if (!container) {
382
+ console.error("❌ MathRichInput: Container not found after creation");
383
+ return;
384
+ }
385
+
386
+ // Create React root if it doesn't exist
387
+ if (!this.root) {
388
+ try {
389
+ this.root = createRoot(container);
390
+ } catch (error) {
391
+ console.error(
392
+ "❌ MathRichInput: Failed to create React root:",
393
+ error
100
394
  );
101
- },
102
- })
103
- );
395
+ return;
396
+ }
397
+ }
398
+
399
+ // Safety check for root
400
+ if (!this.root) {
401
+ console.error("❌ MathRichInput: React root not available");
402
+ return;
403
+ }
404
+
405
+ // Render with additional error boundary
406
+ this.root.render(
407
+ React.createElement(MathRichInput, {
408
+ value: value,
409
+ placeholder: placeholder,
410
+ readOnly: readonly,
411
+ disabled: disabled,
412
+ mimeType: mimeType,
413
+ outputTex: outputTex,
414
+ disableAnimation: disableAnimation,
415
+ className: this.className || "",
416
+ onChange: (data, options) => {
417
+ try {
418
+ const { value: newValue, mimeType: newMimeType } = data;
419
+
420
+ // Only update attributes if they actually changed to avoid infinite re-render
421
+ const currentValue = this.getAttribute("value") || "";
422
+ const currentMimeType = this.getAttribute("mimetype") || "";
423
+
424
+ if (newValue !== currentValue) {
425
+ this.setAttribute("value", newValue);
426
+ }
427
+ if (newMimeType !== currentMimeType) {
428
+ this.setAttribute("mimetype", newMimeType);
429
+ }
430
+
431
+ this.dispatchEvent(
432
+ new CustomEvent("change", {
433
+ detail: {
434
+ value: newValue,
435
+ mimeType: newMimeType,
436
+ },
437
+ })
438
+ );
439
+ } catch (error) {
440
+ console.error("❌ MathRichInput: onChange error:", error);
441
+ }
442
+ },
443
+ })
444
+ );
445
+
446
+ // Render completed successfully
447
+ } catch (error) {
448
+ console.error("❌ MathRichInput: Render failed:", error);
449
+
450
+ // If this is a React error #409, attempt graceful recovery
451
+ if (error.message && error.message.includes("409")) {
452
+ setTimeout(() => {
453
+ if (!this.isDuringDrag && this.isConnected) {
454
+ this.render();
455
+ }
456
+ }, 200);
457
+ }
458
+
459
+ throw error; // Re-throw for debugging
460
+ }
104
461
  }
105
462
  }
106
463