blob: 88e8e97f30148962a261bb3219e426fe7e0d71b5 [file] [log] [blame]
Blink Reformat4c46d092018-04-07 15:32:371// Copyright 2017 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4/**
5 * @template T
6 * @implements {UI.ListDelegate<T>}
7 */
8UI.SoftDropDown = class {
9 /**
10 * @param {!UI.ListModel<T>} model
11 * @param {!UI.SoftDropDown.Delegate<T>} delegate
12 */
13 constructor(model, delegate) {
14 this._delegate = delegate;
15 this._selectedItem = null;
16 this._model = model;
17
18 this.element = createElementWithClass('button', 'soft-dropdown');
19 const shadowRoot = UI.createShadowRootWithCoreStyles(this.element, 'ui/softDropDownButton.css');
20 this._titleElement = shadowRoot.createChild('span', 'title');
21 const dropdownArrowIcon = UI.Icon.create('smallicon-triangle-down');
22 shadowRoot.appendChild(dropdownArrowIcon);
23
24 this._glassPane = new UI.GlassPane();
25 this._glassPane.setMarginBehavior(UI.GlassPane.MarginBehavior.NoMargin);
26 this._glassPane.setAnchorBehavior(UI.GlassPane.AnchorBehavior.PreferBottom);
27 this._glassPane.setOutsideClickCallback(this._hide.bind(this));
28 this._glassPane.setPointerEventsBehavior(UI.GlassPane.PointerEventsBehavior.BlockedByGlassPane);
29 this._list = new UI.ListControl(model, this, UI.ListMode.EqualHeightItems);
30 this._list.element.classList.add('item-list');
31 this._rowHeight = 36;
32 this._width = 315;
33 UI.createShadowRootWithCoreStyles(this._glassPane.contentElement, 'ui/softDropDown.css')
34 .appendChild(this._list.element);
35
36 this._listWasShowing200msAgo = false;
37 this.element.addEventListener('mousedown', event => {
38 if (this._listWasShowing200msAgo)
39 this._hide(event);
40 else if (!this.element.disabled)
41 this._show(event);
42 }, false);
43 this.element.addEventListener('keydown', this._onKeyDown.bind(this), false);
44 this.element.addEventListener('focusout', this._hide.bind(this), false);
45 this._list.element.addEventListener('mousedown', event => event.consume(true), false);
46 this._list.element.addEventListener('mouseup', event => {
47 if (event.target === this._list.element)
48 return;
49
50 if (!this._listWasShowing200msAgo)
51 return;
52 this._selectHighlightedItem();
53 this._hide(event);
54 }, false);
55 model.addEventListener(UI.ListModel.Events.ItemsReplaced, this._itemsReplaced, this);
56 }
57
58 /**
59 * @param {!Event} event
60 */
61 _show(event) {
62 if (this._glassPane.isShowing())
63 return;
64 this._glassPane.setContentAnchorBox(this.element.boxInWindow());
65 this._glassPane.show(/** @type {!Document} **/ (this.element.ownerDocument));
66 this._updateGlasspaneSize();
67 if (this._selectedItem)
68 this._list.selectItem(this._selectedItem);
69 this.element.focus();
70 event.consume(true);
71 setTimeout(() => this._listWasShowing200msAgo = true, 200);
72 }
73
74 _updateGlasspaneSize() {
75 const maxHeight = this._rowHeight * (Math.min(this._model.length, 9));
76 this._glassPane.setMaxContentSize(new UI.Size(this._width, maxHeight));
77 this._list.viewportResized();
78 }
79
80 /**
81 * @param {!Event} event
82 */
83 _hide(event) {
84 setTimeout(() => this._listWasShowing200msAgo = false, 200);
85 this._glassPane.hide();
86 this._list.selectItem(null);
87 event.consume(true);
88 }
89
90 /**
91 * @param {!Event} event
92 */
93 _onKeyDown(event) {
94 let handled = false;
95 switch (event.key) {
96 case 'ArrowLeft':
97 case 'ArrowUp':
98 handled = this._list.selectPreviousItem(false, false);
99 break;
100 case 'ArrowRight':
101 case 'ArrowDown':
102 handled = this._list.selectNextItem(false, false);
103 break;
104 case 'PageUp':
105 handled = this._list.selectItemPreviousPage(false);
106 break;
107 case 'PageDown':
108 handled = this._list.selectItemNextPage(false);
109 break;
110 case 'Home':
111 for (let i = 0; i < this._model.length; i++) {
112 if (this.isItemSelectable(this._model.at(i))) {
113 this._list.selectItem(this._model.at(i));
114 handled = true;
115 break;
116 }
117 }
118 break;
119 case 'End':
120 for (let i = this._model.length - 1; i >= 0; i--) {
121 if (this.isItemSelectable(this._model.at(i))) {
122 this._list.selectItem(this._model.at(i));
123 handled = true;
124 break;
125 }
126 }
127 break;
128 case 'Escape':
129 this._hide(event);
130 break;
131 case 'Tab':
132 if (!this._glassPane.isShowing())
133 break;
134 this._selectHighlightedItem();
135 this._hide(event);
136 break;
137 case 'Enter':
138 if (!this._glassPane.isShowing()) {
139 this._show(event);
140 break;
141 }
142 this._selectHighlightedItem();
143 this._hide(event);
144 break;
145 case ' ':
146 this._show(event);
147 break;
148 default:
149 if (event.key.length === 1) {
150 const selectedIndex = this._list.selectedIndex();
151 const letter = event.key.toUpperCase();
152 for (let i = 0; i < this._model.length; i++) {
153 const item = this._model.at((selectedIndex + i + 1) % this._model.length);
154 if (this._delegate.titleFor(item).toUpperCase().startsWith(letter)) {
155 this._list.selectItem(item);
156 break;
157 }
158 }
159 handled = true;
160 }
161 break;
162 }
163
164 if (handled) {
165 event.consume(true);
166 this._selectHighlightedItem();
167 }
168 }
169
170 /**
171 * @param {number} width
172 */
173 setWidth(width) {
174 this._width = width;
175 this._updateGlasspaneSize();
176 }
177
178 /**
179 * @param {number} rowHeight
180 */
181 setRowHeight(rowHeight) {
182 this._rowHeight = rowHeight;
183 }
184
185 /**
186 * @param {!Common.Event} event
187 */
188 _itemsReplaced(event) {
189 const removed = /** @type {!Array<T>} */ (event.data.removed);
190 if (removed.indexOf(this._selectedItem) !== -1) {
191 this._selectedItem = null;
192 this._selectHighlightedItem();
193 }
194 this._updateGlasspaneSize();
195 }
196
197 /**
198 * @param {?T} item
199 */
200 selectItem(item) {
201 this._selectedItem = item;
202 if (this._selectedItem)
203 this._titleElement.textContent = this._delegate.titleFor(this._selectedItem);
204 else
205 this._titleElement.textContent = '';
206 this._delegate.itemSelected(this._selectedItem);
207 }
208
209 /**
210 * @override
211 * @param {T} item
212 * @return {!Element}
213 */
214 createElementForItem(item) {
215 const element = createElementWithClass('div', 'item');
216 element.addEventListener('mousemove', e => {
217 if ((e.movementX || e.movementY) && this._delegate.isItemSelectable(item))
218 this._list.selectItem(item, false, /* Don't scroll */ true);
219 });
220 element.classList.toggle('disabled', !this._delegate.isItemSelectable(item));
221 element.classList.toggle('highlighted', this._list.selectedItem() === item);
222
223 element.appendChild(this._delegate.createElementForItem(item));
224
225 return element;
226 }
227
228 /**
229 * @override
230 * @param {T} item
231 * @return {number}
232 */
233 heightForItem(item) {
234 return this._rowHeight;
235 }
236
237 /**
238 * @override
239 * @param {T} item
240 * @return {boolean}
241 */
242 isItemSelectable(item) {
243 return this._delegate.isItemSelectable(item);
244 }
245
246 /**
247 * @override
248 * @param {?T} from
249 * @param {?T} to
250 * @param {?Element} fromElement
251 * @param {?Element} toElement
252 */
253 selectedItemChanged(from, to, fromElement, toElement) {
254 if (fromElement)
255 fromElement.classList.remove('highlighted');
256 if (toElement)
257 toElement.classList.add('highlighted');
258 this._delegate.highlightedItemChanged(
259 from, to, fromElement && fromElement.firstElementChild, toElement && toElement.firstElementChild);
260 }
261
262 _selectHighlightedItem() {
263 this.selectItem(this._list.selectedItem());
264 }
265
266 /**
267 * @param {T} item
268 */
269 refreshItem(item) {
270 this._list.refreshItem(item);
271 }
272};
273
274/**
275 * @interface
276 * @template T
277 */
278UI.SoftDropDown.Delegate = class {
279 /**
280 * @param {T} item
281 * @return {string}
282 */
283 titleFor(item) {
284 }
285
286 /**
287 * @param {T} item
288 * @return {!Element}
289 */
290 createElementForItem(item) {
291 }
292
293 /**
294 * @param {T} item
295 * @return {boolean}
296 */
297 isItemSelectable(item) {
298 }
299
300 /**
301 * @param {?T} item
302 */
303 itemSelected(item) {
304 }
305
306 /**
307 * @param {?T} from
308 * @param {?T} to
309 * @param {?Element} fromElement
310 * @param {?Element} toElement
311 */
312 highlightedItemChanged(from, to, fromElement, toElement) {
313 }
314};