UNPKG

js-draw

Version:

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

321 lines (320 loc) 13.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const types_1 = require("../types"); const math_1 = require("@js-draw/math"); const PanZoom_1 = __importStar(require("./PanZoom")); const Pen_1 = __importDefault(require("./Pen")); const ToolEnabledGroup_1 = __importDefault(require("./ToolEnabledGroup")); const Eraser_1 = __importDefault(require("./Eraser")); const SelectionTool_1 = __importDefault(require("./SelectionTool/SelectionTool")); const UndoRedoShortcut_1 = __importDefault(require("./UndoRedoShortcut")); const TextTool_1 = __importDefault(require("./TextTool")); const PipetteTool_1 = __importDefault(require("./PipetteTool")); const ToolSwitcherShortcut_1 = __importDefault(require("./ToolSwitcherShortcut")); const PasteHandler_1 = __importDefault(require("./PasteHandler")); const ToolbarShortcutHandler_1 = __importDefault(require("./ToolbarShortcutHandler")); const PressureSensitiveFreehandLineBuilder_1 = require("../components/builders/PressureSensitiveFreehandLineBuilder"); const FindTool_1 = __importDefault(require("./FindTool")); const SelectAllShortcutHandler_1 = __importDefault(require("./SelectionTool/SelectAllShortcutHandler")); const SoundUITool_1 = __importDefault(require("./SoundUITool")); const inputEvents_1 = require("../inputEvents"); const InputPipeline_1 = __importDefault(require("./InputFilter/InputPipeline")); const InputStabilizer_1 = __importDefault(require("./InputFilter/InputStabilizer")); const ScrollbarTool_1 = __importDefault(require("./ScrollbarTool")); class ToolController { /** @internal */ constructor(editor, localization) { this.activeTool = null; this.isEditorReadOnly = editor.isReadOnlyReactiveValue(); this.inputPipeline = new InputPipeline_1.default(); this.inputPipeline.setEmitListener((event) => this.onEventInternal(event)); const primaryToolGroup = new ToolEnabledGroup_1.default(); this.primaryToolGroup = primaryToolGroup; const panZoomTool = new PanZoom_1.default(editor, PanZoom_1.PanZoomMode.TwoFingerTouchGestures | PanZoom_1.PanZoomMode.RightClickDrags, localization.touchPanTool); const keyboardPanZoomTool = new PanZoom_1.default(editor, PanZoom_1.PanZoomMode.Keyboard, localization.keyboardPanZoom); const primaryPenTool = new Pen_1.default(editor, localization.penTool(1), { color: math_1.Color4.purple, thickness: 8, }); const secondaryPenTool = new Pen_1.default(editor, localization.penTool(2), { color: math_1.Color4.clay, thickness: 4, }); // Stabilize the secondary pen tool. secondaryPenTool.setInputMapper(new InputStabilizer_1.default(editor.viewport)); const eraser = new Eraser_1.default(editor, localization.eraserTool); const primaryTools = [ // Three pens primaryPenTool, secondaryPenTool, // Highlighter-like pen with width=40 new Pen_1.default(editor, localization.penTool(3), { color: math_1.Color4.ofRGBA(1, 1, 0, 0.5), thickness: 40, factory: PressureSensitiveFreehandLineBuilder_1.makePressureSensitiveFreehandLineBuilder, }), eraser, new SelectionTool_1.default(editor, localization.selectionTool), new TextTool_1.default(editor, localization.textTool, localization), new PanZoom_1.default(editor, PanZoom_1.PanZoomMode.SinglePointerGestures, localization.anyDevicePanning), ]; // Accessibility tools const soundExplorer = new SoundUITool_1.default(editor, localization.soundExplorer); soundExplorer.setEnabled(false); this.tools = [ new ScrollbarTool_1.default(editor), new PipetteTool_1.default(editor, localization.pipetteTool), soundExplorer, panZoomTool, ...primaryTools, keyboardPanZoomTool, new UndoRedoShortcut_1.default(editor), new ToolbarShortcutHandler_1.default(editor), new ToolSwitcherShortcut_1.default(editor), eraser.makeEraserSwitcherTool(), new FindTool_1.default(editor), new PasteHandler_1.default(editor), new SelectAllShortcutHandler_1.default(editor), ]; primaryTools.forEach((tool) => tool.setToolGroup(primaryToolGroup)); panZoomTool.setEnabled(true); primaryPenTool.setEnabled(true); editor.notifier.on(types_1.EditorEventType.ToolEnabled, (event) => { if (event.kind === types_1.EditorEventType.ToolEnabled) { editor.announceForAccessibility(localization.toolEnabledAnnouncement(event.tool.description)); } }); editor.notifier.on(types_1.EditorEventType.ToolDisabled, (event) => { if (event.kind === types_1.EditorEventType.ToolDisabled) { editor.announceForAccessibility(localization.toolDisabledAnnouncement(event.tool.description)); } }); this.activeTool = null; } /** * Replaces the current set of tools with `tools`. This should only be done before * the creation of the app's toolbar (if using `AbstractToolbar`). * * If no `primaryToolGroup` is given, an empty one will be created. */ setTools(tools, primaryToolGroup) { this.tools = tools; this.primaryToolGroup = primaryToolGroup ?? new ToolEnabledGroup_1.default(); } /** * Add a tool that acts like one of the primary tools (only one primary tool can be enabled at a time). * * If the tool is already added to this, the tool is converted to a primary tool. * * This should be called before creating the app's toolbar. */ addPrimaryTool(tool) { tool.setToolGroup(this.primaryToolGroup); if (tool.isEnabled()) { this.primaryToolGroup.notifyEnabled(tool); } if (!this.tools.includes(tool)) { this.addTool(tool); } } getPrimaryTools() { return this.tools.filter((tool) => { return tool.getToolGroup() === this.primaryToolGroup; }); } /** * Add a tool to the end of this' tool list (the added tool receives events after tools already added to this). * This should be called before creating the app's toolbar. * * If `options.addToFront`, the tool is added to the beginning of this' tool list. * * Does nothing if the tool is already present. */ addTool(tool, options) { // Only add if not already present. if (!this.tools.includes(tool)) { if (options?.addToFront) { this.tools.splice(0, 0, tool); } else { this.tools.push(tool); } } } /** * Removes **and destroys** all tools in `tools` from this. */ removeAndDestroyTools(tools) { const newTools = []; for (const tool of this.tools) { if (tools.includes(tool)) { if (this.activeTool === tool) { this.activeTool = null; } tool.onDestroy(); } else { newTools.push(tool); } } this.tools = newTools; } insertTools(insertNear, toolsToInsert, mode) { this.tools = this.tools.filter((tool) => !toolsToInsert.includes(tool)); const newTools = []; for (const tool of this.tools) { if (mode === 'after') { newTools.push(tool); } if (tool === insertNear) { newTools.push(...toolsToInsert); } if (mode === 'before') { newTools.push(tool); } } this.tools = newTools; } /** * Removes a tool from this' tool list and replaces it with `replaceWith`. * * If any of `toolsToInsert` have already been added to this, the tools are * moved. * * This should be called before creating the editor's toolbar. */ insertToolsAfter(insertAfter, toolsToInsert) { this.insertTools(insertAfter, toolsToInsert, 'after'); } /** @see {@link insertToolsAfter} */ insertToolsBefore(insertBefore, toolsToInsert) { this.insertTools(insertBefore, toolsToInsert, 'before'); } // @internal use `dispatchEvent` rather than calling `onEvent` directly. onEventInternal(event) { const isEditorReadOnly = this.isEditorReadOnly.get(); const canToolReceiveInput = (tool) => { return tool.isEnabled() && (!isEditorReadOnly || tool.canReceiveInputInReadOnlyEditor()); }; let handled = false; if (event.kind === inputEvents_1.InputEvtType.PointerDownEvt) { let canOnlySendToActiveTool = false; if (this.activeTool && !this.activeTool.eventCanBeDeliveredToNonActiveTool(event)) { canOnlySendToActiveTool = true; } for (const tool of this.tools) { if (canOnlySendToActiveTool && tool !== this.activeTool) { continue; } if (canToolReceiveInput(tool) && tool.onEvent(event)) { if (this.activeTool !== tool) { this.activeTool?.onEvent({ kind: inputEvents_1.InputEvtType.GestureCancelEvt }); } this.activeTool = tool; handled = true; break; } } } else if (event.kind === inputEvents_1.InputEvtType.PointerUpEvt) { const upResult = this.activeTool?.onEvent(event); const continueHandlingEvents = upResult && event.allPointers.length > 1; // Should the active tool continue handling events (without an additional pointer down?) if (!continueHandlingEvents) { // No -- Remove the current tool this.activeTool = null; } handled = true; } else if (event.kind === inputEvents_1.InputEvtType.PointerMoveEvt) { if (this.activeTool !== null) { this.activeTool.onEvent(event); handled = true; } } else if (event.kind === inputEvents_1.InputEvtType.GestureCancelEvt) { if (this.activeTool !== null) { this.activeTool.onEvent(event); this.activeTool = null; } } else { for (const tool of this.tools) { if (!canToolReceiveInput(tool)) { continue; } handled = tool.onEvent(event); if (handled) { break; } } } return handled; } /** Alias for {@link dispatchInputEvent}. */ onEvent(event) { return this.dispatchInputEvent(event); } // Returns true if the event was handled. dispatchInputEvent(event) { // Feed the event through the input pipeline return this.inputPipeline.onEvent(event); } /** * Adds a new `InputMapper` to this' input pipeline. * * A `mapper` is really a relation that maps each event to no, one, * or many other events. * * @see {@link InputMapper}. */ addInputMapper(mapper) { this.inputPipeline.addToTail(mapper); } getMatchingTools(type) { return this.tools.filter((tool) => tool instanceof type); } // @internal onEditorDestroyed() { for (const tool of this.tools) { tool.onDestroy(); } } } exports.default = ToolController;