TextPrompt.js 14 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. export default class TextPrompt extends Common.Object{constructor(){super();this._proxyElement;this._proxyElementDisplay='inline-block';this._autocompletionTimeout=DefaultAutocompletionTimeout;this._title='';this._queryRange=null;this._previousText='';this._currentSuggestion=null;this._completionRequestId=0;this._ghostTextElement=createElementWithClass('span','auto-complete-text');this._ghostTextElement.setAttribute('contenteditable','false');UI.ARIAUtils.markAsHidden(this._ghostTextElement);}
  2. initialize(completions,stopCharacters){this._loadCompletions=completions;this._completionStopCharacters=stopCharacters||' =:[({;,!+-*/&|^<>.';}
  3. setAutocompletionTimeout(timeout){this._autocompletionTimeout=timeout;}
  4. renderAsBlock(){this._proxyElementDisplay='block';}
  5. attach(element){return this._attachInternal(element);}
  6. attachAndStartEditing(element,blurListener){const proxyElement=this._attachInternal(element);this._startEditing(blurListener);return proxyElement;}
  7. _attachInternal(element){if(this._proxyElement){throw'Cannot attach an attached TextPrompt';}
  8. this._element=element;this._boundOnKeyDown=this.onKeyDown.bind(this);this._boundOnInput=this.onInput.bind(this);this._boundOnMouseWheel=this.onMouseWheel.bind(this);this._boundClearAutocomplete=this.clearAutocomplete.bind(this);this._proxyElement=element.ownerDocument.createElement('span');UI.appendStyle(this._proxyElement,'ui/textPrompt.css');this._contentElement=this._proxyElement.createChild('div','text-prompt-root');this._proxyElement.style.display=this._proxyElementDisplay;element.parentElement.insertBefore(this._proxyElement,element);this._contentElement.appendChild(element);this._element.classList.add('text-prompt');UI.ARIAUtils.markAsTextBox(this._element);this._element.setAttribute('contenteditable','plaintext-only');this._element.addEventListener('keydown',this._boundOnKeyDown,false);this._element.addEventListener('input',this._boundOnInput,false);this._element.addEventListener('mousewheel',this._boundOnMouseWheel,false);this._element.addEventListener('selectstart',this._boundClearAutocomplete,false);this._element.addEventListener('blur',this._boundClearAutocomplete,false);this._suggestBox=new UI.SuggestBox(this,20);if(this._title){this._proxyElement.title=this._title;}
  9. return this._proxyElement;}
  10. detach(){this._removeFromElement();this._focusRestorer.restore();this._proxyElement.parentElement.insertBefore(this._element,this._proxyElement);this._proxyElement.remove();delete this._proxyElement;this._element.classList.remove('text-prompt');this._element.removeAttribute('contenteditable');this._element.removeAttribute('role');}
  11. textWithCurrentSuggestion(){const text=this.text();if(!this._queryRange||!this._currentSuggestion){return text;}
  12. const suggestion=this._currentSuggestion.text;return text.substring(0,this._queryRange.startColumn)+suggestion+text.substring(this._queryRange.endColumn);}
  13. text(){let text=this._element.textContent;if(this._ghostTextElement.parentNode){const addition=this._ghostTextElement.textContent;text=text.substring(0,text.length-addition.length);}
  14. return text;}
  15. setText(text){this.clearAutocomplete();this._element.textContent=text;this._previousText=this.text();if(this._element.hasFocus()){this.moveCaretToEndOfPrompt();this._element.scrollIntoView();}}
  16. focus(){this._element.focus();}
  17. title(){return this._title;}
  18. setTitle(title){this._title=title;if(this._proxyElement){this._proxyElement.title=title;}}
  19. setPlaceholder(placeholder,ariaPlaceholder){if(placeholder){this._element.setAttribute('data-placeholder',placeholder);UI.ARIAUtils.setPlaceholder(this._element,ariaPlaceholder||placeholder);}else{this._element.removeAttribute('data-placeholder');UI.ARIAUtils.setPlaceholder(this._element,null);}}
  20. setEnabled(enabled){if(enabled){this._element.setAttribute('contenteditable','plaintext-only');}else{this._element.removeAttribute('contenteditable');}
  21. this._element.classList.toggle('disabled',!enabled);}
  22. _removeFromElement(){this.clearAutocomplete();this._element.removeEventListener('keydown',this._boundOnKeyDown,false);this._element.removeEventListener('input',this._boundOnInput,false);this._element.removeEventListener('selectstart',this._boundClearAutocomplete,false);this._element.removeEventListener('blur',this._boundClearAutocomplete,false);if(this._isEditing){this._stopEditing();}
  23. if(this._suggestBox){this._suggestBox.hide();}}
  24. _startEditing(blurListener){this._isEditing=true;this._contentElement.classList.add('text-prompt-editing');if(blurListener){this._blurListener=blurListener;this._element.addEventListener('blur',this._blurListener,false);}
  25. this._oldTabIndex=this._element.tabIndex;if(this._element.tabIndex<0){this._element.tabIndex=0;}
  26. this._focusRestorer=new UI.ElementFocusRestorer(this._element);if(!this.text()){this.autoCompleteSoon();}}
  27. _stopEditing(){this._element.tabIndex=this._oldTabIndex;if(this._blurListener){this._element.removeEventListener('blur',this._blurListener,false);}
  28. this._contentElement.classList.remove('text-prompt-editing');delete this._isEditing;}
  29. onMouseWheel(event){}
  30. onKeyDown(event){let handled=false;if(this.isSuggestBoxVisible()&&this._suggestBox.keyPressed(event)){event.consume(true);return;}
  31. switch(event.key){case'Tab':handled=this.tabKeyPressed(event);break;case'ArrowLeft':case'ArrowUp':case'PageUp':case'Home':this.clearAutocomplete();break;case'PageDown':case'ArrowRight':case'ArrowDown':case'End':if(this._isCaretAtEndOfPrompt()){handled=this.acceptAutoComplete();}else{this.clearAutocomplete();}
  32. break;case'Escape':if(this.isSuggestBoxVisible()){this.clearAutocomplete();handled=true;}
  33. break;case' ':if(event.ctrlKey&&!event.metaKey&&!event.altKey&&!event.shiftKey){this.autoCompleteSoon(true);handled=true;}
  34. break;}
  35. if(isEnterKey(event)){event.preventDefault();}
  36. if(handled){event.consume(true);}}
  37. _acceptSuggestionOnStopCharacters(key){if(!this._currentSuggestion||!this._queryRange||key.length!==1||!this._completionStopCharacters.includes(key)){return false;}
  38. const query=this.text().substring(this._queryRange.startColumn,this._queryRange.endColumn);if(query&&this._currentSuggestion.text.startsWith(query+key)){this._queryRange.endColumn+=1;return this.acceptAutoComplete();}
  39. return false;}
  40. onInput(event){const text=this.text();if(event.data&&!this._acceptSuggestionOnStopCharacters(event.data)){const hasCommonPrefix=text.startsWith(this._previousText)||this._previousText.startsWith(text);if(this._queryRange&&hasCommonPrefix){this._queryRange.endColumn+=text.length-this._previousText.length;}}
  41. this._refreshGhostText();this._previousText=text;this.dispatchEventToListeners(Events.TextChanged);this.autoCompleteSoon();}
  42. acceptAutoComplete(){let result=false;if(this.isSuggestBoxVisible()){result=this._suggestBox.acceptSuggestion();}
  43. if(!result){result=this._acceptSuggestionInternal();}
  44. return result;}
  45. clearAutocomplete(){const beforeText=this.textWithCurrentSuggestion();if(this.isSuggestBoxVisible()){this._suggestBox.hide();}
  46. this._clearAutocompleteTimeout();this._queryRange=null;this._refreshGhostText();if(beforeText!==this.textWithCurrentSuggestion()){this.dispatchEventToListeners(Events.TextChanged);}}
  47. _refreshGhostText(){if(this._currentSuggestion&&this._currentSuggestion.hideGhostText){this._ghostTextElement.remove();return;}
  48. if(this._queryRange&&this._currentSuggestion&&this._isCaretAtEndOfPrompt()&&this._currentSuggestion.text.startsWith(this.text().substring(this._queryRange.startColumn))){this._ghostTextElement.textContent=this._currentSuggestion.text.substring(this._queryRange.endColumn-this._queryRange.startColumn);this._element.appendChild(this._ghostTextElement);}else{this._ghostTextElement.remove();}}
  49. _clearAutocompleteTimeout(){if(this._completeTimeout){clearTimeout(this._completeTimeout);delete this._completeTimeout;}
  50. this._completionRequestId++;}
  51. autoCompleteSoon(force){const immediately=this.isSuggestBoxVisible()||force;if(!this._completeTimeout){this._completeTimeout=setTimeout(this.complete.bind(this,force),immediately?0:this._autocompletionTimeout);}}
  52. async complete(force){this._clearAutocompleteTimeout();const selection=this._element.getComponentSelection();const selectionRange=selection&&selection.rangeCount?selection.getRangeAt(0):null;if(!selectionRange){return;}
  53. let shouldExit;if(!force&&!this._isCaretAtEndOfPrompt()&&!this.isSuggestBoxVisible()){shouldExit=true;}else if(!selection.isCollapsed){shouldExit=true;}
  54. if(shouldExit){this.clearAutocomplete();return;}
  55. const wordQueryRange=selectionRange.startContainer.rangeOfWord(selectionRange.startOffset,this._completionStopCharacters,this._element,'backward');const expressionRange=wordQueryRange.cloneRange();expressionRange.collapse(true);expressionRange.setStartBefore(this._element);const completionRequestId=++this._completionRequestId;const completions=await this._loadCompletions(expressionRange.toString(),wordQueryRange.toString(),!!force);this._completionsReady(completionRequestId,selection,wordQueryRange,!!force,completions);}
  56. disableDefaultSuggestionForEmptyInput(){this._disableDefaultSuggestionForEmptyInput=true;}
  57. _boxForAnchorAtStart(selection,textRange){const rangeCopy=selection.getRangeAt(0).cloneRange();const anchorElement=createElement('span');anchorElement.textContent='\u200B';textRange.insertNode(anchorElement);const box=anchorElement.boxInWindow(window);anchorElement.remove();selection.removeAllRanges();selection.addRange(rangeCopy);return box;}
  58. _createRange(){return document.createRange();}
  59. additionalCompletions(query){return[];}
  60. _completionsReady(completionRequestId,selection,originalWordQueryRange,force,completions){if(this._completionRequestId!==completionRequestId){return;}
  61. const query=originalWordQueryRange.toString();const store=new Set();completions=completions.filter(item=>!store.has(item.text)&&!!store.add(item.text));if(query||force){if(query){completions=completions.concat(this.additionalCompletions(query));}else{completions=this.additionalCompletions(query).concat(completions);}}
  62. if(!completions.length){this.clearAutocomplete();return;}
  63. const selectionRange=selection.getRangeAt(0);const fullWordRange=this._createRange();fullWordRange.setStart(originalWordQueryRange.startContainer,originalWordQueryRange.startOffset);fullWordRange.setEnd(selectionRange.endContainer,selectionRange.endOffset);if(query+selectionRange.toString()!==fullWordRange.toString()){return;}
  64. const beforeRange=this._createRange();beforeRange.setStart(this._element,0);beforeRange.setEnd(fullWordRange.startContainer,fullWordRange.startOffset);this._queryRange=new TextUtils.TextRange(0,beforeRange.toString().length,0,beforeRange.toString().length+fullWordRange.toString().length);const shouldSelect=!this._disableDefaultSuggestionForEmptyInput||!!this.text();if(this._suggestBox){this._suggestBox.updateSuggestions(this._boxForAnchorAtStart(selection,fullWordRange),completions,shouldSelect,!this._isCaretAtEndOfPrompt(),this.text());}}
  65. applySuggestion(suggestion,isIntermediateSuggestion){this._currentSuggestion=suggestion;this._refreshGhostText();if(isIntermediateSuggestion){this.dispatchEventToListeners(Events.TextChanged);}}
  66. acceptSuggestion(){this._acceptSuggestionInternal();}
  67. _acceptSuggestionInternal(){if(!this._queryRange){return false;}
  68. const suggestionLength=this._currentSuggestion?this._currentSuggestion.text.length:0;const selectionRange=this._currentSuggestion?this._currentSuggestion.selectionRange:null;const endColumn=selectionRange?selectionRange.endColumn:suggestionLength;const startColumn=selectionRange?selectionRange.startColumn:suggestionLength;this._element.textContent=this.textWithCurrentSuggestion();this.setDOMSelection(this._queryRange.startColumn+startColumn,this._queryRange.startColumn+endColumn);this.clearAutocomplete();this.dispatchEventToListeners(Events.TextChanged);return true;}
  69. setDOMSelection(startColumn,endColumn){this._element.normalize();const node=this._element.childNodes[0];if(!node||node===this._ghostTextElement){return;}
  70. const range=this._createRange();range.setStart(node,startColumn);range.setEnd(node,endColumn);const selection=this._element.getComponentSelection();selection.removeAllRanges();selection.addRange(range);}
  71. isSuggestBoxVisible(){return this._suggestBox&&this._suggestBox.visible();}
  72. isCaretInsidePrompt(){const selection=this._element.getComponentSelection();const selectionRange=selection&&selection.rangeCount?selection.getRangeAt(0):null;if(!selectionRange||!selection.isCollapsed){return false;}
  73. return selectionRange.startContainer.isSelfOrDescendant(this._element);}
  74. _isCaretAtEndOfPrompt(){const selection=this._element.getComponentSelection();const selectionRange=selection&&selection.rangeCount?selection.getRangeAt(0):null;if(!selectionRange||!selection.isCollapsed){return false;}
  75. let node=selectionRange.startContainer;if(!node.isSelfOrDescendant(this._element)){return false;}
  76. if(this._ghostTextElement.isAncestor(node)){return true;}
  77. if(node.nodeType===Node.TEXT_NODE&&selectionRange.startOffset<node.nodeValue.length){return false;}
  78. let foundNextText=false;while(node){if(node.nodeType===Node.TEXT_NODE&&node.nodeValue.length){if(foundNextText&&!this._ghostTextElement.isAncestor(node)){return false;}
  79. foundNextText=true;}
  80. node=node.traverseNextNode(this._element);}
  81. return true;}
  82. moveCaretToEndOfPrompt(){const selection=this._element.getComponentSelection();const selectionRange=this._createRange();let container=this._element;while(container.childNodes.length){container=container.lastChild;}
  83. const offset=container.nodeType===Node.TEXT_NODE?container.textContent.length:0;selectionRange.setStart(container,offset);selectionRange.setEnd(container,offset);selection.removeAllRanges();selection.addRange(selectionRange);}
  84. tabKeyPressed(event){return this.acceptAutoComplete();}
  85. proxyElementForTests(){return this._proxyElement||null;}}
  86. const DefaultAutocompletionTimeout=250;export const Events={TextChanged:Symbol('TextChanged')};self.UI=self.UI||{};UI=UI||{};UI.TextPrompt=TextPrompt;UI.TextPrompt.Events=Events;