openreplay/frontend/app/components/Session_/Inspector/ElementView.tsx
Андрей Бабушкин b822b1c067 applied eslint
2025-02-26 20:31:01 +01:00

191 lines
4.9 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>
);
}