Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Source / web / resources / suggestionPicker.js
blob7db37d1b532f0ce910be51f60d1bc1b2916cf3d0
1 "use strict";
2 /*
3  * Copyright (C) 2012 Google Inc. All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  * 1. Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
15  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17  * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
18  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
21  * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
26 /**
27  * @constructor
28  * @param {!Element} element
29  * @param {!Object} config
30  */
31 function SuggestionPicker(element, config) {
32     Picker.call(this, element, config);
33     this._isFocusByMouse = false;
34     this._containerElement = null;
35     this._setColors();
36     this._layout();
37     this._fixWindowSize();
38     this._handleBodyKeyDownBound = this._handleBodyKeyDown.bind(this);
39     document.body.addEventListener("keydown", this._handleBodyKeyDownBound);
40     this._element.addEventListener("mouseout", this._handleMouseOut.bind(this), false);
42 SuggestionPicker.prototype = Object.create(Picker.prototype);
44 SuggestionPicker.NumberOfVisibleEntries = 20;
46 // An entry needs to be at least this many pixels visible for it to be a visible entry.
47 SuggestionPicker.VisibleEntryThresholdHeight = 4;
49 SuggestionPicker.ActionNames = {
50     OpenCalendarPicker: "openCalendarPicker"
53 SuggestionPicker.ListEntryClass = "suggestion-list-entry";
55 SuggestionPicker.validateConfig = function(config) {
56     if (config.showOtherDateEntry && !config.otherDateLabel)
57         return "No otherDateLabel.";
58     if (config.suggestionHighlightColor && !config.suggestionHighlightColor)
59         return "No suggestionHighlightColor.";
60     if (config.suggestionHighlightTextColor && !config.suggestionHighlightTextColor)
61         return "No suggestionHighlightTextColor.";
62     if (config.suggestionValues.length !== config.localizedSuggestionValues.length)
63         return "localizedSuggestionValues.length must equal suggestionValues.length.";
64     if (config.suggestionValues.length !== config.suggestionLabels.length)
65         return "suggestionLabels.length must equal suggestionValues.length.";
66     if (typeof config.inputWidth === "undefined")
67         return "No inputWidth.";
68     return null;
71 SuggestionPicker.prototype._setColors = function() {
72     var text = "." + SuggestionPicker.ListEntryClass + ":focus {\
73         background-color: " + this._config.suggestionHighlightColor + ";\
74         color: " + this._config.suggestionHighlightTextColor + "; }";
75     text += "." + SuggestionPicker.ListEntryClass + ":focus .label { color: " + this._config.suggestionHighlightTextColor + "; }";
76     document.head.appendChild(createElement("style", null, text));
79 SuggestionPicker.prototype.cleanup = function() {
80     document.body.removeEventListener("keydown", this._handleBodyKeyDownBound, false);
83 /**
84  * @param {!string} title
85  * @param {!string} label
86  * @param {!string} value
87  * @return {!Element}
88  */
89 SuggestionPicker.prototype._createSuggestionEntryElement = function(title, label, value) {
90     var entryElement = createElement("li", SuggestionPicker.ListEntryClass);
91     entryElement.tabIndex = 0;
92     entryElement.dataset.value = value;
93     var content = createElement("span", "content");
94     entryElement.appendChild(content);
95     var titleElement = createElement("span", "title", title);
96     content.appendChild(titleElement);
97     if (label) {
98         var labelElement = createElement("span", "label", label);
99         content.appendChild(labelElement);
100     }
101     entryElement.addEventListener("mouseover", this._handleEntryMouseOver.bind(this), false);
102     return entryElement;
106  * @param {!string} title
107  * @param {!string} actionName
108  * @return {!Element}
109  */
110 SuggestionPicker.prototype._createActionEntryElement = function(title, actionName) {
111     var entryElement = createElement("li", SuggestionPicker.ListEntryClass);
112     entryElement.tabIndex = 0;
113     entryElement.dataset.action = actionName;
114     var content = createElement("span", "content");
115     entryElement.appendChild(content);
116     var titleElement = createElement("span", "title", title);
117     content.appendChild(titleElement);
118     entryElement.addEventListener("mouseover", this._handleEntryMouseOver.bind(this), false);
119     return entryElement;
123 * @return {!number}
125 SuggestionPicker.prototype._measureMaxContentWidth = function() {
126     // To measure the required width, we first set the class to "measuring-width" which
127     // left aligns all the content including label.
128     this._containerElement.classList.add("measuring-width");
129     var maxContentWidth = 0;
130     var contentElements = this._containerElement.getElementsByClassName("content");
131     for (var i=0; i < contentElements.length; ++i) {
132         maxContentWidth = Math.max(maxContentWidth, contentElements[i].offsetWidth);
133     }
134     this._containerElement.classList.remove("measuring-width");
135     return maxContentWidth;
138 SuggestionPicker.prototype._fixWindowSize = function() {
139     var ListBorder = 2;
140     var desiredWindowWidth = this._measureMaxContentWidth() + ListBorder;
141     if (typeof this._config.inputWidth === "number")
142         desiredWindowWidth = Math.max(this._config.inputWidth, desiredWindowWidth);
143     var totalHeight = ListBorder;
144     var maxHeight = 0;
145     var entryCount = 0;
146     for (var i = 0; i < this._containerElement.childNodes.length; ++i) {
147         var node = this._containerElement.childNodes[i];
148         if (node.classList.contains(SuggestionPicker.ListEntryClass))
149             entryCount++;
150         totalHeight += node.offsetHeight;
151         if (maxHeight === 0 && entryCount == SuggestionPicker.NumberOfVisibleEntries)
152             maxHeight = totalHeight;
153     }
154     var desiredWindowHeight = totalHeight;
155     if (maxHeight !== 0 && totalHeight > maxHeight) {
156         this._containerElement.style.maxHeight = (maxHeight - ListBorder) + "px";
157         desiredWindowWidth += getScrollbarWidth();
158         desiredWindowHeight = maxHeight;
159         this._containerElement.style.overflowY = "scroll";
160     }
162     var windowRect = adjustWindowRect(desiredWindowWidth, desiredWindowHeight, desiredWindowWidth, 0);
163     this._containerElement.style.height = (windowRect.height - ListBorder) + "px";
164     setWindowRect(windowRect);
167 SuggestionPicker.prototype._layout = function() {
168     if (this._config.isRTL)
169         this._element.classList.add("rtl");
170     if (this._config.isLocaleRTL)
171         this._element.classList.add("locale-rtl");
172     this._containerElement = createElement("ul", "suggestion-list");
173     this._containerElement.addEventListener("click", this._handleEntryClick.bind(this), false);
174     for (var i = 0; i < this._config.suggestionValues.length; ++i) {
175         this._containerElement.appendChild(this._createSuggestionEntryElement(this._config.localizedSuggestionValues[i], this._config.suggestionLabels[i], this._config.suggestionValues[i]));
176     }
177     if (this._config.showOtherDateEntry) {
178         // Add separator
179         var separator = createElement("div", "separator");
180         this._containerElement.appendChild(separator);
182         // Add "Other..." entry
183         var otherEntry = this._createActionEntryElement(this._config.otherDateLabel, SuggestionPicker.ActionNames.OpenCalendarPicker);
184         this._containerElement.appendChild(otherEntry);
185     }
186     this._element.appendChild(this._containerElement);
190  * @param {!Element} entry
191  */
192 SuggestionPicker.prototype.selectEntry = function(entry) {
193     if (typeof entry.dataset.value !== "undefined") {
194         this.submitValue(entry.dataset.value);
195     } else if (entry.dataset.action === SuggestionPicker.ActionNames.OpenCalendarPicker) {
196         window.addEventListener("didHide", SuggestionPicker._handleWindowDidHide, false);
197         hideWindow();
198     }
201 SuggestionPicker._handleWindowDidHide = function() {
202     openCalendarPicker();
203     window.removeEventListener("didHide", SuggestionPicker._handleWindowDidHide);
207  * @param {!Event} event
208  */
209 SuggestionPicker.prototype._handleEntryClick = function(event) {
210     var entry = enclosingNodeOrSelfWithClass(event.target, SuggestionPicker.ListEntryClass);
211     if (!entry)
212         return;
213     this.selectEntry(entry);
214     event.preventDefault();
218  * @return {?Element}
219  */
220 SuggestionPicker.prototype._findFirstVisibleEntry = function() {
221     var scrollTop = this._containerElement.scrollTop;
222     var childNodes = this._containerElement.childNodes;
223     for (var i = 0; i < childNodes.length; ++i) {
224         var node = childNodes[i];
225         if (node.nodeType !== Node.ELEMENT_NODE || !node.classList.contains(SuggestionPicker.ListEntryClass))
226             continue;
227         if (node.offsetTop + node.offsetHeight - scrollTop > SuggestionPicker.VisibleEntryThresholdHeight)
228             return node;
229     }
230     return null;
234  * @return {?Element}
235  */
236 SuggestionPicker.prototype._findLastVisibleEntry = function() {
237     var scrollBottom = this._containerElement.scrollTop + this._containerElement.offsetHeight;
238     var childNodes = this._containerElement.childNodes;
239     for (var i = childNodes.length - 1; i >= 0; --i){
240         var node = childNodes[i];
241         if (node.nodeType !== Node.ELEMENT_NODE || !node.classList.contains(SuggestionPicker.ListEntryClass))
242             continue;
243         if (scrollBottom - node.offsetTop > SuggestionPicker.VisibleEntryThresholdHeight)
244             return node;
245     }
246     return null;
250  * @param {!Event} event
251  */
252 SuggestionPicker.prototype._handleBodyKeyDown = function(event) {
253     var eventHandled = false;
254     var key = event.keyIdentifier;
255     if (key === "U+001B") { // ESC
256         this.handleCancel();
257         eventHandled = true;
258     } else if (key == "Up") {
259         if (document.activeElement && document.activeElement.classList.contains(SuggestionPicker.ListEntryClass)) {
260             for (var node = document.activeElement.previousElementSibling; node; node = node.previousElementSibling) {
261                 if (node.classList.contains(SuggestionPicker.ListEntryClass)) {
262                     this._isFocusByMouse = false;
263                     node.focus();
264                     break;
265                 }
266             }
267         } else {
268             this._element.querySelector("." + SuggestionPicker.ListEntryClass + ":last-child").focus();
269         }
270         eventHandled = true;
271     } else if (key == "Down") {
272         if (document.activeElement && document.activeElement.classList.contains(SuggestionPicker.ListEntryClass)) {
273             for (var node = document.activeElement.nextElementSibling; node; node = node.nextElementSibling) {
274                 if (node.classList.contains(SuggestionPicker.ListEntryClass)) {
275                     this._isFocusByMouse = false;
276                     node.focus();
277                     break;
278                 }
279             }
280         } else {
281             this._element.querySelector("." + SuggestionPicker.ListEntryClass + ":first-child").focus();
282         }
283         eventHandled = true;
284     } else if (key === "Enter") {
285         this.selectEntry(document.activeElement);
286         eventHandled = true;
287     } else if (key === "PageUp") {
288         this._containerElement.scrollTop -= this._containerElement.clientHeight;
289         // Scrolling causes mouseover event to be called and that tries to move the focus too.
290         // To prevent flickering we won't focus if the current focus was caused by the mouse.
291         if (!this._isFocusByMouse)
292             this._findFirstVisibleEntry().focus();
293         eventHandled = true;
294     } else if (key === "PageDown") {
295         this._containerElement.scrollTop += this._containerElement.clientHeight;
296         if (!this._isFocusByMouse)
297             this._findLastVisibleEntry().focus();
298         eventHandled = true;
299     }
300     if (eventHandled)
301         event.preventDefault();
305  * @param {!Event} event
306  */
307 SuggestionPicker.prototype._handleEntryMouseOver = function(event) {
308     var entry = enclosingNodeOrSelfWithClass(event.target, SuggestionPicker.ListEntryClass);
309     if (!entry)
310         return;
311     this._isFocusByMouse = true;
312     entry.focus();
313     event.preventDefault();
317  * @param {!Event} event
318  */
319 SuggestionPicker.prototype._handleMouseOut = function(event) {
320     if (!document.activeElement.classList.contains(SuggestionPicker.ListEntryClass))
321         return;
322     this._isFocusByMouse = false;
323     document.activeElement.blur();
324     event.preventDefault();