• Jump To … +
    ./source/asset-management/image-asset.js ./source/asset-management/noise-asset.js ./source/asset-management/raw-asset.js ./source/asset-management/reaction-diffusion-asset.js ./source/asset-management/sprite-asset.js ./source/asset-management/video-asset.js ./source/core/animation-loop.js ./source/core/display-cycle.js ./source/core/document.js ./source/core/events.js ./source/core/init.js ./source/core/library.js ./source/core/snippets.js ./source/core/user-interaction.js ./source/factory/action.js ./source/factory/anchor.js ./source/factory/animation.js ./source/factory/bezier.js ./source/factory/block.js ./source/factory/button.js ./source/factory/canvas.js ./source/factory/cell.js ./source/factory/cog.js ./source/factory/color.js ./source/factory/conic-gradient.js ./source/factory/crescent.js ./source/factory/element.js ./source/factory/emitter.js ./source/factory/enhanced-label.js ./source/factory/filter.js ./source/factory/gradient.js ./source/factory/grid.js ./source/factory/group.js ./source/factory/label.js ./source/factory/line-spiral.js ./source/factory/line.js ./source/factory/loom.js ./source/factory/mesh.js ./source/factory/net.js ./source/factory/oval.js ./source/factory/particle-force.js ./source/factory/particle-spring.js ./source/factory/particle-world.js ./source/factory/particle.js ./source/factory/pattern.js ./source/factory/picture.js ./source/factory/polygon.js ./source/factory/polyline.js ./source/factory/quadratic.js ./source/factory/radial-gradient.js ./source/factory/rectangle.js ./source/factory/render-animation.js ./source/factory/shape.js ./source/factory/spiral.js ./source/factory/stack.js ./source/factory/star.js ./source/factory/tetragon.js ./source/factory/ticker.js ./source/factory/tracer.js ./source/factory/tween.js ./source/factory/unstacked-element.js ./source/factory/wheel.js ./source/helper/array-pool.js ./source/helper/color-engine.js ./source/helper/document-root-elements.js ./source/helper/filter-engine-bluenoise-data.js ./source/helper/filter-engine.js ./source/helper/random-seed.js ./source/helper/shape-path-calculation.js ./source/helper/shared-vars.js ./source/helper/system-flags.js ./source/helper/utilities.js ./source/helper/workstore.js ./source/mixin/anchor.js ./source/mixin/asset-advanced-functionality.js ./source/mixin/asset-consumer.js ./source/mixin/asset.js ./source/mixin/base.js ./source/mixin/button.js ./source/mixin/cascade.js ./source/mixin/cell-key-functions.js ./source/mixin/delta.js ./source/mixin/display-shape.js ./source/mixin/dom.js ./source/mixin/entity.js ./source/mixin/filter.js ./source/mixin/hidden-dom-elements.js ./source/mixin/mimic.js ./source/mixin/path.js ./source/mixin/pattern.js ./source/mixin/pivot.js ./source/mixin/position.js ./source/mixin/shape-basic.js ./source/mixin/shape-curve.js ./source/mixin/styles.js ./source/mixin/text.js ./source/mixin/tween.js ./source/scrawl.js ./source/untracked-factory/cell-fragment.js ./source/untracked-factory/coordinate.js ./source/untracked-factory/drag-zone.js ./source/untracked-factory/keyboard-zone.js ./source/untracked-factory/observe-update.js ./source/untracked-factory/palette.js ./source/untracked-factory/particle-history.js ./source/untracked-factory/quaternion.js ./source/untracked-factory/state.js ./source/untracked-factory/text-style.js ./source/untracked-factory/vector.js
  • §

    Drag-and-drop zones.

    makeDragZone is an attempt to make setting up drag-and-drop functionality within a Scrawl-canvas stack or canvas as simple as possible. The functionality has been designed to allow multiple drag zones to be set on a canvas or stack element, while limiting the number of event listeners that need to be applied to the element to make the magic happen.

    Required attribute of the argument object:

    • .zone - either the String name of the Stack or Canvas artefact which will host the zone, or the Stack or Canvas artefact itself

    Additional, optional, attributes in the argument object

    • .coordinateSource - an object containing a here object
    • .coordinateSource - an object containing a here object
    • .collisionGroup - String name of Group object, or Group object itself
    • .startOn - Array of Strings
    • .endOn - Array of Strings
    • .exposeCurrentArtefact - Boolean (default: false)
    • .preventTouchDefaultWhenDragging - Boolean (default: false)
    • .resetCoordsToZeroOnTouchEnd - Boolean (default: true)

    At the heart of the drag zone are a set of functions (or alternatively, objects that can be applied to the selected artefact) which define the actions that happen when a user starts, continues and stops dragging an artefact in the zone. It is possible to set different actions depending on whether the user is pressing the shift button while they perform the drag action:

    • .updateOnStart, .updateOnShiftStart - Function, or a set object to be applied to the current artefact
    • .updateOnEnd, .updateOnShiftEnd - Function, or a set object to be applied to the current artefact
    • .updateWhileMoving, .updateWhileShiftMoving - Function to be run while drag is in progress
    • .updateOnPrematureExit - Function that will run when checks (defined in code elsewhere) trigger a premature exit from the drag zone. The triggering code can be used, for instance, to make sure an artefact does not leave a given area of the cell, within the canvas element boundaries

    Multiple drag zones can be defined on a canvas or stack element. When a user presses the mouse button to start a drag the code will interrogate each drag zone in turn to discover if one of them has an artefact which collides with the mouse cursor; if yes, that drag zone takes precedence for the subsequent move and drop actions

    • .processingOrder - positive integer Number - drag zones with a lower value will be interrogated ahead of those with a higher value

    If the exposeCurrentArtefact attribute is true, the makeDragZone factory returns a function that can be invoked at any time to get the collision data object (containing x, y, artefact attributes) for the artefact being dragged (false if nothing is being dragged). The function can also be triggered with the following string arguments:

    • 'exit' or 'drop - force the drag action to end
    • any other truthy argument - kill the drag zone object. If it was the only drag zone object associated with a given canvas or stack element then the event listeners will also be removed from the element
    • null, undefined, any other falsy argument - return the collision data object

    If the exposeCurrentArtefact attribute is false, or omitted, the function returns a kill function that can be invoked (with no arguments) to remove the event listeners from the Stack or Canvas zone’s DOM element.

    import * as library from "../core/library.js";
    
    import { xta, isa_fn, isa_boolean, isa_obj, λnull, Ωempty } from "../helper/utilities.js";
    
    import { addListener, removeListener } from "../core/events.js";
    
    import { touchAction } from "../core/user-interaction.js";
  • §

    Shared constants

    import { _isArray, $BODY, ACCEPTED_WRAPPERS, DOWN, MOVE, T_CANVAS, T_GROUP, TOUCH_CANCEL, TOUCH_END, TOUCH_MOVE, TOUCH_START, UP } from "../helper/shared-vars.js";
  • §

    Local constants

    const DROP = 'drop',
        EXIT = 'exit';
  • §

    NOTE: drag-and-drop functionality using this factory function is not guaranteed for artefacts referencing a path, or for artefacts whose reference artefact in turn references another artefact in any way.

    const dragZones = {};
  • §

    Generate the drag zone and associate it with the zone element

    const processDragZoneData = function (items = Ωempty, doAddListeners, doRemoveListeners) {
    
        let {
            zone,
            coordinateSource,
            collisionGroup,
            startOn,
            endOn,
            updateOnStart,
            updateOnEnd,
            updateWhileMoving,
            updateOnShiftStart,
            updateOnShiftEnd,
            updateWhileShiftMoving,
            updateOnPrematureExit,
            exposeCurrentArtefact,
            preventTouchDefaultWhenDragging,
            resetCoordsToZeroOnTouchEnd,
            processingOrder,
        } = items;
  • §

    zone is required

    • must be either a Canvas or Stack wrapper, or a wrapper’s String name
        if (!zone) return new Error('dragZone constructor - no zone supplied');
    
        if (zone.substring) zone = library.artefact[zone];
    
        if (!zone || !ACCEPTED_WRAPPERS.includes(zone.type)) return new Error('dragZone constructor - zone object is not a Stack or Canvas wrapper');
    
        const target = zone.domElement;
    
        if (!target) return new Error('dragZone constructor - zone does not contain a target DOM element');
  • §

    collisionGroup is optional; defaults to zone’s namesake group

    • must be a Group object
        if (!collisionGroup) {
    
            if (zone.type === T_CANVAS) collisionGroup = library.group[zone.base.name];
            else collisionGroup = library.group[zone.name];
        }
        else if (collisionGroup.substring) collisionGroup = library.group[collisionGroup];
    
        if (!collisionGroup || collisionGroup.type !== T_GROUP) return new Error('dragZone constructor - unable to recover collisionGroup group');
  • §

    coordinateSource will be an object containing x and y attributes

    • default’s to the zone’s here object
        if (coordinateSource) {
    
            if (coordinateSource.here) coordinateSource = coordinateSource.here;
            else if (!xta(coordinateSource.x, coordinateSource.y)) coordinateSource = false;
        }
        else {
    
            if (zone.type === T_CANVAS) coordinateSource = zone.base.here;
            else coordinateSource = zone.here;
        }
    
        if (!coordinateSource) return new Error('dragZone constructor - unable to discover a usable coordinateSource object');
  • §

    startOn, endOn - if supplied, then need to be arrays

        if (!_isArray(startOn)) startOn = [DOWN];
        if (!_isArray(endOn)) endOn = [UP];
    
        if (exposeCurrentArtefact == null) exposeCurrentArtefact = false;
        if (preventTouchDefaultWhenDragging == null) preventTouchDefaultWhenDragging = false;
        if (resetCoordsToZeroOnTouchEnd == null) resetCoordsToZeroOnTouchEnd = true;
  • §

    updateOnStart, updateOnEnd, updateOnShiftStart, updateOnShiftEnd - if supplied, then needs to be key:value objects which will be applied to the entity (using set) or, alternatively, a callback function

        if (isa_obj(updateOnStart)) updateOnStart = function () { current.artefact.set(items.updateOnStart) };
        if (!isa_fn(updateOnStart)) updateOnStart = λnull;
    
        if (isa_obj(updateOnShiftStart)) updateOnShiftStart = function () { current.artefact.set(items.updateOnShiftStart) };
        if (!isa_fn(updateOnShiftStart)) updateOnShiftStart = updateOnStart;
    
        if (isa_obj(updateOnEnd)) updateOnEnd = function () { current.artefact.set(items.updateOnEnd) };
        if (!isa_fn(updateOnEnd)) updateOnEnd = λnull;
    
        if (isa_obj(updateOnShiftEnd)) updateOnShiftEnd = function () { current.artefact.set(items.updateOnShiftEnd) };
        if (!isa_fn(updateOnShiftEnd)) updateOnShiftEnd = updateOnEnd;
  • §

    updateWhileMoving, updateWhileShiftMoving - if supplied, needs to be a callback function

        if (!isa_fn(updateWhileMoving)) updateWhileMoving = λnull;
        if (!isa_fn(updateWhileShiftMoving)) updateWhileShiftMoving = updateWhileMoving;
  • §

    updateOnPrematureExit - if supplied, needs to be a callback function

        if (!isa_fn(updateOnPrematureExit)) updateOnPrematureExit = λnull;
  • §

    exposeCurrentArtefact - if supplied, then needs to be a boolean

        if (!isa_boolean(exposeCurrentArtefact)) exposeCurrentArtefact = false;
    
        if (processingOrder == null) processingOrder = 0;
  • §

    We can only drag one artefact at a time; that artefact - alongside the hit coordinate’s x and y values - is stored in the current variable

        let current = false;
    
        const checkE = function (e) {
    
            if (e && e.cancelable) {
    
                if (preventTouchDefaultWhenDragging && current) {
    
                    e.preventDefault();
                    e.returnValue = false;
                }
                else if (!preventTouchDefaultWhenDragging) {
    
                    e.preventDefault();
                    e.returnValue = false;
                }
            }
        };
    
        const pickup = function (e = Ωempty) {
    
            checkE(e);
    
            const type = e.type;
            if (type === TOUCH_START || type === TOUCH_CANCEL) touchAction(e, resetCoordsToZeroOnTouchEnd);
    
            current = collisionGroup.getArtefactAt(coordinateSource);
    
            if (current) {
    
                current.artefact.pickupArtefact(coordinateSource);
                if (e.shiftKey) updateOnShiftStart(e);
                else updateOnStart(e);
            }
            return {
                current,
                move,
                drop,
            };
        };
    
        const move = function (e = Ωempty) {
    
            if (current) {
    
                checkE(e);
    
                const type = e.type;
                if (type === TOUCH_MOVE) touchAction(e);
    
                if (e.shiftKey) updateWhileShiftMoving(e);
                else updateWhileMoving(e);
            }
        };
    
        const drop = function (e = Ωempty) {
    
            if (current) {
    
                checkE(e);
    
                const type = e.type;
                if (type === TOUCH_END) {
    
                    touchAction(e, resetCoordsToZeroOnTouchEnd);
                }
    
                current.artefact.dropArtefact();
                if (e.shiftKey) updateOnShiftEnd(e);
                else updateOnEnd(e);
                current = false;
            }
        };
    
        if (!dragZones[zone.name]) {
    
            dragZones[zone.name] = [];
            doAddListeners(startOn, endOn, target);
        }
    
        const kill = function () {
    
            const name = `${zone.name}_${collisionGroup.name}_${processingOrder}`;
    
            dragZones[zone.name] = dragZones[zone.name].filter(z => z.name !== name);
    
            if (!dragZones[zone.name].length) {
                doRemoveListeners(startOn, endOn, target);
                delete dragZones[zone.name];
            }
        };
    
        const getCurrent = function (actionKill) {
    
            if (actionKill) {
    
                if (actionKill === EXIT || actionKill === DROP) {
    
                    drop();
                    updateOnPrematureExit();
                }
                else kill();
            }
            else return current;
        };
    
        const data = {
            name: `${zone.name}_${collisionGroup.name}_${processingOrder}`,
            exposeCurrentArtefact,
            target,
            processingOrder,
            pickup,
            move,
            drop,
            kill,
            getCurrent,
        };
    
        dragZones[zone.name].push(data);
    
        dragZones[zone.name].sort((a, b) => a.processingOrder - b.processingOrder);
    
        return {
            exposeCurrentArtefact,
            getCurrent,
            kill,
            zone,
        };
    };
  • §

    Exported function (to modules and the SC object). Add drag-and-drop functionality to a canvas or stack wrapper.

    export const makeDragZone = function (items = Ωempty) {
  • §

    The exposed pickup function will search for the target element (if user has started the drag while the mouse cursor was over a child of the Stack wrapper) and then interrogate each drag zone associated with that target until it finds a zone reporting a hit

        const pickup = (e = Ωempty) => {
    
            if (e && e.target) {
  • §

    name cannot be ZERO_STR

                let myTarget = e.target,
                    name = '';
    
                while (!name) {
    
                    if (dragZones[myTarget.id]) name = myTarget.id;
                    if (myTarget.tagName === $BODY) break;
                    myTarget = myTarget.parentElement;
                }
    
                const variants = dragZones[name];
    
                if (variants) {
    
                    for (let i = 0, iz = variants.length; i < iz; i++) {
    
                        const v = variants[i];
    
                        const res = v.pickup(e);
    
                        if (res.current) {
    
                            currentMove = res.move;
                            currentDrop = res.drop;
                            break;
                        }
                    }
                }
            }
        };
  • §

    The exposed move and drop functions will change to match the equivalent funtions supplied by the zone selected during the pickup stage of the operation

        let currentMove = λnull;
        const move = (e = Ωempty) => {
    
            currentMove(e);
        };
    
        let currentDrop = λnull;
        const drop = (e = Ωempty) => {
    
            currentDrop(e);
            currentMove = λnull;
            currentDrop = λnull;
        };
  • §

    Listeners are added to the DOM element when the first drag zone is created for that target

        const doAddListeners = (startOn, endOn, target) => {
    
            addListener(startOn, pickup, target);
            addListener(MOVE, move, target);
            addListener(endOn, drop, target);
        };
  • §

    Listeners are only removed from the DOM element when all the drag zones associated with that target have been killed

        const doRemoveListeners = (startOn, endOn, target) => {
    
            removeListener(startOn, pickup, target);
            removeListener(MOVE, move, target);
            removeListener(endOn, drop, target);
        };
    
        const processedData = processDragZoneData(items, doAddListeners, doRemoveListeners);
  • §

    Return the appropriate function based on the value of the exposeCurrentArtefact attribute

        if (processedData.exposeCurrentArtefact) return processedData.getCurrent;
        else return processedData.kill;
    };