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.
7 argumentsReceived
: false,
13 * @param {Event} event
15 function handleMessage(event
) {
16 window
.removeEventListener("message", handleMessage
, false);
17 initialize(JSON
.parse(event
.data
));
18 global
.argumentsReceived
= true;
22 * @param {!Object} args
24 function initialize(args
) {
28 global
.picker
= new ListPicker(main
, args
);
31 function handleArgumentsTimeout() {
32 if (global
.argumentsReceived
)
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;
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
];
76 selectedOption
.scrollIntoView(false);
77 window
.removeEventListener("didHide", this._handleWindowDidHideBound
, false);
80 ListPicker
.prototype._handleWindowMessage = function(event
) {
82 if (window
.updateData
.type
=== "update") {
83 this._config
.baseStyle
= window
.updateData
.baseStyle
;
84 this._config
.children
= window
.updateData
.children
;
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")
102 window
.pagePopupController
.setValueAndClosePopup(0, this._selectElement
.value
);
105 ListPicker
.prototype._handleTouchStart = function(event
) {
106 if (this._trackingTouchId
!== null)
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)
133 var touch
= this._getTouchForId(event
.touches
, this._trackingTouchId
);
136 this._highlightOption(document
.elementFromPoint(touch
.clientX
, touch
.clientY
));
137 this._selectionSetByMouseHover
= false;
140 ListPicker
.prototype._handleWindowTouchEnd = function(event
) {
141 if (this._trackingTouchId
=== null)
143 var touch
= this._getTouchForId(event
.changedTouches
, this._trackingTouchId
);
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
)
160 ListPicker
.prototype._highlightOption = function(target
) {
161 if (target
.tagName
!== "OPTION" || target
.selected
)
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
187 setTimeout(function () {
188 window
.pagePopupController
.closePopup();
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.
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)
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
;
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;
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;
285 var inGroup
= parent
.tagName
=== "OPTGROUP";
286 var lastListIndex
= -1;
287 var limit
= Math
.max(this._config
.selectedIndex
, ListPicker
.DelayedLayoutThreshold
);
289 for (i
= 0; i
< config
.children
.length
; ++i
) {
290 if (!inGroup
&& lastListIndex
>= limit
)
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
]);
300 fragment
= document
.createDocumentFragment();
301 fragment
.appendChild(item
);
306 parent
.appendChild(fragment
);
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
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
)
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
)
343 var tagName
= "OPTION";
344 if (config
.type
=== "optgroup")
345 tagName
= "OPTGROUP";
346 else if (config
.type
=== "separator")
348 for (var i
= startIndex
; i
< parent
.children
.length
; i
++) {
349 var child
= parent
.children
[i
];
350 if (tagName
=== child
.tagName
) {
357 ListPicker
.prototype._createItemElement = function(config
) {
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");
368 ListPicker
.prototype._applyItemStyle = function(element
, styleConfig
) {
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
;
391 element
.title
= config
.title
;
393 element
.removeAttribute("title");
394 element
.disabled
= !!config
.disabled
395 if (config
.ariaLabel
)
396 element
.setAttribute("aria-label", config
.ariaLabel
);
398 element
.removeAttribute("aria-label");
399 element
.style
.webkitPaddingStart
= this._config
.paddingStart
+ "px";
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
);
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
);
428 window
.addEventListener("message", handleMessage
, false);
429 window
.setTimeout(handleArgumentsTimeout
, 1000);