@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/dist/standalone.js +1 -1
- package/package.json +1 -1
- package/src/standalone.js +407 -56
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
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.
|
|
90
|
+
this.safeRender();
|
|
44
91
|
}
|
|
45
92
|
}
|
|
46
93
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|