Skip to content

Commit d468709

Browse files
committed
feat: show hide the highlights instead of recreating it
1 parent 53fe560 commit d468709

1 file changed

Lines changed: 178 additions & 125 deletions

File tree

src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

Lines changed: 178 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,10 @@ function RemoteFunctions(config = {}) {
285285
position: absolute !important;
286286
}
287287
288+
.overlay-container.hidden {
289+
display: none !important;
290+
}
291+
288292
.outline {
289293
position: absolute !important;
290294
box-sizing: border-box !important;
@@ -304,11 +308,163 @@ function RemoteFunctions(config = {}) {
304308
return _highlightShadowRoot;
305309
}
306310

311+
// Overlay pool — overlays are created once and reused across highlights.
312+
// When released, they stay in the shadow DOM (hidden) ready for instant reuse.
313+
// This eliminates all DOM creation/destruction from the highlight hot paths.
314+
const _overlayPool = [];
315+
316+
function _createOverlayStructure() {
317+
const div = window.document.createElement("div");
318+
div.className = "overlay-container hidden";
319+
320+
function createRect() {
321+
const r = window.document.createElement("div");
322+
r.className = "rect";
323+
return r;
324+
}
325+
326+
const padTop = createRect(), padBottom = createRect(),
327+
padLeft = createRect(), padRight = createRect();
328+
const marTop = createRect(), marBottom = createRect(),
329+
marLeft = createRect(), marRight = createRect();
330+
const outline = window.document.createElement("div");
331+
outline.className = "outline";
332+
333+
div.appendChild(padTop);
334+
div.appendChild(padBottom);
335+
div.appendChild(padLeft);
336+
div.appendChild(padRight);
337+
div.appendChild(marTop);
338+
div.appendChild(marBottom);
339+
div.appendChild(marLeft);
340+
div.appendChild(marRight);
341+
div.appendChild(outline);
342+
343+
// Cache child references for O(1) access during updates
344+
div._refs = {
345+
padTop, padBottom, padLeft, padRight,
346+
marTop, marBottom, marLeft, marRight,
347+
outline
348+
};
349+
350+
_ensureHighlightShadowRoot().appendChild(div);
351+
return div;
352+
}
353+
354+
function _getOverlay() {
355+
return _overlayPool.length > 0 ? _overlayPool.pop() : _createOverlayStructure();
356+
}
357+
358+
function _releaseOverlay(overlay) {
359+
overlay.classList.add('hidden');
360+
overlay.trackingElement = null;
361+
_overlayPool.push(overlay);
362+
}
363+
364+
// Update an existing overlay's position, dimensions, and colors to match the target element.
365+
// No DOM elements are created or destroyed — only style properties are updated.
366+
function _updateOverlay(overlay, element) {
367+
const bounds = element.getBoundingClientRect();
368+
if (bounds.width === 0 && bounds.height === 0) {
369+
overlay.classList.add('hidden');
370+
return;
371+
}
372+
373+
const cs = window.getComputedStyle(element);
374+
375+
// Parse box model values (getComputedStyle always resolves to px)
376+
const bt = parseFloat(cs.borderTopWidth) || 0,
377+
br = parseFloat(cs.borderRightWidth) || 0,
378+
bb = parseFloat(cs.borderBottomWidth) || 0,
379+
bl = parseFloat(cs.borderLeftWidth) || 0;
380+
const pt = parseFloat(cs.paddingTop) || 0,
381+
pr = parseFloat(cs.paddingRight) || 0,
382+
pb = parseFloat(cs.paddingBottom) || 0,
383+
pl = parseFloat(cs.paddingLeft) || 0;
384+
const mt = parseFloat(cs.marginTop) || 0,
385+
mr = parseFloat(cs.marginRight) || 0,
386+
mb = parseFloat(cs.marginBottom) || 0,
387+
ml = parseFloat(cs.marginLeft) || 0;
388+
389+
// Compute the 4 absolute boxes exactly like dev tools:
390+
// getBoundingClientRect() always returns the border box regardless of box-sizing.
391+
const scroll = LivePreviewView.screenOffset(element);
392+
const borderBox = {
393+
left: scroll.left,
394+
top: scroll.top,
395+
width: bounds.width,
396+
height: bounds.height
397+
};
398+
const paddingBox = {
399+
left: borderBox.left + bl,
400+
top: borderBox.top + bt,
401+
width: borderBox.width - bl - br,
402+
height: borderBox.height - bt - bb
403+
};
404+
const contentBox = {
405+
left: paddingBox.left + pl,
406+
top: paddingBox.top + pt,
407+
width: paddingBox.width - pl - pr,
408+
height: paddingBox.height - pt - pb
409+
};
410+
const marginBox = {
411+
left: borderBox.left - ml,
412+
top: borderBox.top - mt,
413+
width: borderBox.width + ml + mr,
414+
height: borderBox.height + mt + mb
415+
};
416+
417+
// Update container position
418+
overlay.trackingElement = element;
419+
overlay.style.left = marginBox.left + "px";
420+
overlay.style.top = marginBox.top + "px";
421+
overlay.style.width = marginBox.width + "px";
422+
overlay.style.height = marginBox.height + "px";
423+
overlay.classList.remove('hidden');
424+
425+
const refs = overlay._refs;
426+
const mLeft = marginBox.left;
427+
428+
// Update a rect's position, size, and color in place
429+
function setRect(rect, left, top, width, height, color) {
430+
const s = rect.style;
431+
s.left = (left - mLeft) + "px";
432+
s.top = (top - marginBox.top) + "px";
433+
s.width = Math.max(0, width) + "px";
434+
s.height = Math.max(0, height) + "px";
435+
s.backgroundColor = color;
436+
}
437+
438+
// Padding region
439+
const padColor = COLORS.highlightPadding;
440+
setRect(refs.padTop, paddingBox.left, paddingBox.top, paddingBox.width, pt, padColor);
441+
setRect(refs.padBottom, paddingBox.left, contentBox.top + contentBox.height, paddingBox.width, pb, padColor);
442+
setRect(refs.padLeft, paddingBox.left, contentBox.top, pl, contentBox.height, padColor);
443+
setRect(refs.padRight, contentBox.left + contentBox.width, contentBox.top, pr, contentBox.height, padColor);
444+
445+
// Margin region
446+
const margColor = COLORS.highlightMargin;
447+
setRect(refs.marTop, marginBox.left, marginBox.top, marginBox.width, mt, margColor);
448+
setRect(refs.marBottom, marginBox.left, borderBox.top + borderBox.height, marginBox.width, mb, margColor);
449+
setRect(refs.marLeft, marginBox.left, borderBox.top, ml, borderBox.height, margColor);
450+
setRect(refs.marRight, borderBox.left + borderBox.width, borderBox.top, mr, borderBox.height, margColor);
451+
452+
// Outline
453+
const isEditable = element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR);
454+
const outlineColor = isEditable ? COLORS.outlineEditable : COLORS.outlineNonEditable;
455+
const outlineStyle = refs.outline.style;
456+
outlineStyle.left = (borderBox.left - mLeft) + "px";
457+
outlineStyle.top = (borderBox.top - marginBox.top) + "px";
458+
outlineStyle.width = borderBox.width + "px";
459+
outlineStyle.height = borderBox.height + "px";
460+
outlineStyle.border = `1px solid ${outlineColor}`;
461+
}
462+
307463
function Highlight(trigger) {
308464
this.trigger = !!trigger;
309465
this.elements = [];
310466
this.selector = "";
311-
this._divs = [];
467+
this._overlays = [];
312468
}
313469

314470
Highlight.prototype = {
@@ -321,16 +477,16 @@ function RemoteFunctions(config = {}) {
321477
}
322478

323479
this.elements.push(element);
324-
this._createOverlay(element);
480+
const overlay = _getOverlay();
481+
this._overlays.push(overlay);
482+
_updateOverlay(overlay, element);
325483
},
326484

327485
clear: function () {
328-
this._divs.forEach(function (div) {
329-
if (div.parentNode) {
330-
div.parentNode.removeChild(div);
331-
}
486+
this._overlays.forEach(function (overlay) {
487+
_releaseOverlay(overlay);
332488
});
333-
this._divs = [];
489+
this._overlays = [];
334490

335491
if (this.trigger) {
336492
this.elements.forEach(function (el) {
@@ -345,124 +501,21 @@ function RemoteFunctions(config = {}) {
345501
const elements = this.selector
346502
? Array.from(window.document.querySelectorAll(this.selector))
347503
: this.elements.slice();
348-
this.clear();
349-
elements.forEach(function (el) { this.add(el); }, this);
350-
},
351504

352-
_createOverlay: function (element) {
353-
const bounds = element.getBoundingClientRect();
354-
if (bounds.width === 0 && bounds.height === 0) { return; }
355-
356-
const cs = window.getComputedStyle(element);
357-
358-
// Parse box model values (getComputedStyle always resolves to px)
359-
const bt = parseFloat(cs.borderTopWidth) || 0,
360-
br = parseFloat(cs.borderRightWidth) || 0,
361-
bb = parseFloat(cs.borderBottomWidth) || 0,
362-
bl = parseFloat(cs.borderLeftWidth) || 0;
363-
const pt = parseFloat(cs.paddingTop) || 0,
364-
pr = parseFloat(cs.paddingRight) || 0,
365-
pb = parseFloat(cs.paddingBottom) || 0,
366-
pl = parseFloat(cs.paddingLeft) || 0;
367-
const mt = parseFloat(cs.marginTop) || 0,
368-
mr = parseFloat(cs.marginRight) || 0,
369-
mb = parseFloat(cs.marginBottom) || 0,
370-
ml = parseFloat(cs.marginLeft) || 0;
371-
372-
// Compute the 4 absolute boxes exactly like dev tools:
373-
// getBoundingClientRect() always returns the border box regardless of box-sizing.
374-
const scroll = LivePreviewView.screenOffset(element);
375-
const borderBox = {
376-
left: scroll.left,
377-
top: scroll.top,
378-
width: bounds.width,
379-
height: bounds.height
380-
};
381-
const paddingBox = {
382-
left: borderBox.left + bl,
383-
top: borderBox.top + bt,
384-
width: borderBox.width - bl - br,
385-
height: borderBox.height - bt - bb
386-
};
387-
const contentBox = {
388-
left: paddingBox.left + pl,
389-
top: paddingBox.top + pt,
390-
width: paddingBox.width - pl - pr,
391-
height: paddingBox.height - pt - pb
392-
};
393-
const marginBox = {
394-
left: borderBox.left - ml,
395-
top: borderBox.top - mt,
396-
width: borderBox.width + ml + mr,
397-
height: borderBox.height + mt + mb
398-
};
399-
400-
// Container div — sized to the margin box so all rects fit inside it
401-
const div = window.document.createElement("div");
402-
div.className = "overlay-container";
403-
div.trackingElement = element;
404-
div.style.left = marginBox.left + "px";
405-
div.style.top = marginBox.top + "px";
406-
div.style.width = marginBox.width + "px";
407-
div.style.height = marginBox.height + "px";
408-
409-
// Helper to create a colored rect at absolute page coordinates, offset by the container origin
410-
function makeRect(left, top, width, height, color) {
411-
if (width <= 0 || height <= 0) { return; }
412-
const r = window.document.createElement("div");
413-
r.className = "rect";
414-
r.style.left = (left - marginBox.left) + "px";
415-
r.style.top = (top - marginBox.top) + "px";
416-
r.style.width = width + "px";
417-
r.style.height = height + "px";
418-
r.style.backgroundColor = color;
419-
div.appendChild(r);
505+
// Adjust overlay count to match element count
506+
while (this._overlays.length > elements.length) {
507+
_releaseOverlay(this._overlays.pop());
420508
}
509+
while (this._overlays.length < elements.length) {
510+
this._overlays.push(_getOverlay());
511+
}
512+
513+
this.elements = elements;
421514

422-
// Padding region: 4 rects filling paddingBox minus contentBox
423-
const padColor = COLORS.highlightPadding;
424-
// top padding
425-
makeRect(paddingBox.left, paddingBox.top,
426-
paddingBox.width, pt, padColor);
427-
// bottom padding
428-
makeRect(paddingBox.left, contentBox.top + contentBox.height,
429-
paddingBox.width, pb, padColor);
430-
// left padding
431-
makeRect(paddingBox.left, contentBox.top,
432-
pl, contentBox.height, padColor);
433-
// right padding
434-
makeRect(contentBox.left + contentBox.width, contentBox.top,
435-
pr, contentBox.height, padColor);
436-
437-
// Margin region: 4 rects filling marginBox minus borderBox
438-
const margColor = COLORS.highlightMargin;
439-
// top margin
440-
makeRect(marginBox.left, marginBox.top,
441-
marginBox.width, mt, margColor);
442-
// bottom margin
443-
makeRect(marginBox.left, borderBox.top + borderBox.height,
444-
marginBox.width, mb, margColor);
445-
// left margin
446-
makeRect(marginBox.left, borderBox.top,
447-
ml, borderBox.height, margColor);
448-
// right margin
449-
makeRect(borderBox.left + borderBox.width, borderBox.top,
450-
mr, borderBox.height, margColor);
451-
452-
// Selection outline: 1px border at the border-box edge (drawn inside the border area)
453-
const isEditable = element.hasAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR);
454-
const outlineColor = isEditable ? COLORS.outlineEditable : COLORS.outlineNonEditable;
455-
const outlineDiv = window.document.createElement("div");
456-
outlineDiv.className = "outline";
457-
outlineDiv.style.left = (borderBox.left - marginBox.left) + "px";
458-
outlineDiv.style.top = (borderBox.top - marginBox.top) + "px";
459-
outlineDiv.style.width = borderBox.width + "px";
460-
outlineDiv.style.height = borderBox.height + "px";
461-
outlineDiv.style.border = `1px solid ${outlineColor}`;
462-
div.appendChild(outlineDiv);
463-
464-
_ensureHighlightShadowRoot().appendChild(div);
465-
this._divs.push(div);
515+
// Update all overlays in place — no DOM creation or destruction
516+
for (let i = 0; i < elements.length; i++) {
517+
_updateOverlay(this._overlays[i], elements[i]);
518+
}
466519
}
467520
};
468521

@@ -1485,12 +1538,12 @@ function RemoteFunctions(config = {}) {
14851538

14861539
function getHighlightCount() {
14871540
if (!_highlightShadowRoot) { return 0; }
1488-
return _highlightShadowRoot.querySelectorAll('.overlay-container').length;
1541+
return _highlightShadowRoot.querySelectorAll('.overlay-container:not(.hidden)').length;
14891542
}
14901543

14911544
function getHighlightTrackingElement(index) {
14921545
if (!_highlightShadowRoot) { return null; }
1493-
const overlay = _highlightShadowRoot.querySelectorAll('.overlay-container')[index];
1546+
const overlay = _highlightShadowRoot.querySelectorAll('.overlay-container:not(.hidden)')[index];
14941547
if (!overlay || !overlay.trackingElement) { return null; }
14951548
const el = overlay.trackingElement;
14961549
return {
@@ -1501,7 +1554,7 @@ function RemoteFunctions(config = {}) {
15011554

15021555
function getHighlightStyle(index, property) {
15031556
if (!_highlightShadowRoot) { return null; }
1504-
const overlay = _highlightShadowRoot.querySelectorAll('.overlay-container')[index];
1557+
const overlay = _highlightShadowRoot.querySelectorAll('.overlay-container:not(.hidden)')[index];
15051558
return overlay ? overlay.style[property] : null;
15061559
}
15071560

0 commit comments

Comments
 (0)