diff --git a/js/notebook/src/TableDisplay.js b/js/notebook/src/TableDisplay.js deleted file mode 100644 index 5238f8ff11..0000000000 --- a/js/notebook/src/TableDisplay.js +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2017 TWO SIGMA OPEN SOURCE, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var _ = require('underscore'); -var $ = require('jquery'); -var widgets = require('./widgets'); -var DataGridScope = require('./tableDisplay/dataGrid').DataGridScope; - -require('./tableDisplay/css/datatables.scss'); - -var TableDisplayModel = widgets.DOMWidgetModel.extend({ - defaults: function() { - return _.extend({}, widgets.DOMWidgetModel.prototype.defaults.apply(this), { - _model_name: 'TableDisplayModel', - _view_name: 'TableDisplayView', - _model_module: 'beakerx', - _view_module: 'beakerx', - _model_module_version: BEAKERX_MODULE_VERSION, - _view_module_version: BEAKERX_MODULE_VERSION - }); - } -}); - - -// Custom View. Renders the widget model. -var TableDisplayView = widgets.DOMWidgetView.extend({ - render: function() { - var that = this; - - this._currentScope = null; - - this.$el.addClass('beaker-table-display'); - - this.displayed.then(function() { - var tableModel = that.model.get('model'); - if (tableModel.tooManyRows) { - that.showWarning(tableModel); - } - that.initDataGridTable(tableModel); - - that.listenTo(that.model, 'beakerx-tabSelected', function() { - that._currentScope && that._currentScope.adjustRedraw(); - }); - - that.listenTo(that.model, 'change:updateData', that.handleUpdateData); - that.listenTo(that.model, 'change:model', that.handleModellUpdate); - }); - - this.on('remove', function() { - if (that._currentScope) { - that._currentScope.doDestroy(true); - that._currentScope = null; - } - }); - }, - - handleModellUpdate: function() { - var newModel = this.model.get('model'); - this._currentScope.updateModelData(newModel); - this._currentScope.doResetAll(); - }, - - handleUpdateData: function() { - var change = this.model.get('updateData'); - var currentModel = this.model.get('model'); - var updatedModel = _.extend(currentModel, change); - this.model.set('model', updatedModel, {updated_view: this}); - this.handleModellUpdate(); - }, - - showWarning: function(data) { - var rowLength = data.rowLength; - var rowLimit = data.rowLimit; - var tmpl = '
Note: table is too big to display. ' + - 'The limit is ' + rowLimit + ' rows, but this table has ' + rowLength + ' rows. ' + - 'The first 10000 rows are displayed as a preview.
Note: table is too big to display. + The limit is ${rowLimit} rows, but this table has ${rowLength} rows. + The first 10000 rows are displayed as a preview.
`; + + this.el.appendChild(modal); + } + + initDataGridTable(data) { + this._currentScope = new DataGridScope({ + element: this.el, + data: data, + widgetModel: this.model, + widgetView: this + }); + + this._currentScope.render(); + } + + remove() { + this._currentScope && this._currentScope.doDestroy(); + + if (this.pWidget) { + this.pWidget.dispose(); + } + + setTimeout(() => { this._currentScope = null; }); + + return super.remove.call(this); + } +} + +export default { + TableDisplayModel: TableDisplayModel, + TableDisplayView: TableDisplayView +}; diff --git a/js/notebook/src/contextMenu/BkoContextMenu.ts b/js/notebook/src/contextMenu/BkoContextMenu.ts index 33629a61f2..941d099cda 100644 --- a/js/notebook/src/contextMenu/BkoContextMenu.ts +++ b/js/notebook/src/contextMenu/BkoContextMenu.ts @@ -182,5 +182,9 @@ export default abstract class BkoContextMenu implements MenuInterface { unbind(): void { this.scope.element[0].removeEventListener('contextmenu', this.handleContextMenu); + + setTimeout(() => { + this.scope = null; + }); } } diff --git a/js/notebook/src/index.js b/js/notebook/src/index.js index 343b0bdeb1..cb32c60907 100644 --- a/js/notebook/src/index.js +++ b/js/notebook/src/index.js @@ -28,11 +28,11 @@ module.exports = {}; var loadedModules = [ require("./Plot"), - require("./TableDisplay"), require("./EasyForm"), require("./TabView"), require("./GridView"), require("./CyclingDisplayBox"), + require("./TableDisplay").default, require("./SparkUI").default, require("./SparkStateProgress").default, require("./SparkConfiguration").default, diff --git a/js/notebook/src/tableDisplay/dataGrid/BeakerXDataGrid.ts b/js/notebook/src/tableDisplay/dataGrid/BeakerXDataGrid.ts index c568cede4c..e7d094201f 100644 --- a/js/notebook/src/tableDisplay/dataGrid/BeakerXDataGrid.ts +++ b/js/notebook/src/tableDisplay/dataGrid/BeakerXDataGrid.ts @@ -156,7 +156,7 @@ export class BeakerXDataGrid extends DataGrid { } resize(args?: any): void { - this.dataGridResize.resize(); + this.dataGridResize && this.dataGridResize.resize(); } setFocus(focus: boolean) { @@ -186,10 +186,34 @@ export class BeakerXDataGrid extends DataGrid { } destroy() { + this.model && this.model.destroy(); this.eventManager.destroy(); this.columnManager.destroy(); + this.columnPosition.destroy(); + this.cellFocusManager.destroy(); + this.cellManager.destroy(); + this.cellSelectionManager.destroy(); + this.cellTooltipManager.destroy(); + this.highlighterManager.destroy(); + this.dataGridResize.destroy(); + this.rowManager.destroy(); + this.dispose(); Signal.disconnectAll(this); + + setTimeout(() => { + this.cellSelectionManager = null; + this.cellTooltipManager = null; + this.highlighterManager = null; + this.cellFocusManager = null; + this.dataGridResize = null; + this.columnPosition = null; + this.columnManager = null; + this.eventManager = null; + this.cellManager = null; + this.rowManager = null; + this.store = null; + }); } onAfterAttach(msg) { @@ -243,14 +267,13 @@ export class BeakerXDataGrid extends DataGrid { } private addCellRenderers() { - let cellRendererFactory = new CellRendererFactory(this); - let defaultRenderer = cellRendererFactory.getRenderer(); - let headerCellRenderer = cellRendererFactory.getHeaderRenderer(); + let defaultRenderer = CellRendererFactory.getRenderer(this); + let headerCellRenderer = CellRendererFactory.getHeaderRenderer(this); this.cellRenderers.set( 'body', { dataType: ALL_TYPES[ALL_TYPES.html] }, - cellRendererFactory.getRenderer(ALL_TYPES.html) + CellRendererFactory.getRenderer(this, ALL_TYPES.html) ); this.cellRenderers.set('body', {}, defaultRenderer); this.cellRenderers.set('column-header', {}, headerCellRenderer); @@ -259,6 +282,6 @@ export class BeakerXDataGrid extends DataGrid { } private handleStateChanged() { - this.model.reset(); + this.model && this.model.reset(); } } diff --git a/js/notebook/src/tableDisplay/dataGrid/DataFormatter.ts b/js/notebook/src/tableDisplay/dataGrid/DataFormatter.ts index ff81b50f57..4edce612d6 100644 --- a/js/notebook/src/tableDisplay/dataGrid/DataFormatter.ts +++ b/js/notebook/src/tableDisplay/dataGrid/DataFormatter.ts @@ -56,6 +56,10 @@ export class DataFormatter { this.html = this.html.bind(this); } + destroy(): void { + this.store = null; + } + get stringFormatForColumn() { return selectStringFormatForColumn(this.store.state); } diff --git a/js/notebook/src/tableDisplay/dataGrid/DataGridResize.ts b/js/notebook/src/tableDisplay/dataGrid/DataGridResize.ts index 4daae57e63..1cf1750e8f 100644 --- a/js/notebook/src/tableDisplay/dataGrid/DataGridResize.ts +++ b/js/notebook/src/tableDisplay/dataGrid/DataGridResize.ts @@ -52,6 +52,10 @@ export class DataGridResize { this.installMessageHook(); } + destroy(): void { + this.dataGrid = null; + } + setInitialSize(): void { this.setBaseRowSize(); this.resizeHeader(); @@ -354,13 +358,13 @@ export class DataGridResize { } private viewportResizeMessageHook(handler, msg) { - if (handler !== this.dataGrid.viewport) { + if (!this.dataGrid || handler !== this.dataGrid.viewport) { return true; } if (msg.type === 'resize') { setTimeout(() => { - this.dataGrid['_syncViewport'](); + this.dataGrid && this.dataGrid['_syncViewport'](); }); } diff --git a/js/notebook/src/tableDisplay/dataGrid/DataGridScope.ts b/js/notebook/src/tableDisplay/dataGrid/DataGridScope.ts index cafe5c9070..8571e717e7 100644 --- a/js/notebook/src/tableDisplay/dataGrid/DataGridScope.ts +++ b/js/notebook/src/tableDisplay/dataGrid/DataGridScope.ts @@ -27,9 +27,9 @@ import BeakerXThemeHelper from "../../BeakerXThemeHelper"; export class DataGridScope { contextMenu: DataGridContextMenu; - readonly dataGrid: BeakerXDataGrid; - readonly element: HTMLElement; - readonly store: BeakerXDataStore; + private element: HTMLElement; + private store: BeakerXDataStore; + private dataGrid: BeakerXDataGrid; private tableDisplayModel: any; private tableDisplayView: any; @@ -61,9 +61,14 @@ export class DataGridScope { Widget.attach(this.dataGrid, this.element); } - doDestroy() { + doDestroy(): void { this.dataGrid.destroy(); this.contextMenu.destroy(); + + setTimeout(() => { + this.dataGrid = null; + this.store = null; + }); } updateModelData(newData) { @@ -93,4 +98,8 @@ export class DataGridScope { initColumnLimitModal() { return new ColumnLimitModal(this.dataGrid, this.element); } + + setInitialSize() { + this.dataGrid.setInitialSize(); + } } diff --git a/js/notebook/src/tableDisplay/dataGrid/cell/CellFocusManager.ts b/js/notebook/src/tableDisplay/dataGrid/cell/CellFocusManager.ts index ccbb7fdff7..21253a87bf 100644 --- a/js/notebook/src/tableDisplay/dataGrid/cell/CellFocusManager.ts +++ b/js/notebook/src/tableDisplay/dataGrid/cell/CellFocusManager.ts @@ -32,6 +32,11 @@ export default class CellFocusManager { this.focusedCellData = null; } + destroy(): void { + this.dataGrid = null; + this.focusedCellData = null; + } + setFocusedCell(cellData: ICellData|null) { this.focusedCellData = cellData; } diff --git a/js/notebook/src/tableDisplay/dataGrid/cell/CellManager.ts b/js/notebook/src/tableDisplay/dataGrid/cell/CellManager.ts index 692f8b013d..25ca99a8d5 100644 --- a/js/notebook/src/tableDisplay/dataGrid/cell/CellManager.ts +++ b/js/notebook/src/tableDisplay/dataGrid/cell/CellManager.ts @@ -49,6 +49,11 @@ export default class CellManager { this.dataGrid.cellHovered.connect(this.handleCellHovered, this); } + destroy(): void { + this.dataGrid = null; + this.hoveredCellData = null; + } + repaintRow(cellData) { if( !cellData diff --git a/js/notebook/src/tableDisplay/dataGrid/cell/CellRendererFactory.ts b/js/notebook/src/tableDisplay/dataGrid/cell/CellRendererFactory.ts index e4a9be9103..533d28450d 100644 --- a/js/notebook/src/tableDisplay/dataGrid/cell/CellRendererFactory.ts +++ b/js/notebook/src/tableDisplay/dataGrid/cell/CellRendererFactory.ts @@ -21,22 +21,16 @@ import HeaderCellRenderer from "./HeaderCellRenderer"; import DefaultCellRenderer from "./DefaultCellRenderer"; export class CellRendererFactory { - private dataGrid: BeakerXDataGrid; - - constructor(dataGrid: BeakerXDataGrid) { - this.dataGrid = dataGrid; - } - - getRenderer(dataType?: ALL_TYPES) { + static getRenderer(dataGrid: BeakerXDataGrid, dataType?: ALL_TYPES) { switch (dataType) { case ALL_TYPES.html: - return new HTMLCellRenderer(this.dataGrid); + return new HTMLCellRenderer(dataGrid); default: - return new DefaultCellRenderer(this.dataGrid); + return new DefaultCellRenderer(dataGrid); } } - getHeaderRenderer() { - return new HeaderCellRenderer(this.dataGrid) + static getHeaderRenderer(dataGrid) { + return new HeaderCellRenderer(dataGrid) } } diff --git a/js/notebook/src/tableDisplay/dataGrid/cell/CellSelectionManager.ts b/js/notebook/src/tableDisplay/dataGrid/cell/CellSelectionManager.ts index 528259caad..a2e0e8f116 100644 --- a/js/notebook/src/tableDisplay/dataGrid/cell/CellSelectionManager.ts +++ b/js/notebook/src/tableDisplay/dataGrid/cell/CellSelectionManager.ts @@ -36,6 +36,12 @@ export default class CellSelectionManager { this.dataGrid = dataGrid; } + destroy(): void { + this.dataGrid = null; + this.startCellData = null; + this.endCellData = null; + } + setStartCell(cellData: ICellData) { this.startCellData = cellData; } diff --git a/js/notebook/src/tableDisplay/dataGrid/cell/CellTooltip.ts b/js/notebook/src/tableDisplay/dataGrid/cell/CellTooltip.ts index 3246679b34..acf8d8abe1 100644 --- a/js/notebook/src/tableDisplay/dataGrid/cell/CellTooltip.ts +++ b/js/notebook/src/tableDisplay/dataGrid/cell/CellTooltip.ts @@ -33,6 +33,11 @@ export default class CellTooltip { } } + destroy(): void { + this.container = null; + this.node = null; + } + show(x: number, y: number): void { if (this.container.contains(this.node)) { return; diff --git a/js/notebook/src/tableDisplay/dataGrid/cell/CellTooltipManager.ts b/js/notebook/src/tableDisplay/dataGrid/cell/CellTooltipManager.ts index 4cda7c7c86..02a615cd43 100644 --- a/js/notebook/src/tableDisplay/dataGrid/cell/CellTooltipManager.ts +++ b/js/notebook/src/tableDisplay/dataGrid/cell/CellTooltipManager.ts @@ -37,6 +37,17 @@ export default class CellTooltipManager { this.dataGrid.cellHovered.connect(this.handleCellHovered, this); } + destroy(): void { + this.activeTooltips.forEach(tooltip => tooltip.destroy()); + + setTimeout(() => { + this.dataGrid = null; + this.activeTooltips = null; + this.tooltips = null; + this.lastData = null; + }); + } + hideTooltips() { let tooltip; diff --git a/js/notebook/src/tableDisplay/dataGrid/column/ColumnFilter.ts b/js/notebook/src/tableDisplay/dataGrid/column/ColumnFilter.ts index 4e3e78c998..40807d230c 100644 --- a/js/notebook/src/tableDisplay/dataGrid/column/ColumnFilter.ts +++ b/js/notebook/src/tableDisplay/dataGrid/column/ColumnFilter.ts @@ -91,6 +91,20 @@ export default class ColumnFilter { this.filterInput.blur(); } + destroy(): void { + this.filterWidget.dispose(); + + setTimeout(() => { + this.dataGrid = null; + this.column = null; + this.filterWidget = null; + this.filterNode = null; + this.filterIcon = null; + this.clearIcon = null; + this.filterInput = null; + }); + } + private updateInputPosition() { const position = this.column.getPosition(); const offset = this.dataGrid.getColumnOffset( diff --git a/js/notebook/src/tableDisplay/dataGrid/column/ColumnManager.ts b/js/notebook/src/tableDisplay/dataGrid/column/ColumnManager.ts index ef2c66f196..8f067bb682 100644 --- a/js/notebook/src/tableDisplay/dataGrid/column/ColumnManager.ts +++ b/js/notebook/src/tableDisplay/dataGrid/column/ColumnManager.ts @@ -85,12 +85,7 @@ export default class ColumnManager { } addColumns() { - let bodyColumns: DataGridColumn[] = []; - let indexColumns: DataGridColumn[] = []; - - this.columns[COLUMN_TYPES.index] = indexColumns; - this.columns[COLUMN_TYPES.body] = bodyColumns; - + this.createColumnsMap(); this.addIndexColumns(); this.addBodyColumns(); } @@ -120,7 +115,7 @@ export default class ColumnManager { ); } - destroy() { + destroy(): void { this.destroyAllColumns(); Signal.disconnectAll(this); } @@ -298,10 +293,20 @@ export default class ColumnManager { this.columns[type].push(column); } + private createColumnsMap() { + let bodyColumns: DataGridColumn[] = []; + let indexColumns: DataGridColumn[] = []; + + this.columns[COLUMN_TYPES.index] = indexColumns; + this.columns[COLUMN_TYPES.body] = bodyColumns; + } + private destroyAllColumns() { this.indexColumns.forEach((column: DataGridColumn) => column.destroy()); this.bodyColumns.forEach((column: DataGridColumn) => column.destroy()); + this.createColumnsMap(); + Signal.disconnectAll(this); } } diff --git a/js/notebook/src/tableDisplay/dataGrid/column/ColumnPosition.ts b/js/notebook/src/tableDisplay/dataGrid/column/ColumnPosition.ts index c51e29482e..93f49204d4 100644 --- a/js/notebook/src/tableDisplay/dataGrid/column/ColumnPosition.ts +++ b/js/notebook/src/tableDisplay/dataGrid/column/ColumnPosition.ts @@ -52,6 +52,18 @@ export default class ColumnPosition { this.moveDraggedHeader = this.moveDraggedHeader.bind(this); } + destroy() { + this.draggableHeaderCanvas.remove(); + + setTimeout(() => { + this.dataGrid = null; + this.store = null; + this.grabbedCellData = null; + this.dropCellData = null; + this.draggableHeaderCanvas = null; + }); + } + startDragging(data: ICellData) { this.debounceDragStart(data); } diff --git a/js/notebook/src/tableDisplay/dataGrid/column/DataGridColumn.ts b/js/notebook/src/tableDisplay/dataGrid/column/DataGridColumn.ts index 10a29d35b4..9c3c94217e 100644 --- a/js/notebook/src/tableDisplay/dataGrid/column/DataGridColumn.ts +++ b/js/notebook/src/tableDisplay/dataGrid/column/DataGridColumn.ts @@ -363,6 +363,16 @@ export default class DataGridColumn { destroy() { this.menu.destroy(); + this.columnFilter.destroy(); + + setTimeout(() => { + this.menu = null; + this.dataGrid = null; + this.store = null; + this.columnManager = null; + this.columnFilter = null; + this.formatFn = null; + }); } toggleDataBarsRenderer(enable?: boolean) { diff --git a/js/notebook/src/tableDisplay/dataGrid/column/reducer.ts b/js/notebook/src/tableDisplay/dataGrid/column/reducer.ts index c65ef58778..d6ae093ebf 100644 --- a/js/notebook/src/tableDisplay/dataGrid/column/reducer.ts +++ b/js/notebook/src/tableDisplay/dataGrid/column/reducer.ts @@ -25,7 +25,6 @@ import {COLUMN_TYPES} from "./enums"; export const UPDATE_COLUMNS_STATES = 'UPDATE_COLUMNS_STATES'; export const UPDATE_COLUMN_STATE = 'UPDATE_COLUMNS_STATE'; -export const UPDATE_COLUMNS_VISIBILITY = 'UPDATE_COLUMNS_VISIBILITY'; export const UPDATE_COLUMN_POSITIONS = 'UPDATE_COLUMN_POSITIONS'; export const UPDATE_COLUMNS_TYPES = 'UPDATE_COLUMNS_TYPES'; export const UPDATE_COLUMNS_NAMES = 'UPDATE_COLUMNS_NAMES'; diff --git a/js/notebook/src/tableDisplay/dataGrid/contextMenu/DataGridContextMenu.ts b/js/notebook/src/tableDisplay/dataGrid/contextMenu/DataGridContextMenu.ts index 33cdcb5abe..9c501f596e 100644 --- a/js/notebook/src/tableDisplay/dataGrid/contextMenu/DataGridContextMenu.ts +++ b/js/notebook/src/tableDisplay/dataGrid/contextMenu/DataGridContextMenu.ts @@ -17,7 +17,6 @@ import createHeaderContextMenuItems from './createHeaderContextMenuItems'; import createCellContextMenuItems from './createCellContextMenuItems'; import BkoContextMenu from '../../../contextMenu/BkoContextMenu'; -import {BeakerXDataGrid} from "../BeakerXDataGrid"; import {DataGridScope} from "../DataGridScope"; export default class DataGridContextMenu extends BkoContextMenu { diff --git a/js/notebook/src/tableDisplay/dataGrid/event/EventManager.ts b/js/notebook/src/tableDisplay/dataGrid/event/EventManager.ts index 81037c3122..31768970a7 100644 --- a/js/notebook/src/tableDisplay/dataGrid/event/EventManager.ts +++ b/js/notebook/src/tableDisplay/dataGrid/event/EventManager.ts @@ -28,6 +28,9 @@ import {KEYBOARD_KEYS} from "./enums"; import ColumnManager from "../column/ColumnManager"; import {ICellData} from "../interface/ICell"; import retrieveUrl = DataGridHelpers.retrieveUrl; +import {EventHelpers} from "./helpers"; +import isOutsideNode = EventHelpers.isOutsideNode; +import isInsideGridNode = EventHelpers.isInsideGridNode; const COLUMN_RESIZE_AREA_WIDTH = 4; @@ -90,11 +93,9 @@ export default class EventManager { return x < (this.dataGrid.bodyWidth + this.dataGrid.rowHeaderSections.totalSize) && y < this.dataGrid.headerHeight; } - destroy() { - document.removeEventListener('mouseup', this.handleScrollBarMouseUp, true); - document.removeEventListener('keydown', this.handleKeyDown); - document.removeEventListener('mousemove', this.handleMouseMoveOutsideArea); - window.removeEventListener('resize', this.handleWindowResize); + destroy(): void { + this.removeEventListeners(); + this.clearReferences(); } handleMouseMoveOutsideArea(event: MouseEvent) { @@ -184,22 +185,11 @@ export default class EventManager { } private isOutsideViewport(event: MouseEvent) { - return this.isOutsideNode(event, this.dataGrid.viewport.node); + return isOutsideNode(event, this.dataGrid.viewport.node); } private isOutsideGrid(event) { - return this.isOutsideNode(event, this.dataGrid.node); - } - - private isOutsideNode(event: MouseEvent, node: HTMLElement) { - const rect = node.getBoundingClientRect(); - - return ( - event.clientY - rect.top <= 1 - || rect.bottom - event.clientY <= 1 - || event.clientX - rect.left <= 1 - || rect.right - event.clientX <= 1 - ) + return isOutsideNode(event, this.dataGrid.node); } private handleCellHover(event) { @@ -255,14 +245,7 @@ export default class EventManager { } private isNodeInsideGrid(event: MouseEvent) { - const relatedTarget = (event.relatedTarget || event.target) as HTMLElement; - - return relatedTarget && ( - this.dataGrid.node.contains(relatedTarget) - || relatedTarget === this.dataGrid.node - || relatedTarget.classList.contains('bko-menu') - || relatedTarget.closest('.bko-table-menu') - ); + return isInsideGridNode(event, this.dataGrid.node); } private handleMouseWheel(event: MouseEvent, parentHandler: Function): void { @@ -418,4 +401,29 @@ export default class EventManager { }); } } + + private removeEventListeners() { + this.dataGrid.node.removeEventListener('selectstart', this.handleSelectStart); + this.dataGrid.node.removeEventListener('mouseout', this.handleMouseOut); + this.dataGrid.node.removeEventListener('dblclick', this.handleDoubleClick, true); + this.dataGrid.node.removeEventListener('mouseup', this.handleMouseUp); + this.dataGrid.node.removeEventListener('mousemove', this.handleMouseMove); + + this.dataGrid['_vScrollBar'].node.removeEventListener('mousedown', this.handleMouseDown); + this.dataGrid['_hScrollBar'].node.removeEventListener('mousedown', this.handleMouseDown); + this.dataGrid['_scrollCorner'].node.removeEventListener('mousedown', this.handleMouseDown); + + document.removeEventListener('mousemove', this.handleMouseMoveOutsideArea); + document.removeEventListener('keydown', this.handleKeyDown, true); + + window.removeEventListener('resize', this.handleWindowResize); + } + + private clearReferences() { + setTimeout(() => { + this.dataGrid = null; + this.store = null; + this.cellHoverControll = null + }); + } } diff --git a/js/notebook/src/tableDisplay/dataGrid/event/helpers.ts b/js/notebook/src/tableDisplay/dataGrid/event/helpers.ts new file mode 100644 index 0000000000..c8594d52a7 --- /dev/null +++ b/js/notebook/src/tableDisplay/dataGrid/event/helpers.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2018 TWO SIGMA OPEN SOURCE, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export namespace EventHelpers { + export function isOutsideNode(event: MouseEvent, node: HTMLElement) { + const rect = node.getBoundingClientRect(); + + return ( + event.clientY - rect.top <= 1 + || rect.bottom - event.clientY <= 1 + || event.clientX - rect.left <= 1 + || rect.right - event.clientX <= 1 + ) + } + + export function isInsideGridNode(event: MouseEvent, gridNode: HTMLElement) { + const relatedTarget = (event.relatedTarget || event.target) as HTMLElement; + + return relatedTarget && ( + gridNode.contains(relatedTarget) + || relatedTarget === gridNode + || relatedTarget.classList.contains('bko-menu') + || relatedTarget.closest('.bko-table-menu') + ); + } +} diff --git a/js/notebook/src/tableDisplay/dataGrid/headerMenu/BkoMenu.ts b/js/notebook/src/tableDisplay/dataGrid/headerMenu/BkoMenu.ts index d93da2dabb..9ca5ac89f8 100644 --- a/js/notebook/src/tableDisplay/dataGrid/headerMenu/BkoMenu.ts +++ b/js/notebook/src/tableDisplay/dataGrid/headerMenu/BkoMenu.ts @@ -21,6 +21,11 @@ export default class BkoMenu extends Menu { keepOpen: boolean|undefined; trigger: HTMLElement; + dispose() { + delete this.trigger; + super.dispose(); + } + triggerActiveItem(): void { if (!this.keepOpen) { super.triggerActiveItem(); @@ -37,7 +42,7 @@ export default class BkoMenu extends Menu { } const command = item.command, args = item.args; - if (this.commands.isEnabled(command, args)) { + if (this.commands && this.commands.isEnabled(command, args)) { this.commands.execute(command, args); } } diff --git a/js/notebook/src/tableDisplay/dataGrid/headerMenu/HeaderMenu.ts b/js/notebook/src/tableDisplay/dataGrid/headerMenu/HeaderMenu.ts index 030bf3579a..e6abe51448 100644 --- a/js/notebook/src/tableDisplay/dataGrid/headerMenu/HeaderMenu.ts +++ b/js/notebook/src/tableDisplay/dataGrid/headerMenu/HeaderMenu.ts @@ -26,11 +26,6 @@ import {DataGridHelpers} from "../dataGridHelpers"; import getEventKeyCode = DataGridHelpers.getEventKeyCode; import {KEYBOARD_KEYS} from "../event/enums"; -export interface ITriggerOptions { - x: number, - y: number -} - export default abstract class HeaderMenu implements MenuInterface { columnIndex: number; @@ -57,6 +52,7 @@ export default abstract class HeaderMenu implements MenuInterface { this.column = column; this.handleKeydownEvent = this.handleKeydownEvent.bind(this); + this.handleMenuTriggerClick = this.handleMenuTriggerClick.bind(this); this.addTrigger(); this.buildMenu(); @@ -121,6 +117,17 @@ export default abstract class HeaderMenu implements MenuInterface { destroy(): void { this.menu.isAttached && this.menu.dispose(); + + this.triggerNode.removeEventListener('mousedown', this.handleMenuTriggerClick); + this.triggerNode.remove(); + this.triggerNode = null; + + setTimeout(() => { + this.commands = null; + this.viewport = null; + this.dataGrid = null; + this.column = null; + }); } toggleMenu(submenuIndex?: number): void { @@ -224,6 +231,7 @@ export default abstract class HeaderMenu implements MenuInterface { } protected addTrigger():void { + this.triggerNode && this.triggerNode.remove(); this.triggerNode = document.createElement('span'); this.triggerNode.style.height = `${HeaderMenu.DEFAULT_TRIGGER_HEIGHT}px`; @@ -233,11 +241,13 @@ export default abstract class HeaderMenu implements MenuInterface { this.triggerNode.style.cursor = 'pointer'; this.triggerNode.classList.add('bko-column-header-menu'); this.triggerNode.classList.add('bko-menu'); - this.triggerNode.addEventListener('mousedown', (event) => { - event.preventDefault(); + this.triggerNode.addEventListener('mousedown', this.handleMenuTriggerClick); + } - this.toggleMenu(); - }); + private handleMenuTriggerClick(event) { + event.preventDefault(); + + this.toggleMenu(); } protected getMenuPosition(trigger: any) { diff --git a/js/notebook/src/tableDisplay/dataGrid/headerMenu/createColumnMenuItems.ts b/js/notebook/src/tableDisplay/dataGrid/headerMenu/createColumnMenuItems.ts index 09a71d3314..fd61142469 100644 --- a/js/notebook/src/tableDisplay/dataGrid/headerMenu/createColumnMenuItems.ts +++ b/js/notebook/src/tableDisplay/dataGrid/headerMenu/createColumnMenuItems.ts @@ -24,6 +24,10 @@ import {SORT_ORDER} from "../column/enums"; import {selectColumnsFrozenCount, selectVisibleColumnsFrozenCount} from "../model/selectors"; export function createColumnMenuItems(column: DataGridColumn): MenuItem[] { + if (!column || !column.dataGrid) { + return []; + } + return [ { title: 'Hide column', @@ -49,57 +53,57 @@ export function createColumnMenuItems(column: DataGridColumn): MenuItem[] { { title: 'Sort Ascending', separator: true, - isChecked: (column) => column.getSortOrder() === SORT_ORDER.ASC, + isChecked: (column) => column && column.getSortOrder() === SORT_ORDER.ASC, action: (column) => column.sort(SORT_ORDER.ASC) }, { title: 'Sort Descending', - isChecked: (column) => column.getSortOrder() === SORT_ORDER.DESC, + isChecked: (column) => column && column.getSortOrder() === SORT_ORDER.DESC, action: (column) => column.sort(SORT_ORDER.DESC) }, { title: 'No Sort', - isChecked: (column) => column.getSortOrder() === SORT_ORDER.NO_SORT, + isChecked: (column) => column && column.getSortOrder() === SORT_ORDER.NO_SORT, action: (column) => column.sort(SORT_ORDER.NO_SORT) }, { title: 'Align Left', separator: true, - isChecked: (column) => column.getAlignment() === LEFT, + isChecked: (column) => column && column.getAlignment() === LEFT, action: (column) => { column.setAlignment(LEFT) } }, { title: 'Align Center', - isChecked: (column) => column.getAlignment() === CENTER, + isChecked: (column) => column && column.getAlignment() === CENTER, action: (column) => { column.setAlignment(CENTER) } }, { title: 'Align Right', - isChecked: (column) => column.getAlignment() === RIGHT, + isChecked: (column) => column && column.getAlignment() === RIGHT, action: (column) => { column.setAlignment(RIGHT) } }, { title: 'Heatmap', shortcut: 'H', separator: true, - isChecked: (column) => column.getHighlighter(HIGHLIGHTER_TYPE.heatmap).length, + isChecked: (column) => column && column.getHighlighter(HIGHLIGHTER_TYPE.heatmap).length, action: (column) => column.toggleHighlighter(HIGHLIGHTER_TYPE.heatmap) }, { title: 'Data Bars', shortcut: 'B', - isChecked: (column) => !!column.getRenderer(), + isChecked: (column) => column && !!column.getRenderer(), action: (column) => column.toggleDataBarsRenderer() }, { title: 'Color by unique', shortcut: 'U', - isChecked: (column) => column.getHighlighter(HIGHLIGHTER_TYPE.uniqueEntries).length, + isChecked: (column) => column && column.getHighlighter(HIGHLIGHTER_TYPE.uniqueEntries).length, action: (column) => column.toggleHighlighter(HIGHLIGHTER_TYPE.uniqueEntries) }, { title: 'Fix Left', - isChecked: (column) => column.isFrozen(), + isChecked: (column) => column && column.isFrozen(), action: (column) => column.toggleColumnFrozen() }, { diff --git a/js/notebook/src/tableDisplay/dataGrid/headerMenu/createFormatMenuItems.ts b/js/notebook/src/tableDisplay/dataGrid/headerMenu/createFormatMenuItems.ts index 728d4793c0..93d0689960 100644 --- a/js/notebook/src/tableDisplay/dataGrid/headerMenu/createFormatMenuItems.ts +++ b/js/notebook/src/tableDisplay/dataGrid/headerMenu/createFormatMenuItems.ts @@ -23,6 +23,10 @@ export function createFormatMenuItems(column: DataGridColumn) { const types = getAllowedTypesByType(column.getDataType()); let items: MenuItem[] = []; + if (!column.dataGrid) { + return []; + } + types.forEach((obj) => { if (obj.type === 8) { //datetime items = items.concat(createTimeSubitems()); @@ -33,7 +37,7 @@ export function createFormatMenuItems(column: DataGridColumn) { let item: MenuItem = { title: obj.name, id: `format_${obj.name}`, - isChecked: (column) => column.getDisplayType() === obj.type + isChecked: (column) => column && column.getDisplayType() === obj.type }; if (obj.type === 4) { //double with precision @@ -72,7 +76,7 @@ export function createTimeSubitems(): MenuItem[] { title: TIME_UNIT_FORMATS[key].title, id: `timeunit_${TIME_UNIT_FORMATS[key].title}`, isChecked: (column) => { - const displayType = column.getDisplayType(); + const displayType = column && column.getDisplayType(); return ( displayType === ALL_TYPES.datetime || diff --git a/js/notebook/src/tableDisplay/dataGrid/headerMenu/createIndexMenuItems.ts b/js/notebook/src/tableDisplay/dataGrid/headerMenu/createIndexMenuItems.ts index 70b40b3de2..79fc66d9ca 100644 --- a/js/notebook/src/tableDisplay/dataGrid/headerMenu/createIndexMenuItems.ts +++ b/js/notebook/src/tableDisplay/dataGrid/headerMenu/createIndexMenuItems.ts @@ -24,6 +24,11 @@ import {DEFAULT_PAGE_LENGTH, scopeData} from "../../consts"; export function createIndexMenuItems(column: DataGridColumn): MenuItem[] { const dataGrid = column.dataGrid; + + if (!dataGrid) { + return []; + } + const createShowColumnSubmenu = (): MenuItem[] => { const items: MenuItem[] = []; const columnsStates = selectBodyColumnStates(dataGrid.store.state); @@ -33,7 +38,7 @@ export function createIndexMenuItems(column: DataGridColumn): MenuItem[] { title: state.name, id: `column_${state.name}`, isChecked: () => { - let column = dataGrid.columnManager.getColumnByName(state.name); + let column = dataGrid.columnManager && dataGrid.columnManager.getColumnByName(state.name); return column && column.getVisible(); }, diff --git a/js/notebook/src/tableDisplay/dataGrid/highlighter/Highlighter.ts b/js/notebook/src/tableDisplay/dataGrid/highlighter/Highlighter.ts index c6e522c3d3..4d8bbc3fe1 100644 --- a/js/notebook/src/tableDisplay/dataGrid/highlighter/Highlighter.ts +++ b/js/notebook/src/tableDisplay/dataGrid/highlighter/Highlighter.ts @@ -50,4 +50,10 @@ export default class Highlighter { return valueResolver(value); } + + destroy(): void { + this.column = null; + this.model = null; + this.state = null; + } } diff --git a/js/notebook/src/tableDisplay/dataGrid/highlighter/HighlighterManager.ts b/js/notebook/src/tableDisplay/dataGrid/highlighter/HighlighterManager.ts index 4d365ff680..7d6fe1344d 100644 --- a/js/notebook/src/tableDisplay/dataGrid/highlighter/HighlighterManager.ts +++ b/js/notebook/src/tableDisplay/dataGrid/highlighter/HighlighterManager.ts @@ -46,6 +46,13 @@ export default class HighlighterManager { this.createHighlighters(); } + destroy(): void { + this.dataGrid = null; + this.highlighters = []; + this.cachedHighlighters.forEach(highlighter => highlighter.destroy()); + this.cachedHighlighters.clear(); + } + createHighlighters() { const state = selectCellHighlighters(this.dataGrid.store.state); diff --git a/js/notebook/src/tableDisplay/dataGrid/model/BeakerXDataGridModel.ts b/js/notebook/src/tableDisplay/dataGrid/model/BeakerXDataGridModel.ts index 1615cf8dc0..9107007d69 100644 --- a/js/notebook/src/tableDisplay/dataGrid/model/BeakerXDataGridModel.ts +++ b/js/notebook/src/tableDisplay/dataGrid/model/BeakerXDataGridModel.ts @@ -56,6 +56,17 @@ export class BeakerXDataGridModel extends DataModel { this.addProperties(store, columnManager, rowManager); } + destroy(): void { + this.dataFormatter.destroy(); + + setTimeout(() => { + this.store = null; + this.dataFormatter = null; + this.columnManager = null; + this.rowManager = null; + }); + } + reset() { this.emitChanged({ type: 'model-reset' }); } diff --git a/js/notebook/src/tableDisplay/dataGrid/row/RowManager.ts b/js/notebook/src/tableDisplay/dataGrid/row/RowManager.ts index 86b1cd80ff..ad8b01e297 100644 --- a/js/notebook/src/tableDisplay/dataGrid/row/RowManager.ts +++ b/js/notebook/src/tableDisplay/dataGrid/row/RowManager.ts @@ -40,6 +40,13 @@ export default class RowManager { this.evaluateFilterExpression = this.evaluateFilterExpression.bind(this); } + destroy(): void { + this.rows = []; + this.sortedBy = null; + this.columnManager = null; + this.rowsIterator = null; + } + createRows(data, hasIndex) { hasIndex ? this.createRowsWithIndex(data) : this.createRowsWithGeneratedIndex(data); } @@ -86,7 +93,7 @@ export default class RowManager { this.rows = this.rows.sort((row1, row2) => { let value1 = columnValueResolver(resolverFn(row1, columnIndex)); let value2 = columnValueResolver(resolverFn(row2, columnIndex)); - let result = this.compareSortedValues(value1, value2) + let result = this.compareSortedValues(value1, value2); return shouldReverse ? -result : result; }); diff --git a/js/notebook/test/src/tableDisplay/dataGrid/BeakerxDataGrid.spec.ts b/js/notebook/test/src/tableDisplay/dataGrid/BeakerxDataGrid.spec.ts index df85612e4c..d37f8c4e54 100644 --- a/js/notebook/test/src/tableDisplay/dataGrid/BeakerxDataGrid.spec.ts +++ b/js/notebook/test/src/tableDisplay/dataGrid/BeakerxDataGrid.spec.ts @@ -32,10 +32,6 @@ describe('BeakerXDataGrid', () => { dataGrid = new BeakerXDataGrid({}, dataStore); }); - after(() => { - dataGrid.destroy(); - }); - it('should create the columnManager', () => { expect(dataGrid.columnManager).to.be.an.instanceof(ColumnManager); }); diff --git a/js/notebook/test/src/tableDisplay/dataGrid/cell/CellRendererFactory.spec.ts b/js/notebook/test/src/tableDisplay/dataGrid/cell/CellRendererFactory.spec.ts index 2ccd08f42d..4bd6a3ff92 100644 --- a/js/notebook/test/src/tableDisplay/dataGrid/cell/CellRendererFactory.spec.ts +++ b/js/notebook/test/src/tableDisplay/dataGrid/cell/CellRendererFactory.spec.ts @@ -23,13 +23,11 @@ import createStore from "@beakerx/tableDisplay/dataGrid/store/BeakerXDataStore"; describe('CellRendererFactory', () => { let dataGrid; - let cellRendererFactory; let dataStore; before(() => { dataStore = createStore(modelStateMock); dataGrid = new BeakerXDataGrid({}, dataStore); - cellRendererFactory = new CellRendererFactory(dataGrid); }); after(() => { @@ -37,10 +35,10 @@ describe('CellRendererFactory', () => { }); it('should implement getRenderer method', () => { - expect(cellRendererFactory).to.have.property('getRenderer'); + expect(CellRendererFactory).to.have.property('getRenderer'); }); it('should return CellRenderer', () => { - expect(cellRendererFactory.getRenderer()).to.be.an.instanceof(TextRenderer); + expect(CellRendererFactory.getRenderer(dataGrid)).to.be.an.instanceof(TextRenderer); }); }); diff --git a/js/notebook/test/src/tableDisplay/dataGrid/headerMenu/IndexMenu.spec.ts b/js/notebook/test/src/tableDisplay/dataGrid/headerMenu/IndexMenu.spec.ts index 1a0e5cac4a..9a49265444 100644 --- a/js/notebook/test/src/tableDisplay/dataGrid/headerMenu/IndexMenu.spec.ts +++ b/js/notebook/test/src/tableDisplay/dataGrid/headerMenu/IndexMenu.spec.ts @@ -21,8 +21,6 @@ import IndexMenu from '@beakerx/tableDisplay/dataGrid/headerMenu/IndexMenu'; import { createIndexMenuItems } from '@beakerx/tableDisplay/dataGrid/headerMenu/createIndexMenuItems'; import HeaderMenu from '@beakerx/tableDisplay/dataGrid/headerMenu/HeaderMenu'; import { BeakerXDataGrid } from "@beakerx/tableDisplay/dataGrid/BeakerXDataGrid"; -import menuOptionsMock from "../mock/menuOptionsMock"; -import DataGridColumn from "@beakerx/tableDisplay/dataGrid/column/DataGridColumn"; import modelStateMock from "../mock/modelStateMock"; import createStore from "@beakerx/tableDisplay/dataGrid/store/BeakerXDataStore"; import {COLUMN_TYPES} from "@beakerx/tableDisplay/dataGrid/column/enums"; @@ -36,13 +34,8 @@ describe('IndexMenu', () => { before(() => { dataStore = createStore(modelStateMock); dataGrid = new BeakerXDataGrid({}, dataStore); - column = new DataGridColumn({ - index: 0, - type: COLUMN_TYPES.index, - name: 'index', - menuOptions: menuOptionsMock - }, dataGrid, dataGrid.columnManager); - indexMenu = new IndexMenu(column, menuOptionsMock); + column = dataGrid.columnManager.columns[COLUMN_TYPES.index][0]; + indexMenu = column.menu; }); after(() => { diff --git a/js/notebook/test/src/tableDisplay/dataGrid/headerMenu/createColumnMenuItems.spec.ts b/js/notebook/test/src/tableDisplay/dataGrid/headerMenu/createColumnMenuItems.spec.ts index 6271cee6a8..3be6e4a0bf 100644 --- a/js/notebook/test/src/tableDisplay/dataGrid/headerMenu/createColumnMenuItems.spec.ts +++ b/js/notebook/test/src/tableDisplay/dataGrid/headerMenu/createColumnMenuItems.spec.ts @@ -17,8 +17,6 @@ import { expect } from 'chai'; import { createColumnMenuItems } from '@beakerx/tableDisplay/dataGrid/headerMenu/createColumnMenuItems'; import { BeakerXDataGrid } from "@beakerx/tableDisplay/dataGrid/BeakerXDataGrid"; -import menuOptionsMock from "../mock/menuOptionsMock"; -import DataGridColumn from "@beakerx/tableDisplay/dataGrid/column/DataGridColumn"; import modelStateMock from "../mock/modelStateMock"; import createStore from "@beakerx/tableDisplay/dataGrid/store/BeakerXDataStore"; import {COLUMN_TYPES} from "@beakerx/tableDisplay/dataGrid/column/enums"; @@ -31,12 +29,7 @@ describe('createColumnMenuItems', () => { before(() => { dataStore = createStore(modelStateMock); dataGrid = new BeakerXDataGrid({}, dataStore); - column = new DataGridColumn({ - index: 0, - type: COLUMN_TYPES.index, - name: 'index', - menuOptions: menuOptionsMock - }, dataGrid, dataGrid.columnManager); + column = dataGrid.columnManager.columns[COLUMN_TYPES.index][0]; }); after(() => { diff --git a/js/notebook/test/src/tableDisplay/dataGrid/headerMenu/createFormatMenuItems.spec.ts b/js/notebook/test/src/tableDisplay/dataGrid/headerMenu/createFormatMenuItems.spec.ts index 781c22d84b..fcffee0cda 100644 --- a/js/notebook/test/src/tableDisplay/dataGrid/headerMenu/createFormatMenuItems.spec.ts +++ b/js/notebook/test/src/tableDisplay/dataGrid/headerMenu/createFormatMenuItems.spec.ts @@ -22,8 +22,6 @@ import { } from '@beakerx/tableDisplay/dataGrid/headerMenu/createFormatMenuItems'; import { BeakerXDataGrid } from "@beakerx/tableDisplay/dataGrid/BeakerXDataGrid"; import { scopeData, TIME_UNIT_FORMATS } from '@beakerx/tableDisplay/consts'; -import menuOptionsMock from "../mock/menuOptionsMock"; -import DataGridColumn from "@beakerx/tableDisplay/dataGrid/column/DataGridColumn"; import modelStateMock from "../mock/modelStateMock"; import createStore from "@beakerx/tableDisplay/dataGrid/store/BeakerXDataStore"; import {COLUMN_TYPES} from "@beakerx/tableDisplay/dataGrid/column/enums"; @@ -36,12 +34,7 @@ describe('createFormatMenuItems', () => { before(() => { dataStore = createStore(modelStateMock); dataGrid = new BeakerXDataGrid({}, dataStore); - column = new DataGridColumn({ - index: 0, - type: COLUMN_TYPES.index, - name: 'index', - menuOptions: menuOptionsMock - }, dataGrid, dataGrid.columnManager); + column = dataGrid.columnManager.columns[COLUMN_TYPES.index][0]; }); after(() => {