Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { h } from 'preact';

import { css, Global } from '@emotion/react';
import classnames from 'classnames';
import { useState } from 'preact/hooks';
import { useCallback, useEffect, useState } from 'preact/hooks';
import { ComponentProps, RootNodeProperties } from '../../../types';
import { ChromePicker } from 'react-color';
import { Icon } from '../../Atoms/Icon/Icon';
Expand All @@ -22,7 +22,7 @@ const CSS = {
css({
display: 'flex',
flexDirection: 'column',
minWidth: '420px',
width: '420px',
overflow: 'hidden',
fontSize: '14px',
position: 'fixed',
Expand Down Expand Up @@ -197,6 +197,11 @@ export const TemplatesEditor = observer((properties: TemplatesEditorProps): JSX.

const [isColorPickerVisible, setColorPickerVisible] = useState(false);

const [currentSelector, setCurrentSelector] = useState(''); // three possible selectors
const [contentSelector, setContentSelector] = useState('');
const [componentSelector, setComponentSelector] = useState('');
const [autocompleteSelector, setAutocompleteSelector] = useState('');

return (
<CacheProvider>
<div
Expand Down Expand Up @@ -243,8 +248,35 @@ export const TemplatesEditor = observer((properties: TemplatesEditorProps): JSX.
{!collapsed ? (
<Global
styles={css`
${selectedTarget.selector} {
border: 1px dashed black !important;
.ss-overlay {
position: fixed;
top: 0;
left: 0;
background-color: rgba(58, 35, 173, 0.5);
pointer-events: none;
z-index: 1000;
}
.ss-tooltip {
position: fixed;
background-color: white;
border: 2px solid #3a23ad;
padding: 10px 15px;
max-width: 300px;
min-width: 200px;
max-height: 500px;
overflow: auto;
pointer-events: none;
box-sizing: border-box;
z-index: 1000;
opacity: 1;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.2);
label {
display: block;
margin-bottom: 5px;
}
br {
margin: 5px 0;
}
}
`}
/>
Expand Down Expand Up @@ -429,11 +461,295 @@ export const TemplatesEditor = observer((properties: TemplatesEditorProps): JSX.
/>
)}
</div>

<h2>Find SS-Path Element: </h2>
<div className="section">
<span>Current autocompleteSelector: {autocompleteSelector}</span>
<DomSelector
elementSelector={'input[type="text"]'}
onSelectHandler={(elemSelector: any) => setAutocompleteSelector(elemSelector)}
type="autocomplete"
currentSelector={currentSelector}
setCurrentSelector={setCurrentSelector}
/>
<span>Current contentSelector: {contentSelector}</span>
<DomSelector
elementSelector={'div, section, article, aside'}
onSelectHandler={(elemSelector: any) => setContentSelector(elemSelector)}
type="content"
currentSelector={currentSelector}
setCurrentSelector={setCurrentSelector}
/>
<span>Current componentSelector: {componentSelector}</span>
<DomSelector
elementSelector={'[ss-path]'}
onSelectHandler={(elemSelector: any) => setComponentSelector(elemSelector)}
type="component"
currentSelector={currentSelector}
setCurrentSelector={setCurrentSelector}
/>
</div>
</div>
</CacheProvider>
);
});

const ElementSelectorHelpers = {
getComponent: function (el: EventTarget): HTMLElement | null {
return (el as HTMLElement).closest('[ss-path]');
},
getContent: function (el: EventTarget): HTMLElement | null {
return (el as HTMLElement).closest('div, section, article, aside');
},
getInput: function (el: EventTarget): HTMLInputElement | null {
return (el as HTMLElement).closest('input[type=text], input[type=searc]');
},
markElement: function (type: string | undefined, child: EventTarget | null): HTMLElement | null {
let element: HTMLElement | null = null;
if (!child) {
return null;
}
switch (type) {
case 'component':
element = this.getComponent(child);
if (element) {
element.classList.add('hoverEls');
}
break;
case 'content':
element = this.getContent(child);
if (element) {
element.classList.add('hoverEls');
}
break;
case 'input':
element = this.getInput(child);
if (element) {
element.classList.add('hoverEls');
}
break;
default:
break;
}
return element;
},
getShortestUniqueSelector: function (el: Element | null): string | null {
if (!(el instanceof Element)) return null;

const parts = [];
let current: Element | null = el;

while (current && current.nodeType === 1) {
let selector = current.tagName.toLowerCase();

// Use ID if present, but check for uniqueness later
if (current.id) {
selector = `#${window.CSS.escape(current.id)}`;
parts.unshift(selector);
} else {
// Add class-based selector
if (current.classList.length > 0) {
selector +=
'.' +
Array.from(current.classList)
.map((cls) => window.CSS.escape(cls))
.join('.');
}

// Use :nth-child if needed for uniqueness
const siblings = Array.from(current.parentNode?.children || []);
const sameTagSiblings = siblings.filter((sib) => sib.tagName === current?.tagName);
if (sameTagSiblings.length > 1) {
const index = siblings.indexOf(current) + 1;
selector += `:nth-child(${index})`;
}

parts.unshift(selector);
}

// Try to build the selector
const combinedSelector = parts.join(' > ');
const found = document.querySelectorAll(combinedSelector);

// If it returns exactly the original element, return it
if (found.length === 1 && found[0] === el) {
return combinedSelector;
}

// Move up
current = current.parentElement;
if (current === null) {
break;
}
}

return null;
},
getSSPath: function (elem: HTMLElement) {
return elem.getAttribute('ss-path') || '';
},
};

export interface TemplatesEditorProps extends ComponentProps {
onRemoveClick: () => void;
templatesStore: any;
}

const DomSelector = (props: any) => {
const { elementSelector, onSelectHandler, type, currentSelector, setCurrentSelector } = props;

const [active, setActive] = useState(currentSelector === type);

const elements = document.querySelectorAll(elementSelector);

const clickListener = useCallback((e: any) => {
e.preventDefault();
e.stopPropagation();
// get element from event
const target = e.target.closest(elementSelector);
if (!target) return;
if (type == 'component') {
const ssPath = ElementSelectorHelpers.getSSPath(target);
if (ssPath) {
// TODO: open editor with ssPath component
onSelectHandler(ssPath);
}
} else {
onSelectHandler(ElementSelectorHelpers.getShortestUniqueSelector(target));
}

setCurrentSelector('');
cleanUp();
}, []);

// TODO add an onscroll event that repositions the overlay and tooltip

// TOD remove the dashed border on contentSelector

const onMouseOverListener = useCallback((e: any) => {
e.preventDefault();
e.stopPropagation();
// add styling and tooltip thing
const target = e.target.closest(elementSelector) as HTMLElement;
if (target) {
const overlay = document.createElement('div');
overlay.classList.add('ss-overlay');
overlay.style.width = `${target.getBoundingClientRect().width}px`;
overlay.style.height = `${target.getBoundingClientRect().height}px`;
overlay.style.left = `${target.getBoundingClientRect().left}px`;
overlay.style.top = `${target.getBoundingClientRect().top}px`;

const tooltip = document.createElement('div');
tooltip.classList.add('ss-tooltip');
tooltip.style.left = `${target.getBoundingClientRect().left}px`;
tooltip.style.top = `${target.getBoundingClientRect().bottom}px`;

const ssPath = ElementSelectorHelpers.getSSPath(target);
tooltip.innerHTML += `
${ssPath ? `<label><b>SS-Path:</b> ${ssPath}</label>` : ''}
<br />
<label><b>Shortest Unique Selector:</b> ${ElementSelectorHelpers.getShortestUniqueSelector(target)}</label>
`;
document.body.appendChild(tooltip);
document.body.appendChild(overlay);
}
}, []);

const cleanUp = () => {
// remove the tooltip overlay
const tooltipOverlay = document.querySelector('.ss-tooltip');
if (tooltipOverlay) {
tooltipOverlay.remove();
}
const overlay = document.querySelector('.ss-overlay');
if (overlay) {
overlay.remove();
}
};

const onMouseOutListener = useCallback(() => {
cleanUp();
}, []);

const removeEvents = () => {
elements.forEach((elem) => {
elem.removeEventListener('click', clickListener);
elem.removeEventListener('mouseover', onMouseOverListener);
elem.removeEventListener('mouseout', onMouseOutListener);
elem.style.outline = 'none';
});
cleanUp();
};

const addEvents = () => {
elements.forEach((elem) => {
elem.addEventListener('click', clickListener);
elem.addEventListener('mouseover', onMouseOverListener);
elem.addEventListener('mouseout', onMouseOutListener);
if (type !== 'content') elem.style.outline = '1px dotted #3a23ad';
});
};

useEffect(() => {
setActive(currentSelector === type);
}, [currentSelector]);

useEffect(() => {
if (active) {
// loop through elements and:
// attach listeners
// (optional) add overlay/border to element
console.log('adding events from', type);
addEvents();
} else {
// loop through elements and:
// remove event listeners
console.log('removing events from', type);
removeEvents();
}
}, [active]);

useEffect(() => {
if (!active) return;

const handleKeyDown = (e: KeyboardEvent) => {
console.log('SS: running keydown event listener');
if (e.key === 'Escape') {
setCurrentSelector('');
cleanUp();
}
};

// TODO make sure we only bind once
console.log('SS: adding keydown event listener');
document.addEventListener('keydown', handleKeyDown);
return () => {
console.log('SS: removing keydown event listener');
document.removeEventListener('keydown', handleKeyDown);
};
}, [active]);

const toggleInspecting = () => {
if (active) {
setCurrentSelector('');
} else {
setCurrentSelector(type);
}
};

return (
<div>
<button
onClick={() => {
toggleInspecting();
}}
>
{active ? 'Cancel' : 'Find'}
</button>
</div>
);
};

const ThemeEditor = (props: any): any => {
const pathPrefix: any = props.pathPrefix || [];
const path = [...pathPrefix, props?.propertyName].filter((a) => a);
Expand Down Expand Up @@ -505,8 +821,3 @@ const ThemeEditor = (props: any): any => {
}
}
};

export interface TemplatesEditorProps extends ComponentProps {
onRemoveClick: () => void;
templatesStore: any;
}
Loading