doevisualizations
Version:
Data Visualization Library based on RequireJS and D3.js (v4+)
773 lines (735 loc) • 24.9 kB
JavaScript
steal('jquery', 'jquerypp/lang/vector', 'jquerypp/event/livehack', 'jquerypp/event/reverse', function( $ ) {
if(!$.event.special.move) {
$.event.reverse('move');
}
//modify live
//steal the live handler ....
var bind = function( object, method ) {
var args = Array.prototype.slice.call(arguments, 2);
return function() {
var args2 = [this].concat(args, $.makeArray(arguments));
return method.apply(object, args2);
};
},
event = $.event,
// function to clear the window selection if there is one
clearSelection = window.getSelection ? function(){
window.getSelection().removeAllRanges()
} : function(){},
supportTouch = !window._phantom && "ontouchend" in document,
// Use touch events or map it to mouse events
startEvent = supportTouch ? "touchstart" : "mousedown",
stopEvent = supportTouch ? "touchend" : "mouseup",
moveEvent = supportTouch ? "touchmove" : "mousemove",
// On touchmove events the default (scrolling) event has to be prevented
preventTouchScroll = function(ev) {
ev.preventDefault();
};
/**
* @constructor jQuery.Drag
* @parent jQuery.event.drag
* @plugin jquerypp/event/drag
* @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquerypp/event/drag/drag.js
* @test jquerypp/event/drag/qunit.html
*
* The `jQuery.Drag` constructor is never called directly but an instance of `jQuery.Drag` is passed as the second argument
* to the `dragdown`, `draginit`, `dragmove`, `dragend`, `dragover` and `dragout` event handlers:
*
* $('#dragger').on('draginit', function(el, drag) {
* // drag -> $.Drag
* });
*/
$.Drag = function() {};
/**
* @static
*/
$.extend($.Drag, {
lowerName: "drag",
current: null,
distance: 0,
/**
* @function jQuery.Drag.mousedown
* @parent jQuery.Drag.static
*
* @body
*
* Called when someone mouses down on a draggable object.
* Gathers all callback functions and creates a new Draggable.
* @hide
*/
mousedown: function( ev, element ) {
var isLeftButton = ev.button === 0 || ev.button == 1,
doEvent = isLeftButton || supportTouch;
if (!doEvent || this.current ) {
return;
}
//create Drag
var drag = new $.Drag(),
delegate = ev.delegateTarget || element,
selector = ev.handleObj.selector,
self = this;
this.current = drag;
drag.setup({
element: element,
delegate: ev.delegateTarget || element,
selector: ev.handleObj.selector,
moved: false,
_distance: this.distance,
callbacks: {
dragdown: event.find(delegate, ["dragdown"], selector),
draginit: event.find(delegate, ["draginit"], selector),
dragover: event.find(delegate, ["dragover"], selector),
dragmove: event.find(delegate, ["dragmove"], selector),
dragout: event.find(delegate, ["dragout"], selector),
dragend: event.find(delegate, ["dragend"], selector),
dragcleanup: event.find(delegate, ["dragcleanup"], selector)
},
destroyed: function() {
self.current = null;
}
}, ev);
}
});
/**
* @Prototype
*/
$.extend($.Drag.prototype, {
setup: function( options, ev ) {
$.extend(this, options);
this.element = $(this.element);
this.event = ev;
this.moved = false;
this.allowOtherDrags = false;
var mousemove = bind(this, this.mousemove),
mouseup = bind(this, this.mouseup);
this._mousemove = mousemove;
this._mouseup = mouseup;
this._distance = options.distance ? options.distance : 0;
//where the mouse is located
this.mouseStartPosition = ev.vector();
$(document).bind(moveEvent, mousemove);
$(document).bind(stopEvent, mouseup);
if(supportTouch) {
// On touch devices we want to disable scrolling
$(document).bind(moveEvent, preventTouchScroll);
}
if (!this.callEvents('down', this.element, ev) ) {
this.noSelection(this.delegate);
//this is for firefox
clearSelection();
}
},
/**
* @property jQuery.Drag.prototype.element element
* @parent jQuery.Drag.prototype
*
* @body
* A reference to the element that is being dragged. For example:
*
* $('.draggable').on('draginit', function(ev, drag) {
* drag.element.html('I am the drag element');
* });
*/
/**
* Unbinds listeners and allows other drags ...
* @hide
*/
destroy: function() {
// Unbind the mouse handlers attached for dragging
$(document).unbind(moveEvent, this._mousemove);
$(document).unbind(stopEvent, this._mouseup);
if(supportTouch) {
// Enable scrolling again for touch devices when the drag is done
$(document).unbind(moveEvent, preventTouchScroll);
}
if (!this.moved ) {
this.event = this.element = null;
}
if(!supportTouch) {
this.selection(this.delegate);
}
this.destroyed();
},
mousemove: function( docEl, ev ) {
if (!this.moved ) {
var dist = Math.sqrt( Math.pow( ev.pageX - this.event.pageX, 2 ) + Math.pow( ev.pageY - this.event.pageY, 2 ));
// Don't initialize the drag if it hasn't been moved the minimum distance
if(dist < this._distance){
return false;
}
// Otherwise call init and indicate that the drag has moved
this.init(this.element, ev);
this.moved = true;
}
this.element.trigger('move', this);
var pointer = ev.vector();
if ( this._start_position && this._start_position.equal(pointer) ) {
return;
}
this.draw(pointer, ev);
},
mouseup: function( docEl, event ) {
//if there is a current, we should call its dragstop
if ( this.moved ) {
this.end(event);
}
this.destroy();
},
/**
* The `drag.noSelection(element)` method turns off text selection during a drag event.
* This method is called by default unless a event is listening to the 'dragdown' event.
*
* ## Example
*
* $('div.drag').bind('dragdown', function(elm,event,drag){
* drag.noSelection();
* });
*
* @param [elm] an element to prevent selection on. Defaults to the dragable element.
*/
noSelection: function(elm) {
elm = elm || this.delegate
document.documentElement.onselectstart = function() {
// Disables selection
return false;
};
document.documentElement.unselectable = "on";
this.selectionDisabled = (this.selectionDisabled ? this.selectionDisabled.add(elm) : $(elm));
this.selectionDisabled.css('-moz-user-select', '-moz-none');
},
/**
* @hide
* `drag.selection()` method turns on text selection that was previously turned off during the drag event.
* This method is always called.
*
* ## Example
*
* $('div.drag').bind('dragdown', function(elm,event,drag){
* drag.selection();
* });
*/
selection: function() {
if(this.selectionDisabled) {
document.documentElement.onselectstart = function() {};
document.documentElement.unselectable = "off";
this.selectionDisabled.css('-moz-user-select', '');
}
},
init: function( element, event ) {
element = $(element);
//the element that has been clicked on
var startElement = (this.movingElement = (this.element = $(element)));
//if a mousemove has come after the click
//if the drag has been cancelled
this._cancelled = false;
this.event = event;
/**
* @property jQuery.Drag.prototype.mouseElementPosition mouseElementPosition
* @parent jQuery.Drag.prototype
*
* @body
* The position of start of the cursor on the element
*/
this.mouseElementPosition = this.mouseStartPosition.minus(this.element.offsetv()); //where the mouse is on the Element
this.callEvents('init', element, event);
// Check what they have set and respond accordingly if they canceled
if ( this._cancelled === true ) {
return;
}
// if they set something else as the element
this.startPosition = startElement != this.movingElement ? this.movingElement.offsetv() : this.currentDelta();
this.makePositioned(this.movingElement);
// Adjust the drag elements z-index to a high value
this.oldZIndex = this.movingElement.css('zIndex');
this.movingElement.css('zIndex', 1000);
if (!this._only && this.constructor.responder ) {
// calls $.Drop.prototype.compile if there is a drop element
this.constructor.responder.compile(event, this);
}
},
makePositioned: function( that ) {
var style, pos = that.css('position');
// Position properly, set top and left to 0px for Opera
if (!pos || pos == 'static' ) {
style = {
position: 'relative'
};
if ( window.opera ) {
style.top = '0px';
style.left = '0px';
}
that.css(style);
}
},
callEvents: function( type, element, event, drop ) {
var i, cbs = this.callbacks[this.constructor.lowerName + type];
for ( i = 0; i < cbs.length; i++ ) {
cbs[i].call(element, event, this, drop);
}
return cbs.length;
},
/**
* Returns the position of the movingElement by taking its top and left.
* @hide
* @return {$.Vector}
*/
currentDelta: function() {
return new $.Vector(parseInt(this.movingElement.css('left'), 10) || 0, parseInt(this.movingElement.css('top'), 10) || 0);
},
//draws the position of the dragmove object
draw: function( pointer, event ) {
// only drag if we haven't been cancelled;
if ( this._cancelled ) {
return;
}
clearSelection();
/**
* @property jQuery.Drag.prototype.location location
* @parent jQuery.Drag.prototype
* `drag.location` is a [jQuery.Vector] specifying where the element should be in the page. This
* takes into account the start position of the cursor on the element.
*
* If the drag is going to be moved to an unacceptable location, you can call preventDefault in
* dragmove to prevent it from being moved there.
*
* $('.mover').bind("dragmove", function(ev, drag){
* if(drag.location.top() < 100){
* ev.preventDefault()
* }
* });
*
* You can also set the location to where it should be on the page.
*
*/
// the offset between the mouse pointer and the representative that the user asked for
this.location = pointer.minus(this.mouseElementPosition);
// call move events
this.move(event);
if ( this._cancelled ) {
return;
}
if (!event.isDefaultPrevented() ) {
this.position(this.location);
}
// fill in
if (!this._only && this.constructor.responder ) {
this.constructor.responder.show(pointer, this, event);
}
},
/**
* `drag.position( newOffsetVector )` sets the position of the movingElement. This is overwritten by
* the [$.Drag::scrolls], [$.Drag::limit] and [$.Drag::step] plugins
* to make sure the moving element scrolls some element
* or stays within some boundary. This function is exposed and documented so you could do the same.
*
* The following approximates how step does it:
*
* var oldPosition = $.Drag.prototype.position;
* $.Drag.prototype.position = function( offsetPositionv ) {
* if(this._step){
* // change offsetPositionv to be on the step value
* }
*
* oldPosition.call(this, offsetPosition)
* }
*
* @param {jQuery.Vector} newOffsetv the new [$.Drag::location] of the element.
*/
position: function( newOffsetv ) { //should draw it on the page
var style, dragged_element_css_offset = this.currentDelta(),
// the drag element's current left + top css attributes
// the vector between the movingElement's page and css positions
// this can be thought of as the original offset
dragged_element_position_vector = this.movingElement.offsetv().minus(dragged_element_css_offset);
this.required_css_position = newOffsetv.minus(dragged_element_position_vector);
this.offsetv = newOffsetv;
style = this.movingElement[0].style;
if (!this._cancelled && !this._horizontal ) {
style.top = this.required_css_position.top() + "px";
}
if (!this._cancelled && !this._vertical ) {
style.left = this.required_css_position.left() + "px";
}
},
move: function( event ) {
this.callEvents('move', this.element, event);
},
over: function( event, drop ) {
this.callEvents('over', this.element, event, drop);
},
out: function( event, drop ) {
this.callEvents('out', this.element, event, drop);
},
/**
* Called on drag up
* @hide
* @param {Event} event a mouseup event signalling drag/drop has completed
*/
end: function( event ) {
// If canceled do nothing
if ( this._cancelled ) {
return;
}
// notify the responder - usually a $.Drop instance
if (!this._only && this.constructor.responder ) {
this.constructor.responder.end(event, this);
}
this.callEvents('end', this.element, event);
if ( this._revert ) {
var self = this;
// animate moving back to original position
this.movingElement.animate({
top: this.startPosition.top() + "px",
left: this.startPosition.left() + "px"
}, function() {
self.cleanup.apply(self, arguments);
});
}
else {
this.cleanup(event);
}
this.event = null;
},
/**
* Cleans up drag element after drag drop.
* @hide
*/
cleanup: function(event) {
this.movingElement.css({
zIndex: this.oldZIndex
});
if ( this.movingElement[0] !== this.element[0] &&
!this.movingElement.has(this.element[0]).length &&
!this.element.has(this.movingElement[0]).length ) {
this.movingElement.css({
display: 'none'
});
}
if ( this._removeMovingElement ) {
// Remove the element when using drag.ghost()
this.movingElement.remove();
}
if(event) {
this.callEvents('cleanup', this.element, event);
}
this.movingElement = this.element = this.event = null;
},
/**
* `drag.cancel()` stops a drag motion from from running. This also stops any other events from firing, meaning
* that "dragend" will not be called.
*
* $("#todos").on(".handle", "draginit", function( ev, drag ) {
* if(drag.movingElement.hasClass("evil")){
* drag.cancel();
* }
* })
*
*/
cancel: function() {
this._cancelled = true;
if (!this._only && this.constructor.responder ) {
// clear the drops
this.constructor.responder.clear(this.event.vector(), this, this.event);
}
this.destroy();
},
/**
* `drag.ghost( [parent] )` clones the element and uses it as the
* moving element, leaving the original dragged element in place. The `parent` option can
* be used to specify where the ghost element should be temporarily added into the
* DOM. This method should be called in "draginit".
*
* $("#todos").on(".handle", "draginit", function( ev, drag ) {
* drag.ghost();
* })
*
* @param {HTMLElement} [parent] the parent element of the newly created ghost element. If not provided the
* ghost element is added after the moving element.
* @return {jQuery.fn} the ghost element to do whatever you want with it.
*/
ghost: function( parent ) {
// create a ghost by cloning the source element and attach the clone to the dom after the source element
var ghost = this.movingElement.clone().css('position', 'absolute');
if( parent ) {
$(parent).append(ghost);
} else {
$(this.movingElement).after(ghost)
}
ghost.width(this.movingElement.width()).height(this.movingElement.height());
// put the ghost in the right location ...
ghost.offset(this.movingElement.offset())
// store the original element and make the ghost the dragged element
this.movingElement = ghost;
this.noSelection(ghost)
this._removeMovingElement = true;
return ghost;
},
/**
* `drag.representative( element, [offsetX], [offsetY])` tells the drag motion to use
* a different element than the one that began the drag motion.
*
* For example, instead of
* dragging an drag-icon of a todo element, you want to move some other representation of
* the todo element (or elements). To do this you might:
*
* $("#todos").on(".handle", "draginit", function( ev, drag ) {
* // create what we'll drag
* var rep = $('<div/>').text("todos")
* .appendTo(document.body)
* // indicate we want our mouse on the top-right of it
* drag.representative(rep, rep.width(), 0);
* })
*
* @param {HTMLElement} element the element you want to actually drag. This should be
* already in the DOM.
* @param {Number} offsetX the x position where you want your mouse on the representative element (defaults to 0)
* @param {Number} offsetY the y position where you want your mouse on the representative element (defaults to 0)
* @return {drag} returns the drag object for chaining.
*/
representative: function( element, offsetX, offsetY ) {
this._offsetX = offsetX || 0;
this._offsetY = offsetY || 0;
var p = this.mouseStartPosition;
// Just set the representative as the drag element
this.movingElement = $(element);
this.movingElement.css({
top: (p.y() - this._offsetY) + "px",
left: (p.x() - this._offsetX) + "px",
display: 'block',
position: 'absolute'
}).show();
this.noSelection(this.movingElement)
this.mouseElementPosition = new $.Vector(this._offsetX, this._offsetY);
return this;
},
/**
* `drag.revert([val])` makes the [$.Drag::representative representative] element revert back to it
* original position after the drag motion has completed. The revert is done with an animation.
*
* $("#todos").on(".handle","dragend",function( ev, drag ) {
* drag.revert();
* })
*
* @param {Boolean} [val] optional, set to false if you don't want to revert.
* @return {drag} the drag object for chaining
*/
revert: function( val ) {
this._revert = val === undefined ? true : val;
return this;
},
/**
* `drag.vertical()` isolates the drag to vertical movement. For example:
*
* $("#images").on(".thumbnail","draginit", function(ev, drag){
* drag.vertical();
* });
*
* Call `vertical()` in "draginit" or "dragdown".
*
* @return {drag} the drag object for chaining.
*/
vertical: function() {
this._vertical = true;
return this;
},
/**
* `drag.horizontal()` isolates the drag to horizontal movement. For example:
*
* $("#images").on(".thumbnail","draginit", function(ev, drag){
* drag.horizontal();
* });
*
* Call `horizontal()` in "draginit" or "dragdown".
*
* @return {drag} the drag object for chaining.
*/
horizontal: function() {
this._horizontal = true;
return this;
},
/**
* `drag.only([only])` indicates if you __only__ want a drag motion and the drag should
* not notify drops. The default value is `false`. Call it with no arguments or pass it true
* to prevent drop events.
*
* $("#images").on(".thumbnail","dragdown", function(ev, drag){
* drag.only();
* });
*
* @param {Boolean} [only] true if you want to prevent drops, false if otherwise.
* @return {Boolean} the current value of only.
*/
only: function( only ) {
return (this._only = (only === undefined ? true : only));
},
/**
* `distance([val])` sets or reads the distance the mouse must move before a drag motion is started. This should be set in
* "dragdown" and delays "draginit" being called until the distance is covered. The distance
* is measured in pixels. The default distance is 0 pixels meaning the drag motion starts on the first
* mousemove after a mousedown.
*
* Set this to make drag motion a little "stickier" to start.
*
* $("#images").on(".thumbnail","dragdown", function(ev, drag){
* drag.distance(10);
* });
*
* @param {Number} [val] The number of pixels the mouse must move before "draginit" is called.
* @return {drag|Number} returns the drag instance for chaining if the drag value is being set or the
* distance value if the distance is being read.
*/
distance: function(val){
if(val !== undefined){
this._distance = val;
return this;
}else{
return this._distance
}
}
});
/**
* @add jQuery.event.special
*/
event.setupHelper([
/**
* @function jQuery.event.special.dragdown dragdown
* @parent jQuery.event.drag
*
* @body
*
* `dragdown` is called when a drag movement has started on a mousedown.
* The event handler gets an instance of [jQuery.Drag] passed as the second
* parameter. Listening to `dragdown` allows you to customize
* the behavior of a drag motion, especially when `draginit` should be called.
*
* $(".handles").delegate("dragdown", function(ev, drag){
* // call draginit only when the mouse has moved 20 px
* drag.distance(20);
* })
*
* Typically, when a drag motion is started, `event.preventDefault` is automatically
* called, preventing text selection. However, if you listen to
* `dragdown`, this default behavior is not called. You are responsible for calling it
* if you want it (you probably do).
*
* ### Why might you not want to call `preventDefault`?
*
* You might want it if you want to allow text selection on element
* within the drag element. Typically these are input elements.
*
* $(".handles").delegate("dragdown", function(ev, drag){
* if(ev.target.nodeName === "input"){
* drag.cancel();
* } else {
* ev.preventDefault();
* }
* })
*/
'dragdown',
/**
* @function jQuery.event.special.draginit draginit
* @parent jQuery.event.drag
*
* @body
*
* `draginit` is triggered when the drag motion starts. Use it to customize the drag behavior
* using the [jQuery.Drag] instance passed as the second parameter:
*
* $(".draggable").on('draginit', function(ev, drag) {
* // Only allow vertical drags
* drag.vertical();
* // Create a draggable copy of the element
* drag.ghost();
* });
*/
'draginit',
/**
* @function jQuery.event.special.dragover dragover
* @parent jQuery.event.drag
*
* @body
*
* `dragover` is triggered when a drag is over a [jQuery.event.drop drop element].
* The event handler gets an instance of [jQuery.Drag] passed as the second
* parameter and an instance of [jQuery.Drop] passed as the third argument:
*
* $('.draggable').on('dragover', function(ev, drag, drop) {
* // Add the drop-here class indicating that the drag
* // can be dropped here
* drag.element.addClass('drop-here');
* });
*/
'dragover',
/**
* @function jQuery.event.special.dragmove dragmove
* @parent jQuery.event.drag
*
* @body
*
* `dragmove` is triggered when the drag element moves (similar to a mousemove).
* The event handler gets an instance of [jQuery.Drag] passed as the second
* parameter.
* Use [jQuery.Drag.prototype.location location] to determine the current position
* as a [jQuery.Vector vector].
*
* For example, `dragmove` can be used to create a draggable element to resize
* a container:
*
* $('.resizer').on('dragmove', function(ev, drag) {
* $('#container').width(drag.location.x())
* .height(drag.location.y());
* });
*/
'dragmove',
/**
* @function jQuery.event.special.dragout dragout
* @parent jQuery.event.drag
*
* @body
*
* `dragout` is called when the drag leaves a drop point.
* The event handler gets an instance of [jQuery.Drag] passed as the second
* parameter.
*
* $('.draggable').on('dragout', function(ev, drag) {
* // Remove the drop-here class
* // (e.g. crossing the drag element out indicating that it
* // can't be dropped here
* drag.element.removeClass('drop-here');
* });
*/
'dragout',
/**
* @function jQuery.event.special.dragend dragend
* @parent jQuery.event.drag
*
* @body
*
* `dragend` is called when the drag operation is completed.
* The event handler gets an instance of [jQuery.Drag] passed as the second
* parameter.
*
* $('.draggable').on('dragend', function(ev, drag)
* // Calculation on whether revert should be invoked, alterations based on position of the end event
* });
*/
'dragend',
/**
* @function jQuery.event.special.dragcleanup dragcleanup
* @parent jQuery.event.drag
*
* @body
*
* `dragcleanup` is called after dragend and revert (if applied)
* The event handler gets an instance of [jQuery.Drag] passed as the second
* parameter.
*
* $('.draggable').on('dragcleanup', function(ev, drag)
* // cleanup
* });
*/
'dragcleanup'], startEvent, function( e ) {
$.Drag.mousedown.call($.Drag, e, this);
});
return $;
});