openreplay/frontend/app/components/Session_/Inspector/ElementView.tsx
Shekar Siri 2ed5cac986
Webpack upgrade and dependency cleanup (#523)
* change(ui) - webpack update
* change(ui) - api optimize and other fixes
2022-06-03 16:47:38 +02:00

188 lines
No EOL
4.7 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"
/>
);
} else 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>
);
}