UNPKG

fin-hypergrid

Version:
1,402 lines (1,222 loc) 72 kB
/* 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#