SVG: Allow setting filters using JSX (#5449)

This commit is contained in:
zubiden 2025-01-21 18:21:10 +01:00 committed by Alexander Zinchuk
parent af472d0fd4
commit 8dc935d0b8
9 changed files with 184 additions and 201 deletions

View File

@ -86,6 +86,7 @@ import GiveawayModal from './premium/GiveawayModal.async';
import PremiumMainModal from './premium/PremiumMainModal.async'; import PremiumMainModal from './premium/PremiumMainModal.async';
import StarsGiftingPickerModal from './premium/StarsGiftingPickerModal.async'; import StarsGiftingPickerModal from './premium/StarsGiftingPickerModal.async';
import SafeLinkModal from './SafeLinkModal.async'; import SafeLinkModal from './SafeLinkModal.async';
import SvgController from './SvgController';
import ConfettiContainer from './visualEffects/ConfettiContainer'; import ConfettiContainer from './visualEffects/ConfettiContainer';
import SnapEffectContainer from './visualEffects/SnapEffectContainer'; import SnapEffectContainer from './visualEffects/SnapEffectContainer';
import WaveContainer from './visualEffects/WaveContainer'; import WaveContainer from './visualEffects/WaveContainer';
@ -587,6 +588,7 @@ const Main = ({
<DeleteFolderDialog folder={deleteFolderDialog} /> <DeleteFolderDialog folder={deleteFolderDialog} />
<ReactionPicker isOpen={isReactionPickerOpen} /> <ReactionPicker isOpen={isReactionPickerOpen} />
<DeleteMessageModal isOpen={isDeleteMessageModalOpen} /> <DeleteMessageModal isOpen={isDeleteMessageModalOpen} />
<SvgController />
</div> </div>
); );
}; };

View File

@ -0,0 +1,45 @@
import React, { memo, useEffect } from '../../lib/teact/teact';
import { createCallbackManager } from '../../util/callbacks';
import generateUniqueId from '../../util/generateUniqueId';
import useForceUpdate from '../../hooks/useForceUpdate';
import Portal from '../ui/Portal';
const DEFINITION_MAP = new Map<string, React.ReactElement>();
const CALLBACK_MANAGER = createCallbackManager();
const SvgController = () => {
const forceUpdate = useForceUpdate();
useEffect(() => {
return CALLBACK_MANAGER.addCallback(forceUpdate);
}, []);
return (
<Portal>
<svg width="0" height="0" viewBox="0 0 1 1" className="svg-definitions">
<defs>
{Array.from(DEFINITION_MAP.values())}
</defs>
</svg>
</Portal>
);
};
export default memo(SvgController);
export function addSvgDefinition(element: React.ReactElement, id?: string) {
id ??= generateUniqueId();
element.props.id = id;
DEFINITION_MAP.set(element.props.id, element);
CALLBACK_MANAGER.runCallbacks();
return id;
}
export function removeSvgDefinition(id: string) {
DEFINITION_MAP.delete(id);
CALLBACK_MANAGER.runCallbacks();
}

View File

@ -1,10 +1,10 @@
import React, { memo } from '../../../lib/teact/teact'; import React, { memo } from '../../../lib/teact/teact';
import { getGlobal } from '../../../global'; import { getGlobal } from '../../../global';
import { SNAP_EFFECT_CONTAINER_ID, SNAP_EFFECT_ID } from '../../../config'; import { SNAP_EFFECT_CONTAINER_ID, SNAP_EFFECT_ID, SVG_NAMESPACE } from '../../../config';
import { selectCanAnimateSnapEffect } from '../../../global/selectors'; import { selectCanAnimateSnapEffect } from '../../../global/selectors';
import jsxToHtml from '../../../util/element/jsxToHtml';
import generateUniqueId from '../../../util/generateUniqueId'; import generateUniqueId from '../../../util/generateUniqueId';
import { SVG_NAMESPACE } from '../../../util/svgController';
import styles from './SnapEffectContainer.module.scss'; import styles from './SnapEffectContainer.module.scss';
@ -39,29 +39,25 @@ export function animateSnap(element: HTMLElement) {
const seed = Math.floor(Date.now() / 1000); const seed = Math.floor(Date.now() / 1000);
const filterId = `${SNAP_EFFECT_ID}-${generateUniqueId()}`; const filterId = `${SNAP_EFFECT_ID}-${generateUniqueId()}`;
const svg = document.createElementNS(SVG_NAMESPACE, 'svg'); const component = (
svg.setAttribute('class', styles.ghost); <svg
svg.setAttribute('width', `${width}px`); className={styles.ghost}
svg.setAttribute('height', `${height}px`); width={width}
svg.setAttribute('style', `left: ${x}px; top: ${y}px;`); height={height}
svg.setAttribute('viewBox', `0 0 ${width} ${height}`); style={`left: ${x}px; top: ${y}px;`}
viewBox={`0 0 ${width} ${height}`}
>
<defs>
{createFilter(filterId, Math.min(width, height), seed)}
</defs>
<g filter={`url(#${filterId})`}>
<foreignObject className={styles.elementContainer} width={width} height={height} />
</g>
</svg>
);
const defs = document.createElementNS(SVG_NAMESPACE, 'defs'); const svg = jsxToHtml(component)[0] as HTMLElement;
svg.appendChild(defs); const foreignObject = svg.querySelector('foreignObject')!;
const filter = createFilter(Math.min(width, height), seed);
filter.setAttribute('id', filterId);
defs.appendChild(filter);
const g = document.createElementNS(SVG_NAMESPACE, 'g');
g.setAttribute('filter', `url(#${filterId})`);
svg.appendChild(g);
const foreignObject = document.createElementNS(SVG_NAMESPACE, 'foreignObject');
foreignObject.setAttribute('class', styles.elementContainer);
foreignObject.setAttribute('width', `${width}px`);
foreignObject.setAttribute('height', `${height}px`);
g.appendChild(foreignObject);
const computedStyle = window.getComputedStyle(element); const computedStyle = window.getComputedStyle(element);
const clone = element.cloneNode(true) as HTMLElement; const clone = element.cloneNode(true) as HTMLElement;
@ -83,90 +79,45 @@ export function animateSnap(element: HTMLElement) {
return true; return true;
} }
function createFilter(smallestSide: number, baseSeed: number = 42) { function createFilter(id: string, smallestSide: number, baseSeed: number = 42) {
const filter = document.createElementNS(SVG_NAMESPACE, 'filter'); return (
filter.setAttribute('x', '-150%'); <filter
filter.setAttribute('y', '-150%'); xmlns={SVG_NAMESPACE}
filter.setAttribute('width', '400%'); id={id}
filter.setAttribute('height', '400%'); x="-150%"
filter.setAttribute('color-interpolation-filters', 'sRGB'); y="-150%"
width="400%"
const feTurbulence = document.createElementNS(SVG_NAMESPACE, 'feTurbulence'); height="400%"
feTurbulence.setAttribute('type', 'fractalNoise'); color-interpolation-filters="sRGB"
feTurbulence.setAttribute('baseFrequency', '0.5'); >
feTurbulence.setAttribute('numOctaves', '1'); <feTurbulence type="fractalNoise" baseFrequency="0.5" numOctaves="1" result="dustNoise" seed={baseSeed} />
feTurbulence.setAttribute('result', 'dustNoise'); <feComponentTransfer in="dustNoise" result="dustNoiseMask">
feTurbulence.setAttribute('seed', baseSeed.toString()); <feFuncA type="linear" slope="5" intercept="0">
filter.appendChild(feTurbulence); <animate attributeName="slope" values="5; 2; 1; 0" dur={`${DURATION}ms`} fill="freeze" />
</feFuncA>
const feComponentTransfer = document.createElementNS(SVG_NAMESPACE, 'feComponentTransfer'); </feComponentTransfer>
feComponentTransfer.setAttribute('in', 'dustNoise'); <feComposite in="SourceGraphic" in2="dustNoiseMask" operator="in" result="dustySource" />
feComponentTransfer.setAttribute('result', 'dustNoiseMask'); <feTurbulence
filter.appendChild(feComponentTransfer); type="fractalNoise"
baseFrequency="0.015"
const feFuncA = document.createElementNS(SVG_NAMESPACE, 'feFuncA'); numOctaves="1"
feFuncA.setAttribute('type', 'linear'); result="displacementNoise1"
feFuncA.setAttribute('slope', '5'); seed={baseSeed + 1}
feFuncA.setAttribute('intercept', '0'); />
feComponentTransfer.appendChild(feFuncA); <feTurbulence
type="fractalNoise"
const feFuncAAnimate = document.createElementNS(SVG_NAMESPACE, 'animate'); baseFrequency="1"
feFuncAAnimate.setAttribute('attributeName', 'slope'); numOctaves="2"
feFuncAAnimate.setAttribute('values', '5; 2; 1; 0'); result="displacementNoise2"
feFuncAAnimate.setAttribute('dur', `${DURATION}ms`); seed={baseSeed + 2}
feFuncAAnimate.setAttribute('fill', 'freeze'); />
feFuncA.appendChild(feFuncAAnimate); <feMerge result="combinedNoise">
<feMergeNode in="displacementNoise1" />
const feComposite = document.createElementNS(SVG_NAMESPACE, 'feComposite'); <feMergeNode in="displacementNoise2" />
feComposite.setAttribute('in', 'SourceGraphic'); </feMerge>
feComposite.setAttribute('in2', 'dustNoiseMask'); <feDisplacementMap in="dustySource" in2="combinedNoise" scale="0" xChannelSelector="R" yChannelSelector="G">
feComposite.setAttribute('operator', 'in'); <animate attributeName="scale" values={`0; ${smallestSide * 3}`} dur={`${DURATION}ms`} fill="freeze" />
feComposite.setAttribute('result', 'dustySource'); </feDisplacementMap>
filter.appendChild(feComposite); </filter>
);
const feTurbulence2 = document.createElementNS(SVG_NAMESPACE, 'feTurbulence');
feTurbulence2.setAttribute('type', 'fractalNoise');
feTurbulence2.setAttribute('baseFrequency', '0.015');
feTurbulence2.setAttribute('numOctaves', '1');
feTurbulence2.setAttribute('result', 'displacementNoice1');
feTurbulence2.setAttribute('seed', (baseSeed + 1).toString());
filter.appendChild(feTurbulence2);
const feTurbulence3 = document.createElementNS(SVG_NAMESPACE, 'feTurbulence');
feTurbulence3.setAttribute('type', 'fractalNoise');
feTurbulence3.setAttribute('baseFrequency', '1');
feTurbulence3.setAttribute('numOctaves', '2');
feTurbulence3.setAttribute('result', 'displacementNoice2');
feTurbulence3.setAttribute('seed', (baseSeed + 2).toString());
filter.appendChild(feTurbulence3);
const feMerge = document.createElementNS(SVG_NAMESPACE, 'feMerge');
feMerge.setAttribute('result', 'combinedNoise');
filter.appendChild(feMerge);
const feMergeNode1 = document.createElementNS(SVG_NAMESPACE, 'feMergeNode');
feMergeNode1.setAttribute('in', 'displacementNoice1');
feMerge.appendChild(feMergeNode1);
const feMergeNode2 = document.createElementNS(SVG_NAMESPACE, 'feMergeNode');
feMergeNode2.setAttribute('in', 'displacementNoice2');
feMerge.appendChild(feMergeNode2);
const feDisplacementMap = document.createElementNS(SVG_NAMESPACE, 'feDisplacementMap');
feDisplacementMap.setAttribute('in', 'dustySource');
feDisplacementMap.setAttribute('in2', 'combinedNoise');
feDisplacementMap.setAttribute('scale', '0');
feDisplacementMap.setAttribute('xChannelSelector', 'R');
feDisplacementMap.setAttribute('yChannelSelector', 'G');
filter.appendChild(feDisplacementMap);
const feDisplacementMapAnimate = document.createElementNS(SVG_NAMESPACE, 'animate');
feDisplacementMapAnimate.setAttribute('attributeName', 'scale');
feDisplacementMapAnimate.setAttribute('values', `0; ${smallestSide * 3}`);
feDisplacementMapAnimate.setAttribute('dur', `${DURATION}ms`);
feDisplacementMapAnimate.setAttribute('fill', 'freeze');
feDisplacementMap.appendChild(feDisplacementMapAnimate);
return filter;
} }

View File

@ -5,14 +5,16 @@ import { withGlobal } from '../../../global';
import type { TabState } from '../../../global/types'; import type { TabState } from '../../../global/types';
import { SVG_NAMESPACE } from '../../../config';
import { selectTabState } from '../../../global/selectors'; import { selectTabState } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import buildStyle from '../../../util/buildStyle'; import buildStyle from '../../../util/buildStyle';
import { addSvgDefinition, removeSvgDefinition, SVG_NAMESPACE } from '../../../util/svgController';
import windowSize from '../../../util/windowSize'; import windowSize from '../../../util/windowSize';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import { addSvgDefinition, removeSvgDefinition } from '../SvgController';
import styles from './WaveContainer.module.scss'; import styles from './WaveContainer.module.scss';
import waveRipple from '../../../assets/wave_ripple.svg'; import waveRipple from '../../../assets/wave_ripple.svg';
@ -62,26 +64,19 @@ const WaveContainer = ({ waveInfo }: StateProps) => {
}, [waveInfo]); }, [waveInfo]);
useEffect(() => { useEffect(() => {
const filter = document.createElementNS(SVG_NAMESPACE, 'filter'); addSvgDefinition(
filter.setAttribute('x', '0'); <filter x="0" y="0" width="1" height="1" color-interpolation-filters="sRGB" xmlns={SVG_NAMESPACE}>
filter.setAttribute('y', '0'); <feImage href={waveRipple} result="waveImage" />
filter.setAttribute('width', '1'); <feDisplacementMap
filter.setAttribute('height', '1'); in="SourceGraphic"
filter.setAttribute('color-interpolation-filters', 'sRGB'); in2="waveImage"
addSvgDefinition(filter, FILTER_ID); scale={FILTER_SCALE}
xChannelSelector="R"
const feImage = document.createElementNS(SVG_NAMESPACE, 'feImage'); yChannelSelector="B"
feImage.setAttribute('href', waveRipple); />
feImage.setAttribute('result', 'waveImage'); </filter>,
filter.appendChild(feImage); FILTER_ID,
);
const feDisplacementMap = document.createElementNS(SVG_NAMESPACE, 'feDisplacementMap');
feDisplacementMap.setAttribute('in', 'SourceGraphic');
feDisplacementMap.setAttribute('in2', 'waveImage');
feDisplacementMap.setAttribute('scale', FILTER_SCALE);
feDisplacementMap.setAttribute('xChannelSelector', 'R');
feDisplacementMap.setAttribute('yChannelSelector', 'B');
filter.appendChild(feDisplacementMap);
return () => { return () => {
removeSvgDefinition(FILTER_ID); removeSvgDefinition(FILTER_ID);

View File

@ -227,6 +227,8 @@ export const SLIDE_TRANSITION_DURATION = 450;
export const BIRTHDAY_NUMBERS_SET = 'FestiveFontEmoji'; export const BIRTHDAY_NUMBERS_SET = 'FestiveFontEmoji';
export const RESTRICTED_EMOJI_SET = 'RestrictedEmoji'; export const RESTRICTED_EMOJI_SET = 'RestrictedEmoji';
export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
export const VIDEO_WEBM_TYPE = 'video/webm'; export const VIDEO_WEBM_TYPE = 'video/webm';
export const GIF_MIME_TYPE = 'image/gif'; export const GIF_MIME_TYPE = 'image/gif';

View File

@ -1,8 +1,10 @@
import { useEffect } from '../../lib/teact/teact'; import React, { useEffect } from '../../lib/teact/teact';
import { addSvgDefinition, removeSvgDefinition, SVG_NAMESPACE } from '../../util/svgController'; import { SVG_NAMESPACE } from '../../config';
import { hexToRgb } from '../../util/switchTheme'; import { hexToRgb } from '../../util/switchTheme';
import { addSvgDefinition, removeSvgDefinition } from '../../components/main/SvgController';
const SVG_MAP = new Map<string, SvgColorFilter>(); const SVG_MAP = new Map<string, SvgColorFilter>();
class SvgColorFilter { class SvgColorFilter {
@ -13,20 +15,16 @@ class SvgColorFilter {
constructor(public color: string) { constructor(public color: string) {
this.filterId = `color-filter-${color.slice(1)}`; this.filterId = `color-filter-${color.slice(1)}`;
const filter = document.createElementNS(SVG_NAMESPACE, 'filter');
filter.setAttribute('color-interpolation-filters', 'sRGB');
addSvgDefinition(filter, this.filterId);
const feColorMatrix = document.createElementNS(SVG_NAMESPACE, 'feColorMatrix');
feColorMatrix.setAttribute('type', 'matrix');
const rgbColor = hexToRgb(color); const rgbColor = hexToRgb(color);
feColorMatrix.setAttribute( addSvgDefinition(
'values', <filter color-interpolation-filters="sRGB" xmlns={SVG_NAMESPACE}>
`0 0 0 0 ${rgbColor.r / 255} 0 0 0 0 ${rgbColor.g / 255} 0 0 0 0 ${rgbColor.b / 255} 0 0 0 1 0`, <feColorMatrix
type="matrix"
values={`0 0 0 0 ${rgbColor.r / 255} 0 0 0 0 ${rgbColor.g / 255} 0 0 0 0 ${rgbColor.b / 255} 0 0 0 1 0`}
/>
</filter>,
this.filterId,
); );
filter.appendChild(feColorMatrix);
} }
public getFilterId() { public getFilterId() {

View File

@ -39,6 +39,9 @@ type CurrentContext = Record<string, Signal<unknown>>;
type DOMElement = HTMLElement | SVGElement; type DOMElement = HTMLElement | SVGElement;
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
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'];
@ -89,11 +92,11 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
nextSibling?: ChildNode; nextSibling?: ChildNode;
forceMoveToEnd?: boolean; forceMoveToEnd?: boolean;
fragment?: DocumentFragment; fragment?: DocumentFragment;
isSvg?: true; namespace?: string;
} = {}, } = {},
): T { ): T {
const { skipComponentUpdate, fragment } = options; const { skipComponentUpdate, fragment } = options;
let { nextSibling, isSvg } = options; let { nextSibling, namespace } = options;
const isCurrentComponent = $current?.type === VirtualType.Component; const isCurrentComponent = $current?.type === VirtualType.Component;
const isNewComponent = $new?.type === VirtualType.Component; const isNewComponent = $new?.type === VirtualType.Component;
@ -102,8 +105,9 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
const isCurrentFragment = !isCurrentComponent && $current?.type === VirtualType.Fragment; const isCurrentFragment = !isCurrentComponent && $current?.type === VirtualType.Fragment;
const isNewFragment = !isNewComponent && $new?.type === VirtualType.Fragment; const isNewFragment = !isNewComponent && $new?.type === VirtualType.Fragment;
if (!isSvg && $new?.type === VirtualType.Tag && $new.tag === 'svg') { if ($new?.type === VirtualType.Tag) {
isSvg = true; if ($new.tag === 'svg') namespace = SVG_NAMESPACE;
if ($new.props.xmlns) namespace = $new.props.xmlns;
} }
if ( if (
@ -144,7 +148,7 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
} }
mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, currentContext, { mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, currentContext, {
nextSibling, fragment, isSvg, nextSibling, fragment, namespace,
}); });
} else { } else {
const canSetTextContent = !fragment const canSetTextContent = !fragment
@ -157,7 +161,7 @@ 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, currentContext, isSvg); const node = createNode($newAsReal, currentContext, namespace);
$newAsReal.target = node; $newAsReal.target = node;
insertBefore(fragment || parentEl, node, nextSibling); insertBefore(fragment || parentEl, node, nextSibling);
@ -184,10 +188,10 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
remount(parentEl, $current, currentContext, undefined); remount(parentEl, $current, currentContext, undefined);
mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, currentContext, { mountChildren(parentEl, $new as VirtualElementComponent | VirtualElementFragment, currentContext, {
nextSibling, fragment, isSvg, nextSibling, fragment, namespace,
}); });
} else { } else {
const node = createNode($newAsReal, currentContext, isSvg); const node = createNode($newAsReal, currentContext, namespace);
$newAsReal.target = node; $newAsReal.target = node;
remount(parentEl, $current, currentContext, node, nextSibling); remount(parentEl, $current, currentContext, node, nextSibling);
@ -226,8 +230,10 @@ function renderWithVirtual<T extends VirtualElement | undefined>(
insertBefore(parentEl, currentTarget, nextSibling); insertBefore(parentEl, currentTarget, nextSibling);
} }
updateAttributes($current, $newAsTag, currentTarget as DOMElement, isSvg); updateAttributes($current, $newAsTag, currentTarget as DOMElement, namespace);
renderChildren($current, $newAsTag, currentContext, currentTarget as DOMElement, undefined, undefined, isSvg); renderChildren(
$current, $newAsTag, currentContext, currentTarget as DOMElement, undefined, undefined, namespace,
);
} }
} }
} }
@ -290,7 +296,7 @@ function mountChildren(
options: { options: {
nextSibling?: ChildNode; nextSibling?: ChildNode;
fragment?: DocumentFragment; fragment?: DocumentFragment;
isSvg?: true; namespace?: string;
}, },
) { ) {
const { children } = $element; const { children } = $element;
@ -311,7 +317,7 @@ function unmountChildren(
} }
} }
function createNode($element: VirtualElementReal, currentContext: CurrentContext, isSvg?: true): Node { function createNode($element: VirtualElementReal, currentContext: CurrentContext, namespace = HTML_NAMESPACE): Node {
if ($element.type === VirtualType.Empty) { if ($element.type === VirtualType.Empty) {
return document.createTextNode(''); return document.createTextNode('');
} }
@ -321,7 +327,7 @@ function createNode($element: VirtualElementReal, currentContext: CurrentContext
} }
const { tag, props, children } = $element; const { tag, props, children } = $element;
const element = isSvg ? document.createElementNS('http://www.w3.org/2000/svg', tag) : document.createElement(tag); const element = document.createElementNS(namespace, tag) as DOMElement;
processControlled(tag, props); processControlled(tag, props);
@ -330,7 +336,7 @@ function createNode($element: VirtualElementReal, currentContext: CurrentContext
if (!props.hasOwnProperty(key)) continue; if (!props.hasOwnProperty(key)) continue;
if (props[key] !== undefined) { if (props[key] !== undefined) {
setAttribute(element, key, props[key], isSvg); setAttribute(element, key, props[key], namespace);
} }
} }
@ -338,7 +344,7 @@ function createNode($element: VirtualElementReal, currentContext: CurrentContext
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, currentContext, i, { isSvg }); const $renderedChild = renderWithVirtual(element, undefined, $child, $element, currentContext, i, { namespace });
if ($renderedChild !== $child) { if ($renderedChild !== $child) {
children[i] = $renderedChild; children[i] = $renderedChild;
} }
@ -424,7 +430,7 @@ function renderChildren(
currentEl: DOMElement, currentEl: DOMElement,
nextSibling?: ChildNode, nextSibling?: ChildNode,
forceMoveToEnd = false, forceMoveToEnd = false,
isSvg?: true, namespace?: string,
) { ) {
if (DEBUG) { if (DEBUG) {
DEBUG_checkKeyUniqueness($new.children); DEBUG_checkKeyUniqueness($new.children);
@ -456,7 +462,7 @@ function renderChildren(
$new, $new,
currentContext, currentContext,
i, i,
i >= currentChildrenLength ? { fragment, isSvg } : { nextSibling, forceMoveToEnd, isSvg }, i >= currentChildrenLength ? { fragment, namespace } : { nextSibling, forceMoveToEnd, namespace },
); );
if ($renderedChild && $renderedChild !== newChildren[i]) { if ($renderedChild && $renderedChild !== newChildren[i]) {
@ -691,7 +697,9 @@ function processUncontrolledOnMount(element: DOMElement, props: AnyLiteral) {
} }
} }
function updateAttributes($current: VirtualElementTag, $new: VirtualElementTag, element: DOMElement, isSvg?: true) { function updateAttributes(
$current: VirtualElementTag, $new: VirtualElementTag, element: DOMElement, namespace?: string,
) {
processControlled(element.tagName, $new.props); processControlled(element.tagName, $new.props);
const currentEntries = Object.entries($current.props); const currentEntries = Object.entries($current.props);
@ -715,14 +723,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, isSvg); setAttribute(element, key, newValue, namespace);
} }
} }
} }
function setAttribute(element: DOMElement, key: string, value: any, isSvg?: true) { function setAttribute(element: DOMElement, key: string, value: any, namespace?: string) {
if (key === 'className') { if (key === 'className') {
updateClassName(element, value, isSvg); updateClassName(element, value, namespace);
} else if (key === 'value') { } else if (key === 'value') {
const inputEl = element as HTMLInputElement; const inputEl = element as HTMLInputElement;
@ -749,7 +757,9 @@ function setAttribute(element: DOMElement, key: string, value: any, isSvg?: true
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 (isSvg || key.startsWith('data-') || key.startsWith('aria-') || HTML_ATTRIBUTES.has(key)) { } else if (
namespace === SVG_NAMESPACE || 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;
@ -772,8 +782,8 @@ function removeAttribute(element: DOMElement, key: string, value: any) {
} }
} }
function updateClassName(element: DOMElement, value: string, isSvg?: true) { function updateClassName(element: DOMElement, value: string, namespace?: string) {
if (isSvg) { if (namespace === SVG_NAMESPACE) {
element.setAttribute('class', value); element.setAttribute('class', value);
return; return;
} }

View File

@ -0,0 +1,12 @@
import type { VirtualElement } from '../../lib/teact/teact';
import TeactDOM from '../../lib/teact/teact-dom';
export default function jsxToHtml(jsx: VirtualElement) {
const fragment = document.createElement('div');
TeactDOM.render(jsx, fragment);
const children = Array.from(fragment.children);
TeactDOM.render(undefined, fragment);
return children;
}

View File

@ -1,32 +0,0 @@
import generateUniqueId from './generateUniqueId';
export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
const CONTAINER = document.createElementNS(SVG_NAMESPACE, 'svg');
CONTAINER.setAttribute('width', '0');
CONTAINER.setAttribute('height', '0');
CONTAINER.setAttribute('viewBox', '0 0 1 1');
CONTAINER.classList.add('svg-definitions');
document.body.appendChild(CONTAINER);
const DEFS = document.createElementNS(SVG_NAMESPACE, 'defs');
CONTAINER.appendChild(DEFS);
const DEFINITION_MAP = new Map<string, SVGElement>();
export function addSvgDefinition(element: SVGElement, id?: string) {
id ??= generateUniqueId();
element.id = id;
DEFS.appendChild(element);
DEFINITION_MAP.set(element.id, element);
return id;
}
export function removeSvgDefinition(id: string) {
const element = DEFINITION_MAP.get(id);
if (element) {
element.remove();
DEFINITION_MAP.delete(id);
}
}