openreplay/frontend/app/components/Session_/Inspector/ElementView.tsx
Andrey Babushkin fd5c0c9747
Add lokalisation (#3092)
* applied eslint

* add locales and lint the project

* removed error boundary

* updated locales

* fix min files

* fix locales
2025-03-06 17:43:15 +01:00

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>
);
}