summaryrefslogtreecommitdiffstats
path: root/app/common/components
diff options
context:
space:
mode:
Diffstat (limited to 'app/common/components')
-rw-r--r--app/common/components/table/table-checkbox.js67
-rw-r--r--app/common/components/table/table-toolbar.js112
-rw-r--r--app/common/components/table/table.html223
-rw-r--r--app/common/components/table/table.js170
4 files changed, 449 insertions, 123 deletions
diff --git a/app/common/components/table/table-checkbox.js b/app/common/components/table/table-checkbox.js
new file mode 100644
index 0000000..b15e4ed
--- /dev/null
+++ b/app/common/components/table/table-checkbox.js
@@ -0,0 +1,67 @@
+window.angular && (function(angular) {
+ 'use strict';
+
+ /**
+ *
+ * tableCheckbox Component
+ *
+ */
+
+ const controller = function($element) {
+ // <input> element ref needed to add indeterminate state
+ let inputEl;
+
+ /**
+ * Callback when the input select value changes
+ */
+ this.onSelectChange = () => {
+ const checked = this.ngModel;
+ this.emitChange({checked});
+ };
+
+ /**
+ * onChanges Component lifecycle hook
+ */
+ this.$onChanges = (onChangesObj) => {
+ const indeterminateChange = onChangesObj.indeterminate;
+ if (indeterminateChange && inputEl) {
+ inputEl.prop('indeterminate', this.indeterminate);
+ }
+ };
+
+ /**
+ * postLink Component lifecycle hook
+ */
+ this.$postLink = () => {
+ inputEl = $element.find('input');
+ };
+ };
+
+ /**
+ * Component template
+ */
+ const template = `
+ <div class="bmc-table__checkbox-container">
+ <label class="bmc-table__checkbox"
+ ng-class="{
+ 'checked': $ctrl.ngModel,
+ 'indeterminate': $ctrl.indeterminate
+ }">
+ <input type="checkbox"
+ class="bmc-table__checkbox-input"
+ ng-model="$ctrl.ngModel"
+ ng-change="$ctrl.onSelectChange()"
+ aria-label="Select row"/>
+ <span class="screen-reader-offscreen">Select row</span>
+ </label>
+ </div>`
+
+ /**
+ * Register tableCheckbox component
+ */
+ angular.module('app.common.components').component('tableCheckbox', {
+ controller: ['$element', controller],
+ template,
+ bindings: {ngModel: '=', indeterminate: '<', emitChange: '&'}
+ })
+})(window.angular); \ No newline at end of file
diff --git a/app/common/components/table/table-toolbar.js b/app/common/components/table/table-toolbar.js
new file mode 100644
index 0000000..830bbd1
--- /dev/null
+++ b/app/common/components/table/table-toolbar.js
@@ -0,0 +1,112 @@
+window.angular && (function(angular) {
+ 'use strict';
+
+ /**
+ *
+ * tableToolbar Component
+ *
+ * To use:
+ * The <table-toolbar> component expects an 'actions' attribute
+ * that should be an array of action objects.
+ * Each action object should have 'type', 'label', and 'file'
+ * properties.
+ *
+ * actions: [
+ * {type: 'Edit', label: 'Edit', file: 'icon-edit.svg'},
+ * {type: 'Delete', label: 'Edit', file: 'icon-trashcan.svg'}
+ * ]
+ *
+ * The 'type' property is a string value that will be emitted to the
+ * parent component when clicked.
+ *
+ * The 'label' property is a string value that will render as text in
+ * the button
+ *
+ * The 'file' property is a string value of the filename of the svg icon
+ * to provide <icon> directive.
+ *
+ */
+
+ const controller = function() {
+ /**
+ * Set defaults if properties undefined
+ * @param {[]} actions
+ */
+ const setActions = (actions = []) => {
+ return actions
+ .map((action) => {
+ if (action.type === undefined) {
+ return;
+ }
+ if (action.file === undefined) {
+ action.file = null;
+ }
+ return action;
+ })
+ .filter((action) => action);
+ };
+
+ /**
+ * Callback when button action clicked
+ * Emits the action type to the parent component
+ */
+ this.onClick = (action) => {
+ this.emitAction({action});
+ };
+
+ this.onClickClose = () => {
+ this.emitClose();
+ };
+
+ /**
+ * onInit Component lifecycle hook
+ */
+ this.$onInit = () => {
+ this.actions = setActions(this.actions);
+ };
+ };
+
+ /**
+ * Component template
+ */
+ const template = `
+ <div class="bmc-table__toolbar" ng-if="$ctrl.active">
+ <p class="toolbar__selection-count">{{$ctrl.selectionCount}} selected</p>
+ <div class="toolbar__batch-actions" ng-show="$ctrl.actions.length > 0">
+ <button
+ class="btn btn-tertiary"
+ type="button"
+ aria-label="{{action.label}}"
+ ng-repeat="action in $ctrl.actions"
+ ng-click="$ctrl.onClick(action.type)">
+ <icon ng-if="action.file !== null"
+ ng-file="{{action.file}}"
+ aria-hidden="true">
+ </icon>
+ {{action.label || action.type}}
+ </button>
+ <button
+ class="btn btn-tertiary btn-close"
+ type="button"
+ aria-label="Cancel"
+ ng-click="$ctrl.onClickClose()">
+ Cancel
+ </button>
+ </div>
+ </div>`
+
+ /**
+ * Register tableToolbar component
+ */
+ angular.module('app.common.components').component('tableToolbar', {
+ controller,
+ template,
+ bindings: {
+ actions: '<',
+ selectionCount: '<',
+ active: '<',
+ emitAction: '&',
+ emitClose: '&'
+ }
+ })
+})(window.angular); \ No newline at end of file
diff --git a/app/common/components/table/table.html b/app/common/components/table/table.html
index 96ca870..7d906a1 100644
--- a/app/common/components/table/table.html
+++ b/app/common/components/table/table.html
@@ -1,94 +1,131 @@
-<table class="bmc-table {{$ctrl.size}}">
- <thead class="bmc-table__head">
- <!-- Header row (non-sortable) -->
- <tr ng-if="!$ctrl.sortable">
- <th ng-repeat="headerItem in $ctrl.header"
- class="bmc-table__column-header">
- {{headerItem.label}}
- </th>
- </tr>
- <!-- Header row (sortable) -->
- <tr ng-if="$ctrl.sortable">
- <th ng-repeat="headerItem in $ctrl.header track by $index"
- class="bmc-table__column-header">
- <span ng-if="!headerItem.sortable">
- {{headerItem.label}}
- </span>
- <span ng-if="headerItem.sortable"
- ng-click="$ctrl.onClickSort($index)"
- class="bmc-table__column-header--sortable">
- {{headerItem.label}}
- <!-- Sort icons -->
- <button class="sort-icon"
- type="button"
- aria-label="sort {{headerItem.label}}">
- <icon file="icon-arrow--up.svg"
- ng-if="$index === $ctrl.activeSort"
- ng-class="{
- 'sort-icon--descending': !$ctrl.sortAscending,
- 'sort-icon--ascending' : $ctrl.sortAscending }"
- class="sort-icon--active"
- aria-hidden="true"></icon>
- <span ng-if="$index !== $ctrl.activeSort"
- class="sort-icon--inactive"
- aria-hidden="true">
- <icon file="icon-arrow--up.svg"></icon>
- <icon file="icon-arrow--down.svg"></icon>
- </span>
+<div class="bmc-table__container">
+ <table-toolbar ng-if="$ctrl.selectable"
+ selection-count="$ctrl.selectedRows.size"
+ active="$ctrl.selectedRows.size > 0"
+ actions="$ctrl.batchActions"
+ emit-action="$ctrl.onEmitBatchAction(action)"
+ emit-close="$ctrl.onToolbarClose()"
+ ng-animate-children="true">
+ </table-toolbar>
+ <table class="bmc-table {{$ctrl.size}}"
+ ng-class="{
+ 'bmc-table--sortable': $ctrl.sortable,
+ 'bmc-table--expandable': $ctrl.expandable,
+ 'bmc-table--selectable': $ctrl.selectable,
+ 'bmc-table--row-actions-enabled': '$ctrl.rowActionsEnabled',
+ }">
+ <thead class="bmc-table__head">
+ <!-- Header row -->
+ <tr>
+ <!-- Expandable empty cell -->
+ <th ng-if="$ctrl.expandable"
+ class="bmc-table__column-header"></th>
+ <!-- Select checkbox -->
+ <th ng-if="$ctrl.selectable"
+ class="bmc-table__column-header">
+ <table-checkbox ng-model="$ctrl.selectHeaderCheckbox"
+ indeterminate="$ctrl.someSelected"
+ emit-change="$ctrl.onHeaderSelectChange(checked)">
+ </table-checkbox>
+ </th>
+ <!-- Header items -->
+ <th ng-repeat="headerItem in $ctrl.header track by $index"
+ class="bmc-table__column-header">
+ <span ng-if="!$ctrl.sortable || !headerItem.sortable">
+ {{headerItem.label}}
+ </span>
+ <span ng-if="$ctrl.sortable && headerItem.sortable"
+ ng-click="$ctrl.onClickSort($index)"
+ class="bmc-table__column-header--sortable">
+ {{headerItem.label}}
+ <!-- Sort icons -->
+ <button class="sort-icon"
+ type="button"
+ aria-label="sort {{headerItem.label}}">
+ <icon file="icon-arrow--up.svg"
+ ng-if="$index === $ctrl.activeSort"
+ ng-class="{
+ 'sort-icon--descending': !$ctrl.sortAscending,
+ 'sort-icon--ascending' : $ctrl.sortAscending }"
+ class="sort-icon--active"
+ aria-hidden="true"></icon>
+ <span ng-if="$index !== $ctrl.activeSort"
+ class="sort-icon--inactive"
+ aria-hidden="true">
+ <icon file="icon-arrow--up.svg"></icon>
+ <icon file="icon-arrow--down.svg"></icon>
+ </span>
+ </button>
+ </span>
+ </th>
+ <!-- Row actions empty cell -->
+ <th ng-if="$ctrl.rowActionsEnabled"
+ class="bmc-table__column-header"></th>
+ </tr>
+ </thead>
+ <tbody class="bmc-table__body">
+ <!-- Data rows -->
+ <tr ng-if="$ctrl.data.length > 0"
+ ng-repeat-start="row in $ctrl.data track by $index"
+ class="bmc-table__row"
+ ng-class="{
+ 'bmc-table__row--expanded': $ctrl.expandedRows.has($index),
+ 'bmc-table__row--selected': $ctrl.selectedRows.has($index),
+ }">
+ <!-- Row expansion trigger -->
+ <td ng-if="$ctrl.expandable"
+ class="bmc-table__cell">
+ <button type="button"
+ class="btn btn--expand"
+ aria-label="expand row"
+ ng-click="$ctrl.onClickExpand($index)">
+ <icon file="icon-chevron-right.svg" aria-hidden="true"></icon>
</button>
- </span>
- </th>
- </tr>
- </thead>
- <tbody class="bmc-table__body">
- <!-- Data rows -->
- <tr ng-if="$ctrl.data.length > 0"
- ng-repeat-start="row in $ctrl.data track by $index"
- class="bmc-table__row"
- ng-class="{
- 'bmc-table__row--expanded': $ctrl.expandedRows.has($index)
- }">
- <!-- Row expansion trigger -->
- <td ng-if="$ctrl.expandable"
- class="bmc-table__cell">
- <button type="button"
- class="btn btn--expand"
- aria-label="expand row"
- ng-click="$ctrl.onClickExpand($index)">
- <icon file="icon-chevron-right.svg" aria-hidden="true"></icon>
- </button>
- </td>
- <!-- Row item -->
- <td ng-repeat="item in row.uiData track by $index"
- class="bmc-table__cell">
- <ng-bind-html ng-bind-html="item"></ng-bind-html>
- </td>
- <!-- Row Actions -->
- <td ng-if="$ctrl.rowActionsEnabled"
- class="bmc-table__cell bmc-table__row-actions">
- <table-actions
- actions="row.actions"
- emit-action="$ctrl.onEmitTableAction(action, row)">
- </table-actions>
- </td>
- </tr>
- <!-- Expansion row -->
- <tr ng-repeat-end
- ng-if="$ctrl.expandedRows.has($index)"
- class="bmc-table__expansion-row">
- <td class="bmc-table__cell"></td>
- <td class="bmc-table__cell"
- colspan="{{$ctrl.header.length - 1}}">
- <ng-bind-html
- ng-bind-html="row.expandContent || 'No data'">
- </ng-bind-html>
- </td>
- </tr>
- <!-- Empty table -->
- <tr ng-if="$ctrl.data.length === 0"
- class="bmc-table__expansion-row">
- <td class="bmc-table__cell"
- colspan="{{$ctrl.header.length}}">No data</td>
- </tr>
- </tbody>
-</table> \ No newline at end of file
+ </td>
+ <!-- Row checkbox -->
+ <td ng-if="$ctrl.selectable"
+ class="bmc-table__cell">
+ <table-checkbox ng-if="row.selectable"
+ ng-model="row.selected"
+ emit-change="$ctrl.onRowSelectChange($index)">
+ </table-checkbox>
+ </td>
+ <!-- Row item -->
+ <td ng-repeat="item in row.uiData track by $index"
+ class="bmc-table__cell">
+ <ng-bind-html ng-bind-html="item"></ng-bind-html>
+ </td>
+ <!-- Row Actions -->
+ <td ng-if="$ctrl.rowActionsEnabled"
+ class="bmc-table__cell bmc-table__row-actions">
+ <table-actions
+ actions="row.actions"
+ emit-action="$ctrl.onEmitRowAction(action, row)">
+ </table-actions>
+ </td>
+ </tr>
+ <!-- Expansion row -->
+ <tr ng-repeat-end
+ ng-if="$ctrl.expandedRows.has($index)"
+ class="bmc-table__expansion-row">
+ <td class="bmc-table__cell"></td>
+ <td class="bmc-table__cell"
+ colspan="{{$ctrl.header.length + $ctrl.sortable +
+ $ctrl.expandable + $ctrl.rowActionsEnabled}}">
+ <ng-bind-html
+ ng-bind-html="row.expandContent || 'No data'">
+ </ng-bind-html>
+ </td>
+ </tr>
+ <!-- Empty table -->
+ <tr ng-if="$ctrl.data.length === 0"
+ class="bmc-table__expansion-row">
+ <td class="bmc-table__cell"
+ colspan="{{$ctrl.header.length + $ctrl.sortable +
+ $ctrl.expandable + $ctrl.rowActionsEnabled}}">
+ No data
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</div> \ No newline at end of file
diff --git a/app/common/components/table/table.js b/app/common/components/table/table.js
index 5db05b6..2063555 100644
--- a/app/common/components/table/table.js
+++ b/app/common/components/table/table.js
@@ -18,9 +18,12 @@ window.angular && (function(angular) {
* Each row object can optionally have an 'expandContent' property
* that should be a string value and can contain valid HTML. To render
* the expanded content, set 'expandable' attribute to true.
+ * Each row object can optionally have a 'selectable' property. Defaults
+ * to true if table is selectable. If a particular row should not
+ * be selectable, set to false.
*
* data = [
- * { uiData: ['root', 'Admin', 'enabled' ] },
+ * { uiData: ['root', 'Admin', 'enabled' ], selectable: false },
* { uiData: ['user1', 'User', 'disabled' ] }
* ]
*
@@ -47,6 +50,10 @@ window.angular && (function(angular) {
* The 'expandable' attribute should be a boolean value. If true each
* row object in data array should contain a 'expandContent' property
*
+ * The 'selectable' attribute should be a boolean value.
+ * If 'selectable' is true, include 'batch-actions' property that should
+ * be an array of actions to provide <table-toolbar> component.
+ *
* The 'size' attribute which can be set to 'small' which will
* render a smaller font size in the table.
*
@@ -56,6 +63,11 @@ window.angular && (function(angular) {
this.sortAscending = true;
this.activeSort;
this.expandedRows = new Set();
+ this.selectedRows = new Set();
+ this.selectHeaderCheckbox = false;
+ this.someSelected = false;
+
+ let selectableRowCount = 0;
/**
* Sorts table data
@@ -89,34 +101,99 @@ window.angular && (function(angular) {
return column;
})
}
- if (this.rowActionsEnabled) {
- // If table actions are enabled push an empty
- // string to the header array to account for additional
- // table actions cell
- this.header.push({label: '', sortable: false});
- }
- if (this.expandable) {
- // If table is expandable, push an empty string to the
- // header array to account for additional expansion cell
- this.header.unshift({label: '', sortable: false});
+ };
+
+ /**
+ * Prep data
+ * When data binding changes, make adjustments to account for
+ * optional configurations and undefined values
+ */
+ const prepData = () => {
+ selectableRowCount = 0;
+ this.data.forEach((row) => {
+ if (row.uiData === undefined) {
+ // Check for undefined 'uiData' property for each item in data
+ // array
+ row.uiData = [];
+ }
+ if (this.selectable) {
+ // If table is selectable check row for 'selectable' property
+ row.selectable = row.selectable === undefined ? true : row.selectable;
+ if (row.selectable) {
+ selectableRowCount++;
+ row.selected = false;
+ }
+ }
+ });
+ if (this.sortable) {
+ if (this.activeSort !== undefined || this.defaultSort !== undefined) {
+ // apply default or active sort if one is defined
+ this.activeSort = this.defaultSort !== undefined ? this.defaultSort :
+ this.activeSort;
+ sortData();
+ }
}
};
/**
+ * Select all rows
+ * Sets each selectable row selected property to true
+ * and adds index to selectedRow Set
+ */
+ const selectAllRows = () => {
+ this.selectHeaderCheckbox = true;
+ this.someSelected = false;
+ this.data.forEach((row, index) => {
+ if (!row.selected && row.selectable) {
+ row.selected = true;
+ this.selectedRows.add(index);
+ }
+ })
+ };
+
+ /**
+ * Deselect all rows
+ * Sets each row selected property to false
+ * and clears selectedRow Set
+ */
+ const deselectAllRows = () => {
+ this.selectHeaderCheckbox = false;
+ this.someSelected = false;
+ this.selectedRows.clear();
+ this.data.forEach((row) => {
+ if (row.selectable) {
+ row.selected = false;
+ }
+ })
+ };
+
+ /**
* Callback when table row action clicked
* Emits user desired action and associated row data to
* parent controller
* @param {string} action : action type
* @param {any} row : user object
*/
- this.onEmitTableAction = (action, row) => {
+ this.onEmitRowAction = (action, row) => {
if (action !== undefined && row !== undefined) {
const value = {action, row};
- this.emitAction({value});
+ this.emitRowAction({value});
}
};
/**
+ * Callback when batch action clicked from toolbar
+ * Emits the action type and the selected row data to
+ * parent controller
+ * @param {string} action : action type
+ */
+ this.onEmitBatchAction = (action) => {
+ const filteredRows = this.data.filter((row) => row.selected);
+ const value = {action, filteredRows};
+ this.emitBatchAction({value});
+ };
+
+ /**
* Callback when sortable table header clicked
* @param {number} index : index of header item
*/
@@ -146,6 +223,49 @@ window.angular && (function(angular) {
};
/**
+ * Callback when select checkbox clicked
+ * @param {number} row : index of selected row
+ */
+ this.onRowSelectChange = (row) => {
+ if (this.selectedRows.has(row)) {
+ this.selectedRows.delete(row);
+ } else {
+ this.selectedRows.add(row);
+ }
+ if (this.selectedRows.size === 0) {
+ this.someSelected = false;
+ this.selectHeaderCheckbox = false;
+ deselectAllRows();
+ } else if (this.selectedRows.size === selectableRowCount) {
+ this.someSelected = false;
+ this.selectHeaderCheckbox = true;
+ selectAllRows();
+ } else {
+ this.someSelected = true;
+ }
+ };
+
+ /**
+ * Callback when header select box value changes
+ */
+ this.onHeaderSelectChange = (checked) => {
+ this.selectHeaderCheckbox = checked;
+ if (this.selectHeaderCheckbox) {
+ selectAllRows();
+ } else {
+ deselectAllRows();
+ }
+ };
+
+ /**
+ * Callback when cancel/close button closed
+ * from toolbar
+ */
+ this.onToolbarClose = () => {
+ deselectAllRows();
+ };
+
+ /**
* onInit Component lifecycle hook
* Checking for undefined values
*/
@@ -157,32 +277,19 @@ window.angular && (function(angular) {
this.rowActionsEnabled === undefined ? false : this.rowActionsEnabled;
this.size = this.size === undefined ? '' : this.size;
this.expandable = this.expandable === undefined ? false : this.expandable;
-
- // Check for undefined 'uiData' property for each item in data array
- this.data = this.data.map((row) => {
- if (row.uiData === undefined) {
- row.uiData = [];
- }
- return row;
- })
+ this.selectable = this.selectable === undefined ? false : this.selectable;
prepTable();
};
/**
* onChanges Component lifecycle hook
- * Check for changes in the data array and apply
- * default or active sort if one is defined
*/
this.$onChanges = (onChangesObj) => {
const dataChange = onChangesObj.data;
if (dataChange) {
- if (this.activeSort !== undefined || this.defaultSort !== undefined) {
- this.activeSort = this.defaultSort !== undefined ? this.defaultSort :
- this.activeSort;
- sortData();
- }
+ prepData();
}
- }
+ };
};
/**
@@ -199,7 +306,10 @@ window.angular && (function(angular) {
sortable: '<', // boolean
defaultSort: '<', // number (index of sort)
expandable: '<', // boolean
- emitAction: '&'
+ selectable: '<', // boolean
+ batchActions: '<', // Array
+ emitRowAction: '&',
+ emitBatchAction: '&'
}
})
})(window.angular);
OpenPOWER on IntegriCloud