js-draw 1.19.1 → 1.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -1
- package/dist/Editor.css +49 -7
- package/dist/bundle.js +2 -2
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/SVGLoader/index.js +3 -1
- package/dist/cjs/image/EditorImage.d.ts +2 -1
- package/dist/cjs/image/EditorImage.js +101 -5
- package/dist/cjs/rendering/renderers/CanvasRenderer.js +4 -4
- package/dist/cjs/toolbar/localization.d.ts +1 -0
- package/dist/cjs/toolbar/localization.js +1 -0
- package/dist/cjs/toolbar/widgets/InsertImageWidget/ImageWrapper.d.ts +1 -0
- package/dist/cjs/toolbar/widgets/InsertImageWidget/ImageWrapper.js +7 -0
- package/dist/cjs/toolbar/widgets/InsertImageWidget/index.js +11 -3
- package/dist/cjs/toolbar/widgets/components/makeFileInput.js +12 -1
- package/dist/cjs/toolbar/widgets/components/makeSnappedList.js +69 -4
- package/dist/cjs/tools/Eraser.js +22 -5
- package/dist/cjs/tools/PanZoom.d.ts +54 -0
- package/dist/cjs/tools/PanZoom.js +54 -2
- package/dist/cjs/util/ReactiveValue.d.ts +4 -0
- package/dist/cjs/util/ReactiveValue.js +5 -0
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/SVGLoader/index.mjs +3 -1
- package/dist/mjs/image/EditorImage.d.ts +2 -1
- package/dist/mjs/image/EditorImage.mjs +101 -5
- package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +4 -4
- package/dist/mjs/toolbar/localization.d.ts +1 -0
- package/dist/mjs/toolbar/localization.mjs +1 -0
- package/dist/mjs/toolbar/widgets/InsertImageWidget/ImageWrapper.d.ts +1 -0
- package/dist/mjs/toolbar/widgets/InsertImageWidget/ImageWrapper.mjs +7 -0
- package/dist/mjs/toolbar/widgets/InsertImageWidget/index.mjs +11 -3
- package/dist/mjs/toolbar/widgets/components/makeFileInput.mjs +12 -1
- package/dist/mjs/toolbar/widgets/components/makeSnappedList.mjs +69 -4
- package/dist/mjs/tools/Eraser.mjs +22 -5
- package/dist/mjs/tools/PanZoom.d.ts +54 -0
- package/dist/mjs/tools/PanZoom.mjs +54 -2
- package/dist/mjs/util/ReactiveValue.d.ts +4 -0
- package/dist/mjs/util/ReactiveValue.mjs +5 -0
- package/dist/mjs/version.mjs +1 -1
- package/docs/img/readme-images/unsupported-elements--in-editor.png +0 -0
- package/package.json +2 -2
- package/src/toolbar/EdgeToolbar.scss +8 -3
- package/src/toolbar/widgets/components/makeFileInput.scss +3 -2
- package/src/toolbar/widgets/components/makeSnappedList.scss +58 -12
| @@ -11,8 +11,72 @@ const ReactiveValue_1 = require("../../../util/ReactiveValue"); | |
| 11 11 | 
             
            const makeSnappedList = (itemsValue) => {
         | 
| 12 12 | 
             
                const container = document.createElement('div');
         | 
| 13 13 | 
             
                container.classList.add('toolbar-snapped-scroll-list');
         | 
| 14 | 
            +
                const scroller = document.createElement('div');
         | 
| 15 | 
            +
                scroller.classList.add('scroller');
         | 
| 14 16 | 
             
                const visibleIndex = ReactiveValue_1.MutableReactiveValue.fromInitialValue(0);
         | 
| 15 17 | 
             
                let observer = null;
         | 
| 18 | 
            +
                const makePageMarkers = () => {
         | 
| 19 | 
            +
                    const markerContainer = document.createElement('div');
         | 
| 20 | 
            +
                    markerContainer.classList.add('page-markers');
         | 
| 21 | 
            +
                    // Keyboard focus should go to the main scrolling list.
         | 
| 22 | 
            +
                    // TODO: Does it make sense for the page marker list to be focusable?
         | 
| 23 | 
            +
                    markerContainer.setAttribute('tabindex', '-1');
         | 
| 24 | 
            +
                    const markers = [];
         | 
| 25 | 
            +
                    const pairedItems = ReactiveValue_1.ReactiveValue.union([visibleIndex, itemsValue]);
         | 
| 26 | 
            +
                    pairedItems.onUpdateAndNow(([currentVisibleIndex, items]) => {
         | 
| 27 | 
            +
                        let addedOrRemovedMarkers = false;
         | 
| 28 | 
            +
                        // Items may have been removed from the list of pages. Make the markers reflect that.
         | 
| 29 | 
            +
                        while (items.length < markers.length) {
         | 
| 30 | 
            +
                            markers.pop();
         | 
| 31 | 
            +
                            addedOrRemovedMarkers = true;
         | 
| 32 | 
            +
                        }
         | 
| 33 | 
            +
                        let activeMarker;
         | 
| 34 | 
            +
                        for (let i = 0; i < items.length; i++) {
         | 
| 35 | 
            +
                            let marker;
         | 
| 36 | 
            +
                            if (i >= markers.length) {
         | 
| 37 | 
            +
                                marker = document.createElement('div');
         | 
| 38 | 
            +
                                // Use a separate content element to increase the clickable size of
         | 
| 39 | 
            +
                                // the marker.
         | 
| 40 | 
            +
                                const content = document.createElement('div');
         | 
| 41 | 
            +
                                content.classList.add('content');
         | 
| 42 | 
            +
                                marker.replaceChildren(content);
         | 
| 43 | 
            +
                                markers.push(marker);
         | 
| 44 | 
            +
                                addedOrRemovedMarkers = true;
         | 
| 45 | 
            +
                            }
         | 
| 46 | 
            +
                            else {
         | 
| 47 | 
            +
                                marker = markers[i];
         | 
| 48 | 
            +
                            }
         | 
| 49 | 
            +
                            marker.classList.add('marker');
         | 
| 50 | 
            +
                            if (i === currentVisibleIndex) {
         | 
| 51 | 
            +
                                marker.classList.add('-active');
         | 
| 52 | 
            +
                                activeMarker = marker;
         | 
| 53 | 
            +
                            }
         | 
| 54 | 
            +
                            else {
         | 
| 55 | 
            +
                                marker.classList.remove('-active');
         | 
| 56 | 
            +
                            }
         | 
| 57 | 
            +
                            const markerIndex = i;
         | 
| 58 | 
            +
                            marker.onclick = () => {
         | 
| 59 | 
            +
                                wrappedItems.get()[markerIndex]?.element?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
         | 
| 60 | 
            +
                            };
         | 
| 61 | 
            +
                        }
         | 
| 62 | 
            +
                        // Only call .replaceChildren when necessary -- doing so on every change would
         | 
| 63 | 
            +
                        // break transitions.
         | 
| 64 | 
            +
                        if (addedOrRemovedMarkers) {
         | 
| 65 | 
            +
                            markerContainer.replaceChildren(...markers);
         | 
| 66 | 
            +
                        }
         | 
| 67 | 
            +
                        // Handles the case where there are many markers and the current is offscreen
         | 
| 68 | 
            +
                        if (activeMarker && markerContainer.scrollHeight > container.clientHeight) {
         | 
| 69 | 
            +
                            activeMarker.scrollIntoView({ block: 'nearest' });
         | 
| 70 | 
            +
                        }
         | 
| 71 | 
            +
                        if (markers.length === 1) {
         | 
| 72 | 
            +
                            markerContainer.classList.add('-one-element');
         | 
| 73 | 
            +
                        }
         | 
| 74 | 
            +
                        else {
         | 
| 75 | 
            +
                            markerContainer.classList.remove('-one-element');
         | 
| 76 | 
            +
                        }
         | 
| 77 | 
            +
                    });
         | 
| 78 | 
            +
                    return markerContainer;
         | 
| 79 | 
            +
                };
         | 
| 16 80 | 
             
                const createObserver = () => {
         | 
| 17 81 | 
             
                    observer = new IntersectionObserver((entries) => {
         | 
| 18 82 | 
             
                        for (const entry of entries) {
         | 
| @@ -28,7 +92,7 @@ const makeSnappedList = (itemsValue) => { | |
| 28 92 | 
             
                    }, {
         | 
| 29 93 | 
             
                        // Element to use as the boudning box with which to intersect.
         | 
| 30 94 | 
             
                        // See https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
         | 
| 31 | 
            -
                        root:  | 
| 95 | 
            +
                        root: scroller,
         | 
| 32 96 | 
             
                        // Fraction of an element that must be visible to trigger the callback:
         | 
| 33 97 | 
             
                        threshold: 0.9,
         | 
| 34 98 | 
             
                    });
         | 
| @@ -60,7 +124,7 @@ const makeSnappedList = (itemsValue) => { | |
| 60 124 | 
             
                    for (const item of lastItems) {
         | 
| 61 125 | 
             
                        observer?.unobserve(item.element);
         | 
| 62 126 | 
             
                    }
         | 
| 63 | 
            -
                     | 
| 127 | 
            +
                    scroller.replaceChildren();
         | 
| 64 128 | 
             
                    // An observer is only necessary if there are multiple items to scroll through.
         | 
| 65 129 | 
             
                    if (items.length > 1) {
         | 
| 66 130 | 
             
                        createObserver();
         | 
| @@ -76,7 +140,7 @@ const makeSnappedList = (itemsValue) => { | |
| 76 140 | 
             
                        container.classList.remove('-empty');
         | 
| 77 141 | 
             
                    }
         | 
| 78 142 | 
             
                    for (const item of items) {
         | 
| 79 | 
            -
                         | 
| 143 | 
            +
                        scroller.appendChild(item.element);
         | 
| 80 144 | 
             
                    }
         | 
| 81 145 | 
             
                    visibleIndex.set(0);
         | 
| 82 146 | 
             
                    if (observer) {
         | 
| @@ -94,7 +158,8 @@ const makeSnappedList = (itemsValue) => { | |
| 94 158 | 
             
                });
         | 
| 95 159 | 
             
                // makeSnappedList is generally shown within the toolbar. This allows users to
         | 
| 96 160 | 
             
                // scroll it with a touchpad.
         | 
| 97 | 
            -
                (0, stopPropagationOfScrollingWheelEvents_1.default)( | 
| 161 | 
            +
                (0, stopPropagationOfScrollingWheelEvents_1.default)(scroller);
         | 
| 162 | 
            +
                container.replaceChildren(makePageMarkers(), scroller);
         | 
| 98 163 | 
             
                return {
         | 
| 99 164 | 
             
                    container,
         | 
| 100 165 | 
             
                    visibleItem,
         | 
    
        package/dist/cjs/tools/Eraser.js
    CHANGED
    
    | @@ -71,6 +71,7 @@ class Eraser extends BaseTool_1.default { | |
| 71 71 | 
             
                    this.editor = editor;
         | 
| 72 72 | 
             
                    this.lastPoint = null;
         | 
| 73 73 | 
             
                    this.isFirstEraseEvt = true;
         | 
| 74 | 
            +
                    this.toAdd = new Set();
         | 
| 74 75 | 
             
                    // Commands that each remove one element
         | 
| 75 76 | 
             
                    this.eraseCommands = [];
         | 
| 76 77 | 
             
                    this.addCommands = [];
         | 
| @@ -180,15 +181,17 @@ class Eraser extends BaseTool_1.default { | |
| 180 181 | 
             
                        newAddCommands.forEach(command => command.apply(this.editor));
         | 
| 181 182 | 
             
                        const finalToErase = [];
         | 
| 182 183 | 
             
                        for (const item of toErase) {
         | 
| 183 | 
            -
                            if (this.toAdd. | 
| 184 | 
            -
                                this.toAdd | 
| 184 | 
            +
                            if (this.toAdd.has(item)) {
         | 
| 185 | 
            +
                                this.toAdd.delete(item);
         | 
| 185 186 | 
             
                            }
         | 
| 186 187 | 
             
                            else {
         | 
| 187 188 | 
             
                                finalToErase.push(item);
         | 
| 188 189 | 
             
                            }
         | 
| 189 190 | 
             
                        }
         | 
| 190 191 | 
             
                        this.toRemove.push(...finalToErase);
         | 
| 191 | 
            -
                         | 
| 192 | 
            +
                        for (const item of toAdd) {
         | 
| 193 | 
            +
                            this.toAdd.add(item);
         | 
| 194 | 
            +
                        }
         | 
| 192 195 | 
             
                        this.eraseCommands.push(new Erase_1.default(finalToErase));
         | 
| 193 196 | 
             
                        this.addCommands.push(...newAddCommands);
         | 
| 194 197 | 
             
                    }
         | 
| @@ -199,7 +202,7 @@ class Eraser extends BaseTool_1.default { | |
| 199 202 | 
             
                    if (event.allPointers.length === 1 || event.current.device === Pointer_1.PointerDevice.Eraser) {
         | 
| 200 203 | 
             
                        this.lastPoint = event.current.canvasPos;
         | 
| 201 204 | 
             
                        this.toRemove = [];
         | 
| 202 | 
            -
                        this.toAdd | 
| 205 | 
            +
                        this.toAdd.clear();
         | 
| 203 206 | 
             
                        this.isFirstEraseEvt = true;
         | 
| 204 207 | 
             
                        this.drawPreviewAt(event.current.canvasPos);
         | 
| 205 208 | 
             
                        return true;
         | 
| @@ -215,7 +218,21 @@ class Eraser extends BaseTool_1.default { | |
| 215 218 | 
             
                    const commands = [];
         | 
| 216 219 | 
             
                    if (this.addCommands.length > 0) {
         | 
| 217 220 | 
             
                        this.addCommands.forEach(cmd => cmd.unapply(this.editor));
         | 
| 218 | 
            -
                         | 
| 221 | 
            +
                        // Remove items from toAdd that are also present in toRemove -- adding, then
         | 
| 222 | 
            +
                        // removing these does nothing, and can break undo/redo.
         | 
| 223 | 
            +
                        for (const item of this.toAdd) {
         | 
| 224 | 
            +
                            if (this.toRemove.includes(item)) {
         | 
| 225 | 
            +
                                this.toAdd.delete(item);
         | 
| 226 | 
            +
                                this.toRemove = this.toRemove.filter(other => other !== item);
         | 
| 227 | 
            +
                            }
         | 
| 228 | 
            +
                        }
         | 
| 229 | 
            +
                        for (const item of this.toRemove) {
         | 
| 230 | 
            +
                            if (this.toAdd.has(item)) {
         | 
| 231 | 
            +
                                this.toAdd.delete(item);
         | 
| 232 | 
            +
                                this.toRemove = this.toRemove.filter(other => other !== item);
         | 
| 233 | 
            +
                            }
         | 
| 234 | 
            +
                        }
         | 
| 235 | 
            +
                        commands.push(...[...this.toAdd].map(a => EditorImage_1.default.addElement(a)));
         | 
| 219 236 | 
             
                        this.addCommands = [];
         | 
| 220 237 | 
             
                    }
         | 
| 221 238 | 
             
                    if (this.eraseCommands.length > 0) {
         | 
| @@ -10,13 +10,27 @@ interface PinchData { | |
| 10 10 | 
             
                dist: number;
         | 
| 11 11 | 
             
            }
         | 
| 12 12 | 
             
            export declare enum PanZoomMode {
         | 
| 13 | 
            +
                /** Touch gestures with a single pointer. Ignores non-touch gestures. */
         | 
| 13 14 | 
             
                OneFingerTouchGestures = 1,
         | 
| 15 | 
            +
                /** Touch gestures with exactly two pointers. Ignores non-touch gestures. */
         | 
| 14 16 | 
             
                TwoFingerTouchGestures = 2,
         | 
| 15 17 | 
             
                RightClickDrags = 4,
         | 
| 18 | 
            +
                /** Single-pointer gestures of *any* type (including touch). */
         | 
| 16 19 | 
             
                SinglePointerGestures = 8,
         | 
| 20 | 
            +
                /** Keyboard navigation (e.g. LeftArrow to move left). */
         | 
| 17 21 | 
             
                Keyboard = 16,
         | 
| 22 | 
            +
                /** If provided, prevents **this** tool from rotating the viewport (other tools may still do so). */
         | 
| 18 23 | 
             
                RotationLocked = 32
         | 
| 19 24 | 
             
            }
         | 
| 25 | 
            +
            /**
         | 
| 26 | 
            +
             * This tool moves the viewport in response to touchpad, touchscreen, mouse, and keyboard events.
         | 
| 27 | 
            +
             *
         | 
| 28 | 
            +
             * Which events are handled, and which are skipped, are determined by the tool's `mode`. For example,
         | 
| 29 | 
            +
             * a `PanZoom` tool with `mode = PanZoomMode.TwoFingerTouchGestures|PanZoomMode.RightClickDrags` would
         | 
| 30 | 
            +
             * respond to right-click drag events and two-finger touch gestures.
         | 
| 31 | 
            +
             *
         | 
| 32 | 
            +
             * @see {@link setModeEnabled}
         | 
| 33 | 
            +
             */
         | 
| 20 34 | 
             
            export default class PanZoom extends BaseTool {
         | 
| 21 35 | 
             
                private editor;
         | 
| 22 36 | 
             
                private mode;
         | 
| @@ -58,8 +72,48 @@ export default class PanZoom extends BaseTool { | |
| 58 72 | 
             
                onWheel({ delta, screenPos }: WheelEvt): boolean;
         | 
| 59 73 | 
             
                onKeyPress(event: KeyPressEvent): boolean;
         | 
| 60 74 | 
             
                private isRotationLocked;
         | 
| 75 | 
            +
                /**
         | 
| 76 | 
            +
                 * Changes the types of gestures used by this pan/zoom tool.
         | 
| 77 | 
            +
                 *
         | 
| 78 | 
            +
                 * @see {@link PanZoomMode} {@link setMode}
         | 
| 79 | 
            +
                 *
         | 
| 80 | 
            +
                 * @example
         | 
| 81 | 
            +
                 * ```ts,runnable
         | 
| 82 | 
            +
                 * import { Editor, PanZoomTool, PanZoomMode } from 'js-draw';
         | 
| 83 | 
            +
                 *
         | 
| 84 | 
            +
                 * const editor = new Editor(document.body);
         | 
| 85 | 
            +
                 *
         | 
| 86 | 
            +
                 * // By default, there are multiple PanZoom tools that handle different events.
         | 
| 87 | 
            +
                 * // This gets all PanZoomTools.
         | 
| 88 | 
            +
                 * const panZoomToolList = editor.toolController.getMatchingTools(PanZoomTool);
         | 
| 89 | 
            +
                 *
         | 
| 90 | 
            +
                 * // The first PanZoomTool is the highest priority -- by default,
         | 
| 91 | 
            +
                 * // this tool is responsible for handling multi-finger touch gestures.
         | 
| 92 | 
            +
                 * //
         | 
| 93 | 
            +
                 * // Lower-priority PanZoomTools handle one-finger touch gestures and
         | 
| 94 | 
            +
                 * // key-presses.
         | 
| 95 | 
            +
                 * const panZoomTool = panZoomToolList[0];
         | 
| 96 | 
            +
                 *
         | 
| 97 | 
            +
                 * // Lock rotation for multi-finger touch gestures.
         | 
| 98 | 
            +
                 * panZoomTool.setModeEnabled(PanZoomMode.RotationLocked, true);
         | 
| 99 | 
            +
                 * ```
         | 
| 100 | 
            +
                 */
         | 
| 61 101 | 
             
                setModeEnabled(mode: PanZoomMode, enabled: boolean): void;
         | 
| 102 | 
            +
                /**
         | 
| 103 | 
            +
                 * Sets all modes for this tool using a bitmask.
         | 
| 104 | 
            +
                 *
         | 
| 105 | 
            +
                 * @see {@link setModeEnabled}
         | 
| 106 | 
            +
                 *
         | 
| 107 | 
            +
                 * @example
         | 
| 108 | 
            +
                 * ```ts
         | 
| 109 | 
            +
                 * tool.setMode(PanZoomMode.RotationLocked|PanZoomMode.TwoFingerTouchGestures);
         | 
| 110 | 
            +
                 * ```
         | 
| 111 | 
            +
                 */
         | 
| 62 112 | 
             
                setMode(mode: PanZoomMode): void;
         | 
| 113 | 
            +
                /**
         | 
| 114 | 
            +
                 * Returns a bitmask indicating the currently-enabled modes.
         | 
| 115 | 
            +
                 * @see {@link setModeEnabled}
         | 
| 116 | 
            +
                 */
         | 
| 63 117 | 
             
                getMode(): PanZoomMode;
         | 
| 64 118 | 
             
            }
         | 
| 65 119 | 
             
            export {};
         | 
| @@ -13,11 +13,16 @@ const BaseTool_1 = __importDefault(require("./BaseTool")); | |
| 13 13 | 
             
            const keybindings_1 = require("./keybindings");
         | 
| 14 14 | 
             
            var PanZoomMode;
         | 
| 15 15 | 
             
            (function (PanZoomMode) {
         | 
| 16 | 
            +
                /** Touch gestures with a single pointer. Ignores non-touch gestures. */
         | 
| 16 17 | 
             
                PanZoomMode[PanZoomMode["OneFingerTouchGestures"] = 1] = "OneFingerTouchGestures";
         | 
| 18 | 
            +
                /** Touch gestures with exactly two pointers. Ignores non-touch gestures. */
         | 
| 17 19 | 
             
                PanZoomMode[PanZoomMode["TwoFingerTouchGestures"] = 2] = "TwoFingerTouchGestures";
         | 
| 18 20 | 
             
                PanZoomMode[PanZoomMode["RightClickDrags"] = 4] = "RightClickDrags";
         | 
| 21 | 
            +
                /** Single-pointer gestures of *any* type (including touch). */
         | 
| 19 22 | 
             
                PanZoomMode[PanZoomMode["SinglePointerGestures"] = 8] = "SinglePointerGestures";
         | 
| 23 | 
            +
                /** Keyboard navigation (e.g. LeftArrow to move left). */
         | 
| 20 24 | 
             
                PanZoomMode[PanZoomMode["Keyboard"] = 16] = "Keyboard";
         | 
| 25 | 
            +
                /** If provided, prevents **this** tool from rotating the viewport (other tools may still do so). */
         | 
| 21 26 | 
             
                PanZoomMode[PanZoomMode["RotationLocked"] = 32] = "RotationLocked";
         | 
| 22 27 | 
             
            })(PanZoomMode || (exports.PanZoomMode = PanZoomMode = {}));
         | 
| 23 28 | 
             
            class InertialScroller {
         | 
| @@ -65,6 +70,15 @@ class InertialScroller { | |
| 65 70 | 
             
                    }
         | 
| 66 71 | 
             
                }
         | 
| 67 72 | 
             
            }
         | 
| 73 | 
            +
            /**
         | 
| 74 | 
            +
             * This tool moves the viewport in response to touchpad, touchscreen, mouse, and keyboard events.
         | 
| 75 | 
            +
             *
         | 
| 76 | 
            +
             * Which events are handled, and which are skipped, are determined by the tool's `mode`. For example,
         | 
| 77 | 
            +
             * a `PanZoom` tool with `mode = PanZoomMode.TwoFingerTouchGestures|PanZoomMode.RightClickDrags` would
         | 
| 78 | 
            +
             * respond to right-click drag events and two-finger touch gestures.
         | 
| 79 | 
            +
             *
         | 
| 80 | 
            +
             * @see {@link setModeEnabled}
         | 
| 81 | 
            +
             */
         | 
| 68 82 | 
             
            class PanZoom extends BaseTool_1.default {
         | 
| 69 83 | 
             
                constructor(editor, mode, description) {
         | 
| 70 84 | 
             
                    super(editor.notifier, description);
         | 
| @@ -428,8 +442,32 @@ class PanZoom extends BaseTool_1.default { | |
| 428 442 | 
             
                isRotationLocked() {
         | 
| 429 443 | 
             
                    return !!(this.mode & PanZoomMode.RotationLocked);
         | 
| 430 444 | 
             
                }
         | 
| 431 | 
            -
                 | 
| 432 | 
            -
             | 
| 445 | 
            +
                /**
         | 
| 446 | 
            +
                 * Changes the types of gestures used by this pan/zoom tool.
         | 
| 447 | 
            +
                 *
         | 
| 448 | 
            +
                 * @see {@link PanZoomMode} {@link setMode}
         | 
| 449 | 
            +
                 *
         | 
| 450 | 
            +
                 * @example
         | 
| 451 | 
            +
                 * ```ts,runnable
         | 
| 452 | 
            +
                 * import { Editor, PanZoomTool, PanZoomMode } from 'js-draw';
         | 
| 453 | 
            +
                 *
         | 
| 454 | 
            +
                 * const editor = new Editor(document.body);
         | 
| 455 | 
            +
                 *
         | 
| 456 | 
            +
                 * // By default, there are multiple PanZoom tools that handle different events.
         | 
| 457 | 
            +
                 * // This gets all PanZoomTools.
         | 
| 458 | 
            +
                 * const panZoomToolList = editor.toolController.getMatchingTools(PanZoomTool);
         | 
| 459 | 
            +
                 *
         | 
| 460 | 
            +
                 * // The first PanZoomTool is the highest priority -- by default,
         | 
| 461 | 
            +
                 * // this tool is responsible for handling multi-finger touch gestures.
         | 
| 462 | 
            +
                 * //
         | 
| 463 | 
            +
                 * // Lower-priority PanZoomTools handle one-finger touch gestures and
         | 
| 464 | 
            +
                 * // key-presses.
         | 
| 465 | 
            +
                 * const panZoomTool = panZoomToolList[0];
         | 
| 466 | 
            +
                 *
         | 
| 467 | 
            +
                 * // Lock rotation for multi-finger touch gestures.
         | 
| 468 | 
            +
                 * panZoomTool.setModeEnabled(PanZoomMode.RotationLocked, true);
         | 
| 469 | 
            +
                 * ```
         | 
| 470 | 
            +
                 */
         | 
| 433 471 | 
             
                setModeEnabled(mode, enabled) {
         | 
| 434 472 | 
             
                    let newMode = this.mode;
         | 
| 435 473 | 
             
                    if (enabled) {
         | 
| @@ -440,6 +478,16 @@ class PanZoom extends BaseTool_1.default { | |
| 440 478 | 
             
                    }
         | 
| 441 479 | 
             
                    this.setMode(newMode);
         | 
| 442 480 | 
             
                }
         | 
| 481 | 
            +
                /**
         | 
| 482 | 
            +
                 * Sets all modes for this tool using a bitmask.
         | 
| 483 | 
            +
                 *
         | 
| 484 | 
            +
                 * @see {@link setModeEnabled}
         | 
| 485 | 
            +
                 *
         | 
| 486 | 
            +
                 * @example
         | 
| 487 | 
            +
                 * ```ts
         | 
| 488 | 
            +
                 * tool.setMode(PanZoomMode.RotationLocked|PanZoomMode.TwoFingerTouchGestures);
         | 
| 489 | 
            +
                 * ```
         | 
| 490 | 
            +
                 */
         | 
| 443 491 | 
             
                setMode(mode) {
         | 
| 444 492 | 
             
                    if (mode !== this.mode) {
         | 
| 445 493 | 
             
                        this.mode = mode;
         | 
| @@ -449,6 +497,10 @@ class PanZoom extends BaseTool_1.default { | |
| 449 497 | 
             
                        });
         | 
| 450 498 | 
             
                    }
         | 
| 451 499 | 
             
                }
         | 
| 500 | 
            +
                /**
         | 
| 501 | 
            +
                 * Returns a bitmask indicating the currently-enabled modes.
         | 
| 502 | 
            +
                 * @see {@link setModeEnabled}
         | 
| 503 | 
            +
                 */
         | 
| 452 504 | 
             
                getMode() {
         | 
| 453 505 | 
             
                    return this.mode;
         | 
| 454 506 | 
             
                }
         | 
| @@ -2,6 +2,9 @@ type ListenerResult = { | |
| 2 2 | 
             
                remove(): void;
         | 
| 3 3 | 
             
            };
         | 
| 4 4 | 
             
            type UpdateCallback<T> = (value: T) => void;
         | 
| 5 | 
            +
            type ReactiveValuesOf<T extends unknown[]> = {
         | 
| 6 | 
            +
                [key in keyof T]: ReactiveValue<T[key]>;
         | 
| 7 | 
            +
            };
         | 
| 5 8 | 
             
            /**
         | 
| 6 9 | 
             
             * A `ReactiveValue` is a value that
         | 
| 7 10 | 
             
             * - updates periodically,
         | 
| @@ -56,6 +59,7 @@ export declare abstract class ReactiveValue<T> { | |
| 56 59 | 
             
                 * Returns a reactive value derived from a single `source`.
         | 
| 57 60 | 
             
                 */
         | 
| 58 61 | 
             
                static map<A, B>(source: ReactiveValue<A>, map: (a: A) => B, inverseMap: (b: B) => A): MutableReactiveValue<B>;
         | 
| 62 | 
            +
                static union<Values extends [...unknown[]]>(values: ReactiveValuesOf<Values>): ReactiveValue<Values>;
         | 
| 59 63 | 
             
            }
         | 
| 60 64 | 
             
            export declare abstract class MutableReactiveValue<T> extends ReactiveValue<T> {
         | 
| 61 65 | 
             
                /**
         | 
| @@ -106,6 +106,11 @@ class ReactiveValue { | |
| 106 106 | 
             
                    }
         | 
| 107 107 | 
             
                    return result;
         | 
| 108 108 | 
             
                }
         | 
| 109 | 
            +
                static union(values) {
         | 
| 110 | 
            +
                    return ReactiveValue.fromCallback(() => {
         | 
| 111 | 
            +
                        return values.map(value => value.get());
         | 
| 112 | 
            +
                    }, values);
         | 
| 113 | 
            +
                }
         | 
| 109 114 | 
             
            }
         | 
| 110 115 | 
             
            exports.ReactiveValue = ReactiveValue;
         | 
| 111 116 | 
             
            class MutableReactiveValue extends ReactiveValue {
         | 
    
        package/dist/cjs/version.js
    CHANGED
    
    
| @@ -47,7 +47,9 @@ export default class SVGLoader { | |
| 47 47 | 
             
                    let fill = Color4.transparent;
         | 
| 48 48 | 
             
                    let stroke;
         | 
| 49 49 | 
             
                    // If possible, use computedStyles (allows property inheritance).
         | 
| 50 | 
            -
                     | 
| 50 | 
            +
                    // Chromium, however, sets .fill to a falsy, but not undefined value in some cases where
         | 
| 51 | 
            +
                    // styles are available. As such, use || instead of ??.
         | 
| 52 | 
            +
                    const fillAttribute = node.getAttribute('fill') ?? (computedStyles?.fill || node.style?.fill);
         | 
| 51 53 | 
             
                    if (fillAttribute) {
         | 
| 52 54 | 
             
                        try {
         | 
| 53 55 | 
             
                            fill = Color4.fromString(fillAttribute);
         | 
| @@ -166,7 +166,7 @@ export default class EditorImage { | |
| 166 166 | 
             
                 *
         | 
| 167 167 | 
             
                 * @internal
         | 
| 168 168 | 
             
                 */
         | 
| 169 | 
            -
                setDebugMode(newDebugMode: boolean): void;
         | 
| 169 | 
            +
                static setDebugMode(newDebugMode: boolean): void;
         | 
| 170 170 | 
             
                private static SetImportExportRectCommand;
         | 
| 171 171 | 
             
            }
         | 
| 172 172 | 
             
            /**
         | 
| @@ -214,6 +214,7 @@ export declare class ImageNode { | |
| 214 214 | 
             
                renderAllAsync(renderer: AbstractRenderer, preRenderComponent: PreRenderComponentCallback): Promise<boolean>;
         | 
| 215 215 | 
             
                render(renderer: AbstractRenderer, visibleRect?: Rect2): void;
         | 
| 216 216 | 
             
                renderDebugBoundingBoxes(renderer: AbstractRenderer, visibleRect: Rect2, depth?: number): void;
         | 
| 217 | 
            +
                private checkRep;
         | 
| 217 218 | 
             
            }
         | 
| 218 219 | 
             
            /** An `ImageNode` that can properly handle fullscreen/data components. @internal */
         | 
| 219 220 | 
             
            export declare class RootImageNode extends ImageNode {
         | 
| @@ -314,7 +314,7 @@ class EditorImage { | |
| 314 314 | 
             
                 *
         | 
| 315 315 | 
             
                 * @internal
         | 
| 316 316 | 
             
                 */
         | 
| 317 | 
            -
                setDebugMode(newDebugMode) {
         | 
| 317 | 
            +
                static setDebugMode(newDebugMode) {
         | 
| 318 318 | 
             
                    debugMode = newDebugMode;
         | 
| 319 319 | 
             
                }
         | 
| 320 320 | 
             
            }
         | 
| @@ -586,8 +586,8 @@ export class ImageNode { | |
| 586 586 | 
             
                            const nodeForChildren = new ImageNode(this);
         | 
| 587 587 | 
             
                            nodeForChildren.children = this.children;
         | 
| 588 588 | 
             
                            this.children = [nodeForNewLeaf, nodeForChildren];
         | 
| 589 | 
            -
                            nodeForChildren.recomputeBBox(true);
         | 
| 590 589 | 
             
                            nodeForChildren.updateParents();
         | 
| 590 | 
            +
                            nodeForChildren.recomputeBBox(true);
         | 
| 591 591 | 
             
                        }
         | 
| 592 592 | 
             
                        return nodeForNewLeaf.addLeaf(leaf);
         | 
| 593 593 | 
             
                    }
         | 
| @@ -604,6 +604,9 @@ export class ImageNode { | |
| 604 604 | 
             
                    const newNode = ImageNode.createLeafNode(this, leaf);
         | 
| 605 605 | 
             
                    this.children.push(newNode);
         | 
| 606 606 | 
             
                    newNode.recomputeBBox(true);
         | 
| 607 | 
            +
                    if (this.children.length >= this.targetChildCount) {
         | 
| 608 | 
            +
                        this.rebalance();
         | 
| 609 | 
            +
                    }
         | 
| 607 610 | 
             
                    return newNode;
         | 
| 608 611 | 
             
                }
         | 
| 609 612 | 
             
                // Creates a new leaf node with the given content.
         | 
| @@ -636,6 +639,7 @@ export class ImageNode { | |
| 636 639 | 
             
                            this.parent?.recomputeBBox(true);
         | 
| 637 640 | 
             
                        }
         | 
| 638 641 | 
             
                    }
         | 
| 642 | 
            +
                    this.checkRep();
         | 
| 639 643 | 
             
                }
         | 
| 640 644 | 
             
                // Grows this' bounding box to also include `other`.
         | 
| 641 645 | 
             
                // Always bubbles up.
         | 
| @@ -659,10 +663,12 @@ export class ImageNode { | |
| 659 663 | 
             
                        // Remove this' parent, if this' parent isn't the root.
         | 
| 660 664 | 
             
                        const oldParent = this.parent;
         | 
| 661 665 | 
             
                        if (oldParent.parent !== null) {
         | 
| 662 | 
            -
                             | 
| 663 | 
            -
                             | 
| 664 | 
            -
                            this.parent.children.push(this);
         | 
| 666 | 
            +
                            const newParent = oldParent.parent;
         | 
| 667 | 
            +
                            newParent.children = newParent.children.filter(c => c !== oldParent);
         | 
| 665 668 | 
             
                            oldParent.parent = null;
         | 
| 669 | 
            +
                            oldParent.children = [];
         | 
| 670 | 
            +
                            this.parent = newParent;
         | 
| 671 | 
            +
                            newParent.children.push(this);
         | 
| 666 672 | 
             
                            this.parent.recomputeBBox(false);
         | 
| 667 673 | 
             
                        }
         | 
| 668 674 | 
             
                        else if (this.content === null) {
         | 
| @@ -672,10 +678,63 @@ export class ImageNode { | |
| 672 678 | 
             
                            this.parent = null;
         | 
| 673 679 | 
             
                        }
         | 
| 674 680 | 
             
                    }
         | 
| 681 | 
            +
                    // Create virtual containers for children. Handles the case where there
         | 
| 682 | 
            +
                    // are many small, often non-overlapping children that we still want to be grouped.
         | 
| 683 | 
            +
                    if (this.children.length > this.targetChildCount * 10) {
         | 
| 684 | 
            +
                        const grid = this.getBBox().divideIntoGrid(4, 4);
         | 
| 685 | 
            +
                        const indexToCount = [];
         | 
| 686 | 
            +
                        while (indexToCount.length < grid.length) {
         | 
| 687 | 
            +
                            indexToCount.push(0);
         | 
| 688 | 
            +
                        }
         | 
| 689 | 
            +
                        for (const child of this.children) {
         | 
| 690 | 
            +
                            for (let i = 0; i < grid.length; i++) {
         | 
| 691 | 
            +
                                if (grid[i].containsRect(child.getBBox())) {
         | 
| 692 | 
            +
                                    indexToCount[i]++;
         | 
| 693 | 
            +
                                }
         | 
| 694 | 
            +
                            }
         | 
| 695 | 
            +
                        }
         | 
| 696 | 
            +
                        let indexWithGreatest = 0;
         | 
| 697 | 
            +
                        let greatestCount = indexToCount[0];
         | 
| 698 | 
            +
                        for (let i = 1; i < indexToCount.length; i++) {
         | 
| 699 | 
            +
                            if (indexToCount[i] > greatestCount) {
         | 
| 700 | 
            +
                                indexWithGreatest = i;
         | 
| 701 | 
            +
                                greatestCount = indexToCount[i];
         | 
| 702 | 
            +
                            }
         | 
| 703 | 
            +
                        }
         | 
| 704 | 
            +
                        const targetGridSquare = grid[indexWithGreatest];
         | 
| 705 | 
            +
                        // Avoid clustering if just a few children would be grouped.
         | 
| 706 | 
            +
                        // Unnecessary clustering can lead to unnecessarily nested nodes.
         | 
| 707 | 
            +
                        if (greatestCount > 4) {
         | 
| 708 | 
            +
                            const newChildren = [];
         | 
| 709 | 
            +
                            const childNodeChildren = [];
         | 
| 710 | 
            +
                            for (const child of this.children) {
         | 
| 711 | 
            +
                                if (targetGridSquare.containsRect(child.getBBox())) {
         | 
| 712 | 
            +
                                    childNodeChildren.push(child);
         | 
| 713 | 
            +
                                }
         | 
| 714 | 
            +
                                else {
         | 
| 715 | 
            +
                                    newChildren.push(child);
         | 
| 716 | 
            +
                                }
         | 
| 717 | 
            +
                            }
         | 
| 718 | 
            +
                            if (childNodeChildren.length < this.children.length) {
         | 
| 719 | 
            +
                                this.children = newChildren;
         | 
| 720 | 
            +
                                const child = new ImageNode(this);
         | 
| 721 | 
            +
                                this.children.push(child);
         | 
| 722 | 
            +
                                child.children = childNodeChildren;
         | 
| 723 | 
            +
                                child.updateParents(false);
         | 
| 724 | 
            +
                                child.recomputeBBox(false);
         | 
| 725 | 
            +
                                child.rebalance();
         | 
| 726 | 
            +
                            }
         | 
| 727 | 
            +
                        }
         | 
| 728 | 
            +
                    }
         | 
| 729 | 
            +
                    // Empty?
         | 
| 730 | 
            +
                    if (this.parent && this.children.length === 0 && this.content === null) {
         | 
| 731 | 
            +
                        this.remove();
         | 
| 732 | 
            +
                    }
         | 
| 675 733 | 
             
                }
         | 
| 676 734 | 
             
                // Removes the parent-to-child link.
         | 
| 677 735 | 
             
                // Called internally by `.remove`
         | 
| 678 736 | 
             
                removeChild(child) {
         | 
| 737 | 
            +
                    this.checkRep();
         | 
| 679 738 | 
             
                    const oldChildCount = this.children.length;
         | 
| 680 739 | 
             
                    this.children = this.children.filter(node => {
         | 
| 681 740 | 
             
                        return node !== child;
         | 
| @@ -685,6 +744,8 @@ export class ImageNode { | |
| 685 744 | 
             
                        child.rebalance();
         | 
| 686 745 | 
             
                    });
         | 
| 687 746 | 
             
                    this.recomputeBBox(true);
         | 
| 747 | 
            +
                    this.rebalance();
         | 
| 748 | 
            +
                    this.checkRep();
         | 
| 688 749 | 
             
                }
         | 
| 689 750 | 
             
                // Remove this node and all of its children
         | 
| 690 751 | 
             
                remove() {
         | 
| @@ -699,6 +760,7 @@ export class ImageNode { | |
| 699 760 | 
             
                    this.parent = null;
         | 
| 700 761 | 
             
                    this.content = null;
         | 
| 701 762 | 
             
                    this.children = [];
         | 
| 763 | 
            +
                    this.checkRep();
         | 
| 702 764 | 
             
                }
         | 
| 703 765 | 
             
                // Creates a (potentially incomplete) async rendering of this image.
         | 
| 704 766 | 
             
                // Returns false if stopped early
         | 
| @@ -765,11 +827,45 @@ export class ImageNode { | |
| 765 827 | 
             
                    const lineWidth = isLeaf ? 1 * pixelSize : 2 * pixelSize;
         | 
| 766 828 | 
             
                    renderer.drawRect(bbox.intersection(visibleRect), lineWidth, { fill });
         | 
| 767 829 | 
             
                    renderer.endObject();
         | 
| 830 | 
            +
                    if (bbox.maxDimension > visibleRect.maxDimension / 3) {
         | 
| 831 | 
            +
                        const textStyle = {
         | 
| 832 | 
            +
                            fontFamily: 'monospace',
         | 
| 833 | 
            +
                            size: bbox.minDimension / 20,
         | 
| 834 | 
            +
                            renderingStyle: { fill: Color4.red },
         | 
| 835 | 
            +
                        };
         | 
| 836 | 
            +
                        renderer.drawText(`Depth: ${depth}`, Mat33.translation(bbox.bottomLeft), textStyle);
         | 
| 837 | 
            +
                    }
         | 
| 768 838 | 
             
                    // Render debug information for children
         | 
| 769 839 | 
             
                    for (const child of this.children) {
         | 
| 770 840 | 
             
                        child.renderDebugBoundingBoxes(renderer, visibleRect, depth + 1);
         | 
| 771 841 | 
             
                    }
         | 
| 772 842 | 
             
                }
         | 
| 843 | 
            +
                checkRep(depth = 0) {
         | 
| 844 | 
            +
                    // Slow -- disabld by default
         | 
| 845 | 
            +
                    if (debugMode) {
         | 
| 846 | 
            +
                        if (this.parent && !this.parent.children.includes(this)) {
         | 
| 847 | 
            +
                            throw new Error(`Parent does not have this node as a child. (depth: ${depth})`);
         | 
| 848 | 
            +
                        }
         | 
| 849 | 
            +
                        let expectedBBox = null;
         | 
| 850 | 
            +
                        const seenChildren = new Set();
         | 
| 851 | 
            +
                        for (const child of this.children) {
         | 
| 852 | 
            +
                            expectedBBox ??= child.getBBox();
         | 
| 853 | 
            +
                            expectedBBox = expectedBBox.union(child.getBBox());
         | 
| 854 | 
            +
                            if (child.parent !== this) {
         | 
| 855 | 
            +
                                throw new Error(`Child with bbox ${child.getBBox()} and ${child.children.length} has wrong parent (was ${child.parent}).`);
         | 
| 856 | 
            +
                            }
         | 
| 857 | 
            +
                            // Children should only be present once
         | 
| 858 | 
            +
                            if (seenChildren.has(child)) {
         | 
| 859 | 
            +
                                throw new Error(`Child ${child} is present twice or more in its parent's child list`);
         | 
| 860 | 
            +
                            }
         | 
| 861 | 
            +
                            seenChildren.add(child);
         | 
| 862 | 
            +
                        }
         | 
| 863 | 
            +
                        const tolerance = this.bbox.minDimension / 100;
         | 
| 864 | 
            +
                        if (expectedBBox && !this.bbox.eq(expectedBBox, tolerance)) {
         | 
| 865 | 
            +
                            throw new Error(`Wrong bounding box ${expectedBBox} \\neq ${this.bbox} (depth: ${depth})`);
         | 
| 866 | 
            +
                        }
         | 
| 867 | 
            +
                    }
         | 
| 868 | 
            +
                }
         | 
| 773 869 | 
             
            }
         | 
| 774 870 | 
             
            ImageNode.idCounter = 0;
         | 
| 775 871 | 
             
            /** An `ImageNode` that can properly handle fullscreen/data components. @internal */
         | 
| @@ -64,12 +64,12 @@ export default class CanvasRenderer extends AbstractRenderer { | |
| 64 64 | 
             
                setDraftMode(draftMode) {
         | 
| 65 65 | 
             
                    if (draftMode) {
         | 
| 66 66 | 
             
                        this.minSquareCurveApproxDist = 9;
         | 
| 67 | 
            -
                        this.minRenderSizeBothDimens =  | 
| 68 | 
            -
                        this.minRenderSizeAnyDimen = 0. | 
| 67 | 
            +
                        this.minRenderSizeBothDimens = 1;
         | 
| 68 | 
            +
                        this.minRenderSizeAnyDimen = 0.1;
         | 
| 69 69 | 
             
                    }
         | 
| 70 70 | 
             
                    else {
         | 
| 71 71 | 
             
                        this.minSquareCurveApproxDist = 0.5;
         | 
| 72 | 
            -
                        this.minRenderSizeBothDimens = 0. | 
| 72 | 
            +
                        this.minRenderSizeBothDimens = 0.1;
         | 
| 73 73 | 
             
                        this.minRenderSizeAnyDimen = 1e-6;
         | 
| 74 74 | 
             
                    }
         | 
| 75 75 | 
             
                }
         | 
| @@ -238,7 +238,7 @@ export default class CanvasRenderer extends AbstractRenderer { | |
| 238 238 | 
             
                // @internal
         | 
| 239 239 | 
             
                isTooSmallToRender(rect) {
         | 
| 240 240 | 
             
                    // Should we ignore all objects within this object's bbox?
         | 
| 241 | 
            -
                    const diagonal = rect.size.times(this. | 
| 241 | 
            +
                    const diagonal = rect.size.times(this.getSizeOfCanvasPixelOnScreen());
         | 
| 242 242 | 
             
                    const bothDimenMinSize = this.minRenderSizeBothDimens;
         | 
| 243 243 | 
             
                    const bothTooSmall = Math.abs(diagonal.x) < bothDimenMinSize && Math.abs(diagonal.y) < bothDimenMinSize;
         | 
| 244 244 | 
             
                    const anyDimenMinSize = this.minRenderSizeAnyDimen;
         | 
| @@ -58,6 +58,7 @@ export interface ToolbarLocalization extends ToolbarUtilsLocalization { | |
| 58 58 | 
             
                errorImageHasZeroSize: string;
         | 
| 59 59 | 
             
                describeTheImage: string;
         | 
| 60 60 | 
             
                fileInput__loading: string;
         | 
| 61 | 
            +
                fileInput__andNMoreFiles: (count: number) => string;
         | 
| 61 62 | 
             
                penDropdown__baseHelpText: string;
         | 
| 62 63 | 
             
                penDropdown__colorHelpText: string;
         | 
| 63 64 | 
             
                penDropdown__thicknessHelpText: string;
         | 
| @@ -59,6 +59,7 @@ export const defaultToolbarLocalization = { | |
| 59 59 | 
             
                errorImageHasZeroSize: 'Error: Image has zero size',
         | 
| 60 60 | 
             
                describeTheImage: 'Image description',
         | 
| 61 61 | 
             
                fileInput__loading: 'Loading...',
         | 
| 62 | 
            +
                fileInput__andNMoreFiles: (n) => `(...${n} more)`,
         | 
| 62 63 | 
             
                // Help text
         | 
| 63 64 | 
             
                penDropdown__baseHelpText: 'This tool draws shapes or freehand lines.',
         | 
| 64 65 | 
             
                penDropdown__colorHelpText: 'Changes the pen\'s color',
         | 
| @@ -29,6 +29,12 @@ export class ImageWrapper { | |
| 29 29 | 
             
                isChanged() {
         | 
| 30 30 | 
             
                    return this.imageBase64Url !== this.originalSrc;
         | 
| 31 31 | 
             
                }
         | 
| 32 | 
            +
                // Returns true if the current image is large enough to display a "decrease size"
         | 
| 33 | 
            +
                // option.
         | 
| 34 | 
            +
                isLarge() {
         | 
| 35 | 
            +
                    const largeImageThreshold = 0.12 * 1024 * 1024; // 0.12 MiB
         | 
| 36 | 
            +
                    return this.getBase64Url().length > largeImageThreshold;
         | 
| 37 | 
            +
                }
         | 
| 32 38 | 
             
                getBase64Url() {
         | 
| 33 39 | 
             
                    return this.imageBase64Url;
         | 
| 34 40 | 
             
                }
         | 
| @@ -37,6 +43,7 @@ export class ImageWrapper { | |
| 37 43 | 
             
                }
         | 
| 38 44 | 
             
                setAltText(text) {
         | 
| 39 45 | 
             
                    this.altText = text;
         | 
| 46 | 
            +
                    this.preview.alt = text;
         | 
| 40 47 | 
             
                }
         | 
| 41 48 | 
             
                static fromSrcAndPreview(initialBase64Src, preview, onUrlUpdate) {
         | 
| 42 49 | 
             
                    return new ImageWrapper(initialBase64Src, preview, onUrlUpdate);
         |