modules/drag.js

/*
 * This file is part of AUX.
 *
 * AUX is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * AUX is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General
 * Public License along with this program; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA  02110-1301  USA
 */

import { Range } from './range.js';
import { DragValue } from './dragvalue.js';
import { Base } from '../implements/base.js';
import { addClass, removeClass } from '../utils/dom.js';

function extractMatrix(t) {
  const a = t.indexOf('matrix(');
  if (a < 0) return;
  t = t.substring(a + 7);
  return t
    .split(')')[0]
    .split(',')
    .map(function (v) {
      return parseInt(v.trim());
    });
}

function xyFromTransform(t) {
  const mx = extractMatrix(t);
  return !mx || !mx.length ? [0, 0] : [mx[4], mx[5]];
}

function startDrag(e) {
  this._dragged = 0;
  const O = this.options;
  if (!O.active) return;
  if (e.button !== void 0 && e.button > 0) return;
  this._xstart = this._xlast = e.pageX;
  this._ystart = this._ylast = e.pageY;
  if (O.transform) {
    const xy = xyFromTransform(this._style.transform);
    this._xpos = xy[0];
    this._ypos = xy[1];
  } else {
    this._xpos = O.node.offsetLeft;
    this._ypos = O.node.offsetTop;
  }
  addClass(O.node, 'aux-dragging');
}
function stopDrag(e) {
  if (!this.options.active) return;
  if (e.button !== void 0 && e.button > 0) return;
  removeClass(this.options.node, 'aux-dragging');
}
function dragging(e) {
  const O = this.options;
  if (!O.active) return;
  if (e.button !== void 0 && e.button > 0) return;
  this._dragged +=
    (Math.abs(e.pageX - this._xlast) + Math.abs(e.pageY - this._ylast)) / 2;
  if (this._dragged < O.initial) return;
  this._xlast = e.pageX;
  this._ylast = e.pageY;
  let x = this._xpos + e.pageX - this._xstart;
  let y = this._ypos + e.pageY - this._ystart;
  if (O.min.x !== false) x = Math.max(O.min.x, x);
  if (O.max.x !== false) x = Math.min(O.max.x, x);
  if (O.min.y !== false) y = Math.max(O.min.y, y);
  if (O.max.y !== false) y = Math.min(O.max.y, y);
  if (O.transform) {
    const t = this._style.transform;
    const mx = extractMatrix(t);
    mx[4] = x;
    mx[5] = y;
    const nt = t.replace(/matrix\([0-9 ,]*\)/, 'matrix(' + mx.join(',') + ')');
    O.node.style.transform = nt;
  } else {
    O.node.style.top = y + 'px';
    O.node.style.left = x + 'px';
  }
}
function setHandle() {
  const h = this.options.handle;
  if (this.drag) this.drag.destroy();
  const range = new Range({});
  this.drag = new DragValue(this, {
    node: h,
    range: function () {
      return range;
    },
    get: function () {
      return 0;
    },
    set: function () {
      return;
    },
  });
}
/**
 * Drag enables dragging of elements on the screen positioned absolutely or by CSS transformation.
 *
 * @param {Object} [options={ }] - An object containing initial options.
 *
 * @property {HTMLElement|SVGElement} options.node - The element to drag.
 * @property {HTMLElement|SVGElement} [options.handle] A DOM node to be used as a handle. If not set, <code>options.node</code> is used.
 * @property {Boolean} [options.active=true] - Enable or disable dragging
 * @property {Object|Boolean} [options.min={x: false, y: false}] - Object containing the minimum positions for x and y. A value of <code>false</code> is interpreted as no minimum.
 * @property {Object|Boolean} [options.max={x: false, y: false}] - Object containing the maximum positions for x and y. A value of <code>false</code> is interpreted as no maximum.
 * @property {Number} [options.initial=2] - Number of pixels the user has to move until dragging starts.
 * @property {Boolean} [options.transform=false] - Use CSS transformations instead of absolute positioning.
 *
 * @extends Base
 *
 * @class Drag
 */
/**
 * The user is dragging this item.
 *
 * @event Drag#dragging
 *
 * @param {DOMEvent} event - The native DOM event.
 */
/**
 * The user started dragging this item.
 *
 * @event Drag#startdrag
 *
 * @param {DOMEvent} event - The native DOM event.
 */
/**
 * The user stopped dragging this item.
 *
 * @event Drag#stopdrag
 *
 * @param {DOMEvent} event - The native DOM event.
 */
export class Drag extends Base {
  static get _options() {
    return {
      node: 'object',
      handle: 'object',
      active: 'boolean',
      min: 'object',
      max: 'object',
      initial: 'number',
      transform: 'boolean',
    };
  }

  static get options() {
    return {
      node: null,
      handle: null,
      active: true,
      min: { x: false, y: false },
      max: { x: false, y: false },
      initial: 2,
      transform: false,
    };
  }

  static get static_events() {
    return {
      startdrag: startDrag,
      dragging: dragging,
      stopdrag: stopDrag,
    };
  }

  initialize(options) {
    super.initialize(options);
    this.set('handle', this.options.handle);
    this.set('node', this.options.node);
  }

  // GETTERS & SETTERS
  set(key, value) {
    if (key === 'node') this._style = window.getComputedStyle(value);
    if (key === 'handle' && !value) value = this.options.node;

    super.set(key, value);

    if (key === 'handle') setHandle.call(this);
    if (key === 'initial' && this.drag) this.drag.set('initial', value);
  }
}