'use client';
import React from 'react';
import { observer } from 'mobx-react-lite';
import { Image, Group, Rect, Path } from 'react-konva';
import Quill from 'quill';
import Konva from 'konva';
import * as mobx from 'mobx';
import { reaction, autorun } from 'mobx';
import { flags } from '../utils/flags';
import { applyFilter } from './apply-filters';
import { getDir, getLineHeight, useFontLoader, usePrevious } from './text-element';
import { useColor } from './use-color';
import { detectSize, htmlToCanvas, isContentWrapping } from '../utils/html2canvas';
import { resetStyleContent } from '../utils/reset-style';
import { useFadeIn } from './use-fadein';
import { Html } from 'react-konva-utils';
import styled from '../utils/styled';
import { incrementLoader, triggerLoadError } from '../utils/loader';
import { removeTags, sanitizeHtml } from '../utils/text';
import * as fonts from '../utils/fonts';
import { isTouchDevice } from '../utils/screen';
import { getLimitedFontSize } from './text-element/max-font-size';
import { getCurvePath } from './text-element';
import { useDelayer } from './use-delayer';
import { getOptimalCaretColor } from './text-element/caret-color';
import { StoreType } from '../model/store';
import { TextElementType } from '../model/text-model';
function withAlpha(color: string, opacity: number): string {
if (opacity === 1) return color;
const rgba = Konva.Util.colorToRGBA(color);
return rgba ? `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a * opacity})` : color;
}
function collapseEmptyParagraph(html: string): string {
const trimmed = (html || '').trim();
if (trimmed === '
' || trimmed === '') return '';
if (trimmed.startsWith('') && trimmed.endsWith('
')) {
const inner = trimmed.slice(3, -4);
if (!/<\/?(p|div|h[1-6]|ul|ol|li|table|thead|tbody|tr|td|blockquote)\b/i.test(inner)) {
return inner;
}
}
return html;
}
// MobX-observable Quill state (shared across editor instances)
export const quillRef = mobx.observable({
enabled: false,
currentFormat: {} as Record,
editor: mobx.observable.object({ instance: null as any }, {}, { deep: false }),
});
// Styled div wrapping Quill editor
const QuillContainer = styled('div', React.forwardRef)`
.ql-editor {
outline: none;
}
.ql-clipboard {
pointer-events: none;
}
${resetStyleContent}
strong {
font-weight: 700;
}
.ql-direction-rtl {
direction: rtl;
}
`;
// Default Quill formats
let quillFormats = ['bold', 'color', 'font', 'italic', 'size', 'strike', 'underline', 'indent', 'list', 'direction'];
export const setQuillFormats = (formats: string[]) => { quillFormats = formats; };
export const createQuill = (node: any) =>
new Quill(node, {
modules: { toolbar: false, keyboard: false, clipboard: { matchVisual: false } },
formats: quillFormats,
theme: undefined as any,
});
export const setQuillContent = (quill: any, html: string) => {
const delta = quill.clipboard.convert(
``,
);
quill.setContents(delta);
quill.history.clear();
};
export function setCursorFromCoords(quill: any, coords: { x: number; y: number } | null): void {
if (!quill || !coords) return;
const { x, y } = coords;
try {
let range: Range | null = null;
if ((document as any).caretRangeFromPoint) {
range = (document as any).caretRangeFromPoint(x, y);
} else if ((document as any).caretPositionFromPoint) {
const pos = (document as any).caretPositionFromPoint(x, y);
if (pos) { range = document.createRange(); range.setStart(pos.offsetNode, pos.offset); }
}
if (range) {
const blot = Quill.find(range.startContainer, true);
if (blot) {
const idx = (blot as any).offset(quill.scroll) + range.startOffset;
quill.setSelection(idx, 0, 'api');
return;
}
}
} catch (_) {}
quill.setSelection(0, 0, 'api');
}
// Quill inline editor component
const QuillEditor = ({
html,
onBlur,
onChange,
element,
clickCoords,
}: {
html: string;
onBlur: () => void;
onChange: (html: string) => void;
element: any;
clickCoords: { x: number; y: number } | null;
}) => {
const containerRef = React.useRef(null);
React.useEffect(() => {
if (!containerRef.current) return;
const quill = new Quill(containerRef.current, {
modules: { toolbar: false, keyboard: false, clipboard: { matchVisual: false } },
formats: quillFormats,
theme: undefined as any,
});
mobx.runInAction(() => { quillRef.editor.instance = quill; });
(window as any).__raeditorQuill = quill;
quill.on('text-change', () => {
if (quill.getSelection()) {
mobx.runInAction(() => { quillRef.currentFormat = quill.getFormat(quill.getSelection()!); });
}
setTimeout(() => {
const editorEl = containerRef.current?.childNodes[0] as HTMLElement;
if (!editorEl) return;
onChange(collapseEmptyParagraph(editorEl.innerHTML));
}, 10);
});
setQuillContent(quill, html);
if (clickCoords) {
setCursorFromCoords(quill, clickCoords);
} else {
quill.setSelection(0, 0, 'api');
}
quill.on('selection-change', (range: any) => {
if (range) {
mobx.runInAction(() => { quillRef.currentFormat = quill.getFormat(quill.getSelection()!); });
}
});
(containerRef.current.childNodes[0] as HTMLElement).addEventListener('blur', (e: any) => {
if (e.relatedTarget?.classList.contains('ql-clipboard')) return;
const isColorPicker = !!(e.relatedTarget && (e.relatedTarget as HTMLElement).closest?.('.sketch-picker'));
if (!isColorPicker) onBlur();
});
return () => {
mobx.runInAction(() => { quillRef.editor.instance = null; quillRef.currentFormat = {}; });
delete (window as any).__raeditorQuill;
};
}, []);
// Sync text changes from store
React.useEffect(() => {
return reaction(
() => element.text,
() => {
const quill = quillRef.editor.instance;
if (!quill) return;
const sel = quill.getSelection();
const editorEl = containerRef.current?.childNodes[0] as HTMLElement;
if (collapseEmptyParagraph(editorEl?.innerHTML) === element.text) return;
setQuillContent(quill, html);
if (sel) {
quill.setSelection(sel.index, sel.length);
mobx.runInAction(() => { quillRef.currentFormat = quill.getFormat(quill.getSelection()!); });
}
},
{ fireImmediately: true },
);
}, []);
// Blur on window blur / outside tap
React.useEffect(() => {
const onWindowBlur = () => onBlur();
const onTouchStart = (e: TouchEvent) => {
if (!containerRef.current?.contains(e.target as Node)) onBlur();
};
window.addEventListener('blur', onWindowBlur);
window.addEventListener('touchstart', onTouchStart);
return () => {
window.removeEventListener('blur', onWindowBlur);
window.removeEventListener('touchstart', onTouchStart);
};
}, []);
const fillStyle: React.CSSProperties = { color: element.fill };
if (element.fill.indexOf('gradient') >= 0) {
Object.assign(fillStyle, {
backgroundColor: element.fill,
backgroundImage: element.fill,
backgroundSize: '100% 100%',
backgroundRepeat: 'repeat',
WebkitBackgroundClip: 'text',
MozBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
MozTextFillColor: 'transparent',
});
}
const caretColor = getOptimalCaretColor(element);
return React.createElement(QuillContainer, {
ref: containerRef,
style: {
...fillStyle,
fontSize: element.fontSize,
fontWeight: element.fontWeight,
textTransform: element.textTransform,
width: element.a.width,
fontFamily: `"${element.fontFamily}"`,
lineHeight: element.lineHeight,
letterSpacing: `${element.letterSpacing * element.fontSize}px`,
textAlign: element.align,
opacity: Math.max(element.a.opacity, 0.2),
textShadow: element.shadowEnabled
? `${element.shadowOffsetX}px ${element.shadowOffsetY}px ${element.shadowBlur}px ${withAlpha(element.shadowColor, element.shadowOpacity ?? 1)}`
: undefined,
caretColor,
},
dir: getDir(removeTags(element.text)),
});
};
function isCanvasBlank(canvas: HTMLCanvasElement): boolean {
const data = canvas.getContext('2d')!.getImageData(0, 0, canvas.width, canvas.height).data;
for (let i = 0; i < data.length; i += 4) {
if (data[i + 3] !== 0) return false;
}
return true;
}
function buildHtmlContent(element: any, { fontFamily = '', color = 'black' } = {}): string {
let colorStyle = `color: ${color || element.fill}`;
if (element.fill.indexOf('gradient') >= 0) {
colorStyle = `
background-color: ${color};
background-image: ${element.fill};
background-size: 100% 100%;
background-repeat: repeat;
-webkit-background-clip: text;
-moz-background-clip: text;
-webkit-text-fill-color: transparent;
-moz-text-fill-color: transparent;
`;
}
const rules = [
'white-space: pre-wrap',
'word-break: break-word',
`width: ${Math.round(element.width || 100)}px`,
colorStyle,
`font-size: ${element.fontSize}px`,
`font-family: '${fontFamily}'`,
`text-align: ${element.align}`,
`text-transform: ${element.textTransform}`,
element.textDecoration
? `text-decoration: ${element.textDecoration}; text-decoration-color: ${color || element.fill}; text-decoration-layer: over`
: '',
element.lineHeight ? `line-height: ${element.lineHeight}` : '',
element.letterSpacing ? `letter-spacing: ${element.letterSpacing * element.fontSize}px` : '',
element.fontStyle ? `font-style: ${element.fontStyle}` : '',
element.fontWeight ? `font-weight: ${element.fontWeight}` : '',
element.strokeWidth ? `-webkit-text-stroke: ${element.strokeWidth}px ${element.stroke}` : '',
element.strokeWidth ? 'paint-order: stroke fill' : '',
].filter(Boolean).join('; ');
if (element.curveEnabled) {
// Compute SVG text on path for curve mode
const tmpPath = new Konva.TextPath({
data: getCurvePath(element.a ? element.a.width : element.width, element.a ? element.a.height : element.height, element.curvePower, element.fontSize),
align: 'center',
textBaseline: 'middle',
text: removeTags(element.text),
fontSize: element.fontSize,
fontFamily: element.fontFamily,
fontWeight: element.fontWeight,
fontStyle: element.fontStyle,
letterSpacing: element.letterSpacing * (element.a ? element.a.fontSize : element.fontSize),
fill: element.fill,
});
const svgH = tmpPath.getSelfRect().height;
tmpPath.destroy();
const svgW = Math.round(element.width || 100);
return `
`;
}
return `${sanitizeHtml(element.text).replace(/\n/g, '')}
`;
}
// Safari needs extra render attempts
const isSafari = /^((?!chrome|android).)*safari/i.test(
typeof navigator !== 'undefined' ? navigator.userAgent : '',
);
type ShapeProps = {
store: StoreType;
element: TextElementType;
onClick?: Function;
};
export const HTMLElement = observer(({ element, store }: ShapeProps) => {
const imageRef = React.useRef(null);
const [canvasImage, setCanvasImage] = React.useState(undefined);
const [isTransforming, setIsTransforming] = React.useState(false);
const [isRerendering, setIsRerendering] = React.useState(false);
const prevHeightRef = React.useRef((element as any).height);
const isSelected = (store as any).selectedShapes.indexOf(element) >= 0 && (element as any).selectable;
const padding = (element as any).fontSize / 3;
const { textVerticalResizeEnabled } = flags;
const prevFontFamily = usePrevious((element as any).fontFamily);
const [fontLoaded] = useFontLoader(store as any, (element as any).fontFamily);
const isEditMode = (element as any)._editModeEnabled;
useFadeIn(imageRef);
// Use fallback font during loading
const fontFamily = fontLoaded
? (element as any).fontFamily
: prevFontFamily !== (element as any).fontFamily
? prevFontFamily
: 'Arial';
const fillColor = useColor(element as any).fill;
const htmlContent = buildHtmlContent(element as any, { fontFamily: fontFamily || '', color: fillColor });
// Compute intrinsic size from HTML
let { width: measuredW, height: measuredH } = React.useMemo(
() => detectSize(htmlContent),
[htmlContent, (element as any).width, fontLoaded],
);
// Auto-resize height
React.useEffect(() => {
if (!fontLoaded) return;
if (!(element as any).height) {
(store as any).history.ignore(() => { (element as any).set({ height: measuredH }); });
return;
}
const { textOverflow } = flags;
if (textOverflow === 'change-font-size' && !isTransforming) {
// Find font size that fits
let fs = (element as any).fontSize;
for (let i = 1; i < 50; i++) {
const testHtml = buildHtmlContent({ ...(element as any).toJSON(), fontSize: fs }, { fontFamily: (element as any).fontFamily });
const { height: testH } = detectSize(testHtml);
if (
(!((element as any).height && testH > (element as any).height)) &&
!(flags.textSplitAllowed === false && !(element as any).curveEnabled && isContentWrapping({ html: testHtml }))
) break;
fs -= 0.5;
}
if (fs !== (element as any).fontSize) {
(store as any).history.ignore(() => { (element as any).set({ fontSize: fs }); });
} else if ((element as any).height !== measuredH) {
if (textVerticalResizeEnabled && (element as any).height < measuredH) {
(store as any).history.ignore(() => { (element as any).set({ height: measuredH }); });
} else if (!textVerticalResizeEnabled) {
(store as any).history.ignore(() => { (element as any).set({ height: measuredH }); });
}
}
} else if (textOverflow === 'resize') {
if (textVerticalResizeEnabled && (element as any).height < measuredH && !isTransforming) {
(store as any).history.ignore(() => { (element as any).set({ height: measuredH }); });
} else if (!textVerticalResizeEnabled && (element as any).height !== measuredH) {
(store as any).history.ignore(() => { (element as any).set({ height: measuredH }); });
}
}
});
// Memoized canvas renderer with arg dedup
const memoRenderer = React.useMemo(() => {
const cache: { lastArgs: any; lastResult: any } = { lastArgs: null, lastResult: null };
return async (args: any) => {
if (cache.lastArgs && cache.lastResult && JSON.stringify(cache.lastArgs) === JSON.stringify(args)) {
return cache.lastResult;
}
cache.lastResult = await htmlToCanvas(args);
cache.lastArgs = { ...args };
return cache.lastResult;
};
}, []);
const renderCountRef = React.useRef(0);
const finishLoaderRef = React.useRef<(() => void) | null>(null);
// Render HTML to canvas when content changes
React.useEffect(() => {
if (isTransforming || isEditMode) return;
(async () => {
renderCountRef.current++;
const token = renderCountRef.current;
let finish = incrementLoader(`text ${(element as any).id} ${token}`);
finishLoaderRef.current?.();
finishLoaderRef.current = finish;
setIsRerendering(true);
let canvas: HTMLCanvasElement | null = null;
const attempts = isSafari ? 5 : 1;
for (let attempt = 0; attempt < attempts; attempt++) {
const renderer = attempt > 0 ? memoRenderer : htmlToCanvas;
try {
canvas = await renderer({
skipCache: attempt > 0,
html: htmlContent,
width: (element as any).width || 1,
height: (element as any).height || measuredH || 1,
fontFamily,
padding,
pixelRatio: (store as any)._elementsPixelRatio,
font:
(store as any).fonts.find((f: any) => f.fontFamily === fontFamily) ||
fonts.globalFonts.find((f: any) => f.fontFamily === fontFamily),
});
if (token !== renderCountRef.current) return;
if (isSafari && canvas && isCanvasBlank(canvas)) {
await new Promise((r) => setTimeout(r, 50 * (attempt + 1)));
continue;
}
break;
} catch (e) {
console.error(e);
triggerLoadError(`Error rendering rich text with id ${(element as any).id}`);
break;
}
}
if (canvas) {
setCanvasImage(canvas);
} else if (finish) {
finish();
finish = null;
} else {
console.error('Finish function is called twice!');
}
setIsRerendering(false);
})();
}, [htmlContent, isTransforming, measuredH, isEditMode, fontFamily, (element as any).height, (store as any)._elementsPixelRatio, fontLoaded]);
const [isRerenderingDelayed] = useDelayer(isRerendering, 300);
const [isTransformingDelayed] = useDelayer(isTransforming, 300, true);
const isLoadingState = isTransformingDelayed || isRerenderingDelayed;
// Apply filters
React.useEffect(() => {
if (!isLoadingState) {
return autorun(() => { applyFilter(imageRef.current, element as any); });
}
imageRef.current?.clearCache();
}, [canvasImage, isLoadingState, (element as any).shadowColor, (element as any).shadowOffsetX,
(element as any).shadowOffsetY, (element as any).shadowOpacity]);
// Release loader when canvas ready
React.useEffect(() => {
if (canvasImage && !isRerendering && finishLoaderRef.current) {
finishLoaderRef.current();
finishLoaderRef.current = null;
}
}, [canvasImage, isRerendering]);
// Cleanup loader on unmount
React.useEffect(() => () => { finishLoaderRef.current?.(); }, []);
// Curve width auto-adjust
React.useLayoutEffect(() => {
if (!fontLoaded || !(element as any).curveEnabled) return;
const textPathNode = new Konva.TextPath({
data: getCurvePath((element as any).a.width, (element as any).a.height, (element as any).curvePower, (element as any).a.fontSize),
text: removeTags((element as any).text),
letterSpacing: (element as any).letterSpacing * (element as any).a.fontSize,
fontSize: (element as any).a.fontSize,
fontFamily: (element as any).fontFamily,
fontWeight: (element as any).fontWeight,
fontStyle: (element as any).fontStyle,
align: 'center',
textBaseline: 'middle',
fill: (element as any).fill,
});
const newWidth = textPathNode.getSelfRect().width;
textPathNode.destroy();
if (newWidth) {
const diff = newWidth - (element as any).a.width;
const angle = (element as any).a.rotation * Math.PI / 180;
(element as any).set({
width: newWidth,
x: (element as any).a.x - (diff / 2) * Math.cos(angle),
y: (element as any).a.y - (diff / 2) * Math.sin(angle),
});
}
}, [fontLoaded, (element as any).curveEnabled, (element as any).curvePower, (element as any).text,
(element as any).fontSize, (element as any).fontFamily, (element as any).fontWeight, (element as any).fontStyle, (element as any).letterSpacing]);
// Auto-delete empty, non-placeholder elements when deselected
React.useEffect(() => {
if (!isSelected && removeTags((element as any).text) === '' && (element as any).removable && !(element as any).placeholder) {
(store as any).deleteElements([(element as any).id]);
}
}, [isSelected]);
// Vertical alignment offset
let verticalOffset = 0;
if ((element as any).verticalAlign === 'middle') verticalOffset = ((element as any).height - measuredH) / 2;
if ((element as any).verticalAlign === 'bottom') verticalOffset = (element as any).height - measuredH;
const lineH = getLineHeight({ fontLoaded, fontFamily: (element as any).fontFamily, fontSize: (element as any).fontSize, lineHeight: (element as any).lineHeight });
const isTouch = isTouchDevice();
const curvePath = getCurvePath((element as any).a.width, measuredH, (element as any).curvePower, (element as any).fontSize);
const prevTransformPos = React.useRef<{ x: number; y: number } | null>(null);
const clickCoordsRef = React.useRef<{ x: number; y: number } | null>(null);
const hasStrokeInEditMode = isEditMode && (element as any).strokeWidth > 0 && !(element as any).curveEnabled;
return React.createElement(
React.Fragment,
null,
// Background rect
React.createElement(Rect, {
x: (element as any).a.x,
y: (element as any).a.y,
offsetX: (element as any).backgroundPadding * ((element as any).fontSize * lineH * 0.5),
offsetY: (element as any).backgroundPadding * ((element as any).fontSize * lineH * 0.5),
rotation: (element as any).a.rotation,
hideInExport: !(element as any).showInExport,
listening: false,
visible: (element as any).backgroundEnabled,
opacity: (element as any).backgroundOpacity * (element as any).a.opacity,
fill: (element as any).backgroundColor,
width: (element as any).a.width + (element as any).backgroundPadding * ((element as any).fontSize * lineH),
height: (element as any).a.height + (element as any).backgroundPadding * ((element as any).fontSize * lineH),
cornerRadius: (element as any).backgroundCornerRadius * ((element as any).fontSize * lineH * 0.5),
}),
// Invisible curve path (debug)
React.createElement(Path, {
x: (element as any).a.x,
y: (element as any).a.y,
rotation: (element as any).a.rotation,
data: curvePath,
stroke: 'red',
strokeWidth: 1,
visible: false,
}),
// Hit/drag rect
React.createElement(Rect, {
ref: imageRef,
name: 'element',
x: (element as any).a.x,
y: (element as any).a.y,
listening: (element as any).selectable,
rotation: (element as any).a.rotation,
width: (element as any).a.width,
height: (element as any).a.height,
visible: !isLoadingState,
draggable: isTouch ? (element as any).draggable && isSelected : (element as any).draggable,
preventDefault: !isTouch || isSelected,
opacity: isEditMode ? 0 : (element as any).a.opacity,
hideInExport: !(element as any).showInExport,
onDragMove: (e: any) => { (element as any).set({ x: e.target.x(), y: e.target.y() }); },
onDragEnd: (e: any) => { (element as any).set({ x: e.target.x(), y: e.target.y() }); },
id: (element as any).id,
onDblClick: (e: any) => {
if ((element as any).contentEditable) {
clickCoordsRef.current = { x: e.evt.clientX, y: e.evt.clientY };
(element as any).toggleEditMode(true);
}
},
onDblTap: (e: any) => {
if ((element as any).contentEditable) {
const touch = e.evt.changedTouches?.[0];
clickCoordsRef.current = touch ? { x: touch.clientX, y: touch.clientY } : null;
(element as any).toggleEditMode(true);
}
},
onTransformStart: (e: any) => {
setIsTransforming(true);
prevHeightRef.current = (element as any).height;
},
onTransform: (e: any) => {
const node = e.target;
const stage = node.getStage();
const transformer = stage?.findOne('Transformer');
const anchor = transformer?.getActiveAnchor() || '';
const isHorizontal = anchor === 'middle-left' || anchor === 'middle-right';
const isVertical = anchor === 'top-center' || anchor === 'bottom-center';
const scaleX = node.scaleX();
if (isHorizontal) {
const newW = node.width() * scaleX;
const minW = (element as any).fontSize;
let finalW = newW;
if (newW < minW) {
finalW = minW;
if (prevTransformPos.current) node.position(prevTransformPos.current);
}
node.width(finalW);
node.scaleX(1);
if (textVerticalResizeEnabled) {
const targetH = Math.max(measuredH, prevHeightRef.current);
(element as any).set({ height: targetH });
}
(element as any).set({ width: node.width(), x: node.x(), y: node.y() });
} else if (isVertical) {
const minH = flags.textOverflow === 'resize' ? measuredH : (element as any).lineHeight * (element as any).fontSize;
const newH = Math.max(minH, e.target.height() * e.target.scaleY());
node.scaleY(1);
(element as any).set({ x: node.x(), y: node.y(), height: newH, rotation: node.rotation() });
} else {
node.scaleX(1);
node.scaleY(1);
(element as any).set({
fontSize: (element as any).fontSize * scaleX,
letterSpacing: (element as any).letterSpacing,
width: node.width() * scaleX,
x: node.x(), y: node.y(),
rotation: node.rotation(),
height: node.height() * scaleX,
});
}
prevTransformPos.current = e.target.position();
},
onTransformEnd: (e: any) => {
setIsTransforming(false);
setIsRerendering(true);
const node = e.target;
const scaleX = node.scaleX();
node.scaleX(1);
node.scaleY(1);
(element as any).set({
fontSize: (element as any).fontSize * scaleX,
width: node.width() * scaleX,
x: node.x(), y: node.y(),
rotation: node.rotation(),
shadowBlur: (element as any).shadowBlur * scaleX,
shadowOffsetX: (element as any).shadowOffsetX * scaleX,
shadowOffsetY: (element as any).shadowOffsetY * scaleX,
strokeWidth: (element as any).strokeWidth * scaleX,
});
},
}),
// Rendered canvas image
React.createElement(Image as any, {
ref: imageRef,
image: canvasImage,
x: (element as any).a.x,
y: (element as any).a.y,
offsetX: padding,
offsetY: padding - verticalOffset,
listening: false,
rotation: (element as any).a.rotation,
width: (element as any).a.width + 2 * padding,
height: ((element as any).a.width + 2 * padding) * ((canvasImage?.height || 0) / (canvasImage?.width || 1)),
visible: !isLoadingState && !isEditMode,
opacity: (element as any).a.opacity,
shadowEnabled: (element as any).shadowEnabled,
shadowBlur: (element as any).shadowBlur,
shadowOffsetX: (element as any).shadowOffsetX,
shadowOffsetY: (element as any).shadowOffsetY,
shadowColor: (element as any).shadowColor,
shadowOpacity: (element as any).shadowOpacity,
hideInExport: !(element as any).showInExport,
editModeEnabled: (element as any)._editModeEnabled || isTransforming || isLoadingState,
}),
// Loading/stroke preview overlay
(isLoadingState || hasStrokeInEditMode) && React.createElement(
Group,
{ x: (element as any).a.x, y: (element as any).a.y, rotation: (element as any).a.rotation, offsetY: -verticalOffset },
React.createElement(Html, { divProps: { style: { pointerEvents: 'none' } } },
React.createElement(QuillContainer, {
dangerouslySetInnerHTML: { __html: htmlContent },
style: {
pointerEvents: 'none',
opacity: (element as any).a.opacity,
textShadow: (element as any).shadowEnabled
? `${(element as any).shadowOffsetX}px ${(element as any).shadowOffsetY}px ${(element as any).shadowBlur}px ${withAlpha((element as any).shadowColor, (element as any).shadowOpacity ?? 1)}`
: undefined,
},
}),
),
),
// Edit mode Quill overlay
isEditMode && React.createElement(
Group,
{ x: (element as any).a.x, y: (element as any).a.y, rotation: (element as any).a.rotation, offsetY: -verticalOffset },
React.createElement(Html, null,
React.createElement(QuillEditor, {
html: htmlContent,
element,
onChange: (text: string) => {
const newFontSize = getLimitedFontSize({
oldText: removeTags((element as any).text),
newText: removeTags(text),
element: element as any,
});
(element as any).set({ text, fontSize: newFontSize });
},
onBlur: () => {
(element as any).toggleEditMode(false);
setIsRerendering(true);
},
clickCoords: clickCoordsRef.current,
}),
),
),
);
}) as ((props: ShapeProps) => React.JSX.Element) & { displayName: string };
HTMLElement.displayName = 'HTMLElement';