Files
librenms-librenms/src/jquery.draggable.js
T
vieron 79aff38c60 fix(draggable): namespace events with unique ids
Until now if you had multiple instances of Draggable in the same page,
when destroying one of them, all of the events binded to other
instances were also destroyed.

So, we are namespacing event names with unique id’s per instance to ensure
only that only the corresponding events are unbinded.

The other solution is to cache references to the event handlers, but most
of them are proxied or throttled which makes more mess...
2014-09-25 12:26:15 +02:00

433 lines
13 KiB
JavaScript

/*
* jquery.draggable
* https://github.com/ducksboard/gridster.js
*
* Copyright (c) 2012 ducksboard
* Licensed under the MIT licenses.
*/
;(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define('gridster-draggable', ['jquery'], factory);
} else {
root.GridsterDraggable = factory(root.$ || root.jQuery);
}
}(this, function($) {
var defaults = {
items: 'li',
distance: 1,
limit: true,
offset_left: 0,
autoscroll: true,
ignore_dragging: ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'], // or function
handle: null,
container_width: 0, // 0 == auto
move_element: true,
helper: false, // or 'clone'
remove_helper: true
// drag: function(e) {},
// start : function(e, ui) {},
// stop : function(e) {}
};
var $window = $(window);
var dir_map = { x : 'left', y : 'top' };
var isTouch = !!('ontouchstart' in window);
var capitalize = function(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
};
var idCounter = 0;
var uniqId = function() {
return ++idCounter + '';
}
/**
* Basic drag implementation for DOM elements inside a container.
* Provide start/stop/drag callbacks.
*
* @class Draggable
* @param {HTMLElement} el The HTMLelement that contains all the widgets
* to be dragged.
* @param {Object} [options] An Object with all options you want to
* overwrite:
* @param {HTMLElement|String} [options.items] Define who will
* be the draggable items. Can be a CSS Selector String or a
* collection of HTMLElements.
* @param {Number} [options.distance] Distance in pixels after mousedown
* the mouse must move before dragging should start.
* @param {Boolean} [options.limit] Constrains dragging to the width of
* the container
* @param {Object|Function} [options.ignore_dragging] Array of node names
* that sould not trigger dragging, by default is `['INPUT', 'TEXTAREA',
* 'SELECT', 'BUTTON']`. If a function is used return true to ignore dragging.
* @param {offset_left} [options.offset_left] Offset added to the item
* that is being dragged.
* @param {Number} [options.drag] Executes a callback when the mouse is
* moved during the dragging.
* @param {Number} [options.start] Executes a callback when the drag
* starts.
* @param {Number} [options.stop] Executes a callback when the drag stops.
* @return {Object} Returns `el`.
* @constructor
*/
function Draggable(el, options) {
this.options = $.extend({}, defaults, options);
this.$document = $(document);
this.$container = $(el);
this.$dragitems = $(this.options.items, this.$container);
this.is_dragging = false;
this.player_min_left = 0 + this.options.offset_left;
this.id = uniqId();
this.ns = '.gridster-draggable-' + this.id;
this.init();
}
Draggable.defaults = defaults;
var fn = Draggable.prototype;
fn.init = function() {
var pos = this.$container.css('position');
this.calculate_dimensions();
this.$container.css('position', pos === 'static' ? 'relative' : pos);
this.disabled = false;
this.events();
$(window).bind(this.nsEvent('resize'),
throttle($.proxy(this.calculate_dimensions, this), 200));
};
fn.nsEvent = function(ev) {
return (ev || '') + this.ns;
};
fn.events = function() {
this.pointer_events = {
start: this.nsEvent('touchstart') + ' ' + this.nsEvent('mousedown'),
move: this.nsEvent('touchmove') + ' ' + this.nsEvent('mousemove'),
end: this.nsEvent('touchend') + ' ' + this.nsEvent('mouseup'),
};
this.$container.on(this.nsEvent('selectstart'),
$.proxy(this.on_select_start, this));
this.$container.on(this.pointer_events.start, this.options.items,
$.proxy(this.drag_handler, this));
this.$document.on(this.pointer_events.end, $.proxy(function(e) {
this.is_dragging = false;
if (this.disabled) { return; }
this.$document.off(this.pointer_events.move);
if (this.drag_start) {
this.on_dragstop(e);
}
}, this));
};
fn.get_actual_pos = function($el) {
var pos = $el.position();
return pos;
};
fn.get_mouse_pos = function(e) {
if (e.originalEvent && e.originalEvent.touches) {
var oe = e.originalEvent;
e = oe.touches.length ? oe.touches[0] : oe.changedTouches[0];
}
return {
left: e.clientX,
top: e.clientY
};
};
fn.get_offset = function(e) {
e.preventDefault();
var mouse_actual_pos = this.get_mouse_pos(e);
var diff_x = Math.round(
mouse_actual_pos.left - this.mouse_init_pos.left);
var diff_y = Math.round(mouse_actual_pos.top - this.mouse_init_pos.top);
var left = Math.round(this.el_init_offset.left +
diff_x - this.baseX + $(window).scrollLeft() - this.win_offset_x);
var top = Math.round(this.el_init_offset.top +
diff_y - this.baseY + $(window).scrollTop() - this.win_offset_y);
if (this.options.limit) {
if (left > this.player_max_left) {
left = this.player_max_left;
} else if(left < this.player_min_left) {
left = this.player_min_left;
}
}
return {
position: {
left: left,
top: top
},
pointer: {
left: mouse_actual_pos.left,
top: mouse_actual_pos.top,
diff_left: diff_x + ($(window).scrollLeft() - this.win_offset_x),
diff_top: diff_y + ($(window).scrollTop() - this.win_offset_y)
}
};
};
fn.get_drag_data = function(e) {
var offset = this.get_offset(e);
offset.$player = this.$player;
offset.$helper = this.helper ? this.$helper : this.$player;
return offset;
};
fn.set_limits = function(container_width) {
container_width || (container_width = this.$container.width());
this.player_max_left = (container_width - this.player_width +
- this.options.offset_left);
this.options.container_width = container_width;
return this;
};
fn.scroll_in = function(axis, data) {
var dir_prop = dir_map[axis];
var area_size = 50;
var scroll_inc = 30;
var is_x = axis === 'x';
var window_size = is_x ? this.window_width : this.window_height;
var doc_size = is_x ? $(document).width() : $(document).height();
var player_size = is_x ? this.$player.width() : this.$player.height();
var next_scroll;
var scroll_offset = $window['scroll' + capitalize(dir_prop)]();
var min_window_pos = scroll_offset;
var max_window_pos = min_window_pos + window_size;
var mouse_next_zone = max_window_pos - area_size; // down/right
var mouse_prev_zone = min_window_pos + area_size; // up/left
var abs_mouse_pos = min_window_pos + data.pointer[dir_prop];
var max_player_pos = (doc_size - window_size + player_size);
if (abs_mouse_pos >= mouse_next_zone) {
next_scroll = scroll_offset + scroll_inc;
if (next_scroll < max_player_pos) {
$window['scroll' + capitalize(dir_prop)](next_scroll);
this['scroll_offset_' + axis] += scroll_inc;
}
}
if (abs_mouse_pos <= mouse_prev_zone) {
next_scroll = scroll_offset - scroll_inc;
if (next_scroll > 0) {
$window['scroll' + capitalize(dir_prop)](next_scroll);
this['scroll_offset_' + axis] -= scroll_inc;
}
}
return this;
};
fn.manage_scroll = function(data) {
this.scroll_in('x', data);
this.scroll_in('y', data);
};
fn.calculate_dimensions = function(e) {
this.window_height = $window.height();
this.window_width = $window.width();
};
fn.drag_handler = function(e) {
var node = e.target.nodeName;
// skip if drag is disabled, or click was not done with the mouse primary button
if (this.disabled || e.which !== 1 && !isTouch) {
return;
}
if (this.ignore_drag(e)) {
return;
}
var self = this;
var first = true;
this.$player = $(e.currentTarget);
this.el_init_pos = this.get_actual_pos(this.$player);
this.mouse_init_pos = this.get_mouse_pos(e);
this.offsetY = this.mouse_init_pos.top - this.el_init_pos.top;
this.$document.on(this.pointer_events.move, function(mme) {
var mouse_actual_pos = self.get_mouse_pos(mme);
var diff_x = Math.abs(
mouse_actual_pos.left - self.mouse_init_pos.left);
var diff_y = Math.abs(
mouse_actual_pos.top - self.mouse_init_pos.top);
if (!(diff_x > self.options.distance ||
diff_y > self.options.distance)
) {
return false;
}
if (first) {
first = false;
self.on_dragstart.call(self, mme);
return false;
}
if (self.is_dragging === true) {
self.on_dragmove.call(self, mme);
}
return false;
});
if (!isTouch) { return false; }
};
fn.on_dragstart = function(e) {
e.preventDefault();
if (this.is_dragging) { return this; }
this.drag_start = this.is_dragging = true;
var offset = this.$container.offset();
this.baseX = Math.round(offset.left);
this.baseY = Math.round(offset.top);
this.initial_container_width = this.options.container_width || this.$container.width();
if (this.options.helper === 'clone') {
this.$helper = this.$player.clone()
.appendTo(this.$container).addClass('helper');
this.helper = true;
} else {
this.helper = false;
}
this.win_offset_y = $(window).scrollTop();
this.win_offset_x = $(window).scrollLeft();
this.scroll_offset_y = 0;
this.scroll_offset_x = 0;
this.el_init_offset = this.$player.offset();
this.player_width = this.$player.width();
this.player_height = this.$player.height();
this.set_limits(this.options.container_width);
if (this.options.start) {
this.options.start.call(this.$player, e, this.get_drag_data(e));
}
return false;
};
fn.on_dragmove = function(e) {
var data = this.get_drag_data(e);
this.options.autoscroll && this.manage_scroll(data);
if (this.options.move_element) {
(this.helper ? this.$helper : this.$player).css({
'position': 'absolute',
'left' : data.position.left,
'top' : data.position.top
});
}
var last_position = this.last_position || data.position;
data.prev_position = last_position;
if (this.options.drag) {
this.options.drag.call(this.$player, e, data);
}
this.last_position = data.position;
return false;
};
fn.on_dragstop = function(e) {
var data = this.get_drag_data(e);
this.drag_start = false;
if (this.options.stop) {
this.options.stop.call(this.$player, e, data);
}
if (this.helper && this.options.remove_helper) {
this.$helper.remove();
}
return false;
};
fn.on_select_start = function(e) {
if (this.disabled) { return; }
if (this.ignore_drag(e)) {
return;
}
return false;
};
fn.enable = function() {
this.disabled = false;
};
fn.disable = function() {
this.disabled = true;
};
fn.destroy = function() {
this.disable();
this.$container.off(this.ns);
this.$document.off(this.ns);
$(window).off(this.ns);
$.removeData(this.$container, 'drag');
};
fn.ignore_drag = function(event) {
if (this.options.handle) {
return !$(event.target).is(this.options.handle);
}
if ($.isFunction(this.options.ignore_dragging)) {
return this.options.ignore_dragging(event);
}
return $(event.target).is(this.options.ignore_dragging.join(', '));
};
//jQuery adapter
$.fn.drag = function ( options ) {
return new Draggable(this, options);
};
return Draggable;
}));