| // Copyright 2013 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. |
| |
| /** |
| * Search box. |
| */ |
| class SearchBox extends cr.EventTarget { |
| /** |
| * @param {!Element} element Root element of the search box. |
| * @param {!Element} searchWrapper Wrapper element around the buttons and box. |
| * @param {!Element} searchButton Search button. |
| */ |
| constructor(element, searchWrapper, searchButton) { |
| super(); |
| |
| /** |
| * Autocomplete List. |
| * @type {!SearchBox.AutocompleteList} |
| */ |
| this.autocompleteList = |
| new SearchBox.AutocompleteList(element.ownerDocument); |
| |
| /** |
| * Root element of the search box. |
| * @type {!Element} |
| */ |
| this.element = element; |
| |
| /** |
| * Search wrapper. |
| * @type {!Element} |
| */ |
| this.searchWrapper = searchWrapper; |
| |
| /** |
| * Search button. |
| * @type {!Element} |
| */ |
| this.searchButton = searchButton; |
| |
| /** |
| * Ripple effect of search button. |
| * @private {!FilesToggleRipple} |
| * @const |
| */ |
| this.searchButtonToggleRipple_ = |
| /** @type {!FilesToggleRipple} */ ( |
| queryRequiredElement('files-toggle-ripple', this.searchButton)); |
| |
| /** |
| * Text input of the search box. |
| * @type {!HTMLInputElement} |
| */ |
| this.inputElement = |
| /** @type {!HTMLInputElement} */ (element.querySelector('cr-input')); |
| |
| /** |
| * Clear button of the search box. |
| * @private {!Element} |
| */ |
| this.clearButton_ = assert(element.querySelector('.clear')); |
| |
| /** @private {boolean} */ |
| this.isClicking_ = false; |
| |
| this.collapsed = true; |
| |
| // Register events. |
| this.inputElement.addEventListener('input', this.onInput_.bind(this)); |
| this.inputElement.addEventListener('keydown', this.onKeyDown_.bind(this)); |
| this.inputElement.addEventListener('focus', this.onFocus_.bind(this)); |
| this.inputElement.addEventListener('blur', this.onBlur_.bind(this)); |
| this.inputElement.ownerDocument.addEventListener( |
| 'dragover', this.onDragEnter_.bind(this), true); |
| this.inputElement.ownerDocument.addEventListener( |
| 'dragend', this.onDragEnd_.bind(this)); |
| this.searchButton.addEventListener( |
| 'click', this.onSearchButtonClick_.bind(this)); |
| this.clearButton_.addEventListener( |
| 'click', this.onClearButtonClick_.bind(this)); |
| const dispatchItemSelect = |
| cr.dispatchSimpleEvent.bind(cr, this, SearchBox.EventType.ITEM_SELECT); |
| this.autocompleteList.handleEnterKeydown = dispatchItemSelect; |
| this.autocompleteList.addEventListener('mousedown', dispatchItemSelect); |
| |
| document.addEventListener('mousedown', () => { |
| if (this.collapsed) { |
| return; |
| } |
| this.isClicking_ = true; |
| }, {capture: true, passive: true}); |
| |
| document.addEventListener('mouseup', () => { |
| if (this.collapsed) { |
| return; |
| } |
| this.isClicking_ = false; |
| window.requestAnimationFrame(() => { |
| this.removeHidePending(); |
| }); |
| }, {passive: true}); |
| |
| this.searchWrapper.addEventListener( |
| 'focusout', this.onFocusOut_.bind(this)); |
| |
| // Append dynamically created element. |
| element.parentNode.appendChild(this.autocompleteList); |
| } |
| |
| /** @private {boolean} */ |
| get collapsed() { |
| return this.searchWrapper.hasAttribute('collapsed'); |
| } |
| |
| /** |
| * @private |
| * @param {boolean} collapsed |
| */ |
| set collapsed(collapsed) { |
| if (collapsed) { |
| this.searchWrapper.setAttribute('collapsed', true); |
| } else { |
| this.searchWrapper.removeAttribute('collapsed'); |
| } |
| } |
| |
| /** |
| * Clears the search query. |
| */ |
| clear() { |
| this.inputElement.value = ''; |
| this.updateStyles_(); |
| } |
| |
| /** |
| * Sets hidden attribute for components of search box. |
| * @param {boolean} hidden True when the search box need to be hidden. |
| */ |
| setHidden(hidden) { |
| this.element.hidden = hidden; |
| this.searchButton.hidden = hidden; |
| } |
| |
| /** |
| * Focus out event handler. |
| * @private |
| */ |
| onFocusOut_() { |
| window.requestAnimationFrame(() => { |
| // If the focus is still within the search box don't hide the input. |
| if (document.activeElement && |
| this.element.contains(document.activeElement)) { |
| return; |
| } |
| |
| // If the focus is moved due to a user click, we don't collapse the searc |
| // box here. We wait until "mouseup" to let the mouse events be processed |
| // by the button user is clickinkg, which might change position due to the |
| // search box collapse. |
| if (this.isClicking_) { |
| return; |
| } |
| |
| if (this.element.classList.contains('hide-pending')) { |
| this.removeHidePending(); |
| } |
| }); |
| } |
| |
| /** |
| * @private |
| */ |
| onInput_() { |
| this.updateStyles_(); |
| cr.dispatchSimpleEvent(this, SearchBox.EventType.TEXT_CHANGE); |
| } |
| |
| /** |
| * Handles a focus event of the search box <cr-input> element. |
| * @private |
| */ |
| onFocus_() { |
| // Early out if we closing the search cr-input: do not just go ahead and |
| // re-open it on focus, crbug.com/668427. |
| if (this.element.classList.contains('hide-pending')) { |
| return; |
| } |
| |
| this.inputElement.addEventListener('transitionend', () => { |
| this.collapsed = false; |
| }, {once: true}); |
| |
| this.isClicking_ = false; |
| this.element.classList.toggle('has-cursor', true); |
| this.searchWrapper.classList.toggle('has-cursor', true); |
| this.autocompleteList.attachToInput(this.inputElement); |
| this.updateStyles_(); |
| this.searchButtonToggleRipple_.activated = true; |
| metrics.recordUserAction('SelectSearch'); |
| } |
| |
| /** |
| * Handles a blur event of the search box <cr-input> element. |
| * @private |
| */ |
| onBlur_() { |
| this.element.classList.toggle('has-cursor', false); |
| this.element.classList.toggle('hide-pending', true); |
| this.searchWrapper.classList.toggle('has-cursor', false); |
| this.searchWrapper.classList.toggle('hide-pending', true); |
| this.autocompleteList.detach(); |
| this.updateStyles_(); |
| this.searchButtonToggleRipple_.activated = false; |
| } |
| |
| /** |
| * Handles delayed hiding of the search box (until click). |
| */ |
| removeHidePending() { |
| this.inputElement.disabled = this.inputElement.value.length == 0; |
| this.element.classList.toggle('hide-pending', false); |
| this.searchWrapper.classList.toggle('hide-pending', false); |
| this.inputElement.addEventListener('transitionend', () => { |
| this.collapsed = true; |
| }, {once: true}); |
| } |
| |
| /** |
| * Handles a keydown event of the search box. |
| * @param {Event} event |
| * @private |
| */ |
| onKeyDown_(event) { |
| event = /** @type {KeyboardEvent} */ (event); |
| // Handle only Esc key now. |
| if (event.key != 'Escape' || this.inputElement.value) { |
| return; |
| } |
| |
| this.inputElement.tabIndex = -1; // Focus to default element after blur. |
| this.inputElement.blur(); |
| this.inputElement.disabled = this.inputElement.value.length == 0; |
| this.element.classList.toggle('hide-pending', false); |
| this.searchWrapper.classList.toggle('hide-pending', false); |
| } |
| |
| /** |
| * Handles a dragenter event and refuses a drag source of files. |
| * @param {Event} event The dragenter event. |
| * @private |
| */ |
| onDragEnter_(event) { |
| event = /** @type {DragEvent} */ (event); |
| // For normal elements, they does not accept drag drop by default, and |
| // accept it by using event.preventDefault. But input elements accept drag |
| // drop by default. So disable the input element here to prohibit drag drop. |
| if (event.dataTransfer.types.indexOf('text/plain') === -1) { |
| this.inputElement.style.pointerEvents = 'none'; |
| } |
| } |
| |
| /** |
| * Handles a dragend event. |
| * @private |
| */ |
| onDragEnd_() { |
| this.inputElement.style.pointerEvents = ''; |
| } |
| |
| /** |
| * Updates styles of the search box. |
| * @private |
| */ |
| updateStyles_() { |
| const hasText = !!this.inputElement.value; |
| this.element.classList.toggle('has-text', hasText); |
| this.searchWrapper.classList.toggle('has-text', hasText); |
| const hasFocusOnInput = this.element.classList.contains('has-cursor'); |
| |
| // Focus either the search button or the input. |
| this.inputElement.tabIndex = (hasText || hasFocusOnInput) ? 0 : -1; |
| this.searchButton.tabIndex = (hasText || hasFocusOnInput) ? -1 : 0; |
| } |
| |
| /** |
| * @private |
| */ |
| onSearchButtonClick_() { |
| this.inputElement.disabled = false; |
| this.inputElement.focus(); |
| } |
| |
| /** |
| * @private |
| */ |
| onClearButtonClick_() { |
| this.inputElement.value = ''; |
| this.onInput_(); |
| } |
| } |
| |
| /** |
| * Event type. |
| * @enum {string} |
| */ |
| SearchBox.EventType = { |
| // Dispatched when the text in the search box is changed. |
| TEXT_CHANGE: 'textchange', |
| // Dispatched when the item in the auto complete list is selected. |
| ITEM_SELECT: 'itemselect' |
| }; |
| |
| /** |
| * Autocomplete list for search box. |
| */ |
| SearchBox.AutocompleteList = |
| class AutocompleteList extends cr.ui.AutocompleteList { |
| /** |
| * @param {Document} document Document. |
| */ |
| constructor(document) { |
| super(); |
| this.__proto__ = SearchBox.AutocompleteList.prototype; |
| this.id = 'autocomplete-list'; |
| this.autoExpands = true; |
| this.itemConstructor = /** @type {function(new:cr.ui.ListItem, *)} */ ( |
| SearchBox.AutocompleteListItem_.bind(null, document)); |
| this.addEventListener('mouseover', this.onMouseOver_.bind(this)); |
| } |
| |
| /** |
| * Do nothing when a suggestion is selected. |
| * @override |
| */ |
| handleSelectedSuggestion() {} |
| |
| /** |
| * Change the selection by a mouse over instead of just changing the |
| * color of moused over element with :hover in CSS. Here's why: |
| * |
| * 1) The user selects an item A with up/down keys (item A is highlighted) |
| * 2) Then the user moves the cursor to another item B |
| * |
| * If we just change the color of moused over element (item B), both |
| * the item A and B are highlighted. This is bad. We should change the |
| * selection so only the item B is highlighted. |
| * |
| * @param {Event} event Event. |
| * @private |
| */ |
| onMouseOver_(event) { |
| if (event.target.itemInfo) { |
| this.selectedItem = event.target.itemInfo; |
| } |
| } |
| }; |
| |
| /** |
| * ListItem element for autocomplete. |
| * @private |
| */ |
| SearchBox.AutocompleteListItem_ = |
| class AutocompleteListItem_ extends cr.ui.ListItem { |
| /** |
| * @param {Document} document Document. |
| * @param {SearchItem|chrome.fileManagerPrivate.DriveMetadataSearchResult} |
| * item An object |
| * representing a suggestion. |
| */ |
| constructor(document, item) { |
| super(); |
| this.itemInfo = item; |
| |
| const icon = document.createElement('div'); |
| icon.className = 'detail-icon'; |
| |
| const text = document.createElement('div'); |
| text.className = 'detail-text'; |
| |
| if (item.isHeaderItem) { |
| icon.setAttribute('search-icon', ''); |
| text.innerHTML = |
| strf('SEARCH_DRIVE_HTML', util.htmlEscape(item.searchQuery)); |
| } else { |
| const iconType = FileType.getIcon(item.entry); |
| icon.setAttribute('file-type-icon', iconType); |
| // highlightedBaseName is a piece of HTML with meta characters properly |
| // escaped. See the comment at fileManagerPrivate.searchDriveMetadata(). |
| text.innerHTML = item.highlightedBaseName; |
| } |
| this.appendChild(icon); |
| this.appendChild(text); |
| } |
| }; |