| "use strict"; |
| // Copyright (c) 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| var global = { |
| argumentsReceived: false, |
| params: null, |
| picker: null |
| }; |
| |
| /** |
| * @param {Event} event |
| */ |
| function handleMessage(event) { |
| window.removeEventListener("message", handleMessage, false); |
| initialize(JSON.parse(event.data)); |
| global.argumentsReceived = true; |
| } |
| |
| /** |
| * @param {!Object} args |
| */ |
| function initialize(args) { |
| global.params = args; |
| var main = $("main"); |
| main.innerHTML = ""; |
| global.picker = new ListPicker(main, args); |
| } |
| |
| function handleArgumentsTimeout() { |
| if (global.argumentsReceived) |
| return; |
| initialize({}); |
| } |
| |
| /** |
| * @constructor |
| * @param {!Element} element |
| * @param {!Object} config |
| */ |
| function ListPicker(element, config) { |
| Picker.call(this, element, config); |
| window.pagePopupController.selectFontsFromOwnerDocument(document); |
| this._selectElement = createElement("select"); |
| this._selectElement.size = 20; |
| this._element.appendChild(this._selectElement); |
| this._delayedChildrenConfig = null; |
| this._delayedChildrenConfigIndex = 0; |
| this._layout(); |
| this._selectElement.addEventListener("mouseup", this._handleMouseUp.bind(this), false); |
| this._selectElement.addEventListener("touchstart", this._handleTouchStart.bind(this), false); |
| this._selectElement.addEventListener("keydown", this._handleKeyDown.bind(this), false); |
| this._selectElement.addEventListener("change", this._handleChange.bind(this), false); |
| window.addEventListener("message", this._handleWindowMessage.bind(this), false); |
| window.addEventListener("mousemove", this._handleWindowMouseMove.bind(this), false); |
| this._handleWindowTouchMoveBound = this._handleWindowTouchMove.bind(this); |
| this._handleWindowTouchEndBound = this._handleWindowTouchEnd.bind(this); |
| this._handleTouchSelectModeScrollBound = this._handleTouchSelectModeScroll.bind(this); |
| this.lastMousePositionX = Infinity; |
| this.lastMousePositionY = Infinity; |
| this._selectionSetByMouseHover = false; |
| |
| this._trackingTouchId = null; |
| |
| this._handleWindowDidHide(); |
| this._selectElement.focus(); |
| this._selectElement.value = this._config.selectedIndex; |
| } |
| ListPicker.prototype = Object.create(Picker.prototype); |
| |
| ListPicker.prototype._handleWindowDidHide = function() { |
| this._fixWindowSize(); |
| var selectedOption = this._selectElement.options[this._selectElement.selectedIndex]; |
| if (selectedOption) |
| selectedOption.scrollIntoView(false); |
| window.removeEventListener("didHide", this._handleWindowDidHideBound, false); |
| }; |
| |
| ListPicker.prototype._handleWindowMessage = function(event) { |
| eval(event.data); |
| if (window.updateData.type === "update") { |
| this._config.baseStyle = window.updateData.baseStyle; |
| this._config.children = window.updateData.children; |
| this._update(); |
| } |
| delete window.updateData; |
| }; |
| |
| ListPicker.prototype._handleWindowMouseMove = function (event) { |
| this.lastMousePositionX = event.clientX; |
| this.lastMousePositionY = event.clientY; |
| this._highlightOption(event.target); |
| this._selectionSetByMouseHover = true; |
| // Prevent the select element from firing change events for mouse input. |
| event.preventDefault(); |
| }; |
| |
| ListPicker.prototype._handleMouseUp = function(event) { |
| if (event.target.tagName !== "OPTION") |
| return; |
| window.pagePopupController.setValueAndClosePopup(0, this._selectElement.value); |
| }; |
| |
| ListPicker.prototype._handleTouchStart = function(event) { |
| if (this._trackingTouchId !== null) |
| return; |
| // Enter touch select mode. In touch select mode the highlight follows the |
| // finger and on touchend the highlighted item is selected. |
| var touch = event.touches[0]; |
| this._trackingTouchId = touch.identifier; |
| this._highlightOption(touch.target); |
| this._selectionSetByMouseHover = false; |
| this._selectElement.addEventListener("scroll", this._handleTouchSelectModeScrollBound, false); |
| window.addEventListener("touchmove", this._handleWindowTouchMoveBound, false); |
| window.addEventListener("touchend", this._handleWindowTouchEndBound, false); |
| }; |
| |
| ListPicker.prototype._handleTouchSelectModeScroll = function(event) { |
| this._exitTouchSelectMode(); |
| }; |
| |
| ListPicker.prototype._exitTouchSelectMode = function(event) { |
| this._trackingTouchId = null; |
| this._selectElement.removeEventListener("scroll", this._handleTouchSelectModeScrollBound, false); |
| window.removeEventListener("touchmove", this._handleWindowTouchMoveBound, false); |
| window.removeEventListener("touchend", this._handleWindowTouchEndBound, false); |
| }; |
| |
| ListPicker.prototype._handleWindowTouchMove = function(event) { |
| if (this._trackingTouchId === null) |
| return; |
| var touch = this._getTouchForId(event.touches, this._trackingTouchId); |
| if (!touch) |
| return; |
| this._highlightOption(document.elementFromPoint(touch.clientX, touch.clientY)); |
| this._selectionSetByMouseHover = false; |
| }; |
| |
| ListPicker.prototype._handleWindowTouchEnd = function(event) { |
| if (this._trackingTouchId === null) |
| return; |
| var touch = this._getTouchForId(event.changedTouches, this._trackingTouchId); |
| if (!touch) |
| return; |
| var target = document.elementFromPoint(touch.clientX, touch.clientY) |
| if (target.tagName === "OPTION") |
| window.pagePopupController.setValueAndClosePopup(0, this._selectElement.value); |
| this._exitTouchSelectMode(); |
| }; |
| |
| ListPicker.prototype._getTouchForId = function (touchList, id) { |
| for (var i = 0; i < touchList.length; i++) { |
| if (touchList[i].identifier === id) |
| return touchList[i]; |
| } |
| return null; |
| }; |
| |
| ListPicker.prototype._highlightOption = function(target) { |
| if (target.tagName !== "OPTION" || target.selected) |
| return; |
| var savedScrollTop = this._selectElement.scrollTop; |
| // TODO(tkent): Updating HTMLOptionElement::selected is not efficient. We |
| // should optimize it, or use an alternative way. |
| target.selected = true; |
| this._selectElement.scrollTop = savedScrollTop; |
| }; |
| |
| ListPicker.prototype._handleChange = function(event) { |
| window.pagePopupController.setValue(this._selectElement.value); |
| this._selectionSetByMouseHover = false; |
| }; |
| |
| ListPicker.prototype._handleKeyDown = function(event) { |
| var key = event.keyIdentifier; |
| if (key === "U+001B") { // ESC |
| window.pagePopupController.closePopup(); |
| event.preventDefault(); |
| } else if (key === "U+0009" /* TAB */ || key === "Enter") { |
| window.pagePopupController.setValueAndClosePopup(0, this._selectElement.value); |
| event.preventDefault(); |
| } else if (event.altKey && (key === "Down" || key === "Up")) { |
| // We need to add a delay here because, if we do it immediately the key |
| // press event will be handled by HTMLSelectElement and this popup will |
| // be reopened. |
| setTimeout(function () { |
| window.pagePopupController.closePopup(); |
| }, 0); |
| event.preventDefault(); |
| } |
| }; |
| |
| ListPicker.prototype._fixWindowSize = function() { |
| this._selectElement.style.height = ""; |
| var maxHeight = this._selectElement.offsetHeight; |
| // heightOutsideOfContent should be matched to border widths of the listbox |
| // SELECT. See listPicker.css and html.css. |
| var heightOutsideOfContent = 2; |
| var noScrollHeight = Math.round(this._calculateScrollHeight() + heightOutsideOfContent); |
| var desiredWindowHeight = noScrollHeight; |
| var desiredWindowWidth = this._selectElement.offsetWidth; |
| var expectingScrollbar = false; |
| if (desiredWindowHeight > maxHeight) { |
| desiredWindowHeight = maxHeight; |
| // Setting overflow to auto does not increase width for the scrollbar |
| // so we need to do it manually. |
| desiredWindowWidth += getScrollbarWidth(); |
| expectingScrollbar = true; |
| } |
| desiredWindowWidth = Math.max(this._config.anchorRectInScreen.width, desiredWindowWidth); |
| var windowRect = adjustWindowRect(desiredWindowWidth, desiredWindowHeight, this._selectElement.offsetWidth, 0); |
| // If the available screen space is smaller than maxHeight, we will get an unexpected scrollbar. |
| if (!expectingScrollbar && windowRect.height < noScrollHeight) { |
| desiredWindowWidth = windowRect.width + getScrollbarWidth(); |
| windowRect = adjustWindowRect(desiredWindowWidth, windowRect.height, windowRect.width, windowRect.height); |
| } |
| this._selectElement.style.width = windowRect.width + "px"; |
| this._selectElement.style.height = windowRect.height + "px"; |
| this._element.style.height = windowRect.height + "px"; |
| setWindowRect(windowRect); |
| }; |
| |
| ListPicker.prototype._calculateScrollHeight = function() { |
| // Element.scrollHeight returns an integer value but this calculate the |
| // actual fractional value. |
| var top = Infinity; |
| var bottom = -Infinity; |
| for (var i = 0; i < this._selectElement.children.length; i++) { |
| var rect = this._selectElement.children[i].getBoundingClientRect(); |
| // Skip hidden elements. |
| if (rect.width === 0 && rect.height === 0) |
| continue; |
| top = Math.min(top, rect.top); |
| bottom = Math.max(bottom, rect.bottom); |
| } |
| return Math.max(bottom - top, 0); |
| }; |
| |
| ListPicker.prototype._listItemCount = function() { |
| return this._selectElement.querySelectorAll("option,optgroup,hr").length; |
| }; |
| |
| ListPicker.prototype._layout = function() { |
| if (this._config.isRTL) |
| this._element.classList.add("rtl"); |
| this._selectElement.style.backgroundColor = this._config.baseStyle.backgroundColor; |
| this._selectElement.style.color = this._config.baseStyle.color; |
| this._selectElement.style.textTransform = this._config.baseStyle.textTransform; |
| this._selectElement.style.fontSize = this._config.baseStyle.fontSize + "px"; |
| this._selectElement.style.fontFamily = this._config.baseStyle.fontFamily.join(","); |
| this._selectElement.style.fontStyle = this._config.baseStyle.fontStyle; |
| this._selectElement.style.fontVariant = this._config.baseStyle.fontVariant; |
| this._updateChildren(this._selectElement, this._config); |
| }; |
| |
| ListPicker.prototype._update = function() { |
| var scrollPosition = this._selectElement.scrollTop; |
| var oldValue = this._selectElement.value; |
| this._layout(); |
| this._selectElement.value = this._config.selectedIndex; |
| this._selectElement.scrollTop = scrollPosition; |
| var optionUnderMouse = null; |
| if (this._selectionSetByMouseHover) { |
| var elementUnderMouse = document.elementFromPoint(this.lastMousePositionX, this.lastMousePositionY); |
| optionUnderMouse = elementUnderMouse && elementUnderMouse.closest("option"); |
| } |
| if (optionUnderMouse) |
| optionUnderMouse.selected = true; |
| else |
| this._selectElement.value = oldValue; |
| this._selectElement.scrollTop = scrollPosition; |
| this.dispatchEvent("didUpdate"); |
| }; |
| |
| ListPicker.DelayedLayoutThreshold = 1000; |
| |
| /** |
| * @param {!Element} parent Select element or optgroup element. |
| * @param {!Object} config |
| */ |
| ListPicker.prototype._updateChildren = function(parent, config) { |
| var outOfDateIndex = 0; |
| var fragment = null; |
| var inGroup = parent.tagName === "OPTGROUP"; |
| var lastListIndex = -1; |
| var limit = Math.max(this._config.selectedIndex, ListPicker.DelayedLayoutThreshold); |
| var i; |
| for (i = 0; i < config.children.length; ++i) { |
| if (!inGroup && lastListIndex >= limit) |
| break; |
| var childConfig = config.children[i]; |
| var item = this._findReusableItem(parent, childConfig, outOfDateIndex) || this._createItemElement(childConfig); |
| this._configureItem(item, childConfig, inGroup); |
| lastListIndex = item.value ? Number(item.value) : -1; |
| if (outOfDateIndex < parent.children.length) { |
| parent.insertBefore(item, parent.children[outOfDateIndex]); |
| } else { |
| if (!fragment) |
| fragment = document.createDocumentFragment(); |
| fragment.appendChild(item); |
| } |
| outOfDateIndex++; |
| } |
| if (fragment) { |
| parent.appendChild(fragment); |
| } else { |
| var unused = parent.children.length - outOfDateIndex; |
| for (var j = 0; j < unused; j++) { |
| parent.removeChild(parent.lastElementChild); |
| } |
| } |
| if (i < config.children.length) { |
| // We don't bind |config.children| and |i| to _updateChildrenLater |
| // because config.children can get invalid before _updateChildrenLater |
| // is called. |
| this._delayedChildrenConfig = config.children; |
| this._delayedChildrenConfigIndex = i; |
| // Needs some amount of delay to kick the first paint. |
| setTimeout(this._updateChildrenLater.bind(this), 100); |
| } |
| }; |
| |
| ListPicker.prototype._updateChildrenLater = function(timeStamp) { |
| if (!this._delayedChildrenConfig) |
| return; |
| var fragment = document.createDocumentFragment(); |
| var startIndex = this._delayedChildrenConfigIndex; |
| for (; this._delayedChildrenConfigIndex < this._delayedChildrenConfig.length; ++this._delayedChildrenConfigIndex) { |
| var childConfig = this._delayedChildrenConfig[this._delayedChildrenConfigIndex]; |
| var item = this._createItemElement(childConfig); |
| this._configureItem(item, childConfig, false); |
| fragment.appendChild(item); |
| } |
| this._selectElement.appendChild(fragment); |
| this._selectElement.classList.add("wrap"); |
| this._delayedChildrenConfig = null; |
| }; |
| |
| ListPicker.prototype._findReusableItem = function(parent, config, startIndex) { |
| if (startIndex >= parent.children.length) |
| return null; |
| var tagName = "OPTION"; |
| if (config.type === "optgroup") |
| tagName = "OPTGROUP"; |
| else if (config.type === "separator") |
| tagName = "HR"; |
| for (var i = startIndex; i < parent.children.length; i++) { |
| var child = parent.children[i]; |
| if (tagName === child.tagName) { |
| return child; |
| } |
| } |
| return null; |
| }; |
| |
| ListPicker.prototype._createItemElement = function(config) { |
| var element; |
| if (!config.type || config.type === "option") |
| element = createElement("option"); |
| else if (config.type === "optgroup") |
| element = createElement("optgroup"); |
| else if (config.type === "separator") |
| element = createElement("hr"); |
| return element; |
| }; |
| |
| ListPicker.prototype._applyItemStyle = function(element, styleConfig) { |
| if (!styleConfig) |
| return; |
| var style = element.style; |
| style.visibility = styleConfig.visibility ? styleConfig.visibility : ""; |
| style.display = styleConfig.display ? styleConfig.display : ""; |
| style.direction = styleConfig.direction ? styleConfig.direction : ""; |
| style.unicodeBidi = styleConfig.unicodeBidi ? styleConfig.unicodeBidi : ""; |
| style.color = styleConfig.color ? styleConfig.color : ""; |
| style.backgroundColor = styleConfig.backgroundColor ? styleConfig.backgroundColor : ""; |
| style.fontSize = styleConfig.fontSize ? styleConfig.fontSize + "px" : ""; |
| style.fontWeight = styleConfig.fontWeight ? styleConfig.fontWeight : ""; |
| style.fontFamily = styleConfig.fontFamily ? styleConfig.fontFamily.join(",") : ""; |
| style.fontStyle = styleConfig.fontStyle ? styleConfig.fontStyle : ""; |
| style.fontVariant = styleConfig.fontVariant ? styleConfig.fontVariant : ""; |
| style.textTransform = styleConfig.textTransform ? styleConfig.textTransform : ""; |
| }; |
| |
| ListPicker.prototype._configureItem = function(element, config, inGroup) { |
| if (!config.type || config.type === "option") { |
| element.label = config.label; |
| element.value = config.value; |
| if (config.title) |
| element.title = config.title; |
| else |
| element.removeAttribute("title"); |
| element.disabled = !!config.disabled |
| if (config.ariaLabel) |
| element.setAttribute("aria-label", config.ariaLabel); |
| else |
| element.removeAttribute("aria-label"); |
| element.style.webkitPaddingStart = this._config.paddingStart + "px"; |
| if (inGroup) { |
| element.style.webkitMarginStart = (- this._config.paddingStart) + "px"; |
| // Should be synchronized with padding-end in listPicker.css. |
| element.style.webkitMarginEnd = "-2px"; |
| } |
| } else if (config.type === "optgroup") { |
| element.label = config.label; |
| element.title = config.title; |
| element.disabled = config.disabled; |
| element.setAttribute("aria-label", config.ariaLabel); |
| this._updateChildren(element, config); |
| element.style.webkitPaddingStart = this._config.paddingStart + "px"; |
| } else if (config.type === "separator") { |
| element.title = config.title; |
| element.disabled = config.disabled; |
| element.setAttribute("aria-label", config.ariaLabel); |
| if (inGroup) { |
| element.style.webkitMarginStart = (- this._config.paddingStart) + "px"; |
| // Should be synchronized with padding-end in listPicker.css. |
| element.style.webkitMarginEnd = "-2px"; |
| } |
| } |
| this._applyItemStyle(element, config.style); |
| }; |
| |
| if (window.dialogArguments) { |
| initialize(dialogArguments); |
| } else { |
| window.addEventListener("message", handleMessage, false); |
| window.setTimeout(handleArgumentsTimeout, 1000); |
| } |