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