Teact: Support SVG (#3593)

This commit is contained in:
Alexander Zinchuk 2023-07-20 15:58:43 +02:00
parent 9b967cce31
commit 46d9278900
5 changed files with 132 additions and 62 deletions

View File

@ -28,7 +28,7 @@ import LockScreen from './main/LockScreen.async';
import AppInactive from './main/AppInactive'; import AppInactive from './main/AppInactive';
import Transition from './ui/Transition'; import Transition from './ui/Transition';
import UiLoader from './common/UiLoader'; import UiLoader from './common/UiLoader';
// import Test from './test/TestUpdateRef'; // import Test from './test/TestSvg';
import styles from './App.module.scss'; import styles from './App.module.scss';

View File

@ -0,0 +1,51 @@
import React, { useState } from '../../lib/teact/teact';
export function App() {
const [stateValue, setStateValue] = useState(false);
return (
<div
className="App"
onClick={() => {
setStateValue((current) => !current);
}}
>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" version="1.1">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:rgb(0,255,0);stop-opacity:1" />
<stop offset="100%" style="stop-color:rgb(0,0,255);stop-opacity:1" />
</linearGradient>
</defs>
{stateValue && (
<circle
cx="75"
cy="75"
r="50"
stroke-dasharray="85 20"
stroke="#dddddd"
stroke-linecap="round"
stroke-width="10"
fill="none"
/>
)}
<circle
cx="75"
cy="75"
r="50"
className="shared-canvas-container"
stroke-dashoffset={stateValue ? '140' : '0'}
stroke-dasharray={stateValue ? '85 160' : '85 1000'}
stroke="url(#grad1)"
stroke-linecap="round"
stroke-width="10"
fill="none"
/>
</svg>
</div>
);
}
export default App;

View File

@ -1,15 +1,15 @@
import { DEBUG } from '../../config'; import { DEBUG } from '../../config';
type Handler = (e: Event) => void; type Handler = (e: Event) => void;
type DelegationRegistry = Map<HTMLElement, Handler>; type DelegationRegistry = Map<Element, Handler>;
const NON_BUBBLEABLE_EVENTS = new Set(['scroll', 'mouseenter', 'mouseleave', 'load']); const NON_BUBBLEABLE_EVENTS = new Set(['scroll', 'mouseenter', 'mouseleave', 'load']);
const documentEventCounters: Record<string, number> = {}; const documentEventCounters: Record<string, number> = {};
const delegationRegistryByEventType: Record<string, DelegationRegistry> = {}; const delegationRegistryByEventType: Record<string, DelegationRegistry> = {};
const delegatedEventTypesByElement = new Map<HTMLElement, Set<string>>(); const delegatedEventTypesByElement = new Map<Element, Set<string>>();
export function addEventListener(element: HTMLElement, propName: string, handler: Handler, asCapture = false) { export function addEventListener(element: Element, propName: string, handler: Handler, asCapture = false) {
const eventType = resolveEventType(propName, element); const eventType = resolveEventType(propName, element);
if (canUseEventDelegation(eventType, element, asCapture)) { if (canUseEventDelegation(eventType, element, asCapture)) {
addDelegatedListener(eventType, element, handler); addDelegatedListener(eventType, element, handler);
@ -18,7 +18,7 @@ export function addEventListener(element: HTMLElement, propName: string, handler
} }
} }
export function removeEventListener(element: HTMLElement, propName: string, handler: Handler, asCapture = false) { export function removeEventListener(element: Element, propName: string, handler: Handler, asCapture = false) {
const eventType = resolveEventType(propName, element); const eventType = resolveEventType(propName, element);
if (canUseEventDelegation(eventType, element, asCapture)) { if (canUseEventDelegation(eventType, element, asCapture)) {
removeDelegatedListener(eventType, element); removeDelegatedListener(eventType, element);
@ -27,7 +27,7 @@ export function removeEventListener(element: HTMLElement, propName: string, hand
} }
} }
function resolveEventType(propName: string, element: HTMLElement) { function resolveEventType(propName: string, element: Element) {
const eventType = propName const eventType = propName
.replace(/^on/, '') .replace(/^on/, '')
.replace(/Capture$/, '').toLowerCase(); .replace(/Capture$/, '').toLowerCase();
@ -54,7 +54,7 @@ function resolveEventType(propName: string, element: HTMLElement) {
return eventType; return eventType;
} }
function canUseEventDelegation(realEventType: string, element: HTMLElement, asCapture: boolean) { function canUseEventDelegation(realEventType: string, element: Element, asCapture: boolean) {
return ( return (
!asCapture !asCapture
&& !NON_BUBBLEABLE_EVENTS.has(realEventType) && !NON_BUBBLEABLE_EVENTS.has(realEventType)
@ -63,7 +63,7 @@ function canUseEventDelegation(realEventType: string, element: HTMLElement, asCa
); );
} }
function addDelegatedListener(eventType: string, element: HTMLElement, handler: Handler) { function addDelegatedListener(eventType: string, element: Element, handler: Handler) {
if (!documentEventCounters[eventType]) { if (!documentEventCounters[eventType]) {
documentEventCounters[eventType] = 0; documentEventCounters[eventType] = 0;
document.addEventListener(eventType, handleEvent); document.addEventListener(eventType, handleEvent);
@ -74,7 +74,7 @@ function addDelegatedListener(eventType: string, element: HTMLElement, handler:
documentEventCounters[eventType]++; documentEventCounters[eventType]++;
} }
function removeDelegatedListener(eventType: string, element: HTMLElement) { function removeDelegatedListener(eventType: string, element: Element) {
documentEventCounters[eventType]--; documentEventCounters[eventType]--;
if (!documentEventCounters[eventType]) { if (!documentEventCounters[eventType]) {
// Synchronous deletion on 0 will cause perf degradation in the case of 1 element // Synchronous deletion on 0 will cause perf degradation in the case of 1 element
@ -86,7 +86,7 @@ function removeDelegatedListener(eventType: string, element: HTMLElement) {
delegatedEventTypesByElement.get(element)!.delete(eventType); delegatedEventTypesByElement.get(element)!.delete(eventType);
} }
export function removeAllDelegatedListeners(element: HTMLElement) { export function removeAllDelegatedListeners(element: Element) {
const eventTypes = delegatedEventTypesByElement.get(element); const eventTypes = delegatedEventTypesByElement.get(element);
if (!eventTypes) { if (!eventTypes) {
return; return;
@ -101,7 +101,7 @@ function handleEvent(realEvent: Event) {
if (events) { if (events) {
let furtherCallsPrevented = false; let furtherCallsPrevented = false;
let current: HTMLElement = realEvent.target as HTMLElement; let current: Element = realEvent.target as Element;
const stopPropagation = () => { const stopPropagation = () => {
furtherCallsPrevented = true; furtherCallsPrevented = true;
@ -138,7 +138,7 @@ function handleEvent(realEvent: Event) {
} }
} }
current = current.parentNode as HTMLElement; current = current.parentNode as Element;
} }
} }
} }
@ -151,7 +151,7 @@ function resolveDelegationRegistry(eventType: string) {
return delegationRegistryByEventType[eventType]; return delegationRegistryByEventType[eventType];
} }
function resolveDelegatedEventTypes(element: HTMLElement) { function resolveDelegatedEventTypes(element: Element) {
const existing = delegatedEventTypesByElement.get(element); const existing = delegatedEventTypesByElement.get(element);
if (existing) { if (existing) {
return existing; return existing;

View File

@ -32,6 +32,8 @@ interface SelectionState {
isCaretAtEnd: boolean; isCaretAtEnd: boolean;
} }
type DOMElement = HTMLElement | SVGElement;
const FILTERED_ATTRIBUTES = new Set(['key', 'ref', 'teactFastList', 'teactOrderKey']); const FILTERED_ATTRIBUTES = new Set(['key', 'ref', 'teactFastList', 'teactOrderKey']);
const HTML_ATTRIBUTES = new Set(['dir', 'role', 'form']); const HTML_ATTRIBUTES = new Set(['dir', 'role', 'form']);
const CONTROLLABLE_TAGS = ['INPUT', 'TEXTAREA', 'SELECT']; const CONTROLLABLE_TAGS = ['INPUT', 'TEXTAREA', 'SELECT'];
@ -71,7 +73,7 @@ function render($element: VirtualElement | undefined, parentEl: HTMLElement) {
} }
function renderWithVirtual<T extends VirtualElement | undefined>( function renderWithVirtual<T extends VirtualElement | undefined>(
parentEl: HTMLElement, parentEl: DOMElement,
$current: VirtualElement | undefined, $current: VirtualElement | undefined,
$new: T, $new: T,
$parent: VirtualElementParent | VirtualDomHead, $parent: VirtualElementParent | VirtualDomHead,
@ -81,17 +83,22 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
nextSibling?: ChildNode; nextSibling?: ChildNode;
forceMoveToEnd?: boolean; forceMoveToEnd?: boolean;
fragment?: DocumentFragment; fragment?: DocumentFragment;
isSvg?: true;
} = {}, } = {},
): T { ): T {
const { skipComponentUpdate, fragment } = options; const { skipComponentUpdate, fragment } = options;
let { nextSibling } = options; let { nextSibling, isSvg } = options;
const isCurrentComponent = $current && $current.type === VirtualType.Component; const isCurrentComponent = $current?.type === VirtualType.Component;
const isNewComponent = $new && $new.type === VirtualType.Component; const isNewComponent = $new?.type === VirtualType.Component;
const $newAsReal = $new as VirtualElementReal; const $newAsReal = $new as VirtualElementReal;
const isCurrentFragment = $current && !isCurrentComponent && $current.type === VirtualType.Fragment; const isCurrentFragment = !isCurrentComponent && $current?.type === VirtualType.Fragment;
const isNewFragment = $new && !isNewComponent && $new.type === VirtualType.Fragment; const isNewFragment = !isNewComponent && $new?.type === VirtualType.Fragment;
if ($new?.type === VirtualType.Tag && $new.tag === 'svg') {
isSvg = true;
}
if ( if (
!skipComponentUpdate !skipComponentUpdate
@ -127,7 +134,9 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
$new = initComponent(parentEl, $new as VirtualElementComponent, $parent, index) as unknown as typeof $new; $new = initComponent(parentEl, $new as VirtualElementComponent, $parent, index) as unknown as typeof $new;
} }
mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { nextSibling, fragment }); mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, {
nextSibling, fragment, isSvg,
});
} else { } else {
const canSetTextContent = !fragment const canSetTextContent = !fragment
&& !nextSibling && !nextSibling
@ -139,12 +148,12 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
parentEl.textContent = $newAsReal.value; parentEl.textContent = $newAsReal.value;
$newAsReal.target = parentEl.firstChild!; $newAsReal.target = parentEl.firstChild!;
} else { } else {
const node = createNode($newAsReal); const node = createNode($newAsReal, isSvg);
$newAsReal.target = node; $newAsReal.target = node;
insertBefore(fragment || parentEl, node, nextSibling); insertBefore(fragment || parentEl, node, nextSibling);
if ($newAsReal.type === VirtualType.Tag) { if ($newAsReal.type === VirtualType.Tag) {
setElementRef($newAsReal, node as HTMLElement); setElementRef($newAsReal, node as DOMElement);
} }
} }
} }
@ -162,14 +171,16 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
} }
remount(parentEl, $current, undefined); remount(parentEl, $current, undefined);
mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, { nextSibling, fragment }); mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, {
nextSibling, fragment, isSvg,
});
} else { } else {
const node = createNode($newAsReal); const node = createNode($newAsReal, isSvg);
$newAsReal.target = node; $newAsReal.target = node;
remount(parentEl, $current, node, nextSibling); remount(parentEl, $current, node, nextSibling);
if ($newAsReal.type === VirtualType.Tag) { if ($newAsReal.type === VirtualType.Tag) {
setElementRef($newAsReal, node as HTMLElement); setElementRef($newAsReal, node as DOMElement);
} }
} }
} else { } else {
@ -196,14 +207,14 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
const $newAsTag = $new as VirtualElementTag; const $newAsTag = $new as VirtualElementTag;
setElementRef($current, undefined); setElementRef($current, undefined);
setElementRef($newAsTag, currentTarget as HTMLElement); setElementRef($newAsTag, currentTarget as DOMElement);
if (nextSibling || options.forceMoveToEnd) { if (nextSibling || options.forceMoveToEnd) {
insertBefore(parentEl, currentTarget, nextSibling); insertBefore(parentEl, currentTarget, nextSibling);
} }
updateAttributes($current, $newAsTag, currentTarget as HTMLElement); updateAttributes($current, $newAsTag, currentTarget as DOMElement, isSvg);
renderChildren($current, $newAsTag, currentTarget as HTMLElement); renderChildren($current, $newAsTag, currentTarget as DOMElement, undefined, undefined, isSvg);
} }
} }
} }
@ -213,7 +224,7 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
} }
function initComponent( function initComponent(
parentEl: HTMLElement, parentEl: DOMElement,
$element: VirtualElementComponent, $element: VirtualElementComponent,
$parent: VirtualElementParent | VirtualDomHead, $parent: VirtualElementParent | VirtualDomHead,
index: number, index: number,
@ -235,7 +246,7 @@ function updateComponent($current: VirtualElementComponent, $new: VirtualElement
} }
function setupComponentUpdateListener( function setupComponentUpdateListener(
parentEl: HTMLElement, parentEl: DOMElement,
$element: VirtualElementComponent, $element: VirtualElementComponent,
$parent: VirtualElementParent | VirtualDomHead, $parent: VirtualElementParent | VirtualDomHead,
index: number, index: number,
@ -255,11 +266,12 @@ function setupComponentUpdateListener(
} }
function mountChildren( function mountChildren(
parentEl: HTMLElement, parentEl: DOMElement,
$element: VirtualElementComponent | VirtualElementFragment, $element: VirtualElementComponent | VirtualElementFragment,
options: { options: {
nextSibling?: ChildNode; nextSibling?: ChildNode;
fragment?: DocumentFragment; fragment?: DocumentFragment;
isSvg?: true;
}, },
) { ) {
const { children } = $element; const { children } = $element;
@ -272,13 +284,13 @@ function mountChildren(
} }
} }
function unmountChildren(parentEl: HTMLElement, $element: VirtualElementComponent | VirtualElementFragment) { function unmountChildren(parentEl: DOMElement, $element: VirtualElementComponent | VirtualElementFragment) {
for (const $child of $element.children) { for (const $child of $element.children) {
renderWithVirtual(parentEl, $child, undefined, $element, -1); renderWithVirtual(parentEl, $child, undefined, $element, -1);
} }
} }
function createNode($element: VirtualElementReal): Node { function createNode($element: VirtualElementReal, isSvg?: true): Node {
if ($element.type === VirtualType.Empty) { if ($element.type === VirtualType.Empty) {
return document.createTextNode(''); return document.createTextNode('');
} }
@ -288,7 +300,7 @@ function createNode($element: VirtualElementReal): Node {
} }
const { tag, props, children } = $element; const { tag, props, children } = $element;
const element = document.createElement(tag); const element = isSvg ? document.createElementNS('http://www.w3.org/2000/svg', tag) : document.createElement(tag);
processControlled(tag, props); processControlled(tag, props);
@ -297,7 +309,7 @@ function createNode($element: VirtualElementReal): Node {
if (!props.hasOwnProperty(key)) continue; if (!props.hasOwnProperty(key)) continue;
if (props[key] !== undefined) { if (props[key] !== undefined) {
setAttribute(element, key, props[key]); setAttribute(element, key, props[key], isSvg);
} }
} }
@ -305,7 +317,7 @@ function createNode($element: VirtualElementReal): Node {
for (let i = 0, l = children.length; i < l; i++) { for (let i = 0, l = children.length; i < l; i++) {
const $child = children[i]; const $child = children[i];
const $renderedChild = renderWithVirtual(element, undefined, $child, $element, i); const $renderedChild = renderWithVirtual(element, undefined, $child, $element, i, { isSvg });
if ($renderedChild !== $child) { if ($renderedChild !== $child) {
children[i] = $renderedChild; children[i] = $renderedChild;
} }
@ -315,7 +327,7 @@ function createNode($element: VirtualElementReal): Node {
} }
function remount( function remount(
parentEl: HTMLElement, parentEl: DOMElement,
$current: VirtualElement, $current: VirtualElement,
node: Node | undefined, node: Node | undefined,
componentNextSibling?: ChildNode, componentNextSibling?: ChildNode,
@ -366,7 +378,7 @@ function unmountRealTree($element: VirtualElement) {
} }
} }
function insertBefore(parentEl: HTMLElement | DocumentFragment, node: Node, nextSibling?: ChildNode) { function insertBefore(parentEl: DOMElement | DocumentFragment, node: Node, nextSibling?: ChildNode) {
if (nextSibling) { if (nextSibling) {
parentEl.insertBefore(node, nextSibling); parentEl.insertBefore(node, nextSibling);
} else { } else {
@ -386,9 +398,10 @@ function getNextSibling($current: VirtualElement): ChildNode | undefined {
function renderChildren( function renderChildren(
$current: VirtualElementParent, $current: VirtualElementParent,
$new: VirtualElementParent, $new: VirtualElementParent,
currentEl: HTMLElement, currentEl: DOMElement,
nextSibling?: ChildNode, nextSibling?: ChildNode,
forceMoveToEnd = false, forceMoveToEnd = false,
isSvg?: true,
) { ) {
if (DEBUG) { if (DEBUG) {
DEBUG_checkKeyUniqueness($new.children); DEBUG_checkKeyUniqueness($new.children);
@ -419,7 +432,7 @@ function renderChildren(
newChildren[i], newChildren[i],
$new, $new,
i, i,
i >= currentChildrenLength ? { fragment } : { nextSibling, forceMoveToEnd }, i >= currentChildrenLength ? { fragment, isSvg } : { nextSibling, forceMoveToEnd, isSvg },
); );
if ($renderedChild && $renderedChild !== newChildren[i]) { if ($renderedChild && $renderedChild !== newChildren[i]) {
@ -434,7 +447,7 @@ function renderChildren(
// This function allows to prepend/append a bunch of new DOM nodes to the top/bottom of preserved ones. // This function allows to prepend/append a bunch of new DOM nodes to the top/bottom of preserved ones.
// It also allows to selectively move particular preserved nodes within their DOM list. // It also allows to selectively move particular preserved nodes within their DOM list.
function renderFastListChildren($current: VirtualElementParent, $new: VirtualElementParent, currentEl: HTMLElement) { function renderFastListChildren($current: VirtualElementParent, $new: VirtualElementParent, currentEl: DOMElement) {
const currentChildren = $current.children; const currentChildren = $current.children;
const newChildren = $new.children; const newChildren = $new.children;
@ -549,7 +562,7 @@ function renderFastListChildren($current: VirtualElementParent, $new: VirtualEle
} }
function renderFragment( function renderFragment(
fragmentIndex: number, fragmentSize: number, parentEl: HTMLElement, $parent: VirtualElementParent, fragmentIndex: number, fragmentSize: number, parentEl: DOMElement, $parent: VirtualElementParent,
) { ) {
const nextSibling = parentEl.childNodes[fragmentIndex]; const nextSibling = parentEl.childNodes[fragmentIndex];
@ -576,13 +589,13 @@ function renderFragment(
insertBefore(parentEl, fragment, nextSibling); insertBefore(parentEl, fragment, nextSibling);
} }
function setElementRef($element: VirtualElementTag, htmlElement: HTMLElement | undefined) { function setElementRef($element: VirtualElementTag, DOMElement: DOMElement | undefined) {
const { ref } = $element.props; const { ref } = $element.props;
if (typeof ref === 'object') { if (typeof ref === 'object') {
ref.current = htmlElement; ref.current = DOMElement;
} else if (typeof ref === 'function') { } else if (typeof ref === 'function') {
ref(htmlElement); ref(DOMElement);
} }
} }
@ -629,7 +642,7 @@ function processControlled(tag: string, props: AnyLiteral) {
}; };
} }
function processUncontrolledOnMount(element: HTMLElement, props: AnyLiteral) { function processUncontrolledOnMount(element: DOMElement, props: AnyLiteral) {
if (!CONTROLLABLE_TAGS.includes(element.tagName)) { if (!CONTROLLABLE_TAGS.includes(element.tagName)) {
return; return;
} }
@ -643,7 +656,7 @@ function processUncontrolledOnMount(element: HTMLElement, props: AnyLiteral) {
} }
} }
function updateAttributes($current: VirtualElementTag, $new: VirtualElementTag, element: HTMLElement) { function updateAttributes($current: VirtualElementTag, $new: VirtualElementTag, element: DOMElement, isSvg?: true) {
processControlled(element.tagName, $new.props); processControlled(element.tagName, $new.props);
const currentEntries = Object.entries($current.props); const currentEntries = Object.entries($current.props);
@ -667,14 +680,14 @@ function updateAttributes($current: VirtualElementTag, $new: VirtualElementTag,
const currentValue = $current.props[key]; const currentValue = $current.props[key];
if (newValue !== undefined && newValue !== currentValue) { if (newValue !== undefined && newValue !== currentValue) {
setAttribute(element, key, newValue); setAttribute(element, key, newValue, isSvg);
} }
} }
} }
function setAttribute(element: HTMLElement, key: string, value: any) { function setAttribute(element: DOMElement, key: string, value: any, isSvg?: true) {
if (key === 'className') { if (key === 'className') {
updateClassName(element, value); updateClassName(element, value, isSvg);
} else if (key === 'value') { } else if (key === 'value') {
const inputEl = element as HTMLInputElement; const inputEl = element as HTMLInputElement;
@ -701,14 +714,14 @@ function setAttribute(element: HTMLElement, key: string, value: any) {
element.innerHTML = value.__html; element.innerHTML = value.__html;
} else if (key.startsWith('on')) { } else if (key.startsWith('on')) {
addEventListener(element, key, value, key.endsWith('Capture')); addEventListener(element, key, value, key.endsWith('Capture'));
} else if (key.startsWith('data-') || key.startsWith('aria-') || HTML_ATTRIBUTES.has(key)) { } else if (isSvg || key.startsWith('data-') || key.startsWith('aria-') || HTML_ATTRIBUTES.has(key)) {
element.setAttribute(key, value); element.setAttribute(key, value);
} else if (!FILTERED_ATTRIBUTES.has(key)) { } else if (!FILTERED_ATTRIBUTES.has(key)) {
(element as any)[MAPPED_ATTRIBUTES[key] || key] = value; (element as any)[MAPPED_ATTRIBUTES[key] || key] = value;
} }
} }
function removeAttribute(element: HTMLElement, key: string, value: any) { function removeAttribute(element: DOMElement, key: string, value: any) {
if (key === 'className') { if (key === 'className') {
updateClassName(element, ''); updateClassName(element, '');
} else if (key === 'value') { } else if (key === 'value') {
@ -724,10 +737,16 @@ function removeAttribute(element: HTMLElement, key: string, value: any) {
} }
} }
function updateClassName(element: HTMLElement, value: string) { function updateClassName(element: DOMElement, value: string, isSvg?: true) {
if (isSvg) {
element.setAttribute('class', value);
return;
}
const htmlElement = element as HTMLElement;
const extra = extraClasses.get(element); const extra = extraClasses.get(element);
if (!extra) { if (!extra) {
element.className = value; htmlElement.className = value;
return; return;
} }
@ -736,10 +755,10 @@ function updateClassName(element: HTMLElement, value: string) {
extraArray.push(value); extraArray.push(value);
} }
element.className = extraArray.join(' '); htmlElement.className = extraArray.join(' ');
} }
function updateStyle(element: HTMLElement, value: string) { function updateStyle(element: DOMElement, value: string) {
element.style.cssText = value; element.style.cssText = value;
const extraObject = extraStyles.get(element); const extraObject = extraStyles.get(element);
@ -748,7 +767,7 @@ function updateStyle(element: HTMLElement, value: string) {
} }
} }
export function addExtraClass(element: Element, className: string, forceSingle = false) { export function addExtraClass(element: DOMElement, className: string, forceSingle = false) {
if (!forceSingle) { if (!forceSingle) {
const classNames = className.split(' '); const classNames = className.split(' ');
if (classNames.length > 1) { if (classNames.length > 1) {
@ -770,7 +789,7 @@ export function addExtraClass(element: Element, className: string, forceSingle =
} }
} }
export function removeExtraClass(element: Element, className: string, forceSingle = false) { export function removeExtraClass(element: DOMElement, className: string, forceSingle = false) {
if (!forceSingle) { if (!forceSingle) {
const classNames = className.split(' '); const classNames = className.split(' ');
if (classNames.length > 1) { if (classNames.length > 1) {
@ -794,7 +813,7 @@ export function removeExtraClass(element: Element, className: string, forceSingl
} }
} }
export function toggleExtraClass(element: Element, className: string, force?: boolean, forceSingle = false) { export function toggleExtraClass(element: DOMElement, className: string, force?: boolean, forceSingle = false) {
if (!forceSingle) { if (!forceSingle) {
const classNames = className.split(' '); const classNames = className.split(' ');
if (classNames.length > 1) { if (classNames.length > 1) {
@ -815,12 +834,12 @@ export function toggleExtraClass(element: Element, className: string, force?: bo
} }
} }
export function setExtraStyles(element: HTMLElement, styles: Partial<CSSStyleDeclaration> & AnyLiteral) { export function setExtraStyles(element: DOMElement, styles: Partial<CSSStyleDeclaration> & AnyLiteral) {
extraStyles.set(element, styles); extraStyles.set(element, styles);
applyExtraStyles(element); applyExtraStyles(element);
} }
function applyExtraStyles(element: HTMLElement) { function applyExtraStyles(element: DOMElement) {
const standardStyles = Object.entries(extraStyles.get(element)!).reduce<Record<string, string>>( const standardStyles = Object.entries(extraStyles.get(element)!).reduce<Record<string, string>>(
(acc, [prop, value]) => { (acc, [prop, value]) => {
if (prop.startsWith('--')) { if (prop.startsWith('--')) {

View File

@ -37,7 +37,7 @@ interface VirtualElementText {
export interface VirtualElementTag { export interface VirtualElementTag {
type: VirtualType.Tag; type: VirtualType.Tag;
target?: HTMLElement; target?: HTMLElement | SVGElement;
tag: string; tag: string;
props: Props; props: Props;
children: VirtualElementChildren; children: VirtualElementChildren;