fin-hypergrid
Version:
Canvas-based high-performance grid
1,402 lines (1,222 loc) • 72 kB
JavaScript
/* eslint-env browser */
'use strict';
require('./lib/polyfills'); // Installs misc. polyfills into global objects, as needed
var Point = require('rectangular').Point;
var Rectangle = require('rectangular').Rectangle;
var _ = require('object-iterators'); // fyi: installs the Array.prototype.find polyfill, as needed
var injectCSS = require('inject-stylesheet-template').bind(require('../css'));
var Base = require('./Base');
var themes = require('./themes');
var defaults = require('./defaults');
var dynamicPropertyDescriptors = require('./lib/dynamicProperties');
var Canvas = require('./lib/Canvas');
var Renderer = require('./renderer');
var SelectionModel = require('./lib/SelectionModel');
var Localization = require('./lib/Localization');
var Behavior = require('./behaviors/Behavior');
var behaviorJSON = require('./behaviors/JSON');
var cellRenderers = require('./cellRenderers');
var cellEditors = require('./cellEditors');
var EDGE_STYLES = ['top', 'bottom', 'left', 'right'],
RECT_STYLES = EDGE_STYLES.concat(['width', 'height', 'position']);
/**
* @mixes scrolling.mixin
* @mixes themes.instanceMixin
* @constructor
* @param {string|Element} [container] - CSS selector or Element
* @param {object} [options]
* @param {function} [options.Behavior=behaviors.JSON] - A grid behavior constructor (extended from {@link Behavior}).
* @param {function[]} [options.pipeline] - A list function constructors to use for passing data through a series of transforms to occur on reindex call
* @param {function|object[]} [options.data] - Passed to behavior constructor. May be:
* * An array of congruent raw data objects
* * A function returning same
* @param {function|menuItem[]} [options.schema=derivedSchema] - Passed to behavior constructor. May be:
* * A schema array
* * A function returning a schema array. Called at filter reset time with behavior as context.
* * Omit to generate a basic schema from `this.behavior.columns`.
*
* @param {pluginSpec|pluginSpec[]} [options.plugins]
*
* @param {subgridSpec[]} [options.subgrids]
*
* @param {object} [options.state]
*
* @param {string|Element} [options.container] - CSS selector or Element
*
* @param {string} [options.localization=Hypergrid.localization]
* @param {string|string[]} [options.localization.locale=Hypergrid.localization.locale] - The default locale to use when an explicit `locale` is omitted from localizer constructor calls. Passed to Intl.NumberFomrat` and `Intl.DateFomrat`. See {@ https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_identification_and_negotiation|Locale identification and negotiation} for more information.
* @param {string} [options.localization.numberOptions=Hypergrid.localization.numberOptions] - Options passed to `Intl.NumberFormat` for creating the basic "number" localizer.
* @param {string} [options.localization.dateOptions=Hypergrid.localization.dateOptions] - Options passed to `Intl.DateFomrat` for creating the basic "date" localizer.
*
* @param {object} [options.schema]
*
* @param {object} [options.margin] - Optional canvas "margins" applied to containing div as .left, .top, .right, .bottom. (Default values actually derive from 'grid' stylesheet's `.hypergrid-container` rule.)
* @param {string} [options.margin.top='0px']
* @param {string} [options.margin.right='0px']
* @param {string} [options.margin.bottom='0px']
* @param {string} [options.margin.left='0px']
*
* @param {object} [options.boundingRect] - Optional grid container size & position. (Default values actually derive from 'grid' stylesheet's `.hypergrid-container > div:first-child` rule.)
* @param {string} [options.boundingRect.height='500px']
* @param {string} [options.boundingRect.width='auto']
* @param {string} [options.boundingRect.left='auto']
* @param {string} [options.boundingRect.top='auto']
* @param {string} [options.boundingRect.right='auto']
* @param {string} [options.boundingRect.bottom='auto']
* @param {string} [options.boundingRect.position='relative']
*/
var Hypergrid = Base.extend('Hypergrid', {
initialize: function(container, options) {
this.selectionInitialize();
//Optional container argument
if (!(typeof container === 'string') && !(container instanceof HTMLElement)) {
options = container;
container = null;
}
this.options = options = options || {};
this.clearState();
//Set up the container for a grid instance
this.setContainer(
container ||
options.container ||
findOrCreateContainer(options.boundingRect)
);
// Install shared plug-ins (those with a `preinstall` method)
Hypergrid.prototype.installPlugins(options.plugins);
this.lastEdgeSelection = [0, 0];
this.isWebkit = navigator.userAgent.toLowerCase().indexOf('webkit') > -1;
this.selectionModel = new SelectionModel(this);
this.renderOverridesCache = {};
this.allowEventHandlers = true;
this.dragExtent = new Point(0, 0);
this.numRows = 0;
this.numColumns = 0;
this.clearMouseDown();
this.setFormatter(options.localization);
this.listeners = {};
/**
* @name cellRenderers
* @type {Registry}
* @memberOf Hypergrid#
*/
this.cellRenderers = cellRenderers;
/**
* Private version of cell editors registry with a bound `create` method for use by `getCellEditorAt`.
* @name cellEditors
* @type {Registry}
* @memberOf Hypergrid#
*/
this.cellEditors = Object.create(cellEditors);
Object.defineProperty(this.cellEditors, 'create', { value: createCellEditor.bind(this) });
this.initCanvas();
if (this.options.Behavior) {
this.setBehavior(this.options); // also sets this.options.pipeline and this.options.data
} else if (this.options.data) {
this.setData(this.options.data, this.options); // if no behavior has yet been set, `setData` sets a default behavior and this.options.pipeline
}
if (this.options.state) {
this.loadState(this.options.state);
}
/**
* @name plugins
* @summary Dictionary of named instance plug-ins.
* @desc See examples for how to reference (albeit there is normally no need to reference plugins directly).
*
* For the dictionary of _shared_ plugins, see {@link Hypergrid.plugins|plugins} (a property of the constructor).
* @example
* var instancePlugins = myGrid.plugins;
* var instancePlugins = this.plugins; // internal use
* var myInstancePlugin = myGrid.plugins.myInstancePlugin;
* @type {object}
* @memberOf Hypergrid#
*/
this.plugins = {};
// Install instance plug-ins (those that are constructors OR have an `install` method)
this.installPlugins(options.plugins);
// Listen for propagated mouseclicks. Used for aborting edit mode.
document.addEventListener('mousedown', this.mouseCatcher = function() {
this.abortEditing();
}.bind(this));
setTimeout(this.repaint.bind(this));
Hypergrid.grids.push(this);
this.resetGridBorder('Top');
this.resetGridBorder('Right');
this.resetGridBorder('Bottom');
this.resetGridBorder('Left');
},
terminate: function() {
document.removeEventListener('mousedown', this.mouseCatcher);
this.canvas.stop();
Hypergrid.grids.splice(this.grids.indexOf(this), 1);
},
resetGridBorder: function(edge) {
edge = edge || '';
var propName = 'gridBorder' + edge,
styleName = 'border' + edge,
props = this.properties,
border = props[propName];
switch (border) {
case true:
border = props.lineWidth + 'px solid ' + props.lineColor;
break;
case false:
border = null;
break;
}
this.canvas.canvas.style[styleName] = border;
},
registerCellEditor: function(Constructor, name) {
return this.deprecated('registerCellEditor(Constructor, name)', 'cellEditors.add(name, Constructor)', '1.0.6', arguments);
},
createCellEditor: function(name) {
return this.deprecated('createCellEditor(name)', 'cellEditors.create(name)', '1.0.6', arguments);
},
getCellProvider: function(name) {
return this.deprecated('getCellProvider()', 'cellRenderers', '1.0.6', arguments);
},
registerLocalizer: function(name, localizer, baseClassName, newClassName) {
return this.deprecated('registerLocalizer(name, localizer, baseClassName, newClassName)', 'localization.add(name, localizer)', '1.0.6', arguments,
'STRUCTURAL CHANGE: No longer supports deriving and registering a new cell editor class. Use .cellEditors.get(baseClassName).extend(newClassName || name, {...}) for that.');
},
getRenderer: function() {
return this.deprecated('getRenderer()', 'renderer', '1.1.0');
},
/**
*
* A null object behavior serves as a place holder.
* @type {object}
* @memberOf Hypergrid#
*/
behavior: null,
/**
* Cached resulan}
* @memberOf Hypergrid#
*/
isWebkit: true,
/**
* The pixel location of an initial mousedown click, either for editing a cell or for dragging a selection.
* @type {Point}
* @memberOf Hypergrid#
*/
mouseDown: [],
/**
* The extent from the mousedown point during a drag operation.
* @type {Point}
* @memberOf Hypergrid#
*/
dragExtent: null,
/**
* @property {fin-hypergrid-selection-model} selectionModel - A [fin-hypergrid-selection-model](module-._selection-model.html) instance.
* @memberOf Hypergrid#
*/
selectionModel: null,
/**
* @property {fin-hypergrid-cell-editor} cellEditor - The current instance of [fin-hypergrid-cell-editor](module-cell-editors_base.html).
* @memberOf Hypergrid#
*/
cellEditor: null,
/**
* @property {fin-vampire-bar} sbHScroller - An instance of {@link https://github.com/openfin/finbars|FinBar}.
* @memberOf Hypergrid#
*/
sbHScroller: null,
/**
* is the short term memory of what column I might be dragging around
* @type {object}
* @memberOf Hypergrid#
*/
renderOverridesCache: {},
/**
* The pixel location of the current hovered cell.
* @todo Need to detect hovering over bottom totals.
* @type {Point}
* @memberOf Hypergrid#
*/
hoverCell: null,
lastEdgeSelection: null,
/**
* @memberOf Hypergrid#
*/
setAttribute: function(attribute, value) {
this.div.setAttribute(attribute, value);
},
/**
* @memberOf Hypergrid#
*/
clearState: function() {
/**
* @name properties
* @type {object}
* @summary Object containing the properties of the grid.
* @desc Grid properties objects have the following structure:
* 1. User-configured properties and dynamic properties are in the "own" layer.
* 2. Extends from the theme object.
* 3. The theme object in turn extends from the {@link module:defaults|defaults} object.
*
* Note: Any changes the application developer may wish to make to the {@link module:defaults|defaults} object should be made _before_ reaching this point (_i.e.,_ prior to any grid instantiations).
* @memberOf Hypergrid#
*/
this.properties = Object.defineProperties(this.initThemeLayer(), {
grid: { value: this },
var: { value: new Var() }
});
// For all default props of object type, if a dynamic prop, invoke setter; else deep clone it so changes
// made to inner props won't go to object on theme or defaults layers which are shared by other instances.
Object.keys(defaults).forEach(function(key) {
var value = defaults[key];
if (typeof value === 'object') {
if (dynamicPropertyDescriptors[key]) {
this[key] = value; // invoke dynamic prop setter
} else {
this[key] = deepClone(value); // just a plain object
}
}
}, this.properties);
},
/**
* @desc Clear out all state settings, data (rows), and schema (columns) of a grid instance.
* @param {object} [options]
* @param {object} [options.subgrids] - Consumed by {@link Behavior#reset}.
* If omitted, previously established subgrids list is reused.
* @param {object} [options.pipeline] - Consumed by {@link dataModels.JSON#reset}.
* If omitted, previously established pipeline is reused.
* @param {object} [options.controllers] - Consumed by {@link dataModels.JSON#reset}.
* If omitted, previously established controllers list is reused.
* @memberOf Hypergrid#
*/
reset: function(options) {
this.clearState();
this.removeAllEventListeners();
this.lastEdgeSelection = [0, 0];
this.selectionModel.reset();
this.renderOverridesCache = {};
this.clearMouseDown();
this.dragExtent = new Point(0, 0);
this.numRows = 0;
this.numColumns = 0;
this.vScrollValue = 0;
this.hScrollValue = 0;
this.cancelEditing();
this.sbPrevVScrollValue = null;
this.sbPrevHScrollValue = null;
this.hoverCell = null;
this.scrollingNow = false;
this.lastEdgeSelection = [0, 0];
options = options || {};
this.behavior.reset({
subgrids: options.subgrids,
pipeline: options.pipeline,
controllers: options.controllers
});
this.renderer.reset();
this.canvas.resize();
this.behaviorChanged();
this.refreshProperties();
},
/** @typedef {object|function|Array} pluginSpec
* @desc One of:
* * simple API - a plain object with an `install` method
* * object API - an object constructor
* * array:
* * first element is an optional name for the API or the newly instantiated object
* * next element (or first element when not a string) is the simple or object API
* * remaining arguments are optional arguments for the object constructor
* * falsy value such as `undefined` - ignored
*
* The API may have a `name` or `$$CLASS_NAME` property.
*/
/**
* @summary Install plugins.
* @desc Plugin installation:
* * Each simple API is installed by calling it's `install` method with `this` as first arg + any additional args listed in the `pluginSpec` (when it is an array).
* * Each object API is installed by instantiating it's constructor with `this` as first arg + any additional args listed in the `pluginSpec` (when it is an array).
*
* The resulting plain object or instantiated objects may be named by (in priority order):
* 1. if `pluginSpec` contains an array and first element is a string
* 2. object has a `name` property
* 3. object has a `$$CLASS_NAME` property
*
* If named, a reference to each object is saved in `this.plugins`. If the plug-in is unnamed, no reference is kept.
*
* There are two types of plugin installations:
* * Preinstalled plugins which are installed on the prototype. These are simple API plugins with a `preinstall` method called with the `installPlugins` calling context as the first argument. Preinstallations are automatically performed whenever a grid is instantiated (at the beginning of the constructor), by calling `installPlugins` with `Hypergrid.prototype` as the calling context.
* * Regular plugins which are installed on the instance. These are simple API plugins with an `install` method, as well as all object API plugins (constructors), called with the `installPlugins` calling context as the first argument. These installations are automatically performed whenever a grid is instantiated (at the end of the constructor), called with the new grid instance as the calling context.
*
* The "`installPlugins` calling context" means either the grid instance or its prototype, depending on how this method is called.
*
* Plugins may have both `preinstall` _and_ `install` methods, in which case both will be called. However, note that in any case, `install` methods on object API plugins are ignored.
*
* @this {Hypergrid}
* @param {pluginSpec|pluginSpec[]} [plugins] - The plugins to install. If omitted, the call is a no-op.
* @memberOf Hypergrid#
*/
installPlugins: function(plugins) {
var shared = this === Hypergrid.prototype; // Do shared ("preinstalled") plugins (if any)
if (!plugins) {
return;
} else if (!Array.isArray(plugins)) {
plugins = [plugins];
}
plugins.forEach(function(plugin) {
var name, args, hash;
if (!plugin) {
return; // ignore falsy plugin spec
}
// set first arg of constructor to `this` (the grid instance)
// set first arg of `install` method to `this` (the grid instance)
// set first two args of `preinstall` method to `this` (the Hypergrid prototype) and the Behavior prototype
args = [this];
if (shared) {
args.push(Behavior.prototype);
}
if (Array.isArray(plugin)) {
if (!plugin.length) {
plugin = undefined;
} else if (typeof plugin[0] !== 'string') {
args = args.concat(plugin.slice(1));
plugin = plugin[0];
} else if (plugin.length >= 2) {
args = args.concat(plugin.slice(2));
name = plugin[0];
plugin = plugin[1];
} else {
plugin = undefined;
}
}
if (!plugin) {
return; // ignore empty array or array with single string element
}
// Derive API name if not given in pluginSpec
name = name || plugin.name || plugin.$$CLASS_NAME;
if (name) {
// Translate first character to lower case
name = name.substr(0, 1).toLowerCase() + name.substr(1);
}
if (shared) {
// Execute the `preinstall` method
hash = this.constructor.plugins;
if (plugin.preinstall && !hash[name]) {
plugin.preinstall.apply(plugin, args);
}
} else { // instance plug-ins:
hash = this.plugins;
if (typeof plugin === 'function') {
// Install "object API" by instantiating
plugin = this.createApply(plugin, args);
} else if (plugin.install) {
// Install "simple API" by calling its `install` method
plugin.install.apply(plugin, args);
} else if (!plugin.preinstall) {
throw new Base.prototype.HypergridError('Expected plugin (a constructor; or an API with a `preinstall` method and/or an `install` method).');
}
}
if (name) {
hash[name] = plugin;
}
}, this);
},
/**
* @summary Uninstall all uninstallable plugins or just named plugins.
* @desc Calls `uninstall` on plugins that define such a method.
*
* To uninstall "preinstalled" plugins, call with `Hypergrid.prototype` as context.
*
* For convenience, the following args are passed to the call:
* * `this` - the plugin to be uninstalled
* * `grid` - the hypergrid object
* * `key` - name of the plugin to be uninstalled (_i.e.,_ key in `plugins`)
* * `plugins` - the plugins hash (a.k.a. `grid.plugins`)
* @param {string|stirng[]} [pluginNames] If provided, limit uninstall to the named plugin (string) or plugins (string[]).
* @memberOf Hypergrid#
*/
uninstallPlugins: function(pluginNames) {
if (!pluginNames) {
pluginNames = [];
} else if (!Array.isArray(pluginNames)) {
pluginNames = [pluginNames];
}
_(this.plugins).each(function(plugin, key, plugins) {
if (
plugins.hasOwnProperty(key) &&
pluginNames.indexOf(key) >= 0 &&
plugin.uninstall
) {
plugin.uninstall(this, key, plugins);
}
}, this);
},
getProperties: function() {
return this.deprecated('getProperties()', 'properties', '1.2.0');
},
computeCellsBounds: function() {
this.renderer.computeCellsBounds();
},
setFormatter: function(options) {
options = options || {};
this.localization = new Localization(
options.locale || Hypergrid.localization.locale,
options.numberOptions || Hypergrid.localization.numberOptions,
options.dateOptions || Hypergrid.localization.dateOptions
);
},
getFormatter: function(localizerName) {
return this.localization.get(localizerName).format;
},
formatValue: function(localizerName, value) {
var formatter = this.getFormatter(localizerName);
return formatter(value);
},
isRowResizeable: function() {
return this.deprecated('isRowResizeable()', 'properties.rowResize', 'v1.2.10');
},
isCheckboxOnlyRowSelections: function() {
return this.deprecated('isCheckboxOnlyRowSelections()', 'properties.checkboxOnlyRowSelections', 'v1.2.10');
},
/**
* @memberOf Hypergrid#
* @returns {Point} The cell over which the cursor is hovering.
*/
getHoverCell: function() {
return this.deprecated('getHoverCell()', 'hoverCell', 'v1.2.0');
},
/**
* @memberOf Hypergrid#
* @desc Set the cell under the cursor.
* @param {CellEvent} cellEvent
*/
setHoverCell: function(cellEvent) {
var hoverCell = this.hoverCell;
if (!hoverCell || !hoverCell.equals(cellEvent.gridCell)) {
this.hoverCell = cellEvent.gridCell;
if (hoverCell) {
this.fireSyntheticOnCellExitEvent(cellEvent);
}
this.fireSyntheticOnCellEnterEvent(cellEvent);
this.repaint();
}
},
/**
* @memberOf Hypergrid#
* @desc Amend properties for this hypergrid only.
* @param {object} moreProperties - A simple properties hash.
*/
addProperties: function(properties) {
Object.assign(this.properties, properties);
this.refreshProperties();
},
/**
* @todo deprecate this in favor of making properties dynamic instead (for those that need to be)
* @memberOf Hypergrid#
* @desc Utility function to push out properties if we change them.
* @param {object} properties - An object of various key value pairs.
*/
refreshProperties: function() {
this.behaviorShapeChanged();
this.behavior.defaultRowHeight = null;
this.behavior.autosizeAllColumns();
},
/**
* @memberOf Hypergrid#
* @returns {object} The state object for remembering our state.
* @see [Memento pattern](http://en.wikipedia.org/wiki/Memento_pattern)
*/
getPrivateState: function() {
return this.deprecated('getPrivateState()', 'properties', '1.2.0');
},
/**
* @memberOf Hypergrid#
* @desc Set the state object to return to the given user configuration.
* @param {object} state - A memento object.
* @see [Memento pattern](http://en.wikipedia.org/wiki/Memento_pattern)
*/
setState: function(state) {
this.addState(state, true);
},
/**
* @memberOf Hypergrid#
* @desc Add to the state object.
* @param {object} state
*/
addState: function(state, settingState) {
this.behavior.addState(state, settingState);
this.refreshProperties();
this.behaviorChanged();
},
getState: function() {
return this.behavior.getState();
},
loadState: function(state) {
this.behavior.setState(state);
},
/**
* @todo Only output values when they differ from defaults (deep compare needed).
* @param {object} [options]
* @param {string[]} [options.blacklist] - List of grid properties to exclude. Pertains to grid own properties only.
* @param {boolean} [options.compact] - Run garbage collection first. The only property this current affects is `properties.calculators` (removes unused calculators).
* @param {number|string} [options.space='\t'] - For no space, give `0`. (See {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify|JSON.stringify}'s `space` param other options.)
* @param {function} [options.headerify] - If your headers were generated by a function (taking column name as a parameter), give a reference to that function here to avoid persisting headers that match the generated string.
* @memberOf Hypergrid#
*/
saveState: function(options) {
options = options || {};
var space = options.space === undefined ? '\t' : options.space,
properties = this.properties,
calculators = properties.calculators,
blacklist = options.blacklist = options.blacklist || [];
blacklist.push('columnProperties'); // Never output this synonym of 'columns'
if (calculators) {
if (options.compact) {
var columns = this.behavior.getColumns();
Object.keys(calculators).forEach(function(key) {
if (!columns.find(function(column) { return column.properties.calculator === calculators[key]; })) {
delete calculators[key];
}
});
}
calculators.toJSON = stringifyFunctions;
}
// Temporarily copy the given headerify function for access by columns getter
this.headerify = options.headerify;
var json = JSON.stringify(properties, function(key, value) {
if (this === properties && options.blacklist.indexOf(key) >= 0) {
value = undefined; // JSON.stringify ignores undefined props
} else if (key === 'calculator') {
if (calculators) {
// convert function reference to registry key
value = Object.keys(calculators).find(function(key) {
return calculators[key] === value;
});
} else {
// registry may not exist if Column.calculator setter was used directly so just save as is
value = value.toString();
}
}
return value;
}, space);
// Remove the temporary copy
delete this.headerify;
return json;
},
/**
* @memberOf Hypergrid#
* @returns {object} The initial mouse position on a mouse down event for cell editing or a drag operation.
* @memberOf Hypergrid#
*/
getMouseDown: function() {
if (this.mouseDown.length) {
return this.mouseDown[this.mouseDown.length - 1];
}
},
/**
* @memberOf Hypergrid#
* @desc Remove the last item from the mouse down stack.
*/
popMouseDown: function() {
var result;
if (this.mouseDown.length) {
result = this.mouseDown.pop();
}
return result;
},
/**
* @memberOf Hypergrid#
* @desc Empty out the mouse down stack.
*/
clearMouseDown: function() {
this.mouseDown = [new Point(-1, -1)];
this.dragExtent = null;
},
/**
* Set the mouse point that initiated a cell edit or drag operation.
* @param {Point} point
* @memberOf Hypergrid#
*/
setMouseDown: function(point) {
this.mouseDown.push(point);
},
/**
* @memberOf Hypergrid#
* @returns {Point} The extent point of the current drag selection rectangle.
*/
getDragExtent: function() {
return this.dragExtent;
},
/**
* @memberOf Hypergrid#
* @summary Set the extent point of the current drag selection operation.
* @param {Point} point
*/
setDragExtent: function(point) {
this.dragExtent = point;
},
/**
* @memberOf Hypergrid#
* @desc This function is a callback from the HypergridRenderer sub-component. It is called after each paint of the canvas.
*/
gridRenderedNotification: function() {
if (this.cellEditor) {
this.cellEditor.gridRenderedNotification();
}
if (this.checkColumnAutosizing()) {
this.paintNow();
}
this.fireSyntheticGridRenderedEvent();
},
tickNotification: function() {
this.fireSyntheticTickEvent();
},
/**
* @memberOf Hypergrid#
* @desc The grid has just been rendered, make sure the column widths are optimal.
*/
checkColumnAutosizing: function() {
var autoSized = this.behavior.checkColumnAutosizing(false);
if (autoSized) {
this.behaviorShapeChanged();
}
return autoSized;
},
/**
* @memberOf Hypergrid#
* @summary Conditionally copy to clipboard.
* @desc If we have focus, copy our current selection data to the system clipboard.
* @param {event} event - The copy system event.
*/
checkClipboardCopy: function(event) {
if (this.hasFocus()) {
event.preventDefault();
var csvData = this.getSelectionAsTSV();
event.clipboardData.setData('text/plain', csvData);
}
},
/**
* @memberOf Hypergrid#
* @returns {boolean} We have focus.
*/
hasFocus: function() {
return this.canvas.hasFocus();
},
/**
* @memberOf Hypergrid#
* @summary Set the Behavior (model) object for this grid control.
* @desc This can be done dynamically.
* @param {object} options - _(See {@link behaviors.JSON#setData}.)_
* @param {Behavior} [options.behavior=behaviors.JSON] - The behavior (model) can be either a constructor or an instance.
* @param {dataRowObject[]} [options.data] - _(See {@link behaviors.JSON#setData}.)_
* @param {pipelineSchema} [options.pipeline] - New pipeline description.
*/
setBehavior: function(options) {
if (!this.behavior) {
// If we get here it means:
// 1. Called from constructor because behavior included in options object.
// 2. Called from `setData` _and_ wasn't called explicitly since instantiation
var Behavior = options.Behavior || behaviorJSON;
this.behavior = new Behavior(this, options);
this.initScrollbars();
this.refreshProperties();
this.behavior.reindex();
}
},
/**
* @memberOf Hypergrid#
* @summary Set the underlying datasource.
* @desc This can be done dynamically.
* @param {function|object[]} dataRows - May be:
* * An array of congruent raw data objects.
* * A function returning same.
* @param {object} [options] - _(See {@link behaviors.JSON#setData}.)_
*/
setData: function(dataRows, options) {
// Call `setBehavior` here just in case not previously set by constructor _or_ explicitly since instantiation
this.setBehavior({
pipeline: this.options.pipeline
});
this.behavior.setData(dataRows, options);
this.setInfo(dataRows.length ? '' : this.properties.noDataMessage);
this.behavior.shapeChanged();
},
setInfo: function(messages) {
this.renderer.setInfo(messages);
},
/**
* @memberOf Behavior#
*/
reindex: function() {
if (this.properties.repaintImmediately) {
this.behavior.reindex();
} else {
this.needsReindex = true;
}
this.behaviorShapeChanged();
},
/**
* @memberOf Hypergrid#
* @summary _(See {@link Hypergrid.prototype#setData}.)_
* @desc Binds the data and reshapes the grid (new column objects created)
* @param {function|object[]} dataRows - May be:
* * An array of congruent raw data objects.
* * A function returning same.
* @param {object} [options]
*/
updateData: function(dataRows, options){
this.deprecated('updateData(dataRows, options)', 'setData(dataRows, options)', 'v1.2.10', arguments,
'To update data without changing column definitions, call setData _without a schema._');
},
/**
* @memberOf Hypergrid#
* @param {object} [pipelines] - New pipeline description. _(See {@link dataModels.JSON#setPipeline}.)_
* @param {object} [options] - _(See {@link dataModels.JSON#setPipeline}.)_
*/
setPipeline: function(DataSources, options){
this.behavior.setPipeline(DataSources, options);
},
/**
* @memberOf Hypergrid#
* @desc I've been notified that the behavior has changed.
*/
behaviorChanged: function() {
if (this.divCanvas) {
if (this.numColumns !== this.getColumnCount() || this.numRows !== this.getRowCount()) {
this.numColumns = this.getColumnCount();
this.numRows = this.getRowCount();
this.behaviorShapeChanged();
} else {
this.behaviorStateChanged();
}
}
},
/**
* @memberOf Hypergrid#
* @desc The dimensions of the grid data have changed. You've been notified.
*/
behaviorShapeChanged: function() {
if (!this.properties.repaintImmediately) {
this.needsShapeChanged = true;
this.canvas.repaint();
} else if (this.divCanvas) {
this.synchronizeScrollingBoundaries(); // calls computeCellsBounds
this.repaint();
}
},
/**
* @memberOf Hypergrid#
* @desc The dimensions of the grid data have changed. You've been notified.
*/
behaviorStateChanged: function() {
if (!this.properties.repaintImmediately) {
this.needsStateChanged = true;
this.canvas.repaint();
} else if (this.divCanvas) {
this.computeCellsBounds();
this.repaint();
}
},
/**
* Called from renderer/index.js
*/
deferredBehaviorChange: function() {
if (this.needsReindex) {
this.behavior.reindex();
this.needsReindex = false;
}
if (this.needsShapeChanged) {
if (this.divCanvas) {
this.synchronizeScrollingBoundaries(); // calls computeCellsBounds
}
} else if (this.needsStateChanged) {
if (this.divCanvas) {
this.computeCellsBounds();
}
}
this.needsShapeChanged = this.needsStateChanged = false;
},
/**
* @memberOf Hypergrid#
* @returns {Rectangle} My bounds.
*/
getBounds: function() {
return this.renderer.getBounds();
},
/**
* @memberOf Hypergrid#
* @returns {string} The value of a lnf property.
* @param {string} key - A look-and-feel key.
*/
resolveProperty: function(key) {
// todo: when we remove this method, also remove forwards from Behavior.js and Renderer.js
this.deprecated('resolveProperty', '.resolveProperty(key) deprecated as of v1.2.0 in favor of .properties dereferenced by [key]. (Will be removed in a future version.)');
return this.properties[key];
},
repaint: function() {
var canvas = this.canvas;
if (canvas) {
if (this.properties.repaintImmediately) {
canvas.paintNow();
} else {
canvas.repaint();
}
}
},
/**
* @memberOf Hypergrid#
* @desc Paint immediately in this microtask.
*/
paintNow: function() {
this.canvas.paintNow();
},
/**
* @memberOf Hypergrid#
* @returns {boolean} In HiDPI mode (has an attribute as such).
*/
useHiDPI: function() {
return this.deprecated('useHiDPI()', 'properties.useHiDPI', 'v1.2.10');
},
/**
* @memberOf Hypergrid#
* @summary Set the container for a grid instance
* @private
*/
setContainer: function(div) {
this.initContainer(div);
this.initRenderer();
// injectGridElements.call(this);
},
/**
* @memberOf Hypergrid#
* @summary Initialize container
* @private
*/
initContainer: function(div) {
if (typeof div === 'string') {
div = document.querySelector(div);
}
//Default Position and height to ensure DnD works
if (!div.style.position) {
div.style.position = null; // revert to stylesheet value
}
if (div.clientHeight < 1) {
div.style.height = null; // revert to stylesheet value
}
injectCSS('grid');
//prevent the default context menu for appearing
div.oncontextmenu = function(event) {
event.stopPropagation();
event.preventDefault();
return false;
};
div.removeAttribute('tabindex');
div.classList.add('hypergrid-container');
div.id = div.id || 'hypergrid' + (document.querySelectorAll('.hypergrid-container').length - 1 || '');
this.div = div;
},
/**
* @memberOf Hypergrid#
* @summary Initialize drawing surface.
* @private
*/
initCanvas: function() {
if (!this.divCanvas) {
var divCanvas = document.createElement('div');
setStyles(divCanvas, this.options.margin, EDGE_STYLES);
this.div.appendChild(divCanvas);
var canvas = new Canvas(divCanvas, this.renderer, this.options.canvas);
canvas.canvas.classList.add('hypergrid');
this.divCanvas = divCanvas;
this.canvas = canvas;
this.delegateCanvasEvents();
}
},
convertViewPointToDataPoint: function(unscrolled) {
return this.behavior.convertViewPointToDataPoint(unscrolled);
},
convertDataPointToViewPoint: function(dataPoint) {
return this.behavior.convertDataPointToViewPoint(dataPoint);
},
/**
* @memberOf Hypergrid#
* @desc Switch the cursor for a grid instance.
* @param {string|string[]} cursorName - A well know cursor name.
* @see [cursor names](http://www.javascripter.net/faq/stylesc.htm)
*/
beCursor: function(cursorName) {
if (!cursorName) {
cursorName = ['default'];
} else if (!Array.isArray(cursorName)) {
cursorName = [cursorName];
}
cursorName.forEach(function(name) { this.cursor = name; }, this.div.style);
},
/**
* @summary Shut down the current cell editor and save the edited value.
* @returns {boolean} One of:
* * `false` - Editing BUT could not abort.
* * `true` - Not editing OR was editing AND abort was successful.
* @memberOf Hypergrid#
*/
stopEditing: function() {
return !this.cellEditor || this.cellEditor.stopEditing();
},
/**
* @summary Shut down the current cell editor without saving the edited val
* @returns {boolean} One of:
* * `false` - Editing BUT could not abort.
* * `true` - Not editing OR was editing AND abort was successful.
* @memberOf Hypergrid#
*/
cancelEditing: function() {
return !this.cellEditor || this.cellEditor.cancelEditing();
},
/**
* @summary Give cell editor opportunity to cancel (or something) instead of stop .
* @returns {boolean} One of:
* * `false` - Editing BUT could not abort.
* * `true` - Not editing OR was editing AND abort was successful.
* @memberOf Hypergrid#
*/
abortEditing: function() {
return !this.cellEditor || (
this.cellEditor.abortEditing ? this.cellEditor.abortEditing() : this.cellEditor.stopEditing()
);
},
/**
* @memberOf Hypergrid#
* @returns {Rectangle} The pixel coordinates of just the center 'main" data area.
*/
getDataBounds: function() {
var b = this.canvas.bounds;
return new Rectangle(0, 0, b.origin.x + b.extent.x, b.origin.y + b.extent.y);
},
/**
* @memberOf Hypergrid#
* @returns {Canvas} Our fin-canvas instance.
*/
getCanvas: function() {
return this.deprecated('getCanvas()', 'canvas', '1.2.2');
},
/**
* @memberOf Hypergrid#
* @summary Open the cell-editor for the cell at the given coordinates.
* @param {CellEvent} event - Coordinates of "edit point" (gridCell.x, dataCell.y).
* @return {undefined|CellEditor} The cellEditor determined from the cell's render properties, which may be modified by logic added by overriding {@link DataModel#getCellEditorAt|getCellEditorAt}.
*/
editAt: function(event) {
var cellEditor;
if (arguments.length === 2) {
return this.deprecated('editAt(cellEditor, event)', 'editAt(event)', '1.0.6', arguments);
}
this.abortEditing(); // if another editor is open, close it first
if (
event.isDataColumn &&
event.properties[event.isDataRow ? 'editable' : 'filterable'] &&
(cellEditor = this.getCellEditorAt(event))
) {
cellEditor.beginEditing();
}
return cellEditor;
},
/**
* @memberOf Hypergrid#
* @param {number} columnIndex - The column index in question.
* @returns {boolean} The given column is fully visible.
*/
isColumnVisible: function(columnIndex) {
return this.renderer.isColumnVisible(columnIndex);
},
/**
* @memberOf Hypergrid#
* @param {number} r - The raw row index in question.
* @returns {boolean} The given row is fully visible.
*/
isDataRowVisible: function(r) {
return this.renderer.isDataRowVisible(r);
},
/**
* @memberOf Hypergrid#
* @param {number} c - The column index in question.
* @param {number} rn - The grid row index in question.
* @returns {boolean} The given cell is fully is visible.
*/
isDataVisible: function(c, rn) {
return this.isDataRowVisible(rn) && this.isColumnVisible(c);
},
/**
* @memberOf Hypergrid#
* @summary Scroll in the `offsetX` direction if column index `colIndex` is not visible.
* @param {number} colIndex - The column index in question.
* @param {number} offsetX - The direction and magnitude to scroll if we need to.
* @return {boolean} Column is visible.
*/
insureModelColIsVisible: function(colIndex, offsetX) {
var maxCols = this.getColumnCount() - 1, // -1 excludes partially visible columns
indexToCheck = colIndex + Math.sign(offsetX),
visible = !this.isColumnVisible(indexToCheck) || colIndex === maxCols;
if (visible) {
//the scroll position is the leftmost column
this.scrollBy(offsetX, 0);
}
return visible;
},
/**
* @memberOf Hypergrid#
* @summary Scroll in the `offsetY` direction if column index c is not visible.
* @param {number} rowIndex - The column index in question.
* @param {number} offsetX - The direction and magnitude to scroll if we need to.
* @return {boolean} Row is visible.
*/
insureModelRowIsVisible: function(rowIndex, offsetY) {
var maxRows = this.getRowCount() - 1, // -1 excludes partially visible rows
scrollOffset = (offsetY > -1) ? 2 : 0, // 2 to keep one blank line below active cell, 0 to keep zero lines above active cell
indexToCheck = rowIndex + scrollOffset,
visible = !this.isDataRowVisible(indexToCheck) || rowIndex === maxRows;
if (visible) {
//the scroll position is the topmost row
this.scrollBy(0, offsetY);
}
return visible;
},
/**
* @memberOf Hypergrid#
* @summary Answer which data cell is under a pixel value mouse point.
* @param {mousePoint} mouse - The mouse point to interrogate.
*/
getGridCellFromMousePoint: function(mouse) {
return this.renderer.getGridCellFromMousePoint(mouse);
},
/**
* @param {Point} gridCell - The pixel location of the mouse in physical grid coordinates.
* @returns {Rectangle} The pixel based bounds rectangle given a data cell point.
* @memberOf Hypergrid#
*/
getBoundsOfCell: function(gridCell) {
var b = this.renderer.getBoundsOfCell(gridCell.x, gridCell.y);
//convert to a proper rectangle
return new Rectangle(b.x, b.y, b.width, b.height);
},
/**
* @memberOf Hypergrid#
* @desc This is called by the fin-canvas when a resize occurs.
*/
resized: function() {
this.behaviorShapeChanged();
},
/**
* @memberOf Hypergrid#
* @summary A click event occurred.
* @desc Determine the cell and delegate to the behavior (model).
* @param {MouseEvent} event - The mouse event to interrogate.
* @returns {boolean|undefined} Changed. Specifically, one of:
* * `undefined` row had no drill-down control
* * `true` drill-down changed
* * `false` drill-down unchanged (was already in requested state)
*/
cellClicked: function(event) {
var result = this.behavior.cellClicked(event);
if (result !== undefined) {
this.behavior.changed();
}
return result;
},
/**
* To intercept link clicks, override this method (either on the prototype to apply to all grid instances or on an instance to apply to a specific grid instance).
* @memberOf Hypergrid#
*/
windowOpen: function(url, name, features, replace) {
return window.open.apply(window, arguments);
},
/**
* @param {number} [begin]
* @param {nubmer} [end]
* * @returns {Column[]} A copy of the all columns array by passing the params to `Array.prototype.slice`.
*/
getColumns: function(begin, end) {
var columns = this.behavior.getColumns();
return columns.slice.apply(columns, arguments);
},
/**
* @param {number} [begin]
* @param {nubmer} [end]
* * @returns {Column[]} A copy of the active columns array by passing the params to `Array.prototype.slice`.
*/
getActiveColumns: function(begin, end) {
var columns = this.behavior.getActiveColumns();
return columns.slice.apply(columns, arguments);
},
getHiddenColumns: function(){
//A non in-memory behavior will be more troublesome
return this.behavior.getHiddenColumns();
},
isViewableButton: function(c, r) {
return this.renderer.isViewableButton(c, r);
},
/**
* @memberOf Hypergrid#
* @desc Request input focus.
*/
takeFocus: function() {
var wasCellEditor = this.cellEditor;
this.stopEditing();
if (!wasCellEditor) {
this.canvas.takeFocus();
}
},
/**
* @memberOf Hypergrid#
* @desc Request focus for our cell editor.
*/
editorTakeFocus: function() {
if (this.cellEditor) {
return this.cellEditor.takeFocus();
}
},
/**
* @memberOf Hypergrid#
* @summary Get data value at given cell.
* @param {number} x - The horizontal coordinate.
* @param {number} y - The vertical coordinate.
*/
getValue: function(x, y) {
return this.behavior.getValue.apply(this.behavior, arguments); // must use .apply (see this.behavior.getValue)
},
/**
* @memberOf Hypergrid#
* @summary Set a data value of a given cell.
* @param {number} x - The horizontal coordinate.
* @param {number} y - The vertical coordinate.
* @param {*} value - New cell value.
*/
setValue: function(x, y, value) {
this.behavior.setValue.apply(this.behavior, arguments); // must use .apply (see this.behavior.setValue)
},
/**
* @memberOf Hypergrid#
* @desc Note that "viewable rows" includes any partially viewable rows.
* @returns {number} The number of viewable rows.
*/
getVisibleRows: function() {
return this.renderer.getVisibleRows();
},
/**
* @memberOf Hypergrid#
* @desc Note that "viewable columns" includes any partially viewable columns.
* @returns {number} The number of viewable columns.
*/
getVisibleColumns: function() {
return this.renderer.getVisibleColumns();
},
/**
* @memberOf Hypergrid#
* @summary Initialize the renderer sub-component.
*/
initRenderer: function() {
this.renderer = this.renderer || new Renderer(this);
},
/**
* @memberOf Hypergrid#
* @returns {number} The width of the given column.
* @param {number} columnIndex - The untranslated column index.
*/
getColumnWidth: function(columnIndex) {
return this.behavior.getColumnWidth(columnIndex);
},
/**
* @memberOf Hypergrid#