From fbaf1878230e3908cbc47708872ab3275235105f Mon Sep 17 00:00:00 2001 From: Pierre-Luc Carmel Biron Date: Tue, 27 Nov 2018 09:05:24 -0500 Subject: [PATCH 1/6] Basic behaviors --- addon/behaviors/behavior.js | 18 + addon/behaviors/multi-select.js | 60 ++++ addon/behaviors/row-expansion.js | 39 ++ addon/behaviors/select-all.js | 18 + addon/behaviors/single-select.js | 19 + addon/classes/Table.js | 16 + addon/components/lt-body.js | 190 ++++++---- addon/components/lt-row.js | 65 +++- addon/mixins/has-behaviors.js | 334 ++++++++++++++++++ addon/templates/components/lt-body.hbs | 52 ++- addon/utils/listener-name.js | 26 ++ addon/utils/with-backing-field.js | 17 + app/behaviors/multi-select.js | 1 + app/behaviors/row-expansion.js | 1 + app/behaviors/select-all.js | 1 + app/behaviors/single-select.js | 1 + app/helpers/lt-multi-select.js | 10 + app/helpers/lt-row-expansion.js | 10 + app/helpers/lt-select-all.js | 10 + app/helpers/lt-single-select.js | 10 + config/environment.js | 6 +- package.json | 1 + .../app/components/rows/selectable-table.js | 4 +- .../components/columns/draggable-table.hbs | 2 +- .../components/columns/grouped-table.hbs | 2 +- .../components/columns/resizable-table.hbs | 2 +- .../components/cookbook/client-side-table.hbs | 2 +- .../components/cookbook/custom-row-table.hbs | 2 +- .../cookbook/custom-sort-icon-table.hbs | 2 +- .../cookbook/horizontal-scrolling-table.hbs | 2 +- .../components/cookbook/occluded-table.hbs | 2 +- .../components/cookbook/paginated-table.hbs | 2 +- .../cookbook/table-actions-table.hbs | 2 +- .../templates/components/responsive-table.hbs | 3 +- .../components/rows/expandable-table.hbs | 7 +- .../components/rows/selectable-table.hbs | 3 +- .../templates/components/scrolling-table.hbs | 2 +- .../app/templates/components/simple-table.hbs | 2 +- tests/integration/components/lt-body-test.js | 268 +++++++++++--- yarn.lock | 73 +++- 40 files changed, 1124 insertions(+), 163 deletions(-) create mode 100644 addon/behaviors/behavior.js create mode 100644 addon/behaviors/multi-select.js create mode 100644 addon/behaviors/row-expansion.js create mode 100644 addon/behaviors/select-all.js create mode 100644 addon/behaviors/single-select.js create mode 100644 addon/mixins/has-behaviors.js create mode 100644 addon/utils/listener-name.js create mode 100644 addon/utils/with-backing-field.js create mode 100644 app/behaviors/multi-select.js create mode 100644 app/behaviors/row-expansion.js create mode 100644 app/behaviors/select-all.js create mode 100644 app/behaviors/single-select.js create mode 100644 app/helpers/lt-multi-select.js create mode 100644 app/helpers/lt-row-expansion.js create mode 100644 app/helpers/lt-select-all.js create mode 100644 app/helpers/lt-single-select.js diff --git a/addon/behaviors/behavior.js b/addon/behaviors/behavior.js new file mode 100644 index 00000000..93f7c97c --- /dev/null +++ b/addon/behaviors/behavior.js @@ -0,0 +1,18 @@ +import EmberObject from '@ember/object'; + +export default EmberObject.extend({ + + classNames: [], + + exclusionGroup: null, + + events: null, + + init() { + this._super(...arguments); + this.events = {}; + }, + + onSelectionChanged() {} + +}); diff --git a/addon/behaviors/multi-select.js b/addon/behaviors/multi-select.js new file mode 100644 index 00000000..8c306434 --- /dev/null +++ b/addon/behaviors/multi-select.js @@ -0,0 +1,60 @@ +import SelectAllBehavior from 'ember-light-table/behaviors/select-all'; + +export default SelectAllBehavior.extend({ + + classNames: ['multi-select'], + + // passed in + requiresKeyboard: true, + selectOnClick: true, + + init() { + this._super(...arguments); + this.events.onSelectRow = ['rowClick:_none']; + this.events.onExtendRange = ['rowClick:shift']; + this.events.onToggleRow = ['rowClick:ctrl']; + }, + + _prevSelectedIndex: -1, + + _onRowClick(ltBody, row, f) { + let table = ltBody.get('table'); + let i = table.get('rows').indexOf(row); + f(i, table); + this._prevSelectedIndex = i; + }, + + onSelectRow(ltBody, ltRow) { + let row = ltRow.get('row'); + if (this.get('selectOnClick')) { + let isSelected = row.get('selected'); + if (this.get('requiresKeyboard')) { + this._onRowClick(ltBody, row, (i, table) => { + table.deselectAll(); + row.set('selected', !isSelected); + }); + } else { + this.onToggleRow(ltBody, ltRow); + } + } + }, + + onExtendRange(ltBody, ltRow) { + let row = ltRow.get('row'); + this._onRowClick(ltBody, row, (i, table) => { + let j = this._prevSelectedIndex === -1 ? i : this._prevSelectedIndex; + table + .get('rows') + .slice(Math.min(i, j), Math.max(i, j) + 1) + .forEach((r) => r.set('selected', true)); + }); + }, + + onToggleRow(ltBody, ltRow) { + let row = ltRow.get('row'); + this._onRowClick(ltBody, row, () => { + row.toggleProperty('selected'); + }); + } + +}); diff --git a/addon/behaviors/row-expansion.js b/addon/behaviors/row-expansion.js new file mode 100644 index 00000000..37bdf8ee --- /dev/null +++ b/addon/behaviors/row-expansion.js @@ -0,0 +1,39 @@ +import Behavior from 'ember-light-table/behaviors/behavior'; +import { keyDown } from 'ember-keyboard'; + +export default Behavior.extend({ + + exclusionGroup: 'can-expand', + + // passed in + multiRow: true, + expandOnClick: true, + + init() { + this._super(...arguments); + this.events.onToggleClick = ['rowClick:_none']; + this.events.onToggleFocused = [keyDown('Space')]; + }, + + _onToggle(ltBody, row) { + let shouldExpand = !row.get('expanded'); + if (!this.get('multiRow')) { + ltBody.get('table.expandedRows').setEach('expanded', false); + } + row.set('expanded', shouldExpand); + }, + + onToggleClick(ltBody, ltRow) { + if (this.get('expandOnClick')) { + this._onToggle(ltBody, ltRow.get('row')); + } + }, + + onToggleFocused(ltBody) { + let focusedRow = ltBody.get('table.focusedRow'); + if (focusedRow) { + this._onToggle(ltBody, focusedRow); + } + } + +}); diff --git a/addon/behaviors/select-all.js b/addon/behaviors/select-all.js new file mode 100644 index 00000000..f6cd8d95 --- /dev/null +++ b/addon/behaviors/select-all.js @@ -0,0 +1,18 @@ +import Behavior from 'ember-light-table/behaviors/behavior'; +import { keyDown } from 'ember-keyboard'; + +export default Behavior.extend({ + + exclusionGroup: 'can-select', + + init() { + this._super(...arguments); + this.events.onSelectAll = [keyDown('cmd+KeyA')]; + }, + + onSelectAll(ltBody, e) { + ltBody.get('table').selectAll(); + e.preventDefault(); + } + +}); diff --git a/addon/behaviors/single-select.js b/addon/behaviors/single-select.js new file mode 100644 index 00000000..8c81b5d8 --- /dev/null +++ b/addon/behaviors/single-select.js @@ -0,0 +1,19 @@ +import Behavior from 'ember-light-table/behaviors/behavior'; + +export default Behavior.extend({ + + exclusionGroup: 'can-select', + + init() { + this._super(...arguments); + this.events.onRowClick = ['rowClick:_all']; + }, + + onRowClick(ltBody, ltRow) { + let row = ltRow.get('row'); + let isSelected = row.get('selected'); + ltBody.get('table.selectedRows').setEach('selected', false); + row.set('selected', !isSelected); + } + +}); diff --git a/addon/classes/Table.js b/addon/classes/Table.js index dcdb5ddc..abcbf441 100644 --- a/addon/classes/Table.js +++ b/addon/classes/Table.js @@ -307,6 +307,22 @@ export default class Table extends EmberObject.extend({ this.get('rows').removeAt(index); } + /** + * Select all the rows in the table + * @method selectAll + */ + selectAll() { + this.get('rows').setEach('selected', true); + } + + /** + * Deselect all the rows in the table + * @method selectAll + */ + deselectAll() { + this.get('selectedRows').setEach('selected', false); + } + // Columns /** diff --git a/addon/components/lt-body.js b/addon/components/lt-body.js index 48829b98..36867a0a 100644 --- a/addon/components/lt-body.js +++ b/addon/components/lt-body.js @@ -1,7 +1,15 @@ import Component from '@ember/component'; import { computed, observer } from '@ember/object'; +import { debounce, once, run } from '@ember/runloop'; +import $ from 'jquery'; import layout from 'ember-light-table/templates/components/lt-body'; -import { run } from '@ember/runloop'; +import { EKMixin } from 'ember-keyboard'; +import ActivateKeyboardOnFocusMixin from 'ember-keyboard/mixins/activate-keyboard-on-focus'; +import HasBehaviorsMixin from 'ember-light-table/mixins/has-behaviors'; +import RowExpansionBehavior from 'ember-light-table/behaviors/row-expansion'; +import SingleSelectBehavior from 'ember-light-table/behaviors/single-select'; +import MultiSelectBehavior from 'ember-light-table/behaviors/multi-select'; +import { behaviorGroupFlag, behaviorFlag, behaviorInstanceOf } from 'ember-light-table/mixins/has-behaviors'; import Row from 'ember-light-table/classes/Row'; /** @@ -33,11 +41,14 @@ import Row from 'ember-light-table/classes/Row'; * * @class t.body */ +export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehaviorsMixin, { -export default Component.extend({ layout, classNames: ['lt-body-wrap'], - classNameBindings: ['canSelect', 'multiSelect', 'canExpand'], + + attributeBindings: ['tabindex'], + + tabindex: 0, /** * @property table @@ -59,6 +70,14 @@ export default Component.extend({ */ tableActions: null, + /** + * Turn this off to use the new way of specifying behaviors. + * + * @property useLegacyBehaviors + * @type {Object} + */ + useLegacyBehaviorFlags: true, + /** * @property extra * @type {Object} @@ -81,7 +100,7 @@ export default Component.extend({ * @type {Boolean} * @default true */ - canSelect: true, + canSelect: behaviorGroupFlag('can-select'), /** * Select a row on click. If this is set to `false` and multiSelect is @@ -92,7 +111,7 @@ export default Component.extend({ * @type {Boolean} * @default true */ - selectOnClick: true, + selectOnClick: computed.alias('_multiSelectBehavior.selectOnClick'), /** * Allows for expanding row. This will create a new row under the row that was @@ -108,7 +127,7 @@ export default Component.extend({ * @type {Boolean} * @default false */ - canExpand: false, + canExpand: behaviorGroupFlag('can-expand'), /** * Allows a user to select multiple rows with the `ctrl`, `cmd`, and `shift` keys. @@ -118,7 +137,7 @@ export default Component.extend({ * @type {Boolean} * @default false */ - multiSelect: false, + multiSelect: behaviorFlag('can-select', '_multiSelectBehavior'), /** * When multiSelect is true, this property determines whether or not `ctrl` @@ -131,7 +150,7 @@ export default Component.extend({ * @type {Boolean} * @default true */ - multiSelectRequiresKeyboard: true, + multiSelectRequiresKeyboard: computed.alias('_multiSelectBehavior.requiresKeyboard'), /** * Hide scrollbar when not scrolling @@ -149,7 +168,7 @@ export default Component.extend({ * @type {Boolean} * @default true */ - multiRowExpansion: true, + multiRowExpansion: computed.alias('_rowExpansionBehavior.multiRow'), /** * Expand a row on click @@ -158,7 +177,7 @@ export default Component.extend({ * @type {Boolean} * @default true */ - expandOnClick: true, + expandOnClick: computed.alias('_rowExpansionBehavior.expandOnClick'), /** * If true, the body block will yield columns and rows, allowing you @@ -339,7 +358,9 @@ export default Component.extend({ been initialized since fixedHeader and fixedFooter are set on t.head and t.foot initialization. */ - run.once(this, this._setupVirtualScrollbar); + once(this, this._setupVirtualScrollbar); + + this._initDefaultBehaviorsIfNeeded(); }, didReceiveAttrs() { @@ -352,6 +373,34 @@ export default Component.extend({ this._cancelTimers(); }, + didInsertElement() { + this._super(...arguments); + $(document).on('keydown', this, this._preventPropagation); + }, + + willDestroyElement() { + this._super(...arguments); + $(document).off('keydown', this, this._preventPropagation); + }, + + _preventPropagation(e) { + if (e.target === e.data.element && [32, 33, 34, 35, 36, 38, 40].includes(e.keyCode)) { + return false; + } + }, + + _multiSelectBehavior: behaviorInstanceOf(MultiSelectBehavior), + _rowExpansionBehavior: behaviorInstanceOf(RowExpansionBehavior), + + _initDefaultBehaviorsIfNeeded() { + this._initDefaultBehaviorsIfNeeded = function() {}; + if (this.get('useLegacyBehaviorFlags')) { + this.activateBehavior(MultiSelectBehavior.create({}), true); + this.activateBehavior(SingleSelectBehavior.create({}), true); + this.activateBehavior(RowExpansionBehavior.create({}), false); + } + }, + _setupVirtualScrollbar() { let { fixedHeader, fixedFooter } = this.get('sharedOptions'); this.set('useVirtualScrollbar', fixedHeader || fixedFooter); @@ -432,7 +481,7 @@ export default Component.extend({ This debounce is needed when there is not enough delay between onScrolledToBottom calls. Without this debounce, all rows will be rendered causing immense performance problems */ - this._debounceTimer = run.debounce(this, this.onScrolledToBottom, delay); + this._debounceTimer = debounce(this, this.onScrolledToBottom, delay); }, /** @@ -444,6 +493,9 @@ export default Component.extend({ run.cancel(this._schedulerTimer); run.cancel(this._debounceTimer); }, + signalSelectionChanged() { + this.get('behaviors').forEach((b) => b.onSelectionChanged(this)); + }, // Noop for closure actions onRowClick() {}, @@ -456,60 +508,74 @@ export default Component.extend({ onScrolledToBottom() {}, actions: { - /** - * onRowClick action. Handles selection, and row expansion. - * @event onRowClick - * @param {Row} row The row that was clicked - * @param {Event} event The click event - */ - onRowClick(row, e) { - let rows = this.get('table.rows'); - let multiSelect = this.get('multiSelect'); - let multiSelectRequiresKeyboard = this.get('multiSelectRequiresKeyboard'); - let canSelect = this.get('canSelect'); - let selectOnClick = this.get('selectOnClick'); - let canExpand = this.get('canExpand'); - let expandOnClick = this.get('expandOnClick'); - let isSelected = row.get('selected'); - let currIndex = rows.indexOf(row); - let prevIndex = this._prevSelectedIndex === -1 ? currIndex : this._prevSelectedIndex; - - this._prevSelectedIndex = currIndex; - - let toggleExpandedRow = () => { - if (canExpand && expandOnClick) { - this.toggleExpandedRow(row); - } - }; - - if (canSelect) { - if (e.shiftKey && multiSelect) { - rows.slice(Math.min(currIndex, prevIndex), Math.max(currIndex, prevIndex) + 1).forEach((r) => r.set('selected', !isSelected)); - } else if ((!multiSelectRequiresKeyboard || (e.ctrlKey || e.metaKey)) && multiSelect) { - row.toggleProperty('selected'); - } else { - if (selectOnClick) { - this.get('table.selectedRows').setEach('selected', false); - row.set('selected', !isSelected); - } - - toggleExpandedRow(); - } - } else { - toggleExpandedRow(); + onRowClick() { + this.triggerBehaviorEvent('rowClick', ...arguments); + if (this.onRowClick) { + this.onRowClick(...arguments); } + }, - this.onRowClick(...arguments); + onRowDoubleClick() { + this.triggerBehaviorEvent('rowDoubleClick', ...arguments); + if (this.onRowDoubleClick) { + this.onRowDoubleClick(...arguments); + } }, - /** - * onRowDoubleClick action. - * @event onRowDoubleClick - * @param {Row} row The row that was clicked - * @param {Event} event The click event - */ - onRowDoubleClick(/* row */) { - this.onRowDoubleClick(...arguments); + onRowMouseDown() { + this.triggerBehaviorEvent('rowMouseDown', ...arguments); + if (this.onRowMouseDown) { + this.onRowMouseDown(...arguments); + } + }, + + onRowMouseUp() { + this.triggerBehaviorEvent('rowMouseUp', ...arguments); + if (this.onRowMouseUp) { + this.onRowMouseUp(...arguments); + } + }, + + onRowMouseMove() { + this.triggerBehaviorEvent('rowMouseMove', ...arguments); + if (this.onRowMouseMove) { + this.onRowMouseMove(...arguments); + } + }, + + onRowTouchStart() { + this.triggerBehaviorEvent('rowTouchStart', ...arguments); + if (this.onRowTouchStart) { + this.onRowTouchStart(...arguments); + } + }, + + onRowTouchEnd() { + this.triggerBehaviorEvent('rowTouchEnd', ...arguments); + if (this.onRowTouchEnd) { + this.onRowTouchEnd(...arguments); + } + }, + + onRowTouchCancel() { + this.triggerBehaviorEvent('rowTouchCancel', ...arguments); + if (this.onRowTouchCancel) { + this.onRowTouchCancel(...arguments); + } + }, + + onRowTouchLeave() { + this.triggerBehaviorEvent('rowTouchLeave', ...arguments); + if (this.onRowTouchLeave) { + this.onRowTouchLeave(...arguments); + } + }, + + onRowTouchMove() { + this.triggerBehaviorEvent('rowTouchMove', ...arguments); + if (this.onRowTouchMove) { + this.onRowTouchMove(...arguments); + } }, /** diff --git a/addon/components/lt-row.js b/addon/components/lt-row.js index 2558b99b..0da4342d 100644 --- a/addon/components/lt-row.js +++ b/addon/components/lt-row.js @@ -1,5 +1,7 @@ +/* eslint ember/no-on-calls-in-components:off */ import Component from '@ember/component'; import { computed } from '@ember/object'; +import { on } from '@ember/object/evented'; import layout from 'ember-light-table/templates/components/lt-row'; const Row = Component.extend({ @@ -18,7 +20,68 @@ const Row = Component.extend({ colspan: 1, isSelected: computed.readOnly('row.selected'), - isExpanded: computed.readOnly('row.expanded') + isExpanded: computed.readOnly('row.expanded'), + + _onClick: on('click', function() { + if (this.rowClick) { + this.rowClick(this, ...arguments); + } + }), + + _onDoubleClick: on('doubleClick', function() { + if (this.rowDoubleClick) { + this.rowDoubleClick(this, ...arguments); + } + }), + + _onMouseDown: on('mouseDown', function() { + if (this.rowMouseDown) { + this.rowMouseDown(this, ...arguments); + } + }), + + _onMouseUp: on('mouseUp', function() { + if (this.rowMouseUp) { + this.rowMouseUp(this, ...arguments); + } + }), + + _onMouseMove: on('mouseMove', function() { + if (this.rowMouseMove) { + this.rowMouseMove(this, ...arguments); + } + }), + + _onTouchStart: on('touchStart', function() { + if (this.rowTouchStart) { + this.rowTouchStart(this, ...arguments); + } + }), + + _onTouchEnd: on('touchEnd', function() { + if (this.rowTouchEnd) { + this.rowTouchEnd(this, ...arguments); + } + }), + + _onTouchCancel: on('touchCancel', function() { + if (this.rowTouchCancel) { + this.rowTouchCancel(this, ...arguments); + } + }), + + _onTouchLeave: on('touchLeave', function() { + if (this.rowTouchLeave) { + this.rowTouchLeave(this, ...arguments); + } + }), + + _onTouchMove: on('touchMove', function() { + if (this.rowTouchMove) { + this.rowTouchMove(this, ...arguments); + } + }) + }); Row.reopenClass({ diff --git a/addon/mixins/has-behaviors.js b/addon/mixins/has-behaviors.js new file mode 100644 index 00000000..9daf1085 --- /dev/null +++ b/addon/mixins/has-behaviors.js @@ -0,0 +1,334 @@ +import { A } from '@ember/array'; +import { computed, observer } from '@ember/object'; +import Mixin from '@ember/object/mixin'; +import { on } from '@ember/object/evented'; +import { run } from '@ember/runloop'; +import withBackingField from 'ember-light-table/utils/with-backing-field'; +import listenerName from 'ember-light-table/utils/listener-name'; + +/* + * from ember-keyboard + */ +function gatherKeys(event) { + return A( + ['alt', 'ctrl', 'meta', 'shift'] + .reduce( + (keys, keyName) => { + if (event[`${keyName}Key`]) { + keys.push(keyName); + } + return keys; + }, + [] + ) + ); +} + +/** + * Returns a computed property flag that turns on or off a group of + * mutually exclusive behaviors. + * + * @function + * @param {string} exclusionGroup - If present, it is + * the name of the mutually exclusive group of behaviors for + * which all behaviors should be turned off or for which the + * behavior having the higher priority should be turned on, + * based on the value of the property. + */ +export function behaviorGroupFlag(exclusionGroup) { + return computed('behaviors.[]', { + get() { + this._initDefaultBehaviorsIfNeeded(); + return !!this.getActiveBehaviorOf(exclusionGroup); + }, + set(key, value) { + this._initDefaultBehaviorsIfNeeded(); + if (value) { + this.activateBehavior(this.getFirstBehaviorOf(exclusionGroup)); + } else { + this.inactivateAllBehaviorsOf(exclusionGroup); + } + return value; + } + }); +} + +/** + * Returns a computed property flag that prioritize or not a specific + * behavior in a group of mutually exclusive behaviors. + * + * @function + * @param {string} exclusionGroup - The name of the mutually exclusive + * group of behaviors. + * @param {string} behaviorPropertyName - The name of the property that + * contains the instance of the behavior to prioritize or not. + */ +export function behaviorFlag(exclusionGroup, behaviorPropertyName) { + return computed('behaviors.[]', 'behaviorsOff.[]', { + get() { + this._initDefaultBehaviorsIfNeeded(); + return this.getFirstBehaviorOf(exclusionGroup) === this.get(behaviorPropertyName); + }, + set(key, value) { + this._initDefaultBehaviorsIfNeeded(); + let l = A(this.get('allBehaviors').filterBy('exclusionGroup', exclusionGroup)); + let b = this.get(behaviorPropertyName); + let b0 = l.objectAt(0); + if (b0 !== b && value) { + this.activateBehavior(b); + } else if (b0 === b && !value) { + this.prioritizeBehavior(l.objectAt(1)); + } + return value; + } + }); +} + +/** + * Returns a computed property that is the first behavior of a given type if one is present. + * + * @function + * @param {string} behaviorClass - The class of the behavior to be returned. + */ +export function behaviorInstanceOf(behaviorClass) { + return computed('behaviors.[]', 'behaviorsOff.[]', { + get() { + this._initDefaultBehaviorsIfNeeded(); + return this.get('allBehaviors').find((b) => b instanceof behaviorClass); + } + }); +} + +/** + * Use this mixin to be able to add behaviors to your class. Behaviors are + * small plugins that declare the javascript events they want to listen for. + * They are instances of subclasses of `behaviors/behavior`. + * + * @mixin + */ +export default Mixin.create({ + + /** + * Behaviors that are turned on, in descending order of priority. + */ + behaviors: withBackingField('_behaviors', () => A()), + + /** + * Behaviors that are turned off, in descending order of priority. They are + * backup behaviors that can replace a behavior of the same group + * when the later is turned off. + */ + behaviorsOff: withBackingField('_behaviorsOff', () => A()), + + allBehaviors: computed('behaviors.[]', 'behaviorsOff.[]', function() { + let result = A(); + result.pushObjects(this.get('behaviors')); + result.pushObjects(this.get('behaviorsOff')); + return result; + }), + + /** + * Moves a behavior in front of other behaviors. + * + * @function + * @param {Behavior} behavior - Instance of the behavior to prioritize. + */ + prioritizeBehavior(behavior) { + if (behavior) { + let behaviors = this.get('behaviors'); + let behaviorsOff = this.get('behaviorsOff'); + if (behaviors.includes(behavior)) { + behaviors.removeObject(behavior); + behaviors.insertAt(0, behavior); + } else if (behaviorsOff.includes(behavior)) { + behaviorsOff.removeObject(behavior); + behaviorsOff.insertAt(0, behavior); + } + } + }, + + /** + * Turns off a behavior. + * + * @function + * @param {Behavior} behavior - Instance of the behavior to inactivate. + */ + inactivateBehavior(behavior) { + if (behavior) { + let behaviors = this.get('behaviors'); + let behaviorsOff = this.get('behaviorsOff'); + behaviors.removeObject(behavior); + behaviorsOff.removeObject(behavior); + behaviorsOff.insertAt(0, behavior); + } + }, + + /** + * Turns on or off a behavior. + * + * @function + * @param {Behavior} behavior - Behavior instance to activate/inactivate. + * @param {boolean} [value=true] - Whether the behavior is activated (true) or inactivated (false). + */ + activateBehavior(behavior, value) { + if (value === undefined || value) { + this.inactivateAllBehaviorsOf(behavior.get('exclusionGroup')); + let behaviors = this.get('behaviors'); + let behaviorsOff = this.get('behaviorsOff'); + behaviorsOff.removeObject(behavior); + behaviors.insertAt(0, behavior); + } else { + this.inactivateBehavior(behavior); + } + }, + + /** + * Returns the active behavior of a multually exclusive group. + * + * @function + * @param {string} exclusionGroup + * @returns {Behavior} - The active behavior's instance or `undefined` otherwise. + */ + getActiveBehaviorOf(exclusionGroup) { + return this.get('behaviors').findBy('exclusionGroup', exclusionGroup); + }, + + /** + * Returns the inactive behavior of a multually exclusive group having the highest priority. + * + * @function + * @param {string} exclusionGroup + * @param {boolean} [value=true] - Whether the behavior is activated (true) or inactivated (false). + * @returns {Behavior} - The inactive behavior's instance or `undefined` otherwise. + */ + getInactiveBehaviorOf(exclusionGroup) { + return this.get('behaviorsOff').findBy('exclusionGroup', exclusionGroup); + }, + + /** + * Returns the behavior of a multually exclusive having the highest priority. + * + * @function + * @param {string} exclusionGroup + * @returns {Behavior} - The behavior's instance or `undefined` otherwise. + */ + getFirstBehaviorOf(exclusionGroup) { + return A(this.get('allBehaviors').filterBy('exclusionGroup', exclusionGroup)).objectAt(0); + }, + + /** + * Turns off the active behavior of a mutually exclusive group of behaviors. + * + * @function + * @param {string} exclusionGroup + */ + inactivateAllBehaviorsOf(exclusionGroup) { + let active = this.getActiveBehaviorOf(exclusionGroup); + while (active) { + this.inactivateBehavior(active); + active = this.getActiveBehaviorOf(exclusionGroup); + } + }, + + _oldEvents: null, + + _turnOffOldEvents() { + let oldEvents = this._oldEvents; + if (oldEvents) { + oldEvents.forEach((turnOff) => turnOff()); + } + this._oldEvents = A(); + }, + + /** + * An array containing the set of exclusion groups shared by the behaviors that are on + */ + exclusionGroups: computed('behaviors.{[],exclusionGroup}', function() { + return A(this.get('behaviors').mapBy('exclusionGroup').filter((g) => g)).uniq(); + }), + + /** + * Behaviors that are active. A behavior is active if it's on and if it has the greatest + * priority among the behaviors of its exclusion group. + */ + activeBehaviors: computed('exclusionGroups', function() { + let groups = this.get('exclusionGroups'); + return this + .get('behaviors') + .filter((b) => { + let group = b.get('exclusionGroup'); + if (group) { + if (groups.includes(group)) { + groups.removeObject(b); + return true; + } + return false; + } + return true; + }); + }), + + triggerBehaviorEvent(type) { + let args = Array.prototype.slice.call(arguments, 1); + this.trigger(listenerName(type, gatherKeys(args[args.length - 1])), ...args); + this.trigger(listenerName(type), ...args); + }, + + /** + * This function attach or detach event listeners based on the behaviors present in + * `behaviors` + * + * @function + */ + _updateEvents() { + this._turnOffOldEvents(); + let getCallback + = (b, f) => { + let that = this; + return function() { + return b[f].call(b, that, ...arguments); + }; + }; + let getTurnOffFunc = (name, f) => () => this.off(name, f); + let getInnerLoop = (b, f) => (name) => { + let g = this.on(name, getCallback(b, f)); + this._oldEvents.pushObject(getTurnOffFunc(name, b, g)); + }; + this + .get('activeBehaviors') + .forEach((b) => { + for (let f in b.events) { + b.events[f].forEach(getInnerLoop(b, f)); + } + }); + }, + + _onBehaviorsChanged: on('init', observer( + 'behaviors.[]', + 'behaviors.@each.events', + function() { + run.once(this, '_updateEvents'); + } + )), + + _setClasses: on('didRender', observer( + 'activeBehaviors.[]', + 'behaviorsOff.[]', + function() { + let element = this.get('element'); + if (element) { + let classesOn = A([]); + let classesOff = A([]); + let bOff = this.get('behaviorsOff'); + let bOn = this.get('behaviors'); + bOff.map((b) => b.get('classNames')).filter((l) => l).forEach((l) => classesOff.addObjects(l)); + bOn.map((b) => b.get('classNames')).filter((l) => l).forEach((l) => classesOn.addObjects(l)); + classesOff.addObjects(bOff.mapBy('exclusionGroup').filter((g) => g)); + classesOn.addObjects(bOn.mapBy('exclusionGroup').filter((g) => g)); + classesOff.forEach((c) => element.classList.remove(c)); + classesOn.forEach((c) => element.classList.add(c)); + } + } + )) + +}); diff --git a/addon/templates/components/lt-body.hbs b/addon/templates/components/lt-body.hbs index e05450bd..437ccfda 100644 --- a/addon/templates/components/lt-body.hbs +++ b/addon/templates/components/lt-body.hbs @@ -36,8 +36,16 @@ enableScaffolding=enableScaffolding canExpand=canExpand canSelect=canSelect - click=(action 'onRowClick' row) - doubleClick=(action 'onRowDoubleClick' row)}} + rowClick=(action 'onRowClick') + rowDoubleClick=(action 'onRowDoubleClick') + rowMouseDown=(action 'onRowMouseDown') + rowMouseUp=(action 'onRowMouseUp') + rowMouseMove=(action 'onRowMouseMove') + rowTouchStart=(action 'onRowTouchStart') + rowTouchEnd=(action 'onRowTouchEnd') + rowTouchCancel=(action 'onRowTouchCancel') + rowTouchLeave=(action 'onRowTouchLeave') + rowTouchMove=(action 'onRowTouchMove')}} {{yield (hash expanded-row=(component lt.spanned-row classes='lt-expanded-row' colspan=colspan yield=row visible=row.expanded) @@ -88,21 +96,33 @@ as |row index| }} {{lt.row row columns - data-row-id=row.rowId - table=table - tableActions=tableActions - extra=extra - enableScaffolding=enableScaffolding - canExpand=canExpand - canSelect=canSelect - click=(action 'onRowClick' row) - doubleClick=(action 'onRowDoubleClick' row)}} + data-row-id=row.rowId + ltBody=this + tableActions=tableActions + extra=extra + enableScaffolding=enableScaffolding + canExpand=canExpand + canSelect=canSelect + rowClick=(action 'onRowClick') + rowDoubleClick=(action 'onRowDoubleClick') + rowMouseDown=(action 'onRowMouseDown') + rowMouseUp=(action 'onRowMouseUp') + rowMouseMove=(action 'onRowMouseMove') + rowTouchStart=(action 'onRowTouchStart') + rowTouchEnd=(action 'onRowTouchEnd') + rowTouchCancel=(action 'onRowTouchCancel') + rowTouchLeave=(action 'onRowTouchLeave') + rowTouchMove=(action 'onRowTouchMove') + }} {{/vertical-collection}} - {{yield (hash - loader=(component lt.spanned-row classes='lt-is-loading') - no-data=(component lt.spanned-row classes='lt-no-data') - expanded-row=(component lt.spanned-row visible=false) - ) rows}} + {{yield + (hash + loader=(component lt.spanned-row classes='lt-is-loading') + no-data=(component lt.spanned-row classes='lt-no-data') + expanded-row=(component lt.spanned-row visible=false) + ) + rows + }} {{/if}} diff --git a/addon/utils/listener-name.js b/addon/utils/listener-name.js new file mode 100644 index 00000000..015f5d99 --- /dev/null +++ b/addon/utils/listener-name.js @@ -0,0 +1,26 @@ +import getCmdKey from 'ember-keyboard/utils/get-cmd-key'; + +function sortedKeys(keyArray) { + return keyArray.sort().join('+'); +} + +/* + * Generates an event name from the type of event and the key + * modifiers of the event. + * + * modified from ember-keyboard + * _none is new + */ +export default function listenerName(type, keyArray) { + let keys; + if (keyArray) { + if (keyArray.indexOf('cmd') > -1) { + keyArray[keyArray.indexOf('cmd')] = getCmdKey(); + } + keys = keyArray.length === 0 ? '_none' : sortedKeys(keyArray); + } else { + keys = '_all'; + } + return `${type}:${keys}`; +} + diff --git a/addon/utils/with-backing-field.js b/addon/utils/with-backing-field.js new file mode 100644 index 00000000..ff57c0dd --- /dev/null +++ b/addon/utils/with-backing-field.js @@ -0,0 +1,17 @@ +import { computed } from '@ember/object'; + +export default function withBackingField(backingField, f) { + return computed({ + get() { + if (!this[backingField]) { + this[backingField] = f.call(this); + } + return this[backingField]; + }, + set(key, value) { + this[backingField] = value; + return value; + } + }); +} + diff --git a/app/behaviors/multi-select.js b/app/behaviors/multi-select.js new file mode 100644 index 00000000..23f27634 --- /dev/null +++ b/app/behaviors/multi-select.js @@ -0,0 +1 @@ +export { default } from 'ember-light-table/behaviors/multi-select.js'; diff --git a/app/behaviors/row-expansion.js b/app/behaviors/row-expansion.js new file mode 100644 index 00000000..87095aaa --- /dev/null +++ b/app/behaviors/row-expansion.js @@ -0,0 +1 @@ +export { default } from 'ember-light-table/behaviors/row-expansion.js'; diff --git a/app/behaviors/select-all.js b/app/behaviors/select-all.js new file mode 100644 index 00000000..83b682e1 --- /dev/null +++ b/app/behaviors/select-all.js @@ -0,0 +1 @@ +export { default } from 'ember-light-table/behaviors/select-all.js'; diff --git a/app/behaviors/single-select.js b/app/behaviors/single-select.js new file mode 100644 index 00000000..4dbf124b --- /dev/null +++ b/app/behaviors/single-select.js @@ -0,0 +1 @@ +export { default } from 'ember-light-table/behaviors/single-select.js'; diff --git a/app/helpers/lt-multi-select.js b/app/helpers/lt-multi-select.js new file mode 100644 index 00000000..c784d493 --- /dev/null +++ b/app/helpers/lt-multi-select.js @@ -0,0 +1,10 @@ +import Helper from '@ember/component/helper'; +import MultiSelectBehavior from 'ember-light-table/behaviors/multi-select'; + +export default Helper.extend({ + + compute(params, namedArgs) { + return MultiSelectBehavior.create(namedArgs); + } + +}); diff --git a/app/helpers/lt-row-expansion.js b/app/helpers/lt-row-expansion.js new file mode 100644 index 00000000..81a71a18 --- /dev/null +++ b/app/helpers/lt-row-expansion.js @@ -0,0 +1,10 @@ +import Helper from '@ember/component/helper'; +import RowExpansionBehavior from 'ember-light-table/behaviors/row-expansion'; + +export default Helper.extend({ + + compute(params, namedArgs) { + return RowExpansionBehavior.create(namedArgs); + } + +}); diff --git a/app/helpers/lt-select-all.js b/app/helpers/lt-select-all.js new file mode 100644 index 00000000..080373ec --- /dev/null +++ b/app/helpers/lt-select-all.js @@ -0,0 +1,10 @@ +import Helper from '@ember/component/helper'; +import SelectAllBehavior from 'ember-light-table/behaviors/select-all'; + +export default Helper.extend({ + + compute() { + return SelectAllBehavior.create(); + } + +}); diff --git a/app/helpers/lt-single-select.js b/app/helpers/lt-single-select.js new file mode 100644 index 00000000..c894bc70 --- /dev/null +++ b/app/helpers/lt-single-select.js @@ -0,0 +1,10 @@ +import Helper from '@ember/component/helper'; +import SingleSelectBehavior from 'ember-light-table/behaviors/single-select'; + +export default Helper.extend({ + + compute() { + return SingleSelectBehavior.create(); + } + +}); diff --git a/config/environment.js b/config/environment.js index 0dfaed47..97a0e2f3 100644 --- a/config/environment.js +++ b/config/environment.js @@ -1,5 +1,9 @@ 'use strict'; module.exports = function(/* environment, appConfig */) { - return { }; + return { + emberKeyboard: { + listeners: ['keyUp', 'keyDown', 'keyPress', 'click', 'mouseDown', 'mouseUp', 'touchStart', 'touchEnd'] + } + }; }; diff --git a/package.json b/package.json index b57eba04..682d6bf5 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "ember-disable-prototype-extensions": "^1.1.2", "ember-export-application-global": "^2.0.0", "ember-font-awesome": "^4.0.0-rc.4", + "ember-keyboard": "^4.0.0", "ember-load-initializers": "^1.0.0", "ember-maybe-import-regenerator": "^0.1.6", "ember-native-dom-helpers": "^0.6.2", diff --git a/tests/dummy/app/components/rows/selectable-table.js b/tests/dummy/app/components/rows/selectable-table.js index 63af08d7..2197a456 100644 --- a/tests/dummy/app/components/rows/selectable-table.js +++ b/tests/dummy/app/components/rows/selectable-table.js @@ -35,11 +35,11 @@ export default Component.extend(TableCommon, { actions: { selectAll() { - this.get('table.rows').setEach('selected', true); + this.get('table').selectAll(); }, deselectAll() { - this.get('table.selectedRows').setEach('selected', false); + this.get('table').deselectAll(); }, deleteAll() { diff --git a/tests/dummy/app/templates/components/columns/draggable-table.hbs b/tests/dummy/app/templates/components/columns/draggable-table.hbs index e340ca47..ad3573bf 100644 --- a/tests/dummy/app/templates/components/columns/draggable-table.hbs +++ b/tests/dummy/app/templates/components/columns/draggable-table.hbs @@ -9,7 +9,7 @@ }} {{#t.body - canSelect=false + useLegacyBehaviorFlags=false onScrolledToBottom=(action 'onScrolledToBottom') as |body| }} diff --git a/tests/dummy/app/templates/components/columns/grouped-table.hbs b/tests/dummy/app/templates/components/columns/grouped-table.hbs index ba218156..5012df29 100644 --- a/tests/dummy/app/templates/components/columns/grouped-table.hbs +++ b/tests/dummy/app/templates/components/columns/grouped-table.hbs @@ -9,7 +9,7 @@ }} {{#t.body - canSelect=false + useLegacyBehaviorFlags=false onScrolledToBottom=(action 'onScrolledToBottom') as |body| }} diff --git a/tests/dummy/app/templates/components/columns/resizable-table.hbs b/tests/dummy/app/templates/components/columns/resizable-table.hbs index f2fed617..57f06534 100644 --- a/tests/dummy/app/templates/components/columns/resizable-table.hbs +++ b/tests/dummy/app/templates/components/columns/resizable-table.hbs @@ -11,7 +11,7 @@ }} {{#t.body - canSelect=false + useLegacyBehaviorFlags=false onScrolledToBottom=(action 'onScrolledToBottom') as |body| }} diff --git a/tests/dummy/app/templates/components/cookbook/client-side-table.hbs b/tests/dummy/app/templates/components/cookbook/client-side-table.hbs index 1f274048..d6209dd7 100644 --- a/tests/dummy/app/templates/components/cookbook/client-side-table.hbs +++ b/tests/dummy/app/templates/components/cookbook/client-side-table.hbs @@ -16,7 +16,7 @@ }} {{#t.body - canSelect=false + useLegacyFlagBehaviors=false as |body| }} {{#if isLoading}} diff --git a/tests/dummy/app/templates/components/cookbook/custom-row-table.hbs b/tests/dummy/app/templates/components/cookbook/custom-row-table.hbs index ef628c61..57a1c0af 100644 --- a/tests/dummy/app/templates/components/cookbook/custom-row-table.hbs +++ b/tests/dummy/app/templates/components/cookbook/custom-row-table.hbs @@ -16,7 +16,7 @@ }} {{#t.body - canSelect=false + useLegacyBehaviorFlags=false onScrolledToBottom=(action 'onScrolledToBottom') rowComponent=(component 'colored-row') as |body| diff --git a/tests/dummy/app/templates/components/cookbook/custom-sort-icon-table.hbs b/tests/dummy/app/templates/components/cookbook/custom-sort-icon-table.hbs index 1a1f1cdc..8dc0b0af 100644 --- a/tests/dummy/app/templates/components/cookbook/custom-sort-icon-table.hbs +++ b/tests/dummy/app/templates/components/cookbook/custom-sort-icon-table.hbs @@ -11,7 +11,7 @@ }} {{#t.body - canSelect=false + useLegacyBehaviorFlags=false onScrolledToBottom=(action 'onScrolledToBottom') as |body| }} diff --git a/tests/dummy/app/templates/components/cookbook/horizontal-scrolling-table.hbs b/tests/dummy/app/templates/components/cookbook/horizontal-scrolling-table.hbs index 2300bdd1..40bbcd17 100644 --- a/tests/dummy/app/templates/components/cookbook/horizontal-scrolling-table.hbs +++ b/tests/dummy/app/templates/components/cookbook/horizontal-scrolling-table.hbs @@ -16,7 +16,7 @@ }} {{#t.body - canSelect=false + useLegacyBehaviorFlags=false onScrolledToBottom=(action 'onScrolledToBottom') as |body| }} diff --git a/tests/dummy/app/templates/components/cookbook/occluded-table.hbs b/tests/dummy/app/templates/components/cookbook/occluded-table.hbs index 8c8a3333..88687c09 100644 --- a/tests/dummy/app/templates/components/cookbook/occluded-table.hbs +++ b/tests/dummy/app/templates/components/cookbook/occluded-table.hbs @@ -16,7 +16,7 @@ }} {{#t.body - canSelect=false + useLegacyBehaviorFlags=false scrollBuffer=200 onScrolledToBottom=(action 'onScrolledToBottom') as |body| diff --git a/tests/dummy/app/templates/components/cookbook/paginated-table.hbs b/tests/dummy/app/templates/components/cookbook/paginated-table.hbs index c125cf01..38a6621a 100644 --- a/tests/dummy/app/templates/components/cookbook/paginated-table.hbs +++ b/tests/dummy/app/templates/components/cookbook/paginated-table.hbs @@ -16,7 +16,7 @@ }} {{#t.body - canSelect=false + useLegacyBehaviorFlags=false as |body| }} {{#if isLoading}} diff --git a/tests/dummy/app/templates/components/cookbook/table-actions-table.hbs b/tests/dummy/app/templates/components/cookbook/table-actions-table.hbs index 70c7b405..965c29ea 100644 --- a/tests/dummy/app/templates/components/cookbook/table-actions-table.hbs +++ b/tests/dummy/app/templates/components/cookbook/table-actions-table.hbs @@ -19,7 +19,7 @@ }} {{#t.body - canSelect=false + useLagacyBehaviorFlags=false onScrolledToBottom=(action 'onScrolledToBottom') as |body| }} diff --git a/tests/dummy/app/templates/components/responsive-table.hbs b/tests/dummy/app/templates/components/responsive-table.hbs index 7c06b225..a68f85e3 100644 --- a/tests/dummy/app/templates/components/responsive-table.hbs +++ b/tests/dummy/app/templates/components/responsive-table.hbs @@ -21,8 +21,7 @@ }} {{#t.body - canSelect=false - expandOnClick=false + useLegacyBehaviorFlags=false onScrolledToBottom=(action 'onScrolledToBottom') as |body| }} diff --git a/tests/dummy/app/templates/components/rows/expandable-table.hbs b/tests/dummy/app/templates/components/rows/expandable-table.hbs index 6dcc4223..6bc2756b 100644 --- a/tests/dummy/app/templates/components/rows/expandable-table.hbs +++ b/tests/dummy/app/templates/components/rows/expandable-table.hbs @@ -9,10 +9,11 @@ }} {{#t.body - canExpand=true - multiRowExpansion=false + useLegacyBehaviorFlags=false + behaviors=(array (lt-row-expansion multiRow=false)) onScrolledToBottom=(action 'onScrolledToBottom') - as |body|}} + as |body| + }} {{#body.expanded-row as |row|}} {{expanded-row row=row}} {{/body.expanded-row}} diff --git a/tests/dummy/app/templates/components/rows/selectable-table.hbs b/tests/dummy/app/templates/components/rows/selectable-table.hbs index 1bd951c9..28427dfe 100644 --- a/tests/dummy/app/templates/components/rows/selectable-table.hbs +++ b/tests/dummy/app/templates/components/rows/selectable-table.hbs @@ -18,7 +18,8 @@ }} {{#t.body - multiSelect=true + useLegacyBehaviorFlags=false + behaviors=(array (lt-multi-select)) onScrolledToBottom=(action 'onScrolledToBottom') as |body| }} diff --git a/tests/dummy/app/templates/components/scrolling-table.hbs b/tests/dummy/app/templates/components/scrolling-table.hbs index 07e94ca5..65ea3bff 100644 --- a/tests/dummy/app/templates/components/scrolling-table.hbs +++ b/tests/dummy/app/templates/components/scrolling-table.hbs @@ -16,7 +16,7 @@ }} {{#t.body - canSelect=false + useLegacyBehaviorFlags=false scrollTo=scrollTo scrollToRow=scrollToRow onScroll=(action (mut currentScrollOffset)) diff --git a/tests/dummy/app/templates/components/simple-table.hbs b/tests/dummy/app/templates/components/simple-table.hbs index 51243a67..e402d8bf 100644 --- a/tests/dummy/app/templates/components/simple-table.hbs +++ b/tests/dummy/app/templates/components/simple-table.hbs @@ -16,7 +16,7 @@ }} {{#t.body - canSelect=false + useLegacyBehaviorFlags=false onScrolledToBottom=(action 'onScrolledToBottom') as |body| }} diff --git a/tests/integration/components/lt-body-test.js b/tests/integration/components/lt-body-test.js index a66b0c5f..be8b145d 100644 --- a/tests/integration/components/lt-body-test.js +++ b/tests/integration/components/lt-body-test.js @@ -1,14 +1,28 @@ -import { click, findAll, find, triggerEvent } from '@ember/test-helpers'; +import { + click, + findAll, + find, + triggerEvent, + triggerKeyEvent, + render +} from '@ember/test-helpers'; + +import hasClass from '../../helpers/has-class'; +import Columns from '../../helpers/table-columns'; + import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import setupMirageTest from 'ember-cli-mirage/test-support/setup-mirage'; -import Table from 'ember-light-table'; -import hasClass from '../../helpers/has-class'; -import Columns from '../../helpers/table-columns'; + +import { A as emberArray } from '@ember/array'; import { run } from '@ember/runloop'; import { all } from 'rsvp'; +import getCmdKey from 'ember-keyboard/utils/get-cmd-key'; +import Table from 'ember-light-table'; +import SingleSelectBehavior from 'ember-light-table/behaviors/single-select'; +import MultiSelectBehavior from 'ember-light-table/behaviors/multi-select'; +import RowExpansionBehavior from 'ember-light-table/behaviors/row-expansion'; module('Integration | Component | lt body', function(hooks) { setupRenderingTest(hooks); @@ -31,102 +45,188 @@ module('Integration | Component | lt body', function(hooks) { assert.equal(find('*').textContent.trim(), ''); }); - test('row selection - enable or disable', async function(assert) { - this.set('table', new Table(Columns, this.server.createList('user', 1))); - this.set('canSelect', false); - - await render(hbs `{{lt-body table=table sharedOptions=sharedOptions canSelect=canSelect}}`); - + async function testSelectDisabled(assert) { let row = find('tr'); - assert.notOk(hasClass(row, 'is-selectable')); assert.notOk(hasClass(row, 'is-selected')); await click(row); assert.notOk(hasClass(row, 'is-selected')); + } - this.set('canSelect', true); - + async function testSelectEnabled(assert) { + let row = find('tr'); assert.ok(hasClass(row, 'is-selectable')); assert.notOk(hasClass(row, 'is-selected')); await click(row); assert.ok(hasClass(row, 'is-selected')); + } + + test('row selection - enable or disable (legacy)', async function(assert) { + this.set('table', new Table(Columns, this.server.createList('user', 1))); + this.set('canSelect', false); + await render(hbs ` + {{lt-body + table=table + sharedOptions=sharedOptions + canSelect=canSelect + }}` + ); + await testSelectDisabled(assert); + this.set('canSelect', true); + await testSelectEnabled(assert); }); - test('row selection - ctrl-click to modify selection', async function(assert) { - this.set('table', new Table(Columns, this.server.createList('user', 5))); + test('row selection - enable or disable', async function(assert) { + this.set('table', new Table(Columns, this.server.createList('user', 1))); + this.set('behaviors', emberArray()); + await render(hbs ` + {{lt-body + table=table + sharedOptions=sharedOptions + useLegacyBehaviorFlags=false + behaviors=behaviors + }}` + ); + await testSelectDisabled(assert); + this.set('behaviors', emberArray([SingleSelectBehavior.create({})])); + await testSelectEnabled(assert); + }); - await render(hbs `{{lt-body table=table sharedOptions=sharedOptions canSelect=true multiSelect=true}}`); + async function testSelectCtrlClick(assert) { let firstRow = find('tr:first-child'); let middleRow = find('tr:nth-child(4)'); let lastRow = find('tr:last-child'); - assert.equal(findAll('tbody > tr').length, 5); - await click(firstRow); assert.equal(findAll('tr.is-selected').length, 1, 'clicking a row selects it'); - await click(lastRow, { shiftKey: true }); assert.equal(findAll('tr.is-selected').length, 5, 'shift-clicking another row selects it and all rows between'); - await click(middleRow, { ctrlKey: true }); assert.equal(findAll('tr.is-selected').length, 4, 'ctrl-clicking a selected row deselects it'); - await click(firstRow); assert.equal(findAll('tr.is-selected').length, 0, 'clicking a selected row deselects all rows'); - }); + } - test('row selection - click to modify selection', async function(assert) { + test('row selection - ctrl-click to modify selection (legacy)', async function(assert) { this.set('table', new Table(Columns, this.server.createList('user', 5))); + await render(hbs ` + {{lt-body + table=table + sharedOptions=sharedOptions + canSelect=true + multiSelect=true + }}` + ); + await testSelectCtrlClick(assert); + }); - await render( - hbs `{{lt-body table=table sharedOptions=sharedOptions canSelect=true multiSelect=true multiSelectRequiresKeyboard=false}}` + test('row selection - ctrl-click to modify selection', async function(assert) { + this.set('table', new Table(Columns, this.server.createList('user', 5))); + this.set('behaviors', emberArray([MultiSelectBehavior.create()])); + await render(hbs ` + {{lt-body + table=table + sharedOptions=sharedOptions + useLegacyBehaviorFlags=false + behaviors=behaviors + }}` ); + await testSelectCtrlClick(assert); + }); + async function testSelectClick(assert) { let firstRow = find('tr:first-child'); let middleRow = find('tr:nth-child(4)'); let lastRow = find('tr:last-child'); - assert.equal(findAll('tbody > tr').length, 5); - await click(firstRow); assert.equal(findAll('tr.is-selected').length, 1, 'clicking a row selects it'); - await click(lastRow, { shiftKey: true }); assert.equal(findAll('tr.is-selected').length, 5, 'shift-clicking another row selects it and all rows between'); - await click(middleRow); assert.equal(findAll('tr.is-selected').length, 4, 'clicking a selected row deselects it without affecting other selected rows'); - await click(middleRow); assert.equal(findAll('tr.is-selected').length, 5, 'clicking a deselected row selects it without affecting other selected rows'); + } + + async function triggerSelectAll() { + let options = {}; + options[`${getCmdKey()}Key`] = true; + await triggerKeyEvent(document.body, 'keydown', 65, options); + } + + test('row selection - select all key', async function(assert) { + this.set('table', new Table(Columns, this.server.createList('user', 3))); + await render(hbs ` + {{lt-body + table=table + sharedOptions=sharedOptions + canSelect=true + multiSelect=true}}` + ); + find('.lt-body-wrap').focus(); + await triggerSelectAll(); + assert.equal(findAll('tr.is-selected').length, 3, 'cmd+a selects all rows'); }); - test('row expansion', async function(assert) { - this.set('table', new Table(Columns, this.server.createList('user', 2))); - this.set('canExpand', false); + test('row selection - select all key - single select', async function(assert) { + this.set('table', new Table(Columns, this.server.createList('user', 3))); + await render(hbs `{{lt-body table=table sharedOptions=sharedOptions canSelect=true}}`); + await triggerSelectAll(); + assert.equal(this.$('tr.is-selected').length, 0, 'cmd+a selects no row'); + }); + test('row selection - select all key - select disabled', async function(assert) { + this.set('table', new Table(Columns, this.server.createList('user', 3))); + this.render(hbs `{{lt-body table=table sharedOptions=sharedOptions canSelect=false}}`); + await triggerSelectAll(); + assert.equal(this.$('tr.is-selected').length, 0, 'cmd+a selects no row'); + }); + + test('row selection - click to modify selection (legacy)', async function(assert) { + this.set('table', new Table(Columns, this.server.createList('user', 5))); await render(hbs ` - {{#lt-body table=table sharedOptions=sharedOptions canSelect=false canExpand=canExpand multiRowExpansion=false as |b|}} - {{#b.expanded-row}} Hello {{/b.expanded-row}} - {{/lt-body}} - `); + {{lt-body + table=table + sharedOptions=sharedOptions + canSelect=true + multiSelect=true + multiSelectRequiresKeyboard=false + }}` + ); + await testSelectClick(assert); + }); - let row = find('tr'); + test('row selection - click to modify selection', async function(assert) { + this.set('table', new Table(Columns, this.server.createList('user', 5))); + this.set('behaviors', emberArray([MultiSelectBehavior.create({ requiresKeyboard: false })])); + await render(hbs ` + {{lt-body + table=table + sharedOptions=sharedOptions + useLegacyBehaviorFlags=false + behaviors=behaviors + }}` + ); + await testSelectClick(assert); + }); + async function testRowExpansionDisabled(assert) { + let row = find('tr'); assert.notOk(hasClass(row, 'is-expandable')); await click(row); assert.equal(findAll('tr.lt-expanded-row').length, 0); assert.equal(findAll('tbody > tr').length, 2); assert.notOk(find('tr.lt-expanded-row')); + } - this.set('canExpand', true); - + async function testRowExpansionEnabled(assert) { + let row = find('tr'); assert.ok(hasClass(row, 'is-expandable')); await click(row); assert.equal(findAll('tr.lt-expanded-row').length, 1); assert.equal(findAll('tbody > tr').length, 3); assert.equal(row.nextElementSibling.textContent.trim(), 'Hello'); - let allRows = findAll('tr'); row = allRows[allRows.length - 1]; assert.ok(hasClass(row, 'is-expandable')); @@ -134,19 +234,57 @@ module('Integration | Component | lt body', function(hooks) { assert.equal(findAll('tr.lt-expanded-row').length, 1); assert.equal(findAll('tbody > tr').length, 3); assert.equal(row.nextElementSibling.textContent.trim(), 'Hello'); + } + + test('row expansion (legacy)', async function(assert) { + this.set('table', new Table(Columns, this.server.createList('user', 2))); + this.set('canExpand', false); + await render(hbs ` + {{#lt-body + table=table + sharedOptions=sharedOptions + canSelect=false + canExpand=canExpand + multiRowExpansion=false + as |b| + }} + {{#b.expanded-row}} Hello {{/b.expanded-row}} + {{/lt-body}}` + ); + await testRowExpansionDisabled(assert); + this.set('canExpand', true); + await testRowExpansionEnabled(assert); }); - test('row expansion - multiple', async function(assert) { + test('row expansion', async function(assert) { this.set('table', new Table(Columns, this.server.createList('user', 2))); + this.set('canExpand', false); + this.set( + 'behaviors', + emberArray() + ); await render(hbs ` - {{#lt-body table=table sharedOptions=sharedOptions canExpand=true as |b|}} + {{#lt-body + table=table + sharedOptions=sharedOptions + useLegacyBehaviorFlags=false + behaviors=behaviors + as |b| + }} {{#b.expanded-row}} Hello {{/b.expanded-row}} - {{/lt-body}} - `); + {{/lt-body}}` + ); + await testRowExpansionDisabled(assert); + this.set( + 'behaviors', + emberArray([RowExpansionBehavior.create({ multiRow: false })]) + ); + await testRowExpansionEnabled(assert); + }); + async function testRowExpansionMultiple(assert) { let rows = findAll('tr'); assert.equal(rows.length, 2); - await all( rows.map(async(row) => { assert.ok(hasClass(row, 'is-expandable')); @@ -154,8 +292,42 @@ module('Integration | Component | lt body', function(hooks) { assert.equal(row.nextElementSibling.textContent.trim(), 'Hello'); }) ); - assert.equal(findAll('tr.lt-expanded-row').length, 2); + } + + test('row expansion - multiple (legacy)', async function(assert) { + this.set('table', new Table(Columns, this.server.createList('user', 2))); + await render(hbs ` + {{#lt-body + table=table + sharedOptions=sharedOptions + canExpand=true + as |b| + }} + {{#b.expanded-row}} Hello {{/b.expanded-row}} + {{/lt-body}} + `); + await testRowExpansionMultiple(assert); + }); + + test('row expansion - multiple', async function(assert) { + this.set('table', new Table(Columns, this.server.createList('user', 2))); + this.set( + 'behaviors', + emberArray([RowExpansionBehavior.create()]) + ); + await render(hbs ` + {{#lt-body + table=table + sharedOptions=sharedOptions + useLegacyBehaviorFlags=false + behaviors=behaviors + as |b| + }} + {{#b.expanded-row}} Hello {{/b.expanded-row}} + {{/lt-body}} + `); + await testRowExpansionMultiple(assert); }); test('row actions', async function(assert) { diff --git a/yarn.lock b/yarn.lock index 5052c077..4fcc943a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -722,20 +722,20 @@ dependencies: "@glimmer/util" "^0.27.0" -"@html-next/vertical-collection@^1.0.0-beta.12": - version "1.0.0-beta.12" - resolved "https://registry.yarnpkg.com/@html-next/vertical-collection/-/vertical-collection-1.0.0-beta.12.tgz#c5cbed1a7cc9bfe8d4e82474bbb8d265882bc6f9" - integrity sha512-DAMomcdVH7dxxSmDvI99QRcsl2kV+RKRjbickfyPMN4EcSBQZkAQfkXdJbktk2iW+OuoNHMAlkBV2atwT3OBsw== +"@html-next/vertical-collection@^1.0.0-beta.13": + version "1.0.0-beta.13" + resolved "https://registry.yarnpkg.com/@html-next/vertical-collection/-/vertical-collection-1.0.0-beta.13.tgz#0cd7fbe813fef8c7daea3c46a3d4167cbd8db682" + integrity sha512-YFs+toYLFSCiZSv1kkb3IPvrcVJHVX6q7vkzP0qb2Rvd0s61YqgpPDqtZ8sVViip0Lu3kw+Hs9fysjpAFqJDzQ== dependencies: babel-plugin-transform-es2015-block-scoping "^6.24.1" babel6-plugin-strip-class-callcheck "^6.0.0" broccoli-funnel "^2.0.1" - broccoli-merge-trees "^2.0.0" + broccoli-merge-trees "^3.0.1" broccoli-rollup "^2.0.0" ember-cli-babel "^6.6.0" - ember-cli-htmlbars "^2.0.3" + ember-cli-htmlbars "^3.0.0" ember-cli-version-checker "^2.1.0" - ember-compatibility-helpers "^0.1.2" + ember-compatibility-helpers "^1.0.0" ember-raf-scheduler "0.1.0" "@sindresorhus/is@^0.7.0": @@ -1424,7 +1424,7 @@ babel-plugin-check-es2015-constants@^6.22.0: dependencies: babel-runtime "^6.22.0" -babel-plugin-debug-macros@^0.1.10, babel-plugin-debug-macros@^0.1.11: +babel-plugin-debug-macros@^0.1.10: version "0.1.11" resolved "https://registry.yarnpkg.com/babel-plugin-debug-macros/-/babel-plugin-debug-macros-0.1.11.tgz#6c562bf561fccd406ce14ab04f42c218cf956605" integrity sha512-hZw5qNNGAR02Y+yBUrtsnJHh8OXavkayPRqKGAXnIm4t5rWVpj3ArwsC7TWdpZsBguQvHAeyTxZ7s23yY60HHg== @@ -1438,6 +1438,13 @@ babel-plugin-debug-macros@^0.2.0, babel-plugin-debug-macros@^0.2.0-beta.6: dependencies: semver "^5.3.0" +babel-plugin-ember-modules-api-polyfill@^2.3.2: + version "2.8.0" + resolved "https://registry.yarnpkg.com/babel-plugin-ember-modules-api-polyfill/-/babel-plugin-ember-modules-api-polyfill-2.8.0.tgz#70244800f750bf1c9f380910c1b2eed1db80ab4a" + integrity sha512-3dlBH92qx8so2pRoks73+gwnuX97d0ajirOr96GwTZMnZxFzVR02c/PQbKWBcxpPqoL8CJSE2onuWM8PWezhOQ== + dependencies: + ember-rfc176-data "^0.3.8" + babel-plugin-ember-modules-api-polyfill@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/babel-plugin-ember-modules-api-polyfill/-/babel-plugin-ember-modules-api-polyfill-2.5.0.tgz#860aab9fecbf38c10d1fe0779c6979a854fff154" @@ -1504,6 +1511,11 @@ babel-plugin-syntax-exponentiation-operator@^6.8.0: resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" integrity sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4= +babel-plugin-syntax-object-rest-spread@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" + integrity sha1-/WU28rzhODb/o6VFjEkDpZe7O/U= + babel-plugin-syntax-trailing-function-commas@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" @@ -1736,6 +1748,14 @@ babel-plugin-transform-exponentiation-operator@^6.22.0: babel-plugin-syntax-exponentiation-operator "^6.8.0" babel-runtime "^6.22.0" +babel-plugin-transform-object-rest-spread@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06" + integrity sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY= + dependencies: + babel-plugin-syntax-object-rest-spread "^6.8.0" + babel-runtime "^6.26.0" + babel-plugin-transform-regenerator@^6.22.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" @@ -2121,7 +2141,7 @@ broccoli-babel-transpiler@^6.0.0: rsvp "^4.8.2" workerpool "^2.3.0" -broccoli-babel-transpiler@^6.5.0: +broccoli-babel-transpiler@^6.4.5, broccoli-babel-transpiler@^6.5.0: version "6.5.1" resolved "https://registry.yarnpkg.com/broccoli-babel-transpiler/-/broccoli-babel-transpiler-6.5.1.tgz#a4afc8d3b59b441518eb9a07bd44149476e30738" integrity sha512-w6GcnkxvHcNCte5FcLGEG1hUdQvlfvSN/6PtGWU/otg69Ugk8rUk51h41R0Ugoc+TNxyeFG1opRt2RlA87XzNw== @@ -3811,6 +3831,16 @@ ember-cli-htmlbars@^2.0.1, ember-cli-htmlbars@^2.0.2, ember-cli-htmlbars@^2.0.3: json-stable-stringify "^1.0.0" strip-bom "^3.0.0" +ember-cli-htmlbars@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-3.0.1.tgz#01e21f0fd05e0a6489154f26614b1041769e3e58" + integrity sha512-pyyB2s52vKTXDC5svU3IjU7GRLg2+5O81o9Ui0ZSiBS14US/bZl46H2dwcdSJAK+T+Za36ZkQM9eh1rNwOxfoA== + dependencies: + broccoli-persistent-filter "^1.4.3" + hash-for-dep "^1.2.3" + json-stable-stringify "^1.0.0" + strip-bom "^3.0.0" + ember-cli-inject-live-reload@^1.7.0: version "1.10.2" resolved "https://registry.yarnpkg.com/ember-cli-inject-live-reload/-/ember-cli-inject-live-reload-1.10.2.tgz#43c59f7f1d1e717772da32e5e81d948fb9fe7c94" @@ -4121,13 +4151,13 @@ ember-code-snippet@^2.2.0: es6-promise "^1.0.0" glob "^7.1.3" -ember-compatibility-helpers@^0.1.2: - version "0.1.3" - resolved "https://registry.yarnpkg.com/ember-compatibility-helpers/-/ember-compatibility-helpers-0.1.3.tgz#039a57e9f1a401efda0023c1e3650bd01cfd7087" - integrity sha512-3YBCYrbZ+HqQRSxbGl9jso1FBX6WjGS9FC7tgmmul3us6vtFPxjjnGN25H5ze2xk/mDCG373p0lm5xG+mVpKkA== +ember-compatibility-helpers@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/ember-compatibility-helpers/-/ember-compatibility-helpers-1.2.0.tgz#feee16c5e9ef1b1f1e53903b241740ad4b01097e" + integrity sha512-pUW4MzJdcaQtwGsErYmitFRs0rlCYBAnunVzlFFUBr4xhjlCjgHJo0b53gFnhTgenNM3d3/NqLarzRhDTjXRTg== dependencies: - babel-plugin-debug-macros "^0.1.11" - ember-cli-version-checker "^2.0.0" + babel-plugin-debug-macros "^0.2.0" + ember-cli-version-checker "^2.1.1" semver "^5.4.1" ember-compatibility-helpers@^1.0.2, ember-compatibility-helpers@^1.1.1: @@ -4265,6 +4295,14 @@ ember-invoke-action@^1.5.0: dependencies: ember-cli-babel "^6.6.0" +ember-keyboard@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/ember-keyboard/-/ember-keyboard-4.0.0.tgz#63764ebf0fac8153977b04faa73ef1c11820800c" + integrity sha512-445XJXehcxzB/bPFWNhA0OO/m6pQ3sfipCGcjMGSU3erpOZSfM4kZ7uRFEUIf0Pz0tyHRQ9BoLq5RY7VBEXUWw== + dependencies: + babel-plugin-transform-object-rest-spread "^6.26.0" + ember-cli-babel "^6.6.0" + ember-lifeline@^3.0.1: version "3.0.9" resolved "https://registry.yarnpkg.com/ember-lifeline/-/ember-lifeline-3.0.9.tgz#041133471d8524e89826883a114419cc71a0348a" @@ -4364,6 +4402,11 @@ ember-rfc176-data@^0.3.1, ember-rfc176-data@^0.3.5: resolved "https://registry.yarnpkg.com/ember-rfc176-data/-/ember-rfc176-data-0.3.5.tgz#f630e550572c81a5e5c7220f864c0f06eee9e977" integrity sha512-5NfL1iTkIQDYs16/IZ7/jWCEglNsUrigLelBkBMsNcib9T3XzQwmhhVTjoSsk66s57LmWJ1bQu+2c1CAyYCV7A== +ember-rfc176-data@^0.3.8: + version "0.3.8" + resolved "https://registry.yarnpkg.com/ember-rfc176-data/-/ember-rfc176-data-0.3.8.tgz#d46bbef9a0d57c803217b258cfd2e90d8e191848" + integrity sha512-SQup3iG7SDLZNuf7nMMx5BC5truO8AYKRi80gApeQ07NsbuXV4LH75i5eOaxF0i8l9+H1tzv34kGe6rEh0C1NQ== + ember-router-generator@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/ember-router-generator/-/ember-router-generator-1.2.3.tgz#8ed2ca86ff323363120fc14278191e9e8f1315ee" From 469b7df97c6c0ae2b3b10b473902b4ecfe621a83 Mon Sep 17 00:00:00 2001 From: Pierre-Luc Carmel Biron Date: Tue, 27 Nov 2018 09:10:25 -0500 Subject: [PATCH 2/6] Deprecate legacy behavior flags --- addon/components/lt-body.js | 65 +++++++++++++++++++++++++++++---- addon/utils/deprecated-alias.js | 16 ++++++++ 2 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 addon/utils/deprecated-alias.js diff --git a/addon/components/lt-body.js b/addon/components/lt-body.js index 36867a0a..2dce1702 100644 --- a/addon/components/lt-body.js +++ b/addon/components/lt-body.js @@ -10,8 +10,11 @@ import RowExpansionBehavior from 'ember-light-table/behaviors/row-expansion'; import SingleSelectBehavior from 'ember-light-table/behaviors/single-select'; import MultiSelectBehavior from 'ember-light-table/behaviors/multi-select'; import { behaviorGroupFlag, behaviorFlag, behaviorInstanceOf } from 'ember-light-table/mixins/has-behaviors'; +import deprecatedAlias from 'ember-light-table/utils/deprecated-alias'; import Row from 'ember-light-table/classes/Row'; +const deprecationUntil = '2.0'; + /** * @module Light Table */ @@ -99,8 +102,16 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi * @property canSelect * @type {Boolean} * @default true + * @deprecated Please set the value of the `behaviors` property directly. */ - canSelect: behaviorGroupFlag('can-select'), + canSelect: deprecatedAlias( + '_canSelect', + 'canSelect', + 'Please set the value of the "behaviors" property directly.', + deprecationUntil + ), + + _canSelect: behaviorGroupFlag('can-select'), /** * Select a row on click. If this is set to `false` and multiSelect is @@ -110,8 +121,14 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi * @property selectOnClick * @type {Boolean} * @default true + * @deprecated Please set the flag directly on the `behaviors/multi-select` instance. */ - selectOnClick: computed.alias('_multiSelectBehavior.selectOnClick'), + selectOnClick: deprecatedAlias( + '_multiSelectBehavior.selectOnClick', + 'selectOnClick', + 'Please set the flag directly on the "behaviors/multi-select" instance.', + deprecationUntil + ), /** * Allows for expanding row. This will create a new row under the row that was @@ -126,8 +143,16 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi * @property canExpand * @type {Boolean} * @default false + * @deprecated Please set the value of the `behaviors` property directly. */ - canExpand: behaviorGroupFlag('can-expand'), + canExpand: deprecatedAlias( + '_canExpand', + 'canExpand', + 'Please set the value of the "behaviors" property directly.', + deprecationUntil + ), + + _canExpand: behaviorGroupFlag('can-expand'), /** * Allows a user to select multiple rows with the `ctrl`, `cmd`, and `shift` keys. @@ -136,8 +161,16 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi * @property multiSelect * @type {Boolean} * @default false + * @deprecated Please set the value of the `behaviors` property directly. */ - multiSelect: behaviorFlag('can-select', '_multiSelectBehavior'), + multiSelect: deprecatedAlias( + '_multiSelect', + 'multiSelect', + 'Please set the value of the "behaviors" property directly.', + deprecationUntil + ), + + _multiSelect: behaviorFlag('can-select', '_multiSelectBehavior'), /** * When multiSelect is true, this property determines whether or not `ctrl` @@ -149,8 +182,14 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi * @property multiSelectRequiresKeyboard * @type {Boolean} * @default true + * @deprecated Please set the flag directly on the `behaviors/multi-select` instance. */ - multiSelectRequiresKeyboard: computed.alias('_multiSelectBehavior.requiresKeyboard'), + multiSelectRequiresKeyboard: deprecatedAlias( + '_multiSelectBehavior.requiresKeyboard', + 'multiSelectRequiresKeyboard', + 'Please set the flag directly on the "behaviors/multi-select" instance.', + deprecationUntil + ), /** * Hide scrollbar when not scrolling @@ -167,8 +206,14 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi * @property multiRowExpansion * @type {Boolean} * @default true + * @deprecated Please set the flag directly on the `behaviors/row-expansion` instance. */ - multiRowExpansion: computed.alias('_rowExpansionBehavior.multiRow'), + multiRowExpansion: deprecatedAlias( + '_rowExpansionBehavior.multiRow', + 'multiRowExpansion', + 'Please set the flag directly on the "behaviors/row-expansion" instance.', + deprecationUntil + ), /** * Expand a row on click @@ -176,8 +221,14 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi * @property expandOnClick * @type {Boolean} * @default true + * @deprecated Please set the flag directly on the `behaviors/row-expansion` instance. */ - expandOnClick: computed.alias('_rowExpansionBehavior.expandOnClick'), + expandOnClick: deprecatedAlias( + '_rowExpansionBehavior.expandOnClick', + 'expandOnClick', + 'Please set the flag directly on the "behaviors/row-expansion" instance.', + deprecationUntil + ), /** * If true, the body block will yield columns and rows, allowing you diff --git a/addon/utils/deprecated-alias.js b/addon/utils/deprecated-alias.js new file mode 100644 index 00000000..6de01708 --- /dev/null +++ b/addon/utils/deprecated-alias.js @@ -0,0 +1,16 @@ +import { computed } from '@ember/object'; +import { deprecate } from '@ember/application/deprecations'; + +export default function(key, id, message, until) { + return computed(key, { + get() { + return this.get(key); + }, + set(k, value) { + deprecate(message, false, { id, until }); + this.set(key, value); + return value; + } + }); +} + From c3fdcede850310d59788875db23b21ed0475ccf2 Mon Sep 17 00:00:00 2001 From: Pierre-Luc Carmel Biron Date: Tue, 27 Nov 2018 10:31:33 -0500 Subject: [PATCH 3/6] Move the scrollable zone outside of the ember-light-table component. --- addon/components/columns/base.js | 1 + addon/components/light-table.js | 35 +-- addon/components/lt-body.js | 172 ++++++--------- addon/components/lt-fixed-foot-here.js | 11 + addon/components/lt-fixed-head-here.js | 11 + addon/components/lt-foot.js | 3 +- addon/components/lt-frame.js | 20 ++ addon/components/lt-head.js | 3 +- addon/components/lt-row.js | 6 + addon/components/lt-scrollable.js | 36 +++- addon/components/lt-standard-scrollable.js | 46 ++++ addon/helpers/lt-foot-id.js | 11 + addon/helpers/lt-head-id.js | 11 + addon/mixins/table-header.js | 27 +-- addon/styles/addon.css | 66 ++++-- addon/templates/components/columns/base.hbs | 1 + addon/templates/components/light-table.hbs | 2 - addon/templates/components/lt-body.hbs | 96 ++++----- .../components/lt-fixed-foot-here.hbs | 3 + .../components/lt-fixed-head-here.hbs | 3 + addon/templates/components/lt-foot.hbs | 2 +- addon/templates/components/lt-frame.hbs | 7 + addon/templates/components/lt-head.hbs | 4 +- addon/templates/components/lt-scrollable.hbs | 20 +- .../components/lt-standard-scrollable.hbs | 3 + app/components/lt-fixed-foot-here.js | 1 + app/components/lt-fixed-head-here.js | 1 + app/components/lt-frame.js | 1 + app/components/lt-put-foot-here.js | 9 + app/components/lt-put-head-here.js | 9 + app/components/lt-standard-scrollable.js | 1 + app/helpers/lt-foot-id.js | 1 + app/helpers/lt-head-id.js | 1 + package.json | 2 +- .../components/fixed-header-table-action.js | 10 + tests/dummy/app/components/scrolling-table.js | 2 - .../app/components/status-table-action.js | 31 +++ .../virtual-scrollbar-table-action.js | 8 + tests/dummy/app/mixins/table-common.js | 2 + tests/dummy/app/styles/app.less | 11 +- tests/dummy/app/styles/table.less | 41 ++-- .../components/columns/draggable-table.hbs | 71 ++++--- .../components/columns/grouped-table.hbs | 55 +++-- .../components/columns/resizable-table.hbs | 72 ++++--- .../components/cookbook/client-side-table.hbs | 102 +++++---- .../components/cookbook/custom-row-table.hbs | 66 +++--- .../cookbook/custom-sort-icon-table.hbs | 56 +++-- .../cookbook/horizontal-scrolling-table.hbs | 63 +++--- .../components/cookbook/occluded-table.hbs | 80 ++++--- .../components/cookbook/paginated-table.hbs | 5 +- .../cookbook/table-actions-table.hbs | 75 ++++--- .../templates/components/responsive-table.hbs | 106 ++++++---- .../components/rows/expandable-table.hbs | 63 +++--- .../components/rows/selectable-table.hbs | 64 +++--- .../templates/components/scrolling-table.hbs | 162 +++++++------- .../app/templates/components/simple-table.hbs | 66 +++--- .../components/light-table-occlusion-test.js | 192 ++++++++--------- .../components/light-table-test.js | 200 +++++++++--------- .../components/lt-body-occlusion-test.js | 70 ++++-- 59 files changed, 1360 insertions(+), 939 deletions(-) create mode 100644 addon/components/lt-fixed-foot-here.js create mode 100644 addon/components/lt-fixed-head-here.js create mode 100644 addon/components/lt-frame.js create mode 100644 addon/components/lt-standard-scrollable.js create mode 100644 addon/helpers/lt-foot-id.js create mode 100644 addon/helpers/lt-head-id.js create mode 100644 addon/templates/components/lt-fixed-foot-here.hbs create mode 100644 addon/templates/components/lt-fixed-head-here.hbs create mode 100644 addon/templates/components/lt-frame.hbs create mode 100644 addon/templates/components/lt-standard-scrollable.hbs create mode 100644 app/components/lt-fixed-foot-here.js create mode 100644 app/components/lt-fixed-head-here.js create mode 100644 app/components/lt-frame.js create mode 100644 app/components/lt-put-foot-here.js create mode 100644 app/components/lt-put-head-here.js create mode 100644 app/components/lt-standard-scrollable.js create mode 100644 app/helpers/lt-foot-id.js create mode 100644 app/helpers/lt-head-id.js create mode 100644 tests/dummy/app/components/fixed-header-table-action.js create mode 100644 tests/dummy/app/components/status-table-action.js create mode 100644 tests/dummy/app/components/virtual-scrollbar-table-action.js diff --git a/addon/components/columns/base.js b/addon/components/columns/base.js index 84c1a3a4..220ce10a 100644 --- a/addon/components/columns/base.js +++ b/addon/components/columns/base.js @@ -22,6 +22,7 @@ const Column = Component.extend(DraggableColumnMixin, { attributeBindings: ['style', 'colspan', 'rowspan'], classNameBindings: ['align', 'isGroupColumn:lt-group-column', 'isHideable', 'isSortable', 'isSorted', 'isResizable', 'isResizing', 'isDraggable', 'column.classNames'], + frameId: null, isGroupColumn: computed.readOnly('column.isGroupColumn'), isSortable: computed.readOnly('column.sortable'), isSorted: computed.readOnly('column.sorted'), diff --git a/addon/components/light-table.js b/addon/components/light-table.js index eab05033..3dd59eed 100644 --- a/addon/components/light-table.js +++ b/addon/components/light-table.js @@ -14,6 +14,15 @@ function intersections(array1, array2) { }); } +const sharedProperties = [ + 'height', + 'frameId', + 'occlusion', + 'estimatedRowHeight', + 'occlusionContainerSelector', + 'shouldRecycle' +]; + /** * @module Light Table * @main light-table @@ -37,7 +46,7 @@ function intersections(array1, array2) { const LightTable = Component.extend({ layout, - classNameBindings: [':ember-light-table', 'occlusion'], + classNameBindings: [':ember-light-table', ':lt-table-container', 'occlusion'], attributeBindings: ['style'], media: service(), @@ -159,6 +168,12 @@ const LightTable = Component.extend({ */ breakpoints: null, + /** + * This value is passed to lt-head and lt-foot so they can create a unique ids + * for ember-wormhole + */ + frameId: null, + /** * Toggles occlusion rendering functionality. Currently experimental. * If set to true, you must set {{#crossLink 't.body/estimatedRowHeight:property'}}{{/crossLink}} to @@ -191,22 +206,8 @@ const LightTable = Component.extend({ */ shouldRecycle: true, - /** - * Table component shared options - * - * @property sharedOptions - * @type {Object} - * @private - */ - sharedOptions: computed(function() { - return { - height: this.get('height'), - fixedHeader: false, - fixedFooter: false, - occlusion: this.get('occlusion'), - estimatedRowHeight: this.get('estimatedRowHeight'), - shouldRecycle: this.get('shouldRecycle') - }; + sharedOptions: computed(...sharedProperties, function() { + return this.getProperties(sharedProperties); }).readOnly(), visibleColumns: computed.readOnly('table.visibleColumns'), diff --git a/addon/components/lt-body.js b/addon/components/lt-body.js index 2dce1702..6d6092a2 100644 --- a/addon/components/lt-body.js +++ b/addon/components/lt-body.js @@ -1,6 +1,9 @@ import Component from '@ember/component'; +import { A as emberArray } from '@ember/array'; import { computed, observer } from '@ember/object'; -import { debounce, once, run } from '@ember/runloop'; +import { getOwner } from '@ember/application'; +import { debounce, run, schedule } from '@ember/runloop'; +import { warn } from '@ember/debug'; import $ from 'jquery'; import layout from 'ember-light-table/templates/components/lt-body'; import { EKMixin } from 'ember-keyboard'; @@ -11,7 +14,6 @@ import SingleSelectBehavior from 'ember-light-table/behaviors/single-select'; import MultiSelectBehavior from 'ember-light-table/behaviors/multi-select'; import { behaviorGroupFlag, behaviorFlag, behaviorInstanceOf } from 'ember-light-table/mixins/has-behaviors'; import deprecatedAlias from 'ember-light-table/utils/deprecated-alias'; -import Row from 'ember-light-table/classes/Row'; const deprecationUntil = '2.0'; @@ -290,18 +292,11 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi */ useVirtualScrollbar: false, - /** - * Set this property to scroll to a specific px offset. - * - * This only works when `useVirtualScrollbar` is `true`, i.e. when you are - * using fixed headers / footers. - * - * @property scrollTo - * @type {Number} - * @default null - */ scrollTo: null, - _scrollTo: null, + + _onScrollTo: observer('scrollTo', function() { + warn('Property "scrollTo" is not supported anymore, please use lt-scrollable directly instead.'); + }), /** * Set this property to a `Row` to scroll that `Row` into view. @@ -314,7 +309,18 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi * @default null */ scrollToRow: null, - _scrollToRow: null, + + _onScrollToRow: observer('scrollToRow', function() { + let row = this.get('scrollToRow'); + if (row) { + let ltRow = this.get('ltRows').findBy('row', row); + if (ltRow) { + schedule('afterRender', () => this.makeRowVisible(ltRow.$())); + } else { + throw 'Row passed to scrollToRow() is not part of the rendered table.'; + } + } + }), /** * @property targetScrollOffset @@ -401,22 +407,19 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi _prevSelectedIndex: -1, + scrollableContainer: computed('sharedOptions.frameId', function() { + // TODO: FIX: lt-body should not know about .tse-scroll-content + const id = this.get('sharedOptions.frameId'); + return `#${id} .tse-scroll-content, #${id} .lt-scrollable`; + }), + init() { this._super(...arguments); - - /* - We can only set `useVirtualScrollbar` once all contextual components have - been initialized since fixedHeader and fixedFooter are set on t.head and t.foot - initialization. - */ - once(this, this._setupVirtualScrollbar); - this._initDefaultBehaviorsIfNeeded(); }, didReceiveAttrs() { this._super(...arguments); - this.setupScrollOffset(); }, destroy() { @@ -452,75 +455,38 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi } }, - _setupVirtualScrollbar() { - let { fixedHeader, fixedFooter } = this.get('sharedOptions'); - this.set('useVirtualScrollbar', fixedHeader || fixedFooter); + makeRowAtVisible(i, nbExtraRows = 0) { + this.makeRowVisible(this.get('ltRows').objectAt(i).$(), nbExtraRows); }, - onRowsChange: observer('rows.[]', function() { - this._checkTargetOffsetTimer = run.scheduleOnce('afterRender', this, this.checkTargetScrollOffset); - }), - - setupScrollOffset() { - let { - scrollTo, - _scrollTo, - scrollToRow, - _scrollToRow - } = this.getProperties(['scrollTo', '_scrollTo', 'scrollToRow', '_scrollToRow']); - let targetScrollOffset = null; - - this.setProperties({ _scrollTo: scrollTo, _scrollToRow: scrollToRow }); - - if (scrollTo !== _scrollTo) { - targetScrollOffset = Number.parseInt(scrollTo, 10); - - if (Number.isNaN(targetScrollOffset)) { - targetScrollOffset = null; - } - - this.setProperties({ - targetScrollOffset, - hasReachedTargetScrollOffset: targetScrollOffset <= 0 - }); - } else if (scrollToRow !== _scrollToRow) { - if (scrollToRow instanceof Row) { - let rowElement = this.element.querySelector(`[data-row-id=${scrollToRow.get('rowId')}]`); - - if (rowElement instanceof Element) { - targetScrollOffset = rowElement.offsetTop; + $scrollableContainer: computed(function() { + return this.$().parents('.lt-scrollable'); + }).volatile().readOnly(), + + $scrollableContent: computed(function() { + return this.$().parents('.scrollable-content'); + }).volatile().readOnly(), + + makeRowVisible($row, nbExtraRows = 0) { + let $scrollableContent = this.get('$scrollableContent'); + let $scrollableContainer = this.get('$scrollableContainer'); + if ($row.length !== 0 && $scrollableContent.length !== 0 && $scrollableContainer.length !== 0) { + let rt = $row.offset().top - $scrollableContent.offset().top; + let rh = $row.height(); + let rb = rt + rh; + let h = $scrollableContainer.height(); + let t = this.get('scrollTop'); + let b = t + h; + let extraSpace = rh * nbExtraRows; + if (rt - extraSpace <= t) { + if (this.onScrollTo) { + this.onScrollTo(rt - extraSpace); + } + } else if (rb + extraSpace >= b) { + if (this.onScrollTo) { + this.onScrollTo(t + rb - b + extraSpace); } } - - this.setProperties({ targetScrollOffset, hasReachedTargetScrollOffset: true }); - } - }, - - checkTargetScrollOffset() { - if (!this.get('hasReachedTargetScrollOffset')) { - let targetScrollOffset = this.get('targetScrollOffset'); - let currentScrollOffset = this.get('currentScrollOffset'); - - if (targetScrollOffset > currentScrollOffset) { - this.set('targetScrollOffset', null); - this._setTargetOffsetTimer = run.schedule('render', null, () => { - this.set('targetScrollOffset', targetScrollOffset); - }); - } else { - this.set('hasReachedTargetScrollOffset', true); - } - } - }, - - toggleExpandedRow(row) { - let multiRowExpansion = this.get('multiRowExpansion'); - let shouldExpand = !row.expanded; - - if (multiRowExpansion) { - row.toggleProperty('expanded'); - } else { - this.get('table.expandedRows').setEach('expanded', false); - row.set('expanded', shouldExpand); } }, @@ -544,6 +510,13 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi run.cancel(this._schedulerTimer); run.cancel(this._debounceTimer); }, + + ltRows: computed(function() { + let vrm = getOwner(this).lookup('-view-registry:main'); + let q = this.$('tr:not(.lt-expanded-row)'); + return emberArray($.makeArray(q.map((i, e) => vrm[e.id]))); + }).volatile(), + signalSelectionChanged() { this.get('behaviors').forEach((b) => b.onSelectionChanged(this)); }, @@ -629,21 +602,6 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi } }, - /** - * onScroll action - sent when user scrolls in the Y direction - * - * This only works when `useVirtualScrollbar` is `true`, i.e. when you are - * using fixed headers / footers. - * - * @event onScroll - * @param {Number} scrollOffset The scroll offset in px - * @param {Event} event The scroll event - */ - onScroll(scrollOffset /* , event */) { - this.set('currentScrollOffset', scrollOffset); - this.onScroll(...arguments); - }, - /** * lt-infinity action to determine if component is still in viewport * @event inViewport @@ -659,10 +617,10 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi this.set('isInViewport', false); }, - firstVisibleChanged(item, index /* , key */) { - this.firstVisibleChanged(...arguments); - const estimateScrollOffset = index * this.get('sharedOptions.estimatedRowHeight'); - this.onScroll(estimateScrollOffset, null); + firstVisibleChanged(/* item, index, key */) { + if (this.firstVisibleChanged) { + this.firstVisibleChanged(...arguments); + } }, lastVisibleChanged(/* item, index, key */) { diff --git a/addon/components/lt-fixed-foot-here.js b/addon/components/lt-fixed-foot-here.js new file mode 100644 index 00000000..00052a31 --- /dev/null +++ b/addon/components/lt-fixed-foot-here.js @@ -0,0 +1,11 @@ +import Component from '@ember/component'; + +import layout from 'ember-light-table/templates/components/lt-fixed-foot-here'; + +export default Component.extend({ + layout, + tagName: '', + + // passed in + frameId: '' +}); diff --git a/addon/components/lt-fixed-head-here.js b/addon/components/lt-fixed-head-here.js new file mode 100644 index 00000000..caa03f40 --- /dev/null +++ b/addon/components/lt-fixed-head-here.js @@ -0,0 +1,11 @@ +import Component from '@ember/component'; + +import layout from 'ember-light-table/templates/components/lt-fixed-head-here'; + +export default Component.extend({ + layout, + tagName: '', + + // passed in + frameId: '' +}); diff --git a/addon/components/lt-foot.js b/addon/components/lt-foot.js index 81edbd8f..6398b4d9 100644 --- a/addon/components/lt-foot.js +++ b/addon/components/lt-foot.js @@ -34,6 +34,5 @@ export default Component.extend(TableHeaderMixin, { layout, classNames: ['lt-foot-wrap'], table: null, - sharedOptions: null, - sharedOptionsFixedKey: 'fixedFooter' + sharedOptions: null }); diff --git a/addon/components/lt-frame.js b/addon/components/lt-frame.js new file mode 100644 index 00000000..bebb3ac0 --- /dev/null +++ b/addon/components/lt-frame.js @@ -0,0 +1,20 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import cssStyleify from 'ember-light-table/utils/css-styleify'; +import layout from 'ember-light-table/templates/components/lt-frame'; + +export default Component.extend({ + layout, + + classNames: ['lt-frame'], + + attributeBindings: ['style'], + + height: null, + scrollbar: 'standard', + + style: computed('height', function() { + return cssStyleify(this.getProperties(['height'])); + }) + +}); diff --git a/addon/components/lt-head.js b/addon/components/lt-head.js index 5b36f8cc..5152fb17 100644 --- a/addon/components/lt-head.js +++ b/addon/components/lt-head.js @@ -36,6 +36,5 @@ export default Component.extend(TableHeaderMixin, { layout, classNames: ['lt-head-wrap'], table: null, - sharedOptions: null, - sharedOptionsFixedKey: 'fixedHeader' + sharedOptions: null }); diff --git a/addon/components/lt-row.js b/addon/components/lt-row.js index 0da4342d..01d1fefc 100644 --- a/addon/components/lt-row.js +++ b/addon/components/lt-row.js @@ -22,6 +22,12 @@ const Row = Component.extend({ isSelected: computed.readOnly('row.selected'), isExpanded: computed.readOnly('row.expanded'), + ltBody: null, + + $ltBody: computed(function() { + return this.get('ltBody').$(); + }).volatile().readOnly(), + _onClick: on('click', function() { if (this.rowClick) { this.rowClick(this, ...arguments); diff --git a/addon/components/lt-scrollable.js b/addon/components/lt-scrollable.js index 4d7bd00f..96ffd6ce 100644 --- a/addon/components/lt-scrollable.js +++ b/addon/components/lt-scrollable.js @@ -1,9 +1,43 @@ import Component from '@ember/component'; +import { computed } from '@ember/object'; import layout from '../templates/components/lt-scrollable'; export default Component.extend({ layout, tagName: '', + + // passed in + virtual: false, vertical: true, - horizontal: false + horizontal: false, + autoHide: true, // only for virtual=true + classNames: ['lt-scrollable'], + + scrollTop: computed('_scrollTopGet', { + get() { + return this.get('_scrollTopGet'); + }, + set(key, value) { + this.set('_scrollTopSet', value); + return value; + } + }), + + _scrollTopSet: null, + _scrollTopGet: 0, + + actions: { + + onScroll(scrollTop) { + this.set('_scrollTopGet', scrollTop); + if (this.onScroll) { + this.onScroll(...arguments); + } + }, + + onScrollTo(y) { + this.set('_scrollTopSet', y); + } + + } }); diff --git a/addon/components/lt-standard-scrollable.js b/addon/components/lt-standard-scrollable.js new file mode 100644 index 00000000..590902bc --- /dev/null +++ b/addon/components/lt-standard-scrollable.js @@ -0,0 +1,46 @@ +import Component from '@ember/component'; +import { computed, observer } from '@ember/object'; +import layout from '../templates/components/lt-standard-scrollable'; +import cssStyleify from 'ember-light-table/utils/css-styleify'; + +export default Component.extend({ + + layout, + + classNames: ['lt-standard-scrollable'], + attributeBindings: ['tabIndex', 'style'], + + tabIndex: -1, + height: null, + + style: computed('height', function() { + return cssStyleify(this.getProperties(['height'])); + }), + + // passed in + scrollToY: null, + onScroll: null, + + init() { + this._super(...arguments); + this.get('scrollToY'); + }, + + didInsertElement() { + this.$().on('scroll', (evt) => this._onScroll(evt)); + }, + + _onScroll(event) { + let $ = this.$(); + if ($) { + if (this.onScroll) { + this.onScroll($.scrollTop(), event); + } + } + }, + + _onScrollToY: observer('scrollToY', function() { + this.$().scrollTop(this.get('scrollToY')); + }) + +}); diff --git a/addon/helpers/lt-foot-id.js b/addon/helpers/lt-foot-id.js new file mode 100644 index 00000000..c0c35443 --- /dev/null +++ b/addon/helpers/lt-foot-id.js @@ -0,0 +1,11 @@ +import Helper from '@ember/component/helper'; + +export default Helper.extend({ + + compute(params) { + return params.length == 0 && params[0] + ? 'lt-foot-position' + : `lt-foot-position-${params[0]}`; + } + +}); diff --git a/addon/helpers/lt-head-id.js b/addon/helpers/lt-head-id.js new file mode 100644 index 00000000..7d9e0f67 --- /dev/null +++ b/addon/helpers/lt-head-id.js @@ -0,0 +1,11 @@ +import Helper from '@ember/component/helper'; + +export default Helper.extend({ + + compute(params) { + return params.length == 0 && params[0] + ? 'lt-head-position' + : `lt-head-position-${params[0]}`; + } + +}); diff --git a/addon/mixins/table-header.js b/addon/mixins/table-header.js index 797fbe08..7cf80057 100644 --- a/addon/mixins/table-header.js +++ b/addon/mixins/table-header.js @@ -1,7 +1,5 @@ import Mixin from '@ember/object/mixin'; -import { computed, trySet } from '@ember/object'; -import { isEmpty } from '@ember/utils'; -import { warn } from '@ember/debug'; +import { computed } from '@ember/object'; import { inject as service } from '@ember/service'; import cssStyleify from 'ember-light-table/utils/css-styleify'; @@ -122,12 +120,13 @@ export default Mixin.create({ iconComponent: null, /** - * ID of main table component. Used to generate divs for ember-wormhole + * Id used to figure out where to render the content + * @property to * @type {String} */ - tableId: null, + frameId: null, - renderInPlace: computed.oneWay('fixed'), + renderInPlace: computed.not('fixed'), columnGroups: computed.readOnly('table.visibleColumnGroups'), subColumns: computed.readOnly('table.visibleSubColumns'), columns: computed.readOnly('table.visibleColumns'), @@ -143,22 +142,6 @@ export default Mixin.create({ } }).readOnly(), - init() { - this._super(...arguments); - - const fixed = this.get('fixed'); - const sharedOptionsFixedPath = `sharedOptions.${this.get('sharedOptionsFixedKey')}`; - trySet(this, sharedOptionsFixedPath, fixed); - - const height = this.get('sharedOptions.height'); - - warn( - 'You did not set a `height` attribute for your table, but marked a header or footer to be fixed. This means that you have to set the table height via CSS. For more information please refer to: https://github.com/offirgolan/ember-light-table/issues/446', - !fixed || fixed && !isEmpty(height), - { id: 'ember-light-table.height-attribute' } - ); - }, - actions: { /** * onColumnClick action. Handles column sorting. diff --git a/addon/styles/addon.css b/addon/styles/addon.css index d3493c95..b61d654e 100644 --- a/addon/styles/addon.css +++ b/addon/styles/addon.css @@ -1,6 +1,6 @@ -.ember-light-table { +.lt-table-container { height: inherit; - overflow: auto; + overflow: hidden; display: -webkit-box; display: -ms-flexbox; display: flex; @@ -10,14 +10,14 @@ flex-direction: column; } -.ember-light-table table { +.lt-table-container table { table-layout: fixed; border-collapse: collapse; width: 100%; box-sizing: border-box; } -.ember-light-table .lt-scaffolding { +td.lt-scaffolding { border: none; padding-top: 0; padding-bottom: 0; @@ -27,8 +27,8 @@ visibility: hidden; } -.ember-light-table .lt-head-wrap, -.ember-light-table .lt-foot-wrap { +.lt-head-wrap, +.lt-foot-wrap { overflow-y: auto; overflow-x: hidden; @@ -50,27 +50,25 @@ -webkit-box-flex: 1; -ms-flex: 1 1 auto; flex: 1 1 auto; + position: relative; } -.ember-light-table .lt-column { +.lt-table-container .lt-column { position: relative; } -.ember-light-table .lt-scrollable { +.lt-scrollable { width: 100%; + overflow-y: auto; -webkit-box-flex: 1; -ms-flex: 1 1 auto; flex: 1 1 auto; } -.lt-infinity { +.ember-light-table .lt-infinity { min-height: 1px; } -.ember-light-table .lt-scrollable.vertical-collection { - overflow-y: auto; -} - .ember-light-table vertical-collection { width: 100%; display: table; @@ -79,26 +77,31 @@ .ember-light-table vertical-collection occluded-content:first-of-type { display: table-caption; + position: relative; } -.ember-light-table .align-left { +.lt-standard-scrollable { + overflow: auto; +} + +.lt-table-container .align-left { text-align: left; } -.ember-light-table .align-right { +.lt-table-container .align-right { text-align: right; } -.ember-light-table .align-center { +.lt-table-container .align-center { text-align: center; } -.ember-light-table .lt-column .lt-sort-icon { +.lt-column .lt-sort-icon { float: right; } -.ember-light-table .lt-column.is-draggable, -.ember-light-table .lt-column.is-sortable { +.lt-column.is-draggable, +.lt-column.is-sortable { cursor: pointer; -webkit-user-select: none; -moz-user-select: none; @@ -106,7 +109,7 @@ user-select: none; } -.ember-light-table .lt-column.is-resizing { +.lt-column.is-resizing { pointer-events: none; } @@ -114,7 +117,7 @@ cursor: col-resize; } -.ember-light-table .lt-column .lt-column-resizer { +.lt-column .lt-column-resizer { width: 5px; cursor: col-resize; height: 100%; @@ -124,7 +127,24 @@ top: 0; } -.ember-light-table .lt-row.is-expandable, -.ember-light-table .lt-row.is-selectable { +.lt-row.is-expandable, +.lt-row.is-selectable { cursor: pointer; } + +.lt-scrollable { + width: 100%; + position: relative; +} + +.lt-standard-scrollable { + overflow: auto; +} + +.scrollable-content { + position: relative; +} + +.lt-frame .lt-standard-scrollable { + height: 'auto'; +} diff --git a/addon/templates/components/columns/base.hbs b/addon/templates/components/columns/base.hbs index 22cd102e..32b252b5 100644 --- a/addon/templates/components/columns/base.hbs +++ b/addon/templates/components/columns/base.hbs @@ -11,6 +11,7 @@ {{#if isResizable}} {{lt-column-resizer + frameId=frameId column=column table=table resizeOnDrag=resizeOnDrag diff --git a/addon/templates/components/light-table.hbs b/addon/templates/components/light-table.hbs index 3b3e82bb..a7bdbed4 100644 --- a/addon/templates/components/light-table.hbs +++ b/addon/templates/components/light-table.hbs @@ -1,6 +1,5 @@ {{yield (hash head=(component 'lt-head' - tableId=elementId table=table tableActions=tableActions extra=extra @@ -8,7 +7,6 @@ sharedOptions=sharedOptions ) body=(component 'lt-body' - tableId=elementId table=table tableActions=tableActions extra=extra diff --git a/addon/templates/components/lt-body.hbs b/addon/templates/components/lt-body.hbs index 437ccfda..125330ae 100644 --- a/addon/templates/components/lt-body.hbs +++ b/addon/templates/components/lt-body.hbs @@ -5,17 +5,8 @@ ) as |lt| }} {{#unless sharedOptions.occlusion}} - {{#lt-scrollable - tagName='' - virtualScrollbar=useVirtualScrollbar - autoHide=autoHideScrollbar - scrollTo=targetScrollOffset - onScrollY=(action 'onScroll') - }} -
- - - +
+ {{#if enableScaffolding}} {{#each columns as |column|}} @@ -29,54 +20,51 @@ {{else}} {{#each rows as |row|}} {{lt.row row columns - data-row-id=row.rowId - table=table - tableActions=tableActions - extra=extra - enableScaffolding=enableScaffolding - canExpand=canExpand - canSelect=canSelect - rowClick=(action 'onRowClick') - rowDoubleClick=(action 'onRowDoubleClick') - rowMouseDown=(action 'onRowMouseDown') - rowMouseUp=(action 'onRowMouseUp') - rowMouseMove=(action 'onRowMouseMove') - rowTouchStart=(action 'onRowTouchStart') - rowTouchEnd=(action 'onRowTouchEnd') - rowTouchCancel=(action 'onRowTouchCancel') - rowTouchLeave=(action 'onRowTouchLeave') - rowTouchMove=(action 'onRowTouchMove')}} - + data-row-id=row.rowId + ltBody=this + tableActions=tableActions + extra=extra + enableScaffolding=enableScaffolding + canExpand=canExpand + canSelect=canSelect + rowClick=(action 'onRowClick') + rowDoubleClick=(action 'onRowDoubleClick') + rowMouseDown=(action 'onRowMouseDown') + rowMouseUp=(action 'onRowMouseUp') + rowMouseMove=(action 'onRowMouseMove') + rowTouchStart=(action 'onRowTouchStart') + rowTouchEnd=(action 'onRowTouchEnd') + rowTouchCancel=(action 'onRowTouchCancel') + rowTouchLeave=(action 'onRowTouchLeave') + rowTouchMove=(action 'onRowTouchMove') + }} {{yield (hash - expanded-row=(component lt.spanned-row classes='lt-expanded-row' colspan=colspan yield=row visible=row.expanded) - loader=(component lt.spanned-row visible=false) - no-data=(component lt.spanned-row visible=false) - ) rows}} + expanded-row=(component lt.spanned-row classes='lt-expanded-row' colspan=colspan yield=row visible=row.expanded) + loader=(component lt.spanned-row visible=false) + no-data=(component lt.spanned-row visible=false) + ) rows}} {{/each}} {{yield (hash - loader=(component lt.spanned-row classes='lt-is-loading' colspan=colspan) - no-data=(component lt.spanned-row classes='lt-no-data' colspan=colspan) - expanded-row=(component lt.spanned-row visible=false) - ) rows}} + loader=(component lt.spanned-row classes='lt-is-loading' colspan=colspan) + no-data=(component lt.spanned-row classes='lt-no-data' colspan=colspan) + expanded-row=(component lt.spanned-row visible=false) + ) rows}} {{/if}} - -
+ + - {{#if onScrolledToBottom}} - {{lt.infinity - rows=rows - inViewport=(action "inViewport") - exitViewport=(action "exitViewport") - scrollableContent=".lt-scrollable"}} - {{/if}} - -
- {{/lt-scrollable}} + {{#if onScrolledToBottom}} + {{lt.infinity + rows=rows + inViewport=(action "inViewport") + exitViewport=(action "exitViewport") + scrollableContent=scrollableContainer + scrollBuffer=scrollBuffer + }} + {{/if}} {{else}} -
-
- +
{{#if overwrite}} @@ -88,7 +76,7 @@ estimateHeight=sharedOptions.estimatedRowHeight shouldRecycle=sharedOptions.shouldRecycle bufferSize=scrollBufferRows - containerSelector='.lt-scrollable' + containerSelector=scrollableContainer firstVisibleChanged=(action 'firstVisibleChanged') lastVisibleChanged=(action 'lastVisibleChanged') firstReached=(action 'firstReached') @@ -126,8 +114,6 @@ {{/if}}
- -
{{/unless}} {{/with}} diff --git a/addon/templates/components/lt-fixed-foot-here.hbs b/addon/templates/components/lt-fixed-foot-here.hbs new file mode 100644 index 00000000..8a436f17 --- /dev/null +++ b/addon/templates/components/lt-fixed-foot-here.hbs @@ -0,0 +1,3 @@ +
+ +
diff --git a/addon/templates/components/lt-fixed-head-here.hbs b/addon/templates/components/lt-fixed-head-here.hbs new file mode 100644 index 00000000..5cb94fb1 --- /dev/null +++ b/addon/templates/components/lt-fixed-head-here.hbs @@ -0,0 +1,3 @@ +
+ +
diff --git a/addon/templates/components/lt-foot.hbs b/addon/templates/components/lt-foot.hbs index cd37def4..0301ebc8 100644 --- a/addon/templates/components/lt-foot.hbs +++ b/addon/templates/components/lt-foot.hbs @@ -1,4 +1,4 @@ -{{#ember-wormhole to=(concat tableId '_inline_foot') renderInPlace=renderInPlace}} +{{#ember-wormhole to=(lt-foot-id sharedOptions.frameId) renderInPlace=renderInPlace}} {{!-- Scaffolding is needed here to allow use of colspan in the footer --}} diff --git a/addon/templates/components/lt-frame.hbs b/addon/templates/components/lt-frame.hbs new file mode 100644 index 00000000..0c241d7e --- /dev/null +++ b/addon/templates/components/lt-frame.hbs @@ -0,0 +1,7 @@ +{{yield (hash + fixed-head-here=(component 'lt-fixed-head-here' frameId=elementId) + fixed-foot-here=(component 'lt-fixed-foot-here' frameId=elementId) + table=(component 'light-table' frameId=elementId) + scrollable-zone=(component 'lt-scrollable' virtual=(eq scrollbar 'virtual')) + frameId=elementId +)}} diff --git a/addon/templates/components/lt-head.hbs b/addon/templates/components/lt-head.hbs index 33e72973..fae871ea 100644 --- a/addon/templates/components/lt-head.hbs +++ b/addon/templates/components/lt-head.hbs @@ -1,4 +1,4 @@ -{{#ember-wormhole to=(concat tableId '_inline_head') renderInPlace=renderInPlace}} +{{#ember-wormhole to=(lt-head-id sharedOptions.frameId) renderInPlace=renderInPlace}}
{{#if hasBlock}} @@ -19,6 +19,7 @@ {{#each columnGroups as |column|}} {{component (concat 'light-table/columns/' column.type) column + frameId=sharedOptions.frameId table=table tableActions=tableActions extra=extra @@ -35,6 +36,7 @@ {{#each subColumns as |column|}} {{component (concat 'light-table/columns/' column.type) column + frameId=sharedOptions.frameId table=table rowspan=1 classNames="lt-sub-column" diff --git a/addon/templates/components/lt-scrollable.hbs b/addon/templates/components/lt-scrollable.hbs index 3d719289..a690bcdb 100644 --- a/addon/templates/components/lt-scrollable.hbs +++ b/addon/templates/components/lt-scrollable.hbs @@ -1,15 +1,21 @@ -{{#if virtualScrollbar}} +{{#if virtual}} {{#ember-scrollable - classNames='lt-scrollable' + classNames=classNames autoHide=autoHide horizontal=horizontal vertical=vertical - scrollToY=scrollTo - onScrollY=onScrollY - as |scrollbar| + scrollToY=_scrollTopSet + onScrollY=(action 'onScroll') }} - {{yield}} + {{yield (hash scrollTop=_scrollTopGet onScrollTo=(action 'onScrollTo'))}} {{/ember-scrollable}} {{else}} - {{yield}} + {{#lt-standard-scrollable + height=height + classNames=classNames + scrollToY=_scrollTopSet + onScroll=(action 'onScroll') + }} + {{yield (hash scrollTop=_scrollTopGet onScrollTo=(action 'onScrollTo'))}} + {{/lt-standard-scrollable}} {{/if}} diff --git a/addon/templates/components/lt-standard-scrollable.hbs b/addon/templates/components/lt-standard-scrollable.hbs new file mode 100644 index 00000000..1964a0c4 --- /dev/null +++ b/addon/templates/components/lt-standard-scrollable.hbs @@ -0,0 +1,3 @@ +
+ {{yield}} +
diff --git a/app/components/lt-fixed-foot-here.js b/app/components/lt-fixed-foot-here.js new file mode 100644 index 00000000..d474fe96 --- /dev/null +++ b/app/components/lt-fixed-foot-here.js @@ -0,0 +1 @@ +export { default } from 'ember-light-table/components/lt-fixed-foot-here'; diff --git a/app/components/lt-fixed-head-here.js b/app/components/lt-fixed-head-here.js new file mode 100644 index 00000000..d01131d8 --- /dev/null +++ b/app/components/lt-fixed-head-here.js @@ -0,0 +1 @@ +export { default } from 'ember-light-table/components/lt-fixed-head-here'; diff --git a/app/components/lt-frame.js b/app/components/lt-frame.js new file mode 100644 index 00000000..79c06df8 --- /dev/null +++ b/app/components/lt-frame.js @@ -0,0 +1 @@ +export { default } from 'ember-light-table/components/lt-frame'; diff --git a/app/components/lt-put-foot-here.js b/app/components/lt-put-foot-here.js new file mode 100644 index 00000000..2232bcac --- /dev/null +++ b/app/components/lt-put-foot-here.js @@ -0,0 +1,9 @@ +import Component from '@ember/component'; + +export default Component.extend({ + + tagName: '', + + frameId: null + +}); diff --git a/app/components/lt-put-head-here.js b/app/components/lt-put-head-here.js new file mode 100644 index 00000000..2232bcac --- /dev/null +++ b/app/components/lt-put-head-here.js @@ -0,0 +1,9 @@ +import Component from '@ember/component'; + +export default Component.extend({ + + tagName: '', + + frameId: null + +}); diff --git a/app/components/lt-standard-scrollable.js b/app/components/lt-standard-scrollable.js new file mode 100644 index 00000000..30f075c1 --- /dev/null +++ b/app/components/lt-standard-scrollable.js @@ -0,0 +1 @@ +export { default } from 'ember-light-table/components/lt-standard-scrollable'; diff --git a/app/helpers/lt-foot-id.js b/app/helpers/lt-foot-id.js new file mode 100644 index 00000000..00470b13 --- /dev/null +++ b/app/helpers/lt-foot-id.js @@ -0,0 +1 @@ +export { default } from 'ember-light-table/helpers/lt-foot-id'; diff --git a/app/helpers/lt-head-id.js b/app/helpers/lt-head-id.js new file mode 100644 index 00000000..a4003b0b --- /dev/null +++ b/app/helpers/lt-head-id.js @@ -0,0 +1 @@ +export { default } from 'ember-light-table/helpers/lt-head-id'; diff --git a/package.json b/package.json index 682d6bf5..7bb46adc 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "ember-in-viewport": "~3.0.3", "ember-scrollable": "^0.5.2", "ember-truth-helpers": "^2.0.0", - "ember-wormhole": "^0.5.4" + "ember-wormhole": "^0.5.5" }, "devDependencies": { "broccoli-asset-rev": "^2.7.0", diff --git a/tests/dummy/app/components/fixed-header-table-action.js b/tests/dummy/app/components/fixed-header-table-action.js new file mode 100644 index 00000000..cfcb398a --- /dev/null +++ b/tests/dummy/app/components/fixed-header-table-action.js @@ -0,0 +1,10 @@ +import StatusTableActionComponent from './status-table-action'; + +export default StatusTableActionComponent.extend({ + + statusClassOn: 'fa-lock', + statusClassOff: 'fa-unlock', + titleOn: 'Header is fixed', + titleOff: 'Header is inlined' + +}); diff --git a/tests/dummy/app/components/scrolling-table.js b/tests/dummy/app/components/scrolling-table.js index 1d839ac2..b0f33deb 100644 --- a/tests/dummy/app/components/scrolling-table.js +++ b/tests/dummy/app/components/scrolling-table.js @@ -4,8 +4,6 @@ import TableCommon from '../mixins/table-common'; import { computed } from '@ember/object'; export default Component.extend(TableCommon, { - currentScrollOffset: 0, - scrollTo: 0, scrollToRow: null, columns: computed(function() { diff --git a/tests/dummy/app/components/status-table-action.js b/tests/dummy/app/components/status-table-action.js new file mode 100644 index 00000000..f06ab737 --- /dev/null +++ b/tests/dummy/app/components/status-table-action.js @@ -0,0 +1,31 @@ +/* eslint ember/no-on-calls-in-components:off */ +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import { on } from '@ember/object/evented'; + +export default Component.extend({ + + classNameBindings: [':table-action', ':fa', 'statusClass'], + attributeBindings: ['title'], + + statusClassOn: 'fa-toggle-on', + statusClassOff: 'fa-toggle-off', + + titleOn: null, + titleOff: null, + + value: false, + + statusClass: computed('value', function() { + return this.get('value') ? this.get('statusClassOn') : this.get('statusClassOff'); + }), + + title: computed('value', function() { + return this.get('value') ? this.get('titleOn') : this.get('titleOff'); + }), + + _onClick: on('click', function() { + this.onChange(!this.get('value')); + }) + +}); diff --git a/tests/dummy/app/components/virtual-scrollbar-table-action.js b/tests/dummy/app/components/virtual-scrollbar-table-action.js new file mode 100644 index 00000000..f6797826 --- /dev/null +++ b/tests/dummy/app/components/virtual-scrollbar-table-action.js @@ -0,0 +1,8 @@ +import StatusTableActionComponent from './status-table-action'; + +export default StatusTableActionComponent.extend({ + + titleOn: 'Scrollbar is virtual', + titleOff: 'Scrollbar is standard' + +}); diff --git a/tests/dummy/app/mixins/table-common.js b/tests/dummy/app/mixins/table-common.js index 99ab911b..2fe0f3f6 100644 --- a/tests/dummy/app/mixins/table-common.js +++ b/tests/dummy/app/mixins/table-common.js @@ -17,6 +17,8 @@ export default Mixin.create({ isLoading: computed.oneWay('fetchRecords.isRunning'), canLoadMore: true, enableSync: true, + fixed: true, + scrollbar: 'standard', model: null, meta: null, diff --git a/tests/dummy/app/styles/app.less b/tests/dummy/app/styles/app.less index b16559a5..85e43234 100644 --- a/tests/dummy/app/styles/app.less +++ b/tests/dummy/app/styles/app.less @@ -143,13 +143,7 @@ label { .table-container { overflow-y: auto; - - &.fixed-header { - overflow-y: hidden; - margin-bottom: 15px; - } } - .user-actions { a { color: @accent-color; @@ -218,3 +212,8 @@ form .form-group { } } } + +.lt-frame { + display: flex; + flex-flow: column nowrap; +} diff --git a/tests/dummy/app/styles/table.less b/tests/dummy/app/styles/table.less index e248e005..6c9eb667 100644 --- a/tests/dummy/app/styles/table.less +++ b/tests/dummy/app/styles/table.less @@ -1,12 +1,21 @@ @border-color: #DADADA; -.ember-light-table { +.lt-standard-scrollable { + flex: 1 0 0; +} + +.lt-table-container { + width: 95%; margin: 0 auto; - border-collapse: collapse; font-family: 'Open Sans', sans-serif; - .multi-select { + &.lt-fixed-head-here, &.lt-fixed-foot-here { + flex: 0 0 auto; + height: auto; + } + + .lt-body-wrap { -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; @@ -15,32 +24,6 @@ user-select: none; } - tfoot .lt-column { - border-top: 1px solid @border-color; - } - - thead .lt-column { - border-bottom: 1px solid @border-color; - } - - thead th, - tfoot th { - &.is-dragging { - opacity: 0.75; - background: #eee; - } - - &.is-drag-target { - &.drag-right { - border-right: 1px dotted @border-color; - } - - &.drag-left { - border-left: 1px dotted @border-color; - } - } - } - .lt-column { font-weight: 200; font-size: 12px; diff --git a/tests/dummy/app/templates/components/columns/draggable-table.hbs b/tests/dummy/app/templates/components/columns/draggable-table.hbs index ad3573bf..5f319d66 100644 --- a/tests/dummy/app/templates/components/columns/draggable-table.hbs +++ b/tests/dummy/app/templates/components/columns/draggable-table.hbs @@ -1,31 +1,48 @@ {{!-- BEGIN-SNIPPET draggable-table --}} -{{#light-table table height='65vh' as |t|}} - {{t.head - onColumnClick=(action 'onColumnClick') - iconSortable='fa fa-sort' - iconAscending='fa fa-sort-asc' - iconDescending='fa fa-sort-desc' - fixed=true - }} +
+ {{fixed-header-table-action value=fixed onChange=(action (mut fixed))}} + {{virtual-scrollbar-table-action value=virtual onChange=(action (mut virtual))}} +
- {{#t.body - useLegacyBehaviorFlags=false - onScrolledToBottom=(action 'onScrolledToBottom') - as |body| - }} - {{#if isLoading}} - {{#body.loader}} - {{table-loader}} - {{/body.loader}} - {{/if}} - {{/t.body}} +{{#lt-frame + height='65vh' + scrollbar=(if virtual 'virtual' 'standard') + as |frame| +}} + {{frame.fixed-head-here}} - {{#t.foot fixed=true as |columns|}} - - - - {{/t.foot}} -{{/light-table}} + {{#frame.scrollable-zone}} + {{#frame.table table as |t|}} + {{t.head + fixed=fixed + onColumnClick=(action 'onColumnClick') + iconSortable='fa fa-sort' + iconAscending='fa fa-sort-asc' + iconDescending='fa fa-sort-desc' + }} + + {{#t.body + useLegacyBehaviorFlags=false + onScrolledToBottom=(action 'onScrolledToBottom') + as |body| + }} + {{#if isLoading}} + {{#body.loader}} + {{table-loader}} + {{/body.loader}} + {{/if}} + {{/t.body}} + + {{#t.foot fixed=true as |columns|}} + + + + {{/t.foot}} + {{/frame.table}} + {{/frame.scrollable-zone}} + + {{frame.fixed-foot-here}} +{{/lt-frame}} {{!-- END-SNIPPET --}} diff --git a/tests/dummy/app/templates/components/columns/grouped-table.hbs b/tests/dummy/app/templates/components/columns/grouped-table.hbs index 5012df29..0102f421 100644 --- a/tests/dummy/app/templates/components/columns/grouped-table.hbs +++ b/tests/dummy/app/templates/components/columns/grouped-table.hbs @@ -1,23 +1,38 @@ {{!-- BEGIN-SNIPPET grouped-table --}} -{{#light-table table height='65vh' as |t|}} - {{t.head - onColumnClick=(action 'onColumnClick') - iconSortable='fa fa-sort' - iconAscending='fa fa-sort-asc' - iconDescending='fa fa-sort-desc' - fixed=true - }} +
+ {{fixed-header-table-action value=fixed onChange=(action (mut fixed))}} + {{virtual-scrollbar-table-action value=virtual onChange=(action (mut virtual))}} +
- {{#t.body - useLegacyBehaviorFlags=false - onScrolledToBottom=(action 'onScrolledToBottom') - as |body| - }} - {{#if isLoading}} - {{#body.loader}} - {{table-loader}} - {{/body.loader}} - {{/if}} - {{/t.body}} -{{/light-table}} +{{#lt-frame + height='65vh' + scrollbar=(if virtual 'virtual' 'standard') + as |frame| +}} + {{frame.fixed-head-here}} + + {{#frame.scrollable-zone}} + {{#frame.table table as |t|}} + {{t.head + fixed=fixed + onColumnClick=(action 'onColumnClick') + iconSortable='fa fa-sort' + iconAscending='fa fa-sort-asc' + iconDescending='fa fa-sort-desc' + }} + + {{#t.body + useLegacyBehaviorFlags=false + onScrolledToBottom=(action 'onScrolledToBottom') + as |body| + }} + {{#if isLoading}} + {{#body.loader}} + {{table-loader}} + {{/body.loader}} + {{/if}} + {{/t.body}} + {{/frame.table}} + {{/frame.scrollable-zone}} +{{/lt-frame}} {{!-- END-SNIPPET --}} diff --git a/tests/dummy/app/templates/components/columns/resizable-table.hbs b/tests/dummy/app/templates/components/columns/resizable-table.hbs index 57f06534..d7a93815 100644 --- a/tests/dummy/app/templates/components/columns/resizable-table.hbs +++ b/tests/dummy/app/templates/components/columns/resizable-table.hbs @@ -1,33 +1,49 @@ {{!-- BEGIN-SNIPPET resizable-table --}} -{{#light-table table height='65vh' as |t|}} +
+ {{fixed-header-table-action value=fixed onChange=(action (mut fixed))}} + {{virtual-scrollbar-table-action value=virtual onChange=(action (mut virtual))}} +
- {{t.head - onColumnClick=(action 'onColumnClick') - iconSortable='fa fa-sort' - iconAscending='fa fa-sort-asc' - iconDescending='fa fa-sort-desc' - resizeOnDrag=true - fixed=true - }} +{{#lt-frame + height='65vh' + scrollbar=(if virtual 'virtual' 'standard') + as |frame| +}} + {{frame.fixed-head-here}} - {{#t.body - useLegacyBehaviorFlags=false - onScrolledToBottom=(action 'onScrolledToBottom') - as |body| - }} - {{#if isLoading}} - {{#body.loader}} - {{table-loader}} - {{/body.loader}} - {{/if}} - {{/t.body}} + {{#frame.scrollable-zone}} + {{#frame.table table as |t|}} + {{t.head + fixed=fixed + onColumnClick=(action 'onColumnClick') + iconSortable='fa fa-sort' + iconAscending='fa fa-sort-asc' + iconDescending='fa fa-sort-desc' + resizeOnDrag=true + }} - {{t.foot - onColumnClick=(action 'onColumnClick') - iconAscending='fa fa-sort-asc' - iconDescending='fa fa-sort-desc' - resizeOnDrag=true - fixed=true - }} -{{/light-table}} + {{#t.body + useLegacyBehaviorFlags=false + onScrolledToBottom=(action 'onScrolledToBottom') + as |body| + }} + {{#if isLoading}} + {{#body.loader}} + {{table-loader}} + {{/body.loader}} + {{/if}} + {{/t.body}} + + {{t.foot + fixed=fixed + onColumnClick=(action 'onColumnClick') + iconAscending='fa fa-sort-asc' + iconDescending='fa fa-sort-desc' + resizeOnDrag=true + }} + {{/frame.table}} + {{/frame.scrollable-zone}} + + {{frame.fixed-foot-here}} +{{/lt-frame}} {{!-- END-SNIPPET --}} diff --git a/tests/dummy/app/templates/components/cookbook/client-side-table.hbs b/tests/dummy/app/templates/components/cookbook/client-side-table.hbs index d6209dd7..dfa4bcdd 100644 --- a/tests/dummy/app/templates/components/cookbook/client-side-table.hbs +++ b/tests/dummy/app/templates/components/cookbook/client-side-table.hbs @@ -1,47 +1,67 @@ {{!-- BEGIN-SNIPPET client-side-table --}} -{{#light-table table height='65vh' as |t|}} +
+ {{fixed-header-table-action value=fixed onChange=(action (mut fixed))}} + {{virtual-scrollbar-table-action value=virtual onChange=(action (mut virtual))}} +
- {{!-- - In order for `fa-sort-asc` and `fa-sort-desc` icons to work, - you need to have ember-font-awesome installed or manually include - the font-awesome assets, e.g. via a CDN. - --}} +{{#lt-frame + height='65vh' + scrollbar=(if virtual 'virtual' 'standard') as |frame| +}} + {{frame.fixed-head-here}} - {{t.head - onColumnClick=(action 'onColumnClick') - iconSortable='fa fa-sort' - iconAscending='fa fa-sort-asc' - iconDescending='fa fa-sort-desc' - fixed=true - }} + {{#frame.scrollable-zone}} + {{#frame.table table as |t|}} - {{#t.body - useLegacyFlagBehaviors=false - as |body| - }} - {{#if isLoading}} - {{#body.loader}} - {{table-loader}} - {{/body.loader}} - {{/if}} - {{/t.body}} + {{!-- + In order for `fa-sort-asc` and `fa-sort-desc` icons to work, + you need to have ember-font-awesome installed or manually include + the font-awesome assets, e.g. via a CDN. + --}} - {{#t.foot fixed=true as |columns|}} - - - - {{/t.foot}} -{{/light-table}} + {{t.head + fixed=fixed + onColumnClick=(action 'onColumnClick') + iconSortable='fa fa-sort' + iconAscending='fa fa-sort-asc' + iconDescending='fa fa-sort-desc' + }} + + {{#t.body + useLegacyFlagBehaviors=false + as |body| + }} + {{#if isLoading}} + {{#body.loader}} + {{table-loader}} + {{/body.loader}} + {{/if}} + {{/t.body}} + + {{#t.foot + fixed=true + to=(concat elementId '-foot-position') + as |columns| + }} + + + + {{/t.foot}} + {{/frame.table}} + {{/frame.scrollable-zone}} + + {{frame.fixed-foot-here}} +{{/lt-frame}} {{!-- END-SNIPPET --}} diff --git a/tests/dummy/app/templates/components/cookbook/custom-row-table.hbs b/tests/dummy/app/templates/components/cookbook/custom-row-table.hbs index 57a1c0af..1bab55a1 100644 --- a/tests/dummy/app/templates/components/cookbook/custom-row-table.hbs +++ b/tests/dummy/app/templates/components/cookbook/custom-row-table.hbs @@ -1,32 +1,46 @@ {{!-- BEGIN-SNIPPET custom-row-table --}} -{{#light-table table height='65vh' as |t|}} +
+ {{fixed-header-table-action value=fixed onChange=(action (mut fixed))}} + {{virtual-scrollbar-table-action value=virtual onChange=(action (mut virtual))}} +
- {{!-- - In order for `fa-sort-asc` and `fa-sort-desc` icons to work, - you need to have ember-font-awesome installed or manually include - the font-awesome assets, e.g. via a CDN. - --}} +{{#lt-frame + height='65vh' + scrollbar=(if virtual 'virtual' 'standard') as |frame| +}} + {{frame.fixed-head-here}} - {{t.head - onColumnClick=(action 'onColumnClick') - iconSortable='fa fa-sort' - iconAscending='fa fa-sort-asc' - iconDescending='fa fa-sort-desc' - fixed=true - }} + {{#frame.scrollable-zone}} + {{#frame.table table as |t|}} - {{#t.body - useLegacyBehaviorFlags=false - onScrolledToBottom=(action 'onScrolledToBottom') - rowComponent=(component 'colored-row') - as |body| - }} - {{#if isLoading}} - {{#body.loader}} - {{table-loader}} - {{/body.loader}} - {{/if}} - {{/t.body}} + {{!-- + In order for `fa-sort-asc` and `fa-sort-desc` icons to work, + you need to have ember-font-awesome installed or manually include + the font-awesome assets, e.g. via a CDN. + --}} -{{/light-table}} + {{t.head + fixed=fixed + onColumnClick=(action 'onColumnClick') + iconSortable='fa fa-sort' + iconAscending='fa fa-sort-asc' + iconDescending='fa fa-sort-desc' + }} + + {{#t.body + useLegacyBehaviorFlags=false + onScrolledToBottom=(action 'onScrolledToBottom') + rowComponent=(component 'colored-row') + as |body| + }} + {{#if isLoading}} + {{#body.loader}} + {{table-loader}} + {{/body.loader}} + {{/if}} + {{/t.body}} + + {{/frame.table}} + {{/frame.scrollable-zone}} +{{/lt-frame}} {{!-- END-SNIPPET --}} diff --git a/tests/dummy/app/templates/components/cookbook/custom-sort-icon-table.hbs b/tests/dummy/app/templates/components/cookbook/custom-sort-icon-table.hbs index 8dc0b0af..a130ebcc 100644 --- a/tests/dummy/app/templates/components/cookbook/custom-sort-icon-table.hbs +++ b/tests/dummy/app/templates/components/cookbook/custom-sort-icon-table.hbs @@ -1,26 +1,40 @@ {{!-- BEGIN-SNIPPET custom-sort-icon-table --}} -{{#light-table table height='65vh' as |t|}} +
+ {{fixed-header-table-action value=fixed onChange=(action (mut fixed))}} + {{virtual-scrollbar-table-action value=virtual onChange=(action (mut virtual))}} +
- {{t.head - onColumnClick=(action 'onColumnClick') - iconSortable='unfold_more' - iconAscending='keyboard_arrow_up' - iconDescending='keyboard_arrow_down' - iconComponent='materialize-icon' - fixed=true - }} +{{#lt-frame + height='65vh' + scrollbar=(if virtual 'virtual' 'standard') as |frame| +}} + {{frame.fixed-head-here}} - {{#t.body - useLegacyBehaviorFlags=false - onScrolledToBottom=(action 'onScrolledToBottom') - as |body| - }} - {{#if isLoading}} - {{#body.loader}} - {{table-loader}} - {{/body.loader}} - {{/if}} - {{/t.body}} + {{#frame.scrollable-zone}} + {{#frame.table table as |t|}} -{{/light-table}} + {{t.head + fixed=fixed + onColumnClick=(action 'onColumnClick') + iconSortable='unfold_more' + iconAscending='keyboard_arrow_up' + iconDescending='keyboard_arrow_down' + iconComponent='materialize-icon' + }} + + {{#t.body + useLegacyBehaviorFlags=false + onScrolledToBottom=(action 'onScrolledToBottom') + as |body| + }} + {{#if isLoading}} + {{#body.loader}} + {{table-loader}} + {{/body.loader}} + {{/if}} + {{/t.body}} + + {{/frame.table}} + {{/frame.scrollable-zone}} +{{/lt-frame}} {{!-- END-SNIPPET --}} diff --git a/tests/dummy/app/templates/components/cookbook/horizontal-scrolling-table.hbs b/tests/dummy/app/templates/components/cookbook/horizontal-scrolling-table.hbs index 40bbcd17..648b5ab8 100644 --- a/tests/dummy/app/templates/components/cookbook/horizontal-scrolling-table.hbs +++ b/tests/dummy/app/templates/components/cookbook/horizontal-scrolling-table.hbs @@ -1,31 +1,44 @@ {{!-- BEGIN-SNIPPET horizontal-scrolling-table --}} -{{#light-table table height='65vh' as |t|}} +
+ {{fixed-header-table-action value=fixed onChange=(action (mut fixed))}} + {{virtual-scrollbar-table-action value=virtual onChange=(action (mut virtual))}} +
- {{!-- - In order for `fa-sort-asc` and `fa-sort-desc` icons to work, - you need to have ember-font-awesome installed or manually include - the font-awesome assets, e.g. via a CDN. - --}} +{{#lt-frame + height='65vh' + scrollbar=(if virtual 'virtual' 'standard') as |frame| +}} + {{frame.fixed-head-here}} - {{t.head - onColumnClick=(action 'onColumnClick') - iconSortable='fa fa-sort' - iconAscending='fa fa-sort-asc' - iconDescending='fa fa-sort-desc' - fixed=true - }} + {{#frame.scrollable-zone horizontal=true}} + {{#frame.table table as |t|}} + {{!-- + In order for `fa-sort-asc` and `fa-sort-desc` icons to work, + you need to have ember-font-awesome installed or manually include + the font-awesome assets, e.g. via a CDN. + --}} - {{#t.body - useLegacyBehaviorFlags=false - onScrolledToBottom=(action 'onScrolledToBottom') - as |body| - }} - {{#if isLoading}} - {{#body.loader}} - {{table-loader}} - {{/body.loader}} - {{/if}} - {{/t.body}} + {{t.head + fixed=fixed + onColumnClick=(action 'onColumnClick') + iconSortable='fa fa-sort' + iconAscending='fa fa-sort-asc' + iconDescending='fa fa-sort-desc' + }} -{{/light-table}} + {{#t.body + useLegacyBehaviorFlags=false + onScrolledToBottom=(action 'onScrolledToBottom') + as |body| + }} + {{#if isLoading}} + {{#body.loader}} + {{table-loader}} + {{/body.loader}} + {{/if}} + {{/t.body}} + + {{/frame.table}} + {{/frame.scrollable-zone}} +{{/lt-frame}} {{!-- END-SNIPPET --}} diff --git a/tests/dummy/app/templates/components/cookbook/occluded-table.hbs b/tests/dummy/app/templates/components/cookbook/occluded-table.hbs index 88687c09..f4a82a65 100644 --- a/tests/dummy/app/templates/components/cookbook/occluded-table.hbs +++ b/tests/dummy/app/templates/components/cookbook/occluded-table.hbs @@ -1,35 +1,49 @@ {{!-- BEGIN-SNIPPET occluded-table --}} -{{#light-table table - height='65vh' - occlusion=true - estimatedRowHeight=50 - as |t|}} - -{{!-- - In order for `fa-sort-asc` and `fa-sort-desc` icons to work, - you need to have ember-font-awesome installed or manually include - the font-awesome assets, e.g. via a CDN. ---}} - - {{t.head - fixed=true - }} - - {{#t.body - useLegacyBehaviorFlags=false - scrollBuffer=200 - onScrolledToBottom=(action 'onScrolledToBottom') - as |body| - }} - {{#if isLoading}} - {{#body.loader}} - {{table-loader}} - {{/body.loader}} - {{/if}} - {{/t.body}} - - {{t.foot - fixed=true - }} -{{/light-table}} +
+ {{fixed-header-table-action value=fixed onChange=(action (mut fixed))}} + {{virtual-scrollbar-table-action value=virtual onChange=(action (mut virtual))}} +
+ +{{#lt-frame + height='65vh' + scrollbar=(if virtual 'virtual' 'standard') as |frame| +}} + {{frame.fixed-head-here}} + + {{#frame.scrollable-zone}} + + {{#frame.table + table + occlusion=true + estimatedRowHeight=50 + as |t| + }} + {{!-- + In order for `fa-sort-asc` and `fa-sort-desc` icons to work, + you need to have ember-font-awesome installed or manually include + the font-awesome assets, e.g. via a CDN. + --}} + + {{t.head fixed=fixed}} + + {{#t.body + useLegacyBehaviorFlags=false + scrollBuffer=200 + onScrolledToBottom=(action 'onScrolledToBottom') + as |body| + }} + {{#if isLoading}} + {{#body.loader}} + {{table-loader}} + {{/body.loader}} + {{/if}} + {{/t.body}} + + {{t.foot fixed=fixed}} + {{/frame.table}} + + {{/frame.scrollable-zone}} + + {{frame.fixed-foot-here}} +{{/lt-frame}} {{!-- END-SNIPPET --}} diff --git a/tests/dummy/app/templates/components/cookbook/paginated-table.hbs b/tests/dummy/app/templates/components/cookbook/paginated-table.hbs index 38a6621a..1d969585 100644 --- a/tests/dummy/app/templates/components/cookbook/paginated-table.hbs +++ b/tests/dummy/app/templates/components/cookbook/paginated-table.hbs @@ -1,5 +1,5 @@ {{!-- BEGIN-SNIPPET paginated-table --}} -{{#light-table table height='65vh' as |t|}} +{{#light-table table as |t|}} {{!-- In order for `fa-sort-asc` and `fa-sort-desc` icons to work, @@ -12,7 +12,6 @@ iconSortable='fa fa-sort' iconAscending='fa fa-sort-asc' iconDescending='fa fa-sort-desc' - fixed=true }} {{#t.body @@ -27,7 +26,7 @@ {{/t.body}} {{#if meta}} - {{#t.foot fixed=true as |columns|}} + {{#t.foot as |columns|}} + + + {{/t.foot}} + + {{/frame.table}} + {{/frame.scrollable-zone}} - {{!-- - In order for `fa-sort-asc` and `fa-sort-desc` icons to work, - you need to have ember-font-awesome installed or manually include - the font-awesome assets, e.g. via a CDN. - --}} - - {{t.head - onColumnClick=(action 'onColumnClick') - iconSortable='fa fa-sort' - iconAscending='fa fa-sort-asc' - iconDescending='fa fa-sort-desc' - fixed=true - }} - - {{#t.body - useLegacyBehaviorFlags=false - onScrolledToBottom=(action 'onScrolledToBottom') - as |body| - }} - {{#body.expanded-row as |row|}} - {{responsive-expanded-row table=table row=row}} - {{/body.expanded-row}} - - {{#if isLoading}} - {{#body.loader}} - {{table-loader}} - {{/body.loader}} - {{/if}} - {{/t.body}} - - {{#t.foot fixed=true as |columns|}} - - - - {{/t.foot}} - -{{/light-table}} + {{frame.fixed-foot-here}} +{{/lt-frame}} {{!-- END-SNIPPET --}} diff --git a/tests/dummy/app/templates/components/rows/expandable-table.hbs b/tests/dummy/app/templates/components/rows/expandable-table.hbs index 6bc2756b..73134c02 100644 --- a/tests/dummy/app/templates/components/rows/expandable-table.hbs +++ b/tests/dummy/app/templates/components/rows/expandable-table.hbs @@ -1,28 +1,43 @@ {{!-- BEGIN-SNIPPET expandable-table --}} -{{#light-table table height='65vh' as |t|}} - {{t.head - onColumnClick=(action 'onColumnClick') - iconSortable='fa fa-sort' - iconAscending='fa fa-sort-asc' - iconDescending='fa fa-sort-desc' - fixed=true - }} +
+ {{fixed-header-table-action value=fixed onChange=(action (mut fixed))}} + {{virtual-scrollbar-table-action value=virtual onChange=(action (mut virtual))}} +
- {{#t.body - useLegacyBehaviorFlags=false - behaviors=(array (lt-row-expansion multiRow=false)) - onScrolledToBottom=(action 'onScrolledToBottom') - as |body| - }} - {{#body.expanded-row as |row|}} - {{expanded-row row=row}} - {{/body.expanded-row}} +{{#lt-frame + height='65vh' + scrollbar=(if virtual 'virtual' 'standard') + as |frame| +}} + {{frame.fixed-head-here}} - {{#if isLoading}} - {{#body.loader}} - {{table-loader}} - {{/body.loader}} - {{/if}} - {{/t.body}} -{{/light-table}} + {{#frame.scrollable-zone}} + {{#frame.table table as |t|}} + {{t.head + fixed=fixed + onColumnClick=(action 'onColumnClick') + iconSortable='fa fa-sort' + iconAscending='fa fa-sort-asc' + iconDescending='fa fa-sort-desc' + }} + + {{#t.body + useLegacyBehaviorFlags=false + behaviors=(array (lt-row-expansion multiRow=false)) + onScrolledToBottom=(action 'onScrolledToBottom') + as |body| + }} + {{#body.expanded-row as |row|}} + {{expanded-row row=row}} + {{/body.expanded-row}} + + {{#if isLoading}} + {{#body.loader}} + {{table-loader}} + {{/body.loader}} + {{/if}} + {{/t.body}} + {{/frame.table}} + {{/frame.scrollable-zone}} +{{/lt-frame}} {{!-- END-SNIPPET --}} diff --git a/tests/dummy/app/templates/components/rows/selectable-table.hbs b/tests/dummy/app/templates/components/rows/selectable-table.hbs index 28427dfe..372afa87 100644 --- a/tests/dummy/app/templates/components/rows/selectable-table.hbs +++ b/tests/dummy/app/templates/components/rows/selectable-table.hbs @@ -1,5 +1,7 @@ {{!-- BEGIN-SNIPPET selectable-table --}}
+ {{fixed-header-table-action value=fixed onChange=(action (mut fixed))}} + {{virtual-scrollbar-table-action value=virtual onChange=(action (mut virtual))}} {{#if hasSelection}}
@@ -8,32 +10,42 @@ {{/if}}
-{{#light-table table height='65vh' as |t|}} - {{t.head - onColumnClick=(action 'onColumnClick') - iconSortable='fa fa-sort' - iconAscending='fa fa-sort-asc' - iconDescending='fa fa-sort-desc' - fixed=true - }} +{{#lt-frame + height='65vh' + scrollbar=(if virtual 'virtual' 'standard') + as |frame| +}} + {{frame.fixed-head-here}} - {{#t.body - useLegacyBehaviorFlags=false - behaviors=(array (lt-multi-select)) - onScrolledToBottom=(action 'onScrolledToBottom') - as |body| - }} - {{#if isLoading}} - {{#body.loader}} - {{table-loader}} - {{/body.loader}} - {{/if}} + {{#frame.scrollable-zone}} + {{#frame.table table as |t|}} + {{t.head + fixed=fixed + onColumnClick=(action 'onColumnClick') + iconSortable='fa fa-sort' + iconAscending='fa fa-sort-asc' + iconDescending='fa fa-sort-desc' + }} - {{#if (and (not isLoading) table.isEmpty)}} - {{#body.no-data}} - {{no-data}} - {{/body.no-data}} - {{/if}} - {{/t.body}} -{{/light-table}} + {{#t.body + useLegacyBehaviorFlags=false + behaviors=(array (lt-multi-select)) + onScrolledToBottom=(action 'onScrolledToBottom') + as |body| + }} + {{#if isLoading}} + {{#body.loader}} + {{table-loader}} + {{/body.loader}} + {{/if}} + + {{#if (and (not isLoading) table.isEmpty)}} + {{#body.no-data}} + {{no-data}} + {{/body.no-data}} + {{/if}} + {{/t.body}} + {{/frame.table}} + {{/frame.scrollable-zone}} +{{/lt-frame}} {{!-- END-SNIPPET --}} diff --git a/tests/dummy/app/templates/components/scrolling-table.hbs b/tests/dummy/app/templates/components/scrolling-table.hbs index 65ea3bff..77d8b736 100644 --- a/tests/dummy/app/templates/components/scrolling-table.hbs +++ b/tests/dummy/app/templates/components/scrolling-table.hbs @@ -1,82 +1,98 @@ {{!-- BEGIN-SNIPPET scrolling-table --}} -{{#light-table table height='65vh' as |t|}} +
+ {{fixed-header-table-action value=fixed onChange=(action (mut fixed))}} + {{virtual-scrollbar-table-action value=virtual onChange=(action (mut virtual))}} +
- {{!-- - In order for `fa-sort-asc` and `fa-sort-desc` icons to work, - you need to have ember-font-awesome installed or manually include - the font-awesome assets, e.g. via a CDN. - --}} +{{#lt-frame + height='65vh' + scrollbar=(if virtual 'virtual' 'standard') + as |frame| +}} + {{frame.fixed-head-here}} - {{t.head - onColumnClick=(action 'onColumnClick') - iconSortable='fa fa-sort' - iconAscending='fa fa-sort-asc' - iconDescending='fa fa-sort-desc' - fixed=true - }} + {{#frame.scrollable-zone as |zone|}} + {{#frame.table table as |t|}} + {{!-- + In order for `fa-sort-asc` and `fa-sort-desc` icons to work, + you need to have ember-font-awesome installed or manually include + the font-awesome assets, e.g. via a CDN. + --}} - {{#t.body - useLegacyBehaviorFlags=false - scrollTo=scrollTo - scrollToRow=scrollToRow - onScroll=(action (mut currentScrollOffset)) - onScrolledToBottom=(action 'onScrolledToBottom') - as |body| - }} - {{#if isLoading}} - {{#body.loader}} - {{table-loader}} - {{/body.loader}} - {{/if}} - {{/t.body}} + {{t.head + fixed=fixed + onColumnClick=(action 'onColumnClick') + iconSortable='fa fa-sort' + iconAscending='fa fa-sort-asc' + iconDescending='fa fa-sort-desc' + }} - {{#t.foot fixed=true as |columns|}} - - + - - {{/t.foot}} +
+ + {{one-way-input + update=zone.onScrollTo + value=zone.scrollTop + class="form-control" + name="onScrollTo" + type="number" + min=0 + step=10 + }} +
-{{/light-table}} +
+ + {{#one-way-select selectedValue + options=table.visibleRows + value=scrollToRow + update=(action (mut scrollToRow)) + class="form-control" + name="scrollToRow" + as |row| + }} + {{row.id}} - {{row.firstName}} {{row.lastName}} + {{/one-way-select}} +
+ + + + {{/t.foot}} + + {{/frame.table}} + {{/frame.scrollable-zone}} + + {{frame.fixed-foot-here}} +{{/lt-frame}} {{!-- END-SNIPPET --}} diff --git a/tests/dummy/app/templates/components/simple-table.hbs b/tests/dummy/app/templates/components/simple-table.hbs index e402d8bf..e010a3ca 100644 --- a/tests/dummy/app/templates/components/simple-table.hbs +++ b/tests/dummy/app/templates/components/simple-table.hbs @@ -1,31 +1,47 @@ {{!-- BEGIN-SNIPPET simple-table --}} -{{#light-table table height='65vh' as |t|}} +
+ {{fixed-header-table-action value=fixed onChange=(action (mut fixed))}} + {{virtual-scrollbar-table-action value=virtual onChange=(action (mut virtual))}} +
- {{!-- - In order for `fa-sort-asc` and `fa-sort-desc` icons to work, - you need to have ember-font-awesome installed or manually include - the font-awesome assets, e.g. via a CDN. - --}} +{{#lt-frame + height='65vh' + scrollbar=(if virtual 'virtual' 'standard') + as |frame| +}} + {{frame.fixed-head-here}} - {{t.head - onColumnClick=(action 'onColumnClick') - iconSortable='fa fa-sort' - iconAscending='fa fa-sort-asc' - iconDescending='fa fa-sort-desc' - fixed=true - }} + {{#frame.scrollable-zone}} + {{#frame.table table as |t|}} + {{!-- + In order for `fa-sort-asc` and `fa-sort-desc` icons to work, + you need to have ember-font-awesome installed or manually include + the font-awesome assets, e.g. via a CDN. + --}} - {{#t.body - useLegacyBehaviorFlags=false - onScrolledToBottom=(action 'onScrolledToBottom') - as |body| - }} - {{#if isLoading}} - {{#body.loader}} - {{table-loader}} - {{/body.loader}} - {{/if}} - {{/t.body}} + {{t.head + fixed=fixed + onColumnClick=(action 'onColumnClick') + iconSortable='fa fa-sort' + iconAscending='fa fa-sort-asc' + iconDescending='fa fa-sort-desc' + }} -{{/light-table}} + {{#t.body + useLegacyBehaviorFlags=false + onScrolledToBottom=(action 'onScrolledToBottom') + as |body| + }} + {{#if isLoading}} + {{#body.loader}} + {{table-loader}} + {{/body.loader}} + {{/if}} + {{/t.body}} + + {{/frame.table}} + {{/frame.scrollable-zone}} + + {{frame.fixed-foot-here}} +{{/lt-frame}} {{!-- END-SNIPPET --}} diff --git a/tests/integration/components/light-table-occlusion-test.js b/tests/integration/components/light-table-occlusion-test.js index 2268e428..a8b24f84 100644 --- a/tests/integration/components/light-table-occlusion-test.js +++ b/tests/integration/components/light-table-occlusion-test.js @@ -25,8 +25,11 @@ module('Integration | Component | light table | occlusion', function(hooks) { test('it renders', async function(assert) { this.set('table', new Table()); - await render(hbs `{{light-table table height="40vh" occlusion=true estimatedRowHeight=30}}`); - + await render(hbs ` + {{#lt-frame height="40vh" as |frame|}} + {{frame.table table occlusion=true estimatedRowHeight=30}} + {{/lt-frame}} + `); assert.equal(find('*').textContent.trim(), ''); }); @@ -40,15 +43,20 @@ module('Integration | Component | light table | occlusion', function(hooks) { }); await render(hbs ` - {{#light-table table height='40vh' occlusion=true estimatedRowHeight=30 as |t|}} - {{t.head fixed=true}} - {{t.body onScrolledToBottom=(action onScrolledToBottom)}} - {{/light-table}} + {{#lt-frame height='300px' scrollbar='virtual' as |frame|}} + {{frame.fixed-head-here}} + {{#frame.scrollable-zone}} + {{#frame.table table occlusion=true estimatedRowHeight=30 as |t|}} + {{t.head fixed=true}} + {{t.body onScrolledToBottom=(action onScrolledToBottom)}} + {{/frame.table}} + {{/frame.scrollable-zone}} + {{/lt-frame}} `); assert.ok(findAll('.vertical-collection tbody.lt-body tr.lt-row').length < 30, 'only some rows are rendered'); - let scrollContainer = '.lt-scrollable.tse-scrollable.vertical-collection'; + let scrollContainer = '.lt-scrollable .tse-scroll-content'; let { scrollHeight } = find(scrollContainer); assert.ok(findAll(scrollContainer).length > 0, 'scroll container was rendered'); @@ -57,92 +65,58 @@ module('Integration | Component | light table | occlusion', function(hooks) { await scrollTo(scrollContainer, 0, scrollHeight); }); - test('fixed header', async function(assert) { - assert.expect(2); - this.set('table', new Table(Columns, this.server.createList('user', 5))); - - await render(hbs ` - {{#light-table table height='500px' id='lightTable' occlusion=true estimatedRowHeight=30 as |t|}} - {{t.head fixed=true}} - {{t.body}} - {{/light-table}} - `); - - assert.equal(findAll('#lightTable_inline_head thead').length, 0); - - await render(hbs ` - {{#light-table table height='500px' id='lightTable' occlusion=true estimatedRowHeight=30 as |t|}} - {{t.head fixed=false}} - {{t.body}} - {{/light-table}} - `); - - assert.equal(findAll('#lightTable_inline_head thead').length, 1); - }); - - test('fixed footer', async function(assert) { - assert.expect(2); - this.set('table', new Table(Columns, this.server.createList('user', 5))); - - await render(hbs ` - {{#light-table table height='500px' id='lightTable' occlusion=true estimatedRowHeight=30 as |t|}} - {{t.body}} - {{t.foot fixed=true}} - {{/light-table}} - `); - - assert.equal(findAll('#lightTable_inline_foot tfoot').length, 0); - + async function renderWithHeader() { await render(hbs ` - {{#light-table table height='500px' id='lightTable' occlusion=true estimatedRowHeight=30 as |t|}} - {{t.body}} - {{t.foot fixed=false}} - {{/light-table}} + {{#lt-frame height='500px' scrollbar='virtual' as |frame|}} + {{frame.fixed-head-here}} + {{#frame.scrollable-zone}} + {{#frame.table table occlusion=true estimatedRowHeight=30 as |t|}} + {{t.head fixed=fixed}} + {{t.body}} + {{/frame.table}} + {{/frame.scrollable-zone}} + {{/lt-frame}} `); + } - assert.equal(findAll('#lightTable_inline_foot tfoot').length, 1); - }); - - test('table assumes height of container', async function(assert) { - + test('fixed header', async function(assert) { + assert.expect(4); this.set('table', new Table(Columns, this.server.createList('user', 5))); this.set('fixed', true); - - await render(hbs ` -
- {{#light-table table id='lightTable' occlusion=true estimatedRowHeight=30 as |t|}} - {{t.body}} - {{t.foot fixed=fixed}} - {{/light-table}} -
- `); - - assert.equal(find('#lightTable').offsetHeight, 500, 'table is 500px height'); - + await renderWithHeader(); + assert.equal(findAll('.lt-frame thead').length, 1, 'fixed - thead is rendered'); + assert.equal(findAll('.lt-scrollable thead').length, 0, 'fixed - not rendered inside scrollable zone'); + this.set('fixed', false); + await renderWithHeader(); + assert.equal(findAll('.lt-frame thead').length, 1, 'inline - thead is rendered'); + assert.equal(findAll('.lt-scrollable thead ').length, 1, 'inline - rendered inside scrollable zone'); }); - test('table body should consume all available space when not enough content to fill it', async function(assert) { - this.set('table', new Table(Columns, this.server.createList('user', 1))); - this.set('fixed', true); - + async function renderWithFooter() { await render(hbs ` -
- {{#light-table table id='lightTable' occlusion=true estimatedRowHeight=30 as |t|}} - {{t.head fixed=true}} - {{t.body}} - {{#t.foot fixed=true}} - Hello World - {{/t.foot}} - {{/light-table}} -
+ {{#lt-frame height='500px' as |frame|}} + {{#frame.scrollable-zone}} + {{#frame.table table occlusion=true estimatedRowHeight=30 as |t|}} + {{t.body}} + {{t.foot fixed=fixed}} + {{/frame.table}} + {{/frame.scrollable-zone}} + {{frame.fixed-foot-here}} + {{/lt-frame}} `); + } - const bodyHeight = find('.lt-body-wrap').offsetHeight; - const headHeight = find('.lt-head-wrap').offsetHeight; - const footHeight = find('.lt-foot-wrap').offsetHeight; - - assert.equal(bodyHeight + headHeight + footHeight, 500, 'combined table content is 500px tall'); - assert.ok(bodyHeight > (headHeight + footHeight), 'body is tallest element'); + test('fixed footer', async function(assert) { + assert.expect(4); + this.set('table', new Table(Columns, this.server.createList('user', 5))); + this.set('fixed', true); + await renderWithFooter(); + assert.equal(findAll('.lt-frame tfoot').length, 1, 'fixed - tfoot is rendered'); + assert.equal(findAll('.lt-scrollable tfoot').length, 0, 'fixed - not rendered inside scrollable zone'); + this.set('fixed', false); + await renderWithFooter(); + assert.equal(findAll('.lt-frame tfoot').length, 1, 'inline - tfoot is rendered'); + assert.equal(findAll('.lt-scrollable tfoot ').length, 1, 'inline - rendered inside scrollable zone'); }); test('accepts components that are used in the body', async function(assert) { @@ -152,9 +126,13 @@ module('Integration | Component | light table | occlusion', function(hooks) { this.set('table', new Table(Columns, this.server.createList('user', 1))); await render(hbs ` - {{#light-table table occlusion=true estimatedRowHeight=30 as |t|}} - {{t.body rowComponent=(component "custom-row" classNames="custom-row")}} - {{/light-table}} + {{#lt-frame height='500px' as |frame|}} + {{#frame.scrollable-zone}} + {{#frame.table table occlusion=true estimatedRowHeight=30 as |t|}} + {{t.body rowComponent=(component "custom-row" classNames="custom-row")}} + {{/frame.table}} + {{/frame.scrollable-zone}} + {{/lt-frame}} `); assert.equal(findAll('.lt-row.custom-row').length, 1, 'row has custom-row class'); @@ -174,11 +152,15 @@ module('Integration | Component | light table | occlusion', function(hooks) { this.set('table', new Table(Columns, users)); await render(hbs ` - {{#light-table table height='500px' occlusion=true estimatedRowHeight=30 as |t|}} - {{t.body - rowComponent=(component "custom-row" classNames="custom-row" current=current) - }} - {{/light-table}} + {{#lt-frame height='500px' as |frame|}} + {{#frame.scrollable-zone}} + {{#frame.table table occlusion=true estimatedRowHeight=30 as |t|}} + {{t.body + rowComponent=(component "custom-row" classNames="custom-row" current=current) + }} + {{/frame.table}} + {{/frame.scrollable-zone}} + {{/lt-frame}} `); assert.equal(findAll('.custom-row').length, 3, 'three custom rows were rendered'); @@ -228,18 +210,22 @@ module('Integration | Component | light table | occlusion', function(hooks) { }; await render(hbs ` - {{#light-table table - occlusion=true - estimatedRowHeight=30 - extra=(hash someData="someValue") - tableActions=(hash - someAction=(action "someAction") - ) - as |t| - }} - {{t.head}} - {{t.body}} - {{/light-table}} + {{#lt-frame height='500px' as |frame|}} + {{#frame.scrollable-zone}} + {{#frame.table table + occlusion=true + estimatedRowHeight=30 + extra=(hash someData="someValue") + tableActions=(hash + someAction=(action "someAction") + ) + as |t| + }} + {{t.head}} + {{t.body}} + {{/frame.table}} + {{/frame.scrollable-zone}} + {{/lt-frame}} `); for (const element of findAll('.some-component')) { diff --git a/tests/integration/components/light-table-test.js b/tests/integration/components/light-table-test.js index 8d9c7186..12c80ec4 100644 --- a/tests/integration/components/light-table-test.js +++ b/tests/integration/components/light-table-test.js @@ -37,15 +37,21 @@ module('Integration | Component | light table', function(hooks) { }); await render(hbs ` - {{#light-table table height='40vh' as |t|}} - {{t.head fixed=true}} - {{t.body onScrolledToBottom=(action onScrolledToBottom)}} - {{/light-table}} + {{#lt-frame height='40vh' as |frame|}} + {{frame.fixed-head-here}} + {{#frame.scrollable-zone}} + {{#frame.table table as |t|}} + {{t.head fixed=true}} + {{t.body onScrolledToBottom=(action onScrolledToBottom) + }} + {{/frame.table}} + {{/frame.scrollable-zone}} + {{/lt-frame}} `); assert.equal(findAll('tbody > tr').length, 50, '50 rows are rendered'); - let scrollContainer = '.tse-scroll-content'; + let scrollContainer = '.lt-scrollable'; let { scrollHeight } = find(scrollContainer); assert.ok(findAll(scrollContainer).length > 0, 'scroll container was rendered'); @@ -54,92 +60,108 @@ module('Integration | Component | light table', function(hooks) { await scrollTo(scrollContainer, 0, scrollHeight); }); - test('fixed header', async function(assert) { - assert.expect(2); - this.set('table', new Table(Columns, this.server.createList('user', 5))); + test('scrolled to bottom (multiple tables)', async function(assert) { + assert.expect(4); - await render(hbs ` - {{#light-table table height='500px' id='lightTable' as |t|}} - {{t.head fixed=true}} - {{t.body}} - {{/light-table}} - `); + this.set('table', new Table(Columns, this.server.createList('user', 50))); - assert.equal(findAll('#lightTable_inline_head thead').length, 0); + this.set('onScrolledToBottomTable1', () => { + assert.ok(false); + }); + + this.set('onScrolledToBottomTable2', () => { + assert.ok(true); + }); await render(hbs ` - {{#light-table table height='500px' id='lightTable' as |t|}} - {{t.head fixed=false}} - {{t.body}} - {{/light-table}} +
+ {{#lt-frame height='20vh' scrollbar='virtual' as |frame|}} + {{frame.fixed-head-here}} + {{#frame.scrollable-zone}} + {{#frame.table table as |t|}} + {{t.head fixed=true}} + {{t.body onScrolledToBottom=(action onScrolledToBottomTable1)}} + {{/frame.table}} + {{/frame.scrollable-zone}} + {{/lt-frame}} +
+ +
+ {{#lt-frame height='20vh' scrollbar='virtual' as |frame|}} + {{frame.fixed-head-here}} + {{#frame.scrollable-zone}} + {{#frame.table table as |t|}} + {{t.head fixed=true}} + {{t.body onScrolledToBottom=(action onScrolledToBottomTable2)}} + {{/frame.table}} + {{/frame.scrollable-zone}} + {{/lt-frame}} +
`); - assert.equal(findAll('#lightTable_inline_head thead').length, 1); - }); + assert.equal(findAll('#table-2 tbody > tr').length, 50, '50 rows are rendered'); - test('fixed footer', async function(assert) { - assert.expect(2); - this.set('table', new Table(Columns, this.server.createList('user', 5))); + let scrollContainer = '#table-2 .lt-scrollable .tse-scroll-content'; + let { scrollHeight } = find(scrollContainer); - await render(hbs ` - {{#light-table table height='500px' id='lightTable' as |t|}} - {{t.body}} - {{t.foot fixed=true}} - {{/light-table}} - `); + assert.ok(findAll(scrollContainer).length > 0, 'scroll container was rendered'); + assert.equal(scrollHeight, 2501, 'scroll height is 2500 + 1px for height of lt-infinity'); - assert.equal(findAll('#lightTable_inline_foot tfoot').length, 0); + await scrollTo(scrollContainer, 0, scrollHeight); + }); + async function renderWithHeader() { await render(hbs ` - {{#light-table table height='500px' id='lightTable' as |t|}} - {{t.body}} - {{t.foot fixed=false}} - {{/light-table}} + {{#lt-frame height='500px' scrollbar='virtual' as |frame|}} + {{frame.fixed-head-here}} + {{#frame.scrollable-zone}} + {{#frame.table table as |t|}} + {{t.head fixed=fixed}} + {{t.body}} + {{/frame.table}} + {{/frame.scrollable-zone}} + {{/lt-frame}} `); + } - assert.equal(findAll('#lightTable_inline_foot tfoot').length, 1); - }); - - test('table assumes height of container', async function(assert) { - + test('fixed header', async function(assert) { + assert.expect(4); this.set('table', new Table(Columns, this.server.createList('user', 5))); this.set('fixed', true); - - await render(hbs ` -
- {{#light-table table id='lightTable' as |t|}} - {{t.body}} - {{t.foot fixed=fixed}} - {{/light-table}} -
- `); - - assert.equal(find('#lightTable').offsetHeight, 500, 'table is 500px height'); - + await renderWithHeader(); + assert.equal(findAll('.lt-frame thead').length, 1, 'fixed - thead is rendered'); + assert.equal(findAll('.lt-scrollable thead').length, 0, 'fixed - not rendered inside scrollable zone'); + this.set('fixed', false); + await renderWithHeader(); + assert.equal(findAll('.lt-frame thead').length, 1, 'inline - thead is rendered'); + assert.equal(findAll('.lt-scrollable thead ').length, 1, 'inline - rendered inside scrollable zone'); }); - test('table body should consume all available space when not enough content to fill it', async function(assert) { - this.set('table', new Table(Columns, this.server.createList('user', 1))); - this.set('fixed', true); - + async function renderWithFooter() { await render(hbs ` -
- {{#light-table table id='lightTable' as |t|}} - {{t.head fixed=true}} - {{t.body}} - {{#t.foot fixed=true}} - Hello World - {{/t.foot}} - {{/light-table}} -
+ {{#lt-frame height='500px' as |frame|}} + {{#frame.scrollable-zone}} + {{#frame.table table as |t|}} + {{t.body}} + {{t.foot fixed=fixed}} + {{/frame.table}} + {{/frame.scrollable-zone}} + {{frame.fixed-foot-here}} + {{/lt-frame}} `); + } - const bodyHeight = find('.lt-body-wrap').offsetHeight; - const headHeight = find('.lt-head-wrap').offsetHeight; - const footHeight = find('.lt-foot-wrap').offsetHeight; - - assert.equal(bodyHeight + headHeight + footHeight, 500, 'combined table content is 500px tall'); - assert.ok(bodyHeight > (headHeight + footHeight), 'body is tallest element'); + test('fixed footer', async function(assert) { + assert.expect(4); + this.set('table', new Table(Columns, this.server.createList('user', 5))); + this.set('fixed', true); + await renderWithFooter(); + assert.equal(findAll('.lt-frame tfoot').length, 1, 'fixed - tfoot is rendered'); + assert.equal(findAll('.lt-scrollable tfoot').length, 0, 'fixed - not rendered inside scrollable zone'); + this.set('fixed', false); + await renderWithFooter(); + assert.equal(findAll('.lt-frame tfoot').length, 1, 'inline - tfoot is rendered'); + assert.equal(findAll('.lt-scrollable tfoot ').length, 1, 'inline - rendered inside scrollable zone'); }); test('accepts components that are used in the body', async function(assert) { @@ -194,31 +216,6 @@ module('Integration | Component | light table', function(hooks) { assert.notOk(find('.custom-row.is-active'), 'none of the items are active'); }); - test('onScroll', async function(assert) { - let table = new Table(Columns, this.server.createList('user', 10)); - let expectedScroll = 50; - - this.setProperties({ - table, - onScroll(actualScroll) { - assert.ok(true, 'onScroll worked'); - assert.equal(actualScroll, expectedScroll, 'scroll position is correct'); - } - }); - - await render(hbs ` - {{#light-table table height='40vh' as |t|}} - {{t.head fixed=true}} - {{t.body - useVirtualScrollbar=true - onScroll=onScroll - }} - {{/light-table}} - `); - - await scrollTo('.tse-scroll-content', 0, expectedScroll); - }); - test('extra data and tableActions', async function(assert) { assert.expect(4); @@ -265,10 +262,13 @@ module('Integration | Component | light table', function(hooks) { let table = new Table(ResizableColumns, this.server.createList('user', 10)); this.setProperties({ table }); await render(hbs ` - {{#light-table table height='40vh' as |t|}} - {{t.head fixed=true}} - {{t.body}} - {{/light-table}} + {{#lt-frame height='40vh' as |frame|}} + {{frame.fixed-head-here}} + {{#frame.table table as |t|}} + {{t.head fixed=true}} + {{t.body}} + {{/frame.table}} + {{/lt-frame}} `); let ths = this.element.querySelectorAll('th.is-resizable'); assert.equal(ths.length, 5); diff --git a/tests/integration/components/lt-body-occlusion-test.js b/tests/integration/components/lt-body-occlusion-test.js index e20398ec..35abc76e 100644 --- a/tests/integration/components/lt-body-occlusion-test.js +++ b/tests/integration/components/lt-body-occlusion-test.js @@ -26,12 +26,19 @@ module('Integration | Component | lt body | occlusion', function(hooks) { fixedFooter: false, height: '500px', occlusion: true, - estimatedRowHeight: 30 + estimatedRowHeight: 30, + frameId: 'some-frame' }); }); test('it renders', async function(assert) { - await render(hbs `{{lt-body sharedOptions=sharedOptions}}`); + await render(hbs ` +
+
+ {{lt-body sharedOptions=sharedOptions}} +
+
+ `); assert.equal(find('*').textContent.trim(), ''); }); @@ -39,7 +46,13 @@ module('Integration | Component | lt body | occlusion', function(hooks) { this.set('table', new Table(Columns, this.server.createList('user', 1))); this.set('canSelect', false); - await render(hbs `{{lt-body table=table sharedOptions=sharedOptions canSelect=canSelect}}`); + await render(hbs ` +
+
+ {{lt-body table=table sharedOptions=sharedOptions canSelect=canSelect}} +
+
+ `); let row = find('tr'); @@ -59,7 +72,19 @@ module('Integration | Component | lt body | occlusion', function(hooks) { test('row selection - ctrl-click to modify selection', async function(assert) { this.set('table', new Table(Columns, this.server.createList('user', 5))); - await render(hbs `{{lt-body table=table scrollBuffer=200 sharedOptions=sharedOptions canSelect=true multiSelect=true}}`); + await render(hbs ` +
+
+ {{lt-body + table=table + scrollBuffer=200 + sharedOptions=sharedOptions + canSelect=true + multiSelect=true + }} +
+
+ `); let firstRow = find('tr:nth-child(2)'); let middleRow = find('tr:nth-child(4)'); let lastRow = find('tr:nth-child(6)'); @@ -83,8 +108,13 @@ module('Integration | Component | lt body | occlusion', function(hooks) { this.set('table', new Table(Columns, this.server.createList('user', 5))); await render( - hbs `{{lt-body table=table sharedOptions=sharedOptions canSelect=true multiSelect=true multiSelectRequiresKeyboard=false}}` - ); + hbs ` +
+
+ {{lt-body table=table sharedOptions=sharedOptions canSelect=true multiSelect=true multiSelectRequiresKeyboard=false}} +
+
+ `); let firstRow = find('tr:nth-child(2)'); let middleRow = find('tr:nth-child(4)'); @@ -111,9 +141,13 @@ module('Integration | Component | lt body | occlusion', function(hooks) { this.set('table', new Table(Columns, this.server.createList('user', 1))); this.actions.onRowClick = (row) => assert.ok(row); this.actions.onRowDoubleClick = (row) => assert.ok(row); - await render( - hbs `{{lt-body table=table sharedOptions=sharedOptions onRowClick=(action 'onRowClick') onRowDoubleClick=(action 'onRowDoubleClick')}}` - ); + await render(hbs ` +
+
+ {{lt-body table=table sharedOptions=sharedOptions onRowClick=(action 'onRowClick') onRowDoubleClick=(action 'onRowDoubleClick')}} +
+
+ `); let row = find('tr'); await click(row); @@ -123,7 +157,13 @@ module('Integration | Component | lt body | occlusion', function(hooks) { test('hidden rows', async function(assert) { this.set('table', new Table(Columns, this.server.createList('user', 5))); - await render(hbs `{{lt-body table=table sharedOptions=sharedOptions}}`); + await render(hbs ` +
+
+ {{lt-body table=table sharedOptions=sharedOptions}} +
+
+ `); assert.equal(findAll('tbody tr').length, 5); @@ -147,9 +187,13 @@ module('Integration | Component | lt body | occlusion', function(hooks) { this.set('table', new Table(Columns, this.server.createList('user', 5))); await render(hbs ` - {{#lt-body table=table sharedOptions=sharedOptions overwrite=true as |columns rows|}} - {{columns.length}}, {{rows.length}} - {{/lt-body}} +
+
+ {{#lt-body table=table sharedOptions=sharedOptions overwrite=true as |columns rows|}} + {{columns.length}}, {{rows.length}} + {{/lt-body}} +
+
`); assert.equal(find('*').textContent.trim(), '6, 5'); From 8fa6d9c554655a13c1aa91b6e5bda69c3b6eb2d2 Mon Sep 17 00:00:00 2001 From: Pierre-Luc Carmel Biron Date: Tue, 27 Nov 2018 21:23:19 -0500 Subject: [PATCH 4/6] Row focus --- addon/behaviors/row-focus.js | 101 ++++++++++++++++++ addon/classes/Row.js | 12 +++ addon/classes/Table.js | 56 +++++++++- addon/components/lt-body.js | 47 +++++++- addon/components/lt-row.js | 30 +++++- addon/listeners/mouse-events.js | 19 ++++ addon/templates/components/lt-body.hbs | 1 + addon/utils/enforce-uniqueness.js | 46 ++++++++ app/behaviors/row-focus.js | 1 + app/helpers/lt-row-focus.js | 10 ++ tests/dummy/app/styles/table.less | 9 ++ .../templates/components/scrolling-table.hbs | 2 + 12 files changed, 331 insertions(+), 3 deletions(-) create mode 100644 addon/behaviors/row-focus.js create mode 100644 addon/listeners/mouse-events.js create mode 100644 addon/utils/enforce-uniqueness.js create mode 100644 app/behaviors/row-focus.js create mode 100644 app/helpers/lt-row-focus.js diff --git a/addon/behaviors/row-focus.js b/addon/behaviors/row-focus.js new file mode 100644 index 00000000..c51f4416 --- /dev/null +++ b/addon/behaviors/row-focus.js @@ -0,0 +1,101 @@ +import Behavior from 'ember-light-table/behaviors/behavior'; +import { keyDown } from 'ember-keyboard'; +import { rowMouseDown } from 'ember-light-table/listeners/mouse-events'; + +export default Behavior.extend({ + + exclusionGroup: 'row-focus', + + init() { + this._super(...arguments); + this.events.onFocusToRow = [rowMouseDown('_none'), rowMouseDown('ctrl')]; + this.events.onGoDown = [keyDown('ArrowDown')]; + this.events.onGoUp = [keyDown('ArrowUp')]; + this.events.onGoPageDown = [keyDown('PageDown')]; + this.events.onGoPageUp = [keyDown('PageUp')]; + this.events.onGoFirst = [keyDown('Home')]; + this.events.onGoLast = [keyDown('End')]; + this.events.onNavSelForward = [keyDown('Enter')]; + this.events.onNavSelBackward = [keyDown('Enter+shift')]; + this.events.onClearFocus = [keyDown('Escape')]; + }, + + onFocusToRow(ltBody, ltRow, event) { + if (event.button === 0) { + let row = ltRow.get('row'); + ltBody.set('table.focusedRow', row); + } + }, + + onGoDown(ltBody) { + ltBody.set('table.focusIndex', ltBody.get('table.focusIndex') + 1); + }, + + onGoUp(ltBody) { + ltBody.set('table.focusIndex', ltBody.get('table.focusIndex') - 1); + }, + + onGoPageDown(ltBody) { + ltBody.set('table.focusIndex', ltBody.get('table.focusIndex') + ltBody.get('pageSize') - 1); + }, + + onGoPageUp(ltBody) { + ltBody.set('table.focusIndex', ltBody.get('table.focusIndex') - ltBody.get('pageSize') + 1); + }, + + onGoFirst(ltBody) { + ltBody.set('table.focusIndex', 0); + }, + + onGoLast(ltBody) { + ltBody.set('table.focusIndex', ltBody.get('table.rows.length') - 1); + }, + + onNavSelForward(ltBody) { + let table = ltBody.get('table'); + let rows = table.get('rows'); + let i = Math.max(0, table.get('focusIndex')); + let n = rows.get('length'); + let k = null; + for (let j = i + 1; !k && j < n; j++) { + if (rows.objectAt(j).get('selected')) { + k = j; + } + } + for (let j = 0; !k && j <= i; j++) { + if (rows.objectAt(j).get('selected')) { + k = j; + } + } + if (k) { + table.set('focusIndex', k); + } + }, + + onNavSelBackward(ltBody) { + let table = ltBody.get('table'); + let rows = table.get('rows'); + let i = Math.max(0, table.get('focusIndex')); + let n = rows.get('length'); + let k = null; + for (let j = i - 1; !k && j >= 0; j--) { + if (rows.objectAt(j).get('selected')) { + k = j; + } + } + for (let j = n - 1; !k && j >= i; j--) { + if (rows.objectAt(j).get('selected')) { + k = j; + } + } + if (k) { + table.set('focusIndex', k); + } + }, + + onClearFocus(ltBody) { + let table = ltBody.get('table'); + table.set('focusedRow', null); + } + +}); diff --git a/addon/classes/Row.js b/addon/classes/Row.js index 31f77291..0dcca459 100644 --- a/addon/classes/Row.js +++ b/addon/classes/Row.js @@ -45,6 +45,18 @@ export default class Row extends ObjectProxy.extend({ */ selected: false, + /** + * Whether the focus is on the row. + * + * CSS Classes: + * - `has-focus` + * + * @property hasFocus + * @type {Boolean} + * @default false + */ + hasFocus: false, + /** * Class names to be applied to this row * diff --git a/addon/classes/Table.js b/addon/classes/Table.js index abcbf441..33417dae 100644 --- a/addon/classes/Table.js +++ b/addon/classes/Table.js @@ -6,7 +6,8 @@ import Column from 'ember-light-table/classes/Column'; import SyncArrayProxy from 'ember-light-table/-private/sync-array-proxy'; import { mergeOptionsWithGlobals } from 'ember-light-table/-private/global-options'; import fixProto from 'ember-light-table/utils/fix-proto'; -import { isNone } from '@ember/utils'; +import { isNone, isEmpty } from '@ember/utils'; +import enforceUniqueness from 'ember-light-table/utils/enforce-uniqueness'; const RowSyncArrayProxy = SyncArrayProxy.extend({ serializeContentObjects(objects) { @@ -66,6 +67,59 @@ export default class Table extends EmberObject.extend({ */ visibleRows: computed.filterBy('rows', 'hidden', false).readOnly(), + /** + * Index of the row currently having the focus. Equals -1 if no row has the focus. + * + * @property focusIndex + * @type {Number} + */ + focusIndex: computed('focusedRow', { + get() { + let rows = this.get('rows'); + return rows.indexOf(this.get('focusedRow')); + }, + set(key, value) { + let rows = this.get('rows'); + if (!isEmpty(rows)) { + value = Math.min(rows.get('length') - 1, Math.max(0, value)); + if (value !== -1) { + this.set('focusedRow', rows.objectAt(value)); + } + } else { + value = -1; + } + return value; + } + }), + + /** + * Row currently having the focus. Equals `null` if no row has the focus. + * + * @property focusedRow + * @type {Number} + */ + focusedRow: computed('rows.{[],@each.hasFocus}', { + get() { + return this.get('rows').findBy('hasFocus'); + }, + set(key, value) { + if (value) { + value.set('hasFocus', true); + } else { + let r = this.get('focusedRow'); + if (r) { + r.set('hasFocus', false); + } + } + return value; + } + }), + + /** + * There is at most one row that has focus. + */ + _applyFocusRules: enforceUniqueness('rows', 'hasFocus'), + /** * @property sortableColumns * @type {Ember.Array} diff --git a/addon/components/lt-body.js b/addon/components/lt-body.js index 6d6092a2..fa0b25c3 100644 --- a/addon/components/lt-body.js +++ b/addon/components/lt-body.js @@ -405,7 +405,13 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi } }), - _prevSelectedIndex: -1, + /* Components to add in the scrollable content + * + * @property + * @type {[ { component, namedArgs ]} ]} + * @default [] + */ + decorations: null, scrollableContainer: computed('sharedOptions.frameId', function() { // TODO: FIX: lt-body should not know about .tse-scroll-content @@ -415,6 +421,13 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi init() { this._super(...arguments); + + if (this.get('decorations') === null) { + this.set('decorations', emberArray()); + } + + this.get('table.focusIndex'); // so the observers are triggered + this._initDefaultBehaviorsIfNeeded(); }, @@ -490,6 +503,12 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi } }, + _onFocusedRowChanged: observer('table.focusIndex', function() { + if (typeof FastBoot === 'undefined') { + run.schedule('afterRender', null, () => this.makeRowVisible(this.$('tr.has-focus'), 0.5)); + } + }), + /** * @method _debounceScrolledToBottom */ @@ -517,6 +536,32 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi return emberArray($.makeArray(q.map((i, e) => vrm[e.id]))); }).volatile(), + getLtRowAt(position) { + return this + .get('ltRows') + .find((ltr) => { + let top = ltr.get('top'); + return top <= position && position < top + ltr.get('height'); + }); + }, + + pageSize: computed(function() { + let rows = this.get('table.rows'); + if (rows.get('length') === 0) { + return 0; + } + let r0 = this.getLtRowAt(0); + if (!r0) { + r0 = this.get('ltRows').get('firstObject'); + } + let rN = this.getLtRowAt(this.get('$scrollableContainer').height()); + if (!rN) { + rN = this.get('ltRows').get('lastObject'); + } + let i = (r) => rows.indexOf(r.get('row')); + return i(rN) - i(r0); + }).volatile().readOnly(), + signalSelectionChanged() { this.get('behaviors').forEach((b) => b.onSelectionChanged(this)); }, diff --git a/addon/components/lt-row.js b/addon/components/lt-row.js index 01d1fefc..76dd0cab 100644 --- a/addon/components/lt-row.js +++ b/addon/components/lt-row.js @@ -8,7 +8,17 @@ const Row = Component.extend({ layout, tagName: 'tr', classNames: ['lt-row'], - classNameBindings: ['isSelected', 'isExpanded', 'canExpand:is-expandable', 'canSelect:is-selectable', 'row.classNames'], + + classNameBindings: [ + 'isSelected', + 'isExpanded', + 'hasFocus', + 'canExpand:is-expandable', + 'canSelect:is-selectable', + 'canFocus:is-focusable', + 'row.classNames' + ], + attributeBindings: ['colspan', 'data-row-id'], columns: null, @@ -17,10 +27,12 @@ const Row = Component.extend({ extra: null, canExpand: false, canSelect: false, + canFocus: false, colspan: 1, isSelected: computed.readOnly('row.selected'), isExpanded: computed.readOnly('row.expanded'), + hasFocus: computed.readOnly('row.hasFocus'), ltBody: null, @@ -28,6 +40,22 @@ const Row = Component.extend({ return this.get('ltBody').$(); }).volatile().readOnly(), + left: computed(function() { + return this.$().offset().left - this.get('$ltBody').offset().left; + }).volatile().readOnly(), + + width: computed(function() { + return this.$().width(); + }).volatile().readOnly(), + + top: computed(function() { + return this.$().offset().top - this.get('$ltBody').offset().top; + }).volatile().readOnly(), + + height: computed(function() { + return this.$().height(); + }).volatile().readOnly(), + _onClick: on('click', function() { if (this.rowClick) { this.rowClick(this, ...arguments); diff --git a/addon/listeners/mouse-events.js b/addon/listeners/mouse-events.js new file mode 100644 index 00000000..01822664 --- /dev/null +++ b/addon/listeners/mouse-events.js @@ -0,0 +1,19 @@ +import listenerName from 'ember-light-table/utils/listener-name'; + +const formattedListener = function formattedListener(type, keysString) { + const keys = keysString !== undefined ? keysString.split('+') : []; + return listenerName(type, keys); +}; + +export function rowMouseDown(keys) { + return formattedListener('rowMouseDown', keys); +} + +export function rowMouseUp(keys) { + return formattedListener('rowMouseUp', keys); +} + +export function rowMouseMove(keys) { + return formattedListener('rowMouseMove', keys); +} + diff --git a/addon/templates/components/lt-body.hbs b/addon/templates/components/lt-body.hbs index 125330ae..7ac5d30b 100644 --- a/addon/templates/components/lt-body.hbs +++ b/addon/templates/components/lt-body.hbs @@ -27,6 +27,7 @@ enableScaffolding=enableScaffolding canExpand=canExpand canSelect=canSelect + canFocus=canFocus rowClick=(action 'onRowClick') rowDoubleClick=(action 'onRowDoubleClick') rowMouseDown=(action 'onRowMouseDown') diff --git a/addon/utils/enforce-uniqueness.js b/addon/utils/enforce-uniqueness.js new file mode 100644 index 00000000..abe083aa --- /dev/null +++ b/addon/utils/enforce-uniqueness.js @@ -0,0 +1,46 @@ +import { isEmpty } from '@ember/utils'; +import { A as emberArray } from '@ember/array'; +import { observer } from '@ember/object'; +import { on } from '@ember/object/evented'; + +/** + * Makes sure that there is at most one flag that is set in the array. + */ +export default function(arrayName, flagName) { + let i = -1; // previous index + let f = function() { + let a = this.get(arrayName); + if (!isEmpty(a)) { + let on = emberArray(a.filterBy(flagName)); + let n = on.get('length'); + if (n !== 0) { + if (n > 1 && i >= 0 && i <= a.get('length')) { + // removes the last object that had a flag that is still "on" + let e = a.objectAt(i); + e.set(flagName, false); + on.removeObject(e); + n--; + } + for (let j = 1; j < n; j++) { + on.objectAt(j).set(flagName, false); + } + let on0 = on.objectAt(0); + i = a.indexOf(on0); + } + } + }; + let running = false; + return on( + 'init', + observer(`${arrayName}.[]`, `${arrayName}.@each.${flagName}`, function() { + if (!running) { + running = true; + try { + f.call(this); + } finally { + running = false; + } + } + }) + ); +} diff --git a/app/behaviors/row-focus.js b/app/behaviors/row-focus.js new file mode 100644 index 00000000..18ea4503 --- /dev/null +++ b/app/behaviors/row-focus.js @@ -0,0 +1 @@ +export { default } from 'ember-light-table/behaviors/row-focus.js'; diff --git a/app/helpers/lt-row-focus.js b/app/helpers/lt-row-focus.js new file mode 100644 index 00000000..819a978c --- /dev/null +++ b/app/helpers/lt-row-focus.js @@ -0,0 +1,10 @@ +import Helper from '@ember/component/helper'; +import FocusBehavior from 'ember-light-table/behaviors/row-focus'; + +export default Helper.extend({ + + compute() { + return FocusBehavior.create(); + } + +}); diff --git a/tests/dummy/app/styles/table.less b/tests/dummy/app/styles/table.less index 6c9eb667..f9b7e7c8 100644 --- a/tests/dummy/app/styles/table.less +++ b/tests/dummy/app/styles/table.less @@ -44,6 +44,15 @@ } } + .lt-body-wrap:focus { + .lt-row.has-focus { + outline-color: black; + outline-style: inset; + outline-width: 1px; + outline-offset: -1px; + } + } + .lt-row { height: 50px; diff --git a/tests/dummy/app/templates/components/scrolling-table.hbs b/tests/dummy/app/templates/components/scrolling-table.hbs index 77d8b736..109fa835 100644 --- a/tests/dummy/app/templates/components/scrolling-table.hbs +++ b/tests/dummy/app/templates/components/scrolling-table.hbs @@ -27,8 +27,10 @@ iconDescending='fa fa-sort-desc' }} + {{#t.body useLegacyBehaviorFlags=false + behaviors=(array (lt-row-focus)) scrollToRow=scrollToRow onScrolledToBottom=(action 'onScrolledToBottom') scrollTop=zone.scrollTop From a299d2923ca2291369ddeb91c7f287509dd9b6ab Mon Sep 17 00:00:00 2001 From: Pierre-Luc Carmel Biron Date: Sun, 24 Mar 2019 22:38:21 -0400 Subject: [PATCH 5/6] Spreadsheet --- addon/behaviors/common/row-range.js | 159 +++++++++++++ addon/behaviors/common/row-ranges.js | 217 ++++++++++++++++++ addon/behaviors/spreadsheet-select.js | 137 +++++++++++ addon/components/lt-body.js | 47 ++-- addon/components/lt-row-range.js | 186 +++++++++++++++ addon/components/lt-row.js | 14 +- addon/components/lt-selection-handle.js | 180 +++++++++++++++ addon/components/lt-standard-scrollable.js | 10 +- addon/styles/addon.css | 8 + addon/templates/components/lt-body.hbs | 5 + addon/templates/components/lt-row-range.hbs | 22 ++ .../components/lt-selection-handle.hbs | 1 + app/behaviors/behavior.js | 1 + app/behaviors/common/row-range.js | 1 + app/behaviors/spreadsheet-select.js | 1 + app/components/lt-row-range.js | 1 + app/components/lt-selection-handle.js | 1 + app/helpers/lt-spreadsheet-select.js | 10 + .../app/components/rows/spreadsheet-table.js | 52 +++++ tests/dummy/app/router.js | 1 + tests/dummy/app/routes/rows/spreadsheet.js | 1 + tests/dummy/app/styles/table.less | 99 +++++++- tests/dummy/app/templates/application.hbs | 3 + .../components/rows/spreadsheet-table.hbs | 84 +++++++ .../dummy/app/templates/rows/spreadsheet.hbs | 12 + 25 files changed, 1215 insertions(+), 38 deletions(-) create mode 100644 addon/behaviors/common/row-range.js create mode 100644 addon/behaviors/common/row-ranges.js create mode 100644 addon/behaviors/spreadsheet-select.js create mode 100644 addon/components/lt-row-range.js create mode 100644 addon/components/lt-selection-handle.js create mode 100644 addon/templates/components/lt-row-range.hbs create mode 100644 addon/templates/components/lt-selection-handle.hbs create mode 100644 app/behaviors/behavior.js create mode 100644 app/behaviors/common/row-range.js create mode 100644 app/behaviors/spreadsheet-select.js create mode 100644 app/components/lt-row-range.js create mode 100644 app/components/lt-selection-handle.js create mode 100644 app/helpers/lt-spreadsheet-select.js create mode 100644 tests/dummy/app/components/rows/spreadsheet-table.js create mode 100644 tests/dummy/app/routes/rows/spreadsheet.js create mode 100644 tests/dummy/app/templates/components/rows/spreadsheet-table.hbs create mode 100644 tests/dummy/app/templates/rows/spreadsheet.hbs diff --git a/addon/behaviors/common/row-range.js b/addon/behaviors/common/row-range.js new file mode 100644 index 00000000..d44f5d97 --- /dev/null +++ b/addon/behaviors/common/row-range.js @@ -0,0 +1,159 @@ +import EmberObject, { computed } from '@ember/object'; +import Evented, { on } from '@ember/object/evented'; +import { run } from '@ember/runloop'; + +export default EmberObject.extend(Evented, { + + // passed-in + a: null, // the default anchor point + b: null, // the other point + anchorIsB: false, + anchorAdjustment: 0, + + realA: computed('anchorIsB', 'a', 'anchorAdjustment', { + get() { + let a = this.get('a'); + return this.get('anchorIsB') ? a : a + this.get('anchorAdjustment'); + }, + set(key, value) { + this.set('a', this.get('anchorIsB') ? value : value - this.get('anchorAdjustment')); + return value; + } + }), + + realB: computed('anchorIsB', 'b', 'anchorAdjustment', { + get() { + let b = this.get('b'); + return !this.get('anchorIsB') ? b : b + this.get('anchorAdjustment'); + }, + set(key, value) { + this.set('b', !this.get('anchorIsB') ? value : value - this.get('anchorAdjustment')); + return value; + } + }), + + first: computed('a', 'b', { + get() { + return Math.min(this.get('a'), this.get('b')); + }, + set(key, value) { + if (this.get('a') <= this.get('b')) { + this.set('a', value); + } else { + this.set('b', value); + } + return value; + } + }), + + last: computed('a', 'b', { + get() { + return Math.max(this.get('a'), this.get('b')); + }, + set(key, value) { + if (this.get('a') > this.get('b')) { + this.set('a', value); + } else { + this.set('b', value); + } + return value; + } + }), + + realFirst: computed('realA', 'realB', { + get() { + return Math.min(this.get('realA'), this.get('realB')); + }, + set(key, value) { + if (this.get('realA') <= this.get('realB')) { + this.set('realA', value); + } else { + this.set('realB', value); + } + return value; + } + }), + + realLast: computed('realA', 'realB', { + get() { + return Math.max(this.get('realA'), this.get('realB')); + }, + set(key, value) { + if (this.get('realA') > this.get('realB')) { + this.set('realA', value); + } else { + this.set('realB', value); + } + return value; + } + }), + + normalize() { + this.setProperties({ + a: this.get('realA'), + b: this.get('realB'), + anchorAdjustment: 0 + }); + if (this.get('anchorIsB')) { + let a = this.get('a'); + let b = this.get('b'); + this.setProperties({ + a: b, + b: a, + anchorIsB: false + }); + } + }, + + move(ltBody, pivot, direction) { + let a = this.get('a'); + let b = this.get('b'); + let n = ltBody.get('table.rows.length'); + let clip = (i) => Math.min(Math.max(0, i), n - 1); + let i; + if (a === b) { + i = clip(b + direction); + this.set('b', i); + } else { + if (pivot === -1) { + pivot = a; + } + if (pivot === a) { + i = clip(b + direction); + this.set('b', i); + } else if (pivot === b) { + i = clip(a + direction); + this.set('a', i); + } else if (direction > 0) { + i = clip(this.get('last') + direction); + this.set('last', i); + } else { + i = clip(this.get('first') + direction); + this.set('first', i); + } + } + run.schedule('afterRender', null, () => ltBody.makeRowAtVisible(i, 0.5)); + }, + + addDecorations(ltBody) { + ltBody.get('decorations').pushObject({ + component: 'lt-row-range', + namedArgs: { range: this } + }); + }, + + removeDecorations(ltBody) { + let decorations = ltBody.get('decorations'); + decorations.removeObject(decorations.findBy('namedArgs.range', this)); + }, + + _onHandleMove: on('move', function() { + this.trigger('handleMove', ...arguments); + }), + + _onHandleDrop: on('drop', function() { + this.trigger('handleDrop', ...arguments); + }) + +}); + diff --git a/addon/behaviors/common/row-ranges.js b/addon/behaviors/common/row-ranges.js new file mode 100644 index 00000000..f53fd9cd --- /dev/null +++ b/addon/behaviors/common/row-ranges.js @@ -0,0 +1,217 @@ +import { A as emberArray } from '@ember/array'; +import EmberObject, { computed } from '@ember/object'; +import { run } from '@ember/runloop'; +import RowRange from './row-range'; + +function positionFrom(table, rowOrPosition) { + let rows = table.get('rows'); + return typeof rowOrPosition === 'number' + ? Math.max(0, Math.min(rows.get('length') - 1, rowOrPosition)) + : rows.indexOf(rowOrPosition); +} + +export default EmberObject.extend({ + + /* + * A cell is selected if it is inside an odd number of ranges. + */ + list: null, + + init() { + this._super(...arguments); + this.list = emberArray(); + }, + + isEmpty: computed('list.length', function() { + return !this.get('list.length'); + }), + + clear() { + this.list.clear(); + }, + + startNewRange(i) { + let rn = this._createNewRange(i); + this.list.insertAt(0, rn); + }, + + updateFirstRange(ltBody, rowOrPosition) { + let b = positionFrom(ltBody.get('table'), rowOrPosition); + this.removeDecorations(ltBody); + try { + this.list.objectAt(0).set('b', b); + } finally { + this.noSimplification(ltBody); + } + run.schedule('afterRender', null, () => ltBody.makeRowAtVisible(b, 0.5)); + }, + + moveFirstRange(ltBody, direction) { + let { list } = this; + let focusIndex = ltBody.get('table.focusIndex'); + if (list.get('length')) { + list.objectAt(0).move(ltBody, focusIndex, direction); + } else { + this.extendRangeTo(ltBody, focusIndex + direction); + } + }, + + extendRangeTo(ltBody, rowOrPosition) { + let table = ltBody.get('table'); + let b = positionFrom(table, rowOrPosition); + let { list } = this; + if (list.get('length')) { + let rn = list.objectAt(0); + rn.set('b', b); + } else { + let a = table.get('focusIndex'); + if (a === -1) { + a = b; + } + let rn = this._createNewRange(a, b); + list.pushObject(rn); + } + run.schedule('afterRender', null, () => ltBody.makeRowAtVisible(b, 0.5)); + }, + + removeDecorations(ltBody) { + this.list.forEach((r) => r.removeDecorations(ltBody)); + }, + + thenSimplify(ltBody) { + this.removeDecorations(ltBody); + this._simplifyRanges(ltBody); + this.addDecorations(ltBody); + }, + + noSimplification(ltBody) { + this._syncSelection(ltBody.get('table')); + this.addDecorations(ltBody); + }, + + immediateSimplification(ltBody) { + this._syncSelection(ltBody.get('table')); + this._simplifyRanges(ltBody); + this.addDecorations(ltBody); + }, + + updateRangesIfNeeded(ltBody) { + let rows = ltBody.get('table.rows'); + let n = rows.get('length'); + let v1 = rows.mapBy('selected'); + let v2 = this._getVectorFromRanges(n); + let upToDate = true; + for (let i = 0; upToDate && i < n; i++) { + upToDate = v1.objectAt(i) === v2.objectAt(i); + } + if (!upToDate) { + this.removeDecorations(ltBody); + try { + this._generateRangesFromVector(v1); + } finally { + this.addDecorations(ltBody); + } + } + }, + + _syncSelection(table) { + table + .get('rows') + .forEach((r, i) => r.set('selected', this._findRanges(i).get('length') % 2 === 1)); + }, + + _findRanges(i) { + return emberArray( + this.list.filter((rn) => rn.get('realFirst') <= i && i <= rn.get('realLast')) + ); + }, + + _createNewRange(a, b = a) { + let rn = RowRange.create({ a, b }); + rn.on('handleMove', this, this._onHandleMove); + rn.on('handleDrop', this, this._onHandleDrop); + return rn; + }, + + _generateRangesFromVector(isSelected) { + let { list } = this; + let anchor; + if (list.get('length')) { + anchor = list.objectAt(0).get('a'); + } + list.clear(); + let rn = null; + let n = isSelected.get('length'); + for (let i = 0; i <= n; i++) { + let isSel = isSelected.objectAt(i); + if (rn && !isSel) { + rn.set('b', i - 1); + list.pushObject(rn); + rn = null; + } else if (!rn && isSel) { + rn = this._createNewRange(i); + } + } + if (rn) { + rn.set('b', n); + list.pushObject(rn); + } + let rn0 = list.find((rn) => rn.get('a') === anchor || rn.get('b') === anchor); + if (rn0) { + let a = rn0.get('a'); + let b = rn0.get('b'); + if (a !== anchor) { + rn0.setProperties({ a: b, b: a }); + } + list.removeObject(rn0); + list.insertAt(0, rn0); + } + }, + + _getVectorFromRanges(n) { + let isSelected = emberArray(); + for (let i = 0; i <= n; i++) { + isSelected.pushObject(this._findRanges(i).get('length') % 2 === 1); + } + return isSelected; + }, + + _simplifyRanges(ltBody) { + this._generateRangesFromVector(this._getVectorFromRanges(ltBody.get('table.rows.length'))); + }, + + addDecorations(ltBody) { + this.list.forEach((r) => r.addDecorations(ltBody)); + }, + + _updateRange(ltBody, range, pointName, position, direction) { + let ltDropRow = ltBody.getLtRowAt(position); + if (ltDropRow) { + let ltRows = ltBody.get('ltRows'); + let i = ltRows.indexOf(ltDropRow); + let side = (ltDropRow.get('top') + ltDropRow.get('height') / 2 - position); + if (side * direction > 0) { + i -= direction; + } + i = Math.max(0, Math.min(i, ltBody.get('table.rows.length'))); + let realFirst = range.get('realFirst'); + let realLast = range.get('realLast'); + if (!(direction < 0 && i > realLast) && !(direction > 0 && i < realFirst)) { + range.set(pointName, i); + this._syncSelection(ltBody.get('table')); + run.schedule('afterRender', null, () => ltBody.makeRowAtVisible(i, 0.5)); + } + } + }, + + _onHandleMove() { + this._updateRange(...arguments); + }, + + _onHandleDrop(ltBody, range) { + this._updateRange(...arguments); + range.normalize(); + this.thenSimplify(ltBody); + } + +}); diff --git a/addon/behaviors/spreadsheet-select.js b/addon/behaviors/spreadsheet-select.js new file mode 100644 index 00000000..b0f65334 --- /dev/null +++ b/addon/behaviors/spreadsheet-select.js @@ -0,0 +1,137 @@ +import { keyDown, keyUp } from 'ember-keyboard'; +import SelectAll from './select-all'; +import RowRanges from './common/row-ranges'; +import { rowMouseUp, rowMouseDown, rowMouseMove } from 'ember-light-table/listeners/mouse-events'; + +export default SelectAll.extend({ + + init() { + this._super(...arguments); + this.events.onExtendRange = [rowMouseDown('shift')]; + this.events.onRangeDown = [keyDown('ArrowDown+shift')]; + this.events.onRangeUp = [keyDown('ArrowUp+shift')]; + this.events.onDeselectAll = [rowMouseDown('_none'), keyDown('ArrowDown'), keyDown('ArrowUp'), keyDown('Escape')]; + this.events.onStopRangeUpDown = [keyUp('ShiftLeft')]; + this.events.onRowMouseStartNewSelection = [rowMouseDown('_none')]; + this.events.onRowMouseStartAddRange = [rowMouseDown('cmd')]; + this.events.onRowMouseEndNewSelection = [rowMouseUp('_all')]; + this.events.onRowMouseEndAddRange = [rowMouseUp('_all')]; + this.events.onRowMouseNewSelectionMove = [rowMouseMove('_all')]; + this.events.onRowMouseAddRangeMove = [rowMouseMove('_all')]; + this.ranges = RowRanges.create({}); + }, + + _rangeUpDownActive: false, + _mouseSelectionAnchor: null, + _mouseNewSelectionActive: false, + _mouseAddRangeActive: false, + + ranges: null, + + onSelectionChanged(ltBody) { + this._super(...arguments); + this.ranges.updateRangesIfNeeded(ltBody); + }, + + onExtendRange(ltBody, ltRow) { + this.ranges.removeDecorations(ltBody); + try { + let row = ltRow.get('row'); + this.ranges.extendRangeTo(ltBody, row); + } finally { + this.ranges.immediateSimplification(ltBody); + } + }, + + onRangeDown(ltBody) { + this._rangeUpDownActive = true; + this.ranges.removeDecorations(ltBody); + try { + this.ranges.moveFirstRange(ltBody, 1); + } finally { + this.ranges.noSimplification(ltBody); + } + }, + + onRangeUp(ltBody) { + this._rangeUpDownActive = true; + this.ranges.removeDecorations(ltBody); + try { + this.ranges.moveFirstRange(ltBody, -1); + } finally { + this.ranges.noSimplification(ltBody); + } + }, + + onDeselectAll(ltBody) { + let args = arguments; + let event = args[args.length - 1]; + if (event && (args.length === 3 && event.button === 0 || args.length === 2)) { + ltBody.get('table').deselectAll(); + } + }, + + onStopRangeUpDown(ltBody) { + if (this._rangeUpDownActive) { + this._rangeUpDownActive = false; + this.ranges.thenSimplify(ltBody); + } + }, + + onRowMouseStartNewSelection(ltBody, ltRow, event) { + if (event.button === 0) { + this._mouseSelectionAnchor = ltBody.get('table.rows').indexOf(ltRow.get('row')); + } + }, + + onRowMouseStartAddRange(ltBody, ltRow, event) { + if (event.button === 0) { + this.ranges.removeDecorations(ltBody); + try { + let i = ltBody.get('table.rows').indexOf(ltRow.get('row')); + this.ranges.startNewRange(i); + this._mouseAddRangeActive = true; + this._mouseSelectionAnchor = i; + } finally { + this.ranges.noSimplification(ltBody); + } + } + }, + + onRowMouseEndNewSelection() { + this._mouseSelectionAnchor = null; + this._mouseNewSelectionActive = false; + }, + + onRowMouseEndAddRange(ltBody) { + if (this._mouseAddRangeActive) { + this._mouseSelectionAnchor = null; + this._mouseAddRangeActive = false; + this.ranges.thenSimplify(ltBody); + } + }, + + onRowMouseNewSelectionMove(ltBody, ltRow, event) { + if (event.button === 0 && !this._mouseAddRangeActive) { + if (!this._mouseNewSelectionActive + && this._mouseSelectionAnchor !== null + // && this._mouseSelectionAnchor !== i + ) { + this.ranges.startNewRange(this._mouseSelectionAnchor); + this._mouseNewSelectionActive = true; + } + if (this._mouseNewSelectionActive && !this.ranges.get('isEmpty')) { + this.ranges.updateFirstRange(ltBody, ltRow.get('row')); + } + } + }, + + onRowMouseAddRangeMove(ltBody, ltRow, event) { + if (event.button === 0 && !this._mouseNewSelectionActive) { + if (this._mouseAddRangeActive && !this.ranges.get('isEmpty')) { + this.ranges.updateFirstRange(ltBody, ltRow.get('row')); + } + } + } + +}); diff --git a/addon/components/lt-body.js b/addon/components/lt-body.js index fa0b25c3..074051dc 100644 --- a/addon/components/lt-body.js +++ b/addon/components/lt-body.js @@ -4,7 +4,6 @@ import { computed, observer } from '@ember/object'; import { getOwner } from '@ember/application'; import { debounce, run, schedule } from '@ember/runloop'; import { warn } from '@ember/debug'; -import $ from 'jquery'; import layout from 'ember-light-table/templates/components/lt-body'; import { EKMixin } from 'ember-keyboard'; import ActivateKeyboardOnFocusMixin from 'ember-keyboard/mixins/activate-keyboard-on-focus'; @@ -315,7 +314,7 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi if (row) { let ltRow = this.get('ltRows').findBy('row', row); if (ltRow) { - schedule('afterRender', () => this.makeRowVisible(ltRow.$())); + schedule('afterRender', () => this.makeRowVisible(ltRow.get('element'))); } else { throw 'Row passed to scrollToRow() is not part of the rendered table.'; } @@ -421,13 +420,11 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi init() { this._super(...arguments); - if (this.get('decorations') === null) { this.set('decorations', emberArray()); } - this.get('table.focusIndex'); // so the observers are triggered - + this.__preventPropagation = (e) => this._preventPropagation(e); this._initDefaultBehaviorsIfNeeded(); }, @@ -442,17 +439,17 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi didInsertElement() { this._super(...arguments); - $(document).on('keydown', this, this._preventPropagation); + document.addEventListener('keydown', this.__preventPropagation); }, willDestroyElement() { this._super(...arguments); - $(document).off('keydown', this, this._preventPropagation); + document.removeEventListener('keydown', this.__preventPropagation); }, _preventPropagation(e) { - if (e.target === e.data.element && [32, 33, 34, 35, 36, 38, 40].includes(e.keyCode)) { - return false; + if (e.target === this.get('element') && [32, 33, 34, 35, 36, 38, 40].includes(e.keyCode)) { + return e.preventDefault(); } }, @@ -469,25 +466,25 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi }, makeRowAtVisible(i, nbExtraRows = 0) { - this.makeRowVisible(this.get('ltRows').objectAt(i).$(), nbExtraRows); + this.makeRowVisible(this.get('ltRows').objectAt(i).get('element'), nbExtraRows); }, - $scrollableContainer: computed(function() { - return this.$().parents('.lt-scrollable'); + scrollableContainerElement: computed(function() { + return this.get('element').closest('.lt-scrollable'); }).volatile().readOnly(), - $scrollableContent: computed(function() { - return this.$().parents('.scrollable-content'); + scrollableContentElement: computed(function() { + return this.get('element').closest('.scrollable-content'); }).volatile().readOnly(), - makeRowVisible($row, nbExtraRows = 0) { - let $scrollableContent = this.get('$scrollableContent'); - let $scrollableContainer = this.get('$scrollableContainer'); - if ($row.length !== 0 && $scrollableContent.length !== 0 && $scrollableContainer.length !== 0) { - let rt = $row.offset().top - $scrollableContent.offset().top; - let rh = $row.height(); + makeRowVisible(rowElement, nbExtraRows = 0) { + let scrollableContentElement = this.get('scrollableContentElement'); + let scrollableContainerElement = this.get('scrollableContainerElement'); + if (rowElement && scrollableContentElement && scrollableContainerElement) { + let rt = rowElement.getBoundingClientRect().top - scrollableContentElement.getBoundingClientRect().top; + let rh = rowElement.clientHeight; let rb = rt + rh; - let h = $scrollableContainer.height(); + let h = scrollableContainerElement.clientHeight; let t = this.get('scrollTop'); let b = t + h; let extraSpace = rh * nbExtraRows; @@ -505,7 +502,7 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi _onFocusedRowChanged: observer('table.focusIndex', function() { if (typeof FastBoot === 'undefined') { - run.schedule('afterRender', null, () => this.makeRowVisible(this.$('tr.has-focus'), 0.5)); + run.schedule('afterRender', null, () => this.makeRowVisible(this.get('element').querySelector('tr.has-focus'), 0.5)); } }), @@ -532,8 +529,8 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi ltRows: computed(function() { let vrm = getOwner(this).lookup('-view-registry:main'); - let q = this.$('tr:not(.lt-expanded-row)'); - return emberArray($.makeArray(q.map((i, e) => vrm[e.id]))); + let rowElements = this.get('element').querySelectorAll('tr:not(.lt-expanded-row)'); + return emberArray([...rowElements].map((e) => vrm[e.id])); }).volatile(), getLtRowAt(position) { @@ -554,7 +551,7 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi if (!r0) { r0 = this.get('ltRows').get('firstObject'); } - let rN = this.getLtRowAt(this.get('$scrollableContainer').height()); + let rN = this.getLtRowAt(this.get('scrollableContainerElement').clientHeight); if (!rN) { rN = this.get('ltRows').get('lastObject'); } diff --git a/addon/components/lt-row-range.js b/addon/components/lt-row-range.js new file mode 100644 index 00000000..a1a653db --- /dev/null +++ b/addon/components/lt-row-range.js @@ -0,0 +1,186 @@ +/* eslint ember/no-on-calls-in-components:off */ +/* eslint ember/no-side-effects:off */ +import Component from '@ember/component'; +import { computed, observer } from '@ember/object'; +import { on } from '@ember/object/evented'; +import { htmlSafe } from '@ember/string'; +import { run } from '@ember/runloop'; +import layout from '../templates/components/lt-row-range'; + +export default Component.extend({ + + layout, + + // passed in + namedArgs: null, + ltBody: null, + + range: computed.reads('namedArgs.range'), + a: null, + b: null, + startA: null, + startB: null, + offsetA: 0, + offsetB: 0, + movingA: false, + movingB: false, + + _getPosition(pointName) { + let i = this.get(pointName); + let r = this.get('ltBody.ltRows').objectAt(i); + let top = r.get('top'); + let directionA = this.get('directionA'); + if (pointName === 'a' && directionA < 0 || pointName === 'b' && directionA > 0) { + return top; + } else { + return top + r.get('height'); + } + }, + + _updateA: observer('range.a', 'range.b', function() { + run.once(this, this.__updateA); + }), + + __updateA() { + if (!this.get('movingA')) { + let a = this.get('range.a'); + this.set('a', a); + this.set('startA', this._getPosition('a')); + } + }, + + _updateB: observer('range.a', 'range.b', function() { + run.once(this, this.__updateB); + }), + + __updateB() { + if (!this.get('movingB')) { + let b = this.get('range.b'); + this.set('b', b); + this.set('startB', this._getPosition('b')); + } + }, + + _updateAnchor: observer('movingA', 'movingB', function() { + run.once(this, this.__updateAnchor); + }), + + __updateAnchor() { + let ma = this.get('movingA'); + let mb = this.get('movingB'); + let rn = this.get('range'); + if (ma && !mb) { + rn.set('anchorIsB', true); + } else if (mb && !ma) { + rn.set('anchorIsB', false); + } + }, + + init() { + this._super(...arguments); + this.get('inverse'); + this.set('a', this.get('range.a')); + this.set('b', this.get('range.b')); + }, + + _computePosition: on('didInsertElement', function() { + this._super(...arguments); + this._updateA(); + this._updateB(); + this.set('boxStyle', this.get('__boxStyle')); + }), + + positionA: computed('startA', 'offsetA', { + get() { + return this.get('startA') + this.get('offsetA'); + }, + set(key, value) { + this.set('offsetA', value - this.get('startA')); + return value; + } + }), + + positionB: computed('startB', 'offsetB', { + get() { + return this.get('startB') + this.get('offsetB'); + }, + set(key, value) { + this.set('offsetB', value - this.get('startB')); + return value; + } + }), + + directionA: computed('a', 'b', function() { + return this.get('a') <= this.get('b') ? -1 : 1; + }).readOnly(), + + directionB: computed('directionA', function() { + return -this.get('directionA'); + }).readOnly(), + + inverse: computed('directionA', 'movingA', 'movingB', 'positionA', 'positionB', function() { + return (this.get('movingA') || this.get('movingB')) + && (this.get('directionA') < 0) !== (this.get('positionA') <= this.get('positionB')); + }).readOnly(), + + _updateAnchorAdjustment: observer('inverse', 'directionA', 'range.anchorIsB', function() { + run.once(this, this.__updateAnchorAdjustment); + }), + + __updateAnchorAdjustment() { + let inv = this.get('inverse'); + let dA = this.get('directionA'); + this.set('range.anchorAdjustment', !inv ? 0 : this.get('range.anchorIsB') ? -dA : dA); + }, + + __boxStyle: computed(function() { + let top = this.get('positionA'); + let height = this.get('positionB') - top; + if (height < 0) { + top += height; + height = -height; + } + let r = this.get('ltBody.ltRows').objectAt(0); + let left = r.get('left'); + let width = r.get('width'); + return htmlSafe(`left: ${left}px; width: ${width}px; top: ${top}px; height: ${height}px;`); + }).volatile().readOnly(), + + _boxStyle: null, + + boxStyle: computed('positionA', 'positionB', { + get() { + let style = this.get('_boxStyle'); + if (style) { + this.set('_boxStyle', null); + return style; + } else if (this.get('ltBody')) { + return this.get('__boxStyle'); + } + }, + set(key, value) { + this.set('_boxstyle', value); + return value; + } + }), + + _updateOffset(pointName, position) { + let X = pointName.toUpperCase(); + this.set(`position${X}`, position); + }, + + actions: { + onDrag(pointName) { + this.set(`moving${pointName.toUpperCase()}`, true); + }, + onMove(pointName, position, direction) { + this._updateOffset(pointName, position); + this.get('range').trigger('move', this.get('ltBody'), this.get('range'), pointName, position, direction); + }, + onDrop(pointName, position, direction) { + this.setProperties({ offsetA: 0, offsetB: 0, movingA: false, movingB: false }); + this.get('range').trigger('drop', this.get('ltBody'), this.get('range'), pointName, position, direction); + } + } + +}); diff --git a/addon/components/lt-row.js b/addon/components/lt-row.js index 76dd0cab..b70eb0b6 100644 --- a/addon/components/lt-row.js +++ b/addon/components/lt-row.js @@ -36,24 +36,26 @@ const Row = Component.extend({ ltBody: null, - $ltBody: computed(function() { - return this.get('ltBody').$(); + ltBodyElement: computed(function() { + return this.get('ltBody.element'); }).volatile().readOnly(), left: computed(function() { - return this.$().offset().left - this.get('$ltBody').offset().left; + return this.get('element').getBoundingClientRect().left + - this.get('ltBodyElement').getBoundingClientRect().left; }).volatile().readOnly(), width: computed(function() { - return this.$().width(); + return this.get('element').clientWidth; }).volatile().readOnly(), top: computed(function() { - return this.$().offset().top - this.get('$ltBody').offset().top; + return this.get('element').getBoundingClientRect().top + - this.get('ltBodyElement').getBoundingClientRect().top; }).volatile().readOnly(), height: computed(function() { - return this.$().height(); + return this.get('element').clientHeight; }).volatile().readOnly(), _onClick: on('click', function() { diff --git a/addon/components/lt-selection-handle.js b/addon/components/lt-selection-handle.js new file mode 100644 index 00000000..fd3b6d70 --- /dev/null +++ b/addon/components/lt-selection-handle.js @@ -0,0 +1,180 @@ +/* eslint ember/no-on-calls-in-components:off */ +/* eslint ember/no-side-effects:off */ +import Component from '@ember/component'; +import { computed, observer } from '@ember/object'; +import { on } from '@ember/object/evented'; +import { run } from '@ember/runloop'; +import { htmlSafe } from '@ember/string'; +import layout from '../templates/components/lt-selection-handle'; + +export default Component.extend({ + + layout, + + classNameBindings: [':lt-selection-handle', 'isUp:lt-selection-handle-up:lt-selection-handle-down'], + attributeBindings: ['style'], + + // passed in + ltBody: null, + rowIndex: null, + direction: null, + inverse: false, + + _initialMousePosition: null, + _oldUserSelect: null, + offset: 0, + + init() { + this._super(...arguments); + this.__onMouseUp = (e) => + run.scheduleOnce('afterRender', null, () => this._onMouseUp(e)); + this.__onMouseMove = (e) => + run.scheduleOnce('afterRender', null, () => this._onMouseMove(e)); + }, + + ltBodyElement: computed(function() { + return this.get('ltBody.element'); + }).volatile().readOnly(), + + ltRow: computed(function() { + let ltBody = this.get('ltBody'); + if (ltBody) { + return ltBody.get('ltRows').objectAt(this.get('rowIndex')); + } + }).volatile().readOnly(), + + isUp: computed('direction', 'inverse', function() { + let inverse = this.get('inverse'); + return this.get('direction') < 0 ? !inverse : inverse; + }).readOnly(), + + position: computed(function() { + let r = this.get('ltRow'); + let result = r.get('top'); + if (!this.get('isUp')) { + result += r.get('height'); + } + return result; + }).volatile().readOnly(), + + _getMousePosition(event) { + return event.clientY + - this.get('ltBodyElement').getBoundingClientRect().top + - window.scrollY; + }, + + _addEvents() { + document.body.addEventListener('mouseup', this.__onMouseUp); + document.body.addEventListener('mousemove', this.__onMouseMove); + }, + + _removeEvents: on('willDestroyElement', function() { + document.body.removeEventListener('mousemove', this.__onMouseMove); + document.body.removeEventListener('mouseup', this.__onMouseUp); + }), + + _onMouseDown: on('mouseDown', function(event) { + this._initialMousePosition = this._getMousePosition(event); + this._addEvents(); + this._oldUserSelect = document.body.style['user-select']; + document.body.style['user-select'] = 'none'; + if (this.drag) { + this.drag(); + } + }), + + extra: computed('direction', 'inverse', function() { + return !this.get('ltRow') + ? 0 + : this.get('inverse') + ? this.get('direction') * this.get('ltRow.height') + : 0; + }).readOnly(), + + _onMouseMove(event) { + if (this.get('isDestroyed')) { + this._removeEvents(); + } else if (this._initialMousePosition) { + let offset = this._getMousePosition(event) - this.get('_initialMousePosition'); + this.set('offset', offset); + if (this.move) { + this.move(offset + this.get('extra') + this.get('position'), this.get('isUp') ? -1 : 1); + } + } else { + this._onMouseDown(event); + } + }, + + _onMouseUp(event) { + if (!this.get('isDestroyed')) { + this._removeEvents(); + let offset = this._getMousePosition(event) - this.get('_initialMousePosition'); + document.body.style['user-select'] = this._oldUserSelect; + this._initialMousePosition = null; + this.set('offset', 0); + if (this.drop) { + this.drop(offset + this.get('extra') + this.get('position'), this.get('isUp') ? -1 : 1); + } + } + }, + + positionUp: computed(function() { + return this.get('ltRow.top'); + + }).volatile().readOnly(), + + positionDown: computed(function() { + return this.get('positionUp') + this.get('ltRow.height'); + }).volatile().readOnly(), + + _onResize: null, + + _attachResizeEventListener: on('didInsertElement', function() { + this._onResize = () => this.set('style', this.get('__style')); + window.addEventListener('resize', this._onResize); + }), + + _removeEventListener: on('willDestroyElement', function() { + window.removeEventListener('resize', this._onResize); + }), + + _forceStyleUpdate: on('didInsertElement', observer('isUp', 'extra', function() { + run.once(this, this.__forceStyleUpdate); + })), + + __forceStyleUpdate() { + this.set('style', this.get('__style')); + }, + + __style: computed(function() { + if (this.get('ltBody')) { + let isUp = this.get('isUp'); + let y = (isUp ? this.get('positionUp') : this.get('positionDown')); + let translation = this.get('offset') + this.get('extra'); + return htmlSafe(`top: ${y}px; transform: translateY(${translation}px);`); + } + }).volatile().readOnly(), + + _style: null, + + style: computed('isUp', 'offset', 'extra', 'rowIndex', { + get() { + let style = this.get('_style'); + if (style) { + this.set('_style', null); + return style; + } else { + if (this.get('ltBody')) { + return this.get('__style'); + } else { + return null; + } + } + }, + set(key, value) { + this.set('_style', value); + return value; + } + }) + +}); diff --git a/addon/components/lt-standard-scrollable.js b/addon/components/lt-standard-scrollable.js index 590902bc..f279089b 100644 --- a/addon/components/lt-standard-scrollable.js +++ b/addon/components/lt-standard-scrollable.js @@ -27,20 +27,20 @@ export default Component.extend({ }, didInsertElement() { - this.$().on('scroll', (evt) => this._onScroll(evt)); + this.get('element').addEventListener('scroll', (evt) => this._onScroll(evt)); }, _onScroll(event) { - let $ = this.$(); - if ($) { + let element = this.get('element'); + if (element) { if (this.onScroll) { - this.onScroll($.scrollTop(), event); + this.onScroll(element.scrollTop, event); } } }, _onScrollToY: observer('scrollToY', function() { - this.$().scrollTop(this.get('scrollToY')); + this.get('element').scrollTop = this.get('scrollToY'); }) }); diff --git a/addon/styles/addon.css b/addon/styles/addon.css index b61d654e..1a182662 100644 --- a/addon/styles/addon.css +++ b/addon/styles/addon.css @@ -10,6 +10,10 @@ flex-direction: column; } +.ember-light-table table { + overflow: visible; /* for decorations */ +} + .lt-table-container table { table-layout: fixed; border-collapse: collapse; @@ -145,6 +149,10 @@ td.lt-scaffolding { position: relative; } +.ember-light-table .lt-selection-handle { + left: 0; +} + .lt-frame .lt-standard-scrollable { height: 'auto'; } diff --git a/addon/templates/components/lt-body.hbs b/addon/templates/components/lt-body.hbs index 7ac5d30b..f6f22cb5 100644 --- a/addon/templates/components/lt-body.hbs +++ b/addon/templates/components/lt-body.hbs @@ -64,6 +64,11 @@ scrollBuffer=scrollBuffer }} {{/if}} +
+ {{#each decorations as |decoration|}} + {{component decoration.component ltBody=this namedArgs=decoration.namedArgs}} + {{/each}} +
{{else}}
- Drag and drop a column onto another to reorder the columns -
+ Drag and drop a column onto another to reorder the columns +
- {{one-way-select selectedFilter - options=possibleFilters - optionValuePath="valuePath" - optionLabelPath="label" - update=(action (pipe (action (mut selectedFilter)) (action 'onSearchChange'))) - }} - {{one-way-input - value=query - placeholder="Search..." - update=(action (pipe (action (mut query)) (action 'onSearchChange'))) - }} -
+ {{one-way-select selectedFilter + options=possibleFilters + optionValuePath="valuePath" + optionLabelPath="label" + update=(action (pipe (action (mut selectedFilter)) (action 'onSearchChange'))) + }} + {{one-way-input + value=query + placeholder="Search..." + update=(action (pipe (action (mut query)) (action 'onSearchChange'))) + }} +
    diff --git a/tests/dummy/app/templates/components/cookbook/table-actions-table.hbs b/tests/dummy/app/templates/components/cookbook/table-actions-table.hbs index 965c29ea..ea12851b 100644 --- a/tests/dummy/app/templates/components/cookbook/table-actions-table.hbs +++ b/tests/dummy/app/templates/components/cookbook/table-actions-table.hbs @@ -1,34 +1,53 @@ {{!-- BEGIN-SNIPPET table-actions-table --}} -{{#light-table table height='65vh' tableActions=(hash - deleteUser=(action 'deleteUser') - notifyUser=(action 'notifyUser') -) as |t|}} +
    + {{fixed-header-table-action value=fixed onChange=(action (mut fixed))}} + {{virtual-scrollbar-table-action value=virtual onChange=(action (mut virtual))}} +
    - {{!-- - In order for `fa-sort-asc` and `fa-sort-desc` icons to work, - you need to have ember-font-awesome installed or manually include - the font-awesome assets, e.g. via a CDN. - --}} +{{#lt-frame + height='65vh' + scrollbar=(if virtual 'virtual' 'standard') as |frame| +}} + {{frame.fixed-head-here}} - {{t.head - onColumnClick=(action 'onColumnClick') - iconSortable='fa fa-sort' - iconAscending='fa fa-sort-asc' - iconDescending='fa fa-sort-desc' - fixed=true - }} + {{#frame.scrollable-zone}} + {{#frame.table + table + tableActions= + (hash + deleteUser=(action 'deleteUser') + notifyUser=(action 'notifyUser') + ) + as |t| + }} - {{#t.body - useLagacyBehaviorFlags=false - onScrolledToBottom=(action 'onScrolledToBottom') - as |body| - }} - {{#if isLoading}} - {{#body.loader}} - {{table-loader}} - {{/body.loader}} - {{/if}} - {{/t.body}} + {{!-- + In order for `fa-sort-asc` and `fa-sort-desc` icons to work, + you need to have ember-font-awesome installed or manually include + the font-awesome assets, e.g. via a CDN. + --}} -{{/light-table}} + {{t.head + fixed=fixed + onColumnClick=(action 'onColumnClick') + iconSortable='fa fa-sort' + iconAscending='fa fa-sort-asc' + iconDescending='fa fa-sort-desc' + }} + + {{#t.body + useLagacyBehaviorFlags=false + onScrolledToBottom=(action 'onScrolledToBottom') + as |body| + }} + {{#if isLoading}} + {{#body.loader}} + {{table-loader}} + {{/body.loader}} + {{/if}} + {{/t.body}} + + {{/frame.table}} + {{/frame.scrollable-zone}} +{{/lt-frame}} {{!-- END-SNIPPET --}} diff --git a/tests/dummy/app/templates/components/responsive-table.hbs b/tests/dummy/app/templates/components/responsive-table.hbs index a68f85e3..4d63d414 100644 --- a/tests/dummy/app/templates/components/responsive-table.hbs +++ b/tests/dummy/app/templates/components/responsive-table.hbs @@ -1,50 +1,66 @@ {{!-- BEGIN-SNIPPET responsive-table --}} -{{#light-table table +
    + {{fixed-header-table-action value=fixed onChange=(action (mut fixed))}} + {{virtual-scrollbar-table-action value=virtual onChange=(action (mut virtual))}} +
    + +{{#lt-frame height='65vh' - responsive=true - onAfterResponsiveChange=(action 'onAfterResponsiveChange') - as |t| + scrollbar=(if virtual 'virtual' 'standard') as |frame| }} + {{frame.fixed-head-here}} + + {{#frame.scrollable-zone}} + {{#frame.table + table + responsive=true + onAfterResponsiveChange=(action 'onAfterResponsiveChange') + as |t| + }} + + {{!-- + In order for `fa-sort-asc` and `fa-sort-desc` icons to work, + you need to have ember-font-awesome installed or manually include + the font-awesome assets, e.g. via a CDN. + --}} + + {{t.head + fixed=fixed + onColumnClick=(action 'onColumnClick') + iconSortable='fa fa-sort' + iconAscending='fa fa-sort-asc' + iconDescending='fa fa-sort-desc' + }} + + {{#t.body + useLegacyBehaviorFlags=false + onScrolledToBottom=(action 'onScrolledToBottom') + as |body| + }} + {{#body.expanded-row as |row|}} + {{responsive-expanded-row table=table row=row}} + {{/body.expanded-row}} + + {{#if isLoading}} + {{#body.loader}} + {{table-loader}} + {{/body.loader}} + {{/if}} + {{/t.body}} + + {{#t.foot fixed=true as |columns|}} +
+ + Resize your browser to check out the responsive behavior + +
- - Resize your browser to check out the responsive behavior - -
-
-
- - - {{if (eq currentScrollOffset null) 'N/A' (concat currentScrollOffset 'px')}} - -
+ {{#t.body + useLegacyBehaviorFlags=false + scrollToRow=scrollToRow + onScrolledToBottom=(action 'onScrolledToBottom') + scrollTop=zone.scrollTop + onScrollTo=zone.onScrollTo + as |body| + }} + {{#if isLoading}} + {{#body.loader}} + {{table-loader}} + {{/body.loader}} + {{/if}} + {{/t.body}} -
- - {{one-way-input - update=(action (mut scrollTo)) - value=scrollTo - class="form-control" - name="scrollTo" - type="number" - min=0 - step=10 - }} -
+ {{#t.foot fixed=true as |columns|}} +
+ +
+ + + {{if (eq zone.scrollTop null) 'N/A' (concat zone.scrollTop 'px')}} + +
-
- - {{#one-way-select selectedValue - options=table.visibleRows - value=scrollToRow - update=(action (mut scrollToRow)) - class="form-control" - name="scrollToRow" - as |row| - }} - {{row.id}} - {{row.firstName}} {{row.lastName}} - {{/one-way-select}} -
- -
diff --git a/addon/templates/components/lt-row-range.hbs b/addon/templates/components/lt-row-range.hbs new file mode 100644 index 00000000..4fd99095 --- /dev/null +++ b/addon/templates/components/lt-row-range.hbs @@ -0,0 +1,22 @@ +{{lt-selection-handle + ltBody=ltBody + rowIndex=a + direction=directionA + inverse=inverse + drag=(action 'onDrag' 'a') + move=(action 'onMove' 'a') + drop=(action 'onDrop' 'a') +}} + +{{lt-selection-handle + ltBody=ltBody + rowIndex=b + direction=directionB + inverse=inverse + drag=(action 'onDrag' 'b') + move=(action 'onMove' 'b') + drop=(action 'onDrop' 'b') +}} + +
+ diff --git a/addon/templates/components/lt-selection-handle.hbs b/addon/templates/components/lt-selection-handle.hbs new file mode 100644 index 00000000..fbd1f6df --- /dev/null +++ b/addon/templates/components/lt-selection-handle.hbs @@ -0,0 +1 @@ +
diff --git a/app/behaviors/behavior.js b/app/behaviors/behavior.js new file mode 100644 index 00000000..c9a35c5f --- /dev/null +++ b/app/behaviors/behavior.js @@ -0,0 +1 @@ +export { default } from 'ember-light-table/behaviors/behavior.js'; diff --git a/app/behaviors/common/row-range.js b/app/behaviors/common/row-range.js new file mode 100644 index 00000000..2d6be157 --- /dev/null +++ b/app/behaviors/common/row-range.js @@ -0,0 +1 @@ +export { default } from 'ember-light-table/behaviors/common/row-range.js'; diff --git a/app/behaviors/spreadsheet-select.js b/app/behaviors/spreadsheet-select.js new file mode 100644 index 00000000..409fa8e4 --- /dev/null +++ b/app/behaviors/spreadsheet-select.js @@ -0,0 +1 @@ +export { default } from 'ember-light-table/behaviors/spreadsheet-select.js'; diff --git a/app/components/lt-row-range.js b/app/components/lt-row-range.js new file mode 100644 index 00000000..6537e944 --- /dev/null +++ b/app/components/lt-row-range.js @@ -0,0 +1 @@ +export { default } from 'ember-light-table/components/lt-row-range'; diff --git a/app/components/lt-selection-handle.js b/app/components/lt-selection-handle.js new file mode 100644 index 00000000..8ba0db4b --- /dev/null +++ b/app/components/lt-selection-handle.js @@ -0,0 +1 @@ +export { default } from 'ember-light-table/components/lt-selection-handle'; diff --git a/app/helpers/lt-spreadsheet-select.js b/app/helpers/lt-spreadsheet-select.js new file mode 100644 index 00000000..4ce97fa2 --- /dev/null +++ b/app/helpers/lt-spreadsheet-select.js @@ -0,0 +1,10 @@ +import Helper from '@ember/component/helper'; +import SpreadsheetSelectBehavior from 'ember-light-table/behaviors/spreadsheet-select'; + +export default Helper.extend({ + + compute() { + return SpreadsheetSelectBehavior.create(); + } + +}); diff --git a/tests/dummy/app/components/rows/spreadsheet-table.js b/tests/dummy/app/components/rows/spreadsheet-table.js new file mode 100644 index 00000000..32f59342 --- /dev/null +++ b/tests/dummy/app/components/rows/spreadsheet-table.js @@ -0,0 +1,52 @@ +// BEGIN-SNIPPET spreadsheet-table +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import TableCommon from '../../mixins/table-common'; + +export default Component.extend(TableCommon, { + + hasSelection: computed.notEmpty('table.selectedRows'), + + columns: computed(function() { + return [{ + label: 'Avatar', + valuePath: 'avatar', + width: '60px', + sortable: false, + cellComponent: 'user-avatar' + }, { + label: 'First Name', + valuePath: 'firstName', + width: '150px' + }, { + label: 'Last Name', + valuePath: 'lastName', + width: '150px' + }, { + label: 'Address', + valuePath: 'address' + }, { + label: 'State', + valuePath: 'state' + }, { + label: 'Country', + valuePath: 'country' + }]; + }), + + actions: { + selectAll() { + this.get('table').selectAll(); + }, + + deselectAll() { + this.get('table').deselectAll(); + }, + + deleteAll() { + this.get('table').removeRows(this.get('table.selectedRows')); + } + } + +}); +// END-SNIPPET diff --git a/tests/dummy/app/router.js b/tests/dummy/app/router.js index 11f77dd0..5c634817 100644 --- a/tests/dummy/app/router.js +++ b/tests/dummy/app/router.js @@ -20,6 +20,7 @@ Router.map(function() { this.route('rows', function() { this.route('expandable'); this.route('selectable'); + this.route('spreadsheet'); }); this.route('cookbook', function() { diff --git a/tests/dummy/app/routes/rows/spreadsheet.js b/tests/dummy/app/routes/rows/spreadsheet.js new file mode 100644 index 00000000..0040140d --- /dev/null +++ b/tests/dummy/app/routes/rows/spreadsheet.js @@ -0,0 +1 @@ +export { default } from '../table-route'; diff --git a/tests/dummy/app/styles/table.less b/tests/dummy/app/styles/table.less index f9b7e7c8..6388103f 100644 --- a/tests/dummy/app/styles/table.less +++ b/tests/dummy/app/styles/table.less @@ -1,5 +1,9 @@ @border-color: #DADADA; +@selected-background-color: lighten(@accent-color, 25%); +@range-border: 1px solid @accent-color; +@handle-color: darken(@accent-color, 25%); + .lt-standard-scrollable { flex: 1 0 0; } @@ -57,7 +61,7 @@ height: 50px; &.is-selected { - background-color: #DEDEDE; + background-color: @selected-background-color; } &:not(.is-selected):hover { @@ -129,9 +133,100 @@ tfoot { } } - .ember-light-table.occlusion { .lt-row td { vertical-align: middle; } } + +.ember-light-table .body-decorations { + height: 0; + overflow: visible; +} + +.ember-light-table .body-decorations .lt-selection-handle { + position: absolute; + width: 100%; + height: 0; + overflow: visible; + &.lt-selection-handle-up > div { + cursor: n-resize; + bottom: 0; + > div { + top: 1px; + } + } + &.lt-selection-handle-down > div { + cursor: s-resize; + top: 0; + > div { + top: -1px; + } + } + > div { + display: block; + width: 0px; + height: 10px; + position: absolute; + left: 50%; + > div { + width: 50px; + height: 100%; + position: relative; + left: -50%; + border-radius: 2px; + background-color: @handle-color; + opacity: 0.5; + } + } +} + +.ember-light-table .body-decorations .lt-row-range-box { + pointer-events: none; + background: transparent; + position: absolute; + border: @range-border; +} + +.explanations { + display: flex; + align-content: flex-start; + + font-size: smaller; + + h4 { + padding: 5px; + background-color: lightgray; + font-size: small; + } + + > * { + overflow: hidden; + flex: 1 1 auto; + margin: 15px 30px 15px 0; + dt { + &:after { + content: ':' + } + margin-top: 5px; + } + } + + .mouse-list, .keyboard-list { + height: 75px; + display: flex; + flex-flow: column wrap; + align-content: flex-start; + dt { + margin-top: 5px; + flex-grow: 0; + } + dd { + flex-grow: 1; + justify-self: flex-start; + } + > * { + margin: 0 10px; + } + } +} diff --git a/tests/dummy/app/templates/application.hbs b/tests/dummy/app/templates/application.hbs index 77a5eb96..e6906f82 100644 --- a/tests/dummy/app/templates/application.hbs +++ b/tests/dummy/app/templates/application.hbs @@ -49,6 +49,9 @@ {{#link-to 'rows.selectable' tagName="li"}} {{link-to 'Selectable Rows' 'rows.selectable'}} {{/link-to}} + {{#link-to 'rows.spreadsheet' tagName="li"}} + {{link-to 'Selectable Rows - Spreadsheet style' 'rows.spreadsheet'}} + {{/link-to}} {{/link-to}} diff --git a/tests/dummy/app/templates/components/rows/spreadsheet-table.hbs b/tests/dummy/app/templates/components/rows/spreadsheet-table.hbs new file mode 100644 index 00000000..03410a42 --- /dev/null +++ b/tests/dummy/app/templates/components/rows/spreadsheet-table.hbs @@ -0,0 +1,84 @@ +{{!-- BEGIN-SNIPPET spreadsheet-table --}} +
+ {{fixed-header-table-action value=fixed onChange=(action (mut fixed))}} + {{virtual-scrollbar-table-action value=virtual onChange=(action (mut virtual))}} + {{#if hasSelection}} +
+
+ {{else}} +
+ {{/if}} +
+ +{{#lt-frame + height='75vh' + scrollbar=(if virtual 'virtual' 'standard') + as |frame| +}} + {{frame.fixed-head-here}} + + {{#frame.scrollable-zone as |zone|}} + {{#frame.table table as |t|}} + {{t.head + fixed=fixed + onColumnClick=(action 'onColumnClick') + iconAscending='fa fa-sort-asc' + iconDescending='fa fa-sort-desc' + }} + + {{#t.body + useLegacyBehaviorFlags=false + behaviors=(array (lt-row-focus) (lt-spreadsheet-select)) + scrollTop=zone.scrollTop + onScrolledToBottom=(action 'onScrolledToBottom') + onScrollTo=zone.onScrollTo + as |body| + }} + {{#if isLoading}} + {{#body.loader}} + {{table-loader}} + {{/body.loader}} + {{/if}} + + {{#if (and (not isLoading) table.isEmpty)}} + {{#body.no-data}} + {{no-data}} + {{/body.no-data}} + {{/if}} + {{/t.body}} + + {{#t.foot fixed=true as |columns|}} + + + + {{/t.foot}} + + {{/frame.table}} + {{/frame.scrollable-zone}} + + {{frame.fixed-foot-here}} +{{/lt-frame}} +{{!-- END-SNIPPET --}} diff --git a/tests/dummy/app/templates/rows/spreadsheet.hbs b/tests/dummy/app/templates/rows/spreadsheet.hbs new file mode 100644 index 00000000..99a9ff63 --- /dev/null +++ b/tests/dummy/app/templates/rows/spreadsheet.hbs @@ -0,0 +1,12 @@ +{{#code-panel + title="Selectable Rows - Spreadsheet style" + snippets=(array + "spreadsheet-table.js" + "table-common.js" + "spreadsheet-table.hbs" + "user-avatar.hbs" + "no-data.hbs" + "table-loader.hbs" +)}} + {{rows/spreadsheet-table model=model}} +{{/code-panel}} From 27e2c9640d1db601719f2b0e67809ae54beb0e51 Mon Sep 17 00:00:00 2001 From: Pierre-Luc Carmel Biron Date: Sat, 27 Apr 2019 14:03:52 -0400 Subject: [PATCH 6/6] Fix bug: selection was not cleared --- addon/components/lt-body.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/addon/components/lt-body.js b/addon/components/lt-body.js index 074051dc..a009cd87 100644 --- a/addon/components/lt-body.js +++ b/addon/components/lt-body.js @@ -2,7 +2,7 @@ import Component from '@ember/component'; import { A as emberArray } from '@ember/array'; import { computed, observer } from '@ember/object'; import { getOwner } from '@ember/application'; -import { debounce, run, schedule } from '@ember/runloop'; +import { debounce, once, run, schedule } from '@ember/runloop'; import { warn } from '@ember/debug'; import layout from 'ember-light-table/templates/components/lt-body'; import { EKMixin } from 'ember-keyboard'; @@ -563,6 +563,10 @@ export default Component.extend(EKMixin, ActivateKeyboardOnFocusMixin, HasBehavi this.get('behaviors').forEach((b) => b.onSelectionChanged(this)); }, + onSelectionChanged: observer('table.rows.@each.selected', function() { + once(this, this.signalSelectionChanged); + }), + // Noop for closure actions onRowClick() {}, onRowDoubleClick() {},
+
+
+

Mouse

+
+
Left
Set focus
+
Left + Move
Start or expand a selection range
+
Shift + Left
Create a selection range from focus
+
Cmd + Left
Add or expand a new inversion range
+
+
+
+

Keyboard

+
+
Up / Down
Move focus
+
Shift Up / Down
Create or expand a selection range
+
Enter / Shift + Enter
Move focus inside range
+
Home / End
Go to start / end of table
+
Page Up / Page Down
Move up / down one page
+
Ctrl + A / Esc
Select / deselect all
+
+
+
+