Skip to content

Commit 2a182eb

Browse files
committed
fix: implement shadow DOM to prevent box styles being overridden by user stylesheets
1 parent 0221abc commit 2a182eb

File tree

1 file changed

+83
-59
lines changed

1 file changed

+83
-59
lines changed

src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

Lines changed: 83 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -310,87 +310,111 @@ function RemoteFunctions(config) {
310310
}
311311

312312
NodeMoreOptionsBox.prototype = {
313-
create: function() {
314-
// Remove existing more options box if any
315-
this.remove();
316-
317-
let elemBounds = this.element.getBoundingClientRect();
318-
319-
// for styling the svg's
320-
if (!document.getElementById("node-more-options-style")) {
321-
const style = document.createElement("style");
322-
style.id = "node-more-options-style";
323-
style.textContent = `
324-
.node-options span > svg {
325-
width: 16px;
326-
height: 16px;
327-
display: block;
328-
}
329-
`;
330-
document.head.appendChild(style);
331-
}
332-
333-
// create the container
313+
_style: function() {
334314
this.body = window.document.createElement("div");
335-
this.body.style.setProperty("z-index", 2147483647);
336-
this.body.style.setProperty("position", "fixed");
337315

338-
const boxWidth = 82;
316+
// this is shadow DOM.
317+
// we need it because if we add the box directly to the DOM then users style might override it.
318+
// {mode: "closed"} means that users will not be able to access the shadow DOM
319+
const shadow = this.body.attachShadow({ mode: "closed" });
339320

340-
this.body.style.setProperty("left", (elemBounds.right - boxWidth) + "px");
341-
this.body.style.setProperty(
342-
"top",
343-
// if there's not enough space to show the box above the element,
344-
// we show it below the element
345-
(elemBounds.top - 30 < 0 ? elemBounds.top + elemBounds.height + 5 : elemBounds.top - 30) + "px"
346-
);
347-
this.body.style.setProperty("font-size", "12px");
348-
this.body.style.setProperty("font-family", "Arial, sans-serif");
321+
// the element that was clicked
322+
let elemBounds = this.element.getBoundingClientRect();
349323

350-
// style the box with a blue background. this will appear on the right side of the clicked DOM element
351-
this.body.style.setProperty("background", "#4285F4");
352-
this.body.style.setProperty("color", "white");
353-
this.body.style.setProperty("border-radius", "3px");
354-
this.body.style.setProperty("padding", "5px 8px");
355-
this.body.style.setProperty("box-shadow", "0 2px 5px rgba(0,0,0,0.2)");
356-
this.body.style.setProperty("width", boxWidth + "px");
324+
// the box width and the positions where it should be placed
325+
const boxWidth = 82;
326+
const leftPos = (elemBounds.right - boxWidth);
327+
const topPos = (elemBounds.top - 30 < 0 ? elemBounds.top + elemBounds.height + 5 : elemBounds.top - 30);
357328

329+
// the icons that is displayed in the box
358330
const ICONS = {
359-
arrowUp: `<svg viewBox="0 0 24 24" fill="currentColor">
360-
<path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.59 5.58L20 12l-8-8-8 8z"/>
361-
</svg>`,
362-
363-
copy: `<svg viewBox="0 0 24 24" fill="currentColor">
364-
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
365-
</svg>`,
366-
367-
trash: `<svg viewBox="0 0 24 24" fill="currentColor">
368-
<path d="M6 7V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v2h3v2h-2l-1.5 12.5a2 2 0 0 1-2 1.5H8.5a2 2 0 0 1-2-1.5L5 9H3V7h3zm2 0h8V5H8v2z"/>
369-
</svg>`
331+
arrowUp: `
332+
<svg viewBox="0 0 24 24" fill="currentColor">
333+
<path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.59 5.58L20 12l-8-8-8 8z"/>
334+
</svg>
335+
`,
336+
337+
copy: `
338+
<svg viewBox="0 0 24 24" fill="currentColor">
339+
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0
340+
1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
341+
</svg>
342+
`,
343+
344+
trash: `
345+
<svg viewBox="0 0 24 24" fill="currentColor">
346+
<path d="M6 7V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v2h3v2h-2l-1.5 12.5a2 2 0 0
347+
1-2 1.5H8.5a2 2 0 0 1-2-1.5L5 9H3V7h3zm2 0h8V5H8v2z"/>
348+
</svg>
349+
`
370350
};
371351

372-
let content = `<div class="node-options" style="display: flex; gap: 8px; align-items: center;">
373-
<span style="cursor: pointer; display: flex; align-items: center;" data-action="select-parent" title="Select Parent">
352+
let content = `<div class="node-options">
353+
<span data-action="select-parent" title="Select Parent">
374354
${ICONS.arrowUp}
375355
</span>
376-
<span style="cursor: pointer; display: flex; align-items: center;" data-action="duplicate" title="Duplicate">
356+
<span data-action="duplicate" title="Duplicate">
377357
${ICONS.copy}
378358
</span>
379-
<span style="cursor: pointer; display: flex; align-items: center;" data-action="delete" title="Delete">
359+
<span data-action="delete" title="Delete">
380360
${ICONS.trash}
381361
</span>
382362
</div>`;
383363

384-
this.body.innerHTML = content;
364+
const styles = `
365+
.box {
366+
background-color: #4285F4;
367+
color: white;
368+
border-radius: 3px;
369+
padding: 5px 8px;
370+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
371+
font-size: 12px;
372+
font-family: Arial, sans-serif;
373+
z-index: 2147483647;
374+
position: fixed;
375+
left: ${leftPos}px;
376+
top: ${topPos}px;
377+
width: ${boxWidth}px;
378+
box-sizing: border-box;
379+
}
380+
381+
.node-options {
382+
display: flex;
383+
gap: 8px;
384+
align-items: center;
385+
}
386+
387+
.node-options span {
388+
cursor: pointer;
389+
display: flex;
390+
align-items: center;
391+
}
392+
393+
.node-options span > svg {
394+
width: 16px;
395+
height: 16px;
396+
display: block;
397+
}
398+
`;
399+
400+
// add everything to the shadow box
401+
shadow.innerHTML = `<style>${styles}</style><div class="box">${content}</div>`;
402+
this._shadow = shadow;
403+
},
404+
405+
create: function() {
406+
this.remove(); // remove existing box if already present
407+
this._style(); // style the box
408+
385409
window.document.body.appendChild(this.body);
386410

387-
// add the click handler to all the buttons
388-
const spans = this.body.querySelectorAll('.node-options span');
411+
// add click handler to all the buttons
412+
const spans = this._shadow.querySelectorAll('.node-options span');
389413
spans.forEach(span => {
390-
// to differentiate between each button click we can use the data-action attribute
391414
span.addEventListener('click', (event) => {
392415
event.stopPropagation();
393416
event.preventDefault();
417+
// data-action is to differentiate between the buttons (duplicate, delete or select-parent)
394418
const action = event.currentTarget.getAttribute('data-action');
395419
handleOptionClick(event, action, this.element);
396420
this.remove();

0 commit comments

Comments
 (0)