Skip to content

Instantly share code, notes, and snippets.

@optikalefx
Created June 20, 2021 05:11
Show Gist options
  • Save optikalefx/bb624697e30564cf5d1d447cb913367f to your computer and use it in GitHub Desktop.
Save optikalefx/bb624697e30564cf5d1d447cb913367f to your computer and use it in GitHub Desktop.
New Twiddle
import Component from '@ember/component';
import EmberObject, { computed, action } from '@ember/object';
import StringUtils from '../utils/string-utils';
import Lodash from 'lodash';
/**
* Internal WbDataTable component for rendering cells. This component should not be used directly.
*/
export default Component.extend({
/**
* Ember class for this component.
*/
classNames: ['wb-data-table-cell'],
/**
* Ember class bindings for this component.
*/
classNameBindings: [
'highlightClassName',
'column.isHidden:wb-data-table-cell-hidden',
'isAlignLeft:wb-data-table-cell-align-left',
'isAlignCenter:wb-data-table-cell-align-center',
'isAlignRight:wb-data-table-cell-align-right',
'isVAlignTop:wb-data-table-cell-valign-top',
'isVAlignCenter:wb-data-table-cell-valign-center',
'isVAlignBottom:wb-data-table-cell-valign-bottom',
'editable:wb-data-table-cell-editable',
],
/**
* The value of this cell. Should mostly be null unless you specifically want this cell to force a specific value.
*/
value: null,
/**
* The field to use when computing the value for a given cell. The value of a cell is the
* intersection of a row, which should be an object, and the column.field argument.
* The field is deeply traversed against row, so if the field name contains the
* period separator, it walks into nested objects if it can.
*
* `value = row[column.field] || '';`
*/
field: null,
/**
* Horizontal alignment for this cell. May be one of the following: "left", "right", "center".
* Defaults to "left".
*/
align: 'left',
/**
* Vertical alignment for this cell. May be one of the following: "top", "center", "bottom".
* Defaults to "top".
*/
valign: 'top',
/**
* Is this column currently showing?
*/
showing: true,
/**
* Can this column be shown?
*/
enabled: true,
/**
* Must this column be shown?
*/
required: false,
/**
* Should some highlighting be applied to this column.
*
* Can be one of the following:
*
* - `red`, `red1`, `red2`, `red3`, `red4`, `red5`
* - `green`, `green1`, `green2`, `green3`, `green4`, `green5`
* - `blue`, `blue1`, `blue2`, `blue3`, `blue4`, `blue5`
* - `yellow`, `yellow1`, `yellow2`, `yellow3`, `yellow4`, `yellow5`
* - `cyan`, `cyan1`, `cyan2`, `cyan3`, `cyan4`, `cyan5`
* - `purple`, `purple1`, `purple2`, `purple3`, `purple4`, `purple5`
* - `gray`, `gray1`, `grey`, `grey1`, `gray2`, `grey2`, `gray3`, `grey3`, `gray4`, `grey4`, `gray5`, `grey5`
* - `invert`
*/
highlight: null,
/**
* What to display in this cell if the value is undefined?
*/
undefinedValue: '',
/**
* What to display in this cell if the value is null?
*/
nullValue: '',
/**
* If true each cell will indicate that it can be double clicked to bring up an editor. The
* actual code to handle the click and render an editor of some type is entirely
* up to the developer. WbDataTable will do nothing other than correctly indicate
* that the cell is "editable".
*/
editable: false,
/**
* Callback function for registering this cell with its parent table.
* Provided by WbDataTable when created. Do not override.
*/
cellAdded: null,
/**
* Callback function for unregistering this cell with its parent table.
* Provided by WbDataTable when created. Do not override.
*/
cellRemoved: null, // provided by WbDataTable, do not override.
/**
* @private
*
* Reference to the parent table associated with this cell.
*/
_table: null, // set by parent table when added.
/**
* @private
*
* internal one up counter used to force ember to recompute the value for this cell.
* Should not be manipulated except by the dynamic row value observer.
*/
_recomputeValueCounter: 0,
/**
* @private
*
* stores the last initialization of the row value observer, so we can unwind it
* if row, column, or column.field changes.
*/
_priorDynamicObserver: null,
/**
* Computed property for getting the value to render into this cell. This goes through
* three separate questions: First, does the cell have an `@value` argument and is
* it non-undefined/non-null; if so, use that. Otherwise, does the column have a
* `@valueProvider` function; if so use that. Finally, does the column have a
* `@field` function; if so use that by trying to get the value using `@field` against
* the row object.
*
* The `@field` is deeply traversed against row, so if the field name contains the
* period separator, it walks into nested objects if it can.
*
* After all that if the value is null or undefined, the @undefinedValue and @nullValue
* arguments are applied as applicable.
*/
valueComputed: computed('_recomputeValueCounter', 'column.valueProvider', 'row', 'field', 'value', function () {
// first use `@value` if provided.
let value = this.value;
// next, use the column valueProvider if provided.
if (
(value === undefined || value === null) &&
this.column &&
this.column.valueProvider &&
this.column.valueProvider instanceof Function
) {
try {
value = this.column.valueProvider(this.column, this.row);
} catch (ex) {
this.remoteLogging.log('Column valueProvider function produced an error.', ex, 'error');
}
}
// next, use the `@field` if provided
if ((value === undefined || value === null) && this.field && this.row) {
let rowval = undefined;
if (this.row instanceof EmberObject) rowval = this.row.get(this.field);
else rowval = Lodash.get(this.row, this.field);
if (rowval !== undefined) value = rowval;
}
// if value is undefined or null, replace it with the overrides for undefined or null values.
if (value === undefined) {
value = (this.column && this.column.undefinedValue) || this.undefinedValue || '';
}
if (value === null) {
value = (this.column && this.column.nullValue) || this.nullValue || '';
}
// finally return our value.
return value;
}),
/**
* Returns the column index for this cell.
*/
columnIndex: computed(
'element',
'element.parentElement',
'_table',
'_table.{_columnArray,isSelectable,_isSubrowsAllowed,_isHierarchyActive}',
function () {
if (!this.element || !this.element.parentElement) return -1;
if (!this._table) return -1;
return (
[...this.element.parentElement.children].indexOf(this.element) +
(this._table && this._table.isSelectable ? -1 : 0) +
(this._table && this._table._isSubrowsAllowed ? -1 : 0) +
(this._table && this._table._isHierarchyActive ? -1 : 0)
);
}
),
/**
* Returns the column for this cell.
*/
column: computed('columnIndex', '_table._columnArray', function () {
if (!this._table || !this._table._columnArray) return null;
return this._table._columnArray[this.columnIndex];
}),
/**
* Returns true if the column is hidden or not.
*/
isHidden: computed('showing', 'enabled', 'required', function () {
if (this.required) return false;
if (!this.enabled) return true;
return !this.showing;
}),
highlightClassName: computed('highlight', function () {
return StringUtils.safeTags((this.highlight && 'wb-data-table-cell-highlight-' + this.highlight) || '');
}),
/**
* Returns true if the column is horizontally aligned left.
*/
isAlignLeft: computed('align', function () {
return !this.align || this.align.toUpperCase() === 'LEFT';
}),
/**
* Returns true if the column is horizontally aligned center.
*/
isAlignCenter: computed('align', function () {
return this.align && this.align.toUpperCase() === 'CENTER';
}),
/**
* Returns true if the column is horizontally aligned right.
*/
isAlignRight: computed('align', function () {
return this.align && this.align.toUpperCase() === 'RIGHT';
}),
/**
* Returns true if the column is vertically aligned top.
*/
isVAlignTop: computed('valign', function () {
return !this.valign || this.valign.toUpperCase() === 'TOP';
}),
/**
* Returns true if the column is vertically aligned center.
*/
isVAlignCenter: computed('valign', function () {
return this.valign && this.valign.toUpperCase() === 'CENTER';
}),
/**
* Returns true if the column is vertically aligned right.
*/
isVAlignBottom: computed('valign', function () {
return this.valign && this.valign.toUpperCase() === 'BOTTOM';
}),
/**
* @private
*
* Ember lifecycle event. Takes care of registering this cell with its parent table.
*/
didInsertElement() {
this._super(...arguments);
if (this.cellAdded && this.cellAdded instanceof Function) {
this.cellAdded(this);
}
},
/**
* @private
*
* Ember lifecylce event. Takes care of creating observers for row value observers.
*/
init() {
this._super(...arguments);
this.addObserver('column.field', this.createDynamicRowValueObserver);
this.addObserver('row', this.createDynamicRowValueObserver);
},
/**
* @private
*
* Ember lifecycle event. Takes care of unregistering this cell from its parent table.
*/
willDestroyElement() {
this._super(...arguments);
if (this.cellRemoved && this.cellRemoved instanceof Function) {
this.cellRemoved(this);
}
this.removeObserver('column.field', this.createDynamicRowValueObserver);
this.removeObserver('row', this.createDynamicRowValueObserver);
},
/**
* Utility function to get the current column.
*/
getColumn() {
return this.column;
},
/**
* Utility function to get the current column index.
*/
getColumnIndex() {
return this.columnIndex;
},
/**
* Returns the current rows index relative to its sibling rows.
*/
getRowIndex() {
if (!this.element || !this.element.parentElement || !this.element.parentElement.parentElement) {
return -1;
}
return [...this.element.parentElement.parentElement.children].indexOf(this.element.parentElement);
},
/**
* Handler for mouse entering this component so we can adjust column hovering if needed.
*/
// mouseEnter: action(function () {
// this._table && this._table.hoverColumn(this.getColumnIndex());
// }),
/**
* Handler for mouse exiting this component so we can adjust column hovering if needed.
*/
// mouseLeave: action(function () {
// this._table && this._table.hoverColumn();
// }),
/**
* @private
*
* Whenever the row or the column.field properties are changed, we need to establish
* and observer to watch for changes to the row[column.field] value. This function
* handles setting up that observer and also making sure we can remove it later.
*/
createDynamicRowValueObserver: action(function () {
if (!this.column || !this.column.field) return;
if (!this.row || !(this.row instanceof EmberObject)) return;
if (this._priorDynamicObserver) this.removeDynamicRowValueObserver();
this.row.addObserver(this.column.field, this.dynamicRowValueChanged);
this.set('_priorDynamicObserver', {
row: this.row,
column: this.column,
field: this.column.field,
handler: this.dynamicRowValueChanged,
});
}),
/**
* @private
*
* Remove the dynamic row value observer setup by createDynamicRowValueObserver.
*/
removeDynamicRowValueObserver: action(function () {
if (!this._priorDynamicObserver) return;
this._priorDynamicObserver.row.removeObserver(this._priorDynamicObserver.field, this._priorDynamicObserver.handler);
this.set('_priorDynamicObserver', null);
}),
/**
* @private
*
* Fired whenever the dynamic row value observer is trigger due to the underlying value
* in a row changing. This triggers the highlight animation if enabled and also
* forces the value to be recomputed by ember.
*/
dynamicRowValueChanged: action(function () {
if (this.isDestroyed || this.isDestroying) return;
if (this.element) this.element.style.animation = null;
this.incrementProperty('_recomputeValueCounter');
setTimeout(() => {
if (this.element) this.element.style.animation = '1s linear wb-data-table-cell-value-updated-animation';
}, 0);
}),
});
import Component from '@ember/component';
import { computed, action } from '@ember/object';
import SortUtils from '../utils/sort-utils';
import StringUtils from '../utils/string-utils';
/**
* Internal WbDataTable component for rendering columns. This component should not be used directly.
*/
export default Component.extend({
/**
* Ember class name for this component.
*/
classNames: ['wb-data-table-column'],
/**
* Ember class bindings for this component.
*/
classNameBindings: [
'isRoleHeading:wb-data-table-column-role-heading',
'isRoleFooting:wb-data-table-column-role-footing',
'isHidden:wb-data-table-column-hidden',
'isAlignLeft:wb-data-table-column-align-left',
'isAlignCenter:wb-data-table-column-align-center',
'isAlignRight:wb-data-table-column-align-right',
'isVAlignTop:wb-data-table-column-valign-top',
'isVAlignCenter:wb-data-table-column-valign-center',
'isVAlignBottom:wb-data-table-column-valign-bottom',
'sortable:wb-data-table-column-sortable',
'isSortedNone:wb-data-table-column-sorted-none',
'isSortedUp:wb-data-table-column-sorted-up',
'isSortedDown:wb-data-table-column-sorted-down',
'fitlerable:wb-data-table-column-fitlerable',
'columnHovered:wb-data-table-column-hover',
'_isFilterDialogShowing:wb-data-table-column-filter-dialog',
'isFiltered:wb-data-table-column-filtered',
],
/**
* The label for this component. If label is not provided and the component has block content,
* the block content will be rendered. However, it should be noted that the block content for
* a column element is meant to be used for rendering the cell contents, so using this outside
* of some very specific use cases of wb-data-table.hbs is HIGHLY NOT RECOMMENDED.
*/
label: '',
/**
* The field to use when computing the value for a given cell. The value of a cell is the
* intersection of a row, which should be an object, and the column.field argument.
* The field is deeply traversed against row, so if the field name contains the
* period separator, it walks into nested objects if it can.
*
* `value = row[column.field] || '';`
*/
field: null,
/**
* Is this column currently showing?
*/
showing: true,
/**
* Can this column be shown?
*/
enabled: true,
/**
* Must this column be shown?
*/
required: false,
/**
* Is this column sortable?
*/
sortable: true,
/**
* Is this column currently sorting. If this is set to null, the column is not currently sorting.
* If it is set to "ASC" or "DESC" it is currently sorted and this value implies the sorting direction.
* Only one column may ever be sorted at a time.
*/
sorted: null,
/**
* On sorting how should the value of this row/column intersection be compared with the value of
* another row/column intersection.
*
* This may be a string with the following values: default, number, string, stringIgnoreCase, date.
*
* Or it may be a function, in which case, the function will be passed the two values and must
* return a 0 if they are equals, a -1 if the first is less than the second, or a +1 if the
* second is less than the first.
*/
comparator: 'default', // can be , or a custom function.
/**
* Is this column filterable?
*/
filterable: true,
/**
* Is this column currently being filtered and if so what is the value of that filter. If this is
* null or an empty string, filtering is not currently being applied. Anything else implies filtering
* is actively being applied.
*
* Only one filter may be applied at any given time for an one column.
*/
filterBy: null,
/**
* The filter mode currently being applied. May be one of the following: contains, exact, starts, ends.
*/
filterMode: null,
/**
* If true, filtering is paused for this column, but filterBy and filterMode will still have values.
*/
filterPaused: false,
/**
* If given and a valid function, this will call for each row to determine if the row should be included
* in the filtered results or not. If this function returns true, the row will be included. If false,
* the row will be excluded and filtered out.
*
* This function gets the following arguments when called: (text, mode, value, row, column)
*
* text - the text to filter by. The same as the filterBy argument above.
*
* mode - the filter mode in use. The same as the filterMode argument above.
*
* value - the current value for the current row being matched.
*
* row - the current row being matched.
*
* column - the current column being matched, effectively `this`.
*/
filterMatch: null,
/**
* Should some highlighting be applied to this column.
*
* Can be one of the following:
*
* - `red`, `red1`, `red2`, `red3`, `red4`, `red5`
* - `green`, `green1`, `green2`, `green3`, `green4`, `green5`
* - `blue`, `blue1`, `blue2`, `blue3`, `blue4`, `blue5`
* - `yellow`, `yellow1`, `yellow2`, `yellow3`, `yellow4`, `yellow5`
* - `cyan`, `cyan1`, `cyan2`, `cyan3`, `cyan4`, `cyan5`
* - `purple`, `purple1`, `purple2`, `purple3`, `purple4`, `purple5`
* - `gray`, `gray1`, `grey`, `grey1`, `gray2`, `grey2`, `gray3`, `grey3`, `gray4`, `grey4`, `gray5`, `grey5`
* - `invert`
*/
highlight: null,
/**
* Horizontal alignment for this column, applies to column headings and column footings and each cell
* in the column. May be one of the following: "left", "right", "center". Defaults to "left".
*/
align: 'left',
/**
* Vertical alignment for this column. ONLY applies to the column cells. Column headings and footings
* are always vertically aligned "center". May be one of the following: "top", "center", "bottom".
* Defaults to "top".
*/
valign: 'top',
/**
* Width of this column. This may be a CSS Width value such as "400px" or "10%" or it may be a
* number without units like "3" which would be turned into "3fr".
*/
width: '1fr',
/**
* What to display in this cell if the value is undefined?
*/
undefinedValue: '',
/**
* What to display in this cell if the value is null?
*/
nullValue: '',
/**
* If true each cell will indicate that it can be double clicked to bring up an editor. The
* actual code to handle the click and render an editor of some type is entirely
* up to the developer. WbDataTable will do nothing other than correctly indicate
* that the cell is "editable".
*/
editable: false,
/**
* Should cells in this column have a specific additional classname applied to them, and if so what?
*/
additionalCellClassName: null,
/**
* callback handler used to register this column with its parent table.
*
* Provided by WbDataTable when created. Do not override.
*/
columnAdded: null,
/**
* Callback handler used to unregister this column with its parent table.
*
* Provided by WbDataTable when created. Do not override.
*/
columnRemoved: null, // provided by WbDataTable, do not override.
/**
* @private
*
* Reference to the parent table, supplied during registration.
*/
_table: null,
/**
* @private
*
* The role of this column. In order for column information to be applied at heading,
* footing, and cell levels, we take advantage of ember's yield behavior and yield columns
* out in each of those locations. This _role argument is used to tell us which of
* those locations we are currently in.
*/
_role: null,
/**
* @private
*
* Set to true if the filter dialog is currently showing for this column.
*/
_isFilterDialogShowing: false,
/**
* @private
*
* Computed property of the Ember HTML Safe version of the label for this column. Allows for limited HTML tags such as B, UL, etc.
*/
_safeLabel: computed('label', function () {
return StringUtils.safeTags(this.label);
}),
/**
* @private
*
* Computed property of the Ember HTML stripped version of the label for this column. Removes all html.
*/
_stripLabel: computed('label', function () {
return StringUtils.stripTags(this.label);
}),
/**
* Computed property of the width argument.
*/
computedWidth: computed('width', function () {
return this.width || '1fr';
}),
/**
* Computed property to determine if we rendering in a heading?
*/
isRoleHeading: computed(function () {
return this._role === 'HEADING';
}),
/**
* Computed property to determine if we rendering in a footing?
*/
isRoleFooting: computed(function () {
return this._role === 'FOOTING';
}),
/**
* Computed property to determine if the column is hidden or not.
*/
isHidden: computed('showing', 'enabled', 'required', function () {
if (this.required) return false;
if (!this.enabled) return true;
return !this.showing;
}),
/**
* Computed property to determine if the column is horizontally aligned left.
*/
isAlignLeft: computed('align', function () {
return !this.align || this.align.toUpperCase() === 'LEFT';
}),
/**
* Computed property to determine if the column is horizontally aligned center.
*/
isAlignCenter: computed('align', function () {
return this.align && this.align.toUpperCase() === 'CENTER';
}),
/**
* Computed property to determine if the column is horizontally aligned right.
*/
isAlignRight: computed('align', function () {
return this.align && this.align.toUpperCase() === 'RIGHT';
}),
/**
* Computed property to determine if the column is vertically aligned top.
*/
isVAlignTop: computed('valign', function () {
return !this.valign || this.valign.toUpperCase() === 'TOP';
}),
/**
* Computed property to determine if the column is vertically aligned center.
*/
isVAlignCenter: computed('valign', function () {
return this.valign && this.valign.toUpperCase() === 'CENTER';
}),
/**
* Computed property to determine if the column is vertically aligned bottom.
*/
isVAlignBottom: computed('valign', function () {
return this.valign && this.valign.toUpperCase() === 'BOTTOM';
}),
/**
* Computed property to determine if the column is sortable.
*/
isSortable: computed('sortable', '_table.isUsingCustomRows', function () {
return (this.sortable && this._table && !this._table.isUsingCustomRows) || false;
}),
/**
* Computed property to determine if the column is sortable but not sorted.
*/
isSortedNone: computed('sorted', function () {
return this.sorted === null;
}),
/**
* Computed property to determine if the column is sortable and sorting ASC.
*/
isSortedUp: computed('sorted', function () {
return this.sorted === 'ASC';
}),
/**
* Computed property to determine if the column is sortable and sorting DESC.
*/
isSortedDown: computed('sorted', function () {
return this.sorted === 'DESC';
}),
/**
* Computed property to determine if the table is completely selected.
*/
isAllSelected: computed('_table.isAllSelected', function () {
return this._table.isAllSelected;
}),
/**
* Computed property to determine if the table is completely unselected.
*/
isNoneSelected: computed('_table.isNoneSelected', function () {
return this._table.isNoneSelected;
}),
/**
* Computed property to determine if the column is filterable.
*/
isFilterable: computed('filterable', '_table.isUsingCustomRows', function () {
return (this.filterable && this._table && !this._table.isUsingCustomRows) || false;
}),
/**
* Computed property to determine if the column is actively filtered.
*/
isFiltered: computed('filterBy', 'filterMode', function () {
return !!(this.filterBy && this.filterMode);
}),
/**
* @private
*
* Returns the comparator function to use.
*/
_sortComparator: computed('comparator', function () {
let comparator = this.comparator;
const compUC = typeof comparator === 'string' ? comparator.toUpperCase() : comparator;
if (!comparator) comparator = SortUtils.compare;
else if (compUC === 'GENERIC') comparator = SortUtils.compare;
else if (compUC === 'DEFAULT') comparator = SortUtils.compare;
else if (compUC === 'NUMBER') comparator = SortUtils.compareNumber;
else if (compUC === 'DATE') comparator = SortUtils.compareDate;
else if (compUC === 'ALPHA') comparator = SortUtils.compareString;
else if (compUC === 'STRING') comparator = SortUtils.compareString;
else if (compUC === 'ALPHAIGNORECASE') {
comparator = SortUtils.compareStringIgnoreCase;
} else if (compUC === 'STRINGIGNORECASE') {
comparator = SortUtils.compareStringIgnoreCase;
}
if (!comparator || !(comparator instanceof Function)) {
this.remoteLogging.log('sortComparator', new Error('Comparator function for WbDataTable not provided.'), 'warn');
comparator = SortUtils.compare;
}
return comparator;
}),
/**
* @private
*
* Ember lifecycle event. Handles registering this column with the table.
*/
didInsertElement() {
this._super(...arguments);
if (this.columnAdded && this.columnAdded instanceof Function) {
this.columnAdded(this);
}
},
/**
* @private
*
* Ember lifecycle event. Handles unregistering this column from the table.
*/
willDestroyElement() {
this._super(...arguments);
if (this.columnRemoved && this.columnRemoved instanceof Function) {
this.columnRemoved(this);
}
},
/**
* Returns the column index for this column relative to all of the other columns.
*
* @returns {Number}
*/
getColumnIndex() {
return (
[...this.element.parentElement.children].indexOf(this.element) +
(this._table && this._table.isSelectable ? -1 : 0) +
(this._table && this._table._isSubrowsAllowed ? -1 : 0) +
(this._table && this._table._isHierarchyActive ? -1 : 0)
);
},
/**
* Handler for when the sort icon is clicked.
*/
sortToggleHandler: action(function () {
if (!this._table) return;
if (this.sorted === null) this._table.sort(this, 'DESC');
else if (this.sorted === 'DESC') this._table.sort(this, 'ASC');
else if (this.sorted === 'ASC') this._table.sort(this, null);
}),
/**
* Handler for when the filter icon is clicked.
*/
filterHandler: action(function () {
this._table && this._table.showFilterDialog(this);
}),
});
import Component from '@ember/component';
import { action, computed } from '@ember/object';
/**
* Internal WbDataTable component for rendering rows. This component should not be used directly.
*/
export default Component.extend({
/**
* Ember classname for this component.
*/
classNames: ['wb-data-table-row'],
/**
* Ember class bindings for this component.
*/
classNameBindings: [
'_selected:wb-data-table-row-selected',
'_hasSubrow:wb-data-table-row-has-subrow',
'_isSubrowOpened:wb-data-table-row-subrow-opened',
],
/**
* The row object associated with the row component. This is the raw row data.
*/
row: null,
/**
* Should some highlighting be applied to this row.
*
* Can be one of the following:
*
* - `red`, `red1`, `red2`, `red3`, `red4`, `red5`
* - `green`, `green1`, `green2`, `green3`, `green4`, `green5`
* - `blue`, `blue1`, `blue2`, `blue3`, `blue4`, `blue5`
* - `yellow`, `yellow1`, `yellow2`, `yellow3`, `yellow4`, `yellow5`
* - `cyan`, `cyan1`, `cyan2`, `cyan3`, `cyan4`, `cyan5`
* - `purple`, `purple1`, `purple2`, `purple3`, `purple4`, `purple5`
* - `gray`, `gray1`, `grey`, `grey1`, `gray2`, `grey2`, `gray3`, `grey3`, `gray4`, `grey4`, `gray5`, `grey5`
* - `invert`
*/
highlight: null,
/**
* callback handler used to register this row in the table.
*
* Provided by WbDataTable when created. Do not override.
*/
rowAdded: null, // provided by WbDataTable, do not override.
/**
* callback handler used to unregister this row in the table.
*
* Provided by WbDataTable when created. Do not override.
*/
rowRemoved: null, // provided by WbDataTable, do not override.
/**
* @private
*
* WbDataTable associated with this row component.
*/
_table: null,
/**
* @private
*
* Is this row currently selected?
*/
_selected: false,
/**
* @private
*
* Does this row has an aossicated subrow and if so what is it?
*/
_subrow: null,
/**
* @private
*
* Computed property that returns true if a subrow is associated with this row component.
*/
_hasSubrow: computed('_subrow', function () {
return !!this._subrow;
}),
/**
* @private
*
* Computed property that returns true if a subrow is associated and it is currently showing.
*/
_isSubrowOpened: computed('_subrow.opened', function () {
return (this._subrow && this._subrow.opened) || false;
}),
didReceiveAttrs() {
this._super(...arguments);
this.updateHighlight();
},
/**
* @private
*
* Ember lifecycle event. Handles registering this row with its parent table.
*/
didInsertElement() {
this._super(...arguments);
this.addObserver('highlight', this._highlightObserver);
if (this.rowAdded && this.rowAdded instanceof Function) this.rowAdded(this);
},
/**
* @private
*
* Ember lifecycle event. Handles unregistering this row with its parent table.
*/
willDestroyElement() {
this._super(...arguments);
this.removeObserver('highlight', this._highlightObserver);
if (this.rowRemoved && this.rowRemoved instanceof Function) {
this.rowRemoved(this);
}
},
/**
* Returns the row index of this row component relative to its sibling components.
*/
getRowIndex() {
return [...this.element.parentElement.children].indexOf(this.element);
},
/**
* If highlighting is applied, remove it an apply any new highlighting.
*/
updateHighlight() {
// remove all highlights if any, and add whatever is given.
// needs to be done in a setTimeout because of race condition with ember.
setTimeout(() => {
if (this.element) {
[...this.element.classList].forEach((cls) => {
if (this.highlight && cls === 'wb-data-table-row-highlight-' + this.highlight) {
return;
}
if (cls.startsWith('wb-data-table-row-highlight-')) {
this.element.classList.remove(cls);
}
});
if (this.highlight) {
this.element.classList.add('wb-data-table-row-highlight-' + this.highlight);
}
}
}, 0);
},
/**
* @private
*
* Observer handler for highlight property.
*/
_highlightObserver: action(function () {
this.updateHighlight();
}),
/**
* Click handler for this row, to handle what happens when it is clicked.
*
* @param {HTMLEvent}
*/
click: action(function (event) {
if (this._table) {
if (this._table.isSelectable) {
this._table.selectRow(this, event);
// clears any text seleciton the window might have done.
window.getSelection().empty();
} else if (this._hasSubrow) {
this._table.toggleSubrow(this.row);
// clears any text seleciton the window might have done.
window.getSelection().empty();
}
}
}),
});
/*
DEVELOPER WARNING - DEVELOPER WARNING - DEVELOPER WARNING - DEVELOPER WARNING - DEVELOPER WARNING - DEVELOPER WARNING
The code in this file and all wb-data-table related files is a highly complex component with a lot of moving
pieces. As such changes to these components should be undertaken with a great deal of care and a lot of
attention to how it all works together. Please be mindful of that as you move forward.
Also, a full set of ember tests for these components is available and should be run prior to commiting
any changes to these components.
Thank you!
DEVELOPER WARNING - DEVELOPER WARNING - DEVELOPER WARNING - DEVELOPER WARNING - DEVELOPER WARNING - DEVELOPER WARNING
*/
/**
* WbDataTable is a complete rewrite of WbTable from the ground up to provide all of the aspects of tables
* that we need to provide to our users. There are many, many features that exist in WbTable that have been
* brought over, but there are equally many new features that were not possible with WbTable that have been added.
*
* Because the documentation for this component is significantly long,
* it has been located in a separate documentation file. **Please refer
* to docs/WbDataTable.md for more information.**
*
*/
import Component from '@ember/component';
import { computed, action } from '@ember/object';
import { once, next } from '@ember/runloop';
import { inject as service } from '@ember/service';
// import { task, timeout } from 'ember-concurrency';
import SortUtils from '../utils/sort-utils';
import StringUtils from '../utils/string-utils';
import EmberUtils from '../utils/ember-utils';
export default Component.extend({
/**
* Injects the exportService.
*/
exportService: service('export'),
/**
* Injects the popper service.
*/
popper: service(),
/**
* Sets up the ember component class name.
*/
classNames: ['wb-data-table'],
/**
* Sets up the ember component class name bindings.
*/
classNameBindings: [
'isReady:wb-data-table-ready',
'isEmpty:wb-data-table-empty',
'_isAllFilteredOut:wb-data-table-empty',
'isSelectable:wb-data-table-selectable',
'isSelectModeSingle:wb-data-table-select-mode-single',
'isSelectModeMulti:wb-data-table-select-mode-multi',
'isSelectModeToggle:wb-data-table-select-mode-toggle',
'_isHoverRowsOn:wb-data-table-hover-row',
'_isHoverColumnsOn:wb-data-table-hover-column',
'_isHeadingText:wb-data-table-headings-text',
'_isHeadingBar:wb-data-table-headings-bar',
'_isHeadingNone:wb-data-table-headings-none',
'_isFootingText:wb-data-table-footings-text',
'_isFootingBar:wb-data-table-footings-bar',
'_isFootingNone:wb-data-table-footings-none',
'_isActionBarNone:wb-data-table-action-bar-none',
'_isActionBarTop:wb-data-table-action-bar-top',
'_isActionBarBottom:wb-data-table-action-bar-bottom',
],
/**
* Sets up the ember component attribute bindings.
*/
attributeBindings: ['_styleString:style'],
/**
* The internal name for this table. This should be highly unique as the table state will be stored into
* localStorage using this name. If this is null or undefined a warning message will show encouraging
* developers to add a name. If this is an empty string or a string of spaces, this will not show
* a warning message but it will also not save table state.
*/
name: null, // string
/**
* A label to display above the table in the header. Optional.
*/
label: '',
/**
* Set to false if you want the data loading busy indicator to appear. This is provided so you may manually
* control when the table is flagged as ready. If ready is null, then isReady is a function of whether or
* not rows is not null.
*/
ready: null, // boolean
/**
* An optional function to use to fetch data for this table. If a fetcher function is provided it is responsible
* for handling sorting, filtering, and pagination itself.
*
* The fetcher is called with the following arguments:
*
* state - The current table state. See TABLE STATE for more information.
*
* columns - An array of the current columns, including hidden columns.
*
* rows - An array of the current rows, for reference.
*
* If fetcher retunrs a promise, it will await it.
*
* Fetcher should return an object or resolve to an object. The object should have the following structure:
*
* rows - An array of rows for the current showing results.
*
* count - The total number of rows in the entire data set.
*
* start - The index of the first row displayed relative to the entire data set.
*
* end - The end index of the last row displayed relative to the entire data set.
*
* If a fetcher function is provided it will run automatically the first time the table is initialized.
*
* Please read the external documentation on DYNAMIC TABLE FETCHER for more information.
*/
fetcher: null, // function
/**
* An array of row objects that are used to generate the dynamic row elements for this component.
*
* rows should be null if you are defining a static table.
*
* If rows is null and @ready is not set, the table will display a loading indicator.
*
* If rows is an empty array, the @emptyMessage will be displayed.
*/
rows: null, // array or null
/**
* Allows the developers to override the defacto row width with a fixed value. This string must
* be in the form of a CSS measurement. If the rowWidth is greater than 100% of the viewport,
* the horizontal scrollbar will be enabled.
*/
rowWidth: '100%',
/**
* Used to override the total number of rows in the entire data set. Do not set unless you are
* using pagination and providing an external fetcher.
*
* If the external fetcher returns `count` in its response object, this value is set to that
* after the fetch is done.
*/
rowCount: null, // number or null
/**
* Set to true to display an infinity symbol instead of the total number of rows. Generally you
* would only use this with infinite tables with pagination and an unknowable amount of rows.
*/
rowCountInfinite: false, // boolean
/**
* Used to override the first row index position. Do not set unless you are using pagination and
* providing an external fetcher.
*
* If the external fetcher returns `start` in its response object, this value is set to that
* after the fetch is done.
*/
rowStartPosition: null, // number or null
/**
* Used to override the last row index position. Do not set unless you are using pagination and
* providing an external fetcher.
*
* If the external fetcher returns `end` in its response object, this value is set to that
* after the fetch is done.
*/
rowEndPosition: null, // number or null
/**
* Use to override the default width of the table. By default width is set to "100%". You may
* override it with a fixed dimension like "400px" or a percentage of it parent container
* "50%" or with "auto". Avoid "min-content" and "max-content".
*
* Please read the external documentation on DYNAMIC TABLE WIDTH AND HEIGHT for more information.
*/
width: null, // string, css width property
/**
* Use to override the default height of the table. By default height is set to "100%". You may
* override it with a fixed dimension like "400px" or a percentage of it parent container
* "50%" or with "auto". Avoid "min-content" and "max-content".
*
* Please read the external documentation on DYNAMIC TABLE WIDTH AND HEIGHT for more information.
*/
height: null, // string, css height property
/**
* Set to false to prevent showing the header of the table. Defaults to true.
*
* Please read the external documentation on DYNAMIC TABLE HEADERS for more information.
*/
showHeader: true, // boolean
/**
* Set to false to prevent showing the footer of the table. Defaults to true.
*
* Even if showing the footer you may also need to set @actionBar="both" if you want the action
* bar on the bottom to show up.
*
* Please read the external documentation on DYNAMIC TABLE FOOTERS for more information.
*/
showFooter: true, // boolean
/**
* Set to specify how you want column headings to display. Options are "text", "bar", or "none".
* Defaults to "text".
*
* Please read the external documentation on DYNAMIC TABLE HEADINGS for more information.
*/
headings: 'text', // string: 'text', 'bar', 'none'
/**
* Set to specify how you want column footings to display. Options are "text", "bar", or "none".
* Defaults to "bar".
*
* Please read the external documentation on DYNAMIC TABLE FOOTINGS for more information.
*/
footings: 'bar', // string: 'text', 'bar', 'none'
/**
* Set to false to prevent all column headings from displaying. Defaults to true. Overrides
* @headings.
*
* Please read the external documentation on DYNAMIC TABLE HEADINGS for more information.
*/
showColumnHeadings: true, // boolean
/**
* Set to false to prevent all column footings from displaying. Defaults to true. Overrides
* @footings.
*
* Please read the external documentation on DYNAMIC TABLE FOOTINGS for more information.
*/
showColumnFootings: true, // boolean
/**
* Specifies how you want hovering indicators to show for this table. May be set to "row",
* "column", "both", or "none". Defaults to "row".
*
* Only "row" will work if you are using with Static Tables.
*
* Please read the external documentation on DYNAMIC TABLE HOVERING for more information.
*/
hoverMode: 'row', // string 'row', 'column', 'both', 'none'
/**
* Use to tell the table whether selection is enabled or not. You do so by specifying how you
* want table selection clicks to be handled. Possible values are "none", "single", "toggle",
* or "multi". Defaults to "none".
*
* none - Selection is disabled.
*
* single - Allows the user to select only a single row at a time and never more
* than one row.
*
* toggle - Allows the user to select many rows, one at a time.
*
* multi - Allows the user to select many rows in a few different ways. Clicking
* on a row will select that row and deselect all other rows. CTRL Clicking will
* select/deselect the row but does not clear other rows first. SHIFT clicking will
* select from the last selected row to the current row, inclusively.
*
* Please read the external documentation on DYNAMIC TABLE SELECTION for more information.
*/
selectMode: 'none', // string: 'none' 'single' 'toggle' 'multi'
/**
* If true, show a textual description of the current selection immediately below the table
* label. Defaults to true.
*/
showSelectionInfo: true, // boolean
/**
* If true, the WbDataTable is searchable and a search field and button will be displayed
* on the top of the table near the actions area.
*
* If a custom @fetcher is not being used, this will use an internal searching algorithm
* which merely converts everything to strings and does a contains match against them. You
* can control which fields are search be providing @searchFields.
*
* If a custom @fetcher is in use, the developer is responsible for handling how searching
* is done.
*/
searchable: false,
/**
* A comma separated list of fields to search when @searchable is true and a @fetcher is not
* provided. If the searchFields is equal to "*" all fields, that is all known column fields
* will be searched.
*/
searchFields: '*',
/**
* Sets where action bars are displayed. Options are "top", "bottom", "both", "none". Defaults
* to "top".
*
* Please read the external documentation on DYNAMIC TABLE ACTION BARS and DYNAMIC TABLE
* CUSTOM ACTIONS for more information.
*/
actionBar: 'top', // string: 'top' 'bottom' 'both' 'none'
/**
* Set to false to prevent any actions from being displayed. Defaults to true.
*/
showActions: true, // boolean
/**
* Set to false to prevent any actions from being displayed in the header. Defaults to true.
*/
showActionsTop: true, // boolean
/**
* Set to false to prevent any actions from being displayed in the footer. Defaults to true.
*/
showActionsBottom: true, // boolean
/**
* Set to false to prevent CUSTOM actions from being displayed in the header. Defaults to true.
*/
showCustomActionsTop: true, // boolean
/**
* Set to false to prevent CUSTOM actions from being displayed in the footer. Defaults to true.
*/
showCustomActionsBottom: true, // boolean
/**
* Set to true to turn on pagination. Defaults to false.
*
* If a custom @fetcher is provided the fetcher must handle pagination.
*
* Please read the external documentation on DYNAMIC TABLE PAGINATION for more information.
*/
pageable: false, // boolean
/**
* Sets the current size of a page of rows being shown. Defaults to 100.
*/
pageSize: 100, // number
/**
* Sets the current page. Defaults to 1.
*
* NOTE: It is best to set the page via the `changePage()` function rather than setting this directly.
*/
page: 1, // number
/**
* If true, show a textual description of the current pagination immediately below the table
* label. Defaults to true.
*/
showPagingInfo: true, // boolean
/**
* Set to false to prevent exporting this table. Defaults to true.
*/
exportable: true, // boolean
/**
* Set to a Function to provide a custom exporting system for this table. Defaults to null.
*
* The exporter function gets the following arguments when it is called:
*
* type - A string representing the type of data requested for exporting, either 'JSON' or 'CSV'.
*
* rows - The @rows to be exported based on the All, Selected, or Current rows setting.
*
* columns - The columns to be exported based on the All or Showing columns setting.
*
* fields - An array of fields that are to be exported.
*
* headers - An array of header labels for each column to be exported.
*
* state - An object containing all of the state detail for the table. This includes things
* like sorting, filtering, pagination, etc. See TABLE STATE towards the end of this document.
*
* Please read the external documentation on DYNAMIC TABLE EXPORTING for more information.
*/
exporter: null, // function
/**
* If true, show a textual description of the current filtering immediately below the table
* label. Defaults to true.
*/
showFilteringInfo: true,
/**
* Sets whether or not subrows are displayed for each row. If you wish to use subrows you must both
* set this to an appropriate value and define a <table.subrow> value in the component HBS.
*
* This take ths possible values of "none", "single", or "many".
*
* none - Subrows are disabled.
*
* single - Subrows are enabled, but only one subrow may be shown at any given time.
*
* many - Subrows are enabled and any number of subrows may be showing.
*
* Please read the external documentation on DYNAMIC TABLE SUBROWS for more information.
*/
subrows: 'none', // single, many, none
/**
* Sets whther or not heirarchy rows are supported by this table. If this is a non-null, non-empty
* string, that value will be used against each row to determine if the rows has subordinate rows.
* The subordinate rows are then examined likewise to determine an hierarchy.
*
* Please read the external documentation on DYNAMIC TABLE HIERARCHY for more information.
*/
hierarchyField: null,
/**
* Override the default text for a loading message in the busy indicator.
*/
loadingMessage: 'Loading...', // string
/**
* Override the default text for a empty message when @rows is an empty array.
*/
emptyMessage: 'No Data Available.', // string
/**
* Override the default text for a fetching message in the busy indicator.
*/
fetchingMessage: 'Loading data...', // string
/**
* Override the default text for a exporting message in the busy indicator.
*/
exportingMessage: 'Exporting data...', // string
/**
* Override the default text for the rendering indicator screen.
*/
renderingMessage: 'Preparing data...', // string
/**
* Override the default text for a message when all results are filtered out.
*/
allFilteredOutMessage: 'All results filtered out.', // string
/**
* Allows the developer to provide an additional class name to associate with each row.
*/
additionalRowClassName: null,
/**
* Allows the developer to provide an additional class name to associate with each subrow.
*/
additionalSubrowClassName: null,
/**
* Allows the developer to provide an additional class name to associate with each column.
*/
additionalColumnClassName: null,
/**
* Allows the developer to provide an additional class name to associate with each cell.
*/
additionalCellClassName: null,
/**
* Allows the developer to provide additional table state data to be saved. It is entirely
* on the developer to set this to be whatever additional data they want. Then when
* tableState would be written to the localStorage, this is included.
*
* Whenever possible, set this to an Object, and then you can use the keys inside of that.
*
* When tableState is loaded from localStorage, anything in this gets overwritten.
*/
additionalTableState: null,
/**
* A callback function to execute when the table is initialized. The function will be called
* with the arguments (table,api).
*
* Please read the external documentation on TABLE EVENTS for more information.
*/
onInit: null,
/**
* A callback function to execute when the table is initialized. The function will be called
* with the arguments (table,api).
*
* Please read the external documentation on TABLE EVENTS for more information.
*/
onReady: null,
/**
* A callback function to execute when the table selection is changed. The function will
* be called with the arguments (table,selectedRows).
*
* Please read the external documentation on TABLE EVENTS for more information.
*/
onSelect: null, // function
/**
* A callback function to execute when the table export is complete. The function will be
* called with the arguments (table).
*
* Please read the external documentation on TABLE EVENTS for more information.
*/
onExported: null, // function
/**
* A callback function to execute when the table fetch is complete. The function will be
* called with the arguments (table).
*
* Please read the external documentation on TABLE EVENTS for more information.
*/
onFetched: null, // function
/**
* A callback function to execute when a column is added. The function will be called with
* the arguments (table, column).
*
* Please read the external documentation on TABLE EVENTS for more information.
*/
onColumnAdd: null,
/**
* A callback function to execute when a column is removed. The function will be called with
* the arguments (table, column).
*
* Please read the external documentation on TABLE EVENTS for more information.
*/
onColumnRemove: null,
/**
* A callback function to execute when a row is added. The function will be called with
* the arguments (table, rowComponent, row).
*
* Please read the external documentation on TABLE EVENTS for more information.
*/
onRowAdd: null,
/**
* A callback function to execute when a row is removed. The function will be called with
* the arguments (table, rowComponent, row).
*
* Please read the external documentation on TABLE EVENTS for more information.
*/
onRowRemove: null,
/**
* A callback function to execute when a subrow is added. The function will be called with
* the arguments (table, subrowComponent, row).
*
* Please read the external documentation on TABLE EVENTS for more information.
*/
onSubrowAdd: null,
/**
* A callback function to execute when a subrow is removed. The function will be called with
* the arguments (table, subrowComponent, row).
*
* Please read the external documentation on TABLE EVENTS for more information.
*/
onSubrowRemove: null,
/**
* A callback function to execute when a cell is added. The function will be called with
* the arguments (table, cell, column, row).
*
* Please read the external documentation on TABLE EVENTS for more information.
*/
onCellAdd: null,
/**
* A callback function to execute when a cell is removed. The function will be called with
* the arguments (table, cell, column, row).
*
* Please read the external documentation on TABLE EVENTS for more information.
*/
onCellRemove: null,
// private internal variables
/**
* Internal holder of rows. When this.rows changes, the _rowsObserver is
* notified and will programatically transfer this.rows into _internalRows,
* which is what is used to actually render the rows. This is done to
* allow us to display the rendering message before the rendering of
* rows begins, allowing the repaint cycle to occur.
*/
_internalRows: null,
/**
* @private
* Stores a reference to all the row components.
*/
_rowComponents: [],
/**
* @private
* Stores a reference to all the subrow components.
*/
_subrowComponents: [],
/**
* @private
* Stores a reference to all the column components.
*/
_columns: new Set(),
/**
* @private
* Array version of _columns.
*/
_columnArray: [],
/**
* @private
* Stores a reference to all the cell components.
*/
_cells: new Set(),
/**
* @private
* The current width string that is computed periodically to ensure the table is aligned correctly.
*/
_widthString: null,
/**
* @private
* The column index currently being hovered.
*/
_hoverColumnIndex: null,
/**
* @private
* The last component that was clicked for selection
*/
_lastSelectedRowComponent: null,
/**
* @private
* True if we are currently in the "fetching" state.
*/
_inFetching: false,
/**
* @private
* True if we are currently in the "exporting" state.
*/
_inExporting: false,
/**
* If true we are currently repainting the rows. This is used to
* display the preparingMessage while the rows are being drawn.
*/
_inRendering: false,
/**
* @private
* One up counter to indicate selection has changed.
*/
_selectionIndex: 0,
/**
* @private
*
* Text to be searched on to further filter the rows in the table.
*/
_searchText: '',
/**
* @private
*
* Holder of text to be searched on, which does not get updated until search is executed.
*/
_pendingSearchText: '',
/**
* @private
* Reference to the filter popup dialog.
*/
_filterPopup: null,
/**
* @private
* Reference to the column for which the filterDialog popup is currently showing.
*/
_filterColumn: null,
/**
* @private
* Exporting setting.
*/
_exportRowsChoice: 'ALL',
/**
* @private
* Exporting setting.
*/
_exportColumnsChoice: 'ALL',
/**
* @private
* Exporting setting.
*/
_exportFlavorChoice: 'CSV',
/**
* @private
* Maps rows to an object that describes its role in the hierarchy.
*/
_hierarchyInfo: new Map(),
/**
* @private
* A one up counter for hierarchy to trigger some ember computed properties.
*/
_hierarchyIndex: 0,
/**
* Tick this number up, if you want to reset all the saved table states.
*/
_tableStateVersion: 1,
// table styles
/**
* @private
* Computed property that is used in the setting width/hieght of this table, if provided.
*/
_styleString: computed('width', 'height', function () {
return StringUtils.safeTags(
`${this.width ? 'width: ' + this.width + ';' : ''}${this.height ? 'height: ' + this.height + ';' : ''}`
);
}),
// table state
/**
* Computed property that returns true if this table is ready, has rows, is not exporting and is
* not fetching.
*/
isReady: computed('_internalRows', 'ready', '_inExporting', '_inFetching', '_inRendering', function () {
if (this._inRendering) return false;
if (this._inFetching) return false;
if (this._inExporting) return false;
if (this.ready === null) return this._internalRows !== null;
if (this.ready === true) return true;
return false;
}),
/**
* Computed property that return true if _internalRows is empty.
*/
isEmpty: computed('isReady', '_internalRows', function () {
if (this.ready && this._rowComponents.length > 0) return false;
return (this.isReady && this._internalRows && this._internalRows.length < 1) || false;
}),
/**
* Computed property that returns the current busy indicator message.
* This prioritizes fetching over exporting over loading. So if you are both loading
* and exporting at the same time, the exporting message will win.
*
*/
notReadyMessage: computed(
'isReady',
'_inFetching',
'_inExporting',
'_inRendering',
'loadingMessage',
'exportingMessage',
'renderingMessage',
function () {
if (this._inRendering) return this.renderingMessage;
if (this._inFetching) return this.fetchingMessage;
else if (this._inExporting) return this.exportingMessage;
else return this.loadingMessage;
}
),
/**
* Computed property that returns the current "state" of the table as an object.
* Please see TABLE STATE in the external documentation.
*/
tableState: computed(
'pageable',
'pageSize',
'page',
'rowCount',
'rowCountInfinite',
'rowStartPosition',
'rowEndPosition',
'_sortingColumns',
'_filteringColumns',
'_showingColumns',
'_filteringColumns.@each.filterPaused',
'_searchText',
'searchable',
'searchFields',
'additionalTableState',
function () {
return {
pageable: this.pageable,
pageSize: this.pageSize,
page: this.page,
rowCount: this.rowCount,
rowCountInfinite: this.rowCountInfinite,
rowStartPosition: this.rowStartPosition,
rowEndPosition: this.rowEndPosition,
searchable: this.searchable,
searchText: this._searchText,
searchFields: this.searchFields,
showingColumns: this._showingColumns.map((column) => {
return {
field: column.field,
showing: column.showing,
};
}),
sortingColumns: this._sortingColumns.map((column) => {
return {
field: column.field,
sorted: column.sorted,
};
}),
filteringColumns: this._filteringColumns.map((column) => {
return {
field: column.field,
filterBy: column.filterBy,
filterMode: column.filterMode,
filterPaused: column.filterPaused,
};
}),
additional: this.additionalTableState || null,
};
}
),
/**
* @private
* Computed property that returns the key used in to store the table state
* into localStorage.
*/
_tableStateKey: computed('name', function () {
if (!this.name) return null;
const name = ('' + this.name).trim();
if (!name) return null;
const url = location.href.slice(location.search.length + location.hash.length);
return 'whitebox.data-table.' + this._tableStateVersion + '.' + name + '.' + btoa(url);
}),
/**
* @private
*
* Action for handling incoming requests to save table state. Fired when tableState computed
* property changes.
*/
_saveTableState: action(function () {
if (!this.name) return;
const key = this._tableStateKey;
if (!key) return;
window.localStorage.setItem(key, JSON.stringify(this.tableState));
}),
/**
* @private
*
* Action for handling incoming requests to restore the table state. Fired when this table is
* added to the DOM via ember's didiInsertElement lifecycle event.
*/
_restoreTableState: action(function () {
if (!this.name) return;
const key = this._tableStateKey;
if (!key) return;
let data = window.localStorage.getItem(key);
if (!data) return;
try {
data = JSON.parse(data);
} catch (ex) {
this.remoteLogging.log(`Restoring tableState data for table ${key} produced a JSON parsing error.`, ex, 'error');
}
if (!data) return;
const showing = data.showingColumns || null;
const sorting = data.sortingColumns || null;
const filtering = data.filteringColumns || null;
this._columnArray.forEach((column) => {
if (showing) {
const show = showing.find((col) => column.field === col.field);
if (show) column.set('showing', show.showing);
}
if (filtering) {
const filter = filtering.find((col) => column.field === col.field);
if (filter) {
column.set('filterBy', filter.filterBy || null);
column.set('filterMode', filter.filterMode || 'contains');
column.set('filterPaused', filter.filterPaused || false);
}
}
if (sorting) {
const sort = sorting.find((col) => column.field === col.field);
if (sort) column.set('sorted', sort.sorted || null);
}
});
if (data.searchable === true && data.searchText) {
this.search(data.searchText);
}
if (data.additional) this.set('additionalTableState', data.additional);
once(this, this._recalculateColumnWidths);
}),
/**
* Action for handling incoming requests to scroll this table to the top such that the
* first row is visibile.
*
* @param {Boolean} smooth If true, smoothly animates the scroll.
*/
scrollToTop: action(function (smooth = false) {
if (!this.bodyElement) return;
this.bodyElement.scrollTo({
top: 0,
left: 0,
behavior: smooth ? 'smooth' : 'auto',
});
}),
_readyObserver: action(function () {
if (this.isReady) this._fireReadyEvent();
}),
// api
/**
* Returns an object that exposes a specific set of API allowed calls for this table.
* Please see the individual functions for specific details.
*
* @returns {Object} An object containing all available api calls.
*/
getAPI() {
return {
getRowByIndex: this.getRow.bind(this),
getColumnByIndex: this.getColumn.bind(this),
recalculateColumnWidths: this._recalculateColumnWidths.bind(this),
clearSelection: this.clearSelection.bind(this),
selectRow: this.selectRow.bind(this),
isRowSelected: this.isRowSelected.bind(this),
fetch: () => this.fetchRows.perform(),
scrollToTop: this.scrollToTop.bind(this),
search: this.search.bind(this),
export: this.export.bind(this),
sort: this.sort.bind(this),
filter: this.filter.bind(this),
changePage: this.changePage.bind(this),
toggleSubrow: this.toggleSubrow.bind(this),
toggleColumnShowing: this.toggleColumnShowing.bind(this),
toggleHierarchyRow: this.toggleHierarchyRow.bind(this),
};
},
// columns
/**
* Computed property that returns true if all columns are currently visible.
*/
isAllColumnsShowing: computed('_columnArray.@each.{showing,enabled}', function () {
return this._columnArray.every((column) => {
return column.enabled && column.showing;
});
}),
/**
* @private
* Computed property that returns an array of only showing columns.
*/
_showingColumns: computed('_columnArray.@each.showing', function () {
return this._columnArray.filter((column) => !!column.showing);
}),
/**
* @private
* Internal method to keep _columnArray in sync of this.columns.
*/
_updateColumnArray() {
this.set('_columnArray', [...this._columns]);
},
/**
* Returns the column at the given columnIndex position, starting at 0.
*
* @param {Number} columnIndex
* @returns {wb-data-table-column}
*/
getColumn(columnIndex) {
return [...this._columns][columnIndex] || null;
},
/**
* Forces the table to recompute the width of all columns. This is driven by
* each columns' @width settings.
*/
_recalculateColumnWidths() {
if (this.isDestroyed || this.isDestroying) return;
let widthString = [...this._columns]
.reduce((widthString, column) => {
if (!column.required && (!column.showing || !column.enabled)) {
return widthString;
}
let colwidth = column.get('width') || '1fr';
if (typeof colwidth === 'number') colwidth = colwidth + 'fr';
if (typeof colwidth === 'string' && colwidth.match(/^[\d.]*$/)) {
const pf = parseFloat(colwidth);
colwidth = pf + 'fr';
}
if (colwidth === 'auto' || colwidth === 'min-content' || colwidth === 'max-content') {
colwidth = '1fr';
}
return (widthString += ' ' + colwidth);
}, '')
.trim();
if (!this.isUsingCustomRows) {
if (this._isSubrowsAllowed) widthString = '30px ' + widthString;
if (this.isSelectable) widthString = '30px ' + widthString;
if (this._isHierarchyActive) {
widthString = Math.max(30, this._hierarchyMaxDepth * 12 + 20) + 'px ' + widthString;
}
}
this.set('_widthString', StringUtils.safeTags('grid-template-columns: ' + widthString));
},
_repaintRequiredObserver: action(function () {
once(this, this._recalculateColumnWidths);
}),
/**
* Action for handling incoming requests to toggle a column showing/hidden.
*
* @param column {wb-data-table-column}
* @param showing {Boolean}
*/
toggleColumnShowing: action(function (column, showing) {
if (!column) return;
if (column.required) return;
if (!column.enabled) return;
if (showing === undefined || showing === null) showing = !column.showing;
// prevent removing all columns from the table.
const showingColumns = this._columnArray.filter((column) => column.enabled && (column.showing || column.required));
if (showing === false && showingColumns.length < 2) return;
column.set('showing', showing);
once(this, this._recalculateColumnWidths);
}),
// rows
/**
* @private
* Fired whenever the @rows argument changes. Triggers the rendering process.
*/
_rowsObserver: action(function () {
if (this.isDestroyed || this.isDestroying) return;
this.set('_inRendering', true);
next(this._renderingStarted);
}),
/**
* @private
* Called to transfer the value of @rows to @_internalRows, which will cause
* ember to render the rows out. We do this so we can control the visual rendering
* message before ember starts rendering.
*/
_renderingStarted: action(function () {
if (this.isDestroyed || this.isDestroying) return;
this.set('_internalRows', this.rows);
next(this._renderingComplete);
}),
/**
* @private
* Called once rendering of the rows is complete.
*/
_renderingComplete: action(function () {
if (this.isDestroyed || this.isDestroying) return;
this.set('_inRendering', false);
}),
/**
* @private
* Called whenever the body element is scrolled. We then adjust the headings
* and footings positions (through negative margins) to match the scrolled
* body area. Tricksy.
*/
_scrollHandler: action(function () {
const left = this.bodyElement.scrollLeft;
this.headingsElement.style.marginLeft = '' + -left + 'px';
this.footingsElement.style.marginLeft = '' + -left + 'px';
}),
/**
* Computed property that returns the showing columns. The showing columns is taken from rows
* then run through filters, then sorting, then hierarchy, to finally land in showingRows. This
* is the final set of rows shown to the client.
*
* This computed property does nothing in and of itself, but is rather just an alias, to the last
* row computation. It should be largely left alone. Please do not change to ember alias either.
* f
*/
showingRows: computed('_hierarchyRowsShowing', function () {
// this is a rollup of all the different types of modifiers rows can have (sort, pagination, filter, hierarcy, etc).
// do not change.
return this._hierarchyRowsShowing;
}),
/**
* Computed property that returns true if static table mode is being used.
*/
isUsingCustomRows: computed('_internalRows.[]', 'isReady', function () {
return (this._internalRows === null && this.isReady) || false;
}),
/**
* Returns the row for the given rowIndex position, relative to 0.
*
* @param {Number} rowIndex
* @returns {*}
*/
getRow(rowIndex) {
return (this._internalRows && this._internalRows[rowIndex]) || null;
},
/**
* Returns the wb-data-table-row component for a given row.
*
* @param {*} row
* @returns {wb-data-table-row}
*/
_getRowComponent(row) {
if (!row) return null;
return this._rowComponents.find((rc) => rc.row === row);
},
// searching
/**
* @private
* Computed property that returns _internalRows with filtering applied.
*/
_searchedRows: computed(
'fetcher',
'_internalRows.[]',
'_isSearchingActive',
'_searchText',
'searchFields',
'_showingColumns',
function () {
let rows =
(this._internalRows && this._internalRows.toArray && [].concat(this._internalRows.toArray())) ||
(this._internalRows && [].concat(this._internalRows)) ||
[];
let search = (this._searchText && this._searchText.trim()) || '';
search = search.replaceAll(/[\]{}]/g, '').replaceAll(/([-_*+,.<>?;:'"[]\{\}\\\/\^\$!@#%&\(\)])/g, '\\$1');
if (!this.fetcher && this._isSearchingActive && search) {
let fields =
(this.searchFields === '*' &&
this._showingColumns.map((column) => column.field).filter((field) => !!field)) ||
(this.searchFields.length > 0 && ('' + this.searchFields).split(/,/g)) ||
[];
fields = fields.map((field) => ('' + field).trim()).filter((field) => !!field);
const words = search.split(/\s+/g);
const regexs = words.map((word) => new RegExp(word, 'ig'));
rows = rows.filter((row) => {
return regexs.every((regex) => {
return fields.some((field) => {
const value = row[field];
if (value === undefined || value === null) return false;
return ('' + value).match(regex);
});
});
});
}
return rows;
}
),
/**
* @private
* Handler for pressing the search button.
*/
_executeSearchHandler: action(function () {
const search = (this._pendingSearchText && ('' + this._pendingSearchText).trim()) || '';
if (search !== this._searchText) this.search(search);
}),
/**
* @private
* Handler for when search input field is changed.
*/
_searchTextChangeHandler: action(function (text) {
const search = (text && ('' + text).trim()) || '';
if (this._pendingSearchText !== text) {
this.set('_pendingSearchText', search);
}
}),
/**
* Allows the developer to externally specify the search text.
*
* @param {String} text The text to search on. Null or empty string will clear searching.
*/
search: action(function (text = '') {
if (text === null || text === undefined) text = '';
if (this._searchText !== text) this.set('_searchText', text);
if (this._pendingSearchText !== text) this.set('_pendingSearchText', text);
this.clearSelection();
next(this.changePage, 1);
next(this.scrollToTop);
}),
/**
* @private
* Computed property that returns true if searching is active.
*/
_isSearchingActive: computed('searchable', '_searchText', function () {
const search = (this._searchText && this._searchText.trim()) || '';
return this.searchable && search.length > 0;
}),
// filtering
/**
* @private
* Computed property that returns rows with filtering applied.
*/
_filteredRows: computed(
'fetcher',
'_searchedRows',
'_isFilteringActive',
'_filteringColumns',
'_filteringColumns.@each.filterPaused',
function () {
let rows = [].concat(this._searchedRows || []);
if (!this.fetcher && this._isFilteringActive) {
const columns = this._filteringColumns.filter((column) => !column.filterPaused);
rows = rows.filter((row) => {
return !columns.some((column) => {
if (!column) return false;
if (!column.filterBy) return false;
if (!column.filterMode) return false;
let mode = column.filterMode;
let text = column.filterBy;
let value = row[column.field];
if (column.filterMatch && column.filterMatch instanceof Function) {
return column.filterMatch(text, mode, value, row, column);
}
if (value === undefined || value === null) value = '';
mode = mode.toLowerCase();
text = text.toLowerCase();
value = ('' + value).toLowerCase();
if (mode === 'contains') return value.indexOf(text) < 0;
else if (mode === 'exact') return value !== text;
else if (mode === 'starts') return !value.startsWith(text);
else if (mode === 'ends') return !value.endsWith(text);
return false;
});
});
}
return rows;
}
),
/**
* @private
* Computed property that returns an array of all columns where filtering is active.
*/
_filteringColumns: computed('fetcher', '_columnArray.@each.{filterBy,filterMode}', function () {
return this._columnArray.filter((column) => !!column.filterBy);
}),
/**
* @private
* Computed property that returns true if any column allows filtering.
*/
_isFilteringAvailable: computed('_columnArray.@each.filterable', function () {
return this._columnArray.some((column) => column.filterable);
}),
/**
* @private
* Computed property that returns true if any column is filtering.
*/
_isFilteringActive: computed('_filteringColumns', function () {
return this._filteringColumns.length > 0;
}),
/**
* @private
* Computed property that returns the number of rows currently hidden due to filtering.
*/
_filteredOutCount: computed('rowCount', '_internalRows', '_filteredRows', function () {
if (this.rowCount !== null) return null;
return ((this._internalRows && this._internalRows.length) || 0) - this._filteredRows.length;
}),
/**
* @private
* Computed property that returns true if all results are filtered out.
*/
_isAllFilteredOut: computed('_isFilteringActive', '_filteredRows', function () {
if (this.rowCount !== null) return null;
return this._isFilteringActive && this._filteredRows.length < 1;
}),
/**
* Action for handling incoming requests to apply a new filter.
*
* @param column {wb-data-table-column} The column to filter on.
* @param value {String} the value to filter against.
* @param mode {String} The filter mode: 'contains', 'exact', 'starts', 'ends'
*/
filter: action(function (column, value = null, mode = 'contains') {
if (!column) return;
if (!this.fetcher) this.set('_inRendering', true);
next(() => {
this.clearSelection();
next(this.changePage, 1);
next(this.scrollToTop);
column.set('filterBy', value);
column.set('filterMode', mode);
column.set('filterPaused', false);
next(() => {
if (!this.fetcher) this.set('_inRendering', false);
else this.fetchRows.perform();
});
});
}),
toggleFilterPause: action(function (column, state = null) {
if (!column) return;
column.set('filterPaused', state === null ? !column.filterPaused : state);
}),
/**
* Action for handling incoming requests to show the filter dialog for a given column.
* Will hide any other filter dialog currently showing.
*
* @param column {wb-data-table-column} Column to show dialog for.
*/
showFilterDialog: action(function (column) {
if (!column) return;
if (!column.filterable) return;
if (this._filterPopup) this.hideFilterDialog();
const args = {
column: column,
table: this,
filterText: column.filterBy || '',
filterMode: column.filterMode || 'contains',
onChange: this.filterApplyHandler,
};
const positioning = {
relativeTo: column.element,
relativeSpot: 'top-left',
originSpot: 'bottom-left',
ensureVisible: true,
};
const popup = this.popper.openPopup(
'wb-data-table-filter-panel',
args,
positioning,
undefined,
undefined,
this.hideFilterDialog
);
this.set('_filterPopup', popup);
this.set('_filterColumn', column);
column.set('_isFilterDialogShowing', true);
}),
/**
* Action for handling incoming requests to hide the currently showing filter dialog.
*/
hideFilterDialog: action(function () {
if (!this._filterPopup) return;
this.popper.closePopup(this._filterPopup);
if (this._filterColumn) {
this._filterColumn.set('_isFilterDialogShowing', false);
}
this.set('_filterColumn', null);
this.set('_filterPopup', null);
}),
/**
* Action for handling incoming requests to apply the results of the currently showing filter dialog.
* Hides the filter dialog.
*
* @param column {wb-data-table-column} The column to filter on.
* @param value {String} the value to filter against.
* @param mode {String} The filter mode: 'contains', 'exact', 'starts', 'ends'
*/
filterApplyHandler: action(function (column, value, mode) {
this.hideFilterDialog();
this.filter(column, value, mode);
}),
// sorting
/**
* @private
* Computed property that returns rows in a sorted order. This is done after filtering
* is applied.
*/
_sortedRows: computed('fetcher', '_filteredRows', '_isSortingActive', '_sortingColumns', function () {
let rows = [].concat(this._filteredRows || []);
let columns = this._sortingColumns;
columns = columns.reverse();
if (!this.fetcher && this._isSortingActive) {
columns.forEach((column) => {
const comparator = column._sortComparator || SortUtils.compare;
const field = column.field;
const direction = column.sorted ? column.sorted.toUpperCase() : 'DESC';
if (field) {
rows = rows.sort(function (a, b) {
// if we got undefined or null object, handle them.
if (a === undefined) a = null;
if (b === undefined) b = null;
if (a === null && b === null) return 0;
if (a !== null && b === null) return -1;
if (a === null && b !== null) return 1;
// get the field vale we want to sort on.
a = a[field];
b = b[field];
// if field value undefined or null, handle that.
if (a === undefined) a = null;
if (b === undefined) b = null;
if (a === null && b === null) return 0;
if (a !== null && b === null) return -1;
if (a === null && b !== null) return 1;
return direction === 'DESC' ? comparator(a, b) : comparator(b, a);
});
}
});
}
return rows;
}),
/**
* @private
* Computed property that returns an array of columns with sorting enabled.
*/
_sortingColumns: computed('fetcher', '_columnArray.@each.sorted', function () {
return this._columnArray.filter((column) => !!column.sorted);
}),
/**
* @private
* Computed property that returns true if sorting is active.
*/
_isSortingActive: computed('_sortingColumns', function () {
return this._sortingColumns.length > 0;
}),
/**
* Action for handling incoming requests to sort a specific column.
*
* @param column {wb-data-table-column} The column to sort.
* @param direction {String} The direction of the sort "ASC" or "DESC" or null for no sort.
*/
sort: action(function (column, direction = null) {
if (!this._internalRows) return;
if (!column) return;
direction = (direction && direction.toUpperCase()) || null;
this.clearSelection();
next(this.changePage, 1);
next(this.scrollToTop);
this._columns.forEach((col) => {
if (col === column) column.set('sorted', direction);
else col.set('sorted', null);
});
if (this.fetcher) this.fetchRows.perform();
}),
// paging
/**
* @private
* Computed property that returns rows if internal paging is in use.
*/
_pagedRows: computed('fetcher', '_sortedRows', '_isInternalPagerActive', 'page', 'pageSize', function () {
let rows = [].concat(this._sortedRows || []);
if (!this.fetcher && this._isInternalPagerActive) {
const start = Math.max(0, (this.page - 1) * this.pageSize);
rows = rows.slice(start, start + this.pageSize);
}
return rows;
}),
/**
* @private
* Computed property that returns true if internal paging is being used. Internal
* paging is when no fetcher is given and pageable is true.
*/
_isInternalPagerActive: computed('fetcher', 'pageable', function () {
return this.pageable && !this.fetcher;
}),
/**
* @private
* Computed property that returns true if page info is to be shown under the label.
*/
_showPagerInfo: computed('fetcher', 'rowStartPosition', 'rowEndPosition', 'rowCount', function () {
if (!this.fetcher) return true;
if (this.fetcher && this.rowStartPosition !== null && this.rowEndPosition !== null && this.rowCount !== null) {
return true;
}
return false;
}),
/**
* @private
* Computed property that returns the starting index of the current rows showing. This is
* either computed from the page and page size, or it can be overridden by setting
* rowStartPosition to a non-null number.
*/
_startingPosition: computed('pageable', 'page', 'pageSize', 'rowStartPosition', function () {
if (this.rowStartPosition !== null) return this.rowStartPosition;
if (!this.pageable) return 1;
return Math.max(1, (this.page - 1) * this.pageSize + 1);
}),
/**
* @private
* Computed property that returns the ending index of the current rows showing. This is
* either computed from the page and page size, or it can be overridden by setting
* rowEndPosition to a non-null number.
*/
_endingPosition: computed(
'pageable',
'_startingPosition',
'rowEndPosition',
'_pagedRows',
'_sortedRows',
function () {
if (this.rowEndPosition !== null) return this.rowEndPosition;
if (!this.pageable) return this._sortedRows.length;
if (this.rowCount !== null) {
return Math.min(this.rowCount0, this.page * this.pageSize + this.pageSize + 1);
}
return this._startingPosition + this._pagedRows.length - 1;
}
),
/**
* @private
* Computed property that returns the total number of rows in the entire data set. This
* is computed if not pageable or the internal pager is being used. Otherwise it is
* taken from the rowCount argument which must be provided.
*/
_totalRowCount: computed('pageable', 'page', 'pageSize', 'rowCount', '_sortedRows', '_pagedRows', function () {
if (!this.pageable) return this._sortedRows.length;
if (this.rowCount !== null) return this.rowCount;
if (this._isInternalPagerActive) return this._sortedRows.length;
return this.page * this.pageSize + this._pagedRows.length;
}),
/**
* @private
* Computed property that returns true if we are viewing the first page.
*/
_isPageFirstDisabled: computed('_startingPosition', function () {
return this._startingPosition <= 1;
}),
/**
* @private
* Computed property that returns true if there is no previous page.
*/
_isPagePreviousDisabled: computed('_startingPosition', function () {
return this._startingPosition <= 1;
}),
/**
* @private
* Computed property that returns true if no next page is available.
*/
_isPageNextDisabled: computed('_endingPosition', '_totalRowCount', function () {
return this._endingPosition >= this._totalRowCount;
}),
/**
* @private
* Computed property that returns true if the current page is the last page.
*/
_isPageLastDisabled: computed('_endingPosition', '_totalRowCount', function () {
return this._endingPosition >= this._totalRowCount;
}),
/**
* Action for handling incoming requests to move the page some number of
* pages forward or backward. You may never move before the first page or
* after the last page.
*
* @param amount {Number} The number of pages to move forward (positive numbers) or backward (negative numbers).
*/
pageAdvanceHandler: action(function (amount = 1) {
this.changePage(this.page + amount);
}),
/**
* Action for handling incoming requests to move to the first page.
*/
pageStartHandler: action(function () {
this.changePage(1);
}),
/**
* Action for handling incoming requests to move to the last page.
*/
pageEndHandler: action(function () {
this.changePage(Math.min(this._totalRowCount, ((this._totalRowCount / this.pageSize) | 0) + 1));
}),
/**
* Action for handling incoming requests to change to a specific page.
*
* @param newPage {Number} The page to switch to.
*/
changePage: action(function (newPage = 1) {
if (typeof newPage !== 'number') return;
newPage = Math.max(1, Math.min(newPage, this._totalRowCount, ((this._totalRowCount / this.pageSize) | 0) + 1));
next(this.scrollToTop);
this.set('page', newPage);
if (this.fetcher) this.fetchRows.perform();
}),
// hierarchy
/**
* @private
* Computed property that returns the rows as a hierarchy set of rows. This in essence flattens out
* the hierarchy and associates each row is a hierarchInfo entry which contains specifics about
* the rows place in the hierarchy.
*/
_hierarchyRows: computed('_pagedRows', '_hierarchyIndex', 'hierarchyField', '_isHierarchyActive', function () {
let rows = [].concat(this._pagedRows || []);
if (this._isHierarchyActive) {
const isLastChild = (row, parent) => {
if (!row) return true;
if (!parent) return rows.indexOf(row) === rows.length - 1;
const children = this._getHierarchyChildren(parent);
return (children && children.indexOf(row) === children.length - 1) || false;
};
const getAncestors = (row) => {
const ancestors = [];
let ancestor = this._getHierarchyParent(row);
while (ancestor) {
if (ancestor) {
ancestors.unshift(ancestor);
ancestor = this._getHierarchyParent(ancestor);
}
}
return ancestors;
};
const builder = (row, depth = 1, parent = null) => {
if (!row) return null;
let children = row[this.hierarchyField];
const info = this._getHierarchyInfo(row);
info.depth = depth;
info.parent = parent;
info.children = children;
info.lastChild = isLastChild(row, parent);
info.ancestors = getAncestors(row);
let rows = [row];
if (children) {
if (!(children instanceof Array)) children = [children];
if (children.length > 0) {
children.forEach((child) => {
const built = builder(child, depth + 1, row);
if (built) rows = rows.concat(built);
});
}
}
return rows;
};
rows = rows.reduce((rows, row) => {
if (!row) return rows;
const built = builder(row);
if (built) rows = rows.concat(built);
return rows;
}, []);
const maxDepth = rows.reduce((max, row) => {
const info = this._getHierarchyInfo(row);
return Math.max(max, info.depth);
}, 0);
rows.forEach((row) => {
const info = this._getHierarchyInfo(row);
info.steps = new Array(maxDepth).fill(0).map((x, pos) => {
let step = 'to';
const stepDepth = pos + 1;
if (stepDepth === info.depth - 1) {
// current
const parentInfo = this._getHierarchyInfo(info.parent);
if (parentInfo.children && parentInfo.children.indexOf(row) > -1) {
step = 'sibling';
}
if (step === 'sibling' && parentInfo.children.indexOf(row) === parentInfo.children.length - 1) {
step = 'sibling-last';
}
} else if (stepDepth === info.depth) {
// current
if (!info.children || info.children.length < 1) step = 'leaf';
else step = 'parent';
} else if (stepDepth < info.depth) {
// prev
step = 'none';
const ancestor = info.ancestors[pos];
const ancestorInfo = this._getHierarchyInfo(ancestor);
if (ancestor && ancestorInfo && !ancestorInfo.lastChild) {
step = 'thru';
}
} else {
// future
step = 'to';
}
return step;
});
});
}
return rows;
}),
/**
* @private
* Computed property that returns true if and hierarchy row is expanded.
*/
_hierarchyRowsShowing: computed('_hierarchyRows', '_hierarchyIndex', '_isHierarchyActive', function () {
let rows = [].concat(this._hierarchyRows || []);
if (this._isHierarchyActive) {
rows = rows.filter((row) => this._getHierarchyRowShowing(row));
}
return rows;
}),
/**
* @private
* Computed property that returns true if hierarchy rows are enabled.
*/
_isHierarchyActive: computed('hierarchyField', function () {
return !!this.hierarchyField;
}),
/**
* @private
* Computed property that returns the maximum depth any rows might have in the hierarchy.
*/
_hierarchyMaxDepth: computed('_hierarchyRows', 'hierarchyField', function () {
if (!this._isHierarchyActive) return 0;
return this._hierarchyRows.reduce((max, row) => {
const info = this._getHierarchyInfo(row);
return Math.max(max, info.depth);
}, 0);
}),
/**
* @private
* Computed property that returns true if all hierarchy rows are expanded.
*/
_hierarchyAllExpanded: computed(
'_isHierarchyActive',
'_hierarchyIndex',
'_hierarchyRows',
'hierarchyField',
function () {
if (!this._isHierarchyActive) return false;
return this._hierarchyRows.every((row) => {
const info = this._getHierarchyInfo(row);
return info.opened || !info.children || info.children.length < 1;
});
}
),
/**
* @private
* Computed property that returns true of all hierarchy rows are collapsed.
*/
_hierarchyNoneExpanded: computed(
'_isHierarchyActive',
'_hierarchyIndex',
'_hierarchyRows',
'hierarchyField',
function () {
if (!this._isHierarchyActive) return false;
return this._hierarchyRows.every((row) => {
const info = this._getHierarchyInfo(row);
if (!info.children) return true;
if (info.children.length < 1) return true;
return !info.opened;
});
}
),
/**
* @private
*
* Returns the parent row for a given hierarchy row.
*
* @param {*} row
* @returns {*}
*/
_getHierarchyParent(row) {
if (!row) return null;
const info = this._getHierarchyInfo(row);
return info.parent;
},
/**
* @private
*
* Returns an array of all child rows for a given row.
*
* @param {*} row
* @returns {Array<*>}
*/
_getHierarchyChildren(row) {
if (!row) return null;
return row[this.hierarchyField] || null;
},
/**
* @private
*
* Returns a hierarchy info object for the given row. The hierarchy info objects are
* created most likely when a hierarchy table is enabled and rows are added to that
* table.
*
* @param {*} row
* @returns {Object}
*/
_getHierarchyInfo(row) {
if (!row) return null;
let info = this._hierarchyInfo.get(row) || null;
if (!info) {
info = {
row,
depth: 1,
opened: false,
parent: null,
lastChild: true,
children: null,
};
this._hierarchyInfo.set(row, info);
}
return info;
},
/**
* @private
*
* Returns the depth for a given row in the hierarchy structure.
*
* @param {*} row
* @returns {Number}
*/
_getHierarchyDepth(row) {
if (!row) return 0;
const info = this._getHierarchyInfo(row);
return info.depth;
},
/**
* @private
*
* Returns true if the hierarchy row is showing or not.
*
* @param {*} row
* @returns {Boolean}
*/
_getHierarchyRowShowing(row) {
if (!row) return false;
const info = this._getHierarchyInfo(row);
return (
(info.ancestors &&
info.ancestors.reverse().every((ancestor) => {
const ancestorInfo = this._getHierarchyInfo(ancestor);
return ancestorInfo.opened;
})) ||
false
);
},
/**
* @private
*
* Action for handling incoming requests to determine if a hierarchy row is expanded
* or not.
*
* @param row {*} The row to test.
* @return {Boolean} true if the row is expanded.
*/
_getHierarchyOpened: action(function (row) {
if (!row) return false;
const info = this._getHierarchyInfo(row);
return info.opened;
}),
/**
* @private
*
* Action for handling incoming requests to determine how each level of depth is to be rendered.
* This is largely only useful internally.
*
* @param row {*} The row to lookup.
* @return {Array<String>} An array of steps for each level of depth from 0 to maxDepth.
*/
_getHierarchySteps: action(function (row) {
if (!row) return 0;
const info = this._getHierarchyInfo(row);
return info.steps || [];
}),
/**
* Action for handling incoming requests to toggle a hierarchy row expanded or collapsed.
* if state is null the row will be toggled. If state is true, the row will be expanded.
* If state is false, the row will be collapsed.
*
* @param row {*} The row to toggle
* @param state {null|Boolean} The state to toggle to.
*/
toggleHierarchyRow: action(function (row, state = null) {
if (!row) return;
const info = this._getHierarchyInfo(row);
if (!info) return;
if (state === null || state === undefined) info.opened = !info.opened;
else info.opened = !!state;
this.set('_hierarchyIndex', this._hierarchyIndex + 1);
}),
/**
* Action for handling incoming requests to toggle all hierarchy rows expanded.
*/
toggleHierarchyAllCollapsed: action(function () {
if (!this._isHierarchyActive) return;
this._hierarchyRows.forEach((row) => {
const info = this._getHierarchyInfo(row);
info.opened = false;
});
this.set('_hierarchyIndex', this._hierarchyIndex + 1);
}),
/**
* Action for handling incoming requests to toggle all hierarchy rows collapsed.
*/
toggleHierarchyAllExpanded: action(function () {
if (!this._isHierarchyActive) return;
this._hierarchyRows.forEach((row) => {
const info = this._getHierarchyInfo(row);
info.opened = true;
});
this.set('_hierarchyIndex', this._hierarchyIndex + 1);
}),
/**
* Action for handling incoming requests to the hierarchy row toggle header clicks.
*/
hierarchyAllHandler: action(function () {
if (!this._isHierarchyActive) return;
if (this._hierarchyAllExpanded) this.toggleHierarchyAllCollapsed();
else this.toggleHierarchyAllExpanded();
}),
// hovering
/**
* @private
* Computed property that returns true if column hovering is set.
*/
_isHoverColumnsOn: computed('hoverMode', function () {
const mode = EmberUtils.validateComponentArgumentStringOptions(this.hoverMode, 'NONE,ROW,COLUMN,BOTH', 'ROW');
return mode === 'BOTH' || mode === 'COLUMN' || false;
}),
/**
* @private
* Computed property that returns true of row hovering is set.
*/
_isHoverRowsOn: computed('hoverMode', function () {
const mode = EmberUtils.validateComponentArgumentStringOptions(this.hoverMode, 'NONE,ROW,COLUMN,BOTH', 'ROW');
return mode === 'BOTH' || mode === 'ROW' || false;
}),
/**
* Action for handling incoming requests to identify which column is currently being hovered.
*/
hoverColumn: action(function (columnIndex = null) {
if (columnIndex === this._hoverColumnIndex) return;
if (this._hoverColumnIndex !== null) {
const col = [...this._columns][this._hoverColumnIndex];
if (col && col.element) {
col.element.classList.remove('wb-data-table-column-hover');
}
[...this._cells].forEach((cell) => {
if (!cell || !cell.element) return;
const colIndex = cell.getColumnIndex();
if (colIndex === this._hoverColumnIndex) {
cell.element.classList.remove('wb-data-table-cell-column-hover');
}
});
}
if (columnIndex !== null) {
const col = [...this._columns][columnIndex];
if (col && col.element) {
col.element.classList.add('wb-data-table-column-hover');
}
[...this._cells].forEach((cell) => {
if (!cell || !cell.element) return;
const colIndex = cell.getColumnIndex();
if (colIndex === columnIndex) {
cell.element.classList.add('wb-data-table-cell-column-hover');
}
});
}
this.set('_hoverColumnIndex', columnIndex);
}),
// header, footer, heading, footing
/**
* @private
* Computed property that returns true if headings is 'text' mode.
*/
_isHeadingText: computed('headings', function () {
const mode = EmberUtils.validateComponentArgumentStringOptions(this.headings, 'TEXT,BAR,NONE', 'TEXT');
return mode === 'TEXT';
}),
/**
* @private
* Computed property that returns true if headings is 'bar' mode.
*/
_isHeadingBar: computed('headings', function () {
const mode = EmberUtils.validateComponentArgumentStringOptions(this.headings, 'TEXT,BAR,NONE', 'TEXT');
return mode === 'BAR';
}),
/**
* @private
* Computed property that returns true if headings if 'none' mode.
*/
_isHeadingNone: computed('headings', function () {
const mode = EmberUtils.validateComponentArgumentStringOptions(this.headings, 'TEXT,BAR,NONE', 'TEXT');
return mode === 'NONE';
}),
/**
* @private
* Computed property that returns true if footings is 'text' mode.
*/
_isFootingText: computed('footings', function () {
const mode = EmberUtils.validateComponentArgumentStringOptions(this.footings, 'TEXT,BAR,NONE', 'BAR');
return mode === 'TEXT';
}),
/**
* @private
* Computed property that returns true if footings is '_isFootingBar' mode.
*/
_isFootingBar: computed('footings', function () {
const mode = EmberUtils.validateComponentArgumentStringOptions(this.footings, 'TEXT,BAR,NONE', 'BAR');
return mode === 'BAR';
}),
/**
* @private
* Computed property that returns true if footings is 'none' mode.
*/
_isFootingNone: computed('footings', function () {
const mode = EmberUtils.validateComponentArgumentStringOptions(this.footings, 'TEXT,BAR,NONE', 'BAR');
return mode === 'NONE';
}),
// actions
/**
* @private
* Computed property that returns true if action bars should be displayed on 'top'.
*/
_isActionBarTop: computed('actionBar', function () {
const mode = EmberUtils.validateComponentArgumentStringOptions(this.actionBar, 'TOP,BOTTOM,BOTH,NONE', 'TOP');
return mode === 'BOTH' || mode === 'TOP';
}),
/**
* @private
* Computed property that returns true if action bars should be displayed on 'bottom'.
*/
_isActionBarBottom: computed('actionBar', function () {
const mode = EmberUtils.validateComponentArgumentStringOptions(this.actionBar, 'TOP,BOTTOM,BOTH,NONE', 'TOP');
return mode === 'BOTH' || mode === 'BOTTOM';
}),
/**
* @private
* Computed property that returns true if action bars are disabled.
*/
_isActionBarNone: computed('actionBar', function () {
const mode = EmberUtils.validateComponentArgumentStringOptions(this.actionBar, 'TOP,BOTTOM,BOTH,NONE', 'TOP');
return mode === 'NONE';
}),
/**
* @private
* Computed property that returns the WbMenu menu structure for column hiding.
*/
_toggleColumnMenu: computed('_columnArray.@each.{label,showing}', function () {
const menu = [
{
type: 'text',
label: 'Select which columns you<br/>want to display in this table.',
},
{
type: 'break',
},
{
type: 'item',
label: 'Reset Default<br/>Columns Showing',
icon: 'line-columns',
value: 'reset',
},
];
if (this._columnArray && this._columnArray.length > 1) {
menu.push({
type: 'break',
});
}
(this._columnArray || []).forEach((column, i) => {
menu.push({
type: 'toggle',
label: StringUtils.stripTags(column.label || 'Column ' + (i + 1)),
selected: column.showing,
disabled: column.required || !column.enabled,
value: column,
});
});
return menu;
}),
/**
* @private
* Computed property that returns the WbMenu menu structure for exporting.
*/
_exportMenu: computed(
'exportable',
'pageable',
'isSelectable',
'_selectedRowCount',
'isAllColumnsShowing',
'_isFilteringActive',
'_exportRowsChoice',
'_exportColumnsChoice',
'_exportFlavorChoice',
function () {
const menu = [
{
type: 'text',
label: 'Select how you want to export<br/>the data in this table.',
},
{
type: 'break',
},
];
// Rows
menu.push({
type: 'choice',
label: 'All Rows',
value: 'export-rows-all',
selected: this._exportRowsChoice === 'ALL',
});
if (this.pageable || this._isFilteringActive) {
menu.push({
type: 'choice',
label: 'Currently Showing Rows',
value: 'export-rows-current',
selected: this._exportRowsChoice === 'CURRENT',
});
}
if (this.isSelectable && this._selectedRowCount > 0) {
menu.push({
type: 'choice',
label: 'Currently Selected Rows',
value: 'export-rows-selected',
selected: this._exportRowsChoice === 'SELECTED',
});
}
menu.push({
type: 'break',
});
// columns
menu.push({
type: 'choice',
label: 'All Columns',
value: 'export-columns-all',
selected: this._exportColumnsChoice === 'ALL',
});
if (!this.isAllColumnsShowing) {
menu.push({
type: 'choice',
label: 'Currently Showing Columns',
value: 'export-columns-current',
selected: this._exportColumnsChoice === 'CURRENT',
});
}
menu.push({
type: 'break',
});
// flavor
menu.push({
type: 'choice',
label: 'As CSV',
value: 'export-flavor-csv',
selected: this._exportFlavorChoice === 'CSV',
});
menu.push({
type: 'choice',
label: 'As JSON',
value: 'export-flavor-json',
selected: this._exportFlavorChoice === 'JSON',
});
menu.push({
type: 'break',
});
// export
menu.push({
type: 'item',
label: 'Export',
icon: 'download',
value: 'export',
});
return menu;
}
),
/**
* @private
* Computed property that returns the WbMenu menu structure for filtering.
*/
_filteringMenu: computed('_filteringColumns.@each.{filterBy,filterMode,filterPaused}', function () {
const menu = [
{
type: 'text',
label: 'Adjust which columns you<br/>want to filter on.',
},
{
type: 'break',
},
{
type: 'item',
label: 'Remove All Active Filters',
icon: 'times',
value: 'remove-all',
},
{
type: 'break',
},
];
(this._filteringColumns || []).forEach((column) => {
const index = column.getColumnIndex();
let label = StringUtils.stripTags(column.label || 'Column ' + (index + 1));
if (column.filterMode === 'contains') label += ' contains ';
else if (column.filterMode === 'exact') label += ' is exactly ';
else if (column.filterMode === 'starts') label += ' starts with ';
else if (column.filterMode === 'ends') label += ' ends with ';
label += '"' + StringUtils.stripTags(column.filterBy) + '"';
menu.push({
type: 'toggle',
label: label,
selected: !column.filterPaused,
value: column,
});
});
return menu;
}),
/**
* Action for handling incoming requests to deal with clicking on the column showing menu items.
*/
columnMenuHandler: action(function (column) {
if (!column) return;
if (column === 'reset') {
this._columnArray.forEach((column) => this.toggleColumnShowing(column, column._defaultShowing));
} else {
this.toggleColumnShowing(column);
}
}),
/**
* Action for handling incoming requests to deal with clicking on the export menu items.
*/
exportMenuHandler: action(function (value) {
if (value === 'export-rows-all') this.set('_exportRowsChoice', 'ALL');
if (value === 'export-rows-current') {
this.set('_exportRowsChoice', 'CURRENT');
}
if (value === 'export-rows-selected') {
this.set('_exportRowsChoice', 'SELECTED');
}
if (value === 'export-columns-all') this.set('_exportColumnsChoice', 'ALL');
if (value === 'export-columns-current') {
this.set('_exportColumnsChoice', 'CURRENT');
}
if (value === 'export-flavor-csv') this.set('_exportFlavorChoice', 'CSV');
if (value === 'export-flavor-json') this.set('_exportFlavorChoice', 'JSON');
if (value === 'export') this.export();
}),
/**
* Action for handling incoming requests to deal with clicking on the filtering menu items.
*/
filteringMenuHandler: action(function (column) {
if (!column) return;
if (column === 'remove-all') {
this._filteringColumns.forEach((column) => this.filter(column, null));
} else {
this.toggleFilterPause(column);
}
}),
// selection
/**
* Computed property that returns true if the table is selectable.
*/
isSelectable: computed('selectMode', function () {
const mode = EmberUtils.validateComponentArgumentStringOptions(this.selectMode, 'SINGLE,MULTI,TOGGLE,NONE', 'NONE');
return mode === 'SINGLE' || mode === 'MULTI' || mode === 'TOGGLE';
}),
/**
* Computed property that returns true of selection mode is 'single'.
*/
isSelectModeSingle: computed('selectMode', function () {
const mode = EmberUtils.validateComponentArgumentStringOptions(this.selectMode, 'SINGLE,MULTI,TOGGLE,NONE', 'NONE');
return mode === 'SINGLE';
}),
/**
* Computed property that returns true of selection mode is 'multi'.
*/
isSelectModeMulti: computed('selectMode', function () {
const mode = EmberUtils.validateComponentArgumentStringOptions(this.selectMode, 'SINGLE,MULTI,TOGGLE,NONE', 'NONE');
return mode === 'MULTI';
}),
/**
* Computed property that returns true of selection mode is 'toggle'.
*/
isSelectModeToggle: computed('selectMode', function () {
const mode = EmberUtils.validateComponentArgumentStringOptions(this.selectMode, 'SINGLE,MULTI,TOGGLE,NONE', 'NONE');
return mode === 'TOGGLE';
}),
/**
* Computed property that returns true if all rows are selected.
*/
isAllSelected: computed('isSelectable', 'showingRows', '_selectedRowCount', function () {
if (!this.isSelectable) return false;
return this.showingRows.length === this._selectedRowCount;
}),
/**
* Computed property that returns true if no rows are selected.
*/
isNoneSelected: computed('isSelectable', '_selectedRowCount', function () {
if (!this.isSelectable) return true;
return this._selectedRowCount === 0;
}),
/**
* Computed property that returns true if any rows are selected.
*/
isSomeSelected: computed('isSelectable', '_selectedRowCount', function () {
if (!this.isSelectable) return false;
return this._selectedRowCount > 0;
}),
/**
* Computed property that returns an array of selected rows.
*/
selectedRows: computed('_selectionIndex', function () {
return this._rowComponents.filterBy('_selected').mapBy('row');
}),
/**
* Returns the number of selected rows.
*/
_selectedRowCount: computed('_internalRows', 'selectedRows', function () {
return (this._internalRows && this._internalRows.length > 0 && this.selectedRows.length) || 0;
}),
/**
* @private
* Computed property that returns an array of selected row components.
*/
_selectedRowComponents: computed('_rowComponents.@each._selected', function () {
return this._rowComponents.filterBy('_selected');
}),
/**
* Returns true if the given row is currently selected.
*
* @param {*} row
* @returns {Boolean}
*/
isRowSelected(row) {
if (!row) return false;
return this.selectedRows.indexOf(row) > -1;
},
/**
* Toggle the given row selected based on the selection mode (@selectMode).
*
* @param {*} row
* @param {HTMLEvent} event
* @param {boolean} stateShift
*/
selectRow(row, event = null, stateShift = false) {
const mode = EmberUtils.validateComponentArgumentStringOptions(this.selectMode, 'SINGLE,MULTI,TOGGLE,NONE', 'NONE');
if (mode === 'NONE') return;
const isHoldingShift = (event && event.shiftKey) || stateShift;
if (!(row instanceof Component)) {
row = this._getRowComponent(row);
}
if (!row) return;
if (mode === 'SINGLE') {
if (!row._selected) {
this.clearSelection();
row.set('_selected', true);
}
} else if (mode === 'TOGGLE') {
if (isHoldingShift && this._lastSelectedRowComponent) {
let inside = false;
const state =
this._lastSelectedRowComponent._selected === undefined ? true : this._lastSelectedRowComponent._selected;
this._rowComponents.forEach((rc) => {
if (rc === row || rc === this._lastSelectedRowComponent) {
inside = !inside;
}
if (inside) {
rc.set('_selected', state);
}
});
row.set('_selected', state);
if (!this._lastSelectedRowComponent.get('isDestroyed') && !this._lastSelectedRowComponent.get('isDestroying')) {
this._lastSelectedRowComponent.set('_selected', state);
}
} else {
row.set('_selected', !row._selected);
}
} else if (mode === 'MULTI') {
if (!event) {
this.clearSelection();
row.set('_selected', true);
} else if (event.ctrlKey) {
row.set('_selected', !row._selected);
} else if (isHoldingShift && this._lastSelectedRowComponent) {
let inside = false;
this._rowComponents.forEach((rc) => {
if (rc === row || rc === this._lastSelectedRowComponent) {
inside = !inside;
}
if (inside) {
rc.set('_selected', true);
}
});
row.set('_selected', true);
if (!this._lastSelectedRowComponent.get('isDestroyed') && !this._lastSelectedRowComponent.get('isDestroying')) {
this._lastSelectedRowComponent.set('_selected', true);
}
} else {
const sel = row._selected;
this.clearSelection();
row.set('_selected', !sel);
}
}
this.set('_lastSelectedRowComponent', row);
this.set('_selectionIndex', this._selectionIndex + 1);
this._fireSelectEvent();
},
/**
* Action for handling incoming requests to clear all selected rows.
*/
clearSelection: action(function () {
const selectedCountBefore = this._selectedRowCount;
this._rowComponents.forEach((rc) => rc.set('_selected', false));
this.set('_selectionIndex', this._selectionIndex + 1);
this.set('_lastSelectedRowComponent', undefined);
if (selectedCountBefore !== 0) {
this._fireSelectEvent();
}
}),
/**
* @private
* Action for handling incoming requests to mark all rows as selected.
*/
selectAllHandler: action(function () {
if (this.isSelectModeMulti || this.isSelectModeToggle) {
const totalSelectedBefore = this._selectedRowCount;
if (this.isAllSelected) this.clearSelection();
else {
this._rowComponents.forEach((rc) => rc.set('_selected', true));
this.set('_selectionIndex', this._selectionIndex + 1);
}
// only fire the event if the total selected count changed
if (totalSelectedBefore !== this._selectedRowCount) {
this._fireSelectEvent();
}
}
}),
async exportRowsAndColumns(rows = [], columns = []) {
const type = this._exportFlavorChoice;
const fields = columns.map((column) => column.field);
const headers = columns.map((column) => column.label || column.field);
const state = this.tableState;
const needToFetchAllRows = this._exportRowsChoice === 'ALL';
const agreed = await this.exportService.confirmAgreement();
if (agreed) {
try {
if (this.exporter && this.exporter instanceof Function) {
await this.exporter(type, rows, columns, fields, headers, state, needToFetchAllRows);
} else {
const filename = 'whitebox-data-export.' + type.toLowerCase();
await this.exportService.export(type, filename, fields, rows, headers, true);
}
this._fireExportedEvent();
} catch (ex) {
this.remoteLogging.log('Error while exporting.', ex, 'error');
}
}
},
// exporting
/**
* Action for handling incoming requests to initiate a table export.
*/
export: action(async function () {
this.set('_inExporting', true);
let rows =
(this._internalRows && this._internalRows.toArray && [].concat(this._internalRows.toArray())) ||
(this._internalRows && [].concat(this._internalRows)) ||
[];
if (this.hierarchyField !== null) rows = this._hierarchyRows;
if (this._exportRowsChoice === 'CURRENT') rows = this.showingRows;
if (this._exportRowsChoice === 'SELECTED') rows = this.selectedRows;
let columns = this._columnArray;
if (this._exportColumnsChoice === 'CURRENT') {
columns = this._columnArray.filter((column) => column.enabled && (column.showing || column.required));
}
await this.exportRowsAndColumns(rows, columns);
this.set('_inExporting', false);
}),
// subrows
/**
* @private
* Computed property that returns true if subrows is enabled.
*/
_isSubrowsAllowed: computed('subrows', function () {
const mode = EmberUtils.validateComponentArgumentStringOptions(this.subrows, 'SINGLE,MANY,NONE', 'NONE');
return mode !== 'NONE';
}),
/**
* @private
* Computed property that returns true if subrows are provided in the WbDataTable children.
*/
_hasSubrows: computed('_isSubrowsAllowed', '_subrowComponents.@each._rowComponent', function () {
return (this._isSubrowsAllowed && this._subrowComponents.some((subrow) => !!subrow._rowComponent)) || false;
}),
/**
* @private
* Computed property that returns true if subrows are in use.
*/
_isSubrowsActive: computed('_hasSubrows', '_isSubrowsAllowed', function () {
return this._isSubrowsAllowed && this._hasSubrows;
}),
/**
* @private
* Computed property that returns true if all subrows are opened.
*/
_allSubrowsOpen: computed('_isSubrowsAllowed', '_subrowComponents.@each.opened', function () {
if (!this._isSubrowsAllowed) return false;
if (this._subrowComponents.length < 1) return false;
return this._subrowComponents.every((subrow) => subrow.opened);
}),
/**
* @private
* Computed property that returns true if all subrows are closed.
*/
_allSubrowsClosed: computed('_isSubrowsAllowed', '_subrowComponents.@each.opened', function () {
if (!this._isSubrowsAllowed) return false;
if (this._subrowComponents.length < 1) return false;
return this._subrowComponents.every((subrow) => !subrow.opened);
}),
/**
* @private
* Computed property that returns true if some subrows are opened.
*/
_someSubrowsOpened: computed('_isSubrowsAllowed', '_subrowComponents.@each.opened', function () {
if (!this._isSubrowsAllowed) return false;
return this._subrowComponents.some((subrow) => subrow.opened);
}),
/**
* Returns the subrow component for a given row.
*
* @param {*} row
* @returns {wb-data-table-subrow}
*/
getSubrowForRow(row) {
if (!row) return null;
const rowComponent = this._getRowComponent(row);
if (!rowComponent) return null;
return rowComponent._subrow;
},
/**
* Toggle a subrow open or closed. If state is null, this will toggle the subrow.
* If state is true, this will open the subrow. If state is false, it will close the subrow.
*
* @param {*} row
* @param {null|Boolean} state
*/
toggleSubrow(row, state = null) {
if (!this._isSubrowsAllowed) return;
const subrow = this.getSubrowForRow(row);
if (!subrow) return false;
const mode = EmberUtils.validateComponentArgumentStringOptions(this.subrows, 'SINGLE,MANY,NONE', 'NONE');
if (mode === 'NONE') return;
if (subrow.required) return;
if (!subrow.enabled) return;
subrow.set('showing', state === null ? !subrow.showing : state);
if (mode === 'SINGLE' && subrow.showing) {
this._subrowComponents.forEach((sr) => {
if (!sr.enabled) return;
if (sr.required) return;
sr !== subrow && sr.set('showing', false);
});
}
},
/**
* Action for handling incoming requests to toggle open all subrows. Note that if subrows mode
* is "single" this will still open ALL subrows.
*/
toggleAllSubrowsOpen: action(function () {
if (!this._isSubrowsAllowed) return;
this._subrowComponents.forEach((subrow) => {
if (!subrow.enabled) return;
if (subrow.required) return;
subrow.set('showing', true);
});
}),
/**
* Action for handling incoming requests to toggle all subrows closed.
*/
toggleAllSubrowsClosed: action(function () {
if (!this._isSubrowsAllowed) return;
this._subrowComponents.forEach((subrow) => {
if (!subrow.enabled) return;
if (subrow.required) return;
subrow.set('showing', false);
});
}),
/**
* @private
* Action for handling incoming requests to open/close a subrow.
*/
_subrowToggleHandler: action(function (row, event) {
if (!row) return;
this.toggleSubrow(row);
event.stopPropagation();
}),
/**
* @private
*
* Action for handling incoming requests to handle click events on the subrow column heading.
*/
subrowAllHandler: action(function () {
const mode = EmberUtils.validateComponentArgumentStringOptions(this.subrows, 'SINGLE,MANY,NONE', 'NONE');
if (mode === 'NONE') return;
if (this._someSubrowsOpened) this.toggleAllSubrowsClosed();
}),
// fetcher
fetchRows: task(function* (event) {
if (!this.fetcher) return;
if (!(this.fetcher instanceof Function)) {
this.remoteLogging.log('fetchRows', 'Fetcher for table is provided but not a function. Not executing.', 'warn');
return;
}
this.set('_inFetching', true);
yield timeout(250);
try {
const state = this.tableState;
const columns = this._columnArray;
const rows = this._internalRows;
// fetcher takes in the table state, current columns, and current rows.
// it expects an object in return that has the following fields: rows, count, start, end.
const fetched = yield this.fetcher(state, columns, rows);
this.set('rows', (fetched && fetched.rows) || []);
this.set('rowCount', (fetched && fetched.count) || 0);
this.set('rowStartPosition', (fetched && fetched.start) || 0);
this.set('rowEndPosition', (fetched && fetched.end) || 0);
} catch (ex) {
this.remoteLogging.log('Fetcher execution triggered an exception:', ex, 'error');
this.set('rows', []);
}
once(this, this._recalculateColumnWidths);
this.set('_inFetching', false);
}).restartable(),
// ember lifecylce stuff
/**
* @private
*
* Ember Lifecycle init event handler.
*/
init() {
this._super(...arguments);
if (this.name === null || this.name === undefined) {
this.remoteLogging.log(
'wb-data-table init',
"A WbDataTable name is not required, but if provided and unique will allow table settings (columns showing, sorting, filtering) to be saved in the browser's localStorage for reuse.",
'warn'
);
}
// super important these are initialized here. If you dont do this, nested tables will not work.
this.set('_columns', new Set());
this.set('_cells', new Set());
this.set('_subrowComponents', []);
this.set('_rowComponents', []);
this.set('_columnArray', []);
this.set('_hierarchyInfo', new Map());
this._fireInitEvent();
},
/**
* @private
*
* Ember Lifecycle didInsertElement event handler.
*/
didInsertElement() {
this._super(...arguments);
if (this.name !== null && this.name !== undefined) {
this.addObserver('tableState', this._saveTableState);
setTimeout(this._restoreTableState, 0); // needs to happen later or ember freaks out.
}
this.addObserver('isReady', this._readyObserver);
if (this.isReady) this._readyObserver();
this.addObserver('rows', this._rowsObserver);
if (this.rows) this._rowsObserver();
this.addObserver('width', this._repaintRequiredObserver);
this.addObserver('selectMode', this._repaintRequiredObserver);
this.addObserver('subrows', this._repaintRequiredObserver);
this.addObserver('hierarchyField', this._repaintRequiredObserver);
this.addObserver('_hierarchyMaxDepth', this._repaintRequiredObserver);
this.addObserver('_columnArray', this._repaintRequiredObserver);
// double next is intentional as bodyElement get instantiated a bit late.
next(() => {
next(() => {
this.bodyElement.addEventListener('scroll', this._scrollHandler);
});
});
if (this.fetcher && this.rows === null) this.fetchRows.perform(); // asdffasd
},
/**
* @private
*
* Ember Lifecycle willDestroyElement event handler.
*/
willDestroyElement() {
this._super(...arguments);
if (this.name !== null && this.name !== undefined) {
this.removeObserver('tableState', this._saveTableState);
}
this.removeObserver('isReady', this._readyObserver);
this.removeObserver('rows', this._rowsObserver);
this.removeObserver('width', this._repaintRequiredObserver);
this.removeObserver('selectMode', this._repaintRequiredObserver);
this.removeObserver('subrows', this._repaintRequiredObserver);
this.removeObserver('hierarchyField', this._repaintRequiredObserver);
this.removeObserver('_hierarchyMaxDepth', this._repaintRequiredObserver);
this.removeObserver('_columnArray', this._repaintRequiredObserver);
if (this.bodyElement) this.bodyElement.removeEventListener('scroll', this._scrollHandler);
},
// fire events
/**
* @private
*
* Internal function for firing Init event.
*/
_fireInitEvent() {
if (this.onInit && this.onInit instanceof Function) {
try {
this.onInit(this, this.getAPI());
} catch (ex) {
this.remoteLogging.log('Error executing @onInit function.', ex, 'error');
}
}
},
/**
* @private
*
* Internal function for firing Ready event.
*/
_fireReadyEvent() {
if (this.onReady && this.onReady instanceof Function) {
try {
this.onReady(this, this.getAPI());
} catch (ex) {
this.remoteLogging.log('Error executing @onReady function.', ex, 'error');
}
}
},
/**
* @private
*
* Internal function for firing Select event.
*/
_fireSelectEvent() {
if (this.onSelect && this.onSelect instanceof Function) {
try {
this.onSelect(this, this.selectedRows);
} catch (ex) {
this.remoteLogging.log('Error executing @onSelect function.', ex, 'error');
}
}
},
/**
* @private
*
* Internal function for firing Exported event.
*/
_fireExportedEvent() {
if (this.onExported && this.onExported instanceof Function) {
try {
this.onExported(this);
} catch (ex) {
this.remoteLogging.log('Error executing @onExported function.', ex, 'error');
}
}
},
/**
* @private
*
* Internal function for firing Fetched event.
*/
_fireFetchedEvent() {
if (this.onFetched && this.onFetched instanceof Function) {
try {
this.onFetched(this);
} catch (ex) {
this.remoteLogging.log('Error executing @onFetched function.', ex, 'error');
}
}
},
/**
* @private
*
* Internal function for firing ColumnAdd event.
*/
_fireColumnAddEvent(column) {
if (this.onColumnAdd && this.onColumnAdd instanceof Function) {
next(() => {
try {
this.onColumnAdd(this, column);
} catch (ex) {
this.remoteLogging.log('Error executing @onColumnAdd function.', ex, 'error');
}
});
}
},
/**
* @private
*
* Internal function for firing ColumnRemove event.
*/
_fireColumnRemoveEvent(column) {
if (this.onColumnRemove && this.onColumnRemove instanceof Function) {
next(() => {
try {
this.onColumnRemove(this, column);
} catch (ex) {
this.remoteLogging.log('Error executing @onColumnRemove function.', ex, 'error');
}
});
}
},
/**
* @private
*
* Internal function for firing RowAdd event.
*/
_fireRowAddEvent(rowComponent, row) {
if (this.onRowAdd && this.onRowAdd instanceof Function) {
next(() => {
try {
this.onRowAdd(this, rowComponent, row);
} catch (ex) {
this.remoteLogging.log('Error executing @onRowAdd function.', ex, 'error');
}
});
}
},
/**
* @private
*
* Internal function for firing RowRemove event.
*/
_fireRowRemoveEvent(rowComponent, row) {
if (this.onRowRemove && this.onRowRemove instanceof Function) {
next(() => {
try {
this.onRowRemove(this, rowComponent, row);
} catch (ex) {
this.remoteLogging.log('Error executing @onRowRemove function.', ex, 'error');
}
});
}
},
/**
* @private
*
* Internal function for firing SubrowAdd event.
*/
_fireSubrowAddEvent(subrowComponent, row) {
if (this.onSubrowAdd && this.onSubrowAdd instanceof Function) {
next(() => {
try {
this.onSubrowAdd(this, subrowComponent, row);
} catch (ex) {
this.remoteLogging.log('Error executing @onSubrowAdd function.', ex, 'error');
}
});
}
},
/**
* @private
*
* Internal function for firing SubrowRemove event.
*/
_fireSubrowRemoveEvent(subrowComponent, row) {
if (this.onSubrowRemove && this.onSubrowRemove instanceof Function) {
next(() => {
try {
this.onSubrowRemove(this, subrowComponent, row);
} catch (ex) {
this.remoteLogging.log('Error executing @onSubrowRemove function.', ex, 'error');
}
});
}
},
/**
* @private
*
* Internal function for firing CellAdd event.
*/
_fireCellAddEvent(cell, column, row) {
if (this.onCellAdd && this.onCellAdd instanceof Function) {
next(() => {
try {
this.onCellAdd(this, cell, column, row);
} catch (ex) {
this.remoteLogging.log('Error executing @onCellAdd function.', ex, 'error');
}
});
}
},
/**
* @private
*
* Internal function for firing CellRemove event.
*/
_fireCellRemoveEvent(cell, column, row) {
if (this.onCellRemove && this.onCellRemove instanceof Function) {
next(() => {
try {
this.onCellRemove(this, cell, column, row);
} catch (ex) {
this.remoteLogging.log('Error executing @onCellRemove function.', ex, 'error');
}
});
}
},
// child component tracking
/**
* @private
* Internal callback for any time a column is added to the table.
*/
_columnAddedHandler: action(function (column) {
if (!column) return;
this._columns.add(column);
once(this, this._updateColumnArray);
if (column.enabled === undefined || column.enabled === null) {
column.set('enabled', true);
}
if (column.showing === undefined || column.showing === null) {
column.set('showing', column.enabled);
}
column.set('_defaultShowing', column.showing);
column.set('_table', this);
once(this, this._recalculateColumnWidths);
this._fireColumnAddEvent(column);
}),
/**
* @private
* Internal callback for any time a column is removed from the table.
*/
_columnRemovedHandler: action(function (column) {
column.set('_table', null);
this._columns.delete(column);
once(this, this._updateColumnArray);
this._fireColumnRemoveEvent(column);
}),
/**
* @private
* Internal callback for any time a row is added to the table.
*/
_rowAddedHandler: action(function (rowComponent) {
this._rowComponents.pushObject(rowComponent);
rowComponent.set('_table', this);
this._fireRowAddEvent(rowComponent, rowComponent.row);
}),
/**
* @private
* Internal callback for any time a row is removed from the table.
*/
_rowRemovedHandler: action(function (rowComponent) {
this._rowComponents.removeObject(rowComponent);
rowComponent.set('_table', null);
if (rowComponent.row) this._hierarchyInfo.delete(rowComponent.row);
this._fireRowRemoveEvent(rowComponent, rowComponent.row);
}),
/**
* @private
* Internal callback for any time a subrow is added to the table.
*/
_subrowAddedHandler: action(function (row, subrowComponent) {
this._subrowComponents.pushObject(subrowComponent);
const rowComponent = this._getRowComponent(row);
rowComponent.set('_subrow', subrowComponent);
subrowComponent.set('_table', this);
subrowComponent.set('_rowComponent', rowComponent);
this._fireSubrowAddEvent(subrowComponent, row);
}),
/**
* @private
* Internal callback for any time a subrow is removed from the table.
*/
_subrowRemovedHandler: action(function (parentRow, subrowComponent) {
this._subrowComponents.removeObject(subrowComponent);
const rowComponent = subrowComponent._rowComponent;
if (rowComponent) rowComponent.set('_subrow', null);
subrowComponent.set('_table', null);
subrowComponent.set('_rowComponent', null);
this._fireSubrowRemoveEvent(subrowComponent, subrowComponent.row);
}),
/**
* @private
* Internal callback for any time a cell is added to the table.
*/
_cellAddedHandler: action(function (cell) {
this._cells.add(cell);
cell.set('_table', this);
if (typeof cell.column === 'number') {
cell.set('column', this._columnArray[cell.column]);
}
this._fireCellAddEvent(cell, cell.column, cell.row);
}),
/**
* @private
* Internal callback for any time a cell is removed from the table.
*/
_cellRemovedHandler: action(function (cell) {
this._cells.delete(cell);
cell.set('_table', null);
this._fireCellAddEvent(cell, cell.column, cell.row);
}),
});
// this computes the width of a scrollbar for us and stores it for usage by CSS.
// This is a complete hack, but there is no other way to do this.
// do not move or delete. Very important.
(() => {
const e = document.createElement('div');
e.style.overflowX = 'scroll';
e.style.overflowY = 'scroll';
e.style.position = 'absolute';
e.style.left = '-100000px';
document.body.appendChild(e);
document.documentElement.style.setProperty('--scrollbar-width', e.offsetWidth - e.clientWidth + 'px');
e.remove();
})();
import Controller from '@ember/controller';
export default class ApplicationController extends Controller {
appName = 'Ember Twiddle';
products = [
{},
{}
];
}
&.wb-data-table {
height: 100%;
display: grid;
grid-template-columns: minmax(0px,100%);
grid-template-rows: min-content minmax(0px,100%) min-content;
overflow: hidden;
& > .wb-data-table-header {
display: flex;
justify-content: space-between;
& > .wb-data-table-header-label {
display: grid;
grid-template-columns: auto;
grid-auto-flow: row;
grid-auto-rows: 1fr;
margin-bottom: 5px;
align-items: flex-end;
& > .wb-data-table-header-label-details {
font-size: smaller;
font-style: italic;
opacity: 0.8;
}
}
& > .wb-data-table-header-actions {
display: flex;
justify-content: flex-end;
align-items: flex-end;
margin-bottom: 0px;
& > .wb-data-table-header-search {
display: grid;
grid-template-columns: 150px 20px;
grid-template-rows: 20px;
grid-column-gap: 2px;
box-shadow: 0 1px 0 0 rgba(22, 29, 28, 0.05);
background:#EDF2F5;
border-radius: 5px 5px 0px 0px;
border: none;
padding: 3px 5px;
& > .wb-input-button {
width: auto;
height: auto;
}
& > .wb-input-text {
height: auto;
margin-bottom: 0px;
display: flex;
& > input {
margin: 1px 0px;
height: auto;
font-size: 9px;
background: white;
}
}
}
& > .wb-data-table-header-search + .wb-button-bar {
margin-left: 5px;
}
& > .wb-button-bar {
border-radius: 5px 5px 0px 0px;
border: none;
padding: 3px 5px;
& > .wb-input-button,
& > .wb-input-button-menu,
& > .wb-data-table-action,
& > .wb-data-table-action-menu {
width: auto;
height: auto;
}
& > .wb-data-table-custom-action:not(.wb-data-table-custom-action-hidden) {
margin-left: 10px;
& + .wb-data-table-custom-action {
margin-left: unset;
}
& ~ .wb-data-table-custom-action {
margin-left: unset;
}
}
& > .wb-data-table-custom-action-hidden {
width: 0px;
overflow: hidden;
}
}
}
}
& > .wb-data-table-inner {
position: relative;
display: grid;
grid-template-columns: 100%;
grid-template-rows: min-content minmax(0px, auto) min-content;
background: #EDF2F5;
padding: 2px;
& > .wb-data-table-headings,
& > .wb-data-table-footings {
width: calc(100% - var(--scrollbar-width));
display: grid;
grid-template-rows: minmax(0px,100%);
grid-auto-flow: column;
grid-auto-columns: 1fr;
overflow-x: hidden;
overflow-y: hidden;
& > .wb-data-table-column {
height: auto;
display: grid;
grid-template-rows: min-content;
grid-template-columns: minmax(0px,100%) min-content min-content;
padding: 3px;
border-bottom: 2px solid transparent;
border-left: 1px solid #DDDDDD;
border-right: 1px solid #DDDDDD;
background: #EDF2F5;
user-select: none;
align-content: center;
&.wb-data-table-column-hidden {
display: none;
}
& > .wb-data-table-column-body {
width: 100%;
display: flex;
justify-content: flex-start;
white-space: nowrap;
color: var(--primaryBlue);
text-transform: uppercase;
text-overflow: ellipsis;
overflow: hidden;
}
&.wb-data-table-column-align-right {
grid-template-columns: min-content min-content minmax(0px,100%) ;
& > .wb-data-table-column-body {
justify-content: flex-end;
}
}
&.wb-data-table-column-align-center > .wb-data-table-column-body {
justify-content: center;
}
&.wb-data-table-column-valign-center {
align-content: center;
}
&.wb-data-table-column-valign-bottom {
align-content: flex-end;
}
&.wb-data-table-column-filter-dialog {
border: 1px solid #00000044;
border-bottom: none;
background: #848da5;
color: white;
& > .wb-data-table-column-body {
color: white;
}
& > .wb-data-table-column-filter {
opacity: 0.5;
}
}
& > .wb-data-table-column-filter {
width: 17px;
display: grid;
grid-template-rows: minmax(0px,1005);
grid-template-columns: min-content;
align-items: center;
font-size: 1em;
margin-left: 5px;
opacity: 0;
transition: opacity 0.5s;
}
& > .wb-data-table-column-sort {
width: 10px;
font-size: 20px;
position: relative;
margin-left: 5px;
cursor: pointer;
& > .wb-data-table-column-sort-up {
position: absolute;
font-size: 18px;
top: calc(100% / 2 - 12px);
right: 0px;
opacity: 0.5;
transition: opacity 0.5s;
}
& > .wb-data-table-column-sort-down {
position: absolute;
font-size: 18px;
bottom: calc(100% / 2 - 12px);
right: 0px;
opacity: 0.5;
transition: opacity 0.5s;
}
}
&.wb-data-table-column-sorted-none > .wb-data-table-column-sort > .wb-data-table-column-sort-down,
&.wb-data-table-column-sorted-none > .wb-data-table-column-sort > .wb-data-table-column-sort-up {
opacity: 0.5;
transition: opacity 0.5s;
}
&.wb-data-table-column-sorted-up > .wb-data-table-column-sort > .wb-data-table-column-sort-up {
opacity: 1;
}
&.wb-data-table-column-sorted-down > .wb-data-table-column-sort > .wb-data-table-column-sort-down {
opacity: 1;
}
&.wb-data-table-column-filtered > .wb-data-table-column-filter {
opacity: 1;
transition: opacity 0.5s;
}
&.wb-data-table-headings-hierarchy-toggle {
padding-left: 8px;
padding-right: 8px;
color: var(--primaryBlue);
}
&:hover {
color: var(--primaryBlue);
transition: color 0.5s;
& > .wb-data-table-column-filter {
opacity: 1;
transition: opacity 0.5s;
}
}
}
}
& > .wb-data-table-headings > .wb-data-table-headings-select-all {
cursor: pointer;
transition: color 0.5s;
color: var(--primaryBlue);
&:hover {
transition: color 0.5s;
color: var(--primaryBlue);
}
}
& > .wb-data-table-headings > .wb-data-table-headings-subrow-toggle {
cursor: pointer;
transition: color 0.5s;
color: var(--primaryBlue);
font-size: 18px;
&:hover {
transition: color 0.5s;
color: var(--primaryBlue);
}
}
& > .wb-data-table-body {
position: relative;
overflow-x: auto;
overflow-y: scroll;
& > .wb-data-table-body-messages {
position: sticky;
left: 0px;
right: 0px;
top: 0px;
bottom: 0px;
width: 100%;
height: 100%;
z-index: 1;
display: grid;
background: white;
grid-template-rows: minmax(0px,100%);
grid-template-columns: min-content min-content;
grid-column-gap: 10px;
justify-content: center;
align-items: center;
font-size: 3em;
font-weight: bold;
opacity: 0.9;
letter-spacing: -0.05em;
white-space: nowrap;
& > * {
animation: 1s linear 0s 1 wb-data-table-body-messages-animation;
}
}
& > .wb-data-table-body-messages ~ .wb-data-table-body-rows {
height: 1px;
}
& > .wb-data-table-body-rows {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: minmax(0px,100%);
grid-auto-flow: rows;
grid-auto-rows: min-content;
& > div:nth-of-type(odd) {
background: #FFFFFF;
& > .wb-data-table-cell {
background: #FFFFFF;
}
& + .wb-data-table-subrow {
background: #FFFFFF;
}
}
& > div:nth-of-type(even) {
background: #F5FBFF;
& > .wb-data-table-cell {
background: #F5FBFF;
}
& + .wb-data-table-subrow {
background: #F5FBFF;
}
}
& > .wb-data-table-row {
display: grid;
grid-template-rows: 1fr;
grid-auto-flow: column;
grid-auto-columns: 1fr;
& > .wb-data-table-cell {
display: flex;
padding: 3px;
border-left: 1px solid #DDDDDD;
border-right: 1px solid #DDDDDD;
justify-content: flex-start;
align-items: flex-start;
text-overflow: ellipsis;
overflow: hidden;
&.wb-data-table-cell-hidden {
display: none;
}
&.wb-data-table-cell-align-right {
justify-content: flex-end;
}
&.wb-data-table-cell-align-center {
justify-content: center;
}
&.wb-data-table-cell-valign-center {
align-items: center;
}
&.wb-data-table-cell-valign-bottom {
align-items: flex-end;
}
&.wb-data-table-cell-editable {
user-select: none;
position: relative;
border-bottom: 2px dashed #CCC;
cursor: pointer;
&:hover {
border-bottom: 2px solid #266E9E;
background: var(--barelyBlue);
}
}
&.wb-data-table-subrow-toggle-cell {
font-size: 18px;
}
&.wb-data-table-hierarchy-cell {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 12px;
grid-template-rows: auto;
justify-content: flex-end;
padding-right: 10px;
padding-top: 0px;
padding-bottom: 0px;
& > .wb-data-table-hierarchy-cell-step {
display: grid;
grid-template-rows: 100%;
grid-template-columns: 100%;
width: 12px;
height: 100%;
justify-content: center;
align-items: center;
overflow-x: hidden;
overflow-y: hidden;
&.wb-data-table-hierarchy-cell-step-to {
position: relative;
&::before {
content: " ";
position: absolute;
top: calc(50% - 1px);
bottom: calc(50% - 1px);
left: 0px;
right: 0px;
background: #AAAAAA;
}
}
&.wb-data-table-hierarchy-cell-step-thru {
position: relative;
&::after {
content: " ";
position: absolute;
left: calc(50% - 1px);
right: calc(50% - 1px);
width: 2px;
top: 0px;
bottom: 0px;
background: #AAAAAA;
}
}
&.wb-data-table-hierarchy-cell-step-parent-opened {
position: relative;
& > svg {
color: var(--primaryBlue);
&:hover {
color: #468EBE;
}
}
&::before {
content: " ";
position: absolute;
left: calc(50% - 1px);
right: calc(50% - 1px);
width: 2px;
top: calc(50% + .4em);
bottom: -2px;
background: #AAAAAA;
}
}
&.wb-data-table-hierarchy-cell-step-parent-closed {
position: relative;
& > svg {
color: var(--primaryBlue);
&:hover {
color: #468EBE;
}
}
}
&.wb-data-table-hierarchy-cell-step-sibling {
position: relative;
&::before {
content: " ";
position: absolute;
left: calc(50% - 1px);
right: calc(50% - 1px);
width: 2px;
top: 0px;
bottom: 0px;
background: #AAAAAA;
}
&::after {
content: " ";
position: absolute;
top: calc(50% - 1px);
bottom: calc(50% - 1px);
height: 2px;
left: 50%;
right: 0px;
background: #AAAAAA;
}
}
&.wb-data-table-hierarchy-cell-step-sibling-last {
position: relative;
&::before {
content: " ";
position: absolute;
left: calc(50% - 1px);
right: calc(50% - 1px);
width: 2px;
top: 0px;
bottom: 50%;
background: #AAAAAA;
}
&::after {
content: " ";
position: absolute;
top: calc(50% - 1px);
bottom: calc(50% - 1px);
height: 2px;
left: 50%;
right: 0px;
background: #AAAAAA;
}
}
}
}
& > .wb-input-button {
width: 25px;
height: 25px;
overflow: hidden;
& > button {
display: grid;
align-items: center;
justify-content: center;
width: 23px;
height: 23px;
overflow: hidden;
padding: 0px;
background: var(--primaryBlue);
color: white;
font-size: 1.25em;
border: none;
box-shadow: none;
cursor: pointer;
line-height: none;
outline: none;
& > .wb-input-button-icon {
width: 17px;
height: 17px;
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
&:hover:not(:disabled) {
background: #468EBE;
}
&:active:not(:disabled) {
background: #064E7E;
}
}
}
}
&.wb-data-table-row-has-subrow.wb-data-table-row-subrow-opened {
border: 1px solid #AAA;
& + .wb-data-table-subrow {
position: relative;
border: 1px solid #AAA;
border-top-width: 0px;
}
}
& > .wb-data-table-subrow-toggle-cell > .wb-data-table-subrow-toggle-cell-opened {
display: none;
}
& > .wb-data-table-subrow-toggle-cell > .wb-data-table-subrow-toggle-cell-closed {
display: block;
}
&.wb-data-table-row-subrow-opened {
& > .wb-data-table-subrow-toggle-cell > .wb-data-table-subrow-toggle-cell-opened {
display: block;
}
& > .wb-data-table-subrow-toggle-cell > .wb-data-table-subrow-toggle-cell-closed {
display: none;
}
}
}
& > .wb-data-table-subrow {
display: block;
padding: none;
height: 0;
overflow: hidden;
&.wb-data-table-subrow-opened {
margin-left: 30px;
padding: 3px 5px 8px 5px;
height: unset;
}
}
}
}
}
& > .wb-data-table-footer {
display: flex;
justify-content: flex-start;
& > .wb-data-table-footer-actions {
display: flex;
justify-content: flex-start;
align-items: flex-start;
margin-top: 0px;
& > .wb-button-bar {
border-radius: 0px 0px 5px 5px;
border: none;
padding: 3px 5px;
& > .wb-input-button,
& > .wb-input-button-menu,
& > .wb-data-table-action,
& > .wb-data-table-action-menu {
width: auto;
height: auto;
}
& > .wb-data-table-custom-action:not(.wb-data-table-custom-action-hidden) {
margin-left: 10px;
& + .wb-data-table-custom-action {
margin-left: unset;
}
& ~ .wb-data-table-custom-action {
margin-left: unset;
}
}
& > .wb-data-table-custom-action-hidden {
width: 0px;
overflow: hidden;
}
}
}
}
&.wb-data-table-hover-column > .wb-data-table-inner > .wb-data-table-headings > .wb-data-table-column.wb-data-table-column-hover {
/* filter: invert(5%); */
/* background: #ccc !important; */
}
&.wb-data-table-hover-column > .wb-data-table-inner > .wb-data-table-body > .wb-data-table-body-rows > .wb-data-table-row > .wb-data-table-cell.wb-data-table-cell-column-hover {
/* filter: invert(10%); */
/* background: #ccc !important; */
}
&.wb-data-table-hover-row > .wb-data-table-inner > .wb-data-table-body > .wb-data-table-body-rows > .wb-data-table-row:hover {
/* filter: invert(15%); */
& .wb-data-table-cell {
background: #ccc;
}
}
&.wb-data-table-headings-none > .wb-data-table-inner > .wb-data-table-headings > .wb-data-table-column {
height: 0px;
padding-top: 0px;
padding-bottom: 0px;
overflow: hidden;
& > .wb-data-table-column-body {
color: transparent;
}
}
&.wb-data-table-headings-bar > .wb-data-table-inner > .wb-data-table-headings > .wb-data-table-column {
height: 2px;
padding-top: 0px;
padding-bottom: 0px;
overflow: hidden;
& > .wb-data-table-column-body {
color: transparent;
}
}
&.wb-data-table-footings-none > .wb-data-table-inner > .wb-data-table-footings > .wb-data-table-column {
height: 0px;
padding-top: 0px;
padding-bottom: 0px;
overflow: hidden;
& > .wb-data-table-column-body {
color: transparent;
}
}
&.wb-data-table-footings-bar > .wb-data-table-inner > .wb-data-table-footings > .wb-data-table-column {
height: 5px;
padding-top: 0px;
padding-bottom: 0px;
overflow: hidden;
& > .wb-data-table-column-body {
color: transparent;
}
}
&.wb-data-table-selectable > .wb-data-table-inner > .wb-data-table-body > .wb-data-table-body-rows > .wb-data-table-subrow {
margin-left: 60px;
}
&.wb-data-table-selectable > .wb-data-table-inner > .wb-data-table-body > .wb-data-table-body-rows > .wb-data-table-row {
cursor: pointer;
}
&:not(.wb-data-table-selectable) > .wb-data-table-inner > .wb-data-table-body > .wb-data-table-body-rows > .wb-data-table-row.wb-data-table-row-has-subrow {
cursor: pointer;
}
&.wb-data-table-selectable > .wb-data-table-inner > .wb-data-table-body > .wb-data-table-body-rows > .wb-data-table-row > .wb-data-table-selection-cell {
position: relative;
min-height: 18px;
& > .wb-data-table-selection-selected {
color: #44CC00;
display: none;
}
& > .wb-data-table-selection-unselected {
display: unset;
}
}
&.wb-data-table-selectable > .wb-data-table-inner > .wb-data-table-body > .wb-data-table-body-rows > .wb-data-table-row.wb-data-table-row-selected > .wb-data-table-selection-cell {
& > .wb-data-table-selection-selected {
display: unset;
}
& > .wb-data-table-selection-unselected {
display: none;
}
}
&.wb-data-table-selectable > .wb-data-table-inner > .wb-data-table-body > .wb-data-table-body-rows {
& > div:nth-of-type(odd) {
&.wb-data-table-row.wb-data-table-row-selected {
background: #FFFFD0;
& > .wb-data-table-cell {
background: #FFFFD0;
}
}
}
& > div:nth-of-type(even) {
&.wb-data-table-row.wb-data-table-row-selected {
background: #FFFFB0;
& > .wb-data-table-cell {
background: #FFFFB0;
}
}
}
}
&.wb-data-table-selectable > .wb-data-table-inner > .wb-data-table-body > .wb-data-table-body-rows > .wb-data-table-row:hover {
background: #D0FFFF;
& > .wb-data-table-cell {
background: #D0FFFF;
}
}
&.wb-data-table > .wb-data-table-inner > .wb-data-table-body > .wb-data-table-body-rows > .wb-data-table-row {
&.wb-data-table-row-highlight-red > .wb-data-table-cell,
&.wb-data-table-row-highlight-red1 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-red,
& > .wb-data-table-cell.wb-data-table-cell-highlight-red1 {
background: #FF000018;
}
&.wb-data-table-row-highlight-red2 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-red2 {
background: #FF000020;
}
&.wb-data-table-row-highlight-red3 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-red3 {
background: #FF000028;
}
&.wb-data-table-row-highlight-red4 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-red4 {
background: #FF000030;
}
&.wb-data-table-row-highlight-red5 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-red5 {
background: #FF000038;
}
&.wb-data-table-row-highlight-green > .wb-data-table-cell,
&.wb-data-table-row-highlight-green1 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-green,
& > .wb-data-table-cell.wb-data-table-cell-highlight-green1 {
background: #00FF0018;
}
&.wb-data-table-row-highlight-green2 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-green2 {
background: #00FF0020;
}
&.wb-data-table-row-highlight-green3 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-green3 {
background: #00FF0028;
}
&.wb-data-table-row-highlight-green4 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-green4 {
background: #00FF0030;
}
&.wb-data-table-row-highlight-green5 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-green5 {
background: #00FF0038;
}
&.wb-data-table-row-highlight-blue > .wb-data-table-cell,
&.wb-data-table-row-highlight-blue1 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-blue,
& > .wb-data-table-cell.wb-data-table-cell-highlight-blue1 {
background: #0000FF18;
}
&.wb-data-table-row-highlight-blue2 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-blue2 {
background: #0000FF20;
}
&.wb-data-table-row-highlight-blue3 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-blue3 {
background: #0000FF28;
}
&.wb-data-table-row-highlight-blue4 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-blue4 {
background: #0000FF30;
}
&.wb-data-table-row-highlight-blue5 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-blue5 {
background: #0000FF38;
}
&.wb-data-table-row-highlight-yellow > .wb-data-table-cell,
&.wb-data-table-row-highlight-yellow1 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-yellow,
& > .wb-data-table-cell.wb-data-table-cell-highlight-yellow1 {
background: #FFFF0018;
}
&.wb-data-table-row-highlight-yellow2 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-yellow2 {
background: #FFFF0020;
}
&.wb-data-table-row-highlight-yellow3 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-yellow3 {
background: #FFFF0028;
}
&.wb-data-table-row-highlight-yellow4 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-yellow4 {
background: #FFFF0030;
}
&.wb-data-table-row-highlight-yellow5 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-yellow5 {
background: #FFFF0038;
}
&.wb-data-table-row-highlight-cyan > .wb-data-table-cell,
&.wb-data-table-row-highlight-cyan1 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-cyan,
& > .wb-data-table-cell.wb-data-table-cell-highlight-cyan1 {
background: #00FFFF18;
}
&.wb-data-table-row-highlight-cyan2 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-cyan2 {
background: #00FFFF20;
}
&.wb-data-table-row-highlight-cyan3 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-cyan3 {
background: #00FFFF28;
}
&.wb-data-table-row-highlight-cyan4 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-cyan4 {
background: #00FFFF30;
}
&.wb-data-table-row-highlight-cyan5 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-cyan5 {
background: #00FFFF38;
}
&.wb-data-table-row-highlight-purple > .wb-data-table-cell,
&.wb-data-table-row-highlight-purple1 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-purple,
& > .wb-data-table-cell.wb-data-table-cell-highlight-purple1 {
background: #FF00FF18;
}
&.wb-data-table-row-highlight-purple2 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-purple2 {
background: #FF00FF20;
}
&.wb-data-table-row-highlight-purple3 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-purple3 {
background: #FF00FF28;
}
&.wb-data-table-row-highlight-purple4 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-purple4 {
background: #FF00FF30;
}
&.wb-data-table-row-highlight-purple5 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-purple5 {
background: #FF00FF38;
}
&.wb-data-table-row-highlight-gray > .wb-data-table-cell,
&.wb-data-table-row-highlight-gray1 > .wb-data-table-cell,
&.wb-data-table-row-highlight-grey > .wb-data-table-cell,
&.wb-data-table-row-highlight-grey1 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-gray,
& > .wb-data-table-cell.wb-data-table-cell-highlight-gray1,
& > .wb-data-table-cell.wb-data-table-cell-highlight-grey,
& > .wb-data-table-cell.wb-data-table-cell-highlight-grey1 {
background: #44444418;
}
&.wb-data-table-row-highlight-gray2 > .wb-data-table-cell,
&.wb-data-table-row-highlight-grey2 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-gray2,
& > .wb-data-table-cell.wb-data-table-cell-highlight-grey2 {
background: #44444420;
}
&.wb-data-table-row-highlight-gray3 > .wb-data-table-cell,
&.wb-data-table-row-highlight-grey3 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-gray3,
& > .wb-data-table-cell.wb-data-table-cell-highlight-grey3 {
background: #44444428;
}
&.wb-data-table-row-highlight-gray4 > .wb-data-table-cell,
&.wb-data-table-row-highlight-grey4 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-gray4,
& > .wb-data-table-cell.wb-data-table-cell-highlight-grey4 {
background: #44444430;
}
&.wb-data-table-row-highlight-gray5 > .wb-data-table-cell,
&.wb-data-table-row-highlight-grey5 > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-gray5,
& > .wb-data-table-cell.wb-data-table-cell-highlight-grey5 {
background: #44444438;
}
&.wb-data-table-row-highlight-invert > .wb-data-table-cell,
& > .wb-data-table-cell.wb-data-table-cell-highlight-invert {
/* filter: invert(100%); */
background: rgb(92, 88, 88);
}
}
}
/*
Spinkit
https://tobiasahlin.com/spinkit/
MIT License
<div class="spinkit-folding-cube">
<div class="spinkit-cube1 spinkit-cube"></div>
<div class="spinkit-cube2 spinkit-cube"></div>
<div class="spinkit-cube4 spinkit-cube"></div>
<div class="spinkit-cube3 spinkit-cube"></div>
</div>
*/
& .spinkit-folding-cube {
margin: 25px auto 20px auto;
width: 40px;
height: 40px;
position: relative;
animation-duration: infinite;
& > .spinkit-cube {
float: left;
width: 50%;
height: 50%;
position: relative;
transform: scale(1.1);
}
& > .spinkit-cube:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--brightBlue);
animation: spinkit-foldCubeAngle 2.4s infinite linear both;
transform-origin: 100% 100%;
}
& > .spinkit-cube2 {
transform: scale(1.1) rotateZ(90deg);
}
& > .spinkit-cube3 {
transform: scale(1.1) rotateZ(180deg);
}
& > .spinkit-cube4 {
transform: scale(1.1) rotateZ(270deg);
}
& > .spinkit-cube2:before {
animation-delay: 0.3s;
}
& > .spinkit-cube3:before {
animation-delay: 0.6s;
}
& > .spinkit-cube4:before {
animation-delay: 0.9s;
}
}
@keyframes spinkit-foldCubeAngle {
0%, 10% {
transform: perspective(140px) rotateX(-180deg);
opacity: 0;
} 25%, 75% {
transform: perspective(140px) rotateX(0deg);
opacity: 1;
} 90%, 100% {
transform: perspective(140px) rotateY(180deg);
opacity: 0;
}
}
@keyframes wb-data-table-body-messages-animation {
0% {
opacity: 0;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
<WbDataTable
@name="whitebox-admin-product-edit-table"
@searchable={{true}}
@hierarchyField={{this.productsTableHierarchyField}}
@label="Products table"
@pageable={{true}}
@pageSize={{this.resultLimit}}
@selectMode="toggle"
@actionsBar="top"
@exportable={{true}}
@exporter={{this.exportProducts}}
@onInit={{this.onProductsTableInit}}
@onSelect={{this.setSelectedProducts}}
@rows={{this.products}}
as |table|
>
{{#each productsTableColumns.all as |column|}}
{{#if (eq column "productBullets")}}
<table.column
@field={{column}}
@label={{get productsTableColumns.headers column}}
@width="3"
@showing={{contains column productsTableColumns.defaultShowing}}
@filterable={{not (contains column productsTableColumns.nonFilterable)}} as |value|
>
<div class="product-bullets-row">
{{#each value as |productBullet|}}
<li>
<WbCopyableValue @value={{productBullet.content}}/>
</li>
{{/each}}
</div>
</table.column>
{{else if (eq column "sku")}}
<table.column
@field={{column}}
@label={{get productsTableColumns.headers column}}
@showing={{contains column productsTableColumns.defaultShowing}}
@filterable={{not (contains column productsTableColumns.nonFilterable)}} as |value|
>
<WbCopyableValue @value={{value}}/>
</table.column>
{{else if (eq column "relationshipIndicator")}}
<table.column
@valign="center"
@field={{column}}
@sortable={{false}}
@filterable={{false}}
@showing={{contains column productsTableColumns.defaultShowing}}
@width="35px"
@align="center" as |value column row|
>
{{#if row.isChildSku}}
<div class="tooltip-container">
<FaIcon
class="sku-relationship-icon"
@icon="copyright" />
<EmberTooltip @tooltipClass="wide" @delay={{100}}>
Child of:
<WbItem @item={{row.parentProduct}}/>
</EmberTooltip>
</div>
{{else if row.isParentSku}}
<FaIcon
class="sku-relationship-icon"
@icon="parking"
/>
{{/if}}
</table.column>
{{else if (eq column "images")}}
<table.column
@label={{column}}
@field={{column}}
@class="photo-preview-column"
@sortable={{false}}
@filterable={{false}}
@showing={{contains column productsTableColumns.defaultShowing}}
@width="200px"
@align="center" as |value column row|
>
<div class="photo-previews">
{{#each row.sortedProductImages as |image|}}
<div class="photo-preview">
<WbS3ImageViewer
@bucket="whitebox-sku-images"
@key={{image.s3Key}}
@url={{image.sourceImage}}
@height={{75}}
/>
</div>
{{/each}}
</div>
</table.column>
{{else}}
<table.column
@field={{column}}
@label={{get productsTableColumns.headers column}}
@showing={{contains column productsTableColumns.defaultShowing}}
@filterable={{not (contains column productsTableColumns.nonFilterable)}} as |value|
>
<WbCopyableValue @value={{value}}/>
</table.column>
{{/if}}
{{/each}}
<table.column
@label=" "
@required={{true}}
@sortable={{false}}
@filterable={{false}}
@enabled={{false}}
@width="100px"
@align="center" as |value column row|
>
<WbInputButton
@icon="eye"
@tooltip="View in marketplace"
@disabled={{eq row.marketplaceListingLink null}}
@onClick={{action "openMarketplaceListingInTab" row}}
/>
<WbInputButton
@icon="pen"
@tooltip="Edit product"
@onClick={{action "showSingleEditDialog" row}}
/>
<WbInputButton
@icon="clone"
@tooltip="Clone product"
@onClick={{action "showCloneProductDialog" row}}
/>
</table.column>
<table.actionBreak />
<table.actionMenu
@icon="copy"
@tooltip="Copy values to clipboard"
@disabled={{this.isProductTableEmpty}}
@menu={{this.copyValuesToClipboardMenu}}
@onSelect={{this.copyValuesToClipboardMenuHandler}}
/>
<table.actionBreak />
<table.action
@class="push-button"
@disabled={{this.isPushFeedsButtonDisabled}}
@icon="paper-plane"
@onClick={{this.showPushProductFeedsDialog}}
@tooltip="Push feeds"
/>
<table.action
@class="view-feeds-button"
@icon="history"
@tooltip="View recently pushed feeds"
@onClick={{this.showRecentlyPushedFeedsDialog}}
/>
<table.actionBreak />
<table.action
@class="bulk-edit"
@disabled={{this.isBulkEditButtonDisabled}}
@icon="pen"
@tooltip="Edit {{this.selectedProducts.length}} products"
@onClick={{this.showBulkEditDialog}}
/>
</WbDataTable>
{{#if (has-block)}}
{{yield this.valueComputed this.column this.row this.index}}
{{else}}
{{this.valueComputed}}
{{/if}}
{{#if (not this.isAlignRight)}}
<div class="wb-data-table-column-body">
{{#if this.label}}
<EmberTooltip @text={{this._stripLabel}} />
{{this._safeLabel}}
{{else if (has-block)}}
{{yield null this null}}
{{/if}}
</div>
{{#if this.isRoleHeading}}
{{#if this.isFilterable}}
<div class="wb-data-table-column-filter" {{on "click" filterHandler}}>
<EmberTooltip @text="Filter Column" />
<FaIcon @icon={{if this.filterPaused "pause-circle" "filter"}} @prefix="fas" />
</div>
{{else}}
<div>
{{!-- Intentionally empty --}}
</div>
{{/if}}
{{#if this.isSortable}}
<div class="wb-data-table-column-sort" {{on "click" sortToggleHandler}}>
<EmberTooltip @text="Sort Column" />
<div class="wb-data-table-column-sort-up">
<FaIcon @icon="sort-up" @prefix="fas" />
</div>
<div class="wb-data-table-column-sort-down">
<FaIcon @icon="sort-down" @prefix="fas" />
</div>
</div>
{{else}}
<div>
{{!-- Intentionally empty --}}
</div>
{{/if}}
{{/if}}
{{else}}
{{#if this.isRoleHeading}}
{{#if this.isSortable}}
<div class="wb-data-table-column-sort" {{on "click" sortToggleHandler}}>
<EmberTooltip @text="Sort Column" />
<div class="wb-data-table-column-sort-up">
<FaIcon @icon="sort-up" @prefix="fas" />
</div>
<div class="wb-data-table-column-sort-down">
<FaIcon @icon="sort-down" @prefix="fas" />
</div>
</div>
{{else}}
<div>
{{!-- Intentionally empty --}}
</div>
{{/if}}
{{#if this.isFilterable}}
<div class="wb-data-table-column-filter" {{on "click" filterHandler}}>
<EmberTooltip @text="Filter Column" />
<FaIcon @icon={{if this.filterPaused "pause-circle" "filter"}} @prefix="fas" />
</div>
{{else}}
<div>
{{!-- Intentionally empty --}}
</div>
{{/if}}
{{/if}}
<div class="wb-data-table-column-body" title={{if this.label this._stripLabel}}>
{{#if this.label}}
{{this._safeLabel}}
{{else if (has-block)}}
{{yield null this null}}
{{/if}}
</div>
{{/if}}
{{!-- If you remove data-tableState argument on the line below, table state will not save. Do not change. --}}
<div class="wb-data-table-header" data-tableState={{this.tableState}}>
{{#if this.showHeader}}
<div class="wb-data-table-header-label">
{{#if this.label}}
<WbLabel @text={{this.label}} />
{{/if}}
{{#if (or (and this.showPagingInfo this.pageable) (and this.showSelectionInfo this.isSelectable))}}
<div class="wb-data-table-header-label-details">
{{#if (and this._showPagerInfo this.pageable)}}
Showing <b>{{this._startingPosition}}</b> to <b>{{this._endingPosition}}</b> of
{{#if this.rowCountInfinite}}
&nbsp;<b>&infin;</b> rows.
{{else}}
<b>{{this._totalRowCount}}</b> {{if (eq this._totalRowCount 1) "row." "rows."}}
{{/if}}
{{/if}}
{{#if (and this.showFilteringInfo (or this._isFilteringActive this._isSearchingActive) this._filteredOutCount)}}
<b>{{this._filteredOutCount}}</b> {{if (eq this._filteredOutCount 1) "row" "rows"}}
filtered out.
{{/if}}
{{#if (and this.showSelectionInfo this.isSelectable)}}
<b>{{this._selectedRowCount}}</b> {{if (eq this._selectedRowCount 1) "row" "rows"}}
selected.
{{/if}}
</div>
{{/if}}
</div>
<div class="wb-data-table-header-actions">
{{#if this._isActionBarTop}}
{{#if (and (not this.isUsingCustomRows) this.searchable)}}
<div class="wb-data-table-header-search">
<WbInputText @label="" @icon="" @placeholder="Search For..." @value={{this._pendingSearchText}} @onChange={{this._searchTextChangeHandler}} @onEnter={{this._executeSearchHandler}} />
<WbInputButton @label="" @icon="search" @onClick={{this._executeSearchHandler}} />
</div>
{{/if}}
{{#if (or this.pageable this.showActions)}}
<WbButtonBar as |bar|>
{{#if (and (not this.isUsingCustomRows) this.pageable)}}
<WbInputButton @label="" @icon="fast-backward" @tooltip="Move back to 1st page." @disabled={{this._isPageFirstDisabled}} @onClick={{fn this.pageStartHandler}} />
<WbInputButton @label="" @icon="step-backward" @tooltip="Move back one page." @disabled={{this._isPagePreviousDisabled}} @onClick={{fn this.pageAdvanceHandler -1}} />
<WbInputButton @label="" @icon="step-forward" @tooltip="Move forward one page." @disabled={{this._isPageNextDisabled}} @onClick={{fn this.pageAdvanceHandler 1}} />
<WbInputButton @label="" @icon="fast-forward" @tooltip="Move forward to last page." @disabled={{this._isPageLastDisabled}} @onClick={{fn this.pageEndHandler}} />
{{/if}}
{{#if (and this.showActions this.showActionsTop)}}
{{#if (and (not this.isUsingCustomRows) this.pageable)}}
<bar.break/>
{{/if}}
{{#if (not this.isUsingCustomRows)}}
<WbInputButtonMenu
@label=""
@icon="columns"
@tooltip="Toggle which columns are displayed in the table."
@menu={{this._toggleColumnMenu}}
@relativeSpot="top-right"
@originSpot="bottom-right"
@onSelect={{this.columnMenuHandler}}
/>
{{/if}}
{{#if (and (not this.isUsingCustomRows) this._isFilteringAvailable)}}
<WbInputButtonMenu
@label=""
@icon="filter"
@tooltip="Modify Filtering."
@menu={{this._filteringMenu}}
@submenuPosition="left"
@relativeSpot="top-right"
@originSpot="bottom-right"
@disabled={{not this._isFilteringActive}}
@onSelect={{this.filteringMenuHandler}}
/>
{{/if}}
{{#if (and (not this.isUsingCustomRows) this.exportable)}}
<WbInputButtonMenu
@label=""
@icon="download"
@tooltip="Export table contents."
@menu={{this._exportMenu}}
@submenuPosition="left"
@relativeSpot="top-right"
@originSpot="bottom-right"
@onSelect={{this.exportMenuHandler}}
/>
{{/if}}
{{#if this.showCustomActionsTop}}
{{yield
(hash
action=(component
"wb-data-table-action"
_actionBarPlacement="top"
class="wb-data-table-custom-action"
)
actionMenu=(component
"wb-data-table-action-menu"
relativeSpot="top-right"
originSpot="bottom-right"
_actionBarPlacement="top"
class="wb-data-table-custom-action-menu"
)
actionBreak=(component
"wb-data-table-action-break"
width="10px"
height="100%"
_actionBarPlacement="top"
class="wb-data-table-custom-action-break"
)
)
}}
{{/if}}
{{/if}}
</WbButtonBar>
{{/if}}
{{/if}}
</div>
{{/if}}
</div>
<div class="wb-data-table-inner" >
<div class="wb-data-table-headings" style="width: {{this.rowWidth}}; {{this._widthString}}" {{ref this "headingsElement"}} >
{{#if this.showColumnHeadings}}
{{#if (and (not this.isUsingCustomRows) this._isHierarchyActive)}}
<WbDataTableColumn @class="wb-data-table-headings-hierarchy-toggle" @label="" @field="" @sortable={{false}} @filterable={{false}} @align="left" valign="center" {{on "click" this.hierarchyAllHandler}}>
{{#if this._hierarchyAllExpanded}}
<FaIcon @icon="minus-square" @prefix="far" />
{{else}}
<FaIcon @icon="plus-square" @prefix="far" />
{{/if}}
</WbDataTableColumn>
{{/if}}
{{#if (and (not this.isUsingCustomRows) this.isSelectable)}}
<WbDataTableColumn @class="wb-data-table-headings-select-all" @label="" @field="" @sortable={{false}} @filterable={{false}} @align="center" valign="center" {{on "click" this.selectAllHandler}}>
{{#if this.isSelectModeSingle}}
{{#if this.isSomeSelected}}
<FaIcon @icon="check-circle" @prefix="fas" />
{{else}}
<FaIcon @icon="circle" @prefix="far" />
{{/if}}
{{else}}
{{#if this.isAllSelected}}
<FaIcon @icon="square" @prefix="far" />
{{else}}
<FaIcon @icon="check-square" @prefix="fas" />
{{/if}}
{{/if}}
</WbDataTableColumn>
{{/if}}
{{#if (and (not this.isUsingCustomRows) this._isSubrowsAllowed)}}
<WbDataTableColumn @class="wb-data-table-headings-subrow-toggle" @label="" @field="" @sortable={{false}} @filterable={{false}} @align="center" valign="center" {{on "click" this.subrowAllHandler}}>
{{#if this._someSubrowsOpened}}
<FaIcon @icon="caret-down" @prefix="fas" />
{{else}}
<FaIcon @icon="caret-right" @prefix="fas" />
{{/if}}
</WbDataTableColumn>
{{/if}}
{{yield
(hash
column=(
component "wb-data-table-column"
_role="HEADING"
class=this.additionalColumnClassName
columnAdded=(fn this._columnAddedHandler)
columnRemoved=(fn this._columnRemovedHandler)
)
)
}}
{{/if}}
</div>
<div class="wb-data-table-body" {{ref this "bodyElement"}}>
{{#if (not isReady)}}
<div class="wb-data-table-body-messages">
<div class="spinkit-folding-cube">
<div class="spinkit-cube1 spinkit-cube"></div>
<div class="spinkit-cube2 spinkit-cube"></div>
<div class="spinkit-cube4 spinkit-cube"></div>
<div class="spinkit-cube3 spinkit-cube"></div>
</div>
<div class="wb-data-table-body-messages-text">
{{notReadyMessage}}
</div>
</div>
{{else if (and (not this.isUsingCustomRows) _isAllFilteredOut)}}
<div class="wb-data-table-body-messages">
<div class="wb-data-table-body-empty">
{{this.allFilteredOutMessage}}
</div>
</div>
{{else if isEmpty}}
<div class="wb-data-table-body-messages">
<div class="wb-data-table-body-empty">
{{this.emptyMessage}}
</div>
</div>
{{/if}}
<div class="wb-data-table-body-rows" style="width: {{this.rowWidth}};">
{{#each this.showingRows as |row index|}}
<WbDataTableRow class={{this.additionalRowClassName}} @row={{row}} style={{this._widthString}} @rowAdded={{fn this._rowAddedHandler}} @rowRemoved={{fn this._rowRemovedHandler}}>
{{#if (and (not this.isUsingCustomRows) this._isHierarchyActive)}}
<WbDataTableCell class="wb-data-table-hierarchy-cell" @align="left" @valign="center">
{{#each (compute this._getHierarchySteps row) as |step|}}
{{#if (eq step "leaf")}}
<div class="wb-data-table-hierarchy-cell-step wb-data-table-hierarchy-cell-step-leaf">
<FaIcon @icon="square" @prefix="fas" />
</div>
{{else if (eq step "parent")}}
{{#if (compute this._getHierarchyOpened row this._hierarchyIndex)}}
<div class="wb-data-table-hierarchy-cell-step wb-data-table-hierarchy-cell-step-parent-opened" {{on "click" (fn this.toggleHierarchyRow row false)}}>
<FaIcon @icon="minus-square" @prefix="fas" />
</div>
{{else}}
<div class="wb-data-table-hierarchy-cell-step wb-data-table-hierarchy-cell-step-parent-closed" {{on "click" (fn this.toggleHierarchyRow row true)}}>
<FaIcon @icon="plus-square" @prefix="fas" />
</div>
{{/if}}
{{else if (eq step "sibling")}}
<div class="wb-data-table-hierarchy-cell-step wb-data-table-hierarchy-cell-step-sibling">
</div>
{{else if (eq step "sibling-last")}}
<div class="wb-data-table-hierarchy-cell-step wb-data-table-hierarchy-cell-step-sibling-last">
</div>
{{else if (eq step "to")}}
<div class="wb-data-table-hierarchy-cell-step wb-data-table-hierarchy-cell-step-to">
</div>
{{else if (eq step "thru")}}
<div class="wb-data-table-hierarchy-cell-step wb-data-table-hierarchy-cell-step-thru">
</div>
{{else if (eq step "none")}}
<div class="wb-data-table-hierarchy-cell-step wb-data-table-hierarchy-cell-step-none">
</div>
{{/if}}
{{/each}}
</WbDataTableCell>
{{/if}}
{{#if (and (not this.isUsingCustomRows) this.isSelectable)}}
{{#if this.isSelectModeSingle}}
<WbDataTableCell class="wb-data-table-selection-cell" @align="center" @valign="center">
<div class="wb-data-table-selection-selected">
<FaIcon @icon="check-circle" @prefix="fas" />
</div>
<div class="wb-data-table-selection-unselected">
<FaIcon @icon="circle" @prefix="far" />
</div>
</WbDataTableCell>
{{else if (or this.isSelectModeMulti this.isSelectModeToggle)}}
<WbDataTableCell class="wb-data-table-selection-cell" @align="center" @valign="center">
<div class="wb-data-table-selection-selected">
<FaIcon @icon="check-square" @prefix="fas" />
</div>
<div class="wb-data-table-selection-unselected">
<FaIcon @icon="square" @prefix="far" />
</div>
</WbDataTableCell>
{{/if}}
{{/if}}
{{#if (and (not this.isUsingCustomRows) this._isSubrowsAllowed)}}
<WbDataTableCell class="wb-data-table-subrow-toggle-cell" @align="center" @valign="center" {{on "click" (fn this._subrowToggleHandler row)}}>
<div class="wb-data-table-subrow-toggle-cell-opened">
<FaIcon @icon="caret-down" @prefix="fas" />
</div>
<div class="wb-data-table-subrow-toggle-cell-closed">
<FaIcon @icon="caret-right" @prefix="fas" />
</div>
</WbDataTableCell>
{{/if}}
{{yield
(hash
column=(
component "wb-data-table-cell"
_role="CELL"
row=row
index=index
cellAdded=(fn this._cellAddedHandler)
cellRemoved=(fn this._cellRemovedHandler)
)
)
}}
</WbDataTableRow>
{{yield
(hash
subrow=(
component "wb-data-table-subrow"
row=row
index=index
class=this.additionalSubrowClassName
subrowAdded=this._subrowAddedHandler
subrowRemoved=this._subrowRemovedHandler
)
)
}}
{{/each}}
{{yield
(hash
row=(
component "wb-data-table-row"
class=this.additionalRowClassName
rowAdded=this._rowAddedHandler
rowRemoved=this._rowRemovedHandler
)
cell=(
component "wb-data-table-cell"
class=this.additionalCellClassName
cellAdded=this._cellAddedHandler
cellRemoved=this._cellRemovedHandler
)
)
}}
</div>
</div>
<div class="wb-data-table-footings" style="width: {{this.rowWidth}}; {{this._widthString}}" {{ref this "footingsElement"}}>
{{#if this.showColumnFootings}}
{{#if (and (not this.isUsingCustomRows) this._isHierarchyActive)}}
<WbDataTableColumn @class="wb-data-table-headings-hierarchy-toggle" @label="" @field="" @sortable={{false}} @filterable={{false}} @align="center" valign="center" {{on "click" this.selectAllHandler}}>
<FaIcon @icon="plus-square" @prefix="far" />
</WbDataTableColumn>
{{/if}}
{{#if (and (not this.isUsingCustomRows) this.isSelectable)}}
<WbDataTableColumn @class="wb-data-table-headings-select-all" @label="" @field="" @sortable={{false}} @filterable={{false}} @align="center" valign="center" {{on "click" this.selectAllHandler}}>
{{#if this.isSelectModeSingle}}
{{#if this.isSomeSelected}}
<FaIcon @icon="check-circle" @prefix="fas" />
{{else}}
<FaIcon @icon="circle" @prefix="far" />
{{/if}}
{{else}}
{{#if this.isAllSelected}}
<FaIcon @icon="square" @prefix="far" />
{{else}}
<FaIcon @icon="check-square" @prefix="fas" />
{{/if}}
{{/if}}
</WbDataTableColumn>
{{/if}}
{{#if (and (not this.isUsingCustomRows) this._isSubrowsAllowed)}}
<WbDataTableColumn @class="wb-data-table-headings-subrow-toggle" @label="" @field="" @sortable={{false}} @filterable={{false}} @align="center" valign="center" {{on "click" this.selectAllHandler}}>
{{#if this._isSubrowsAllOpen}}
<FaIcon @icon="caret-down" @prefix="fas" />
{{else}}
<FaIcon @icon="caret-right" @prefix="fas" />
{{/if}}
</WbDataTableColumn>
{{/if}}
{{yield
(hash
column=(
component "wb-data-table-column"
_role="FOOTING"
sortable=false
filterable=false
class=this.additionalColumnClassName
columnAdded=null
columnRemoved=null
)
)
}}
{{/if}}
</div>
</div>
<div class="wb-data-table-footer" >
{{#if this.showFooter}}
<div class="wb-data-table-footer-actions">
{{#if this._isActionBarBottom}}
{{#if (or this.pageable this.showActions)}}
<WbButtonBar as |bar|>
{{#if (and (not this.isUsingCustomRows) this.pageable)}}
<WbInputButton @label="" @icon="fast-backward" @tooltip="Move back to 1st page." @disabled={{this._isPageFirstDisabled}} @onClick={{fn this.pageStartHandler}} />
<WbInputButton @label="" @icon="step-backward" @tooltip="Move back one page." @disabled={{this._isPagePreviousDisabled}} @onClick={{fn this.pageAdvanceHandler -1}} />
<WbInputButton @label="" @icon="step-forward" @tooltip="Move forward one page." @disabled={{this._isPageNextDisabled}} @onClick={{fn this.pageAdvanceHandler 1}} />
<WbInputButton @label="" @icon="fast-forward" @tooltip="Move forward to last page." @disabled={{this._isPageLastDisabled}} @onClick={{fn this.pageEndHandler}} />
{{/if}}
{{#if (and this.showActions this.showActionsBottom)}}
{{#if (and (not this.isUsingCustomRows) this.pageable)}}
<bar.break/>
{{/if}}
{{#if (not this.isUsingCustomRows)}}
<WbInputButtonMenu
@label=""
@icon="columns"
@tooltip="Toggle which columns are displayed in the table."
@menu={{this._toggleColumnMenu}}
@relativeSpot="bottom-left"
@originSpot="top-left"
@onSelect={{this.columnMenuHandler}}
/>
{{/if}}
{{#if (and (not this.isUsingCustomRows) this._isFilteringAvailable)}}
<WbInputButton @label="" @icon="filter" @tooltip="Remove all active filters." @disabled={{not this._isFilteringActive}} @onClick={{this.resetFiltersHandler}} />
{{/if}}
{{#if (and (not this.isUsingCustomRows) this.exportable)}}
<WbInputButtonMenu
@label=""
@icon="download"
@tooltip="Export table."
@menu={{this._exportMenu}}
@submenuPosition="right"
@relativeSpot="bottom-left"
@originSpot="top-left"
@onSelect={{this.exportMenuHandler}}
/>
{{/if}}
{{#if this.showCustomActionsBottom}}
{{yield
(hash
action=(component
"wb-data-table-action"
_actionBarPlacement="bottom"
class="wb-data-table-custom-action"
)
actionMenu=(component
"wb-data-table-action-menu"
relativeSpot="bottom-left"
originSpot="top-left"
_actionBarPlacement="bottom"
class="wb-data-table-custom-action"
)
actionBreak=(component
"wb-data-table-action-break"
width="10px"
height="100%"
_actionBarPlacement="bottom"
class="wb-data-table-custom-action"
)
)
}}
{{/if}}
{{/if}}
</WbButtonBar>
{{/if}}
{{/if}}
</div>
{{/if}}
</div>
{
"version": "0.17.1",
"EmberENV": {
"FEATURES": {},
"_TEMPLATE_ONLY_GLIMMER_COMPONENTS": false,
"_APPLICATION_TEMPLATE_WRAPPER": true,
"_JQUERY_INTEGRATION": true
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.js",
"ember": "3.18.1",
"ember-template-compiler": "3.18.1",
"ember-testing": "3.18.1",
"ember-concurrency": "^2.0.3"
},
"addons": {
"@glimmer/component": "1.0.0"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment