/* * Drop-down cell processor for the table control. */ /* * TODO: implement the search */ +function ($) { "use strict"; // NAMESPACE CHECK // ============================ if ($.oc.table === undefined) throw new Error("The $.oc.table namespace is not defined. Make sure that the table.js script is loaded."); if ($.oc.table.processor === undefined) throw new Error("The $.oc.table.processor namespace is not defined. Make sure that the table.processor.base.js script is loaded."); // CLASS DEFINITION // ============================ var Base = $.oc.table.processor.base, BaseProto = Base.prototype var DropdownProcessor = function(tableObj, columnName, columnConfiguration) { // // State properties // this.itemListElement = null this.cachedOptionPromises = {} // Event handlers this.itemClickHandler = this.onItemClick.bind(this) this.itemKeyDownHandler = this.onItemKeyDown.bind(this) // // Parent constructor // Base.call(this, tableObj, columnName, columnConfiguration) } DropdownProcessor.prototype = Object.create(BaseProto) DropdownProcessor.prototype.constructor = DropdownProcessor DropdownProcessor.prototype.dispose = function() { this.unregisterListHandlers() this.itemClickHandler = null this.itemKeyDownHandler = null this.itemListElement = null this.cachedOptionPromises = null BaseProto.dispose.call(this) } DropdownProcessor.prototype.unregisterListHandlers = function() { if (this.itemListElement) { // This processor binds custom click handler to the item list, // the standard registerHandlers/unregisterHandlers functionality // can't be used here because the element belongs to the document // body, not to the table. this.itemListElement.removeEventListener('click', this.itemClickHandler) this.itemListElement.removeEventListener('keydown', this.itemKeyDownHandler) } } /* * Renders the cell in the normal (no edit) mode */ DropdownProcessor.prototype.renderCell = function(value, cellContentContainer) { var viewContainer = this.createViewContainer(cellContentContainer, '...') this.fetchOptions(cellContentContainer.parentNode, function renderCellFetchOptions(options) { if (options[value] !== undefined) viewContainer.textContent = options[value] cellContentContainer.setAttribute('tabindex', 0) }) } /* * This method is called when the cell managed by the processor * is focused (clicked or navigated with the keyboard). */ DropdownProcessor.prototype.onFocus = function(cellElement, isClick) { if (this.activeCell === cellElement) { this.showDropdown() return } this.activeCell = cellElement var cellContentContainer = this.getCellContentContainer(cellElement) this.buildEditor(cellElement, cellContentContainer, isClick) if (!isClick) cellContentContainer.focus() } /* * Forces the processor to hide the editor when the user navigates * away from the cell. Processors can update the sell value in this method. * Processors must clear the reference to the active cell in this method. */ DropdownProcessor.prototype.onUnfocus = function() { if (!this.activeCell) return this.unregisterListHandlers() this.hideDropdown() this.itemListElement = null this.activeCell = null } DropdownProcessor.prototype.buildEditor = function(cellElement, cellContentContainer, isClick) { // Create the select control var currentValue = this.tableObj.getCellValue(cellElement), containerPosition = this.getAbsolutePosition(cellContentContainer) self = this this.itemListElement = document.createElement('div') this.itemListElement.addEventListener('click', this.itemClickHandler) this.itemListElement.addEventListener('keydown', this.itemKeyDownHandler) this.itemListElement.setAttribute('class', 'table-control-dropdown-list') this.itemListElement.style.width = cellContentContainer.offsetWidth + 'px' this.itemListElement.style.left = containerPosition.left + 'px' this.itemListElement.style.top = containerPosition.top - 2 + cellContentContainer.offsetHeight + 'px' this.fetchOptions(cellElement, function renderCellFetchOptions(options) { var listElement = document.createElement('ul') for (var value in options) { var itemElement = document.createElement('li') itemElement.setAttribute('data-value', value) itemElement.textContent = options[value] itemElement.setAttribute('tabindex', 0) if (value == currentValue) itemElement.setAttribute('class', 'selected') listElement.appendChild(itemElement) } self.itemListElement.appendChild(listElement) if (isClick) self.showDropdown() self = null }) } /* * Hide the drop-down, but don't delete it. */ DropdownProcessor.prototype.hideDropdown = function() { if (this.itemListElement && this.activeCell && this.itemListElement.parentNode) { var cellContentContainer = this.getCellContentContainer(this.activeCell) cellContentContainer.setAttribute('data-dropdown-open', 'false') this.itemListElement.parentNode.removeChild(this.itemListElement) cellContentContainer.focus() } } DropdownProcessor.prototype.showDropdown = function() { if (this.itemListElement && this.itemListElement.parentNode !== document.body) { this.getCellContentContainer(this.activeCell).setAttribute('data-dropdown-open', 'true') document.body.appendChild(this.itemListElement) var activeItemElement = this.itemListElement.querySelector('ul li.selected') if (!activeItemElement) { activeItemElement = this.itemListElement.querySelector('ul li:first-child') if (activeItemElement) activeItemElement.setAttribute('class', 'selected') } if (activeItemElement) { window.setTimeout(function(){ activeItemElement.focus() }, 0) } } } DropdownProcessor.prototype.fetchOptions = function(cellElement, onSuccess) { if (this.columnConfiguration.options) { onSuccess(this.columnConfiguration.options) } else { // If options are not provided and not found in the cache, // request them from the server. For dependent drop-downs // the caching key contains the master column values. var row = cellElement.parentNode, cachingKey = this.createOptionsCachingKey(row), viewContainer = this.getViewContainer(cellElement) // Request options from the server. When the table widget builds, // multiple cells in the column could require loading the options. // The AJAX promises are cached here so that we have a single // request per caching key. viewContainer.setAttribute('class', 'loading') if (!this.cachedOptionPromises[cachingKey]) { var requestData = { column: this.columnName, rowData: this.tableObj.getRowData(row) }, handlerName = this.tableObj.getAlias()+'::onGetDropdownOptions' this.cachedOptionPromises[cachingKey] = this.tableObj.$el.request(handlerName, {data: requestData}) } this.cachedOptionPromises[cachingKey].done(function onDropDownLoadOptionsSuccess(data){ onSuccess(data.options) }).always(function onDropDownLoadOptionsAlways(){ viewContainer.setAttribute('class', '') }) } } DropdownProcessor.prototype.createOptionsCachingKey = function(row) { var cachingKey = 'non-dependent', dependsOn = this.columnConfiguration.dependsOn if (dependsOn) { if (typeof dependsOn == 'object') { for (var i = 0, len = dependsOn.length; i < len; i++ ) cachingKey += dependsOn[i] + this.tableObj.getRowCellValueByColumnName(row, dependsOn[i]) } else cachingKey = dependsOn + this.tableObj.getRowCellValueByColumnName(row, dependsOn) } return cachingKey } DropdownProcessor.prototype.getAbsolutePosition = function(element) { // TODO: use the foundation library var top = document.body.scrollTop, left = 0 do { top += element.offsetTop || 0; top -= element.scrollTop || 0; left += element.offsetLeft || 0; element = element.offsetParent; } while(element) return { top: top, left: left } } DropdownProcessor.prototype.updateCellFromSelectedItem = function(selectedItem) { this.tableObj.setCellValue(this.activeCell, selectedItem.getAttribute('data-value')) this.setViewContainerValue(this.activeCell, selectedItem.textContent) } DropdownProcessor.prototype.findSelectedItem = function() { if (this.itemListElement) return this.itemListElement.querySelector('ul li.selected') return null } DropdownProcessor.prototype.onItemClick = function(ev) { var target = this.tableObj.getEventTarget(ev) if (target.tagName == 'LI') { this.updateCellFromSelectedItem(target) var selected = this.findSelectedItem() if (selected) selected.setAttribute('class', '') target.setAttribute('class', 'selected') this.hideDropdown() } } DropdownProcessor.prototype.onItemKeyDown = function(ev) { if (!this.itemListElement) return if (ev.keyCode == 40 || ev.keyCode == 38) { // Up or down keys - find previous/next list item and select it var selected = this.findSelectedItem(), newSelectedItem = selected.nextElementSibling if (ev.keyCode == 38) newSelectedItem = selected.previousElementSibling if (newSelectedItem) { selected.setAttribute('class', '') newSelectedItem.setAttribute('class', 'selected') newSelectedItem.focus() } return } if (ev.keyCode == 13 || ev.keyCode == 32) { // Return or space keys - update the selected value and hide the editor this.updateCellFromSelectedItem(this.findSelectedItem()) this.hideDropdown() return } if (ev.keyCode == 9) { // Tab - update the selected value and pass control to the table navigation this.updateCellFromSelectedItem(this.findSelectedItem()) this.tableObj.navigation.navigateNext(ev) this.tableObj.stopEvent(ev) } if (ev.keyCode == 27) { // Esc - hide the drop-down this.hideDropdown() } } /* * Event handler for the keydown event. The table class calls this method * for all processors. */ DropdownProcessor.prototype.onKeyDown = function(ev) { if (ev.keyCode == 32) this.showDropdown() } /* * This method is called when a cell value in the row changes. */ DropdownProcessor.prototype.onRowValueChanged = function(columnName, cellElement) { // Determine if this drop-down depends on the changed column // and update the option list if necessary // TODO: setting drop-down values with table.setRowValues() is not implemented currently if (!this.columnConfiguration.dependsOn) return var dependsOnColumn = false, dependsOn = this.columnConfiguration.dependsOn if (typeof dependsOn == 'object') { for (var i = 0, len = dependsOn.length; i < len; i++ ) { if (dependsOn[i] == columnName) { dependsOnColumn = true break } } } else { dependsOnColumn = dependsOn == columnName } if (!dependsOnColumn) return var currentValue = this.tableObj.getCellValue(cellElement), viewContainer = this.getViewContainer(cellElement) this.fetchOptions(cellElement, function rowValueChangedFetchOptions(options) { var value = options[currentValue] !== undefined ? options[currentValue] : '...' viewContainer.textContent = value viewContainer = null }) } /* * Determines whether the specified element is some element created by the * processor. */ DropdownProcessor.prototype.elementBelongsToProcessor = function(element) { if (!this.itemListElement) return false return this.tableObj.parentContainsElement(this.itemListElement, element) } $.oc.table.processor.dropdown = DropdownProcessor; }(window.jQuery);