define([ 'dojo/_base/declare', 'dojo/dom-construct', 'dojo/has', 'dojo/on', '../util/misc', 'dojo/i18n!./nls/columnHider' ], function (declare, domConstruct, has, listen, miscUtil, i18n) { /* * Column Hider plugin for dgrid * Originally contributed by TRT 2011-09-28 * * A dGrid plugin that attaches a menu to a dgrid, along with a way of opening it, * that will allow you to show and hide columns. A few caveats: * * 1. Menu placement is entirely based on CSS definitions. * 2. If you want columns initially hidden, you must add "hidden: true" to your * column definition. * 3. This implementation does NOT support ColumnSet, and has not been tested * with multi-subrow records. * 4. Column show/hide is controlled via straight up HTML checkboxes. If you * are looking for something more fancy, you'll probably need to use this * definition as a template to write your own plugin. * */ var activeGrid, // references grid for which the menu is currently open bodyListener; // references pausable event handler for body mousedown function getColumnIdFromCheckbox(cb, grid) { // Given one of the checkboxes from the hider menu, // return the id of the corresponding column. // (e.g. gridIDhere-hider-menu-check-colIDhere -> colIDhere) return cb.id.substr(grid.id.length + 18); } return declare(null, { // hiderMenuNode: DOMNode // The node for the menu to show/hide columns. hiderMenuNode: null, // hiderToggleNode: DOMNode // The node for the toggler to open the menu. hiderToggleNode: null, // i18nColumnHider: Object // This object contains all of the internationalized strings for // the ColumnHider extension as key/value pairs. i18nColumnHider: i18n, // _hiderMenuOpened: Boolean // Records the current open/closed state of the menu. _hiderMenuOpened: false, // _columnHiderRules: Object // Hash containing handles returned from addCssRule. _columnHiderRules: null, // _columnHiderCheckboxes: Object // Hash containing checkboxes generated for menu items. _columnHiderCheckboxes: null, _renderHiderMenuEntries: function () { // summary: // Iterates over subRows for the sake of adding items to the // column hider menu. var subRows = this.subRows, first = true, srLength, cLength, sr, c; delete this._columnHiderFirstCheckbox; for (sr = 0, srLength = subRows.length; sr < srLength; sr++) { for (c = 0, cLength = subRows[sr].length; c < cLength; c++) { this._renderHiderMenuEntry(subRows[sr][c]); if (first) { first = false; this._columnHiderFirstCheckbox = this._columnHiderCheckboxes[subRows[sr][c].id]; } } } }, _renderHiderMenuEntry: function (col) { var id = col.id, replacedId = miscUtil.escapeCssIdentifier(id, '-'), div, checkId, checkbox, label; if (col.hidden) { // Hide the column (reset first to avoid short-circuiting logic) col.hidden = false; this._hideColumn(id); col.hidden = true; } // Allow cols to opt out of the hider (e.g. for selector column). if (col.unhidable) { return; } // Create the checkbox and label for each column selector. div = domConstruct.create('div', { className: 'dgrid-hider-menu-row' }); checkId = this.domNode.id + '-hider-menu-check-' + replacedId; checkbox = this._columnHiderCheckboxes[id] = domConstruct.create('input', { className: 'dgrid-hider-menu-check hider-menu-check-' + replacedId, id: checkId, type: 'checkbox' }, div); label = domConstruct.create('label', { className: 'dgrid-hider-menu-label hider-menu-label-' + replacedId, 'for': checkId }, div); label.appendChild(document.createTextNode(col.label || col.field || '')); this.hiderMenuNode.appendChild(div); if (!col.hidden) { // Hidden state is false; checkbox should be initially checked. // (Need to do this after adding to DOM to avoid IE6 clobbering it.) checkbox.checked = true; } }, renderHeader: function () { var grid = this, hiderMenuNode = this.hiderMenuNode, hiderToggleNode = this.hiderToggleNode, id, scrollbarWidth, hiderNodeScale, hiderNodeTranslate; function stopPropagation(event) { event.stopPropagation(); } this.inherited(arguments); if (!hiderMenuNode) { // First run // Assume that if this plugin is used, then columns are hidable. // Create the toggle node. hiderToggleNode = this.hiderToggleNode = domConstruct.create('div', { 'aria-label': this.i18nColumnHider.popupTriggerLabel, className: 'ui-icon dgrid-hider-toggle', type: 'button' }, this.domNode); // The ColumnHider icon is 16 x 16 pixels. Presumably, when it was created that size worked in all // browsers. Hopefully any browsers (or updates) introduced since then that reduce the scrollbar width // also include support for scaling with CSS transforms. scrollbarWidth = this.bodyNode.offsetWidth - this.bodyNode.clientWidth; if (scrollbarWidth < 16 && scrollbarWidth > 0) { hiderNodeScale = scrollbarWidth / 16; hiderNodeTranslate = (16 - scrollbarWidth) / 2; hiderToggleNode.style.transform = 'scale(' + (hiderNodeScale) + ') translate(' + hiderNodeTranslate + 'px)'; } this._listeners.push(listen(hiderToggleNode, 'click', function (e) { grid._toggleColumnHiderMenu(e); })); // Create the column list, with checkboxes. hiderMenuNode = this.hiderMenuNode = domConstruct.create('div', { 'aria-label': this.i18nColumnHider.popupLabel, className: 'dgrid-hider-menu', id: this.id + '-hider-menu', role: 'dialog' }); this._listeners.push(listen(hiderMenuNode, 'keyup', function (e) { var charOrCode = e.charCode || e.keyCode; if (charOrCode === /*ESCAPE*/ 27) { grid._toggleColumnHiderMenu(e); hiderToggleNode.focus(); } })); // Make sure our menu is initially hidden, then attach to the document. hiderMenuNode.style.display = 'none'; this.domNode.appendChild(hiderMenuNode); // Hook up delegated listener for modifications to checkboxes. this._listeners.push(listen(hiderMenuNode, '.dgrid-hider-menu-check:' + (has('ie') < 9 ? 'click' : 'change'), function (e) { grid._updateColumnHiddenState( getColumnIdFromCheckbox(e.target, grid), !e.target.checked); } )); // Stop click events from propagating from menu or trigger nodes, // so that we can simply track body clicks for hide without // having to drill-up to check. this._listeners.push( listen(hiderMenuNode, 'mousedown', stopPropagation), listen(hiderToggleNode, 'mousedown', stopPropagation) ); // Hook up top-level mousedown listener if it hasn't been yet. if (!bodyListener) { bodyListener = listen.pausable(document, 'mousedown', function (e) { // If an event reaches this listener, the menu is open, // but a click occurred outside, so close the dropdown. activeGrid && activeGrid._toggleColumnHiderMenu(e); }); bodyListener.pause(); // pause initially; will resume when menu opens } } else { // subsequent run // Remove active rules, and clear out the menu (to be repopulated). for (id in this._columnHiderRules) { this._columnHiderRules[id].remove(); } hiderMenuNode.innerHTML = ''; } this._columnHiderCheckboxes = {}; this._columnHiderRules = {}; // Populate menu with checkboxes/labels based on current columns. this._renderHiderMenuEntries(); }, destroy: function () { this.inherited(arguments); // Remove any remaining rules applied to hidden columns. for (var id in this._columnHiderRules) { this._columnHiderRules[id].remove(); } }, left: function (cell, steps) { return this.right(cell, -steps); }, right: function (cell, steps) { if (!cell.element) { cell = this.cell(cell); } var nextCell = this.inherited(arguments), prevCell = cell; // Skip over hidden cells while (nextCell.column.hidden) { nextCell = this.inherited(arguments, [nextCell, steps > 0 ? 1 : -1]); if (prevCell.element === nextCell.element) { // No further visible cell found - return original return cell; } prevCell = nextCell; } return nextCell; }, isColumnHidden: function (id) { // summary: // Convenience method to determine current hidden state of a column return !!this._columnHiderRules[id]; }, _toggleColumnHiderMenu: function () { var hidden = this._hiderMenuOpened, // reflects hidden state after toggle hiderMenuNode = this.hiderMenuNode, domNode = this.domNode, firstCheckbox; // Show or hide the hider menu hiderMenuNode.style.display = (hidden ? 'none' : ''); // Adjust height of menu if (hidden) { // Clear the set size hiderMenuNode.style.height = ''; } else { // Adjust height of the menu if necessary // Why 12? Based on menu default paddings and border, we need // to adjust to be 12 pixels shorter. Given the infrequency of // this style changing, we're assuming it will remain this // static value of 12 for now, to avoid pulling in any sort of // computed styles. if (hiderMenuNode.offsetHeight > domNode.offsetHeight - 12) { hiderMenuNode.style.height = (domNode.offsetHeight - 12) + 'px'; } // focus on the first checkbox (firstCheckbox = this._columnHiderFirstCheckbox) && firstCheckbox.focus(); } // Pause or resume the listener for clicks outside the menu bodyListener[hidden ? 'pause' : 'resume'](); // Update activeGrid appropriately activeGrid = hidden ? null : this; // Toggle the instance property this._hiderMenuOpened = !hidden; }, _hideColumn: function (id) { // summary: // Hides the column indicated by the given id. // Use miscUtil function directly, since we clean these up ourselves anyway var grid = this, selectorPrefix = '#' + miscUtil.escapeCssIdentifier(this.domNode.id) + ' .dgrid-column-', tableRule; // used in IE8 code path if (this._columnHiderRules[id]) { return; } this._columnHiderRules[id] = miscUtil.addCssRule(selectorPrefix + miscUtil.escapeCssIdentifier(id, '-'), 'display: none;'); if (has('ie') === 8 || has('ie') === 10) { // Work around IE8 display issue and IE10 issue where // header/body cells get out of sync when ColumnResizer is also used tableRule = miscUtil.addCssRule('.dgrid-row-table', 'display: inline-table;'); window.setTimeout(function () { tableRule.remove(); grid.resize(); }, 0); } }, _showColumn: function (id) { // summary: // Shows the column indicated by the given id // (by removing the rule responsible for hiding it). if (this._columnHiderRules[id]) { this._columnHiderRules[id].remove(); delete this._columnHiderRules[id]; } }, _updateColumnHiddenState: function (id, hidden) { // summary: // Performs internal work for toggleColumnHiddenState; see the public // method for more information. this[hidden ? '_hideColumn' : '_showColumn'](id); // Update hidden state in actual column definition, // in case columns are re-rendered. this.columns[id].hidden = hidden; // Emit event to notify of column state change. listen.emit(this.domNode, 'dgrid-columnstatechange', { grid: this, column: this.columns[id], hidden: hidden, bubbles: true }); // Adjust the size of the header. this.resize(); }, toggleColumnHiddenState: function (id, hidden) { // summary: // Shows or hides the column with the given id. // id: String // ID of column to show/hide. // hide: Boolean? // If specified, explicitly sets the hidden state of the specified // column. If unspecified, toggles the column from the current state. if (typeof hidden === 'undefined') { hidden = !this._columnHiderRules[id]; } this._updateColumnHiddenState(id, hidden); // Since this can be called directly, re-sync the appropriate checkbox. if (this._columnHiderCheckboxes[id]) { this._columnHiderCheckboxes[id].checked = !hidden; } } }); });