Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: simplify SVG path filtering and parent selection #899

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
20 changes: 16 additions & 4 deletions apps/studio/electron/preload/webview/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,17 @@ export function buildLayerTree(root: HTMLElement): Map<string, LayerNode> | null

const layerMap = new Map<string, LayerNode>();
const treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
acceptNode: (node: Node) =>
isValidHtmlElement(node as HTMLElement)
Copy link
Contributor

@Kitenite Kitenite Dec 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could just update this to check for the parent being svg and node being path

export function isValidHtmlElement(element: Element): boolean {
return (
element &&
element instanceof Node &&
element.nodeType === Node.ELEMENT_NODE &&
!DOM_IGNORE_TAGS.includes(element.tagName) &&
!element.hasAttribute(EditorAttributes.DATA_ONLOOK_IGNORE) &&
(element as HTMLElement).style.display !== 'none'
);
}

? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_SKIP,
acceptNode: (node: Node) => {
const element = node as HTMLElement;
if (!isValidHtmlElement(element)) {
return NodeFilter.FILTER_SKIP;
}
// Skip all SVG child elements
if (element.closest('svg') && element.tagName.toLowerCase() !== 'svg') {
return NodeFilter.FILTER_SKIP;
}
return NodeFilter.FILTER_ACCEPT;
},
});

// Process root node
Expand Down Expand Up @@ -95,6 +102,11 @@ function processNode(node: HTMLElement): LayerNode {
| string
| null;

// Add pointer-events: none to SVG paths and other SVG children
if (node.closest('svg') && node.tagName.toLowerCase() !== 'svg') {
node.style.pointerEvents = 'none';
}

const layerNode: LayerNode = {
domId,
oid: oid || null,
Expand Down
6 changes: 6 additions & 0 deletions apps/studio/electron/preload/webview/elements/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export const getDeepElement = (x: number, y: number): Element | undefined => {
if (!el) {
return;
}

// Check if element is a child of SVG and return parent SVG
if (el.parentElement?.tagName.toLowerCase() === 'svg') {
return el.parentElement;
}

const crawlShadows = (node: Element): Element => {
if (node?.shadowRoot) {
const potential = node.shadowRoot.elementFromPoint(x, y);
Expand Down
20 changes: 16 additions & 4 deletions apps/studio/electron/preload/webview/elements/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,35 @@ const getDeepElement = (x: number, y: number): Element | undefined => {
if (!el) {
return;
}

// Helper function to get the parent SVG of a path element
const getParentSvgOrSelf = (element: Element): Element => {
if (
element.tagName.toLowerCase() === 'path' ||
(element.closest('svg') && element.tagName.toLowerCase() !== 'svg')
) {
return element.closest('svg') || element;
}
return element;
};

const crawlShadows = (node: Element): Element => {
if (node?.shadowRoot) {
const potential = node.shadowRoot.elementFromPoint(x, y);
if (potential == node) {
return node;
return getParentSvgOrSelf(node);
} else if (potential?.shadowRoot) {
return crawlShadows(potential);
} else {
return potential || node;
return getParentSvgOrSelf(potential || node);
}
} else {
return node;
return getParentSvgOrSelf(node);
}
};

const nested_shadow = crawlShadows(el);
return nested_shadow || el;
return getParentSvgOrSelf(nested_shadow || el);
};

export const updateElementInstance = (domId: string, instanceId: string, component: string) => {
Expand Down
78 changes: 63 additions & 15 deletions apps/studio/src/lib/editor/engine/element/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,48 @@ export class ElementManager {
this.selectedElements = elements;
}

mouseover(domEl: DomElement, webview: Electron.WebviewTag) {
async mouseover(domEl: DomElement, webview: Electron.WebviewTag) {
if (!domEl) {
this.editorEngine.overlay.removeHoverRect();
this.clearHoveredElement();
return;
}
if (this.hoveredElement && this.hoveredElement.domId === domEl.domId) {

// Check if element is SVG child
const isSvgChild = await webview.executeJavaScript(`
(function() {
const el = document.querySelector('[data-onlook-dom-id="${domEl.domId}"]');
return el?.closest('svg') && el.tagName.toLowerCase() !== 'svg';
})()
`);

let elementToHover = domEl;
if (isSvgChild) {
const parentEl = await webview.executeJavaScript(`
(function() {
const el = document.querySelector('[data-onlook-dom-id="${domEl.domId}"]');
const parent = el.closest('svg');
return window.api?.getDomElementByDomId(parent.getAttribute('data-onlook-dom-id'));
})()
`);
if (parentEl) {
elementToHover = parentEl;
}
}

if (this.hoveredElement && this.hoveredElement.domId === elementToHover.domId) {
return;
}

const webviewEl: DomElement = {
...domEl,
...elementToHover,
webviewId: webview.id,
};
const adjustedRect = this.editorEngine.overlay.adaptRectFromSourceElement(
webviewEl.rect,
webview,
);
const isComponent = !!domEl.instanceId;
const isComponent = !!elementToHover.instanceId;
this.editorEngine.overlay.updateHoverRect(adjustedRect, isComponent);
this.setHoveredElement(webviewEl);
}
Expand All @@ -57,8 +80,8 @@ export class ElementManager {
const selectedEl = this.selected[0];
const hoverEl = this.hovered;

const webViewId = selectedEl.webviewId;
const webview = this.editorEngine.webviews.getWebview(webViewId);
const webviewId = selectedEl.webviewId;
const webview = this.editorEngine.webviews.getWebview(webviewId);
if (!webview) {
return;
}
Expand All @@ -75,7 +98,7 @@ export class ElementManager {
this.editorEngine.overlay.updateMeasurement(selectedRect, hoverRect);
}

shiftClick(domEl: DomElement, webview: Electron.WebviewTag) {
async shiftClick(domEl: DomElement, webview: Electron.WebviewTag) {
const selectedEls = this.selected;
const isAlreadySelected = selectedEls.some((el) => el.domId === domEl.domId);
let newSelectedEls: DomElement[] = [];
Expand All @@ -84,26 +107,51 @@ export class ElementManager {
} else {
newSelectedEls = [...selectedEls, domEl];
}
this.click(newSelectedEls, webview);
await this.click(newSelectedEls, webview);
}

click(domEls: DomElement[], webview: Electron.WebviewTag) {
async click(domEls: DomElement[], webview: Electron.WebviewTag) {
this.editorEngine.overlay.removeClickedRects();
this.clearSelectedElements();

for (const domEl of domEls) {
const isSvgChild = await webview.executeJavaScript(`
(function() {
const el = document.querySelector('[data-onlook-dom-id="${domEl.domId}"]');
return el?.closest('svg') && el.tagName.toLowerCase() !== 'svg';
})()
`);

let elementToSelect = domEl;
if (isSvgChild) {
const parentEl = await webview.executeJavaScript(`
(function() {
const el = document.querySelector('[data-onlook-dom-id="${domEl.domId}"]');
const parent = el.closest('svg');
return window.api?.getDomElementByDomId(parent.getAttribute('data-onlook-dom-id'));
})()
`);
if (parentEl) {
elementToSelect = parentEl;
}
}

const adjustedRect = this.editorEngine.overlay.adaptRectFromSourceElement(
domEl.rect,
elementToSelect.rect,
webview,
);
const isComponent = !!domEl.instanceId;
this.editorEngine.overlay.addClickRect(adjustedRect, domEl.styles, isComponent);
this.addSelectedElement(domEl);
const isComponent = !!elementToSelect.instanceId;
this.editorEngine.overlay.addClickRect(
adjustedRect,
elementToSelect.styles,
isComponent,
);
this.addSelectedElement(elementToSelect);
}
}

refreshSelectedElements(webview: Electron.WebviewTag) {
this.debouncedRefreshClickedElements(webview);
async refreshSelectedElements(webview: Electron.WebviewTag) {
await this.debouncedRefreshClickedElements(webview);
}

setHoveredElement(element: DomElement) {
Expand Down
12 changes: 12 additions & 0 deletions apps/studio/src/lib/editor/engine/overlay/rect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ export class HoverRect extends RectImpl {
}

render(rectDimensions: RectDimensions, isComponent?: boolean) {
// Don't render hover rect for SVG children
const { width, height, top, left } = rectDimensions;
const targetEl = document.elementFromPoint(left + width / 2, top + height / 2);
if (targetEl?.closest('svg') && targetEl.tagName.toLowerCase() !== 'svg') {
return;
}
super.render(rectDimensions, isComponent);
}
}
Expand Down Expand Up @@ -352,6 +358,12 @@ export class ClickRect extends RectImpl {
},
isComponent?: boolean,
) {
// Don't render click rect for SVG children
const targetEl = document.elementFromPoint(left + width / 2, top + height / 2);
if (targetEl?.closest('svg') && targetEl.tagName.toLowerCase() !== 'svg') {
return;
}

// Sometimes a selected element can be removed. We handle this gracefully.
try {
this.updateMargin(margin, { width, height });
Expand Down