Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | |
|
caseq
2017/05/30 23:40:08
2017 :-)
| |
| 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 */ | |
| 8 UI.SoftDropDown = class { | |
| 9 /** | |
| 10 * @param {!UI.SoftDropDown.Delegate} delegate | |
| 11 */ | |
| 12 constructor(delegate) { | |
| 13 this._delegate = delegate; | |
| 14 this._selectedItem = null; | |
| 15 | |
| 16 this.element = createElementWithClass('button', 'soft-dropdown'); | |
| 17 var shadowRoot = UI.createShadowRootWithCoreStyles(this.element, 'ui/softDro pDownButton.css'); | |
| 18 this._titleElement = shadowRoot.createChild('span', 'title'); | |
| 19 this.element.tabIndex = 0; | |
| 20 | |
| 21 this._glassPane = new UI.GlassPane(); | |
| 22 this._glassPane.setMarginBehavior(UI.GlassPane.MarginBehavior.NoMargin); | |
| 23 this._glassPane.setAnchorBehavior(UI.GlassPane.AnchorBehavior.PreferBottom); | |
| 24 this._glassPane.setOutsideClickCallback(this._hide.bind(this)); | |
| 25 this._glassPane.setPointerEventsBehavior(UI.GlassPane.PointerEventsBehavior. BlockedByGlassPane); | |
| 26 this._list = new UI.ListControl(this, UI.ListMode.EqualHeightItems); | |
| 27 this._list.element.classList.add('item-list'); | |
| 28 this._list.element.tabIndex = -1; | |
|
caseq
2017/05/30 23:40:07
This belongs to UI.ListControl, I'm already moving
einbinder
2017/05/31 21:14:27
I guess that is ok. It removes the ability to have
| |
| 29 this._rowHeight = 36; | |
|
caseq
2017/05/30 23:40:08
Can I have a different rowHeight in my control?
einbinder
2017/05/31 21:14:27
Yeah, but we will need to figure out who owns what
| |
| 30 UI.createShadowRootWithCoreStyles(this._glassPane.contentElement, 'ui/softDr opDown.css') | |
| 31 .appendChild(this._list.element); | |
| 32 | |
| 33 this._listWasShowing200msAgo = false; | |
| 34 this.element.addEventListener('mousedown', event => { | |
| 35 if (this._listWasShowing200msAgo) | |
| 36 this._hide(event); | |
| 37 else | |
| 38 this._show(event); | |
| 39 }, false); | |
| 40 this.element.addEventListener('keydown', this._onKeyDown.bind(this), false); | |
| 41 // this.element.addEventListener('focusout', this._hide.bind(this), false); | |
| 42 this._list.element.addEventListener('mousedown', event => { | |
| 43 event.consume(true); | |
|
caseq
2017/05/30 23:40:08
nit: this could be written shorter as event => voi
einbinder
2017/05/31 21:14:27
Done.
| |
| 44 }, false); | |
| 45 this._list.element.addEventListener('mouseup', event => { | |
| 46 if (event.target === this._list.element) | |
| 47 return; | |
| 48 | |
| 49 if (!this._listWasShowing200msAgo) | |
| 50 return; | |
| 51 this._selectHighlightedItem(); | |
| 52 this._hide(event); | |
| 53 }, false); | |
| 54 | |
| 55 var dropdownArrowIcon = UI.Icon.create('smallicon-triangle-down'); | |
| 56 shadowRoot.appendChild(dropdownArrowIcon); | |
|
caseq
2017/05/30 23:40:08
please move this up to where we setup this.element
einbinder
2017/05/31 21:14:26
Done.
| |
| 57 } | |
| 58 | |
| 59 /** | |
| 60 * @param {!Event} event | |
| 61 */ | |
| 62 _show(event) { | |
| 63 if (this._glassPane.isShowing()) | |
| 64 return; | |
| 65 this._glassPane.setContentAnchorBox(this.element.boxInWindow()); | |
| 66 this._glassPane.show(/** @type {!Document} **/ (this.element.ownerDocument)) ; | |
| 67 this._updateGlasspaneSize(); | |
| 68 if (this._selectedItem) { | |
| 69 this._list.selectItem(this._selectedItem); | |
| 70 this._list.scrollItemIntoView(this._selectedItem, true); | |
|
caseq
2017/05/30 23:40:07
This should not be necessary, selectItem() above s
einbinder
2017/05/31 21:14:27
Done.
| |
| 71 } | |
| 72 this.element.focus(); | |
|
caseq
2017/05/30 23:40:08
Do we need focus on the button after _show()?
einbinder
2017/05/31 21:14:27
Yes, because we cancel the mousedown.
| |
| 73 event.consume(true); | |
| 74 setTimeout(() => this._listWasShowing200msAgo = true, 200); | |
| 75 } | |
| 76 | |
| 77 _updateGlasspaneSize() { | |
| 78 var maxHeight = this._rowHeight * (Math.min(this._list.length(), 9)); | |
| 79 this._glassPane.setMaxContentSize(new UI.Size(315, maxHeight)); | |
|
caseq
2017/05/30 23:40:08
Why 315? Also, some use cases may call for larger
einbinder
2017/05/31 21:14:26
Yep.
| |
| 80 this._list.viewportResized(); | |
| 81 } | |
| 82 | |
| 83 /** | |
| 84 * @param {!Event} event | |
| 85 */ | |
| 86 _hide(event) { | |
| 87 setTimeout(() => this._listWasShowing200msAgo = false, 200); | |
| 88 this._glassPane.hide(); | |
| 89 this._delegate.itemHighlighted(null); | |
| 90 event.consume(true); | |
| 91 } | |
| 92 | |
| 93 /** | |
| 94 * @param {!Event} event | |
| 95 */ | |
| 96 _onKeyDown(event) { | |
| 97 var handled = false; | |
| 98 switch (event.key) { | |
| 99 case 'ArrowUp': | |
|
caseq
2017/05/30 23:40:07
This and the one below are actually already handle
einbinder
2017/05/31 21:14:27
The soft drop down needs to know if the key was ha
| |
| 100 handled = this._list.selectPreviousItem(false, false); | |
| 101 break; | |
| 102 case 'ArrowDown': | |
| 103 handled = this._list.selectNextItem(false, false); | |
| 104 break; | |
| 105 case 'ArrowRight': | |
|
caseq
2017/05/30 23:40:08
The very notion of depth is rather specific to the
einbinder
2017/05/31 21:14:26
Putting it back into the ConsoleContextSelector wo
| |
| 106 var currentItem = this._list.selectedItem(); | |
| 107 if (!currentItem) | |
| 108 break; | |
| 109 var nextItem = this._list.itemAtIndex(this._list.selectedIndex() + 1); | |
| 110 if (nextItem && this._delegate.depthFor(currentItem) < this._delegate.de pthFor(nextItem)) | |
| 111 handled = this._list.selectNextItem(false, false); | |
| 112 break; | |
| 113 case 'ArrowLeft': | |
| 114 var currentItem = this._list.selectedItem(); | |
| 115 if (!currentItem) | |
| 116 break; | |
| 117 var depth = this._delegate.depthFor(currentItem); | |
| 118 for (var i = this._list.selectedIndex() - 1; i >= 0; i--) { | |
| 119 if (this._delegate.depthFor(this._list.itemAtIndex(i)) < depth) { | |
| 120 handled = true; | |
| 121 this._list.selectItem(this._list.itemAtIndex(i), false); | |
| 122 break; | |
| 123 } | |
| 124 } | |
| 125 break; | |
| 126 case 'PageUp': | |
|
caseq
2017/05/30 23:40:08
This and the one below are also handled in UI.List
| |
| 127 handled = this._list.selectItemPreviousPage(false); | |
| 128 break; | |
| 129 case 'PageDown': | |
| 130 handled = this._list.selectItemNextPage(false); | |
| 131 break; | |
| 132 case 'Home': | |
|
caseq
2017/05/30 23:40:08
Should we add Home/End to UI.ListControl as well?
einbinder
2017/05/31 21:14:26
We can, but that seems like something for a differ
| |
| 133 for (var i = 0; i < this._list.length(); i++) { | |
| 134 if (this.isItemSelectable(this._list.itemAtIndex(i))) { | |
| 135 this._list.selectItem(this._list.itemAtIndex(i)); | |
| 136 handled = true; | |
| 137 break; | |
| 138 } | |
| 139 } | |
| 140 break; | |
| 141 case 'End': | |
| 142 for (var i = this._list.length() - 1; i >= 0; i--) { | |
| 143 if (this.isItemSelectable(this._list.itemAtIndex(i))) { | |
| 144 this._list.selectItem(this._list.itemAtIndex(i)); | |
| 145 handled = true; | |
| 146 break; | |
| 147 } | |
| 148 } | |
| 149 break; | |
| 150 case 'Escape': | |
| 151 this._hide(event); | |
| 152 break; | |
| 153 case 'Tab': | |
| 154 if (!this._glassPane.isShowing()) | |
| 155 break; | |
| 156 this._selectHighlightedItem(); | |
| 157 this._hide(event); | |
| 158 break; | |
| 159 case 'Enter': | |
| 160 if (!this._glassPane.isShowing()) { | |
| 161 this._show(event); | |
| 162 break; | |
| 163 } | |
| 164 this._selectHighlightedItem(); | |
| 165 this._hide(event); | |
| 166 break; | |
| 167 case ' ': | |
| 168 this._show(event); | |
| 169 break; | |
| 170 default: | |
| 171 if (event.key.length === 1) { | |
| 172 var selectedIndex = this._list.selectedIndex(); | |
| 173 var letter = event.key.toUpperCase(); | |
| 174 for (var i = 0; i < this._list.length(); i++) { | |
| 175 var item = this._list.itemAtIndex((selectedIndex + i + 1) % this._li st.length()); | |
| 176 if (this._delegate.titleFor(item).toUpperCase().startsWith(letter)) { | |
| 177 this._list.selectItem(item); | |
| 178 break; | |
| 179 } | |
| 180 } | |
| 181 handled = true; | |
| 182 } | |
| 183 break; | |
| 184 } | |
| 185 | |
| 186 if (handled) { | |
| 187 event.consume(true); | |
| 188 this._selectHighlightedItem(); | |
| 189 } | |
| 190 } | |
| 191 | |
| 192 /** | |
| 193 * @param {T} item | |
| 194 * @param {function(T, T):number} comparator | |
| 195 */ | |
| 196 insertItemWithComparator(item, comparator) { | |
| 197 this._list.insertItemWithComparator(item, comparator); | |
| 198 } | |
| 199 | |
| 200 /** | |
| 201 * @param {T} item | |
| 202 */ | |
| 203 removeItem(item) { | |
| 204 if (this._list.indexOfItem(item) !== -1) | |
|
caseq
2017/05/30 23:40:08
Do we really need this check?
einbinder
2017/05/31 21:14:26
The list asserts that the item exists before it ca
| |
| 205 this._list.removeItem(item); | |
| 206 if (this._selectedItem === item) { | |
| 207 this._selectedItem = null; | |
| 208 this._selectHighlightedItem(); | |
| 209 } | |
| 210 this._updateGlasspaneSize(); | |
| 211 } | |
| 212 | |
| 213 /** | |
| 214 * @param {?T} item | |
| 215 */ | |
| 216 selectItem(item) { | |
| 217 this._selectedItem = item; | |
|
caseq
2017/05/30 23:40:08
Do we need our own copy of the selected item? Perh
einbinder
2017/05/31 21:14:27
The selection in the dropdown is separate from tha
| |
| 218 this._updateSelectedItem(); | |
| 219 } | |
| 220 | |
| 221 /** | |
| 222 * @override | |
| 223 * @param {T} item | |
| 224 * @return {!Element} | |
| 225 */ | |
| 226 createElementForItem(item) { | |
| 227 var element = this._delegate.elementForItem(item); | |
| 228 element.style.paddingLeft = (8 + this._delegate.depthFor(item) * 15) + 'px'; | |
|
caseq
2017/05/30 23:40:08
This is too specific, let's keep in in the console
einbinder
2017/05/31 21:14:27
If we go with the SoftDropDown handling depth, it
einbinder
2017/06/05 21:03:39
Removed depth-aware keyboard shortcuts, and moved
| |
| 229 element.addEventListener('mousemove', e => { | |
|
caseq
2017/05/30 23:40:08
Let's have one mousemove listener for the entire c
einbinder
2017/05/31 21:14:27
Why?
| |
| 230 if ((e.movementX || e.movementY) && this._delegate.isItemSelectable(item)) | |
| 231 this._list.selectItem(item, false, /* Don't scroll */ true); | |
| 232 }); | |
| 233 element.classList.toggle('item', true); | |
| 234 element.classList.toggle('disabled', !this._delegate.isItemSelectable(item)) ; | |
| 235 element.classList.toggle('selected', this._list.selectedItem() === item); | |
| 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('selected'); | |
| 267 if (toElement) | |
| 268 toElement.classList.add('selected'); | |
| 269 this._delegate.itemHighlighted(to); | |
| 270 } | |
| 271 | |
| 272 _selectHighlightedItem() { | |
| 273 this._selectedItem = this._list.selectedItem(); | |
| 274 this._updateSelectedItem(); | |
| 275 } | |
| 276 | |
| 277 _updateSelectedItem() { | |
| 278 if (this._selectedItem) | |
| 279 this._titleElement.textContent = this._delegate.titleFor(this._selectedIte m); | |
| 280 else | |
| 281 this._titleElement.textContent = ''; | |
| 282 this._delegate.itemSelected(this._selectedItem); | |
| 283 } | |
| 284 | |
| 285 /** | |
| 286 * @param {T} item | |
| 287 */ | |
| 288 refreshItem(item) { | |
| 289 var index = this._list.indexOfItem(item); | |
| 290 this._list.refreshItemsInRange(index, index + 1); | |
| 291 } | |
| 292 }; | |
| 293 | |
| 294 /** | |
| 295 * @interface | |
| 296 * @template T | |
| 297 */ | |
| 298 UI.SoftDropDown.Delegate = class { | |
| 299 /** | |
| 300 * @param {T} item | |
| 301 * @return {string} | |
| 302 */ | |
| 303 titleFor(item) { | |
| 304 } | |
| 305 | |
| 306 /** | |
| 307 * @param {T} item | |
| 308 * @return {number} | |
| 309 */ | |
| 310 depthFor(item) { | |
| 311 } | |
| 312 | |
| 313 /** | |
| 314 * @param {T} item | |
| 315 * @return {!Element} | |
| 316 */ | |
| 317 elementForItem(item) { | |
| 318 } | |
| 319 | |
| 320 /** | |
| 321 * @param {T} item | |
| 322 * @return {boolean} | |
| 323 */ | |
| 324 isItemSelectable(item) { | |
| 325 } | |
| 326 | |
| 327 /** | |
| 328 * @param {?T} item | |
| 329 */ | |
| 330 itemSelected(item) { | |
| 331 } | |
| 332 | |
| 333 /** | |
| 334 * @param {?T} item | |
| 335 */ | |
| 336 itemHighlighted(item) { | |
| 337 } | |
| 338 }; | |
| OLD | NEW |