@zzish/math-rich-input 0.1.49 → 0.1.50

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