/** @jsxImportSource react */ import { RenderingContext } from "../ui/RenderingContext"; import { ResizeManager } from "../ui/ResizeManager"; import { VDOM, Widget } from "../ui/Widget"; import { addEventListenerWithOptions } from "../util/addEventListenerWithOptions"; import { BoundedObject, BoundedObjectConfig, BoundedObjectInstance, SvgRenderingContext } from "./BoundedObject"; import { Rect } from "./util/Rect"; interface SvgInstance extends BoundedObjectInstance { state: { size: { width: number; height: number; }; }; clipRects?: Record; clipRectId?: number; } export interface SvgConfig extends BoundedObjectConfig { /** Set to `true` to automatically calculate width based on the measured height and `aspectRatio`. */ autoWidth?: boolean; /** Set to `true` to automatically calculate height based on the measured width and `aspectRatio`. */ autoHeight?: boolean; /** * Aspect ratio of the the SVG element. Default value is `1.618`. * This value doesn't have any effect unless `autoWidth` or `autoHeight` is set. */ aspectRatio?: number; /** Base CSS class to be applied to the element. Defaults to `svg`. */ baseClass?: string; onMouseDown?(e: MouseEvent, instance: SvgInstance): void; onMouseUp?(e: MouseEvent, instance: SvgInstance): void; onMouseMove?(e: MouseEvent, instance: SvgInstance): void; onTouchStart?(e: TouchEvent, instance: SvgInstance): void; onTouchMove?(e: TouchEvent, instance: SvgInstance): void; onTouchEnd?(e: TouchEvent, instance: SvgInstance): void; onWheel?(e: WheelEvent, instance: SvgInstance): void; onWheelActive?(e: WheelEvent, instance: SvgInstance): void; } export class Svg extends BoundedObject { declare autoWidth?: boolean; declare autoHeight?: boolean; declare aspectRatio?: number; declare onWheelActive?: (e: WheelEvent, instance: SvgInstance) => void; constructor(config?: SvgConfig) { super(config); } initState(context: RenderingContext, instance: SvgInstance) { const size = { width: 0, height: 0, }; instance.state = { size }; } explore(context: SvgRenderingContext, instance: SvgInstance) { context.push("inSvg", true); super.explore(context, instance); } exploreCleanup(context: SvgRenderingContext, instance: SvgInstance) { context.pop("inSvg"); } prepare(context: SvgRenderingContext, instance: SvgInstance) { const size = instance.state.size; context.parentRect = new Rect({ l: 0, t: 0, r: size.width, b: size.height, }); instance.clipRects = {}; instance.clipRectId = 0; context.push("addClipRect", (rect: Rect) => { const id = `clip-${instance.id}-${++instance.clipRectId!}`; instance.clipRects![id] = rect; return id; }); context.push("inSvg", true); super.prepare(context, instance); } prepareCleanup(context: SvgRenderingContext, instance: SvgInstance) { super.prepareCleanup(context, instance); context.pop("addClipRect"); context.pop("inSvg"); } render(context: RenderingContext, instance: SvgInstance, key: string) { let eventHandlers = instance.getJsxEventProps(); if (eventHandlers) { delete eventHandlers["onWheelActive"]; } return ( {this.renderChildren(context, instance)} ); } } Svg.prototype.anchors = "0 1 1 0"; Svg.prototype.baseClass = "svg"; Svg.prototype.autoWidth = false; Svg.prototype.autoHeight = false; Svg.prototype.aspectRatio = 1.618; function sameSize(a: { width: number; height: number } | null, b: { width: number; height: number } | null): boolean { if (!a || !b) return false; return a.width == b.width && a.height == b.height; } interface SvgComponentProps { instance: SvgInstance; data: any; size: { width: number; height: number }; children: any; eventHandlers?: any; options?: any; } class SvgComponent extends VDOM.Component { el?: HTMLDivElement; offResize?: () => void; offWheelActive?: () => void; render() { const { instance, data, size, children, eventHandlers } = this.props; const { widget } = instance; const defs: any[] = []; for (const k in (instance as any).clipRects) { let cr = (instance as any).clipRects[k]; defs.push( , ); } let style = data.style; if ((widget as any).autoHeight) style = { ...style, height: `${size.height}px`, }; if ((widget as any).autoWidth) style = { ...style, width: `${size.width}px`, }; //parent div is needed because clientWidth doesn't work on the svg element in FF return (
{ this.el = el as HTMLDivElement; }} className={data.classNames} style={style} {...eventHandlers} > {size.width > 0 && size.height > 0 && ( {defs} {children} )}
); } onResize() { const { instance } = this.props; const { widget } = this.props.instance; if (!this.el) return; let size = { width: this.el.clientWidth, height: this.el.clientHeight, }; if ((widget as any).autoHeight) size.height = size.width / (widget as any).aspectRatio; if ((widget as any).autoWidth) size.width = size.height * (widget as any).aspectRatio; if (!sameSize(instance.state.size, size)) instance.setState({ size: size, }); } componentDidMount() { if (!this.el) return; this.offResize = ResizeManager.trackElement(this.el, this.onResize.bind(this)); this.onResize(); if ((this.props.instance.widget as any).onWheelActive) { this.offWheelActive = addEventListenerWithOptions( this.el, "wheel", (event) => { const { instance } = this.props; instance.invoke("onWheelActive", event, instance); }, { passive: false }, ); } } componentDidUpdate() { this.onResize(); } componentWillUnmount() { this.offResize && this.offResize(); this.offWheelActive && this.offWheelActive(); } } Widget.alias("svg", Svg);