2020-10-02 15:10:00 +00:00
|
|
|
/**
|
|
|
|
* Selector takes care of attaching callbacks to the relevant mouse events on the given view.
|
|
|
|
* Selection and dragging/dropping consists of 3 phases: mouse down -> mouse move -> mouse up
|
|
|
|
* The Selector class is purposefully lightweight because different views might have
|
|
|
|
* different select/drag/drop behavior. Most of the work is done in the callbacks
|
|
|
|
* provided to the Selector class.
|
|
|
|
*
|
|
|
|
* Usage:
|
|
|
|
Selectors are instantiated with a view.
|
|
|
|
@param{view}: The view containing the selectable/draggable elements
|
|
|
|
* Views must also supply the Selector class with mousedown/mousemove/mouseup callbacks and
|
|
|
|
* the associated element's that listen for the mouse events.
|
|
|
|
* through registerMouseHandlers.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/* globals document */
|
|
|
|
|
|
|
|
var ko = require('knockout');
|
|
|
|
var _ = require('underscore');
|
|
|
|
var dispose = require('../lib/dispose');
|
|
|
|
var gutil = require('app/common/gutil');
|
2022-06-29 09:47:28 +00:00
|
|
|
const {isWin} = require('app/client/lib/browserInfo')
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
var ROW = 'row';
|
|
|
|
var COL = 'col';
|
|
|
|
var CELL = 'cell';
|
|
|
|
var NONE = '';
|
|
|
|
var SELECT = 'select';
|
|
|
|
var DRAG = 'drag';
|
|
|
|
|
|
|
|
exports.ROW = ROW;
|
|
|
|
exports.COL = COL;
|
|
|
|
exports.CELL = CELL;
|
|
|
|
exports.NONE = NONE;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {Object} view
|
|
|
|
* @param {Object} opt
|
|
|
|
* @param {function} opt.isDisabled - Is this selector disabled? Allows caller to specify
|
|
|
|
* conditions for temporarily disabling capturing of mouse events.
|
|
|
|
*/
|
|
|
|
function Selector(view, opt) {
|
|
|
|
this.view = view;
|
|
|
|
// TODO: There should be a better way to ensure that select/drag doesnt happen when clicking
|
|
|
|
// on these things. Also, these classes should not be in the generic Selector class.
|
|
|
|
// TODO: get rid of the Selector class entirely and make this a Cell/GridSelector class specifically
|
|
|
|
// for GridView(and its derived views).
|
|
|
|
this.exemptClasses = [
|
|
|
|
'glyphicon-pencil',
|
|
|
|
'ui-resizable-handle',
|
|
|
|
'dropdown-toggle',
|
|
|
|
];
|
|
|
|
|
|
|
|
opt = opt || {};
|
|
|
|
|
|
|
|
this.isDisabled = opt.isDisabled || _.constant(false);
|
2022-06-29 09:47:28 +00:00
|
|
|
this.isWindows = isWin();
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Register mouse callbacks to various sources.
|
|
|
|
* @param {Object} callbacks - an object containing mousedown/mouseup/mouseup functions
|
|
|
|
* for selecting and dragging, along with with the source string name and target element
|
|
|
|
* string name to which the mouse events must listen on.
|
|
|
|
* @param {string} handlerName - string name of the kind of element that the mouse callbacks
|
|
|
|
* are acting on.
|
|
|
|
* handlerName is used to deduce what kind of element is triggering the mouse callbacks
|
|
|
|
* The alternative is to look at triggering DOM element's css classes which is more hacky.
|
|
|
|
*/
|
|
|
|
Selector.prototype.registerMouseHandlers = function(callbacks, handlerName) {
|
|
|
|
this.setCallbackDefaults(callbacks);
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
this.view.onEvent(callbacks.mousedown.source, 'mousedown', callbacks.mousedown.elemName,
|
|
|
|
function(elem, event) {
|
|
|
|
if (self.isExemptMouseTarget(event) || event.button !== 0 || self.isDisabled()) {
|
|
|
|
return true; // Do nothing if the mouse event if exempt or not a left click
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!self.isSelected(elem, handlerName) && !callbacks.disableSelect()) {
|
|
|
|
self.applyCallbacks(SELECT, callbacks, elem, event);
|
|
|
|
} else if (!callbacks.disableDrag()) {
|
|
|
|
self.applyCallbacks(DRAG, callbacks, elem, event);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
Selector.prototype.isExemptMouseTarget = function(event) {
|
|
|
|
var cl = event.target.classList;
|
|
|
|
return _.some(this.exemptClasses, cl.contains.bind(cl));
|
|
|
|
};
|
|
|
|
|
|
|
|
Selector.prototype.setCallbackDefaults = function(callbacks) {
|
|
|
|
_.defaults(callbacks, {'mousedown': {}, 'mousemove': {}, 'mouseup': {},
|
|
|
|
'disableDrag': _.constant(false), 'disableSelect': _.constant(false)}
|
|
|
|
);
|
|
|
|
_.defaults(callbacks.mousedown, {'select': _.noop, 'drag': _.noop, 'elemName': null,
|
|
|
|
'source': null});
|
|
|
|
_.defaults(callbacks.mousemove, {'select': _.noop, 'drag': _.noop, 'elemName': null,
|
|
|
|
'source': document});
|
|
|
|
_.defaults(callbacks.mouseup, {'select': _.noop, 'drag': _.noop, 'elemName': null,
|
|
|
|
'source': document});
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Applies the drag or select callback for mousedown and then registers
|
|
|
|
* the appropriate mousemove and mouseup callbacks. We only register mousemove/mouseup
|
|
|
|
* after seeing a mousedown event so that we don't have to constantly listen for
|
|
|
|
* mousemove/mouseup.
|
|
|
|
* @param {String} dragOrSelect - string that is either 'drag' or 'select' which denotes
|
|
|
|
* which mouse methods to apply on mouse events.
|
|
|
|
* @param {Object} callbacks - an object containing mousedown/mouseup/mouseup functions
|
|
|
|
* for selecting and dragging, along with with the source string name and target element
|
|
|
|
* string name to which the mouse events must listen on.
|
|
|
|
*/
|
|
|
|
Selector.prototype.applyCallbacks = function(dragOrSelect, callbacks, mouseDownElem, mouseDownEvent) {
|
|
|
|
console.assert(dragOrSelect === DRAG || dragOrSelect === SELECT);
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
callbacks.mousedown[dragOrSelect].call(this.view, mouseDownElem, mouseDownEvent);
|
|
|
|
this.view.onEvent(callbacks.mousemove.source, 'mousemove', function(elem, event) {
|
2022-06-29 09:47:28 +00:00
|
|
|
// On windows, when browser doesn't have focus, the first click produces artificial mousemove
|
|
|
|
// event. Fortunately, the mousemove event has the same coordinates as the mousedown event, so
|
|
|
|
// we will ignore it.
|
|
|
|
// Related issues:
|
|
|
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=161464
|
|
|
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=721341#c34
|
|
|
|
if (self.isWindows) {
|
|
|
|
if (event.screenX === mouseDownEvent.screenX && event.screenY === mouseDownEvent.screenY) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2020-10-02 15:10:00 +00:00
|
|
|
callbacks.mousemove[dragOrSelect].call(self.view, elem, event);
|
|
|
|
});
|
|
|
|
|
|
|
|
this.view.onEvent(callbacks.mouseup.source, 'mouseup', function(elem, event) {
|
|
|
|
callbacks.mouseup[dragOrSelect].call(self.view, elem, event);
|
|
|
|
self.view.clearEvent(callbacks.mousemove.source, 'mousemove');
|
|
|
|
self.view.clearEvent(callbacks.mouseup.source, 'mouseup');
|
|
|
|
if (dragOrSelect === DRAG) self.currentDragType(NONE);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
// ===========================================================================
|
|
|
|
// CELL SELECTOR
|
|
|
|
|
|
|
|
function CellSelector(view, opt) {
|
|
|
|
Selector.call(this, view, opt);
|
|
|
|
|
|
|
|
// row or col.start denotes the anchor/initial index of the select range.
|
|
|
|
// start is not necessarily smaller than end.
|
|
|
|
// IE: clicking on col 10 and dragging until the mouse is on col 5 will yield: start = 10, end = 5
|
|
|
|
this.row = {
|
|
|
|
start: ko.observable(0),
|
|
|
|
end: ko.observable(0),
|
|
|
|
linePos: ko.observable('0px'),
|
|
|
|
dropIndex: ko.observable(-1),
|
|
|
|
};
|
|
|
|
this.col = {
|
|
|
|
start: ko.observable(0),
|
|
|
|
end: ko.observable(0),
|
|
|
|
linePos: ko.observable('0px'),
|
|
|
|
dropIndex: ko.observable(-1),
|
|
|
|
};
|
|
|
|
this.currentSelectType = ko.observable(NONE);
|
|
|
|
this.currentDragType = ko.observable(NONE);
|
|
|
|
|
|
|
|
this.autoDispose(this.view.cursor.rowIndex.subscribeInit(function(rowIndex) {
|
|
|
|
this.setToCursor();
|
|
|
|
}, this));
|
|
|
|
this.autoDispose(this.view.cursor.fieldIndex.subscribeInit(function(colIndex) {
|
|
|
|
this.setToCursor();
|
|
|
|
}, this));
|
|
|
|
}
|
|
|
|
|
|
|
|
dispose.makeDisposable(CellSelector);
|
|
|
|
_.extend(CellSelector.prototype, Selector.prototype);
|
|
|
|
|
|
|
|
CellSelector.prototype.setToCursor = function(elemType) {
|
|
|
|
// Must check that the view contains cursor.rowIndex/cursor.fieldIndex
|
|
|
|
// in case it has changed.
|
|
|
|
if (this.view.cursor.rowIndex) {
|
|
|
|
this.row.start(this.view.cursor.rowIndex());
|
|
|
|
this.row.end(this.view.cursor.rowIndex());
|
|
|
|
}
|
|
|
|
if (this.view.cursor.fieldIndex) {
|
|
|
|
this.col.start(this.view.cursor.fieldIndex());
|
|
|
|
this.col.end(this.view.cursor.fieldIndex());
|
|
|
|
}
|
|
|
|
this.currentSelectType(elemType || NONE);
|
|
|
|
};
|
|
|
|
|
|
|
|
CellSelector.prototype.containsCell = function(rowIndex, colIndex) {
|
|
|
|
return this.containsCol(colIndex) && this.containsRow(rowIndex);
|
|
|
|
};
|
|
|
|
|
|
|
|
CellSelector.prototype.containsRow = function(rowIndex) {
|
|
|
|
return gutil.between(rowIndex, this.row.start(), this.row.end());
|
|
|
|
};
|
|
|
|
|
|
|
|
CellSelector.prototype.containsCol = function(colIndex) {
|
|
|
|
return gutil.between(colIndex, this.col.start(), this.col.end());
|
|
|
|
};
|
|
|
|
|
|
|
|
CellSelector.prototype.isSelected = function(elem, handlerName) {
|
|
|
|
if (handlerName !== this.currentSelectType()) return false;
|
|
|
|
|
|
|
|
// TODO: this only works with view: GridView.
|
|
|
|
// But it seems like we only ever use selectors with gridview anyway
|
|
|
|
let row = this.view.domToRowModel(elem, handlerName);
|
|
|
|
let col = this.view.domToColModel(elem, handlerName);
|
|
|
|
switch (handlerName) {
|
|
|
|
case ROW:
|
|
|
|
return this.containsRow(row._index());
|
|
|
|
case COL:
|
|
|
|
return this.containsCol(col._index());
|
|
|
|
case CELL:
|
|
|
|
return this.containsCell(row._index(), col._index());
|
|
|
|
default:
|
|
|
|
console.error('Given element is not a row, cell or column');
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
CellSelector.prototype.isRowSelected = function(rowIndex) {
|
|
|
|
return this.isCurrentSelectType(COL) || this.containsRow(rowIndex);
|
|
|
|
};
|
|
|
|
|
|
|
|
CellSelector.prototype.isColSelected = function(colIndex) {
|
|
|
|
return this.isCurrentSelectType(ROW) || this.containsCol(colIndex);
|
|
|
|
};
|
|
|
|
|
|
|
|
CellSelector.prototype.isCellSelected = function(rowIndex, colIndex) {
|
|
|
|
return this.isColSelected(colIndex) && this.isRowSelected(rowIndex);
|
|
|
|
};
|
|
|
|
|
|
|
|
CellSelector.prototype.onlyCellSelected = function(rowIndex, colIndex) {
|
|
|
|
return (this.row.start() === rowIndex && this.row.end() === rowIndex) &&
|
|
|
|
(this.col.start() === colIndex && this.col.end() === colIndex);
|
|
|
|
};
|
|
|
|
|
|
|
|
CellSelector.prototype.isCurrentSelectType = function(elemType) {
|
|
|
|
return this._isCurrentType(this.currentSelectType(), elemType);
|
|
|
|
};
|
|
|
|
|
|
|
|
CellSelector.prototype.isCurrentDragType = function(elemType) {
|
|
|
|
return this._isCurrentType(this.currentDragType(), elemType);
|
|
|
|
};
|
|
|
|
|
|
|
|
CellSelector.prototype._isCurrentType = function(currentType, elemType) {
|
|
|
|
console.assert([ROW, COL, CELL, NONE].indexOf(elemType) !== -1);
|
|
|
|
return currentType === elemType;
|
|
|
|
};
|
|
|
|
|
|
|
|
CellSelector.prototype.colLower = function() {
|
|
|
|
return Math.min(this.col.start(), this.col.end());
|
|
|
|
};
|
|
|
|
|
|
|
|
CellSelector.prototype.colUpper = function() {
|
|
|
|
return Math.max(this.col.start(), this.col.end());
|
|
|
|
};
|
|
|
|
|
|
|
|
CellSelector.prototype.rowLower = function() {
|
|
|
|
return Math.min(this.row.start(), this.row.end());
|
|
|
|
};
|
|
|
|
|
|
|
|
CellSelector.prototype.rowUpper = function() {
|
|
|
|
return Math.max(this.row.start(), this.row.end());
|
|
|
|
};
|
|
|
|
|
|
|
|
CellSelector.prototype.colCount = function() {
|
|
|
|
return this.colUpper() - this.colLower() + 1;
|
|
|
|
};
|
|
|
|
|
|
|
|
CellSelector.prototype.rowCount = function() {
|
|
|
|
return this.rowUpper() - this.rowLower() + 1;
|
|
|
|
};
|
|
|
|
|
2021-04-26 21:54:09 +00:00
|
|
|
CellSelector.prototype.selectArea = function(rowStartIdx, colStartIdx, rowEndIdx, colEndIdx) {
|
2020-10-02 15:10:00 +00:00
|
|
|
this.row.start(rowStartIdx);
|
|
|
|
this.col.start(colStartIdx);
|
|
|
|
this.row.end(rowEndIdx);
|
|
|
|
this.col.end(colEndIdx);
|
|
|
|
// Only select the area if it's not a single cell
|
|
|
|
if (this.colCount() > 1 || this.rowCount() > 1) {
|
|
|
|
this.currentSelectType(CELL);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.CellSelector = CellSelector;
|