@teachinglab/omd 0.2.6 → 0.2.7

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.
@@ -27,8 +27,8 @@ const defaultConfig = {
27
27
  stepVisualizer: {
28
28
  dotSizes: {
29
29
  level0: 8,
30
- level1: 6,
31
- level2: 4
30
+ level1: 8,
31
+ level2: 8
32
32
  },
33
33
  fontWeights: {
34
34
  level0: 400,
@@ -34,7 +34,7 @@ export class omdEquationStack extends jsvgGroup {
34
34
 
35
35
  // The sequence is the core. If a visualizer is needed, that's our sequence.
36
36
  if (options.stepVisualizer) {
37
- this.sequence = new omdStepVisualizer(steps);
37
+ this.sequence = new omdStepVisualizer(steps, this.stylingOptions || {});
38
38
  } else {
39
39
  this.sequence = new omdEquationSequenceNode(steps);
40
40
  }
@@ -41,6 +41,19 @@ export class omdDisplay {
41
41
  this.svg.svgObject.style.overflow = 'hidden';
42
42
  this.container.appendChild(this.svg.svgObject);
43
43
 
44
+ // Create a dedicated content group we can translate to compensate for
45
+ // viewBox origin changes (so expanding the origin doesn't visually move content).
46
+ try {
47
+ const ns = 'http://www.w3.org/2000/svg';
48
+ this._contentGroup = document.createElementNS(ns, 'g');
49
+ this._contentGroup.setAttribute('id', 'omd-content-root');
50
+ this.svg.svgObject.appendChild(this._contentGroup);
51
+ this._contentOffsetX = 0;
52
+ this._contentOffsetY = 0;
53
+ } catch (e) {
54
+ this._contentGroup = null;
55
+ }
56
+
44
57
  // Handle resize
45
58
  if (window.ResizeObserver) {
46
59
  this.resizeObserver = new ResizeObserver(() => {
@@ -61,6 +74,324 @@ export class omdDisplay {
61
74
 
62
75
  // Reposition overlay toolbar (if any) on resize
63
76
  this._repositionOverlayToolbar();
77
+ if (this.options.debugExtents) this._drawDebugOverlays();
78
+ }
79
+
80
+ /**
81
+ * Ensure the internal SVG viewBox is at least as large as the provided content dimensions.
82
+ * This prevents clipping when content is larger than the current viewBox.
83
+ * @param {number} contentWidth
84
+ * @param {number} contentHeight
85
+ */
86
+ _ensureViewboxFits(contentWidth, contentHeight) {
87
+ // If caller provided just width/height, but we prefer extents, bail early
88
+ if (!this.node) return;
89
+ const pad = 10;
90
+
91
+ // Prefer DOM measured extents (accounts for strokes, transforms, children SVG geometry)
92
+ let ext = null;
93
+ try {
94
+ const collected = this._collectNodeExtents(this.node);
95
+ if (collected) {
96
+ ext = { minX: collected.minX, minY: collected.minY, maxX: collected.maxX, maxY: collected.maxY };
97
+ }
98
+ } catch (e) {
99
+ ext = null;
100
+ }
101
+ if (!ext) {
102
+ ext = this._computeNodeExtents(this.node);
103
+ }
104
+ if (!ext) return;
105
+
106
+ const minX = Math.floor(ext.minX - pad);
107
+ const minY = Math.floor(ext.minY - pad);
108
+ const maxX = Math.ceil(ext.maxX + pad);
109
+ const maxY = Math.ceil(ext.maxY + pad);
110
+
111
+ const curView = this.svg.svgObject.getAttribute('viewBox') || '';
112
+ let curX = 0, curY = 0, curW = 0, curH = 0;
113
+ if (curView) {
114
+ const parts = curView.split(/\s+/).map(Number).filter(n => !isNaN(n));
115
+ if (parts.length === 4) {
116
+ curX = parts[0];
117
+ curY = parts[1];
118
+ curW = parts[2];
119
+ curH = parts[3];
120
+ }
121
+ }
122
+
123
+ // To avoid shifting visible content, keep the current viewBox origin (curX,curY)
124
+ // and only expand width/height as needed. Changing the origin would change
125
+ // the mapping from SVG coordinates to screen coordinates and appear to move
126
+ // existing content.
127
+ const desiredX = curX;
128
+ const desiredY = curY;
129
+ const desiredRight = Math.max(curX + curW, maxX);
130
+ const desiredBottom = Math.max(curY + curH, maxY);
131
+ const desiredW = Math.max(curW, desiredRight - desiredX);
132
+ const desiredH = Math.max(curH, desiredBottom - desiredY);
133
+
134
+ if (desiredW !== curW || desiredH !== curH) {
135
+ this.svg.svgObject.setAttribute('viewBox', `${desiredX} ${desiredY} ${desiredW} ${desiredH}`);
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Walk the node tree and compute absolute extents in SVG coordinates.
141
+ * Uses `xpos`/`ypos` and `width`/`height` properties; falls back to 0 when missing.
142
+ * @param {omdNode} root
143
+ * @returns {{minX:number,minY:number,maxX:number,maxY:number}}
144
+ */
145
+ _computeNodeExtents(root) {
146
+ if (!root) return null;
147
+ const visited = new Set();
148
+ const stack = [{ node: root, absX: root.xpos || 0, absY: root.ypos || 0 }];
149
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
150
+
151
+ while (stack.length) {
152
+ const { node, absX, absY } = stack.pop();
153
+ if (!node || visited.has(node)) continue;
154
+ visited.add(node);
155
+
156
+ const w = node.width || 0;
157
+ const h = node.height || 0;
158
+ const nx = absX;
159
+ const ny = absY;
160
+ minX = Math.min(minX, nx);
161
+ minY = Math.min(minY, ny);
162
+ maxX = Math.max(maxX, nx + w);
163
+ maxY = Math.max(maxY, ny + h);
164
+
165
+ // push children
166
+ if (Array.isArray(node.childList)) {
167
+ for (const c of node.childList) {
168
+ if (!c) continue;
169
+ const cx = (c.xpos || 0) + nx;
170
+ const cy = (c.ypos || 0) + ny;
171
+ stack.push({ node: c, absX: cx, absY: cy });
172
+ }
173
+ }
174
+ if (node.argumentNodeList) {
175
+ for (const val of Object.values(node.argumentNodeList)) {
176
+ if (Array.isArray(val)) {
177
+ for (const v of val) {
178
+ if (!v) continue;
179
+ const vx = (v.xpos || 0) + nx;
180
+ const vy = (v.ypos || 0) + ny;
181
+ stack.push({ node: v, absX: vx, absY: vy });
182
+ }
183
+ } else if (val) {
184
+ const vx = (val.xpos || 0) + nx;
185
+ const vy = (val.ypos || 0) + ny;
186
+ stack.push({ node: val, absX: vx, absY: vy });
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ if (minX === Infinity) return null;
193
+ return { minX, minY, maxX, maxY };
194
+ }
195
+
196
+ /**
197
+ * Collect extents for each node and return per-node list plus overall extents.
198
+ * Useful for debugging elements that extend outside parent coordinates.
199
+ * @param {omdNode} root
200
+ * @returns {{nodes:Array, minX:number, minY:number, maxX:number, maxY:number}}
201
+ */
202
+ _collectNodeExtents(root) {
203
+ if (!root) return null;
204
+ const visited = new Set();
205
+ const stack = [{ node: root, absX: root.xpos || 0, absY: root.ypos || 0, parent: null }];
206
+ const nodes = [];
207
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
208
+
209
+
210
+ while (stack.length) {
211
+ const { node, absX, absY, parent } = stack.pop();
212
+ if (!node || visited.has(node)) continue;
213
+ visited.add(node);
214
+
215
+ // Prefer DOM measurement if available for accuracy
216
+ let nx = absX;
217
+ let ny = absY;
218
+ let nminX = nx;
219
+ let nminY = ny;
220
+ let nmaxX = nx;
221
+ let nmaxY = ny;
222
+
223
+ try {
224
+ if (node.svgObject && typeof node.svgObject.getBBox === 'function' && typeof node.svgObject.getCTM === 'function') {
225
+ const bbox = node.svgObject.getBBox();
226
+ const ctm = node.svgObject.getCTM();
227
+ // Transform all four bbox corners into root SVG coordinates
228
+ const corners = [
229
+ { x: bbox.x, y: bbox.y },
230
+ { x: bbox.x + bbox.width, y: bbox.y },
231
+ { x: bbox.x, y: bbox.y + bbox.height },
232
+ { x: bbox.x + bbox.width, y: bbox.y + bbox.height }
233
+ ];
234
+ const tx = corners.map(p => ({
235
+ x: ctm.a * p.x + ctm.c * p.y + ctm.e,
236
+ y: ctm.b * p.x + ctm.d * p.y + ctm.f
237
+ }));
238
+ nminX = Math.min(...tx.map(t => t.x));
239
+ nminY = Math.min(...tx.map(t => t.y));
240
+ nmaxX = Math.max(...tx.map(t => t.x));
241
+ nmaxY = Math.max(...tx.map(t => t.y));
242
+ nx = nminX;
243
+ ny = nminY;
244
+ } else {
245
+ const w = node.width || 0;
246
+ const h = node.height || 0;
247
+ nx = absX;
248
+ ny = absY;
249
+ nminX = nx;
250
+ nminY = ny;
251
+ nmaxX = nx + w;
252
+ nmaxY = ny + h;
253
+ }
254
+ } catch (e) {
255
+ const w = node.width || 0;
256
+ const h = node.height || 0;
257
+ nx = absX;
258
+ ny = absY;
259
+ nminX = nx;
260
+ nminY = ny;
261
+ nmaxX = nx + w;
262
+ nmaxY = ny + h;
263
+ }
264
+
265
+ nodes.push({ node, minX: nminX, minY: nminY, maxX: nmaxX, maxY: nmaxY, parent });
266
+
267
+ minX = Math.min(minX, nminX);
268
+ minY = Math.min(minY, nminY);
269
+ maxX = Math.max(maxX, nmaxX);
270
+ maxY = Math.max(maxY, nmaxY);
271
+
272
+ // push children
273
+ if (Array.isArray(node.childList)) {
274
+ for (const c of node.childList) {
275
+ if (!c) continue;
276
+ const cx = (c.xpos || 0) + nx;
277
+ const cy = (c.ypos || 0) + ny;
278
+ stack.push({ node: c, absX: cx, absY: cy, parent: node });
279
+ }
280
+ }
281
+ if (node.argumentNodeList) {
282
+ for (const val of Object.values(node.argumentNodeList)) {
283
+ if (Array.isArray(val)) {
284
+ for (const v of val) {
285
+ if (!v) continue;
286
+ const vx = (v.xpos || 0) + nx;
287
+ const vy = (v.ypos || 0) + ny;
288
+ stack.push({ node: v, absX: vx, absY: vy, parent: node });
289
+ }
290
+ } else if (val) {
291
+ const vx = (val.xpos || 0) + nx;
292
+ const vy = (val.ypos || 0) + ny;
293
+ stack.push({ node: val, absX: vx, absY: vy, parent: node });
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ if (minX === Infinity) return null;
300
+ return { nodes, minX, minY, maxX, maxY };
301
+ }
302
+
303
+ _clearDebugOverlays() {
304
+ if (!this.svg || !this.svg.svgObject) return;
305
+ const existing = this.svg.svgObject.querySelector('#omd-debug-overlays');
306
+ if (existing) existing.remove();
307
+ }
308
+
309
+ _drawDebugOverlays() {
310
+ if (!this.options.debugExtents) return;
311
+ if (!this.svg || !this.svg.svgObject || !this.node) return;
312
+
313
+ this._clearDebugOverlays();
314
+
315
+ const ns = 'http://www.w3.org/2000/svg';
316
+ const group = document.createElementNS(ns, 'g');
317
+ group.setAttribute('id', 'omd-debug-overlays');
318
+ group.setAttribute('pointer-events', 'none');
319
+
320
+ // overall node extents
321
+ const collected = this._collectNodeExtents(this.node);
322
+ if (!collected) return;
323
+
324
+ const { nodes, minX, minY, maxX, maxY } = collected;
325
+
326
+ // Draw content extents (blue dashed)
327
+ const contentRect = document.createElementNS(ns, 'rect');
328
+ contentRect.setAttribute('x', String(minX));
329
+ contentRect.setAttribute('y', String(minY));
330
+ contentRect.setAttribute('width', String(maxX - minX));
331
+ contentRect.setAttribute('height', String(maxY - minY));
332
+ contentRect.setAttribute('fill', 'none');
333
+ contentRect.setAttribute('stroke', 'blue');
334
+ contentRect.setAttribute('stroke-dasharray', '6 4');
335
+ contentRect.setAttribute('stroke-width', '0.8');
336
+ group.appendChild(contentRect);
337
+
338
+ // Draw viewBox rect (orange)
339
+ const curView = this.svg.svgObject.getAttribute('viewBox') || '';
340
+ if (curView) {
341
+ const parts = curView.split(/\s+/).map(Number).filter(n => !isNaN(n));
342
+ if (parts.length === 4) {
343
+ const [vx, vy, vw, vh] = parts;
344
+ const vbRect = document.createElementNS(ns, 'rect');
345
+ vbRect.setAttribute('x', String(vx));
346
+ vbRect.setAttribute('y', String(vy));
347
+ vbRect.setAttribute('width', String(vw));
348
+ vbRect.setAttribute('height', String(vh));
349
+ vbRect.setAttribute('fill', 'none');
350
+ vbRect.setAttribute('stroke', 'orange');
351
+ vbRect.setAttribute('stroke-width', '1');
352
+ vbRect.setAttribute('opacity', '0.9');
353
+ group.appendChild(vbRect);
354
+ }
355
+ }
356
+
357
+ // Per-node boxes: green if inside parent, red if overflowing parent bounds
358
+ const overflowing = [];
359
+ for (const item of nodes) {
360
+ const r = document.createElementNS(ns, 'rect');
361
+ r.setAttribute('x', String(item.minX));
362
+ r.setAttribute('y', String(item.minY));
363
+ r.setAttribute('width', String(Math.max(0, item.maxX - item.minX)));
364
+ r.setAttribute('height', String(Math.max(0, item.maxY - item.minY)));
365
+ r.setAttribute('fill', 'none');
366
+ r.setAttribute('stroke-width', '0.6');
367
+
368
+ let stroke = 'green';
369
+ if (item.parent) {
370
+ const pMinX = (item.parent.xpos || 0) + (item.parent._absX || 0);
371
+ const pMinY = (item.parent.ypos || 0) + (item.parent._absY || 0);
372
+ // fallback compute parent's absX/Y from nodes list if available
373
+ const parentEntry = nodes.find(n => n.node === item.parent);
374
+ const pminX = parentEntry ? parentEntry.minX : pMinX;
375
+ const pminY = parentEntry ? parentEntry.minY : pMinY;
376
+ const pmaxX = parentEntry ? parentEntry.maxX : pminX + (item.parent.width || 0);
377
+ const pmaxY = parentEntry ? parentEntry.maxY : pminY + (item.parent.height || 0);
378
+
379
+ if (item.minX < pminX || item.minY < pminY || item.maxX > pmaxX || item.maxY > pmaxY) {
380
+ stroke = 'red';
381
+ overflowing.push({ node: item.node, bounds: item });
382
+ }
383
+ }
384
+
385
+ r.setAttribute('stroke', stroke);
386
+ r.setAttribute('opacity', stroke === 'red' ? '0.9' : '0.6');
387
+ group.appendChild(r);
388
+ }
389
+
390
+ if (overflowing.length) {
391
+ console.warn('omdDisplay: debugExtents found overflowing nodes:', overflowing.map(o => ({ type: o.node?.type, bounds: o.bounds })));
392
+ }
393
+
394
+ this.svg.svgObject.appendChild(group);
64
395
  }
65
396
 
66
397
  centerNode() {
@@ -124,20 +455,49 @@ export class omdDisplay {
124
455
  x = (containerWidth - scaledWidth) / 2;
125
456
  }
126
457
 
127
- // Y is top margin; scaled content will grow downward
128
- this.node.setPosition(x, this.options.topMargin);
458
+ // Decide whether positioning would move content outside container. If so,
459
+ // prefer expanding the SVG viewBox instead of moving nodes.
460
+ const scaledWidthFinal = (this.node.width || contentWidth) * scale;
461
+ const scaledHeightFinal = (this.node.height || contentHeight) * scale;
462
+ const totalNeededH = scaledHeightFinal + (this.options.topMargin || 0) + (this.options.bottomMargin || 0);
463
+
464
+ const willOverflowHoriz = scaledWidthFinal > containerWidth;
465
+ const willOverflowVert = totalNeededH > containerHeight;
466
+
467
+ if (willOverflowHoriz || willOverflowVert) {
468
+ // Set scale but do NOT reposition node (preserve its absolute positions).
469
+ if (this.node.setScale) this.node.setScale(scale);
470
+ // Expand viewBox to contain entire unscaled content so nothing is clipped.
471
+ this._ensureViewboxFits(contentWidth, contentHeight);
472
+ // Reposition overlay toolbar in case viewBox/container changed
473
+ this._repositionOverlayToolbar();
474
+
475
+ // If content still exceeds available height in the host, allow vertical scrolling
476
+ if (willOverflowVert) {
477
+ this.container.style.overflowY = 'auto';
478
+ this.container.style.overflowX = 'hidden';
479
+ } else {
480
+ this.container.style.overflow = 'hidden';
481
+ }
482
+ if (this.options.debugExtents) this._drawDebugOverlays();
483
+ } else {
484
+ // Y is top margin; scaled content will grow downward
485
+ this.node.setPosition(x, this.options.topMargin);
486
+
487
+ // Reposition overlay toolbar (if any)
488
+ this._repositionOverlayToolbar();
129
489
 
130
- // Reposition overlay toolbar (if any)
131
- this._repositionOverlayToolbar();
490
+ // Ensure viewBox can contain the (unscaled) content to avoid clipping in some hosts
491
+ this._ensureViewboxFits(contentWidth, contentHeight);
132
492
 
133
- // If content still exceeds available height (even after scaling), enable container scroll
134
- const totalNeededH = (contentHeight * scale) + (this.options.topMargin || 0) + (this.options.bottomMargin || 0);
135
- if (totalNeededH > containerHeight) {
136
- // Let the host scroll vertically; keep horizontal overflow hidden to avoid layout shift
137
- this.container.style.overflowY = 'auto';
138
- this.container.style.overflowX = 'hidden';
139
- } else {
140
- this.container.style.overflow = 'hidden';
493
+ if (totalNeededH > containerHeight) {
494
+ // Let the host scroll vertically; keep horizontal overflow hidden to avoid layout shift
495
+ this.container.style.overflowY = 'auto';
496
+ this.container.style.overflowX = 'hidden';
497
+ } else {
498
+ this.container.style.overflow = 'hidden';
499
+ }
500
+ if (this.options.debugExtents) this._drawDebugOverlays();
141
501
  }
142
502
  }
143
503
 
@@ -204,6 +564,8 @@ export class omdDisplay {
204
564
  // Update container
205
565
  this.container.style.width = `${newWidth}px`;
206
566
  this.container.style.height = `${newHeight}px`;
567
+ if (this.options.debugExtents) this._drawDebugOverlays();
568
+ else this._clearDebugOverlays();
207
569
  }
208
570
 
209
571
  /**
@@ -214,7 +576,11 @@ export class omdDisplay {
214
576
  render(expression) {
215
577
  // Clear previous node
216
578
  if (this.node) {
217
- this.svg.removeChild(this.node);
579
+ if (this._contentGroup && this.node && this.node.svgObject) {
580
+ this._contentGroup.removeChild(this.node.svgObject);
581
+ } else {
582
+ this.svg.removeChild(this.node);
583
+ }
218
584
  }
219
585
 
220
586
  // Create node from expression
@@ -223,18 +589,18 @@ export class omdDisplay {
223
589
  // Multiple equations
224
590
  const equationStrings = expression.split(';').filter(s => s.trim() !== '');
225
591
  const steps = equationStrings.map(str => omdEquationNode.fromString(str));
226
- this.node = new omdStepVisualizer(steps);
592
+ this.node = new omdStepVisualizer(steps, this.options.styling || {});
227
593
  } else {
228
594
  // Single expression or equation
229
595
  if (expression.includes('=')) {
230
596
  const firstStep = omdEquationNode.fromString(expression);
231
- this.node = new omdStepVisualizer([firstStep]);
597
+ this.node = new omdStepVisualizer([firstStep], this.options.styling || {});
232
598
  } else {
233
599
  // Create node directly from expression
234
600
  const parsedAST = math.parse(expression);
235
601
  const NodeClass = getNodeForAST(parsedAST);
236
602
  const firstStep = new NodeClass(parsedAST);
237
- this.node = new omdStepVisualizer([firstStep]);
603
+ this.node = new omdStepVisualizer([firstStep], this.options.styling || {});
238
604
  }
239
605
  }
240
606
  } else {
@@ -249,7 +615,12 @@ export class omdDisplay {
249
615
  // Apply filtering based on filterLevel
250
616
  sequence.updateStepsVisibility(step => (step.stepMark ?? 0) === sequence.getFilterLevel());
251
617
  }
252
- this.svg.addChild(this.node);
618
+ // Prefer appending the node's svgObject into our content group so DOM measurements are consistent
619
+ if (this._contentGroup && this.node && this.node.svgObject) {
620
+ try { this._contentGroup.appendChild(this.node.svgObject); } catch (e) { this.svg.addChild(this.node); }
621
+ } else {
622
+ this.svg.addChild(this.node);
623
+ }
253
624
 
254
625
  // Apply any stored font settings
255
626
  if (this.options.fontFamily) {
@@ -268,6 +639,14 @@ export class omdDisplay {
268
639
  // Ensure overlay toolbar is positioned initially
269
640
  this._repositionOverlayToolbar();
270
641
 
642
+ // Also ensure the viewBox is large enough to contain the node (avoid clipping)
643
+ const cw = (this.node && this.node.width) ? this.node.width : 0;
644
+ const ch = (this.node && this.node.height) ? this.node.height : 0;
645
+ this._ensureViewboxFits(cw, ch);
646
+
647
+ if (this.options.debugExtents) this._drawDebugOverlays();
648
+ else this._clearDebugOverlays();
649
+
271
650
  // Provide a default global refresh function if not present
272
651
  if (typeof window !== 'undefined' && !window.refreshDisplayAndFilters) {
273
652
  window.refreshDisplayAndFilters = () => {
@@ -282,7 +661,7 @@ export class omdDisplay {
282
661
  sequence.updateStepsVisibility(step => (step.stepMark ?? 0) === 0);
283
662
  }
284
663
  if (typeof node.updateLayout === 'function') {
285
- node.updateLayout();
664
+ node.updateLayout();
286
665
  }
287
666
  }
288
667
  if (this.options.centerContent) {
@@ -303,7 +682,11 @@ export class omdDisplay {
303
682
  * @param {object} child - A jsvg node to add
304
683
  */
305
684
  addChild(child) {
306
- this.svg.addChild(child);
685
+ if (this._contentGroup && child && child.svgObject) {
686
+ try { this._contentGroup.appendChild(child.svgObject); } catch (e) { this.svg.addChild(child); }
687
+ } else {
688
+ this.svg.addChild(child);
689
+ }
307
690
 
308
691
  if (this.options.centerContent) this.centerNode();
309
692
 
@@ -317,10 +700,16 @@ export class omdDisplay {
317
700
  removeChild(child) {
318
701
  if (!this.svg) return;
319
702
  try {
320
- if (typeof this.svg.removeChild === 'function') {
703
+ if (child && child.svgObject) {
704
+ if (this._contentGroup && this._contentGroup.contains(child.svgObject)) {
705
+ this._contentGroup.removeChild(child.svgObject);
706
+ } else if (this.svg.svgObject && this.svg.svgObject.contains(child.svgObject)) {
707
+ this.svg.svgObject.removeChild(child.svgObject);
708
+ } else if (typeof this.svg.removeChild === 'function') {
709
+ this.svg.removeChild(child);
710
+ }
711
+ } else if (typeof this.svg.removeChild === 'function') {
321
712
  this.svg.removeChild(child);
322
- } else if (child && child.svgObject && this.svg.svgObject && this.svg.svgObject.contains(child.svgObject)) {
323
- this.svg.svgObject.removeChild(child.svgObject);
324
713
  }
325
714
  } catch (e) {
326
715
  // no-op
@@ -335,13 +724,21 @@ export class omdDisplay {
335
724
  */
336
725
  update(newNode) {
337
726
  if (this.node) {
338
- this.svg.removeChild(this.node);
727
+ if (this._contentGroup && this.node && this.node.svgObject && this._contentGroup.contains(this.node.svgObject)) {
728
+ this._contentGroup.removeChild(this.node.svgObject);
729
+ } else if (typeof this.svg.removeChild === 'function') {
730
+ this.svg.removeChild(this.node);
731
+ }
339
732
  }
340
733
 
341
734
  this.node = newNode;
342
735
  this.node.setFontSize(this.options.fontSize);
343
736
  this.node.initialize();
344
- this.svg.addChild(this.node);
737
+ if (this._contentGroup && this.node && this.node.svgObject) {
738
+ try { this._contentGroup.appendChild(this.node.svgObject); } catch (e) { this.svg.addChild(this.node); }
739
+ } else {
740
+ this.svg.addChild(this.node);
741
+ }
345
742
 
346
743
  if (this.options.centerContent) {
347
744
  this.centerNode();
@@ -350,36 +747,7 @@ export class omdDisplay {
350
747
  this._repositionOverlayToolbar();
351
748
  }
352
749
 
353
- /**
354
- * Gets the current node
355
- * @returns {omdNode|null} The current node
356
- */
357
- getCurrentNode() {
358
- return this.node;
359
- }
360
-
361
- /**
362
- * Repositions overlay toolbar if current node supports it
363
- * @private
364
- */
365
- _repositionOverlayToolbar() {
366
- const rect = this.container.getBoundingClientRect();
367
- const paddingTop = parseFloat(getComputedStyle(this.container).paddingTop || '0');
368
- const paddingBottom = parseFloat(getComputedStyle(this.container).paddingBottom || '0');
369
- const paddingLeft = parseFloat(getComputedStyle(this.container).paddingLeft || '0');
370
- const paddingRight = parseFloat(getComputedStyle(this.container).paddingRight || '0');
371
- const containerWidth = (rect.width - paddingLeft - paddingRight) || this.container.clientWidth || 0;
372
- const containerHeight = (rect.height - paddingTop - paddingBottom) || this.container.clientHeight || 0;
373
- const node = this.node;
374
- if (!node) return;
375
- const hasOverlayApi = typeof node.isToolbarOverlay === 'function' && typeof node.positionToolbarOverlay === 'function';
376
- if (hasOverlayApi && node.isToolbarOverlay()) {
377
- node.positionToolbarOverlay(containerWidth, containerHeight, 16);
378
- }
379
- }
380
-
381
750
 
382
-
383
751
  /**
384
752
  * Sets the font size
385
753
  * @param {number} size - The font size