| "use strict"; |
| /* |
| * Copyright (C) 2012 Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON |
| * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
| * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| /** |
| * @constructor |
| * @param {!Element} element |
| * @param {!Object} config |
| */ |
| function SuggestionPicker(element, config) { |
| Picker.call(this, element, config); |
| this._isFocusByMouse = false; |
| this._containerElement = null; |
| this._setColors(); |
| this._layout(); |
| this._fixWindowSize(); |
| this._handleBodyKeyDownBound = this._handleBodyKeyDown.bind(this); |
| document.body.addEventListener("keydown", this._handleBodyKeyDownBound); |
| this._element.addEventListener("mouseout", this._handleMouseOut.bind(this), false); |
| }; |
| SuggestionPicker.prototype = Object.create(Picker.prototype); |
| |
| SuggestionPicker.NumberOfVisibleEntries = 20; |
| |
| // An entry needs to be at least this many pixels visible for it to be a visible entry. |
| SuggestionPicker.VisibleEntryThresholdHeight = 4; |
| |
| SuggestionPicker.ActionNames = { |
| OpenCalendarPicker: "openCalendarPicker" |
| }; |
| |
| SuggestionPicker.ListEntryClass = "suggestion-list-entry"; |
| |
| SuggestionPicker.validateConfig = function(config) { |
| if (config.showOtherDateEntry && !config.otherDateLabel) |
| return "No otherDateLabel."; |
| if (config.suggestionHighlightColor && !config.suggestionHighlightColor) |
| return "No suggestionHighlightColor."; |
| if (config.suggestionHighlightTextColor && !config.suggestionHighlightTextColor) |
| return "No suggestionHighlightTextColor."; |
| if (config.suggestionValues.length !== config.localizedSuggestionValues.length) |
| return "localizedSuggestionValues.length must equal suggestionValues.length."; |
| if (config.suggestionValues.length !== config.suggestionLabels.length) |
| return "suggestionLabels.length must equal suggestionValues.length."; |
| if (typeof config.inputWidth === "undefined") |
| return "No inputWidth."; |
| return null; |
| }; |
| |
| SuggestionPicker.prototype._setColors = function() { |
| var text = "." + SuggestionPicker.ListEntryClass + ":focus {\ |
| background-color: " + this._config.suggestionHighlightColor + ";\ |
| color: " + this._config.suggestionHighlightTextColor + "; }"; |
| text += "." + SuggestionPicker.ListEntryClass + ":focus .label { color: " + this._config.suggestionHighlightTextColor + "; }"; |
| document.head.appendChild(createElement("style", null, text)); |
| }; |
| |
| SuggestionPicker.prototype.cleanup = function() { |
| document.body.removeEventListener("keydown", this._handleBodyKeyDownBound, false); |
| }; |
| |
| /** |
| * @param {!string} title |
| * @param {!string} label |
| * @param {!string} value |
| * @return {!Element} |
| */ |
| SuggestionPicker.prototype._createSuggestionEntryElement = function(title, label, value) { |
| var entryElement = createElement("li", SuggestionPicker.ListEntryClass); |
| entryElement.tabIndex = 0; |
| entryElement.dataset.value = value; |
| var content = createElement("span", "content"); |
| entryElement.appendChild(content); |
| var titleElement = createElement("span", "title", title); |
| content.appendChild(titleElement); |
| if (label) { |
| var labelElement = createElement("span", "label", label); |
| content.appendChild(labelElement); |
| } |
| entryElement.addEventListener("mouseover", this._handleEntryMouseOver.bind(this), false); |
| return entryElement; |
| }; |
| |
| /** |
| * @param {!string} title |
| * @param {!string} actionName |
| * @return {!Element} |
| */ |
| SuggestionPicker.prototype._createActionEntryElement = function(title, actionName) { |
| var entryElement = createElement("li", SuggestionPicker.ListEntryClass); |
| entryElement.tabIndex = 0; |
| entryElement.dataset.action = actionName; |
| var content = createElement("span", "content"); |
| entryElement.appendChild(content); |
| var titleElement = createElement("span", "title", title); |
| content.appendChild(titleElement); |
| entryElement.addEventListener("mouseover", this._handleEntryMouseOver.bind(this), false); |
| return entryElement; |
| }; |
| |
| /** |
| * @return {!number} |
| */ |
| SuggestionPicker.prototype._measureMaxContentWidth = function() { |
| // To measure the required width, we first set the class to "measuring-width" which |
| // left aligns all the content including label. |
| this._containerElement.classList.add("measuring-width"); |
| var maxContentWidth = 0; |
| var contentElements = this._containerElement.getElementsByClassName("content"); |
| for (var i=0; i < contentElements.length; ++i) { |
| maxContentWidth = Math.max(maxContentWidth, contentElements[i].offsetWidth); |
| } |
| this._containerElement.classList.remove("measuring-width"); |
| return maxContentWidth; |
| }; |
| |
| SuggestionPicker.prototype._fixWindowSize = function() { |
| var ListBorder = 2; |
| var desiredWindowWidth = this._measureMaxContentWidth() + ListBorder; |
| if (typeof this._config.inputWidth === "number") |
| desiredWindowWidth = Math.max(this._config.inputWidth, desiredWindowWidth); |
| var totalHeight = ListBorder; |
| var maxHeight = 0; |
| var entryCount = 0; |
| for (var i = 0; i < this._containerElement.childNodes.length; ++i) { |
| var node = this._containerElement.childNodes[i]; |
| if (node.classList.contains(SuggestionPicker.ListEntryClass)) |
| entryCount++; |
| totalHeight += node.offsetHeight; |
| if (maxHeight === 0 && entryCount == SuggestionPicker.NumberOfVisibleEntries) |
| maxHeight = totalHeight; |
| } |
| var desiredWindowHeight = totalHeight; |
| if (maxHeight !== 0 && totalHeight > maxHeight) { |
| this._containerElement.style.maxHeight = (maxHeight - ListBorder) + "px"; |
| desiredWindowWidth += getScrollbarWidth(); |
| desiredWindowHeight = maxHeight; |
| this._containerElement.style.overflowY = "scroll"; |
| } |
| |
| var windowRect = adjustWindowRect(desiredWindowWidth, desiredWindowHeight, desiredWindowWidth, 0); |
| this._containerElement.style.height = (windowRect.height - ListBorder) + "px"; |
| setWindowRect(windowRect); |
| }; |
| |
| SuggestionPicker.prototype._layout = function() { |
| if (this._config.isRTL) |
| this._element.classList.add("rtl"); |
| if (this._config.isLocaleRTL) |
| this._element.classList.add("locale-rtl"); |
| this._containerElement = createElement("ul", "suggestion-list"); |
| this._containerElement.addEventListener("click", this._handleEntryClick.bind(this), false); |
| for (var i = 0; i < this._config.suggestionValues.length; ++i) { |
| this._containerElement.appendChild(this._createSuggestionEntryElement(this._config.localizedSuggestionValues[i], this._config.suggestionLabels[i], this._config.suggestionValues[i])); |
| } |
| if (this._config.showOtherDateEntry) { |
| // Add separator |
| var separator = createElement("div", "separator"); |
| this._containerElement.appendChild(separator); |
| |
| // Add "Other..." entry |
| var otherEntry = this._createActionEntryElement(this._config.otherDateLabel, SuggestionPicker.ActionNames.OpenCalendarPicker); |
| this._containerElement.appendChild(otherEntry); |
| } |
| this._element.appendChild(this._containerElement); |
| }; |
| |
| /** |
| * @param {!Element} entry |
| */ |
| SuggestionPicker.prototype.selectEntry = function(entry) { |
| if (typeof entry.dataset.value !== "undefined") { |
| this.submitValue(entry.dataset.value); |
| } else if (entry.dataset.action === SuggestionPicker.ActionNames.OpenCalendarPicker) { |
| window.addEventListener("didHide", SuggestionPicker._handleWindowDidHide, false); |
| hideWindow(); |
| } |
| }; |
| |
| SuggestionPicker._handleWindowDidHide = function() { |
| openCalendarPicker(); |
| window.removeEventListener("didHide", SuggestionPicker._handleWindowDidHide); |
| }; |
| |
| /** |
| * @param {!Event} event |
| */ |
| SuggestionPicker.prototype._handleEntryClick = function(event) { |
| var entry = enclosingNodeOrSelfWithClass(event.target, SuggestionPicker.ListEntryClass); |
| if (!entry) |
| return; |
| this.selectEntry(entry); |
| event.preventDefault(); |
| }; |
| |
| /** |
| * @return {?Element} |
| */ |
| SuggestionPicker.prototype._findFirstVisibleEntry = function() { |
| var scrollTop = this._containerElement.scrollTop; |
| var childNodes = this._containerElement.childNodes; |
| for (var i = 0; i < childNodes.length; ++i) { |
| var node = childNodes[i]; |
| if (node.nodeType !== Node.ELEMENT_NODE || !node.classList.contains(SuggestionPicker.ListEntryClass)) |
| continue; |
| if (node.offsetTop + node.offsetHeight - scrollTop > SuggestionPicker.VisibleEntryThresholdHeight) |
| return node; |
| } |
| return null; |
| }; |
| |
| /** |
| * @return {?Element} |
| */ |
| SuggestionPicker.prototype._findLastVisibleEntry = function() { |
| var scrollBottom = this._containerElement.scrollTop + this._containerElement.offsetHeight; |
| var childNodes = this._containerElement.childNodes; |
| for (var i = childNodes.length - 1; i >= 0; --i){ |
| var node = childNodes[i]; |
| if (node.nodeType !== Node.ELEMENT_NODE || !node.classList.contains(SuggestionPicker.ListEntryClass)) |
| continue; |
| if (scrollBottom - node.offsetTop > SuggestionPicker.VisibleEntryThresholdHeight) |
| return node; |
| } |
| return null; |
| }; |
| |
| /** |
| * @param {!Event} event |
| */ |
| SuggestionPicker.prototype._handleBodyKeyDown = function(event) { |
| var eventHandled = false; |
| var key = event.keyIdentifier; |
| if (key === "U+001B") { // ESC |
| this.handleCancel(); |
| eventHandled = true; |
| } else if (key == "Up") { |
| if (document.activeElement && document.activeElement.classList.contains(SuggestionPicker.ListEntryClass)) { |
| for (var node = document.activeElement.previousElementSibling; node; node = node.previousElementSibling) { |
| if (node.classList.contains(SuggestionPicker.ListEntryClass)) { |
| this._isFocusByMouse = false; |
| node.focus(); |
| break; |
| } |
| } |
| } else { |
| this._element.querySelector("." + SuggestionPicker.ListEntryClass + ":last-child").focus(); |
| } |
| eventHandled = true; |
| } else if (key == "Down") { |
| if (document.activeElement && document.activeElement.classList.contains(SuggestionPicker.ListEntryClass)) { |
| for (var node = document.activeElement.nextElementSibling; node; node = node.nextElementSibling) { |
| if (node.classList.contains(SuggestionPicker.ListEntryClass)) { |
| this._isFocusByMouse = false; |
| node.focus(); |
| break; |
| } |
| } |
| } else { |
| this._element.querySelector("." + SuggestionPicker.ListEntryClass + ":first-child").focus(); |
| } |
| eventHandled = true; |
| } else if (key === "Enter") { |
| this.selectEntry(document.activeElement); |
| eventHandled = true; |
| } else if (key === "PageUp") { |
| this._containerElement.scrollTop -= this._containerElement.clientHeight; |
| // Scrolling causes mouseover event to be called and that tries to move the focus too. |
| // To prevent flickering we won't focus if the current focus was caused by the mouse. |
| if (!this._isFocusByMouse) |
| this._findFirstVisibleEntry().focus(); |
| eventHandled = true; |
| } else if (key === "PageDown") { |
| this._containerElement.scrollTop += this._containerElement.clientHeight; |
| if (!this._isFocusByMouse) |
| this._findLastVisibleEntry().focus(); |
| eventHandled = true; |
| } |
| if (eventHandled) |
| event.preventDefault(); |
| }; |
| |
| /** |
| * @param {!Event} event |
| */ |
| SuggestionPicker.prototype._handleEntryMouseOver = function(event) { |
| var entry = enclosingNodeOrSelfWithClass(event.target, SuggestionPicker.ListEntryClass); |
| if (!entry) |
| return; |
| this._isFocusByMouse = true; |
| entry.focus(); |
| event.preventDefault(); |
| }; |
| |
| /** |
| * @param {!Event} event |
| */ |
| SuggestionPicker.prototype._handleMouseOut = function(event) { |
| if (!document.activeElement.classList.contains(SuggestionPicker.ListEntryClass)) |
| return; |
| this._isFocusByMouse = false; |
| document.activeElement.blur(); |
| event.preventDefault(); |
| }; |