blob: 8df035c7780cd5376b46445e671c8d8b7df6d786 [file] [log] [blame]
Blink Reformat4c46d092018-04-07 15:32:371// Copyright 2015 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.
Paul Lewis9950e182019-12-16 16:06:074
Paul Lewis17e384e2020-01-08 15:46:515import * as Common from '../common/common.js';
Paul Lewis9950e182019-12-16 16:06:076import {Toolbar, ToolbarButton} from './Toolbar.js';
7import {createInput, createTextButton, ElementFocusRestorer} from './UIUtils.js';
8import {VBox} from './Widget.js';
9
Blink Reformat4c46d092018-04-07 15:32:3710/**
11 * @template T
12 */
Paul Lewis9950e182019-12-16 16:06:0713export class ListWidget extends VBox {
Blink Reformat4c46d092018-04-07 15:32:3714 /**
Tim van der Lippe0830b3d2019-10-03 13:20:0715 * @param {!Delegate<T>} delegate
Blink Reformat4c46d092018-04-07 15:32:3716 */
17 constructor(delegate) {
Joel Einbinder7fbe24c2019-01-24 05:19:0118 super(true, true /* delegatesFocus */);
Blink Reformat4c46d092018-04-07 15:32:3719 this.registerRequiredCSS('ui/listWidget.css');
20 this._delegate = delegate;
21
22 this._list = this.contentElement.createChild('div', 'list');
Blink Reformat4c46d092018-04-07 15:32:3723
24 this._lastSeparator = false;
Paul Lewis9950e182019-12-16 16:06:0725 /** @type {?ElementFocusRestorer} */
Blink Reformat4c46d092018-04-07 15:32:3726 this._focusRestorer = null;
27 /** @type {!Array<T>} */
28 this._items = [];
29 /** @type {!Array<boolean>} */
30 this._editable = [];
31 /** @type {!Array<!Element>} */
32 this._elements = [];
Tim van der Lippe0830b3d2019-10-03 13:20:0733 /** @type {?Editor<T>} */
Blink Reformat4c46d092018-04-07 15:32:3734 this._editor = null;
35 /** @type {?T} */
36 this._editItem = null;
37 /** @type {?Element} */
38 this._editElement = null;
39
40 /** @type {?Element} */
41 this._emptyPlaceholder = null;
42
43 this._updatePlaceholder();
44 }
45
46 clear() {
47 this._items = [];
48 this._editable = [];
49 this._elements = [];
50 this._lastSeparator = false;
51 this._list.removeChildren();
52 this._updatePlaceholder();
53 this._stopEditing();
54 }
55
56 /**
57 * @param {!T} item
58 * @param {boolean} editable
59 */
60 appendItem(item, editable) {
Tim van der Lippe1d6e57a2019-09-30 11:55:3461 if (this._lastSeparator && this._items.length) {
Blink Reformat4c46d092018-04-07 15:32:3762 this._list.appendChild(createElementWithClass('div', 'list-separator'));
Tim van der Lippe1d6e57a2019-09-30 11:55:3463 }
Blink Reformat4c46d092018-04-07 15:32:3764 this._lastSeparator = false;
65
66 this._items.push(item);
67 this._editable.push(editable);
68
69 const element = this._list.createChild('div', 'list-item');
70 element.appendChild(this._delegate.renderItem(item, editable));
71 if (editable) {
72 element.classList.add('editable');
73 element.appendChild(this._createControls(item, element));
74 }
75 this._elements.push(element);
76 this._updatePlaceholder();
77 }
78
79 appendSeparator() {
80 this._lastSeparator = true;
81 }
82
83 /**
84 * @param {number} index
85 */
86 removeItem(index) {
Tim van der Lippe1d6e57a2019-09-30 11:55:3487 if (this._editItem === this._items[index]) {
Blink Reformat4c46d092018-04-07 15:32:3788 this._stopEditing();
Tim van der Lippe1d6e57a2019-09-30 11:55:3489 }
Blink Reformat4c46d092018-04-07 15:32:3790
91 const element = this._elements[index];
92
93 const previous = element.previousElementSibling;
94 const previousIsSeparator = previous && previous.classList.contains('list-separator');
95
96 const next = element.nextElementSibling;
97 const nextIsSeparator = next && next.classList.contains('list-separator');
98
Tim van der Lippe1d6e57a2019-09-30 11:55:3499 if (previousIsSeparator && (nextIsSeparator || !next)) {
Blink Reformat4c46d092018-04-07 15:32:37100 previous.remove();
Tim van der Lippe1d6e57a2019-09-30 11:55:34101 }
102 if (nextIsSeparator && !previous) {
Blink Reformat4c46d092018-04-07 15:32:37103 next.remove();
Tim van der Lippe1d6e57a2019-09-30 11:55:34104 }
Blink Reformat4c46d092018-04-07 15:32:37105 element.remove();
106
107 this._elements.splice(index, 1);
108 this._items.splice(index, 1);
109 this._editable.splice(index, 1);
110 this._updatePlaceholder();
111 }
112
113 /**
114 * @param {number} index
115 * @param {!T} item
116 */
117 addNewItem(index, item) {
118 this._startEditing(item, null, this._elements[index] || null);
119 }
120
121 /**
122 * @param {?Element} element
123 */
124 setEmptyPlaceholder(element) {
125 this._emptyPlaceholder = element;
126 this._updatePlaceholder();
127 }
128
129 /**
130 * @param {!T} item
131 * @param {!Element} element
132 * @return {!Element}
133 */
134 _createControls(item, element) {
135 const controls = createElementWithClass('div', 'controls-container fill');
136 controls.createChild('div', 'controls-gradient');
137
138 const buttons = controls.createChild('div', 'controls-buttons');
139
Paul Lewis9950e182019-12-16 16:06:07140 const toolbar = new Toolbar('', buttons);
Blink Reformat4c46d092018-04-07 15:32:37141
Paul Lewis17e384e2020-01-08 15:46:51142 const editButton = new ToolbarButton(Common.UIString.UIString('Edit'), 'largeicon-edit');
Paul Lewis9950e182019-12-16 16:06:07143 editButton.addEventListener(ToolbarButton.Events.Click, onEditClicked.bind(this));
Blink Reformat4c46d092018-04-07 15:32:37144 toolbar.appendToolbarItem(editButton);
145
Paul Lewis17e384e2020-01-08 15:46:51146 const removeButton = new ToolbarButton(Common.UIString.UIString('Remove'), 'largeicon-trash-bin');
Paul Lewis9950e182019-12-16 16:06:07147 removeButton.addEventListener(ToolbarButton.Events.Click, onRemoveClicked.bind(this));
Blink Reformat4c46d092018-04-07 15:32:37148 toolbar.appendToolbarItem(removeButton);
149
150 return controls;
151
152 /**
Tim van der Lippe0830b3d2019-10-03 13:20:07153 * @this {ListWidget}
Blink Reformat4c46d092018-04-07 15:32:37154 */
155 function onEditClicked() {
156 const index = this._elements.indexOf(element);
157 const insertionPoint = this._elements[index + 1] || null;
158 this._startEditing(item, element, insertionPoint);
159 }
160
161 /**
Tim van der Lippe0830b3d2019-10-03 13:20:07162 * @this {ListWidget}
Blink Reformat4c46d092018-04-07 15:32:37163 */
164 function onRemoveClicked() {
165 const index = this._elements.indexOf(element);
166 this.element.focus();
167 this._delegate.removeItemRequested(this._items[index], index);
168 }
169 }
170
171 /**
172 * @override
173 */
174 wasShown() {
175 super.wasShown();
176 this._stopEditing();
177 }
178
179 _updatePlaceholder() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34180 if (!this._emptyPlaceholder) {
Blink Reformat4c46d092018-04-07 15:32:37181 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34182 }
Blink Reformat4c46d092018-04-07 15:32:37183
Tim van der Lippe1d6e57a2019-09-30 11:55:34184 if (!this._elements.length && !this._editor) {
Blink Reformat4c46d092018-04-07 15:32:37185 this._list.appendChild(this._emptyPlaceholder);
Tim van der Lippe1d6e57a2019-09-30 11:55:34186 } else {
Blink Reformat4c46d092018-04-07 15:32:37187 this._emptyPlaceholder.remove();
Tim van der Lippe1d6e57a2019-09-30 11:55:34188 }
Blink Reformat4c46d092018-04-07 15:32:37189 }
190
191 /**
192 * @param {!T} item
193 * @param {?Element} element
194 * @param {?Element} insertionPoint
195 */
196 _startEditing(item, element, insertionPoint) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34197 if (element && this._editElement === element) {
Blink Reformat4c46d092018-04-07 15:32:37198 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34199 }
Blink Reformat4c46d092018-04-07 15:32:37200
201 this._stopEditing();
Paul Lewis9950e182019-12-16 16:06:07202 this._focusRestorer = new ElementFocusRestorer(this.element);
Blink Reformat4c46d092018-04-07 15:32:37203
204 this._list.classList.add('list-editing');
205 this._editItem = item;
206 this._editElement = element;
Tim van der Lippe1d6e57a2019-09-30 11:55:34207 if (element) {
Blink Reformat4c46d092018-04-07 15:32:37208 element.classList.add('hidden');
Tim van der Lippe1d6e57a2019-09-30 11:55:34209 }
Blink Reformat4c46d092018-04-07 15:32:37210
211 const index = element ? this._elements.indexOf(element) : -1;
212 this._editor = this._delegate.beginEdit(item);
213 this._updatePlaceholder();
214 this._list.insertBefore(this._editor.element, insertionPoint);
215 this._editor.beginEdit(
Paul Lewis17e384e2020-01-08 15:46:51216 item, index, element ? Common.UIString.UIString('Save') : Common.UIString.UIString('Add'),
217 this._commitEditing.bind(this), this._stopEditing.bind(this));
Blink Reformat4c46d092018-04-07 15:32:37218 }
219
220 _commitEditing() {
221 const editItem = this._editItem;
222 const isNew = !this._editElement;
Tim van der Lippe0830b3d2019-10-03 13:20:07223 const editor = /** @type {!Editor<T>} */ (this._editor);
Blink Reformat4c46d092018-04-07 15:32:37224 this._stopEditing();
225 this._delegate.commitEdit(editItem, editor, isNew);
226 }
227
228 _stopEditing() {
229 this._list.classList.remove('list-editing');
Tim van der Lippe1d6e57a2019-09-30 11:55:34230 if (this._focusRestorer) {
Blink Reformat4c46d092018-04-07 15:32:37231 this._focusRestorer.restore();
Tim van der Lippe1d6e57a2019-09-30 11:55:34232 }
233 if (this._editElement) {
Blink Reformat4c46d092018-04-07 15:32:37234 this._editElement.classList.remove('hidden');
Tim van der Lippe1d6e57a2019-09-30 11:55:34235 }
236 if (this._editor && this._editor.element.parentElement) {
Blink Reformat4c46d092018-04-07 15:32:37237 this._editor.element.remove();
Tim van der Lippe1d6e57a2019-09-30 11:55:34238 }
Blink Reformat4c46d092018-04-07 15:32:37239
240 this._editor = null;
241 this._editItem = null;
242 this._editElement = null;
243 this._updatePlaceholder();
244 }
Tim van der Lippe0830b3d2019-10-03 13:20:07245}
Blink Reformat4c46d092018-04-07 15:32:37246
247/**
248 * @template T
249 * @interface
250 */
Tim van der Lippe0830b3d2019-10-03 13:20:07251export class Delegate {
Blink Reformat4c46d092018-04-07 15:32:37252 /**
253 * @param {!T} item
254 * @param {boolean} editable
255 * @return {!Element}
256 */
Tim van der Lippe0830b3d2019-10-03 13:20:07257 renderItem(item, editable) {
258 }
Blink Reformat4c46d092018-04-07 15:32:37259
260 /**
261 * @param {!T} item
262 * @param {number} index
263 */
Tim van der Lippe0830b3d2019-10-03 13:20:07264 removeItemRequested(item, index) {
265 }
Blink Reformat4c46d092018-04-07 15:32:37266
267 /**
268 * @param {!T} item
Tim van der Lippe0830b3d2019-10-03 13:20:07269 * @return {!Editor<T>}
Blink Reformat4c46d092018-04-07 15:32:37270 */
Tim van der Lippe0830b3d2019-10-03 13:20:07271 beginEdit(item) {
272 }
Blink Reformat4c46d092018-04-07 15:32:37273
274 /**
275 * @param {!T} item
Tim van der Lippe0830b3d2019-10-03 13:20:07276 * @param {!Editor<T>} editor
Blink Reformat4c46d092018-04-07 15:32:37277 * @param {boolean} isNew
278 */
279 commitEdit(item, editor, isNew) {}
Tim van der Lippe0830b3d2019-10-03 13:20:07280}
Blink Reformat4c46d092018-04-07 15:32:37281
282/**
283 * @template T
284 */
Tim van der Lippe0830b3d2019-10-03 13:20:07285export class Editor {
Blink Reformat4c46d092018-04-07 15:32:37286 constructor() {
287 this.element = createElementWithClass('div', 'editor-container');
288 this.element.addEventListener('keydown', onKeyDown.bind(null, isEscKey, this._cancelClicked.bind(this)), false);
289 this.element.addEventListener('keydown', onKeyDown.bind(null, isEnterKey, this._commitClicked.bind(this)), false);
290
291 this._contentElement = this.element.createChild('div', 'editor-content');
292
293 const buttonsRow = this.element.createChild('div', 'editor-buttons');
Paul Lewis9950e182019-12-16 16:06:07294 this._commitButton = createTextButton('', this._commitClicked.bind(this), '', true /* primary */);
Blink Reformat4c46d092018-04-07 15:32:37295 buttonsRow.appendChild(this._commitButton);
Paul Lewis17e384e2020-01-08 15:46:51296 this._cancelButton = createTextButton(Common.UIString.UIString('Cancel'), this._cancelClicked.bind(this));
Blink Reformat4c46d092018-04-07 15:32:37297 this._cancelButton.addEventListener(
298 'keydown', onKeyDown.bind(null, isEnterKey, this._cancelClicked.bind(this)), false);
299 buttonsRow.appendChild(this._cancelButton);
300
Rob Pavezac671db32019-09-20 01:54:50301 this._errorMessageContainer = this.element.createChild('div', 'list-widget-input-validation-error');
302 UI.ARIAUtils.markAsAlert(this._errorMessageContainer);
303
Blink Reformat4c46d092018-04-07 15:32:37304 /**
305 * @param {function(!Event):boolean} predicate
306 * @param {function()} callback
307 * @param {!Event} event
308 */
309 function onKeyDown(predicate, callback, event) {
310 if (predicate(event)) {
311 event.consume(true);
312 callback();
313 }
314 }
315
316 /** @type {!Array<!HTMLInputElement|!HTMLSelectElement>} */
317 this._controls = [];
318 /** @type {!Map<string, !HTMLInputElement|!HTMLSelectElement>} */
319 this._controlByName = new Map();
Amanda Bakerca502822019-07-02 00:01:28320 /** @type {!Array<function(!T, number, (!HTMLInputElement|!HTMLSelectElement)): !UI.ListWidget.ValidatorResult>} */
Blink Reformat4c46d092018-04-07 15:32:37321 this._validators = [];
322
323 /** @type {?function()} */
324 this._commit = null;
325 /** @type {?function()} */
326 this._cancel = null;
327 /** @type {?T} */
328 this._item = null;
329 /** @type {number} */
330 this._index = -1;
331 }
332
333 /**
334 * @return {!Element}
335 */
336 contentElement() {
337 return this._contentElement;
338 }
339
340 /**
341 * @param {string} name
342 * @param {string} type
343 * @param {string} title
Amanda Bakerca502822019-07-02 00:01:28344 * @param {function(!T, number, (!HTMLInputElement|!HTMLSelectElement)): !UI.ListWidget.ValidatorResult} validator
Blink Reformat4c46d092018-04-07 15:32:37345 * @return {!HTMLInputElement}
346 */
347 createInput(name, type, title, validator) {
Paul Lewis9950e182019-12-16 16:06:07348 const input = /** @type {!HTMLInputElement} */ (createInput('', type));
Blink Reformat4c46d092018-04-07 15:32:37349 input.placeholder = title;
350 input.addEventListener('input', this._validateControls.bind(this, false), false);
351 input.addEventListener('blur', this._validateControls.bind(this, false), false);
Amanda Bakerca502822019-07-02 00:01:28352 UI.ARIAUtils.setAccessibleName(input, title);
Blink Reformat4c46d092018-04-07 15:32:37353 this._controlByName.set(name, input);
354 this._controls.push(input);
355 this._validators.push(validator);
356 return input;
357 }
358
359 /**
360 * @param {string} name
361 * @param {!Array<string>} options
Amanda Bakerca502822019-07-02 00:01:28362 * @param {function(!T, number, (!HTMLInputElement|!HTMLSelectElement)): !UI.ListWidget.ValidatorResult} validator
363 * @param {string=} title
Blink Reformat4c46d092018-04-07 15:32:37364 * @return {!HTMLSelectElement}
365 */
Amanda Bakerca502822019-07-02 00:01:28366 createSelect(name, options, validator, title) {
Blink Reformat4c46d092018-04-07 15:32:37367 const select = /** @type {!HTMLSelectElement} */ (createElementWithClass('select', 'chrome-select'));
368 for (let index = 0; index < options.length; ++index) {
369 const option = select.createChild('option');
370 option.value = options[index];
371 option.textContent = options[index];
372 }
Amanda Bakerca502822019-07-02 00:01:28373 if (title) {
374 select.title = title;
375 UI.ARIAUtils.setAccessibleName(select, title);
376 }
Blink Reformat4c46d092018-04-07 15:32:37377 select.addEventListener('input', this._validateControls.bind(this, false), false);
378 select.addEventListener('blur', this._validateControls.bind(this, false), false);
379 this._controlByName.set(name, select);
380 this._controls.push(select);
381 this._validators.push(validator);
382 return select;
383 }
384
385 /**
386 * @param {string} name
387 * @return {!HTMLInputElement|!HTMLSelectElement}
388 */
389 control(name) {
390 return /** @type {!HTMLInputElement|!HTMLSelectElement} */ (this._controlByName.get(name));
391 }
392
393 /**
394 * @param {boolean} forceValid
395 */
396 _validateControls(forceValid) {
397 let allValid = true;
Amanda Bakerca502822019-07-02 00:01:28398 this._errorMessageContainer.textContent = '';
Blink Reformat4c46d092018-04-07 15:32:37399 for (let index = 0; index < this._controls.length; ++index) {
400 const input = this._controls[index];
Amanda Bakerca502822019-07-02 00:01:28401 const {valid, errorMessage} = this._validators[index].call(null, this._item, this._index, input);
402
Blink Reformat4c46d092018-04-07 15:32:37403 input.classList.toggle('error-input', !valid && !forceValid);
Tim van der Lippe1d6e57a2019-09-30 11:55:34404 if (valid || forceValid) {
Amanda Bakerca502822019-07-02 00:01:28405 UI.ARIAUtils.setInvalid(input, false);
Tim van der Lippe1d6e57a2019-09-30 11:55:34406 } else {
Amanda Bakerca502822019-07-02 00:01:28407 UI.ARIAUtils.setInvalid(input, true);
Tim van der Lippe1d6e57a2019-09-30 11:55:34408 }
Amanda Bakerca502822019-07-02 00:01:28409
Tim van der Lippe1d6e57a2019-09-30 11:55:34410 if (!forceValid && errorMessage && !this._errorMessageContainer.textContent) {
Amanda Bakerca502822019-07-02 00:01:28411 this._errorMessageContainer.textContent = errorMessage;
Tim van der Lippe1d6e57a2019-09-30 11:55:34412 }
Amanda Bakerca502822019-07-02 00:01:28413
Blink Reformat4c46d092018-04-07 15:32:37414 allValid &= valid;
415 }
416 this._commitButton.disabled = !allValid;
417 }
418
419 /**
420 * @param {!T} item
421 * @param {number} index
422 * @param {string} commitButtonTitle
423 * @param {function()} commit
424 * @param {function()} cancel
425 */
426 beginEdit(item, index, commitButtonTitle, commit, cancel) {
427 this._commit = commit;
428 this._cancel = cancel;
429 this._item = item;
430 this._index = index;
431
432 this._commitButton.textContent = commitButtonTitle;
433 this.element.scrollIntoViewIfNeeded(false);
Tim van der Lippe1d6e57a2019-09-30 11:55:34434 if (this._controls.length) {
Blink Reformat4c46d092018-04-07 15:32:37435 this._controls[0].focus();
Tim van der Lippe1d6e57a2019-09-30 11:55:34436 }
Blink Reformat4c46d092018-04-07 15:32:37437 this._validateControls(true);
438 }
439
440 _commitClicked() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34441 if (this._commitButton.disabled) {
Blink Reformat4c46d092018-04-07 15:32:37442 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34443 }
Blink Reformat4c46d092018-04-07 15:32:37444
445 const commit = this._commit;
446 this._commit = null;
447 this._cancel = null;
448 this._item = null;
449 this._index = -1;
450 commit();
451 }
452
453 _cancelClicked() {
454 const cancel = this._cancel;
455 this._commit = null;
456 this._cancel = null;
457 this._item = null;
458 this._index = -1;
459 cancel();
460 }
Tim van der Lippe0830b3d2019-10-03 13:20:07461}