@zzish/math-rich-input 0.1.48 → 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.
@@ -0,0 +1,461 @@
1
+ import React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { MathRichInput } from "./index.js";
4
+
5
+ // Import CSS files
6
+ import "./MathRichInput.css";
7
+ import "./EquationEditorModal.css";
8
+ import "./AccentBar.css";
9
+ import "./Toolbar.css";
10
+ import "./EquationEditor.css";
11
+ import "./SymbolButton.css";
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
+
25
+ class MathRichInputElement extends HTMLElement {
26
+ constructor() {
27
+ super();
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();
37
+ }
38
+
39
+ connectedCallback() {
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();
48
+ }
49
+
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
65
+ if (this.root) {
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
+ }
73
+ }
74
+ }
75
+
76
+ static get observedAttributes() {
77
+ return [
78
+ "value",
79
+ "placeholder",
80
+ "readonly",
81
+ "disabled",
82
+ "mimetype",
83
+ "outputtex",
84
+ ];
85
+ }
86
+
87
+ attributeChangedCallback(name, oldValue, newValue) {
88
+ // Avoid infinite re-render loop by checking if value actually changed
89
+ if (oldValue !== newValue) {
90
+ this.safeRender();
91
+ }
92
+ }
93
+
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
+ }
136
+ }
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
+ );
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
+ }
338
+
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
389
+ );
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
+ }
455
+ }
456
+ }
457
+
458
+ // Define the web component
459
+ customElements.define("math-rich-input", MathRichInputElement);
460
+
461
+ export default MathRichInputElement;
@@ -0,0 +1,63 @@
1
+ const path = require("path");
2
+
3
+ module.exports = {
4
+ mode: "production",
5
+ entry: "./src/standalone.js",
6
+ output: {
7
+ path: path.resolve(__dirname, "dist"),
8
+ filename: "standalone.js",
9
+ library: {
10
+ type: "module",
11
+ },
12
+ environment: {
13
+ module: true,
14
+ },
15
+ },
16
+ experiments: {
17
+ outputModule: true,
18
+ },
19
+ module: {
20
+ rules: [
21
+ {
22
+ test: /\.jsx?$/,
23
+ exclude: /node_modules/,
24
+ use: {
25
+ loader: "babel-loader",
26
+ options: {
27
+ presets: [
28
+ ["@babel/preset-env", { targets: "defaults" }],
29
+ ["@babel/preset-react", { runtime: "classic" }],
30
+ ],
31
+ },
32
+ },
33
+ },
34
+ {
35
+ test: /\.css$/,
36
+ use: [
37
+ "style-loader",
38
+ "css-loader",
39
+ {
40
+ loader: "postcss-loader",
41
+ options: {
42
+ postcssOptions: {
43
+ plugins: [require("postcss-nested")],
44
+ },
45
+ },
46
+ },
47
+ ],
48
+ },
49
+ ],
50
+ },
51
+ resolve: {
52
+ extensions: [".js", ".jsx"],
53
+ alias: {
54
+ react: require.resolve("react"),
55
+ "react-dom/client": require.resolve("react-dom/client"),
56
+ "react-dom": require.resolve("react-dom"),
57
+ },
58
+ },
59
+ externals: {
60
+ // No externals - bundle everything
61
+ },
62
+ };
63
+