OLD | NEW |
1 // Copyright 2017 The Chromium Authors. All rights reserved. | 1 // Copyright 2017 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * @fileoverview Element which shows context menus and handles keyboard | 6 * @fileoverview Element which shows context menus and handles keyboard |
7 * shortcuts. | 7 * shortcuts. |
8 */ | 8 */ |
9 cr.define('bookmarks', function() { | 9 cr.define('bookmarks', function() { |
10 | 10 |
11 var CommandManager = Polymer({ | 11 var CommandManager = Polymer({ |
12 is: 'bookmarks-command-manager', | 12 is: 'bookmarks-command-manager', |
13 | 13 |
14 behaviors: [ | 14 behaviors: [ |
15 bookmarks.StoreClient, | 15 bookmarks.StoreClient, |
16 ], | 16 ], |
17 | 17 |
18 properties: { | 18 properties: { |
19 /** @private {!Array<Command>} */ | 19 /** @private {!Array<Command>} */ |
20 menuCommands_: { | 20 menuCommands_: { |
21 type: Array, | 21 type: Array, |
22 value: function() { | 22 value: function() { |
23 return [ | 23 return [ |
24 Command.EDIT, | 24 Command.EDIT, |
25 Command.COPY, | 25 Command.COPY_URL, |
26 Command.DELETE, | 26 Command.DELETE, |
27 // <hr> | 27 // <hr> |
28 Command.OPEN_NEW_TAB, | 28 Command.OPEN_NEW_TAB, |
29 Command.OPEN_NEW_WINDOW, | 29 Command.OPEN_NEW_WINDOW, |
30 Command.OPEN_INCOGNITO, | 30 Command.OPEN_INCOGNITO, |
31 ]; | 31 ]; |
32 }, | 32 }, |
33 }, | 33 }, |
34 | 34 |
35 /** @private {Set<string>} */ | 35 /** @private {Set<string>} */ |
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
71 document.addEventListener('command-undo', this.boundOnCommandUndo_); | 71 document.addEventListener('command-undo', this.boundOnCommandUndo_); |
72 | 72 |
73 /** @private {function(!Event)} */ | 73 /** @private {function(!Event)} */ |
74 this.boundOnKeydown_ = this.onKeydown_.bind(this); | 74 this.boundOnKeydown_ = this.onKeydown_.bind(this); |
75 document.addEventListener('keydown', this.boundOnKeydown_); | 75 document.addEventListener('keydown', this.boundOnKeydown_); |
76 | 76 |
77 /** @private {Object<Command, cr.ui.KeyboardShortcutList>} */ | 77 /** @private {Object<Command, cr.ui.KeyboardShortcutList>} */ |
78 this.shortcuts_ = {}; | 78 this.shortcuts_ = {}; |
79 | 79 |
80 this.addShortcut_(Command.EDIT, 'F2', 'Enter'); | 80 this.addShortcut_(Command.EDIT, 'F2', 'Enter'); |
81 this.addShortcut_(Command.COPY, 'Ctrl|c', 'Meta|c'); | |
82 this.addShortcut_(Command.DELETE, 'Delete', 'Delete Backspace'); | 81 this.addShortcut_(Command.DELETE, 'Delete', 'Delete Backspace'); |
83 | 82 |
84 this.addShortcut_(Command.OPEN, 'Enter', 'Meta|ArrowDown Meta|o'); | 83 this.addShortcut_(Command.OPEN, 'Enter', 'Meta|ArrowDown Meta|o'); |
85 this.addShortcut_(Command.OPEN_NEW_TAB, 'Ctrl|Enter', 'Meta|Enter'); | 84 this.addShortcut_(Command.OPEN_NEW_TAB, 'Ctrl|Enter', 'Meta|Enter'); |
86 this.addShortcut_(Command.OPEN_NEW_WINDOW, 'Shift|Enter'); | 85 this.addShortcut_(Command.OPEN_NEW_WINDOW, 'Shift|Enter'); |
87 | 86 |
88 this.addShortcut_(Command.UNDO, 'Ctrl|z', 'Meta|z'); | 87 this.addShortcut_(Command.UNDO, 'Ctrl|z', 'Meta|z'); |
89 this.addShortcut_(Command.REDO, 'Ctrl|y Ctrl|Shift|Z', 'Meta|Shift|Z'); | 88 this.addShortcut_(Command.REDO, 'Ctrl|y Ctrl|Shift|Z', 'Meta|Shift|Z'); |
90 | 89 |
91 this.addShortcut_(Command.SELECT_ALL, 'Ctrl|a', 'Meta|a'); | 90 this.addShortcut_(Command.SELECT_ALL, 'Ctrl|a', 'Meta|a'); |
92 this.addShortcut_(Command.DESELECT_ALL, 'Escape'); | 91 this.addShortcut_(Command.DESELECT_ALL, 'Escape'); |
| 92 |
| 93 this.addShortcut_(Command.CUT, 'Ctrl|x', 'Meta|x'); |
| 94 this.addShortcut_(Command.COPY, 'Ctrl|c', 'Meta|c'); |
| 95 this.addShortcut_(Command.PASTE, 'Ctrl|v', 'Meta|v'); |
93 }, | 96 }, |
94 | 97 |
95 detached: function() { | 98 detached: function() { |
96 CommandManager.instance_ = null; | 99 CommandManager.instance_ = null; |
97 document.removeEventListener('open-item-menu', this.boundOnOpenItemMenu_); | 100 document.removeEventListener('open-item-menu', this.boundOnOpenItemMenu_); |
98 document.removeEventListener('command-undo', this.boundOnCommandUndo_); | 101 document.removeEventListener('command-undo', this.boundOnCommandUndo_); |
99 document.removeEventListener('keydown', this.boundOnKeydown_); | 102 document.removeEventListener('keydown', this.boundOnKeydown_); |
100 }, | 103 }, |
101 | 104 |
102 /** | 105 /** |
(...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
148 | 151 |
149 /** | 152 /** |
150 * Determine if the |command| can be executed with the given |itemIds|. | 153 * Determine if the |command| can be executed with the given |itemIds|. |
151 * Commands which appear in the context menu should be implemented | 154 * Commands which appear in the context menu should be implemented |
152 * separately using `isCommandVisible_` and `isCommandEnabled_`. | 155 * separately using `isCommandVisible_` and `isCommandEnabled_`. |
153 * @param {Command} command | 156 * @param {Command} command |
154 * @param {!Set<string>} itemIds | 157 * @param {!Set<string>} itemIds |
155 * @return {boolean} | 158 * @return {boolean} |
156 */ | 159 */ |
157 canExecute: function(command, itemIds) { | 160 canExecute: function(command, itemIds) { |
| 161 var state = this.getState(); |
158 switch (command) { | 162 switch (command) { |
159 case Command.OPEN: | 163 case Command.OPEN: |
160 return itemIds.size > 0; | 164 return itemIds.size > 0; |
161 case Command.UNDO: | 165 case Command.UNDO: |
162 case Command.REDO: | 166 case Command.REDO: |
163 return this.globalCanEdit_; | 167 return this.globalCanEdit_; |
164 case Command.SELECT_ALL: | 168 case Command.SELECT_ALL: |
165 case Command.DESELECT_ALL: | 169 case Command.DESELECT_ALL: |
166 return true; | 170 return true; |
| 171 case Command.COPY: |
| 172 return itemIds.size > 0; |
| 173 case Command.CUT: |
| 174 return itemIds.size > 0 && |
| 175 !this.containsMatchingNode_(itemIds, function(node) { |
| 176 return !bookmarks.util.canEditNode(state, node.id); |
| 177 }); |
| 178 case Command.PASTE: |
| 179 return state.search.term == '' && |
| 180 bookmarks.util.canReorderChildren(state, state.selectedFolder); |
167 default: | 181 default: |
168 return this.isCommandVisible_(command, itemIds) && | 182 return this.isCommandVisible_(command, itemIds) && |
169 this.isCommandEnabled_(command, itemIds); | 183 this.isCommandEnabled_(command, itemIds); |
170 } | 184 } |
171 }, | 185 }, |
172 | 186 |
173 /** | 187 /** |
174 * @param {Command} command | 188 * @param {Command} command |
175 * @param {!Set<string>} itemIds | 189 * @param {!Set<string>} itemIds |
176 * @return {boolean} True if the command should be visible in the context | 190 * @return {boolean} True if the command should be visible in the context |
177 * menu. | 191 * menu. |
178 */ | 192 */ |
179 isCommandVisible_: function(command, itemIds) { | 193 isCommandVisible_: function(command, itemIds) { |
180 switch (command) { | 194 switch (command) { |
181 case Command.EDIT: | 195 case Command.EDIT: |
182 return itemIds.size == 1 && this.globalCanEdit_; | 196 return itemIds.size == 1 && this.globalCanEdit_; |
183 case Command.COPY: | 197 case Command.COPY_URL: |
184 return this.isSingleBookmark_(itemIds); | 198 return this.isSingleBookmark_(itemIds); |
185 case Command.DELETE: | 199 case Command.DELETE: |
186 return itemIds.size > 0 && this.globalCanEdit_; | 200 return itemIds.size > 0 && this.globalCanEdit_; |
187 case Command.OPEN_NEW_TAB: | 201 case Command.OPEN_NEW_TAB: |
188 case Command.OPEN_NEW_WINDOW: | 202 case Command.OPEN_NEW_WINDOW: |
189 case Command.OPEN_INCOGNITO: | 203 case Command.OPEN_INCOGNITO: |
190 return itemIds.size > 0; | 204 return itemIds.size > 0; |
191 default: | 205 default: |
192 return false; | 206 return false; |
193 } | 207 } |
(...skipping 30 matching lines...) Expand all Loading... |
224 * @param {!Set<string>} itemIds | 238 * @param {!Set<string>} itemIds |
225 */ | 239 */ |
226 handle: function(command, itemIds) { | 240 handle: function(command, itemIds) { |
227 var state = this.getState(); | 241 var state = this.getState(); |
228 switch (command) { | 242 switch (command) { |
229 case Command.EDIT: | 243 case Command.EDIT: |
230 var id = Array.from(itemIds)[0]; | 244 var id = Array.from(itemIds)[0]; |
231 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get()) | 245 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get()) |
232 .showEditDialog(state.nodes[id]); | 246 .showEditDialog(state.nodes[id]); |
233 break; | 247 break; |
| 248 case Command.COPY_URL: |
234 case Command.COPY: | 249 case Command.COPY: |
235 var idList = Array.from(itemIds); | 250 var idList = Array.from(itemIds); |
236 chrome.bookmarkManagerPrivate.copy(idList, function() { | 251 chrome.bookmarkManagerPrivate.copy(idList, function() { |
237 bookmarks.ToastManager.getInstance().show( | 252 var labelPromise; |
238 loadTimeData.getString('toastUrlCopied'), false); | 253 if (command == Command.COPY_URL) { |
239 }); | 254 labelPromise = |
| 255 Promise.resolve(loadTimeData.getString('toastUrlCopied')); |
| 256 } else { |
| 257 labelPromise = cr.sendWithPromise( |
| 258 'getPluralString', 'toastItemsCopied', idList.length); |
| 259 } |
| 260 |
| 261 this.showTitleToast_( |
| 262 labelPromise, state.nodes[idList[0]].title, false); |
| 263 }.bind(this)); |
240 break; | 264 break; |
241 case Command.DELETE: | 265 case Command.DELETE: |
242 var idList = Array.from(this.minimizeDeletionSet_(itemIds)); | 266 var idList = Array.from(this.minimizeDeletionSet_(itemIds)); |
243 var title = state.nodes[idList[0]].title; | 267 var title = state.nodes[idList[0]].title; |
244 var labelPromise = cr.sendWithPromise( | 268 var labelPromise = cr.sendWithPromise( |
245 'getPluralString', 'toastItemsDeleted', idList.length); | 269 'getPluralString', 'toastItemsDeleted', idList.length); |
246 chrome.bookmarkManagerPrivate.removeTrees(idList, function() { | 270 chrome.bookmarkManagerPrivate.removeTrees(idList, function() { |
247 labelPromise.then(function(label) { | 271 this.showTitleToast_(labelPromise, title, true); |
248 var pieces = loadTimeData.getSubstitutedStringPieces(label, title) | |
249 .map(function(p) { | |
250 // Make the bookmark name collapsible. | |
251 p.collapsible = !!p.arg; | |
252 return p; | |
253 }); | |
254 bookmarks.ToastManager.getInstance().showForStringPieces( | |
255 pieces, true); | |
256 }.bind(this)); | |
257 }.bind(this)); | 272 }.bind(this)); |
258 break; | 273 break; |
259 case Command.UNDO: | 274 case Command.UNDO: |
260 chrome.bookmarkManagerPrivate.undo(); | 275 chrome.bookmarkManagerPrivate.undo(); |
261 bookmarks.ToastManager.getInstance().hide(); | 276 bookmarks.ToastManager.getInstance().hide(); |
262 break; | 277 break; |
263 case Command.REDO: | 278 case Command.REDO: |
264 chrome.bookmarkManagerPrivate.redo(); | 279 chrome.bookmarkManagerPrivate.redo(); |
265 break; | 280 break; |
266 case Command.OPEN_NEW_TAB: | 281 case Command.OPEN_NEW_TAB: |
(...skipping 14 matching lines...) Expand all Loading... |
281 this.openUrls_(this.expandUrls_(itemIds), command); | 296 this.openUrls_(this.expandUrls_(itemIds), command); |
282 } | 297 } |
283 break; | 298 break; |
284 case Command.SELECT_ALL: | 299 case Command.SELECT_ALL: |
285 var displayedIds = bookmarks.util.getDisplayedList(state); | 300 var displayedIds = bookmarks.util.getDisplayedList(state); |
286 this.dispatch(bookmarks.actions.selectAll(displayedIds, state)); | 301 this.dispatch(bookmarks.actions.selectAll(displayedIds, state)); |
287 break; | 302 break; |
288 case Command.DESELECT_ALL: | 303 case Command.DESELECT_ALL: |
289 this.dispatch(bookmarks.actions.deselectItems()); | 304 this.dispatch(bookmarks.actions.deselectItems()); |
290 break; | 305 break; |
| 306 case Command.CUT: |
| 307 chrome.bookmarkManagerPrivate.cut(Array.from(itemIds)); |
| 308 break; |
| 309 case Command.PASTE: |
| 310 var selectedFolder = state.selectedFolder; |
| 311 var selectedItems = state.selection.items; |
| 312 chrome.bookmarkManagerPrivate.paste( |
| 313 selectedFolder, Array.from(selectedItems)); |
| 314 break; |
291 default: | 315 default: |
292 assert(false); | 316 assert(false); |
293 } | 317 } |
294 }, | 318 }, |
295 | 319 |
296 /** | 320 /** |
297 * @param {!Event} e | 321 * @param {!Event} e |
298 * @param {!Set<string>} itemIds | 322 * @param {!Set<string>} itemIds |
299 * @return {boolean} True if the event was handled, triggering a keyboard | 323 * @return {boolean} True if the event was handled, triggering a keyboard |
300 * shortcut. | 324 * shortcut. |
(...skipping 145 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
446 * node. | 470 * node. |
447 */ | 471 */ |
448 isSingleBookmark_: function(itemIds) { | 472 isSingleBookmark_: function(itemIds) { |
449 return itemIds.size == 1 && | 473 return itemIds.size == 1 && |
450 this.containsMatchingNode_(itemIds, function(node) { | 474 this.containsMatchingNode_(itemIds, function(node) { |
451 return !!node.url; | 475 return !!node.url; |
452 }); | 476 }); |
453 }, | 477 }, |
454 | 478 |
455 /** | 479 /** |
456 * @param {Event} e | |
457 * @private | |
458 */ | |
459 onOpenItemMenu_: function(e) { | |
460 if (e.detail.targetElement) { | |
461 this.openCommandMenuAtElement(e.detail.targetElement); | |
462 } else { | |
463 this.openCommandMenuAtPosition(e.detail.x, e.detail.y); | |
464 } | |
465 }, | |
466 | |
467 /** | |
468 * @param {Event} e | |
469 * @private | |
470 */ | |
471 onCommandClick_: function(e) { | |
472 this.handle( | |
473 e.currentTarget.getAttribute('command'), assert(this.menuIds_)); | |
474 this.closeCommandMenu(); | |
475 }, | |
476 | |
477 /** | |
478 * @param {!Event} e | |
479 * @private | |
480 */ | |
481 onKeydown_: function(e) { | |
482 var selection = this.getState().selection.items; | |
483 if (e.target == document.body) | |
484 this.handleKeyEvent(e, selection); | |
485 }, | |
486 | |
487 /** | |
488 * Close the menu on mousedown so clicks can propagate to the underlying UI. | |
489 * This allows the user to right click the list while a context menu is | |
490 * showing and get another context menu. | |
491 * @param {Event} e | |
492 * @private | |
493 */ | |
494 onMenuMousedown_: function(e) { | |
495 if (e.path[0] != this.$.dropdown.getIfExists()) | |
496 return; | |
497 | |
498 this.closeCommandMenu(); | |
499 }, | |
500 | |
501 /** @private */ | |
502 onOpenCancelTap_: function() { | |
503 this.$.openDialog.get().cancel(); | |
504 }, | |
505 | |
506 /** @private */ | |
507 onOpenConfirmTap_: function() { | |
508 this.confirmOpenCallback_(); | |
509 this.$.openDialog.get().close(); | |
510 }, | |
511 | |
512 /** | |
513 * @param {Command} command | 480 * @param {Command} command |
514 * @return {string} | 481 * @return {string} |
515 * @private | 482 * @private |
516 */ | 483 */ |
517 getCommandLabel_: function(command) { | 484 getCommandLabel_: function(command) { |
518 var multipleNodes = this.menuIds_.size > 1 || | 485 var multipleNodes = this.menuIds_.size > 1 || |
519 this.containsMatchingNode_(this.menuIds_, function(node) { | 486 this.containsMatchingNode_(this.menuIds_, function(node) { |
520 return !node.url; | 487 return !node.url; |
521 }); | 488 }); |
522 var label; | 489 var label; |
523 switch (command) { | 490 switch (command) { |
524 case Command.EDIT: | 491 case Command.EDIT: |
525 if (this.menuIds_.size != 1) | 492 if (this.menuIds_.size != 1) |
526 return ''; | 493 return ''; |
527 | 494 |
528 var id = Array.from(this.menuIds_)[0]; | 495 var id = Array.from(this.menuIds_)[0]; |
529 var itemUrl = this.getState().nodes[id].url; | 496 var itemUrl = this.getState().nodes[id].url; |
530 label = itemUrl ? 'menuEdit' : 'menuRename'; | 497 label = itemUrl ? 'menuEdit' : 'menuRename'; |
531 break; | 498 break; |
532 case Command.COPY: | 499 case Command.COPY_URL: |
533 label = 'menuCopyURL'; | 500 label = 'menuCopyURL'; |
534 break; | 501 break; |
535 case Command.DELETE: | 502 case Command.DELETE: |
536 label = 'menuDelete'; | 503 label = 'menuDelete'; |
537 break; | 504 break; |
538 case Command.OPEN_NEW_TAB: | 505 case Command.OPEN_NEW_TAB: |
539 label = multipleNodes ? 'menuOpenAllNewTab' : 'menuOpenNewTab'; | 506 label = multipleNodes ? 'menuOpenAllNewTab' : 'menuOpenNewTab'; |
540 break; | 507 break; |
541 case Command.OPEN_NEW_WINDOW: | 508 case Command.OPEN_NEW_WINDOW: |
542 label = multipleNodes ? 'menuOpenAllNewWindow' : 'menuOpenNewWindow'; | 509 label = multipleNodes ? 'menuOpenAllNewWindow' : 'menuOpenNewWindow'; |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
580 | 547 |
581 /** | 548 /** |
582 * @param {Command} command | 549 * @param {Command} command |
583 * @return {boolean} | 550 * @return {boolean} |
584 * @private | 551 * @private |
585 */ | 552 */ |
586 showDividerAfter_: function(command, itemIds) { | 553 showDividerAfter_: function(command, itemIds) { |
587 return command == Command.DELETE && | 554 return command == Command.DELETE && |
588 (this.globalCanEdit_ || this.isSingleBookmark_(itemIds)); | 555 (this.globalCanEdit_ || this.isSingleBookmark_(itemIds)); |
589 }, | 556 }, |
| 557 |
| 558 /** |
| 559 * Show a toast with a bookmark |title| inserted into a label, with the |
| 560 * title ellipsised if necessary. |
| 561 * @param {!Promise<string>} labelPromise Promise which resolves with the |
| 562 * label for the toast. |
| 563 * @param {string} title Bookmark title to insert. |
| 564 * @param {boolean} canUndo If true, shows an undo button in the toast. |
| 565 * @private |
| 566 */ |
| 567 showTitleToast_: function(labelPromise, title, canUndo) { |
| 568 labelPromise.then(function(label) { |
| 569 var pieces = loadTimeData.getSubstitutedStringPieces(label, title) |
| 570 .map(function(p) { |
| 571 // Make the bookmark name collapsible. |
| 572 p.collapsible = !!p.arg; |
| 573 return p; |
| 574 }); |
| 575 |
| 576 bookmarks.ToastManager.getInstance().showForStringPieces( |
| 577 pieces, canUndo); |
| 578 }); |
| 579 }, |
| 580 |
| 581 //////////////////////////////////////////////////////////////////////////// |
| 582 // Event handlers: |
| 583 |
| 584 /** |
| 585 * @param {Event} e |
| 586 * @private |
| 587 */ |
| 588 onOpenItemMenu_: function(e) { |
| 589 if (e.detail.targetElement) { |
| 590 this.openCommandMenuAtElement(e.detail.targetElement); |
| 591 } else { |
| 592 this.openCommandMenuAtPosition(e.detail.x, e.detail.y); |
| 593 } |
| 594 }, |
| 595 |
| 596 /** |
| 597 * @param {Event} e |
| 598 * @private |
| 599 */ |
| 600 onCommandClick_: function(e) { |
| 601 this.handle( |
| 602 e.currentTarget.getAttribute('command'), assert(this.menuIds_)); |
| 603 this.closeCommandMenu(); |
| 604 }, |
| 605 |
| 606 /** |
| 607 * @param {!Event} e |
| 608 * @private |
| 609 */ |
| 610 onKeydown_: function(e) { |
| 611 var selection = this.getState().selection.items; |
| 612 if (e.target == document.body) |
| 613 this.handleKeyEvent(e, selection); |
| 614 }, |
| 615 |
| 616 /** |
| 617 * Close the menu on mousedown so clicks can propagate to the underlying UI. |
| 618 * This allows the user to right click the list while a context menu is |
| 619 * showing and get another context menu. |
| 620 * @param {Event} e |
| 621 * @private |
| 622 */ |
| 623 onMenuMousedown_: function(e) { |
| 624 if (e.path[0] != this.$.dropdown.getIfExists()) |
| 625 return; |
| 626 |
| 627 this.closeCommandMenu(); |
| 628 }, |
| 629 |
| 630 /** @private */ |
| 631 onOpenCancelTap_: function() { |
| 632 this.$.openDialog.get().cancel(); |
| 633 }, |
| 634 |
| 635 /** @private */ |
| 636 onOpenConfirmTap_: function() { |
| 637 this.confirmOpenCallback_(); |
| 638 this.$.openDialog.get().close(); |
| 639 }, |
590 }); | 640 }); |
591 | 641 |
592 /** @private {bookmarks.CommandManager} */ | 642 /** @private {bookmarks.CommandManager} */ |
593 CommandManager.instance_ = null; | 643 CommandManager.instance_ = null; |
594 | 644 |
595 /** @return {!bookmarks.CommandManager} */ | 645 /** @return {!bookmarks.CommandManager} */ |
596 CommandManager.getInstance = function() { | 646 CommandManager.getInstance = function() { |
597 return assert(CommandManager.instance_); | 647 return assert(CommandManager.instance_); |
598 }; | 648 }; |
599 | 649 |
600 return { | 650 return { |
601 CommandManager: CommandManager, | 651 CommandManager: CommandManager, |
602 }; | 652 }; |
603 }); | 653 }); |
OLD | NEW |