* applied eslint * add locales and lint the project * removed error boundary * updated locales * fix min files * fix locales
189 lines
5.1 KiB
TypeScript
189 lines
5.1 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import cn from 'classnames';
|
|
import useToggle from 'App/hooks/useToggle';
|
|
import useForceUpdate from 'App/hooks/useForceUpdate';
|
|
import { Icon } from 'UI';
|
|
import stl from './inspector.module.css';
|
|
|
|
import AttrView from './AttrView';
|
|
import TextView from './TextView';
|
|
import InlineInput from './InlineInput';
|
|
|
|
// TODO: add attribute, add child, add text (when there was no text before), Ctrl+Z
|
|
|
|
interface Window {
|
|
Element: typeof Element;
|
|
Text: typeof Text;
|
|
document: typeof document;
|
|
}
|
|
|
|
interface Props {
|
|
element: Element;
|
|
level: number;
|
|
context?: Window;
|
|
openChain: Element[];
|
|
forceUpdateParent: () => void;
|
|
selectedElement?: Element; // for deletion and other things
|
|
setSelectedElement?: (Element) => void;
|
|
onHover?: (Element) => void;
|
|
className?: string;
|
|
}
|
|
|
|
interface TagEditorProps {
|
|
element: Element;
|
|
forceUpdateParent: () => void;
|
|
context: Window;
|
|
}
|
|
|
|
// TODO: to common space
|
|
function stopPropagation(cb: Function): React.MouseEventHandler {
|
|
return function (e) {
|
|
e.stopPropagation();
|
|
cb();
|
|
};
|
|
}
|
|
|
|
const RESTRICTED_TAGS = ['html', 'body', 'head'];
|
|
function TagEditor({ element, forceUpdateParent, context }: TagEditorProps) {
|
|
const [editing, setEditing] = useState(false);
|
|
const commitTag = (newTag: string) => {
|
|
if (
|
|
newTag !== '' &&
|
|
!RESTRICTED_TAGS.includes(newTag) &&
|
|
element.parentNode &&
|
|
/^[a-zA-Z]+$/.test(newTag) // TODO: sync with spec
|
|
) {
|
|
const rElem = context.document.createElement(newTag);
|
|
rElem.innerHTML = element.innerHTML;
|
|
Array.from(element.attributes).forEach((attr) => {
|
|
rElem.setAttribute(attr.name, attr.value);
|
|
});
|
|
element.parentNode.replaceChild(rElem, element);
|
|
}
|
|
setEditing(false);
|
|
forceUpdateParent();
|
|
};
|
|
|
|
const tag = element.tagName.toLowerCase();
|
|
return editing && !RESTRICTED_TAGS.includes(tag) ? (
|
|
<InlineInput value={tag} commit={commitTag} />
|
|
) : (
|
|
<span
|
|
className={stl.tag}
|
|
onDoubleClick={
|
|
RESTRICTED_TAGS.includes(tag) ? undefined : () => setEditing(true)
|
|
}
|
|
>
|
|
{tag}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
// const IGNORE_CLASSES = [ "-openreplay-hover" ];
|
|
|
|
export default function ElementView({
|
|
element,
|
|
level,
|
|
context = window,
|
|
openChain,
|
|
forceUpdateParent,
|
|
selectedElement,
|
|
setSelectedElement,
|
|
onHover,
|
|
className,
|
|
}: Props) {
|
|
const [open, toggleOpen, _, setOpen] = useToggle(false);
|
|
|
|
// useEffect(() => { // TODO: common. something like onElementMount
|
|
// IGNORE_CLASSES.forEach(cls => element.classList.remove(cls));
|
|
// if (element.classList.length === 0) {
|
|
// element.removeAttribute("class");
|
|
// }
|
|
// }, [])
|
|
|
|
useEffect(() => {
|
|
if (openChain[level] === element) {
|
|
setOpen();
|
|
}
|
|
}, [openChain[level]]);
|
|
const forceUpdate = useForceUpdate();
|
|
|
|
const tag = element.tagName.toLowerCase();
|
|
const isSelected = selectedElement === element;
|
|
const selectElement = setSelectedElement
|
|
? stopPropagation(() => setSelectedElement(element))
|
|
: undefined;
|
|
const onMouseOver = onHover
|
|
? stopPropagation(() => onHover(element))
|
|
: undefined;
|
|
return (
|
|
<div
|
|
className={cn('font-mono', className, {
|
|
// todo: only in root
|
|
[stl.bgHighlight]: !open && isSelected,
|
|
'hover:bg-gray-light': !open && !isSelected,
|
|
})}
|
|
style={{ fontSize: '12px' }}
|
|
onMouseOver={onMouseOver}
|
|
>
|
|
<span
|
|
className={cn({
|
|
block: open,
|
|
[stl.bgHighlight]: open && isSelected,
|
|
'hover:bg-gray-light': open && !isSelected,
|
|
})}
|
|
>
|
|
<span role="button mr-1" onClick={toggleOpen}>
|
|
<Icon inline name={open ? 'caret-down-fill' : 'caret-right-fill'} />
|
|
</span>
|
|
<span onClick={selectElement}>
|
|
<span className={stl.tag}>{'<'}</span>
|
|
<TagEditor
|
|
element={element}
|
|
context={context}
|
|
forceUpdateParent={forceUpdateParent}
|
|
/>
|
|
{Array.from(element.attributes).map((attr) => (
|
|
<AttrView attr={attr} forceUpdateElement={forceUpdate} />
|
|
))}
|
|
<span className={stl.tag}>{'>'}</span>
|
|
</span>
|
|
</span>
|
|
{open ? (
|
|
<>
|
|
{Array.from(element.childNodes).map((child) => {
|
|
if (child instanceof context.Element) {
|
|
return (
|
|
<ElementView
|
|
element={child}
|
|
context={context}
|
|
forceUpdateParent={forceUpdate}
|
|
level={level + 1}
|
|
openChain={openChain}
|
|
selectedElement={selectedElement}
|
|
setSelectedElement={setSelectedElement}
|
|
onHover={onHover}
|
|
className="pl-4"
|
|
/>
|
|
);
|
|
}
|
|
if (child instanceof context.Text) {
|
|
if (!child.nodeValue || child.nodeValue.trim() === '') {
|
|
return null;
|
|
}
|
|
return <TextView text={child} />;
|
|
}
|
|
return null;
|
|
})}
|
|
</>
|
|
) : (
|
|
'...'
|
|
)}
|
|
<span className={stl.tag}>
|
|
{'</'}
|
|
{tag}
|
|
{'>'}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|