/** * 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 * 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 . */ /*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 = $('
'); }, /** * 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);