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
TypeScript
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;