466 lines
17 KiB
JavaScript
466 lines
17 KiB
JavaScript
/**
|
|
* This module is an extension to jQuery ui selectable, which scrolls when you try to select
|
|
* outside the container, and the scrollables are not fitting it originally.
|
|
*
|
|
* Created by László Károlyi <http://linkedin.com/in/karolyi>
|
|
* with the GPLv3 License: http://opensource.org/licenses/GPL-3.0
|
|
*
|
|
* This program 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.
|
|
*
|
|
* This program 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, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
/*global jQuery*/
|
|
(function ($) {
|
|
'use strict';
|
|
$.widget('ui.selectableScroll', $.ui.selectable, {
|
|
options: {
|
|
// If an element is passed in here, use it for scrolling instead of widget's element
|
|
scrollElement: null,
|
|
// When the selection is that pixels near to the top/bottom edges, start to scroll
|
|
scrollSnapX: 5,
|
|
// When the selection is that pixels near to the side edges, start to scroll
|
|
scrollSnapY: 5,
|
|
scrollAmount: 25, // In pixels
|
|
scrollIntervalTime: 100 // In milliseconds
|
|
},
|
|
|
|
/**
|
|
* This is a slightly modified version of the original _create function,
|
|
* we just add the element's relative positions too
|
|
*
|
|
* @return null
|
|
*/
|
|
_create: function () {
|
|
this.element.addClass('ui-selectable');
|
|
this.dragged = false;
|
|
this.helperClasses = ['no-top', 'no-right', 'no-bottom', 'no-left'];
|
|
this.scrollElement = this.options.scrollElement || this.element;
|
|
this.refresh();
|
|
this._mouseInit();
|
|
this.helper = $('<div class="ui-selectable-helper"></div>');
|
|
},
|
|
|
|
/**
|
|
* Cache selectee children based on filter
|
|
* @return null
|
|
*/
|
|
refresh: function () {
|
|
var elementOffset = this.scrollElement.offset();
|
|
var scrollLeft = this.scrollElement.prop('scrollLeft');
|
|
var scrollTop = this.scrollElement.prop('scrollTop');
|
|
this.selectees = $(this.options.filter, this.scrollElement[0]);
|
|
this.selectees.addClass('ui-selectee');
|
|
this.selectees.each(function () {
|
|
var $element = $(this),
|
|
pos = $element.offset();
|
|
$.data(this, 'selectable-item', {
|
|
element: this,
|
|
$element: $element,
|
|
left: pos.left,
|
|
top: pos.top,
|
|
right: pos.left + $element.outerWidth(),
|
|
bottom: pos.top + $element.outerHeight(),
|
|
// Relative positions according to the element's 0.0
|
|
relative: {
|
|
left: pos.left - elementOffset.left + scrollLeft,
|
|
top: pos.top - elementOffset.top + scrollTop,
|
|
right: pos.left - elementOffset.left + scrollLeft +
|
|
$element.outerWidth(),
|
|
bottom: pos.top - elementOffset.top + scrollTop +
|
|
$element.outerHeight()
|
|
},
|
|
startselected: false,
|
|
selected: $element.hasClass('ui-selected'),
|
|
selecting: $element.hasClass('ui-selecting'),
|
|
unselecting: $element.hasClass('ui-unselecting')
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* By starting dragging, calculate the element's current scroll states,
|
|
* relative to the elements 0.0 offset
|
|
*
|
|
* @param object The mousedown event
|
|
* @return boolean The parent's _mouseStart return value
|
|
*/
|
|
_mouseStart: function (event) {
|
|
var pos = this.scrollElement.offset();
|
|
if (
|
|
(event.pageX > this.scrollElement.prop('clientWidth') + pos.left) ||
|
|
(event.pageY > this.scrollElement.prop('clientHeight') + pos.top)
|
|
) {
|
|
return false;
|
|
}
|
|
var retValue = $.ui.selectable.prototype._mouseStart.call(this,
|
|
event);
|
|
this.lastDragEvent = null;
|
|
this.scrollInfo = {
|
|
// The element's 0.0 offset related to the document element
|
|
elementOffset: this.scrollElement.offset(),
|
|
// The maximum scrollable width (visible width + scrollLeft)
|
|
scrollWidth: this.scrollElement.prop('scrollWidth'),
|
|
// The maximum scrollable height (visible height + scrollTop)
|
|
scrollHeight: this.scrollElement.prop('scrollHeight'),
|
|
// The visible width (minus scrollbars)
|
|
elementWidth: this.scrollElement.prop('clientWidth'),
|
|
// The visible height (minus scrollbars)
|
|
elementHeight: this.scrollElement.prop('clientHeight')
|
|
};
|
|
// Relative to element's 0
|
|
this.scrollInfo.dragStartXPos = event.pageX - this.scrollInfo.elementOffset
|
|
.left + this.scrollElement.prop('scrollLeft');
|
|
// Relative to element's 0
|
|
this.scrollInfo.dragStartYPos = event.pageY - this.scrollInfo.elementOffset
|
|
.top + this.scrollElement.prop('scrollTop');
|
|
this.scrollIntervalId = null;
|
|
return retValue;
|
|
},
|
|
|
|
/**
|
|
* Calculate/redraw the helper lasso, keep the helper within
|
|
* the scrolled element
|
|
*
|
|
* @param object The options object containing the relative positions of the selected rectangle
|
|
* @return null
|
|
*/
|
|
_updateHelper: function (options) {
|
|
var x1, y1, x2, y2; // Absolute positions for the lasso helper
|
|
var lassoClassesArray = [];
|
|
if (options.x1 - options.scrollLeft < 0) {
|
|
lassoClassesArray.push('no-left');
|
|
x1 = this.scrollInfo.elementOffset.left;
|
|
} else {
|
|
x1 = this.scrollInfo.elementOffset.left + options.x1 - options.scrollLeft;
|
|
}
|
|
if (options.y1 - options.scrollTop < 0) {
|
|
lassoClassesArray.push('no-top');
|
|
y1 = this.scrollInfo.elementOffset.top;
|
|
} else {
|
|
y1 = this.scrollInfo.elementOffset.top + options.y1 - options.scrollTop;
|
|
}
|
|
if (options.x2 - options.scrollLeft > this.scrollInfo.elementWidth) {
|
|
lassoClassesArray.push('no-right');
|
|
x2 = this.scrollInfo.elementOffset.left + this.scrollInfo.elementWidth;
|
|
} else {
|
|
x2 = this.scrollInfo.elementOffset.left + options.x2 - options.scrollLeft;
|
|
}
|
|
if (options.y2 - options.scrollTop > this.scrollInfo.elementHeight) {
|
|
lassoClassesArray.push('no-bottom');
|
|
y2 = this.scrollInfo.elementOffset.top + this.scrollInfo.elementHeight;
|
|
} else {
|
|
y2 = this.scrollInfo.elementOffset.top + options.y2 - options.scrollTop;
|
|
}
|
|
for (var counter = 0; counter < this.helperClasses.length; counter++) {
|
|
var className = this.helperClasses[counter];
|
|
if ($.inArray(className, lassoClassesArray) !== -1) {
|
|
this.helper.addClass(className);
|
|
} else {
|
|
this.helper.removeClass(className);
|
|
}
|
|
}
|
|
this.helper.css({
|
|
left: x1,
|
|
top: y1,
|
|
width: x2 - x1,
|
|
height: y2 - y1
|
|
});
|
|
},
|
|
|
|
/**
|
|
* If the mouse is near to the edges of the elements area, scroll
|
|
*
|
|
* @return object The new scrollLeft and scrollTop values, and a
|
|
* boolean if the element should keep scrolling
|
|
*/
|
|
_scrollIfNeeded: function (options) {
|
|
var scrollLeft = this.scrollElement.prop('scrollLeft');
|
|
var scrollTop = this.scrollElement.prop('scrollTop');
|
|
|
|
var keepScrolling = false;
|
|
// Scroll if close to edges or over them
|
|
if (this.lastDragEvent.pageX - this.scrollInfo.elementOffset.left <
|
|
this.options.scrollSnapX && scrollLeft > 0) {
|
|
scrollLeft = scrollLeft < this.options.scrollAmount ? 0 :
|
|
scrollLeft - this.options.scrollAmount;
|
|
this.scrollElement.prop('scrollLeft', scrollLeft);
|
|
keepScrolling = true;
|
|
}
|
|
if (this.lastDragEvent.pageY - this.scrollInfo.elementOffset.top <
|
|
this.options.scrollSnapY && scrollTop > 0) {
|
|
scrollTop = scrollTop < this.options.scrollAmount ? 0 : scrollTop -
|
|
this.options.scrollAmount;
|
|
this.scrollElement.prop('scrollTop', scrollTop);
|
|
keepScrolling = true;
|
|
}
|
|
if (this.lastDragEvent.pageX - this.scrollInfo.elementOffset.left >
|
|
this.scrollInfo.elementWidth - this.options.scrollSnapX && this.scrollInfo
|
|
.scrollWidth > scrollLeft + this.scrollInfo.elementWidth) {
|
|
scrollLeft = scrollLeft + this.options.scrollAmount > this.scrollInfo
|
|
.scrollWidth - this.scrollInfo.elementWidth ? this.scrollInfo.scrollWidth -
|
|
this.scrollInfo.elementWidth : scrollLeft + this.options.scrollAmount;
|
|
this.scrollElement.prop('scrollLeft', scrollLeft);
|
|
keepScrolling = true;
|
|
}
|
|
if (this.lastDragEvent.pageY - this.scrollInfo.elementOffset.top >
|
|
this.scrollInfo.elementHeight - this.options.scrollSnapY && this.scrollInfo
|
|
.scrollHeight > scrollLeft + this.scrollInfo.elementHeight) {
|
|
scrollTop = scrollTop + this.options.scrollAmount > this.scrollInfo
|
|
.scrollHeight - this.scrollInfo.elementHeight ? this.scrollInfo
|
|
.scrollHeight - this.scrollInfo.elementHeight : scrollTop +
|
|
this.options.scrollAmount;
|
|
this.scrollElement.prop('scrollTop', scrollTop);
|
|
keepScrolling = true;
|
|
}
|
|
return {
|
|
scrollLeft: scrollLeft,
|
|
scrollTop: scrollTop,
|
|
keepScrolling: keepScrolling
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Calculate a relative position to the element's 0.0 offset,
|
|
* from the original drag event's coordinates
|
|
*
|
|
* @param object The absolute X and Y positions
|
|
* @return object The relative X and Y positions
|
|
*/
|
|
_calcRelativePosition: function (options) {
|
|
var relXPos = options.x - this.scrollInfo.elementOffset.left +
|
|
options.scrollLeft;
|
|
var relYPos = options.y - this.scrollInfo.elementOffset.top +
|
|
options.scrollTop;
|
|
return {
|
|
x: relXPos,
|
|
y: relYPos
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Calculate relative area positions to offset element
|
|
*
|
|
* @param object The relative X and Y positions to the 0.0 of the element
|
|
* @return object An object containing the relative area coordinates
|
|
*/
|
|
_calcRelativeArea: function (options) {
|
|
var relX1 = options.xPos < this.scrollInfo.dragStartXPos ? options.xPos :
|
|
this.scrollInfo.dragStartXPos;
|
|
var relY1 = options.yPos < this.scrollInfo.dragStartYPos ? options.yPos :
|
|
this.scrollInfo.dragStartYPos;
|
|
var relX2 = options.xPos > this.scrollInfo.dragStartXPos ? options.xPos :
|
|
this.scrollInfo.dragStartXPos;
|
|
var relY2 = options.yPos > this.scrollInfo.dragStartYPos ? options.yPos :
|
|
this.scrollInfo.dragStartYPos;
|
|
return {
|
|
x1: relX1,
|
|
y1: relY1,
|
|
x2: relX2,
|
|
y2: relY2
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Update the selected elements
|
|
*
|
|
* @param object The return value of _calcRelativeArea()
|
|
* @return null
|
|
*/
|
|
_updateSelectees: function (options) {
|
|
var that = this;
|
|
this.selectees.each(function () {
|
|
var selectee = $.data(this, 'selectable-item'),
|
|
hit = false;
|
|
|
|
// Prevent helper from being selected if appendTo: selectable
|
|
if (!selectee || selectee.element === that.element[0]) {
|
|
return;
|
|
}
|
|
|
|
if (that.options.tolerance === 'touch') {
|
|
hit = (!(selectee.relative.left > options.x2 || selectee.relative
|
|
.right < options.x1 || selectee.relative.top > options.y2 ||
|
|
selectee.relative.bottom < options.y1));
|
|
} else if (that.options.tolerance === 'fit') {
|
|
hit = (selectee.relative.left > options.x1 && selectee.relative
|
|
.right < options.x2 && selectee.relative.top > options.y1 &&
|
|
selectee.relative.bottom < options.y2);
|
|
}
|
|
|
|
if (hit) {
|
|
// SELECT
|
|
if (selectee.selected) {
|
|
selectee.$element.removeClass('ui-selected');
|
|
selectee.selected = false;
|
|
}
|
|
if (selectee.unselecting) {
|
|
selectee.$element.removeClass('ui-unselecting');
|
|
selectee.unselecting = false;
|
|
}
|
|
if (!selectee.selecting) {
|
|
selectee.$element.addClass('ui-selecting');
|
|
selectee.selecting = true;
|
|
// selectable SELECTING callback
|
|
that._trigger('selecting', that.lastDragEvent, {
|
|
selecting: selectee.element
|
|
});
|
|
}
|
|
} else {
|
|
// UNSELECT
|
|
if (selectee.selecting) {
|
|
if ((that.lastDragEvent.metaKey || that.lastDragEvent.ctrlKey) &&
|
|
selectee.startselected) {
|
|
selectee.$element.removeClass('ui-selecting');
|
|
selectee.selecting = false;
|
|
selectee.$element.addClass('ui-selected');
|
|
selectee.selected = true;
|
|
} else {
|
|
selectee.$element.removeClass('ui-selecting');
|
|
selectee.selecting = false;
|
|
if (selectee.startselected) {
|
|
selectee.$element.addClass('ui-unselecting');
|
|
selectee.unselecting = true;
|
|
}
|
|
// Selectable UNSELECTING callback
|
|
that._trigger('unselecting', that.lastDragEvent, {
|
|
unselecting: selectee.element
|
|
});
|
|
}
|
|
}
|
|
if (selectee.selected) {
|
|
if (!that.lastDragEvent.metaKey && !that.lastDragEvent.ctrlKey && !
|
|
selectee.startselected) {
|
|
selectee.$element.removeClass('ui-selected');
|
|
selectee.selected = false;
|
|
|
|
selectee.$element.addClass('ui-unselecting');
|
|
selectee.unselecting = true;
|
|
// Selectable UNSELECTING callback
|
|
that._trigger('unselecting', that.lastDragEvent, {
|
|
unselecting: selectee.element
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* The original _mouseDrag function overvritten by our one
|
|
*
|
|
* @param object The original mousemove event
|
|
* @return boolean Returning false, as the parent returns too
|
|
*/
|
|
_mouseDrag: function (event) {
|
|
this.dragged = true;
|
|
if (this.options.disabled) {
|
|
return;
|
|
}
|
|
this.lastDragEvent = event;
|
|
|
|
var scrollObj = this._scrollIfNeeded();
|
|
|
|
this._updateIntervals({
|
|
keepScrolling: scrollObj.keepScrolling
|
|
});
|
|
this._updateUi({
|
|
doUpdateHelper: true
|
|
});
|
|
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Do the actual calculating/updating of the positions/elements
|
|
*
|
|
* @param object Options containing if the function should update the helper lasso
|
|
* @return null
|
|
*/
|
|
_updateUi: function (options) {
|
|
var scrollObj = this._scrollIfNeeded({
|
|
pageX: this.lastDragEvent.pageX,
|
|
pageY: this.lastDragEvent.pageY
|
|
});
|
|
var relativePos = this._calcRelativePosition({
|
|
x: this.lastDragEvent.pageX,
|
|
y: this.lastDragEvent.pageY,
|
|
scrollLeft: scrollObj.scrollLeft,
|
|
scrollTop: scrollObj.scrollTop
|
|
});
|
|
var relativeArea = this._calcRelativeArea({
|
|
xPos: relativePos.x,
|
|
yPos: relativePos.y
|
|
});
|
|
if (options.doUpdateHelper) {
|
|
this._updateHelper({
|
|
scrollLeft: scrollObj.scrollLeft,
|
|
scrollTop: scrollObj.scrollTop,
|
|
x1: relativeArea.x1,
|
|
y1: relativeArea.y1,
|
|
x2: relativeArea.x2,
|
|
y2: relativeArea.y2
|
|
});
|
|
}
|
|
this._updateSelectees({
|
|
x1: relativeArea.x1,
|
|
y1: relativeArea.y1,
|
|
x2: relativeArea.x2,
|
|
y2: relativeArea.y2
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Start the automatic scrolling if needed
|
|
*
|
|
* @param object Options containing if the function should start the interval
|
|
* @return null
|
|
*/
|
|
_updateIntervals: function (options) {
|
|
var that = this;
|
|
if (options.keepScrolling && !this.scrollIntervalId) {
|
|
this.scrollIntervalId = setInterval(function () {
|
|
that._updateUi({
|
|
doUpdateHelper: false
|
|
});
|
|
}, this.options.scrollIntervalTime);
|
|
}
|
|
if (!options.keepScrolling && this.scrollIntervalId)
|
|
this._clearIntervals();
|
|
},
|
|
|
|
/**
|
|
* Clear the autoscroll interval
|
|
*
|
|
* @return null
|
|
*/
|
|
_clearIntervals: function () {
|
|
// Stop scrolling
|
|
if (this.scrollIntervalId)
|
|
clearInterval(this.scrollIntervalId);
|
|
this.scrollIntervalId = null;
|
|
},
|
|
|
|
/**
|
|
* The original _mouseStop event extended with the interval clearer
|
|
*
|
|
* @param object The original mousestop event
|
|
* @return boolean The parent's return value
|
|
*/
|
|
_mouseStop: function (event) {
|
|
this._clearIntervals();
|
|
var retValue = $.ui.selectable.prototype._mouseStop.call(this,
|
|
event);
|
|
return retValue;
|
|
}
|
|
});
|
|
})(jQuery);
|