src/extensions/colsVisibility/colsVisibility.js
import {Feature} from '../../feature';
import {
addClass, removeClass, createCheckItem, createElm, elm, removeElm,
getText, tag
} from '../../dom';
import {isUndef, EMPTY_FN, isNull} from '../../types';
import {addEvt, targetEvt, removeEvt} from '../../event';
import {root} from '../../root';
import {NONE} from '../../const';
import {
defaultsBool, defaultsStr, defaultsFn, defaultsNb, defaultsArr
} from '../../settings';
import {RIGHT} from '../../modules/toolbar';
/**
* Columns Visibility extension
*/
export default class ColsVisibility extends Feature {
/**
* Creates an instance of ColsVisibility
* @param {TableFilter} tf TableFilter instance
* @param {Object} Configuration object
*/
constructor(tf, f) {
super(tf, f.name);
// Configuration object
let cfg = this.config;
/**
* Module name
* @type {String}
*/
this.name = f.name;
/**
* Module description
* @type {String}
*/
this.desc = defaultsStr(f.description, 'Columns visibility manager');
/**
* show/hide columns container element
* @private
*/
this.spanEl = null;
/**
* show/hide columns button element
* @private
*/
this.btnEl = null;
/**
* show/hide columns main container element
* @private
*/
this.contEl = null;
/**
* Enable tick to hide a column, defaults to true
* @type {Boolean}
*/
this.tickToHide = defaultsBool(f.tick_to_hide, true);
/**
* Enable columns manager UI, defaults to true
* @type {Boolean}
*/
this.manager = defaultsBool(f.manager, true);
/**
* Headers HTML table reference only if headers are external
* @type {DOMElement}
*/
this.headersTbl = f.headers_table || null;
/**
* Headers row index only if headers are external
* @type {Number}
*/
this.headersIndex = defaultsNb(f.headers_index, 1);
/**
* ID of main container element
* @type {String}
*/
this.contElTgtId = defaultsStr(f.container_target_id, null);
/**
* Alternative text for column headers in column manager UI
* @type {Array}
*/
this.headersText = defaultsArr(f.headers_text, []);
/**
* ID of button's container element
* @type {String}
*/
this.btnTgtId = defaultsStr(f.btn_target_id, null);
/**
* Button's text, defaults to Columns▼
* @type {String}
*/
this.btnText = defaultsStr(f.btn_text, 'Columns▼');
/**
* Button's inner HTML
* @type {String}
*/
this.btnHtml = defaultsStr(f.btn_html, null);
/**
* Css class for button
* @type {String}
*/
this.btnCssClass = defaultsStr(f.btn_css_class, 'colVis');
/**
* Columns manager UI close link text, defaults to 'Close'
* @type {String}
*/
this.btnCloseText = defaultsStr(f.btn_close_text, 'Close');
/**
* Columns manager UI close link HTML
* @type {String}
*/
this.btnCloseHtml = defaultsStr(f.btn_close_html, null);
/**
* Css for columns manager UI close link
* @type {String}
*/
this.btnCloseCssClass = defaultsStr(f.btn_close_css_class,
this.btnCssClass);
/**
* Extension's stylesheet filename
* @type {String}
*/
this.stylesheet = defaultsStr(f.stylesheet, 'colsVisibility.css');
/**
* Css for columns manager UI span
* @type {String}
*/
this.spanCssClass = defaultsStr(f.span_css_class, 'colVisSpan');
/**
* Css for columns manager UI main container
* @type {String}
*/
this.contCssClass = defaultsStr(f.cont_css_class, 'colVisCont');
/**
* Css for columns manager UI checklist (ul)
* @type {String}
*/
this.listCssClass = defaultsStr(cfg.list_css_class, 'cols_checklist');
/**
* Css for columns manager UI checklist item (li)
* @type {String}
*/
this.listItemCssClass = defaultsStr(cfg.checklist_item_css_class,
'cols_checklist_item');
/**
* Css for columns manager UI checklist item selected state (li)
* @type {String}
*/
this.listSlcItemCssClass = defaultsStr(
cfg.checklist_selected_item_css_class,
'cols_checklist_slc_item'
);
/**
* Text preceding the columns list, defaults to 'Hide' or 'Show'
* depending on tick mode (tick_to_hide option)
* @type {String}
*/
this.text = defaultsStr(f.text, this.tickToHide ? 'Hide: ' : 'Show: ');
/**
* List of columns indexes to be hidden at initialization
* @type {Array}
*/
this.atStart = defaultsArr(f.at_start, []);
/**
* Enable hover behaviour on columns manager button/link
* @type {Boolean}
*/
this.enableHover = Boolean(f.enable_hover);
/**
* Enable select all option, disabled by default
* @type {Boolean}
*/
this.enableTickAll = Boolean(f.enable_tick_all);
/**
* Text for select all option, defaults to 'Select all:'
* @type {String}
*/
this.tickAllText = defaultsStr(f.tick_all_text, 'Select all:');
/**
* Default position in toolbar ('left'|'center'|'right')
* @type {String}
*/
this.toolbarPosition = defaultsStr(f.toolbar_position, RIGHT);
/**
* List of indexes of hidden columns
* @private
*/
this.hiddenCols = [];
/**
* Bound mouseup wrapper
* @private
*/
this.boundMouseup = null;
/**
* Callback fired when the extension is initialized
* @type {Function}
*/
this.onLoaded = defaultsFn(f.on_loaded, EMPTY_FN);
/**
* Callback fired before the columns manager is opened
* @type {Function}
*/
this.onBeforeOpen = defaultsFn(f.on_before_open, EMPTY_FN);
/**
* Callback fired after the columns manager is opened
* @type {Function}
*/
this.onAfterOpen = defaultsFn(f.on_after_open, EMPTY_FN);
/**
* Callback fired before the columns manager is closed
* @type {Function}
*/
this.onBeforeClose = defaultsFn(f.on_before_close, EMPTY_FN);
/**
* Callback fired after the columns manager is closed
* @type {Function}
*/
this.onAfterClose = defaultsFn(f.on_after_close, EMPTY_FN);
/**
* Callback fired before a column is hidden
* @type {Function}
*/
this.onBeforeColHidden = defaultsFn(f.on_before_col_hidden, EMPTY_FN);
/**
* Callback fired after a column is hidden
* @type {Function}
*/
this.onAfterColHidden = defaultsFn(f.on_after_col_hidden, EMPTY_FN);
/**
* Callback fired before a column is displayed
* @type {Function}
*/
this.onBeforeColDisplayed = defaultsFn(f.on_before_col_displayed,
EMPTY_FN);
/**
* Callback fired after a column is displayed
* @type {Function}
*/
this.onAfterColDisplayed = defaultsFn(f.on_after_col_displayed,
EMPTY_FN);
//Grid layout support
if (tf.gridLayout) {
this.headersTbl = tf.feature('gridLayout').headTbl; //headers table
this.headersIndex = 0; //headers index
}
//Loads extension stylesheet
tf.import(f.name + 'Style', tf.getStylePath() + this.stylesheet, null,
'link');
this.enable();
}
/**
* Mouse-up event handler handling popup auto-close behaviour
* @private
*/
onMouseup(evt) {
let targetElm = targetEvt(evt);
while (targetElm && targetElm !== this.contEl
&& targetElm !== this.btnEl) {
targetElm = targetElm.parentNode;
}
if (targetElm !== this.contEl && targetElm !== this.btnEl) {
this.toggle();
}
return;
}
/**
* Toggle columns manager UI
*/
toggle() {
// ensure mouseup event handler is removed
removeEvt(root, 'mouseup', this.boundMouseup);
let contDisplay = this.contEl.style.display;
if (contDisplay !== 'inline') {
this.onBeforeOpen(this);
}
if (contDisplay === 'inline') {
this.onBeforeClose(this);
}
this.contEl.style.display = contDisplay === 'inline' ?
NONE : 'inline';
if (contDisplay !== 'inline') {
this.onAfterOpen(this);
addEvt(root, 'mouseup', this.boundMouseup);
}
if (contDisplay === 'inline') {
this.onAfterClose(this);
}
}
/**
* Check an item in columns manager UI
* @private
*/
checkItem(lbl) {
let li = lbl.parentNode;
if (!li || !lbl) {
return;
}
let isChecked = lbl.firstChild.checked;
let colIndex = lbl.firstChild.getAttribute('id').split('_')[1];
colIndex = parseInt(colIndex, 10);
if (isChecked) {
addClass(li, this.listSlcItemCssClass);
} else {
removeClass(li, this.listSlcItemCssClass);
}
let hide = false;
if ((this.tickToHide && isChecked) ||
(!this.tickToHide && !isChecked)) {
hide = true;
}
this.setHidden(colIndex, hide);
}
/**
* Initializes ColsVisibility instance
*/
init() {
if (this.initialized || !this.manager) {
return;
}
this.emitter.emit('initializing-extension', this,
!isNull(this.btnTgtId));
this.emitter.on(['hide-column'],
(tf, colIndex) => this.hideCol(colIndex));
this.buildBtn();
this.buildManager();
/** @inherited */
this.initialized = true;
this.boundMouseup = this.onMouseup.bind(this);
this.emitter.emit('columns-visibility-initialized', this.tf, this);
this.emitter.emit('extension-initialized', this);
// Hide columns at start at very end of initialization, do not move
// as order is important
this._hideAtStart();
}
/**
* Build main button UI
*/
buildBtn() {
if (this.btnEl) {
return;
}
let tf = this.tf;
let span = createElm('span');
span.className = this.spanCssClass;
// Container element (rdiv or custom element)
let targetEl = !this.btnTgtId ?
tf.feature('toolbar').container(this.toolbarPosition) :
elm(this.btnTgtId);
if (!this.btnTgtId) {
let firstChild = targetEl.firstChild;
firstChild.parentNode.insertBefore(span, firstChild);
} else {
targetEl.appendChild(span);
}
if (!this.btnHtml) {
let btn = createElm('a', ['href', 'javascript:;']);
btn.className = this.btnCssClass;
btn.title = this.desc;
btn.innerHTML = this.btnText;
span.appendChild(btn);
if (!this.enableHover) {
addEvt(btn, 'click', (evt) => this.toggle(evt));
} else {
addEvt(btn, 'mouseover', (evt) => this.toggle(evt));
}
} else { // Custom html
span.innerHTML = this.btnHtml;
let colVisEl = span.firstChild;
if (!this.enableHover) {
addEvt(colVisEl, 'click', (evt) => this.toggle(evt));
} else {
addEvt(colVisEl, 'mouseover', (evt) => this.toggle(evt));
}
}
this.spanEl = span;
this.btnEl = this.spanEl.firstChild;
this.onLoaded(this);
}
/**
* Build columns manager UI
*/
buildManager() {
let tf = this.tf;
let container = !this.contElTgtId ?
createElm('div') :
elm(this.contElTgtId);
container.className = this.contCssClass;
//Extension description
let extNameLabel = createElm('p');
extNameLabel.innerHTML = this.text;
container.appendChild(extNameLabel);
//Headers list
let ul = createElm('ul');
ul.className = this.listCssClass;
let tbl = this.headersTbl || tf.dom();
let headerIndex = this.headersTbl ?
this.headersIndex : tf.getHeadersRowIndex();
let headerRow = tbl.rows[headerIndex];
//Tick all option
if (this.enableTickAll) {
let li = createCheckItem('col__' + tf.id, this.tickAllText,
this.tickAllText);
addClass(li, this.listItemCssClass);
ul.appendChild(li);
li.check.checked = !this.tickToHide;
addEvt(li.check, 'click', () => {
for (let h = 0; h < headerRow.cells.length; h++) {
let itm = elm('col_' + h + '_' + tf.id);
if (itm && li.check.checked !== itm.checked) {
itm.click();
itm.checked = li.check.checked;
}
}
});
}
for (let i = 0; i < headerRow.cells.length; i++) {
let cell = headerRow.cells[i];
let cellText = this.headersText[i] || this._getHeaderText(cell);
let liElm = createCheckItem('col_' + i + '_' + tf.id, cellText,
cellText);
addClass(liElm, this.listItemCssClass);
if (!this.tickToHide) {
addClass(liElm, this.listSlcItemCssClass);
}
ul.appendChild(liElm);
if (!this.tickToHide) {
liElm.check.checked = true;
}
addEvt(liElm.check, 'click', (evt) => {
let elm = targetEvt(evt);
let lbl = elm.parentNode;
this.checkItem(lbl);
});
}
//separator
let p = createElm('p', ['align', 'center']);
let btn;
//Close link
if (!this.btnCloseHtml) {
btn = createElm('a', ['href', 'javascript:;']);
btn.className = this.btnCloseCssClass;
btn.innerHTML = this.btnCloseText;
addEvt(btn, 'click', (evt) => this.toggle(evt));
p.appendChild(btn);
} else {
p.innerHTML = this.btnCloseHtml;
btn = p.firstChild;
addEvt(btn, 'click', (evt) => this.toggle(evt));
}
container.appendChild(ul);
container.appendChild(p);
this.btnEl.parentNode.insertBefore(container, this.btnEl);
this.contEl = container;
}
/**
* Hide or show specified columns
* @param {Number} colIndex Column index
* @param {Boolean} hide Hide column if true or show if false
*/
setHidden(colIndex, hide) {
let tf = this.tf;
let tbl = tf.dom();
if (hide) {
this.onBeforeColHidden(this, colIndex);
} else {
this.onBeforeColDisplayed(this, colIndex);
}
this._hideElements(tbl, colIndex, hide);
if (this.headersTbl) {
this._hideElements(this.headersTbl, colIndex, hide);
}
let hiddenCols = this.hiddenCols;
let itemIndex = hiddenCols.indexOf(colIndex);
if (hide) {
if (itemIndex === -1) {
this.hiddenCols.push(colIndex);
}
} else {
if (itemIndex !== -1) {
this.hiddenCols.splice(itemIndex, 1);
}
}
if (hide) {
this.onAfterColHidden(this, colIndex);
this.emitter.emit('column-hidden', tf, this, colIndex,
this.hiddenCols);
} else {
this.onAfterColDisplayed(this, colIndex);
this.emitter.emit('column-shown', tf, this, colIndex,
this.hiddenCols);
}
}
/**
* Show specified column
* @param {Number} colIndex Column index
*/
showCol(colIndex) {
if (isUndef(colIndex) || !this.isColHidden(colIndex)) {
return;
}
if (this.manager && this.contEl) {
let itm = elm('col_' + colIndex + '_' + this.tf.id);
if (itm) {
itm.click();
}
} else {
this.setHidden(colIndex, false);
}
}
/**
* Hide specified column
* @param {Number} colIndex Column index
*/
hideCol(colIndex) {
if (isUndef(colIndex) || this.isColHidden(colIndex)) {
return;
}
if (this.manager && this.contEl) {
let itm = elm('col_' + colIndex + '_' + this.tf.id);
if (itm) {
itm.click();
}
} else {
this.setHidden(colIndex, true);
}
}
/**
* Determine if specified column is hidden
* @param {Number} colIndex Column index
*/
isColHidden(colIndex) {
if (this.hiddenCols.indexOf(colIndex) !== -1) {
return true;
}
return false;
}
/**
* Toggle visibility of specified column
* @param {Number} colIndex Column index
*/
toggleCol(colIndex) {
if (isUndef(colIndex) || this.isColHidden(colIndex)) {
this.showCol(colIndex);
} else {
this.hideCol(colIndex);
}
}
/**
* Return the indexes of the columns currently hidden
* @return {Array} column indexes
*/
getHiddenCols() {
return this.hiddenCols;
}
/**
* Remove the columns manager
*/
destroy() {
if (!this.initialized) {
return;
}
if (elm(this.contElTgtId)) {
elm(this.contElTgtId).innerHTML = '';
} else {
this.contEl.innerHTML = '';
removeElm(this.contEl);
this.contEl = null;
}
this.btnEl.innerHTML = '';
removeElm(this.btnEl);
this.btnEl = null;
this.emitter.off(['hide-column'],
(tf, colIndex) => this.hideCol(colIndex));
this.boundMouseup = null;
this.initialized = false;
}
_getHeaderText(cell) {
if (!cell.hasChildNodes) {
return '';
}
for (let i = 0; i < cell.childNodes.length; i++) {
let n = cell.childNodes[i];
if (n.nodeType === 3) {
return n.nodeValue;
} else if (n.nodeType === 1) {
if (n.id && n.id.indexOf('popUp') !== -1) {
continue;
} else {
return getText(n);
}
}
continue;
}
return '';
}
_hideElements(tbl, colIdx, hide) {
this._hideCells(tbl, colIdx, hide);
this._hideCol(tbl, colIdx, hide);
}
_hideCells(tbl, colIdx, hide) {
for (let i = 0; i < tbl.rows.length; i++) {
let row = tbl.rows[i];
let cell = row.cells[colIdx];
if (cell) {
cell.style.display = hide ? NONE : '';
}
}
}
_hideCol(tbl, colIdx, hide) {
let colElms = tag(this.tf.dom(), 'col');
if (colElms.length === 0) {
return;
}
colElms[colIdx].style.display = hide ? NONE : '';
}
_hideAtStart() {
this.atStart.forEach((colIdx) => {
this.hideCol(colIdx);
});
}
}