Home Reference Source Repository

src/es6/d3.bbox.js

// Copyright (c) 2015 Lucas Beyer
// Licensed under the MIT License (MIT)
// Version 1.0
// https://github.com/lucasb-eyer/d3-boundingbox

// ES6 "translation" by (c) 2015 Nick Bollweg

import {d3} from "nbpresent-deps";

export function bbox() {
    // All those are initialized to default further down using the setters.
    var xextent = null
    var yextent = null
    var handlesize = null
    var dirs = null
    var curs = null
    var cbs = {
        dragstart: null,
        dragmove: null,
        dragend: null,
        resizestart: null,
        resizemove: null,
        resizeend: null
    }

    function my(selection) {
        var drag = d3.behavior.drag()
            .origin(function(d, i) { return {x: this.getAttribute("x"), y: this.getAttribute("y")}; })
            .on("drag.lbbbox", dragmove)
            .on("dragstart.lbbbox", dragstart)
            .on("dragend.lbbbox", dragend)
        selection.call(drag)
        selection.on("mousemove.lbbbox", move)
        selection.on("mouseleave.lbbbox", leave)

        return selection
    }

    function clamp(x, extent) { return Math.max(extent[0], Math.min(x, extent[1])); }
    function inside(x, extent) { return extent[0] < x && x < extent[1]; }

    // Will return w, nw, n, ne, e, se, s, sw for the eight borders,
    // M for inside, or "" when current location not in `dirs`.
    function whichborder(xy, elem) {
        var border = ""
        var x = +elem.getAttribute("x")
        var y = +elem.getAttribute("y")
        var w = +elem.getAttribute("width")
        var h = +elem.getAttribute("height")

             if(xy[1] < y + handlesize.n) border += 'n'
        else if(xy[1] > y + h - handlesize.s) border += 's'

             if(xy[0] < x + handlesize.w) border += 'w'
        else if(xy[0] > x + w - handlesize.e) border += 'e'


        if(border == "" && (dirs.indexOf("x") > -1 || dirs.indexOf("y") > -1))
            border = "M"
        else if(dirs.indexOf(border) == -1)
            border = ""

        return border
    }

    function move(d, i) {
        // Don't do anything if we're currently dragging.
        // Otherwise, the cursor might jump horribly!
        // Also don't do anything if no cursors.
        if(this.__resize_action__ !== undefined || !curs)
            return

        var b = whichborder(d3.mouse(this), this)

        var x = dirs.indexOf("x")
        var y = dirs.indexOf("y")
        // Bwahahahaha this works even when one is at index 0.
        if(b == "M" && 1/(x*y) < 0)
            document.body.style.cursor = x >= 0 ? curs.x : curs.y
        else
            document.body.style.cursor = curs[b] || null
    }

    function leave(d, i) {
        // Only unset cursor if we're not dragging,
        // otherwise we get horrible cursor-flipping action.
        // Also only unset it if we actually did set it!
        if(this.__resize_action__ === undefined && curs)
            document.body.style.cursor = null
    }

    function dragstart(d, i) {
        this.__resize_action__ = whichborder(d3.mouse(this), this)
        this.__ow__ = +this.getAttribute("width")
        this.__oh__ = +this.getAttribute("height")

        if(this.__resize_action__ == "M") {
            if(cbs.dragstart) cbs.dragstart.call(this, d, i)
        } else if(this.__resize_action__.length) {
            if(cbs.resizestart) cbs.resizestart.call(this, d, i)
        }
    }

    function dragend(d, i) {
        if(this.__resize_action__ == "M") {
            if(cbs.dragend) cbs.dragend.call(this, d, i)
        } else if(this.__resize_action__.length) {
            if(cbs.resizeend) cbs.resizeend.call(this, d, i)
        }

        delete this.__resize_action__
        delete this.__ow__
        delete this.__oh__

        // Still need to unset here, in case the user stop dragging
        // while the mouse isn't on the element anymore (e.g. off-limits).
        if(curs)
            document.body.style.cursor = null
    }

    function dragmove(d, i) {
        if(this.__resize_action__ == "M") {
            if(cbs.dragmove)
                if(false === cbs.dragmove.call(this, d, i))
                    return
        } else if(this.__resize_action__.length) {
            if(cbs.resizemove)
                if(false === cbs.resizemove.call(this, d, i))
                    return
        }

        // Potentially dynamically determine the allowed space.
        var xext = typeof xextent === "function" ? xextent.call(this, d, i) : xextent
        var yext = typeof yextent === "function" ? yextent.call(this, d, i) : yextent

        // Handle moving around first, more easily.
        if(this.__resize_action__ == "M") {
            if(dirs.indexOf("x") > -1 && d3.event.dx != 0)
                // This is so that even moving the mouse super-fast, this still "sticks" to the extent.
                this.setAttribute("x", clamp(clamp(d3.event.x, xext) + this.__ow__, xext) - this.__ow__)
            if(dirs.indexOf("y") > -1 && d3.event.dy != 0)
                this.setAttribute("y", clamp(clamp(d3.event.y, yext) + this.__oh__, yext) - this.__oh__)
        // Now check for all possible resizes.
        } else {
            var x = +this.getAttribute("x")
            var y = +this.getAttribute("y")

            // First, check for vertical resizes,
            if(/^n/.test(this.__resize_action__)) {
                var b = y + +this.getAttribute("height")
                var newy = clamp(clamp(d3.event.y, yext), [-Infinity, b-1])
                this.setAttribute("y", newy)
                this.setAttribute("height", b - newy)
            } else if(/^s/.test(this.__resize_action__)) {
                var b = clamp(d3.event.y + this.__oh__, yext)
                this.setAttribute("height", clamp(b - y, [1, Infinity]))
            }

            // and then for horizontal ones. Note both may happen.
            if(/w$/.test(this.__resize_action__)) {
                var r = x + +this.getAttribute("width")
                var newx = clamp(clamp(d3.event.x, xext), [-Infinity, r-1])
                this.setAttribute("x", newx)
                this.setAttribute("width", r - newx)
            } else if(/e$/.test(this.__resize_action__)) {
                var r = clamp(d3.event.x + this.__ow__, xext)
                this.setAttribute("width", clamp(r - x, [1, Infinity]))
            }
        }
    }

    my.xextent = function(_) {
        if(!arguments.length) return xextent
        xextent = _ !== false ? _ : [-Infinity, +Infinity]
        return my
    }
    my.xextent(false)

    my.yextent = function(_) {
        if(!arguments.length) return yextent
        yextent = _ !== false ? _ : [-Infinity, +Infinity]
        return my
    }
    my.yextent(false)

    my.handlesize = function(_) {
        if(!arguments.length) return handlesize
        handlesize = !+_ ? _ : {'w': _,'n': _,'e': _,'s': _}  // coolface
        return my
    }
    my.handlesize(3)

    my.cursors = function(_) {
        if(!arguments.length) return curs
        curs = _ !== true ? _ : {
            M: "move",
            x: "col-resize",
            y: "row-resize",
            n: "n-resize",
            e: "e-resize",
            s: "s-resize",
            w: "w-resize",
            nw: "nw-resize",
            ne: "ne-resize",
            se: "se-resize",
            sw: "sw-resize"
        }
        return my
    }
    my.cursors(true)

    my.directions = function(_) {
        if(!arguments.length) return dirs
        dirs = _ !== true ? _ : ["n", "e", "s", "w", "nw", "ne", "se", "sw", "x", "y"]
        return my
    }
    my.directions(true)

    my.on = function(name, cb) {
        if(cb === undefined) return cbs[name]
        cbs[name] = cb
        return my
    }

    my.infect = function(selection) {
        selection.call(my)
        return my
    }

    my.disinfect = function(selection) {
        selection.on(".drag", null)
        selection.on(".lbbbox", null)
        return my
    }

    return my
}