Source: HTMLTableWrapperControl.js

/*
 * Copyright 2020 Martin F. Schlegel Jr. | MIT AND BSD-3-Clause
 */
 
// Virtual Interfaces

// CellInterpreter
/**
 * @interface CellInterpreter
 * @classdesc
 *
 * Object-based implementation of {@link HTMLTableWrapperControl~populateCellValues}.
 */
/**
 * Implementation of {@link HTMLTableWrapperControl~populateCellValues}. See the callback's documentation for further details.
 *
 * @function CellInterpreter#populateCellValues
 * @param {HTMLCellElement} cell Cell element whose values are to be retrieved.
 * @param {ColumnValueSet} values Values to populate.
 * @returns `true` to trigger default column value processing, otherwise {@link Nothing}.
 */

// Callbacks

// populateCellValues
/**
 * Optional callback for {@link HTMLTableWrapperControl} to customize how cell values are interpreted. Based upon the given `cell`,
 * should determine the individual cell value/values, and add them to the given {@link ColumnValueSet}. If `true` is returned
 * from a call to this function, default processing, as defined in {@link HTMLTableWrapperControl#getColumnValues} will be triggered.
 *
 * @callback HTMLTableWrapperControl~populateCellValues
 * @param {HTMLTableCellElement} cell Cell element whose values are to be retrieved.
 * @param {ColumnValueSet} values Values to populate.
 * @returns `true` to trigger default column value processing, otherwise {@link Nothing}.
 */



// Constructor
/**
 * @constructor
 * @implements ColumnControl
 * @param {number} columnIndex Index of the column this `HTMLTableWrapperControl` controls.
 * @param {HTMLTableWrapperListener} parent Parent {@link HTMLTableWrapperListener}.
 * @param {CellInterpreter|HTMLTableWrapperControl~populateCellValues} [cellInterpreter] 
 *   Optional interpreter for cell values to use with calls to {@link HTMLTableWrapperControl#getColumnValues}.
 * @classdesc
 *
 * The default {@link ColumnControl} used by {@link HTMLTableWrapperListener}. Defines a UI where an end-user
 * can select a column's type and sort order, and define filters based upon a custom, user-entered value as well as selection from a list of
 * individual cell values (similar to the column filtering dialogue in MS Excel). Uses a backing {@link ContextControl}.
 */
function HTMLTableWrapperControl(columnIndex, parent, cellInterpreter) {
    'use strict';
    
    this.columnIndex = columnIndex;
    
    /**
     * Backing {@link ContextControl}.
     *
     * @private
     * @type {ContextControl}
     */
    this.contextControl = new ContextControl();
    
    /**
     * Parent {@link HTMLTableWrapperListener}.
     *
     * @private
     * @type {HTMLTableWrapperListener}
     */
    this.parent = parent;
    
    if (cellInterpreter) {
        this.cellInterpreter = cellInterpreter;
    }
    
}


// Static Fields
/**
 * Class name added to {@link ContextControl} {@link ContextControl#getControlElement element}s when they are
 * being {@link ContextControl#event:create created} by instances of this class.
 *
 * @type {string}
 */
HTMLTableWrapperControl.controlClassName = 'data-table-column-options';

/**
 * Constant representing a column should have no sort order.
 *
 * @type {number}
 * @const
 */
HTMLTableWrapperControl.SORT_ORDER_NONE = 1;

/**
 * Constant representing a column should have ascending sort order.
 *
 * @type {number}
 * @const
 */
HTMLTableWrapperControl.SORT_ORDER_ASCENDING = 2;

/**
 * Constant representing a column should have descending sort order.
 *
 * @type {number}
 * @const
 */
HTMLTableWrapperControl.SORT_ORDER_DESCENDING = 3;


HTMLTableWrapperControl.CLICK_TARGETS_SELECTOR = 
        'input.column-type, input.sort-direction, input.filter-by-value-operator, input.filter-option-ignore-case, input[name="clear-filter-button"], input[name="select-all-cell-values"], input.column-value, .close-button';

/**
 * A 'mapping' object whose property values correspond to labels printed on the dialogue presented to the end-user.
 * 
 * By default, values are in en-US (United States English), however clients that require internationalization can redefine the properties
 * of this object on page initialization (or, at minimum before any `HTMLTableWrapperControl`s are created) for the desired locale. 
 * (Alternatively, a completely new object can be assigned, so long as it contains all the property names of the original).
 *
 * @type {object}
 */
HTMLTableWrapperControl.i18nStrings = {
    columnOptionsLabel: 'Column Type',
    sortOptionsLabel: 'Sort Order',
    
    inferDataType: 'Infer',
    textOnlyDataType: 'Text Only',
    
    noSortOrder: 'None',
    ascendingSortOrder: 'Ascending',
    descendingSortOrder: 'Descending',
    
    filterTypeEnteredValue: 'Filter By Entered Value',
    filterTypeCellValue: "Filter By Cell's Value",
    selectAll: 'Select All',
    
    filterOptionEquals: '=',
    filterOptionEqualsToolTip: 'Equals',
    filterOptionNotEquals: '!=',
    filterOptionNotEqualsToolTip: 'Not Equals',
    filterOptionLessThan: '<',
    filterOptionLessThanToolTip: 'Less Than',
    filterOptionGreaterThan: '>',
    filterOptionGreaterThanToolTip: 'Greater Than',
    filterOptionLessThanOrEqualTo: '<=',
    filterOptionLessThanOrEqualToToolTip: 'Less Than or Equal To',
    filterOptionGreaterThanOrEqualTo: '>=',
    filterOptionGreaterThanOrEqualToToolTip: 'Greater Than or Equal To',
    filterOptionContains: 'Contains',
    filterOptionIgnoreTextCase: 'Ignore Case',
    
    filterDescriptionEquals: 'Select cells whose value is equal to the entered value:',
    filterDescriptionNotEquals: 'Select cells whose value is not equal to the entered value:',
    filterDescriptionLessThan: 'Select cells whose value is less than the entered value:',
    filterDescriptionGreaterThan: 'Select cells whose value is greater than the entered value:',
    filterDescriptionLessThanOrEqualTo: 'Select cells whose value is less than or equal to the entered value:',
    filterDescriptionGreaterThanOrEqualTo: 'Select cells whose value is greater than or equal to the entered value:',
    filterDescriptionContains: 'Select cells whose value contain the entered value:',
    
    clearSearchFilters: 'Reset Column',
    
    toolTipCloseDialogue: 'Close'
};

/**
 * Counter for generating unique DOM ids.
 *
 * @type {number}
 * @private
 */
HTMLTableWrapperControl.idCounter = 0;


// Static Methods
/**
 * Utility function for generating a unique id prefix to use in generated dialogue content.
 *
 * @private
 */
HTMLTableWrapperControl.getIdBase = function () {
    'use strict';
    
    return 'dataTable_' + HTMLTableWrapperControl.idCounter++ + '_';
};

/**
 * Returns `true` if any input in the given set of `inputs` is not `checked`, otherwise `false`. Of note, whether each input
 * is of type checkbox, radio, etc. (i.e. an input where the `checked` attribute is appropriate) is not evaluated; the `checked`
 * attribute of each `input` is simply inspected.
 *
 * @param {MinimalList} inputs Inputs to inspect.
 * @returns `true` if at least one input in `inputs` has a `checked` attribute of `false`, otherwise `false`.
 */
HTMLTableWrapperControl.hasUnchecked = function (inputs) {
    'use strict';
    
    var i;
    
    for (i = 0; i < inputs.length; ++i) {
        if (!inputs[i].checked) {
            return true;
        }
    }
    
    return false;
};

/**
 * Sets the first `HTMLInputElement`'s `checked` attribute in the given set of `inputs` whose `value` is the given `value`.
 * Of note, whether each input is of type checkbox, radio, etc. (i.e. an input where the `checked` attribute is appropriate) 
 * is not evaluated; the `checked` attribute is simply set.
 *
 * @param {MinimalList} inputs Collection of inputs to process.
 * @param {string} value Value of the input whose checked attribute is to be set.
 */
HTMLTableWrapperControl.setChecked = function (inputs, value) {
    'use strict';
    
    var i, input;
    
    for (i = 0; i < inputs.length; ++i) {
        input = inputs[i];
        input.checked = input.value === value;
    }
};

/**
 * Utility function to check whether the given callback is a function itself (a {@link HTMLTableWrapperControl~populateCellValues}), or a
 * {@link CellInterpreter}. Throws a `TypeError` if neither.
 *
 * @private
 * @throws TypeError If `callback` is neither a {@link HTMLTableWrapperControl~populateCellValues} nor a {@link CellInterpreter}.
 */
HTMLTableWrapperControl.checkCellInterpreter = function (callback) {
    'use strict';
    
    if (typeof callback !== 'function' && typeof callback.populateCellValues !== 'function') {
        throw new TypeError('Callback must either define a populateCellValues function, or be a function itself.');
    }
};


// Instance fields.
HTMLTableWrapperControl.prototype.cellInterpreter = null;


// Instance Methods
HTMLTableWrapperControl.prototype.init = function () {
    'use strict';
    
    IE8Compatibility.addEventListener(this.contextControl, 'create', this, false);
};


HTMLTableWrapperControl.prototype.dispose = function () {
    'use strict';
    
    var contextControl, controlElement, clickTargets, i;
    
    contextControl = this.contextControl;
    
    controlElement = contextControl.getControlElement();
    if (controlElement) {
        IE8Compatibility.removeEventListener(controlElement.querySelector('input.filter-by-value-value'), 'keyup', this, false);
        
        clickTargets = controlElement.querySelectorAll(HTMLTableWrapperControl.CLICK_TARGETS_SELECTOR);
        for (i = 0; i < clickTargets.length; ++i) {
            IE8Compatibility.removeEventListener(clickTargets[i], 'click', this, false);
        }
    }
    
    IE8Compatibility.removeEventListener(contextControl, 'create', this, false);
    contextControl.dispose();
};


/**
 * Implementation of DOM `EventListener`.
 *
 * @param {Event} event Event being dispatched.
 */
HTMLTableWrapperControl.prototype.handleEvent = function (event) {
    'use strict';
    
    var target, sortOrder, operation, columnValues, value, checked, index, controlElement;
    
    target = IE8Compatibility.getEventTarget(event);
    
    switch (event.type) {
        case 'create':
            this.defineContent(target.getControlElement());
            break;
        case 'click':
            if (IE8Compatibility.hasClass(target, 'close-button')) {
                this.contextControl.close();
            } else if (IE8Compatibility.hasClass(target, 'filter-by-value-operator')) {
                IE8Compatibility.setTextContent(this.contextControl.getControlElement().querySelector('.filter-by-value-description'), this.getOperatorDescription());
                this.updateParent();
            } else if (IE8Compatibility.hasClass(target, 'column-value')) {
                controlElement = this.contextControl.getControlElement();
                controlElement.querySelector('input[name="select-all-cell-values"]').checked = !HTMLTableWrapperControl.hasUnchecked(controlElement.querySelectorAll('input.column-value'));
                this.updateParent();
            } else if (
                IE8Compatibility.hasClass(target, 'column-type')
                || IE8Compatibility.hasClass(target, 'sort-direction')
                || IE8Compatibility.hasClass(target, 'filter-option-ignore-case')
            ) {
                this.updateParent();
            } else {
                switch (target.name) {
                    case 'clear-filter-button':
                        this.reset();
                        this.updateParent();
                        break;

                    case 'select-all-cell-values':
                        this.selectAllColumnValues(target.checked);
                        this.updateParent();
                        break;
                }
                
            }
            break;
        
        case 'keyup':
            this.updateParent();
            break;
    }    
};

HTMLTableWrapperControl.prototype.open = function () {
    'use strict';
    
    this.contextControl.open(this.parent.getTableHeaderElement(this.columnIndex));
};

HTMLTableWrapperControl.prototype.close = function () {
    'use strict';
    
    this.contextControl.close();
};


/**
 * Resets the state of this `HTMLTableWrapperControl`; all fields will be reset to their inital values. Note, this
 * only updates the state of the user interface; if the parent table needs to be updated, {@link HTMLTableWrapperControl#updateParent}
 * must be called subsequently.
 */
HTMLTableWrapperControl.prototype.reset = function () {
    'use strict';
    
    var control, columnValueInputs, i;
    
    control = this.contextControl.getControlElement();
    
    HTMLTableWrapperControl.setChecked(control.querySelectorAll('input.column-type'), 'infer');
    HTMLTableWrapperControl.setChecked(control.querySelectorAll('input.sort-direction'), 'none');
    HTMLTableWrapperControl.setChecked(control.querySelectorAll('input.filter-by-value-operator'), 'contains');
    control.querySelector('input.filter-option-ignore-case').checked = true;
    control.querySelector('input.filter-by-value-value').value = '';
    
    
    columnValueInputs = control.querySelectorAll('input.column-value');
    for (i = 0; i < columnValueInputs.length; ++i) {
        columnValueInputs[i].checked = true;
    }
};

/**
 * {@link HTMLTableWrapperListener#processTable Updates} the parent table.
 */
HTMLTableWrapperControl.prototype.updateParent = function () {
    'use strict';
    
    this.parent.processTable();
    this.contextControl.position();
};

/**
 * Selects or deselects all individual cell values for filtering. Note, this
 * only updates the state of the cell value checkboxes; if the parent table needs to be updated {@link HTMLTableWrapperControl#updateParent}
 * must be called subsequently.
 *
 * @param {boolean} checked Whether to select or deselect all individual cell values.
 */
HTMLTableWrapperControl.prototype.selectAllColumnValues = function (checked) {
    'use strict';
    
    var columnValueInputs, columnValueInput, i;
    
    columnValueInputs = this.contextControl.getControlElement().querySelectorAll('input.column-value');

    for (i = 0; i < columnValueInputs.length; ++i) {
        columnValueInputs[i].checked = checked;
    }
    
};


HTMLTableWrapperControl.prototype.getFilterDescriptor = function () {
    'use strict';
    
    var controlElement, compareValue, columnValueInputs, i, selectedValues, columnValueInput;
    
    controlElement = this.contextControl.getControlElement();
    
    if (!controlElement) {
        return null;
    }
    
    compareValue = controlElement.querySelector('input.filter-by-value-value').value;
    
    columnValueInputs = controlElement.querySelectorAll('input.column-value');
    selectedValues = [];
    for (i = 0; i < columnValueInputs.length; ++i) {
        columnValueInput = columnValueInputs[i];
        if (columnValueInput.checked) {
            selectedValues.push(columnValueInput.value);
        }
    }
    
    if (!compareValue && selectedValues.length === columnValueInputs.length) {
        return null;
    }
    
    return new HTMLTableWrapperControl.ColumnValueFilter(this.columnIndex, this.getOperator(), compareValue, this.getColumnType(), selectedValues, this.cellInterpreter);
};

HTMLTableWrapperControl.prototype.getSortDescriptor = function () {
    'use strict';
    
    var sortOrder, columnType;
    
    if (!this.contextControl.getControlElement()) {
        return null;
    }
    
    sortOrder = this.getSortOrder();
    if (sortOrder === HTMLTableWrapperControl.SORT_ORDER_NONE) {
        return null;
    }
    
    columnType = this.getColumnType();
    
    return new SimpleSortDescriptor(this.columnIndex, sortOrder === HTMLTableWrapperControl.SORT_ORDER_DESCENDING, columnType);
};


/**
 * Returns and `Array` containting all the individual cell values of the column with which this `HTMLTableWrapperControl` is associated.
 * If this `HTMLTableWrapperControl` has a {@link CellInterpreter} or {@link HTMLTableWrapperControl~populateCellValues} callback
 * configured, it will be used to obtain the values of individual cells, otherwise, returns the unique set of each cell's trimmed
 * `textContent`. If any call to a defined interpreter returns `true`, the cell's trimmed `textContent` is also used.
 *
 * By default, the result is sorted prior to being returned, unless the `noSort` parameter is not {@link Nothing}.
 *
 * @param {boolean} [noSort=false] Whether to return the result unsorted.
 * @returns {Array} All the values within the column this `HTMLTableWrapperControl` controls.
 */
HTMLTableWrapperControl.prototype.getColumnValues = function (noSort) {
    'use strict';
    
    var rows, i, cell, values, result, columnIndex, callback, defaultProcessing, itr, itrVal;
    
    callback = this.cellInterpreter;
    if (callback) {
        HTMLTableWrapperControl.checkCellInterpreter(callback);
    }
    
    columnIndex = this.columnIndex;
    
    values = new ColumnValueSet();
    rows = this.parent.getDataTable().getRows(true);
    
    for (i = 0; i < rows.length; ++i) {
        cell = rows[i].cells[columnIndex];
        if (callback) {
            if (callback.populateCellValues) {
                defaultProcessing = callback.populateCellValues(cell, values);
            } else {
                defaultProcessing = callback(cell, values);
            }
        } else {
            defaultProcessing = true;
        }
        
        if (defaultProcessing) {
            values.add(IE8Compatibility.getTextContent(cell));
        }
        
    }
    
    result = [];
    itr = values.iterator();
    while (!(itrVal = itr.next()).done) {
        result.push(itrVal.value);
    }
    
    
    if (!noSort) {
        result.sort();
    }
    
    return result;
};



/**
 * Returns a combination of the {@link HTMLTableWrapperUtils}`.FILTER_OP_`* and `FILTER_FLAG_`* bitfields corresponding to
 * the current selected operator, or `null` if no operator is selected, or this control has yet not been opened.
 *
 * @returns {number}
 *   A combination of the {@link HTMLTableWrapperUtils} bitfields corresponding to the current selected operator, or `null`
 *   if no operator is selected, or this control has not yet been opened.
 */
HTMLTableWrapperControl.prototype.getOperator = function () {
    'use strict';
    
    var result;
    
    switch (this.getCheckedValue('input.filter-by-value-operator')) {
        case 'eq':
            result = HTMLTableWrapperUtils.FILTER_OP_EQUALS;
            break;
        case 'neq':
            result = HTMLTableWrapperUtils.FILTER_OP_NOT_EQUALS;
            break;
        case 'lt':
            result = HTMLTableWrapperUtils.FILTER_OP_LESS_THAN;
            break;
        case 'gt':
            result = HTMLTableWrapperUtils.FILTER_OP_GREATER_THAN;
            break;
        case 'lte':
            result = HTMLTableWrapperUtils.FILTER_OP_LESS_THAN | HTMLTableWrapperUtils.FILTER_OP_EQUALS;
            break;
        case 'gte':
            result = HTMLTableWrapperUtils.FILTER_OP_GREATER_THAN | HTMLTableWrapperUtils.FILTER_OP_EQUALS;
            break;
        case 'contains':
            result = HTMLTableWrapperUtils.FILTER_OP_CONTAINS;
            break;
        default:
            return null;
    }
    
    if (this.contextControl.getControlElement().querySelector('input.filter-option-ignore-case').checked) {
        result |= HTMLTableWrapperUtils.FILTER_FLAG_IGNORE_CASE; 
    }
    
    return result;
};

/**
 * Returns a description based upon the current selected operator, or `null` if no operator is selected, or this control has not
 * yet been opened.
 *
 * @returns {string} 
 *   A description based upon the current selected operator, or `null` if no operator is selected, or this control has not yet
 *   been opened.
 */
HTMLTableWrapperControl.prototype.getOperatorDescription = function () {
    'use strict';
    
    var i18nStrings = HTMLTableWrapperControl.i18nStrings;
    
    switch (this.getCheckedValue('input.filter-by-value-operator')) {
        case 'eq':
            return i18nStrings.filterDescriptionEquals;
        case 'neq':
            return i18nStrings.filterDescriptionNotEquals;
        case 'lt':
            return i18nStrings.filterDescriptionLessThan;
        case 'gt':
            return i18nStrings.filterDescriptionGreaterThan;
        case 'lte':
            return i18nStrings.filterDescriptionLessThanOrEqualTo;
        case 'gte':
            return i18nStrings.filterDescriptionGreaterThanOrEqualTo;
        case 'contains':
            return i18nStrings.filterDescriptionContains;
        default:
            return null;
    }
};

/**
 * Returns the {@link HTMLTableWrapperUtils}`.COLUMN_TYPE_`* constant corresponding to the current selected column type, or
 * `null` if no column type is selected, or this control has not yet been opened.
 *
 * @returns {number}
 *   The {@link HTMLTableWrapperUtils}`.COLUMN_TYPE_`* constant corresponding to the current selected column type, or
 *   `null` if no column type is selected, or this control has not yet been opened.
 */
HTMLTableWrapperControl.prototype.getColumnType = function () {
    'use strict';
    
    switch (this.getCheckedValue('input.column-type')) {
        case 'infer':
            return HTMLTableWrapperUtils.COLUMN_TYPE_INFER;
        case 'text':
            return HTMLTableWrapperUtils.COLUMN_TYPE_TEXT;
        default:
            return null;
    }
};


/**
 * Returns the `SORT_ORDER_`* constant defined on this class corresponding to the current selected sort order, or `null` if
 * no sort order is selected, or this control has not yet been opened.
 *
 * @returns {number}
 *   The `SORT_ORDER_`* constant defined on this class corresponding to the current selected sort order, or `null` if
 *   no sort order is selected, or this control has not yet been opened.
 */
HTMLTableWrapperControl.prototype.getSortOrder = function () {
    'use strict';
    
    switch (this.getCheckedValue('input.sort-direction')) {
        case 'none':
            return HTMLTableWrapperControl.SORT_ORDER_NONE;
        case 'ascending':
            return HTMLTableWrapperControl.SORT_ORDER_ASCENDING;
        case 'descending':
            return HTMLTableWrapperControl.SORT_ORDER_DESCENDING;
        default:
            return null;
    }
};


/**
 * Utility function to obtain the `value` of the first `checked` input element within this control's backing `HTMLElement` matching the given query
 * `selector`. The `querySelectorAll` function is ran on the backing element, and the result is iterated until the first element with a `checked`
 * attribute of `true` is encountered, in which case that element's `value` attribute is returned. If no `checked` element is found with the given
 * `selector`, or this control has not yet been opened, `null` is returned.
 *
 * @private
 * @param {string} selector Query selector string.
 * @return {string} 
 *   The `value` of the first `checked` element within this control matching the given `selector`, or `null` if no checked element is found, or this
 *   control has not yet been opened.
 */
HTMLTableWrapperControl.prototype.getCheckedValue = function (selector) {
    'use strict';
    
    var controlElement, inputs, input, i;
    
    controlElement = this.contextControl.getControlElement();
    if (!controlElement) {
        return null;
    }
    
    inputs = controlElement.querySelectorAll(selector);
    for (i = 0; i < inputs.length; ++i) {
        input = inputs[i];
        if (input.checked) {
            return input.value;
        }
    }
    
    return null;
};




/**
 * Defines the UI content on the given `container`, and registers this `HTMLTableWrapperControl` for the appropriate events on
 * generated elements. Only intended to be called once (in response to {@link ContextControl#event:create} events).
 *
 * @private
 * @param {HTMLElement} container Element upon which the content of this control is to be defined.
 */
HTMLTableWrapperControl.prototype.defineContent = function (container) {
    'use strict';
    
    var idBase, inferColumnTypeId, textOnlyColumnTypeId, i18nStrings, sortOptionAscendingId,
        sortOptionDescendingId, filterOptionEq, filterOptionNeq, filterOptionLt, filterOptionGt,
        filterOptionLte, filterOptionGte, filterOptionContains, filterOptionIgnoreCase, sortOptionNoneId,
        filterByValueInputId, builder, selectAllCells, clickTargets, i, columnValues, columnValue, id, 
        columnIndex, columnTypeName, sortOrderName, operatorName;
    
    i18nStrings = HTMLTableWrapperControl.i18nStrings;
    
    // Generate IDs.
    idBase = HTMLTableWrapperControl.getIdBase();
    
    inferColumnTypeId = idBase + 'inferColumnType';
    textOnlyColumnTypeId = idBase + 'textOnly';
    sortOptionAscendingId = idBase + 'sortDirectionAscending';
    sortOptionDescendingId = idBase + 'sortDirectionDescending';
    filterOptionEq = idBase + 'filterOptionEq';
    filterOptionNeq = idBase + 'filterOptionNeq';
    filterOptionLt = idBase + 'filterOptionLt';
    filterOptionGt = idBase + 'filterOptionGt';
    filterOptionLte = idBase + 'filterOptionLte';
    filterOptionGte = idBase + 'filterOptionGte';
    filterOptionContains = idBase + 'filterOptionContains';
    filterOptionIgnoreCase = idBase + 'filterOptionIgnoreCase';
    sortOptionNoneId = idBase + 'sortOptionNone';
    filterByValueInputId = idBase + 'filterByValue';
    selectAllCells = idBase + 'selectAllCells';
    
    columnTypeName = idBase + 'columnType';
    sortOrderName = idBase + 'sortOrder';
    operatorName = idBase + 'operator';
    
    columnIndex = this.columnIndex;
    columnValues = this.getColumnValues();
    
    IE8Compatibility.addClass(container, HTMLTableWrapperControl.controlClassName)
    
    // Compose content.
    builder = new XMLBuilder();
    
    builder.startTag('div').attribute('class', 'section title-bar')
        .startTag('span').attribute('class', 'column-title').closeTag(true)
        .startTag('span').attribute('class', 'control-buttons')
            .startTag('span').attribute('class', 'control-button close-button').attribute('title', i18nStrings.toolTipCloseDialogue).content('\u00D7').closeTag()
        .closeTag()
    .closeTag()
    .startTag('div').attribute('class', 'section')
        .startTag('div').attribute('class', 'sub-section')
            .startTag('div').attribute('class', 'section-container column-options')
                .startTag('h5').attribute('class', 'section-title').content(i18nStrings.columnOptionsLabel).closeTag()
                .startTag('div').attribute('class', 'section-content')
                    .startTag('div').attribute('class', 'field')
                        .startTag('input').attribute('type', 'radio').attribute('name', columnTypeName).attribute('class', 'column-type').attribute('value', 'infer').attribute('checked').attribute('id', inferColumnTypeId).closeTag()
                        .startTag('label').attribute('for', inferColumnTypeId).content(i18nStrings.inferDataType).closeTag()
                    .closeTag()
                    .startTag('div').attribute('class', 'field')
                        .startTag('input').attribute('type', 'radio').attribute('name', columnTypeName).attribute('class', 'column-type').attribute('value', 'text').attribute('id', textOnlyColumnTypeId).closeTag()
                        .startTag('label').attribute('for', textOnlyColumnTypeId).content(i18nStrings.textOnlyDataType).closeTag()
                    .closeTag()
                .closeTag()
            .closeTag()
        .closeTag()
        .startTag('div').attribute('class', 'sub-section')
            .startTag('div').attribute('class', 'section-container sort-options')
                .startTag('h5').attribute('class', 'section-title').content(i18nStrings.sortOptionsLabel).closeTag()
                .startTag('div').attribute('class', 'sort-options section-content')
                    .startTag('div').attribute('class', 'field')
                        .startTag('input').attribute('type', 'radio').attribute('name', sortOrderName).attribute('class', 'sort-direction').attribute('value', 'none').attribute('checked').attribute('id', sortOptionNoneId).closeTag()
                        .startTag('label').attribute('for', sortOptionNoneId).content(i18nStrings.noSortOrder).closeTag()
                    .closeTag()
                    .startTag('div').attribute('class', 'field')
                        .startTag('input').attribute('type', 'radio').attribute('name', sortOrderName).attribute('class', 'sort-direction').attribute('value', 'ascending').attribute('id', sortOptionAscendingId).closeTag()
                        .startTag('label').attribute('for', sortOptionAscendingId).content(i18nStrings.ascendingSortOrder).closeTag()
                    .closeTag()
                    .startTag('div').attribute('class', 'field')
                        .startTag('input').attribute('type', 'radio').attribute('name', sortOrderName).attribute('class', 'sort-direction').attribute('value', 'descending').attribute('id', sortOptionDescendingId).closeTag()
                        .startTag('label').attribute('for', sortOptionDescendingId).content(i18nStrings.descendingSortOrder).closeTag()
                    .closeTag()
                .closeTag()
            .closeTag()
        .closeTag()
    .closeTag()
    .startTag('div').attribute('class', 'section')
        .startTag('h5').attribute('class', 'section-title').content(i18nStrings.filterTypeEnteredValue).closeTag()
        .startTag('div').attribute('class', 'sub-section')
            .startTag('div').attribute('class', 'filter-by-value-operators section-content')
                .startTag('div').attribute('class', 'field-group')
                    .startTag('div').attribute('class', 'field')
                        .startTag('input').attribute('type', 'radio').attribute('name', operatorName).attribute('class', 'filter-by-value-operator').attribute('value', 'eq').attribute('id', filterOptionEq).attribute('title', i18nStrings.filterOptionEqualsToolTip).closeTag()
                        .startTag('label').attribute('for', filterOptionEq).attribute('title', i18nStrings.filterOptionEqualsToolTip).content(i18nStrings.filterOptionEquals).closeTag()
                    .closeTag()
                    .startTag('div').attribute('class', 'field')
                        .startTag('input').attribute('type', 'radio').attribute('name', operatorName).attribute('class', 'filter-by-value-operator').attribute('value', 'neq').attribute('id', filterOptionNeq).attribute('title', i18nStrings.filterOptionNotEqualsToolTip).closeTag()
                        .startTag('label').attribute('for', filterOptionNeq).attribute('title', i18nStrings.filterOptionNotEqualsToolTip).content(i18nStrings.filterOptionNotEquals).closeTag()
                    .closeTag()
                .closeTag()
                .startTag('div').attribute('class', 'field-group')
                    .startTag('div').attribute('class', 'field')
                        .startTag('input').attribute('type', 'radio').attribute('name', operatorName).attribute('class', 'filter-by-value-operator').attribute('value', 'lt').attribute('id', filterOptionLt).attribute('title', i18nStrings.filterOptionLessThanToolTip).closeTag()
                        .startTag('label').attribute('for', filterOptionLt).attribute('title', i18nStrings.filterOptionLessThanToolTip).content(i18nStrings.filterOptionLessThan).closeTag()
                    .closeTag()
                    .startTag('div').attribute('class', 'field')
                        .startTag('input').attribute('type', 'radio').attribute('name', operatorName).attribute('class', 'filter-by-value-operator').attribute('value', 'gt').attribute('id', filterOptionGt).attribute('title', i18nStrings.filterOptionGreaterThanToolTip).closeTag()
                        .startTag('label').attribute('for', filterOptionGt).attribute('title', i18nStrings.filterOptionGreaterThanToolTip).content(i18nStrings.filterOptionGreaterThan).closeTag()
                    .closeTag()
                .closeTag()
                .startTag('div').attribute('class', 'field-group')
                    .startTag('div').attribute('class', 'field')
                        .startTag('input').attribute('type', 'radio').attribute('name', operatorName).attribute('class', 'filter-by-value-operator').attribute('value', 'lte').attribute('id', filterOptionLte).attribute('title', i18nStrings.filterOptionLessThanOrEqualToToolTip).closeTag()
                        .startTag('label').attribute('for', filterOptionLte).attribute('title', i18nStrings.filterOptionLessThanOrEqualToToolTip).content(i18nStrings.filterOptionLessThanOrEqualTo).closeTag()
                    .closeTag()
                    .startTag('div').attribute('class', 'field')
                        .startTag('input').attribute('type', 'radio').attribute('name', operatorName).attribute('class', 'filter-by-value-operator').attribute('value', 'gte').attribute('id', filterOptionGte).attribute('title', i18nStrings.filterOptionGreaterThanOrEqualToToolTip).closeTag()
                        .startTag('label').attribute('for', filterOptionGte).attribute('title', i18nStrings.filterOptionGreaterThanOrEqualToToolTip).content(i18nStrings.filterOptionGreaterThanOrEqualTo).closeTag()
                    .closeTag()
                .closeTag()
                .startTag('div').attribute('class', 'field')
                    .startTag('input').attribute('type', 'radio').attribute('name', operatorName).attribute('class', 'filter-by-value-operator').attribute('value', 'contains').attribute('id', filterOptionContains).attribute('checked').closeTag()
                    .startTag('label').attribute('for', filterOptionContains).content(i18nStrings.filterOptionContains).closeTag()
                .closeTag()
                .startTag('div').attribute('class', 'field')
                    .startTag('input').attribute('type', 'checkbox').attribute('class', 'filter-option-ignore-case').attribute('id', filterOptionIgnoreCase).attribute('checked').closeTag()
                    .startTag('label').attribute('for', filterOptionIgnoreCase).content(i18nStrings.filterOptionIgnoreTextCase).closeTag()
                .closeTag()
            .closeTag()
        .closeTag()
        .startTag('div').attribute('class', 'sub-section')
            .startTag('div').attribute('class', 'section-content')
                .startTag('div').attribute('class', 'field clear-filter-field')
                    .startTag('input').attribute('type', 'button').attribute('name', 'clear-filter-button').attribute('value', i18nStrings.clearSearchFilters).closeTag()
                .closeTag()
                .startTag('label').attribute('class', 'filter-by-value-description').attribute('for', filterByValueInputId).content(i18nStrings.filterDescriptionContains).closeTag()
            .closeTag()
        .closeTag()
    .closeTag()
    .startTag('div').attribute('class', 'section filter-by-value-container')
        .startTag('input').attribute('class', 'filter-by-value-value').attribute('id', filterByValueInputId).closeTag()
    .closeTag()
    .startTag('div').attribute('class', 'section filter-by-cell-values-container')
        .startTag('h5').attribute('class', 'section-title').content(i18nStrings.filterTypeCellValue).closeTag()
        .startTag('div').attribute('class', 'field')
            .startTag('input').attribute('type', 'checkbox').attribute('name', 'select-all-cell-values').attribute('id', selectAllCells).attribute('checked').closeTag()
            .startTag('label').attribute('for', selectAllCells).content(i18nStrings.selectAll).closeTag()
        .closeTag()
        .startTag('ul').attribute('class', 'filter-by-cell-values');
        
        for (i = 0; i < columnValues.length; ++i) {
            columnValue = columnValues[i];
            id = idBase + 'columnValue_' + i;
            
            builder.startTag('li').attribute('class', 'field')
                .startTag('input').attribute('type', 'checkbox').attribute('checked').attribute('value', columnValue).attribute('id', id).attribute('class', 'column-value').closeTag()
                .startTag('label').attribute('for', id).content(columnValue).closeTag()
            .closeTag();
        }
        
        builder.closeTag(true)
    .closeTag();
    
    // Define content.
    container.insertAdjacentHTML('afterbegin', builder.toString());
    
    
    // Register events.
    IE8Compatibility.addEventListener(container.querySelector('input.filter-by-value-value'), 'keyup', this, false);
    
    clickTargets = container.querySelectorAll(HTMLTableWrapperControl.CLICK_TARGETS_SELECTOR);
    for (i = 0; i < clickTargets.length; ++i) {
        IE8Compatibility.addEventListener(clickTargets[i], 'click', this, false);
    }
    
};




/**
 *
 * @constructor
 * @implements FilterDescriptor
 * @param {number} columnIndex Column index of the parent {@link HTMLTableWrapperControl}.
 * @param {number} operator {@link HTMLTableWrapperUtils}`.FILTER_OP_`* and `FILTER_FLAG_*` combination representing the current selected operator.
 * @param {string} compareValue User-entered filter value.
 * @param {number} columnType {@link HTMLTableWrapperUtils}`.COLUMN_TYPE_`* constant representing the current selected column type.
 * @param {Array} selectedValues A list of values currently selected in the Excel-like filter portion of the parent control.
 * @param {(CellInterpreter|HTMLTableWrapperControl~populateCellValues)} [cellInterpreter=null] {@link CellInterpreter} or callback of the parent {@link HTMLTableWrapperControl}.
 * @private
 * @classdesc
 *
 * The {@link FilterDescriptor} implementation returned by {@link HTMLTableWrapperControl#getFilterDescriptor}. Determines whether individual cell values should be filtered
 * based upon the given `operator`, `compareValue`, `selectedValues` and `columnType`. Individual cell values are determined with the given `cellInterpreter` if present. If no value
 * is given for `cellInterpreter`, each cell's value is its trimmed `textContent`.
 */
HTMLTableWrapperControl.ColumnValueFilter = function (columnIndex, operator, compareValue, columnType, selectedValues, cellInterpreter) {
    'use strict';
    
    this.columnIndex = columnIndex;
    
    /**
     * {@link HTMLTableWrapperUtils}`.FILTER_OP_`* and `FILTER_FLAG_*` combination representing the current selected operator.
     *
     * @private
     * @type {number}
     */
    this.operator = operator;
    
    /**
     * User-entered filter value.
     *
     * @private
     * @type {string}
     */
    this.compareValue = compareValue;
    
    /**
     * {@link HTMLTableWrapperUtils}`.COLUMN_TYPE_`* constant representing the current selected column type.
     *
     * @private
     * @type {number}
     */
    this.columnType = columnType;
    
    /**
     * A list of values currently selected in the Excel-like filter portion of the parent control.
     *
     * @private
     * @type {Array}
     */
    this.selectedValues = selectedValues;
    
    if (cellInterpreter) {
        HTMLTableWrapperControl.checkCellInterpreter(cellInterpreter);
        this.cellInterpreter = cellInterpreter;
        this.currentCellCache = new ColumnValueSet();
    }
};

/**
 * {@link CellInterpreter} or callback of the parent {@link HTMLTableWrapperControl}.
 *
 * @private
 * @type {(CellInterpreter|HTMLTableWrapperControl~populateCellValues)}
 */
HTMLTableWrapperControl.ColumnValueFilter.prototype.cellInterpreter = null;

/**
 * `ColumnValueSet` to use with the configured {@link HTMLTableWrapperControl.ColumnValueFilter#cellInterpreter}, if present. (Prevents the need to create a new
 * `ColumnValueSet` on each call to {@link HTMLTableWrapperControl.ColumnValueFilter#include} when filtering). This property is `null` if no `cellInterpreter`
 * is configured.
 *
 * @private
 * @type {ColumnValueSet}
 */
HTMLTableWrapperControl.ColumnValueFilter.prototype.currentCellCache = null;


HTMLTableWrapperControl.ColumnValueFilter.prototype.include = function (cell) {
    'use strict';
    
    var cellInterpreter, currentCellCache, itr, itrVal, defaultProcessing;
    
    cellInterpreter = this.cellInterpreter;
    
    if (cellInterpreter) {
        currentCellCache = this.currentCellCache;
        if (cellInterpreter.populateCellValues) {
            defaultProcessing = cellInterpreter.populateCellValues(cell, currentCellCache)
        } else {
            defaultProcessing = cellInterpreter(cell, currentCellCache);
        }
        
        if (defaultProcessing) {
            currentCellCache.add(IE8Compatibility.getTextContent(cell));
        }
        
        itr = currentCellCache.iterator();
        while (!(itrVal = itr.next()).done) {
            if (this.shouldInclude(itrVal.value)) {
                currentCellCache.clear();
                return true;
            }
        }
        
        currentCellCache.clear();
        return false;
    } else {
        return this.shouldInclude(IE8Compatibility.getTextContent(cell).trim());
    }
    
};


/**
 * Utility function to test whether an individual cell value qualifies it for inclusion in a filtering operation.
 *
 * @private
 * @param {string} cellValue Cell value to test.
 * @returns {boolean} `false` if the cell containing the given `cellValue` should be filtered, otherwise `true`.
 */
HTMLTableWrapperControl.ColumnValueFilter.prototype.shouldInclude = function (cellValue) {
    'use strict';

    var simple, list;
    
    simple = HTMLTableWrapperUtils.shouldInclude(cellValue, this.operator, this.compareValue, this.columnType);
    list = this.selectedValues.indexOf(cellValue) !== -1;
    
    return simple && list;
};