UNPKG

js-draw

Version:

Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript.

668 lines (667 loc) 25 kB
import EditorImage from './image/EditorImage'; import ToolController from './tools/ToolController'; import { EditorNotifier, ImageLoader } from './types'; import { HTMLPointerEventFilter, InputEvtType, PointerEvtType } from './inputEvents'; import Command from './commands/Command'; import UndoRedoHistory from './UndoRedoHistory'; import Viewport from './Viewport'; import { Point2, Vec2, Color4, Mat33, Rect2 } from '@js-draw/math'; import Display, { RenderingMode } from './rendering/Display'; import { SVGLoaderPlugin } from './SVGLoader/SVGLoader'; import Pointer from './Pointer'; import { EditorLocalization } from './localization'; import IconProvider from './toolbar/IconProvider'; import AbstractComponent from './components/AbstractComponent'; import { BackgroundType } from './components/BackgroundComponent'; import KeyboardShortcutManager from './shortcuts/KeyboardShortcutManager'; import KeyBinding from './shortcuts/KeyBinding'; import AbstractToolbar from './toolbar/AbstractToolbar'; import RenderablePathSpec from './rendering/RenderablePathSpec'; import { AboutDialogEntry } from './dialogs/makeAboutDialog'; import ReactiveValue, { MutableReactiveValue } from './util/ReactiveValue'; import { PenTypeRecord } from './toolbar/widgets/PenToolWidget'; import { ShowCustomFilePickerCallback } from './toolbar/widgets/components/makeFileInput'; /** * Provides settings to an instance of an editor. See the Editor {@link Editor.constructor}. * * ## Example * * [[include:doc-pages/inline-examples/settings-example-1.md]] */ export interface EditorSettings { /** Defaults to `RenderingMode.CanvasRenderer` */ renderingMode: RenderingMode; /** Uses a default English localization if a translation is not given. */ localization: Partial<EditorLocalization>; /** * `true` if touchpad/mousewheel scrolling should scroll the editor instead of the document. * This does not include pinch-zoom events. * Defaults to true. */ wheelEventsEnabled: boolean | 'only-if-focused'; /** Minimum zoom fraction (e.g. 0.5 → 50% zoom). Defaults to $2 \cdot 10^{-10}$. */ minZoom: number; /** Maximum zoom fraction (e.g. 2 → 200% zoom). Defaults to $1 \cdot 10^{12}$. */ maxZoom: number; /** * Overrides for keyboard shortcuts. For example, * ```ts * { * 'some.shortcut.id': [ KeyBinding.keyboardShortcutFromString('ctrl+a') ], * 'another.shortcut.id': [ ] * } * ``` * where shortcut IDs map to lists of associated keybindings. * * @see * - {@link KeyBinding} * - {@link KeyboardShortcutManager} */ keyboardShortcutOverrides: Record<string, Array<KeyBinding>>; /** * Provides a set of icons for the editor. * * See, for example, the `@js-draw/material-icons` package. */ iconProvider: IconProvider; /** * Additional messages to show in the "about" dialog. */ notices: AboutDialogEntry[]; /** * Information about the app/website js-draw is running within. This is shown * at the beginning of the about dialog. */ appInfo: { name: string; description?: string; version?: string; } | null; /** * Configures the default {@link PenTool} tools. * * **Example**: * [[include:doc-pages/inline-examples/editor-settings-polyline-pen.md]] */ pens: { /** * Additional pen types that can be selected in a toolbar. */ additionalPenTypes?: readonly Readonly<PenTypeRecord>[]; /** * Should return `true` if a pen type should be shown in the toolbar. * * @example * ```ts,runnable * import {Editor} from 'js-draw'; * const editor = new Editor(document.body, { * // Only allow selecting the polyline pen from the toolbar. * pens: { filterPenTypes: p => p.id === 'polyline-pen' }, * }); * editor.addToolbar(); * ``` * Notice that this setting only affects the toolbar GUI. */ filterPenTypes?: (penType: PenTypeRecord) => boolean; } | null; /** Configures the default {@link TextTool} control and text tool. */ text: { /** Fonts to show in the text UI. */ fonts?: string[]; } | null; /** Configures the default {@link InsertImageWidget} control. */ image: { /** * A custom callback to show an image picker. If given, this should return * a list of `File`s representing the images selected by the picker. * * If not given, the default file picker shown by a [file input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file) * is shown. */ showImagePicker?: ShowCustomFilePickerCallback; } | null; /** * Allows changing how js-draw interacts with the clipboard. * * **Note**: Even when a custom `clipboardApi` is specified, if a `ClipboardEvent` is available * (e.g. from when a user pastes with ctrl+v), the `ClipboardEvent` will be preferred. */ clipboardApi: { /** Called to read data to the clipboard. Keys in the result are MIME types. Values are the data associated with that type. */ read(): Promise<Map<string, Blob | string>>; /** Called to write data to the clipboard. Keys in `data` are MIME types. Values are the data associated with that type. */ write(data: Map<string, Blob | Promise<Blob> | string>): void | Promise<void>; } | null; svg: { /** Plugins that create custom components while loading with {@link Editor.loadFromSVG}. */ loaderPlugins?: SVGLoaderPlugin[]; } | null; } /** * The main entrypoint for the full editor. * * ## Example * To create an editor with a toolbar, * ```ts,runnable * import { Editor } from 'js-draw'; * * const editor = new Editor(document.body); * * const toolbar = editor.addToolbar(); * toolbar.addSaveButton(() => { * const saveData = editor.toSVG().outerHTML; * // Do something with saveData... * }); * ``` * * See also * * [`examples.md`](https://github.com/personalizedrefrigerator/js-draw/blob/main/docs/examples.md). */ export declare class Editor { private container; private renderingRegion; /** Manages drawing surfaces/{@link AbstractRenderer}s. */ display: Display; /** * Handles undo/redo. * * @example * ``` * const editor = new Editor(document.body); * * // Do something undoable. * // ... * * // Undo the last action * editor.history.undo(); * ``` */ history: UndoRedoHistory; /** * Data structure for adding/removing/querying objects in the image. * * @example * ```ts,runnable * import { Editor, Stroke, Path, Color4, pathToRenderable } from 'js-draw'; * const editor = new Editor(document.body); * * // Create a path. * const stroke = new Stroke([ * pathToRenderable(Path.fromString('M0,0 L100,100 L300,30 z'), { fill: Color4.red }), * ]); * const addComponentCommand = editor.image.addComponent(stroke); * * // Add the stroke to the editor * editor.dispatch(addComponentCommand); * ``` */ readonly image: EditorImage; /** * Allows transforming the view and querying information about * what is currently visible. */ readonly viewport: Viewport; /** @internal */ readonly localization: EditorLocalization; /** {@link EditorSettings.iconProvider} */ readonly icons: IconProvider; /** * Manages and allows overriding of keyboard shortcuts. * * @internal */ readonly shortcuts: KeyboardShortcutManager; /** * Controls the list of tools. See * [the custom tool example](https://github.com/personalizedrefrigerator/js-draw/tree/main/docs/examples/example-custom-tools) * for more. */ readonly toolController: ToolController; /** * Global event dispatcher/subscriber. * * @example * * ```ts,runnable * import { Editor, EditorEventType, SerializableCommand } from 'js-draw'; * * // Create a minimal editor * const editor = new Editor(document.body); * editor.addToolbar(); * * // Create a place to show text output * const log = document.createElement('textarea'); * document.body.appendChild(log); * log.style.width = '100%'; * log.style.height = '200px'; * * // Listen for CommandDone events (there's also a CommandUndone) * editor.notifier.on(EditorEventType.CommandDone, event => { * // Type narrowing for TypeScript -- event will always be of kind CommandDone, * // but TypeScript doesn't know this. * if (event.kind !== EditorEventType.CommandDone) return; * * log.value = `Command done ${event.command.description(editor, editor.localization)}\n`; * * if (event.command instanceof SerializableCommand) { * log.value += `serializes to: ${JSON.stringify(event.command.serialize())}`; * } * }); * * // Dispatch an initial command to trigger the event listener for the first time * editor.dispatch(editor.image.setAutoresizeEnabled(true)); * ``` */ readonly notifier: EditorNotifier; private loadingWarning; private accessibilityAnnounceArea; private accessibilityControlArea; private eventListenerTargets; private readOnly; private readonly settings; /** * @example * ```ts,runnable * import { Editor } from 'js-draw'; * * const container = document.body; * * // Create an editor * const editor = new Editor(container, { * // 2e-10 and 1e12 are the default values for minimum/maximum zoom. * minZoom: 2e-10, * maxZoom: 1e12, * }); * * // Add the default toolbar * const toolbar = editor.addToolbar(); * * const createCustomIcon = () => { * // Create/return an icon here. * }; * * // Add a custom button * toolbar.addActionButton({ * label: 'Custom Button' * icon: createCustomIcon(), * }, () => { * // Do something here * }); * ``` */ constructor(parent: HTMLElement, settings?: Partial<EditorSettings>); /** * @returns a shallow copy of the current settings of the editor. * * Do not modify. */ getCurrentSettings(): Readonly<EditorSettings>; /** * @returns a reference to the editor's container. * * @example * ``` * // Set the editor's height to 500px * editor.getRootElement().style.height = '500px'; * ``` */ getRootElement(): HTMLElement; /** * @returns the bounding box of the main rendering region of the editor in the HTML viewport. * * @internal */ getOutputBBoxInDOM(): Rect2; /** * Shows a "Loading..." message. * @param fractionLoaded - should be a number from 0 to 1, where 1 represents completely loaded. */ showLoadingWarning(fractionLoaded: number): void; /** @see {@link showLoadingWarning} */ hideLoadingWarning(): void; private previousAccessibilityAnnouncement; /** * Announce `message` for screen readers. If `message` is the same as the previous * message, it is re-announced. */ announceForAccessibility(message: string): void; /** * Creates a toolbar. If `defaultLayout` is true, default buttons are used. * @returns a reference to the toolbar. */ addToolbar(defaultLayout?: boolean): AbstractToolbar; private registerListeners; private updateEditorSizeVariables; /** @internal */ handleHTMLWheelEvent(event: WheelEvent): boolean | undefined; private pointers; private getPointerList; /** * A protected method that can override setPointerCapture in environments where it may fail * (e.g. with synthetic events). @internal */ protected setPointerCapture(target: HTMLElement, pointerId: number): void; /** Can be overridden in a testing environment to handle synthetic events. @internal */ protected releasePointerCapture(target: HTMLElement, pointerId: number): void; /** * Dispatches a `PointerEvent` to the editor. The target element for `evt` must have the same top left * as the content of the editor. */ handleHTMLPointerEvent(eventType: 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel', evt: PointerEvent): boolean; private isEventSink; /** @internal */ protected handleDrop(evt: DragEvent | ClipboardEvent): Promise<void>; /** @internal */ protected handlePaste(evt: DragEvent | ClipboardEvent): Promise<boolean | undefined>; /** * Forward pointer events from `elem` to this editor. Such that right-click/right-click drag * events are also forwarded, `elem`'s contextmenu is disabled. * * `filter` is called once per pointer event, before doing any other processing. If `filter` returns `true` the event is * forwarded to the editor. * * **Note**: `otherEventsFilter` is like `filter`, but is called for other pointer-related * events that could also be forwarded to the editor. To forward just pointer events, * for example, `otherEventsFilter` could be given as `()=>false`. * * @example * ```ts * const overlay = document.createElement('div'); * editor.createHTMLOverlay(overlay); * * // Send all pointer events that don't have the control key pressed * // to the editor. * editor.handlePointerEventsFrom(overlay, (event) => { * if (event.ctrlKey) { * return false; * } * return true; * }); * ``` */ handlePointerEventsFrom(elem: HTMLElement, filter?: HTMLPointerEventFilter, otherEventsFilter?: (eventName: string, event: Event) => boolean): { /** Remove all event listeners registered by this function. */ remove: () => void; }; /** * Like {@link handlePointerEventsFrom} except ignores short input gestures like clicks. * * `filter` is called once per event, before doing any other processing. If `filter` returns `true` the event is * forwarded to the editor. * * `otherEventsFilter` is passed unmodified to `handlePointerEventsFrom`. */ handlePointerEventsExceptClicksFrom(elem: HTMLElement, filter?: HTMLPointerEventFilter, otherEventsFilter?: (eventName: string, event: Event) => boolean): { /** Remove all event listeners registered by this function. */ remove: () => void; }; /** @internal */ handleHTMLKeyDownEvent(htmlEvent: KeyboardEvent): boolean; /** @internal */ handleHTMLKeyUpEvent(htmlEvent: KeyboardEvent): boolean; /** * Adds event listners for keypresses (and drop events) on `elem` and forwards those * events to the editor. * * If the given `filter` returns `false` for an event, the event is ignored and not * passed to the editor. */ handleKeyEventsFrom(elem: HTMLElement, filter?: (event: KeyboardEvent) => boolean): void; /** * Attempts to prevent **user-triggered** events from modifying * the content of the image. */ setReadOnly(readOnly: boolean): void; isReadOnlyReactiveValue(): ReactiveValue<boolean>; isReadOnly(): MutableReactiveValue<boolean>; /** * `apply` a command. `command` will be announced for accessibility. * * **Example**: * [[include:doc-pages/inline-examples/adding-a-stroke.md]] */ dispatch(command: Command, addToHistory?: boolean): void | Promise<void>; /** * Dispatches a command without announcing it. By default, does not add to history. * Use this to show finalized commands that don't need to have `announceForAccessibility` * called. * * If `addToHistory` is `false`, this is equivalent to `command.apply(editor)`. * * @example * ``` * const addToHistory = false; * editor.dispatchNoAnnounce(editor.viewport.zoomTo(someRectangle), addToHistory); * ``` */ dispatchNoAnnounce(command: Command, addToHistory?: boolean): void | Promise<void>; /** * Apply a large transformation in chunks. * If `apply` is `false`, the commands are unapplied. * Triggers a re-render after each `updateChunkSize`-sized group of commands * has been applied. */ asyncApplyOrUnapplyCommands(commands: Command[], apply: boolean, updateChunkSize: number): Promise<void>; /** @see {@link asyncApplyOrUnapplyCommands } */ asyncApplyCommands(commands: Command[], chunkSize: number): Promise<void>; /** * @see {@link asyncApplyOrUnapplyCommands} * * If `unapplyInReverseOrder`, commands are reversed before unapplying. */ asyncUnapplyCommands(commands: Command[], chunkSize: number, unapplyInReverseOrder?: boolean): Promise<void>; private announceUndoCallback; private announceRedoCallback; private nextRerenderListeners; private rerenderQueued; /** * Schedule a re-render for some time in the near future. Does not schedule an additional * re-render if a re-render is already queued. * * @returns a promise that resolves when re-rendering has completed. */ queueRerender(): Promise<void>; isRerenderQueued(): boolean; /** * Re-renders the entire image. * * @see {@link Editor.queueRerender} */ rerender(showImageBounds?: boolean): void; /** * Draws the given path onto the wet ink renderer. The given path will * be displayed on top of the main image. * * @see {@link Display.getWetInkRenderer} {@link Display.flatten} */ drawWetInk(...path: RenderablePathSpec[]): void; /** * Clears the wet ink display. * * The wet ink display can be used by the currently active tool to display a preview * of an in-progress action. * * @see {@link Display.getWetInkRenderer} */ clearWetInk(): void; /** * Focuses the region used for text input/key commands. */ focus(): void; /** * Creates an element that will be positioned on top of the dry/wet ink * renderers. * * So as not to change the position of other overlays, `overlay` should either * be styled to have 0 height or have `position: absolute`. * * This is useful for displaying content on top of the rendered content * (e.g. a selection box). */ createHTMLOverlay(overlay: HTMLElement): { remove: () => void; }; /** * Anchors the given `element` to the canvas with a given position/transformation in canvas space. */ anchorElementToCanvas(element: HTMLElement, canvasTransform: Mat33 | ReactiveValue<Mat33>): { remove: () => void; }; /** * Creates a CSS stylesheet with `content` and applies it to the document * (and thus, to this editor). */ addStyleSheet(content: string): HTMLStyleElement; /** * Dispatch a keyboard event to the currently selected tool. * Intended for unit testing. * * If `shiftKey` is undefined, it is guessed from `key`. * * At present, the **key code** dispatched is guessed from the given key and, * while this works for ASCII alphanumeric characters, this does not work for * most non-alphanumeric keys. * * Because guessing the key code from `key` is problematic, **only use this for testing**. */ sendKeyboardEvent(eventType: InputEvtType.KeyPressEvent | InputEvtType.KeyUpEvent, key: string, ctrlKey?: boolean, altKey?: boolean, shiftKey?: boolean | undefined): void; /** * Dispatch a pen event to the currently selected tool. * Intended primarially for unit tests. * * @deprecated * @see {@link sendPenEvent} {@link sendTouchEvent} */ sendPenEvent(eventType: PointerEvtType, point: Point2, allPointers?: Pointer[]): void; /** * Adds all components in `components` such that they are in the center of the screen. * This is a convenience method that creates **and applies** a single command. * * If `selectComponents` is true (the default), the components are selected. * * `actionDescription`, if given, should be a screenreader-friendly description of the * reason components were added (e.g. "pasted"). */ addAndCenterComponents(components: AbstractComponent[], selectComponents?: boolean, actionDescription?: string): Promise<void>; /** * Get a data URL (e.g. as produced by `HTMLCanvasElement::toDataURL`). * If `format` is not `image/png`, a PNG image URL may still be returned (as in the * case of `HTMLCanvasElement::toDataURL`). * * The export resolution is the same as the size of the drawing canvas, unless `outputSize` * is given. * * **Example**: * [[include:doc-pages/inline-examples/adding-an-image-and-data-urls.md]] */ toDataURL(format?: 'image/png' | 'image/jpeg' | 'image/webp', outputSize?: Vec2): string; /** * Converts the editor's content into an SVG image. * * If the output SVG has width or height less than `options.minDimension`, its size * will be increased. * * @see * {@link SVGRenderer} */ toSVG(options?: { minDimension?: number; }): SVGElement; /** * Converts the editor's content into an SVG image in an asynchronous, * but **potentially lossy** way. * * **Warning**: If the image is being edited during an async rendering, edited components * may not be rendered. * * Like {@link toSVG}, but can be configured to briefly pause after processing every * `pauseAfterCount` items. This can prevent the editor from becoming unresponsive * when saving very large images. */ toSVGAsync(options?: { minDimension?: number; pauseAfterCount?: number; onProgress?: (processedCountInLayer: number, totalToProcessInLayer: number) => Promise<void | boolean>; }): Promise<SVGElement>; /** * Load editor data from an `ImageLoader` (e.g. an {@link SVGLoader}). * * @see loadFromSVG */ loadFrom(loader: ImageLoader): Promise<void>; private getTopmostBackgroundComponent; /** * This is a convenience method for adding or updating the {@link BackgroundComponent} * and {@link EditorImage.setAutoresizeEnabled} for the current image. * * If there are multiple {@link BackgroundComponent}s in the image, this only modifies * the topmost such element. * * **Example**: * ```ts,runnable * import { Editor, Color4, BackgroundComponentBackgroundType } from 'js-draw'; * const editor = new Editor(document.body); * editor.dispatch(editor.setBackgroundStyle({ * color: Color4.orange, * type: BackgroundComponentBackgroundType.Grid, * autoresize: true, * })); * ``` * * To change the background size, see {@link EditorImage.setImportExportRect}. */ setBackgroundStyle(style: { color?: Color4; type?: BackgroundType; autoresize?: boolean; }): Command; /** * Set the background color of the image. * * This is a convenience method for adding or updating the {@link BackgroundComponent} * for the current image. * * @see {@link setBackgroundStyle} */ setBackgroundColor(color: Color4): Command; /** * @returns the average of the colors of all background components. Use this to get the current background * color. */ estimateBackgroundColor(): Color4; getImportExportRect(): Rect2; setImportExportRect(imageRect: Rect2): Command; /** * Alias for `loadFrom(SVGLoader.fromString)`. * * @example * ```ts,runnable * import {Editor} from 'js-draw'; * const editor = new Editor(document.body); * * ---visible--- * await editor.loadFromSVG(` * <svg viewBox="5 23 52 30" width="52" height="16" version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg"> * <text style=" * transform: matrix(0.181846, 0.1, 0, 0.181846, 11.4, 33.2); * font-family: serif; * font-size: 32px; * fill: rgb(100, 140, 61); * ">An SVG image!</text> * </svg> * `); * ``` */ loadFromSVG(svgData: string, sanitize?: boolean): Promise<void>; private closeAboutDialog; /** * Shows an information dialog with legal notices. */ showAboutDialog(): void; /** * Removes and **destroys** the editor. The editor cannot be added to a parent * again after calling this method. */ remove(): void; } export default Editor;