Move parseFontFaceDescriptor to CSSPropertyParser.cpp
[chromium-blink-merge.git] / third_party / WebKit / Source / web / resources / listPicker.js
blob167ad3cd77cc1ceb90b253edb6253f6541488ac0
1 "use strict";
2 // Copyright (c) 2014 The Chromium Authors. All rights reserved.
3 // Use of this source code is governed by a BSD-style license that can be
4 // found in the LICENSE file.
6 var global = {
7 argumentsReceived: false,
8 params: null,
9 picker: null
12 /**
13 * @param {Event} event
15 function handleMessage(event) {
16 window.removeEventListener("message", handleMessage, false);
17 initialize(JSON.parse(event.data));
18 global.argumentsReceived = true;
21 /**
22 * @param {!Object} args
24 function initialize(args) {
25 global.params = args;
26 var main = $("main");
27 main.innerHTML = "";
28 global.picker = new ListPicker(main, args);
31 function handleArgumentsTimeout() {
32 if (global.argumentsReceived)
33 return;
34 initialize({});
37 /**
38 * @constructor
39 * @param {!Element} element
40 * @param {!Object} config
42 function ListPicker(element, config) {
43 Picker.call(this, element, config);
44 window.pagePopupController.selectFontsFromOwnerDocument(document);
45 this._selectElement = createElement("select");
46 this._selectElement.size = 20;
47 this._element.appendChild(this._selectElement);
48 this._delayedChildrenConfig = null;
49 this._delayedChildrenConfigIndex = 0;
50 this._layout();
51 this._selectElement.addEventListener("mouseup", this._handleMouseUp.bind(this), false);
52 this._selectElement.addEventListener("touchstart", this._handleTouchStart.bind(this), false);
53 this._selectElement.addEventListener("keydown", this._handleKeyDown.bind(this), false);
54 this._selectElement.addEventListener("change", this._handleChange.bind(this), false);
55 window.addEventListener("message", this._handleWindowMessage.bind(this), false);
56 window.addEventListener("mousemove", this._handleWindowMouseMove.bind(this), false);
57 this._handleWindowTouchMoveBound = this._handleWindowTouchMove.bind(this);
58 this._handleWindowTouchEndBound = this._handleWindowTouchEnd.bind(this);
59 this._handleTouchSelectModeScrollBound = this._handleTouchSelectModeScroll.bind(this);
60 this.lastMousePositionX = Infinity;
61 this.lastMousePositionY = Infinity;
62 this._selectionSetByMouseHover = false;
64 this._trackingTouchId = null;
66 this._handleWindowDidHide();
67 this._selectElement.focus();
68 this._selectElement.value = this._config.selectedIndex;
70 ListPicker.prototype = Object.create(Picker.prototype);
72 ListPicker.prototype._handleWindowDidHide = function() {
73 this._fixWindowSize();
74 var selectedOption = this._selectElement.options[this._selectElement.selectedIndex];
75 if (selectedOption)
76 selectedOption.scrollIntoView(false);
77 window.removeEventListener("didHide", this._handleWindowDidHideBound, false);
80 ListPicker.prototype._handleWindowMessage = function(event) {
81 eval(event.data);
82 if (window.updateData.type === "update") {
83 this._config.baseStyle = window.updateData.baseStyle;
84 this._config.children = window.updateData.children;
85 this._update();
87 delete window.updateData;
90 ListPicker.prototype._handleWindowMouseMove = function (event) {
91 this.lastMousePositionX = event.clientX;
92 this.lastMousePositionY = event.clientY;
93 this._highlightOption(event.target);
94 this._selectionSetByMouseHover = true;
95 // Prevent the select element from firing change events for mouse input.
96 event.preventDefault();
99 ListPicker.prototype._handleMouseUp = function(event) {
100 if (event.target.tagName !== "OPTION")
101 return;
102 window.pagePopupController.setValueAndClosePopup(0, this._selectElement.value);
105 ListPicker.prototype._handleTouchStart = function(event) {
106 if (this._trackingTouchId !== null)
107 return;
108 // Enter touch select mode. In touch select mode the highlight follows the
109 // finger and on touchend the highlighted item is selected.
110 var touch = event.touches[0];
111 this._trackingTouchId = touch.identifier;
112 this._highlightOption(touch.target);
113 this._selectionSetByMouseHover = false;
114 this._selectElement.addEventListener("scroll", this._handleTouchSelectModeScrollBound, false);
115 window.addEventListener("touchmove", this._handleWindowTouchMoveBound, false);
116 window.addEventListener("touchend", this._handleWindowTouchEndBound, false);
119 ListPicker.prototype._handleTouchSelectModeScroll = function(event) {
120 this._exitTouchSelectMode();
123 ListPicker.prototype._exitTouchSelectMode = function(event) {
124 this._trackingTouchId = null;
125 this._selectElement.removeEventListener("scroll", this._handleTouchSelectModeScrollBound, false);
126 window.removeEventListener("touchmove", this._handleWindowTouchMoveBound, false);
127 window.removeEventListener("touchend", this._handleWindowTouchEndBound, false);
130 ListPicker.prototype._handleWindowTouchMove = function(event) {
131 if (this._trackingTouchId === null)
132 return;
133 var touch = this._getTouchForId(event.touches, this._trackingTouchId);
134 if (!touch)
135 return;
136 this._highlightOption(document.elementFromPoint(touch.clientX, touch.clientY));
137 this._selectionSetByMouseHover = false;
140 ListPicker.prototype._handleWindowTouchEnd = function(event) {
141 if (this._trackingTouchId === null)
142 return;
143 var touch = this._getTouchForId(event.changedTouches, this._trackingTouchId);
144 if (!touch)
145 return;
146 var target = document.elementFromPoint(touch.clientX, touch.clientY)
147 if (target.tagName === "OPTION")
148 window.pagePopupController.setValueAndClosePopup(0, this._selectElement.value);
149 this._exitTouchSelectMode();
152 ListPicker.prototype._getTouchForId = function (touchList, id) {
153 for (var i = 0; i < touchList.length; i++) {
154 if (touchList[i].identifier === id)
155 return touchList[i];
157 return null;
160 ListPicker.prototype._highlightOption = function(target) {
161 if (target.tagName !== "OPTION" || target.selected)
162 return;
163 var savedScrollTop = this._selectElement.scrollTop;
164 // TODO(tkent): Updating HTMLOptionElement::selected is not efficient. We
165 // should optimize it, or use an alternative way.
166 target.selected = true;
167 this._selectElement.scrollTop = savedScrollTop;
170 ListPicker.prototype._handleChange = function(event) {
171 window.pagePopupController.setValue(this._selectElement.value);
172 this._selectionSetByMouseHover = false;
175 ListPicker.prototype._handleKeyDown = function(event) {
176 var key = event.keyIdentifier;
177 if (key === "U+001B") { // ESC
178 window.pagePopupController.closePopup();
179 event.preventDefault();
180 } else if (key === "U+0009" /* TAB */ || key === "Enter") {
181 window.pagePopupController.setValueAndClosePopup(0, this._selectElement.value);
182 event.preventDefault();
183 } else if (event.altKey && (key === "Down" || key === "Up")) {
184 // We need to add a delay here because, if we do it immediately the key
185 // press event will be handled by HTMLSelectElement and this popup will
186 // be reopened.
187 setTimeout(function () {
188 window.pagePopupController.closePopup();
189 }, 0);
190 event.preventDefault();
194 ListPicker.prototype._fixWindowSize = function() {
195 this._selectElement.style.height = "";
196 var maxHeight = this._selectElement.offsetHeight;
197 // heightOutsideOfContent should be matched to border widths of the listbox
198 // SELECT. See listPicker.css and html.css.
199 var heightOutsideOfContent = 2;
200 var noScrollHeight = Math.round(this._calculateScrollHeight() + heightOutsideOfContent);
201 var desiredWindowHeight = noScrollHeight;
202 var desiredWindowWidth = this._selectElement.offsetWidth;
203 var expectingScrollbar = false;
204 if (desiredWindowHeight > maxHeight) {
205 desiredWindowHeight = maxHeight;
206 // Setting overflow to auto does not increase width for the scrollbar
207 // so we need to do it manually.
208 desiredWindowWidth += getScrollbarWidth();
209 expectingScrollbar = true;
211 desiredWindowWidth = Math.max(this._config.anchorRectInScreen.width, desiredWindowWidth);
212 var windowRect = adjustWindowRect(desiredWindowWidth, desiredWindowHeight, this._selectElement.offsetWidth, 0);
213 // If the available screen space is smaller than maxHeight, we will get an unexpected scrollbar.
214 if (!expectingScrollbar && windowRect.height < noScrollHeight) {
215 desiredWindowWidth = windowRect.width + getScrollbarWidth();
216 windowRect = adjustWindowRect(desiredWindowWidth, windowRect.height, windowRect.width, windowRect.height);
218 this._selectElement.style.width = windowRect.width + "px";
219 this._selectElement.style.height = windowRect.height + "px";
220 this._element.style.height = windowRect.height + "px";
221 setWindowRect(windowRect);
224 ListPicker.prototype._calculateScrollHeight = function() {
225 // Element.scrollHeight returns an integer value but this calculate the
226 // actual fractional value.
227 var top = Infinity;
228 var bottom = -Infinity;
229 for (var i = 0; i < this._selectElement.children.length; i++) {
230 var rect = this._selectElement.children[i].getBoundingClientRect();
231 // Skip hidden elements.
232 if (rect.width === 0 && rect.height === 0)
233 continue;
234 top = Math.min(top, rect.top);
235 bottom = Math.max(bottom, rect.bottom);
237 return Math.max(bottom - top, 0);
240 ListPicker.prototype._listItemCount = function() {
241 return this._selectElement.querySelectorAll("option,optgroup,hr").length;
244 ListPicker.prototype._layout = function() {
245 if (this._config.isRTL)
246 this._element.classList.add("rtl");
247 this._selectElement.style.backgroundColor = this._config.baseStyle.backgroundColor;
248 this._selectElement.style.color = this._config.baseStyle.color;
249 this._selectElement.style.textTransform = this._config.baseStyle.textTransform;
250 this._selectElement.style.fontSize = this._config.baseStyle.fontSize + "px";
251 this._selectElement.style.fontFamily = this._config.baseStyle.fontFamily.join(",");
252 this._selectElement.style.fontStyle = this._config.baseStyle.fontStyle;
253 this._selectElement.style.fontVariant = this._config.baseStyle.fontVariant;
254 this._updateChildren(this._selectElement, this._config);
257 ListPicker.prototype._update = function() {
258 var scrollPosition = this._selectElement.scrollTop;
259 var oldValue = this._selectElement.value;
260 this._layout();
261 this._selectElement.value = this._config.selectedIndex;
262 this._selectElement.scrollTop = scrollPosition;
263 var optionUnderMouse = null;
264 if (this._selectionSetByMouseHover) {
265 var elementUnderMouse = document.elementFromPoint(this.lastMousePositionX, this.lastMousePositionY);
266 optionUnderMouse = elementUnderMouse && elementUnderMouse.closest("option");
268 if (optionUnderMouse)
269 optionUnderMouse.selected = true;
270 else
271 this._selectElement.value = oldValue;
272 this._selectElement.scrollTop = scrollPosition;
273 this.dispatchEvent("didUpdate");
276 ListPicker.DelayedLayoutThreshold = 1000;
279 * @param {!Element} parent Select element or optgroup element.
280 * @param {!Object} config
282 ListPicker.prototype._updateChildren = function(parent, config) {
283 var outOfDateIndex = 0;
284 var fragment = null;
285 var inGroup = parent.tagName === "OPTGROUP";
286 var lastListIndex = -1;
287 var limit = Math.max(this._config.selectedIndex, ListPicker.DelayedLayoutThreshold);
288 var i;
289 for (i = 0; i < config.children.length; ++i) {
290 if (!inGroup && lastListIndex >= limit)
291 break;
292 var childConfig = config.children[i];
293 var item = this._findReusableItem(parent, childConfig, outOfDateIndex) || this._createItemElement(childConfig);
294 this._configureItem(item, childConfig, inGroup);
295 lastListIndex = item.value ? Number(item.value) : -1;
296 if (outOfDateIndex < parent.children.length) {
297 parent.insertBefore(item, parent.children[outOfDateIndex]);
298 } else {
299 if (!fragment)
300 fragment = document.createDocumentFragment();
301 fragment.appendChild(item);
303 outOfDateIndex++;
305 if (fragment) {
306 parent.appendChild(fragment);
307 } else {
308 var unused = parent.children.length - outOfDateIndex;
309 for (var j = 0; j < unused; j++) {
310 parent.removeChild(parent.lastElementChild);
313 if (i < config.children.length) {
314 // We don't bind |config.children| and |i| to _updateChildrenLater
315 // because config.children can get invalid before _updateChildrenLater
316 // is called.
317 this._delayedChildrenConfig = config.children;
318 this._delayedChildrenConfigIndex = i;
319 // Needs some amount of delay to kick the first paint.
320 setTimeout(this._updateChildrenLater.bind(this), 100);
324 ListPicker.prototype._updateChildrenLater = function(timeStamp) {
325 if (!this._delayedChildrenConfig)
326 return;
327 var fragment = document.createDocumentFragment();
328 var startIndex = this._delayedChildrenConfigIndex;
329 for (; this._delayedChildrenConfigIndex < this._delayedChildrenConfig.length; ++this._delayedChildrenConfigIndex) {
330 var childConfig = this._delayedChildrenConfig[this._delayedChildrenConfigIndex];
331 var item = this._createItemElement(childConfig);
332 this._configureItem(item, childConfig, false);
333 fragment.appendChild(item);
335 this._selectElement.appendChild(fragment);
336 this._selectElement.classList.add("wrap");
337 this._delayedChildrenConfig = null;
340 ListPicker.prototype._findReusableItem = function(parent, config, startIndex) {
341 if (startIndex >= parent.children.length)
342 return null;
343 var tagName = "OPTION";
344 if (config.type === "optgroup")
345 tagName = "OPTGROUP";
346 else if (config.type === "separator")
347 tagName = "HR";
348 for (var i = startIndex; i < parent.children.length; i++) {
349 var child = parent.children[i];
350 if (tagName === child.tagName) {
351 return child;
354 return null;
357 ListPicker.prototype._createItemElement = function(config) {
358 var element;
359 if (!config.type || config.type === "option")
360 element = createElement("option");
361 else if (config.type === "optgroup")
362 element = createElement("optgroup");
363 else if (config.type === "separator")
364 element = createElement("hr");
365 return element;
368 ListPicker.prototype._applyItemStyle = function(element, styleConfig) {
369 if (!styleConfig)
370 return;
371 var style = element.style;
372 style.visibility = styleConfig.visibility ? styleConfig.visibility : "";
373 style.display = styleConfig.display ? styleConfig.display : "";
374 style.direction = styleConfig.direction ? styleConfig.direction : "";
375 style.unicodeBidi = styleConfig.unicodeBidi ? styleConfig.unicodeBidi : "";
376 style.color = styleConfig.color ? styleConfig.color : "";
377 style.backgroundColor = styleConfig.backgroundColor ? styleConfig.backgroundColor : "";
378 style.fontSize = styleConfig.fontSize ? styleConfig.fontSize + "px" : "";
379 style.fontWeight = styleConfig.fontWeight ? styleConfig.fontWeight : "";
380 style.fontFamily = styleConfig.fontFamily ? styleConfig.fontFamily.join(",") : "";
381 style.fontStyle = styleConfig.fontStyle ? styleConfig.fontStyle : "";
382 style.fontVariant = styleConfig.fontVariant ? styleConfig.fontVariant : "";
383 style.textTransform = styleConfig.textTransform ? styleConfig.textTransform : "";
386 ListPicker.prototype._configureItem = function(element, config, inGroup) {
387 if (!config.type || config.type === "option") {
388 element.label = config.label;
389 element.value = config.value;
390 if (config.title)
391 element.title = config.title;
392 else
393 element.removeAttribute("title");
394 element.disabled = !!config.disabled
395 if (config.ariaLabel)
396 element.setAttribute("aria-label", config.ariaLabel);
397 else
398 element.removeAttribute("aria-label");
399 element.style.webkitPaddingStart = this._config.paddingStart + "px";
400 if (inGroup) {
401 element.style.webkitMarginStart = (- this._config.paddingStart) + "px";
402 // Should be synchronized with padding-end in listPicker.css.
403 element.style.webkitMarginEnd = "-2px";
405 } else if (config.type === "optgroup") {
406 element.label = config.label;
407 element.title = config.title;
408 element.disabled = config.disabled;
409 element.setAttribute("aria-label", config.ariaLabel);
410 this._updateChildren(element, config);
411 element.style.webkitPaddingStart = this._config.paddingStart + "px";
412 } else if (config.type === "separator") {
413 element.title = config.title;
414 element.disabled = config.disabled;
415 element.setAttribute("aria-label", config.ariaLabel);
416 if (inGroup) {
417 element.style.webkitMarginStart = (- this._config.paddingStart) + "px";
418 // Should be synchronized with padding-end in listPicker.css.
419 element.style.webkitMarginEnd = "-2px";
422 this._applyItemStyle(element, config.style);
425 if (window.dialogArguments) {
426 initialize(dialogArguments);
427 } else {
428 window.addEventListener("message", handleMessage, false);
429 window.setTimeout(handleArgumentsTimeout, 1000);