SVG: Allow setting filters using JSX (#5449)
This commit is contained in:
parent
af472d0fd4
commit
8dc935d0b8
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
45
src/components/main/SvgController.tsx
Normal file
45
src/components/main/SvgController.tsx
Normal 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();
|
||||||
|
}
|
||||||
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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';
|
||||||
|
|
||||||
|
|||||||
@ -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() {
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/util/element/jsxToHtml.ts
Normal file
12
src/util/element/jsxToHtml.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user