OLD | NEW |
1 /* | 1 /* |
2 * Copyright (C) 2008 Apple Inc. All rights reserved. | 2 * Copyright (C) 2008 Apple Inc. All rights reserved. |
3 * Copyright (C) 2011 Google Inc. All rights reserved. | 3 * Copyright (C) 2011 Google Inc. All rights reserved. |
4 * | 4 * |
5 * Redistribution and use in source and binary forms, with or without | 5 * Redistribution and use in source and binary forms, with or without |
6 * modification, are permitted provided that the following conditions | 6 * modification, are permitted provided that the following conditions |
7 * are met: | 7 * are met: |
8 * | 8 * |
9 * 1. Redistributions of source code must retain the above copyright | 9 * 1. Redistributions of source code must retain the above copyright |
10 * notice, this list of conditions and the following disclaimer. | 10 * notice, this list of conditions and the following disclaimer. |
(...skipping 23 matching lines...) Expand all Loading... |
34 */ | 34 */ |
35 WebInspector.TextPrompt = function() | 35 WebInspector.TextPrompt = function() |
36 { | 36 { |
37 /** | 37 /** |
38 * @type {!Element|undefined} | 38 * @type {!Element|undefined} |
39 */ | 39 */ |
40 this._proxyElement; | 40 this._proxyElement; |
41 this._proxyElementDisplay = "inline-block"; | 41 this._proxyElementDisplay = "inline-block"; |
42 this._autocompletionTimeout = WebInspector.TextPrompt.DefaultAutocompletionT
imeout; | 42 this._autocompletionTimeout = WebInspector.TextPrompt.DefaultAutocompletionT
imeout; |
43 this._title = ""; | 43 this._title = ""; |
| 44 this._prefixRange = null; |
44 this._previousText = ""; | 45 this._previousText = ""; |
45 this._currentSuggestion = ""; | 46 this._currentSuggestion = ""; |
46 this._completionRequestId = 0; | 47 this._completionRequestId = 0; |
| 48 this._ghostTextElement = createElementWithClass("span", "auto-complete-text"
); |
47 }; | 49 }; |
48 | 50 |
49 WebInspector.TextPrompt.DefaultAutocompletionTimeout = 250; | 51 WebInspector.TextPrompt.DefaultAutocompletionTimeout = 250; |
50 | 52 |
51 /** @enum {symbol} */ | 53 /** @enum {symbol} */ |
52 WebInspector.TextPrompt.Events = { | 54 WebInspector.TextPrompt.Events = { |
53 ItemApplied: Symbol("text-prompt-item-applied"), | 55 ItemApplied: Symbol("text-prompt-item-applied"), |
54 ItemAccepted: Symbol("text-prompt-item-accepted") | 56 ItemAccepted: Symbol("text-prompt-item-accepted") |
55 }; | 57 }; |
56 | 58 |
(...skipping 81 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
138 this._proxyElement.appendChild(element); | 140 this._proxyElement.appendChild(element); |
139 this._element.classList.add("text-prompt"); | 141 this._element.classList.add("text-prompt"); |
140 this._element.addEventListener("keydown", this._boundOnKeyDown, false); | 142 this._element.addEventListener("keydown", this._boundOnKeyDown, false); |
141 this._element.addEventListener("input", this._boundOnInput, false); | 143 this._element.addEventListener("input", this._boundOnInput, false); |
142 this._element.addEventListener("mousewheel", this._boundOnMouseWheel, fa
lse); | 144 this._element.addEventListener("mousewheel", this._boundOnMouseWheel, fa
lse); |
143 this._element.addEventListener("selectstart", this._boundClearAutocomple
te, false); | 145 this._element.addEventListener("selectstart", this._boundClearAutocomple
te, false); |
144 this._element.addEventListener("blur", this._boundClearAutocomplete, fal
se); | 146 this._element.addEventListener("blur", this._boundClearAutocomplete, fal
se); |
145 this._element.ownerDocument.defaultView.addEventListener("resize", this.
_boundClearAutocomplete, false); | 147 this._element.ownerDocument.defaultView.addEventListener("resize", this.
_boundClearAutocomplete, false); |
146 | 148 |
147 if (this._suggestBoxEnabled) | 149 if (this._suggestBoxEnabled) |
148 this._suggestBox = new WebInspector.SuggestBox(this); | 150 this._suggestBox = new WebInspector.SuggestBox(this, 20, true); |
149 | 151 |
150 if (this._title) | 152 if (this._title) |
151 this._proxyElement.title = this._title; | 153 this._proxyElement.title = this._title; |
152 | 154 |
153 return this._proxyElement; | 155 return this._proxyElement; |
154 }, | 156 }, |
155 | 157 |
156 detach: function() | 158 detach: function() |
157 { | 159 { |
158 this._removeFromElement(); | 160 this._removeFromElement(); |
(...skipping 11 matching lines...) Expand all Loading... |
170 { | 172 { |
171 return this._element.textContent; | 173 return this._element.textContent; |
172 }, | 174 }, |
173 | 175 |
174 /** | 176 /** |
175 * @return {string} | 177 * @return {string} |
176 */ | 178 */ |
177 text: function() | 179 text: function() |
178 { | 180 { |
179 var text = this.textWithCurrentSuggestion(); | 181 var text = this.textWithCurrentSuggestion(); |
180 if (this._ghostTextElement) { | 182 if (this._ghostTextElement.parentNode) { |
181 var addition = this._ghostTextElement.textContent; | 183 var addition = this._ghostTextElement.textContent; |
182 text = text.substring(0, text.length - addition.length); | 184 text = text.substring(0, text.length - addition.length); |
183 } | 185 } |
184 return text; | 186 return text; |
185 }, | 187 }, |
186 | 188 |
187 /** | 189 /** |
188 * @param {string} x | 190 * @param {string} x |
189 */ | 191 */ |
190 setText: function(x) | 192 setText: function(x) |
(...skipping 78 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
269 onMouseWheel: function(event) | 271 onMouseWheel: function(event) |
270 { | 272 { |
271 // Subclasses can implement. | 273 // Subclasses can implement. |
272 }, | 274 }, |
273 | 275 |
274 /** | 276 /** |
275 * @param {!Event} event | 277 * @param {!Event} event |
276 */ | 278 */ |
277 onKeyDown: function(event) | 279 onKeyDown: function(event) |
278 { | 280 { |
279 if (isEnterKey(event)) | |
280 return; | |
281 | |
282 var handled = false; | 281 var handled = false; |
283 | 282 |
284 switch (event.key) { | 283 switch (event.key) { |
285 case "Tab": | 284 case "Tab": |
286 handled = this.tabKeyPressed(event); | 285 handled = this.tabKeyPressed(event); |
287 break; | 286 break; |
288 case "ArrowLeft": | 287 case "ArrowLeft": |
289 case "Home": | 288 case "Home": |
290 this.clearAutocomplete(); | 289 this.clearAutocomplete(); |
291 break; | 290 break; |
(...skipping 30 matching lines...) Expand all Loading... |
322 event.consume(true); | 321 event.consume(true); |
323 }, | 322 }, |
324 | 323 |
325 /** | 324 /** |
326 * @param {!Event} event | 325 * @param {!Event} event |
327 */ | 326 */ |
328 onInput: function(event) | 327 onInput: function(event) |
329 { | 328 { |
330 var text = this.text(); | 329 var text = this.text(); |
331 var hasCommonPrefix = text.startsWith(this._previousText) || this._previ
ousText.startsWith(text); | 330 var hasCommonPrefix = text.startsWith(this._previousText) || this._previ
ousText.startsWith(text); |
332 if (this._ghostTextElement && hasCommonPrefix) | 331 if (this._prefixRange && hasCommonPrefix) |
333 this._ghostTextElement.textContent = this._currentSuggestion.substri
ng(text.length); | 332 this._prefixRange.endColumn += text.length - this._previousText.leng
th; |
334 else | 333 this._refreshGhostText(); |
335 this._clearGhostTextElement(); | |
336 this._previousText = text; | 334 this._previousText = text; |
337 | 335 |
338 this.autoCompleteSoon(); | 336 this.autoCompleteSoon(); |
339 }, | 337 }, |
340 | 338 |
341 /** | 339 /** |
342 * @return {boolean} | 340 * @return {boolean} |
343 */ | 341 */ |
344 acceptAutoComplete: function() | 342 acceptAutoComplete: function() |
345 { | 343 { |
346 var result = false; | 344 var result = false; |
347 if (this._isSuggestBoxVisible()) | 345 if (this._isSuggestBoxVisible()) |
348 result = this._suggestBox.acceptSuggestion(); | 346 result = this._suggestBox.acceptSuggestion(); |
349 if (!result) | 347 if (!result) |
350 result = this._acceptSuggestionInternal(); | 348 result = this._acceptSuggestionInternal(); |
351 | 349 |
352 return result; | 350 return result; |
353 }, | 351 }, |
354 | 352 |
355 clearAutocomplete: function() | 353 clearAutocomplete: function() |
356 { | 354 { |
357 if (this._isSuggestBoxVisible()) | 355 if (this._isSuggestBoxVisible()) |
358 this._suggestBox.hide(); | 356 this._suggestBox.hide(); |
359 this._clearGhostTextElement(); | 357 this._clearAutocompleteTimeout(); |
| 358 this._prefixRange = null; |
| 359 this._refreshGhostText(); |
360 }, | 360 }, |
361 | 361 |
362 _clearGhostTextElement: function() | 362 _refreshGhostText: function() |
363 { | 363 { |
364 this._clearAutocompleteTimeout(); | 364 if (this._prefixRange && this._isCaretAtEndOfPrompt()) { |
365 | 365 this._ghostTextElement.textContent = this._currentSuggestion.substri
ng(this._prefixRange.endColumn - this._prefixRange.startColumn); |
366 if (!this._ghostTextElement) | 366 this._element.appendChild(this._ghostTextElement); |
367 return; | 367 } else { |
368 | 368 this._ghostTextElement.remove(); |
369 this._ghostTextElement.remove(); | 369 } |
370 delete this._ghostTextElement; | |
371 delete this._userEnteredRange; | |
372 delete this._userEnteredText; | |
373 }, | 370 }, |
374 | 371 |
375 _clearAutocompleteTimeout: function() | 372 _clearAutocompleteTimeout: function() |
376 { | 373 { |
377 if (this._completeTimeout) { | 374 if (this._completeTimeout) { |
378 clearTimeout(this._completeTimeout); | 375 clearTimeout(this._completeTimeout); |
379 delete this._completeTimeout; | 376 delete this._completeTimeout; |
380 } | 377 } |
381 this._completionRequestId++; | 378 this._completionRequestId++; |
382 }, | 379 }, |
(...skipping 22 matching lines...) Expand all Loading... |
405 | 402 |
406 var shouldExit; | 403 var shouldExit; |
407 | 404 |
408 if (!force && !this._isCaretAtEndOfPrompt() && !this._isSuggestBoxVisibl
e()) | 405 if (!force && !this._isCaretAtEndOfPrompt() && !this._isSuggestBoxVisibl
e()) |
409 shouldExit = true; | 406 shouldExit = true; |
410 else if (!selection.isCollapsed) | 407 else if (!selection.isCollapsed) |
411 shouldExit = true; | 408 shouldExit = true; |
412 else if (!force) { | 409 else if (!force) { |
413 // BUG72018: Do not show suggest box if caret is followed by a non-s
top character. | 410 // BUG72018: Do not show suggest box if caret is followed by a non-s
top character. |
414 var wordSuffixRange = selectionRange.startContainer.rangeOfWord(sele
ctionRange.endOffset, this._completionStopCharacters, this._element, "forward"); | 411 var wordSuffixRange = selectionRange.startContainer.rangeOfWord(sele
ctionRange.endOffset, this._completionStopCharacters, this._element, "forward"); |
415 var autocompleteTextLength = (this._ghostTextElement && this._ghostT
extElement.parentNode) ? this._ghostTextElement.textContent.length : 0; | 412 var autocompleteTextLength = this._ghostTextElement.parentNode ? thi
s._ghostTextElement.textContent.length : 0; |
416 if (wordSuffixRange.toString().length !== autocompleteTextLength) | 413 if (wordSuffixRange.toString().length !== autocompleteTextLength) |
417 shouldExit = true; | 414 shouldExit = true; |
418 } | 415 } |
419 if (shouldExit) { | 416 if (shouldExit) { |
420 this.clearAutocomplete(); | 417 this.clearAutocomplete(); |
421 return; | 418 return; |
422 } | 419 } |
423 | 420 |
424 var wordPrefixRange = selectionRange.startContainer.rangeOfWord(selectio
nRange.startOffset, this._completionStopCharacters, this._element, "backward"); | 421 var wordPrefixRange = selectionRange.startContainer.rangeOfWord(selectio
nRange.startOffset, this._completionStopCharacters, this._element, "backward"); |
425 this._loadCompletions(/** @type {!Element} */ (this._proxyElement), word
PrefixRange, force || false, this._completionsReady.bind(this, ++this._completio
nRequestId, selection, wordPrefixRange, !!reverse, !!force)); | 422 this._loadCompletions(/** @type {!Element} */ (this._proxyElement), word
PrefixRange, force || false, this._completionsReady.bind(this, ++this._completio
nRequestId, selection, wordPrefixRange, !!reverse, !!force)); |
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
502 | 499 |
503 var fullWordRange = this._createRange(); | 500 var fullWordRange = this._createRange(); |
504 fullWordRange.setStart(originalWordPrefixRange.startContainer, originalW
ordPrefixRange.startOffset); | 501 fullWordRange.setStart(originalWordPrefixRange.startContainer, originalW
ordPrefixRange.startOffset); |
505 fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffs
et); | 502 fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffs
et); |
506 | 503 |
507 if (prefix + selectionRange.toString() !== fullWordRange.toString()) | 504 if (prefix + selectionRange.toString() !== fullWordRange.toString()) |
508 return; | 505 return; |
509 | 506 |
510 selectedIndex = (this._disableDefaultSuggestionForEmptyInput && !this.te
xt()) ? -1 : (selectedIndex || 0); | 507 selectedIndex = (this._disableDefaultSuggestionForEmptyInput && !this.te
xt()) ? -1 : (selectedIndex || 0); |
511 | 508 |
512 this._userEnteredRange = fullWordRange; | 509 if (this._suggestBox) |
513 this._userEnteredText = fullWordRange.toString(); | 510 this._suggestBox.updateSuggestions(this._boxForAnchorAtStart(selecti
on, fullWordRange), annotatedCompletions, selectedIndex, !this._isCaretAtEndOfPr
ompt(), this.text()); |
514 | 511 |
515 if (this._suggestBox) | 512 var beforeRange = this._createRange(); |
516 this._suggestBox.updateSuggestions(this._boxForAnchorAtStart(selecti
on, fullWordRange), annotatedCompletions, selectedIndex, !this._isCaretAtEndOfPr
ompt(), this._userEnteredText); | 513 beforeRange.setStart(this._element, 0); |
| 514 beforeRange.setEnd(fullWordRange.startContainer, fullWordRange.startOffs
et); |
| 515 this._prefixRange = new WebInspector.TextRange(0, beforeRange.toString()
.length, 0, beforeRange.toString().length + fullWordRange.toString().length); |
517 | 516 |
518 if (selectedIndex === -1) | 517 if (selectedIndex === -1) |
519 return; | 518 return; |
520 | 519 this.applySuggestion(annotatedCompletions[selectedIndex].title, true); |
521 var wordPrefixLength = originalWordPrefixRange.toString().length; | |
522 | |
523 if (this._isCaretAtEndOfPrompt()) { | |
524 var suggestion = annotatedCompletions[selectedIndex].title; | |
525 var prefixText = this._userEnteredRange.toString(); | |
526 var suffixText = suggestion.substring(wordPrefixLength); | |
527 this._userEnteredRange.deleteContents(); | |
528 this._element.normalize(); | |
529 var finalSelectionRange = this._createRange(); | |
530 | |
531 var prefixTextNode = createTextNode(prefixText); | |
532 fullWordRange.insertNode(prefixTextNode); | |
533 | |
534 if (!this._ghostTextElement) | |
535 this._ghostTextElement = createElementWithClass("span", "auto-co
mplete-text"); | |
536 this._ghostTextElement.textContent = suffixText; | |
537 this._currentSuggestion = suggestion; | |
538 | |
539 prefixTextNode.parentNode.insertBefore(this._ghostTextElement, prefi
xTextNode.nextSibling); | |
540 | |
541 finalSelectionRange.setStart(prefixTextNode, wordPrefixLength); | |
542 finalSelectionRange.setEnd(prefixTextNode, wordPrefixLength); | |
543 selection.removeAllRanges(); | |
544 selection.addRange(finalSelectionRange); | |
545 this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApp
lied); | |
546 } | |
547 }, | 520 }, |
548 | 521 |
549 /** | 522 /** |
550 * @override | 523 * @override |
551 * @param {string} suggestion | 524 * @param {string} suggestion |
552 * @param {boolean=} isIntermediateSuggestion | 525 * @param {boolean=} isIntermediateSuggestion |
553 */ | 526 */ |
554 applySuggestion: function(suggestion, isIntermediateSuggestion) | 527 applySuggestion: function(suggestion, isIntermediateSuggestion) |
555 { | 528 { |
556 this._applySuggestion(suggestion, isIntermediateSuggestion); | 529 if (!this._prefixRange) |
| 530 return; |
| 531 this._currentSuggestion = suggestion; |
| 532 this._refreshGhostText(); |
| 533 if (isIntermediateSuggestion) |
| 534 this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApp
lied); |
557 }, | 535 }, |
558 | 536 |
559 /** | 537 /** |
560 * @param {string} suggestion | |
561 * @param {boolean=} isIntermediateSuggestion | |
562 */ | |
563 _applySuggestion: function(suggestion, isIntermediateSuggestion) | |
564 { | |
565 if (!this._userEnteredRange) { | |
566 // We could have already cleared autocompletion range by the time th
is is called. (crbug.com/587683) | |
567 return; | |
568 } | |
569 | |
570 var wordPrefixLength = this._userEnteredText ? this._userEnteredText.len
gth : 0; | |
571 | |
572 this._userEnteredRange.deleteContents(); | |
573 this._element.normalize(); | |
574 var finalSelectionRange = this._createRange(); | |
575 var suggestionNode = createTextNode(suggestion); | |
576 this._userEnteredRange.insertNode(suggestionNode); | |
577 if (this._ghostTextElement) { | |
578 this._ghostTextElement.remove(); | |
579 delete this._ghostTextElement; | |
580 } | |
581 | |
582 if (isIntermediateSuggestion) | |
583 finalSelectionRange.setStart(suggestionNode, wordPrefixLength); | |
584 else | |
585 finalSelectionRange.setStart(suggestionNode, suggestion.length); | |
586 | |
587 finalSelectionRange.setEnd(suggestionNode, suggestion.length); | |
588 | |
589 var selection = this._element.getComponentSelection(); | |
590 selection.removeAllRanges(); | |
591 selection.addRange(finalSelectionRange); | |
592 if (isIntermediateSuggestion) | |
593 this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApp
lied, { itemText: suggestion }); | |
594 }, | |
595 | |
596 /** | |
597 * @override | 538 * @override |
598 */ | 539 */ |
599 acceptSuggestion: function() | 540 acceptSuggestion: function() |
600 { | 541 { |
601 this._acceptSuggestionInternal(); | 542 this._acceptSuggestionInternal(); |
602 }, | 543 }, |
603 | 544 |
604 /** | 545 /** |
605 * @return {boolean} | 546 * @return {boolean} |
606 */ | 547 */ |
607 _acceptSuggestionInternal: function() | 548 _acceptSuggestionInternal: function() |
608 { | 549 { |
609 if (!this._ghostTextElement || !this._ghostTextElement.parentNode) | 550 if (!this._prefixRange) |
610 return false; | 551 return false; |
611 | 552 |
612 var text = this._ghostTextElement.textContent; | 553 var text = this.text(); |
613 var textNode = createTextNode(text); | 554 this._element.textContent = text.substring(0, this._prefixRange.startCol
umn) + this._currentSuggestion + text.substring(this._prefixRange.endColumn); |
614 this._ghostTextElement.parentNode.replaceChild(textNode, this._ghostText
Element); | 555 this._setDOMSelection(this._prefixRange.startColumn + this._currentSugge
stion.length, this._prefixRange.startColumn + this._currentSuggestion.length); |
615 delete this._ghostTextElement; | |
616 | |
617 var finalSelectionRange = this._createRange(); | |
618 finalSelectionRange.setStart(textNode, text.length); | |
619 finalSelectionRange.setEnd(textNode, text.length); | |
620 | |
621 var selection = this._element.getComponentSelection(); | |
622 selection.removeAllRanges(); | |
623 selection.addRange(finalSelectionRange); | |
624 | 556 |
625 this.clearAutocomplete(); | 557 this.clearAutocomplete(); |
626 this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemAccepte
d); | 558 this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemAccepte
d); |
627 | 559 |
628 return true; | 560 return true; |
629 }, | 561 }, |
630 | 562 |
631 /** | 563 /** |
| 564 * @param {number} startColumn |
| 565 * @param {number} endColumn |
| 566 */ |
| 567 _setDOMSelection: function(startColumn, endColumn) |
| 568 { |
| 569 this._element.normalize(); |
| 570 var node = this._element.childNodes[0]; |
| 571 if (!node || node === this._ghostTextElement) |
| 572 return; |
| 573 var range = this._createRange(); |
| 574 range.setStart(node, startColumn); |
| 575 range.setEnd(node, endColumn); |
| 576 var selection = this._element.getComponentSelection(); |
| 577 selection.removeAllRanges(); |
| 578 selection.addRange(range); |
| 579 }, |
| 580 |
| 581 /** |
632 * @return {boolean} | 582 * @return {boolean} |
633 */ | 583 */ |
634 _isSuggestBoxVisible: function() | 584 _isSuggestBoxVisible: function() |
635 { | 585 { |
636 return this._suggestBox && this._suggestBox.visible(); | 586 return this._suggestBox && this._suggestBox.visible(); |
637 }, | 587 }, |
638 | 588 |
639 /** | 589 /** |
640 * @return {boolean} | 590 * @return {boolean} |
641 */ | 591 */ |
(...skipping 20 matching lines...) Expand all Loading... |
662 var node = selectionRange.startContainer; | 612 var node = selectionRange.startContainer; |
663 if (!node.isSelfOrDescendant(this._element)) | 613 if (!node.isSelfOrDescendant(this._element)) |
664 return false; | 614 return false; |
665 | 615 |
666 if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < nod
e.nodeValue.length) | 616 if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < nod
e.nodeValue.length) |
667 return false; | 617 return false; |
668 | 618 |
669 var foundNextText = false; | 619 var foundNextText = false; |
670 while (node) { | 620 while (node) { |
671 if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) { | 621 if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) { |
672 if (foundNextText && (!this._ghostTextElement || !this._ghostTex
tElement.isAncestor(node))) | 622 if (foundNextText && !this._ghostTextElement.isAncestor(node)) |
673 return false; | 623 return false; |
674 foundNextText = true; | 624 foundNextText = true; |
675 } | 625 } |
676 | 626 |
677 node = node.traverseNextNode(this._element); | 627 node = node.traverseNextNode(this._element); |
678 } | 628 } |
679 | 629 |
680 return true; | 630 return true; |
681 }, | 631 }, |
682 | 632 |
(...skipping 28 matching lines...) Expand all Loading... |
711 /** | 661 /** |
712 * @return {?Element} | 662 * @return {?Element} |
713 */ | 663 */ |
714 proxyElementForTests: function() | 664 proxyElementForTests: function() |
715 { | 665 { |
716 return this._proxyElement || null; | 666 return this._proxyElement || null; |
717 }, | 667 }, |
718 | 668 |
719 __proto__: WebInspector.Object.prototype | 669 __proto__: WebInspector.Object.prototype |
720 }; | 670 }; |
OLD | NEW |