ConsoleViewport.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. export default class ConsoleViewport{constructor(provider){this.element=createElement('div');this.element.style.overflow='auto';this._topGapElement=this.element.createChild('div');this._topGapElement.style.height='0px';this._topGapElement.style.color='transparent';this._contentElement=this.element.createChild('div');this._bottomGapElement=this.element.createChild('div');this._bottomGapElement.style.height='0px';this._bottomGapElement.style.color='transparent';this._topGapElement.textContent='\uFEFF';this._bottomGapElement.textContent='\uFEFF';UI.ARIAUtils.markAsHidden(this._topGapElement);UI.ARIAUtils.markAsHidden(this._bottomGapElement);this._provider=provider;this.element.addEventListener('scroll',this._onScroll.bind(this),false);this.element.addEventListener('copy',this._onCopy.bind(this),false);this.element.addEventListener('dragstart',this._onDragStart.bind(this),false);this._contentElement.addEventListener('focusin',this._onFocusIn.bind(this),false);this._contentElement.addEventListener('focusout',this._onFocusOut.bind(this),false);this._contentElement.addEventListener('keydown',this._onKeyDown.bind(this),false);this._virtualSelectedIndex=-1;this._contentElement.tabIndex=-1;this._firstActiveIndex=-1;this._lastActiveIndex=-1;this._renderedItems=[];this._anchorSelection=null;this._headSelection=null;this._itemCount=0;this._cumulativeHeights=new Int32Array(0);this._muteCopyHandler=false;this._observer=new MutationObserver(this.refresh.bind(this));this._observerConfig={childList:true,subtree:true};}
  2. stickToBottom(){return this._stickToBottom;}
  3. setStickToBottom(value){this._stickToBottom=value;if(this._stickToBottom){this._observer.observe(this._contentElement,this._observerConfig);}else{this._observer.disconnect();}}
  4. hasVirtualSelection(){return this._virtualSelectedIndex!==-1;}
  5. copyWithStyles(){this._muteCopyHandler=true;this.element.ownerDocument.execCommand('copy');this._muteCopyHandler=false;}
  6. _onCopy(event){if(this._muteCopyHandler){return;}
  7. const text=this._selectedText();if(!text){return;}
  8. event.preventDefault();event.clipboardData.setData('text/plain',text);}
  9. _onFocusIn(event){const renderedIndex=this._renderedItems.findIndex(item=>item.element().isSelfOrAncestor(event.target));if(renderedIndex!==-1){this._virtualSelectedIndex=this._firstActiveIndex+renderedIndex;}
  10. let focusLastChild=false;if(this._virtualSelectedIndex===-1&&this._isOutsideViewport((event.relatedTarget))&&event.target===this._contentElement&&this._itemCount){focusLastChild=true;this._virtualSelectedIndex=this._itemCount-1;this.refresh();this.scrollItemIntoView(this._virtualSelectedIndex);}
  11. this._updateFocusedItem(focusLastChild);}
  12. _onFocusOut(event){if(this._isOutsideViewport((event.relatedTarget))){this._virtualSelectedIndex=-1;}
  13. this._updateFocusedItem();}
  14. _isOutsideViewport(element){return!!element&&!element.isSelfOrDescendant(this._contentElement);}
  15. _onDragStart(event){const text=this._selectedText();if(!text){return false;}
  16. event.dataTransfer.clearData();event.dataTransfer.setData('text/plain',text);event.dataTransfer.effectAllowed='copy';return true;}
  17. _onKeyDown(event){if(UI.isEditing()||!this._itemCount||event.shiftKey){return;}
  18. let isArrowUp=false;switch(event.key){case'ArrowUp':if(this._virtualSelectedIndex>0){isArrowUp=true;this._virtualSelectedIndex--;}else{return;}
  19. break;case'ArrowDown':if(this._virtualSelectedIndex<this._itemCount-1){this._virtualSelectedIndex++;}else{return;}
  20. break;case'Home':this._virtualSelectedIndex=0;break;case'End':this._virtualSelectedIndex=this._itemCount-1;break;default:return;}
  21. event.consume(true);this.scrollItemIntoView(this._virtualSelectedIndex);this._updateFocusedItem(isArrowUp);}
  22. _updateFocusedItem(focusLastChild){const selectedElement=this.renderedElementAt(this._virtualSelectedIndex);const changed=this._lastSelectedElement!==selectedElement;const containerHasFocus=this._contentElement===this.element.ownerDocument.deepActiveElement();if(this._lastSelectedElement&&changed){this._lastSelectedElement.classList.remove('console-selected');}
  23. if(selectedElement&&(focusLastChild||changed||containerHasFocus)&&this.element.hasFocus()){selectedElement.classList.add('console-selected');if(focusLastChild){this.setStickToBottom(false);this._renderedItems[this._virtualSelectedIndex-this._firstActiveIndex].focusLastChildOrSelf();}else if(!selectedElement.hasFocus()){focusWithoutScroll(selectedElement);}}
  24. if(this._itemCount&&!this._contentElement.hasFocus()){this._contentElement.tabIndex=0;}else{this._contentElement.tabIndex=-1;}
  25. this._lastSelectedElement=selectedElement;function focusWithoutScroll(element){element.focus({preventScroll:true});}}
  26. contentElement(){return this._contentElement;}
  27. invalidate(){delete this._cachedProviderElements;this._itemCount=this._provider.itemCount();if(this._virtualSelectedIndex>this._itemCount-1){this._virtualSelectedIndex=this._itemCount-1;}
  28. this._rebuildCumulativeHeights();this.refresh();}
  29. _providerElement(index){if(!this._cachedProviderElements){this._cachedProviderElements=new Array(this._itemCount);}
  30. let element=this._cachedProviderElements[index];if(!element){element=this._provider.itemElement(index);this._cachedProviderElements[index]=element;}
  31. return element;}
  32. _rebuildCumulativeHeights(){const firstActiveIndex=this._firstActiveIndex;const lastActiveIndex=this._lastActiveIndex;let height=0;this._cumulativeHeights=new Int32Array(this._itemCount);for(let i=0;i<this._itemCount;++i){if(firstActiveIndex<=i&&i-firstActiveIndex<this._renderedItems.length&&i<=lastActiveIndex){height+=this._renderedItems[i-firstActiveIndex].element().offsetHeight;}else{height+=this._provider.fastHeight(i);}
  33. this._cumulativeHeights[i]=height;}}
  34. _rebuildCumulativeHeightsIfNeeded(){let totalCachedHeight=0;let totalMeasuredHeight=0;for(let i=0;i<this._renderedItems.length;++i){const cachedItemHeight=this._cachedItemHeight(this._firstActiveIndex+i);const measuredHeight=this._renderedItems[i].element().offsetHeight;if(Math.abs(cachedItemHeight-measuredHeight)>1){this._rebuildCumulativeHeights();return;}
  35. totalMeasuredHeight+=measuredHeight;totalCachedHeight+=cachedItemHeight;if(Math.abs(totalCachedHeight-totalMeasuredHeight)>1){this._rebuildCumulativeHeights();return;}}}
  36. _cachedItemHeight(index){return index===0?this._cumulativeHeights[0]:this._cumulativeHeights[index]-this._cumulativeHeights[index-1];}
  37. _isSelectionBackwards(selection){if(!selection||!selection.rangeCount){return false;}
  38. const range=document.createRange();range.setStart(selection.anchorNode,selection.anchorOffset);range.setEnd(selection.focusNode,selection.focusOffset);return range.collapsed;}
  39. _createSelectionModel(itemIndex,node,offset){return{item:itemIndex,node:node,offset:offset};}
  40. _updateSelectionModel(selection){const range=selection&&selection.rangeCount?selection.getRangeAt(0):null;if(!range||selection.isCollapsed||!this.element.hasSelection()){this._headSelection=null;this._anchorSelection=null;return false;}
  41. let firstSelected=Number.MAX_VALUE;let lastSelected=-1;let hasVisibleSelection=false;for(let i=0;i<this._renderedItems.length;++i){if(range.intersectsNode(this._renderedItems[i].element())){const index=i+this._firstActiveIndex;firstSelected=Math.min(firstSelected,index);lastSelected=Math.max(lastSelected,index);hasVisibleSelection=true;}}
  42. if(hasVisibleSelection){firstSelected=this._createSelectionModel(firstSelected,(range.startContainer),range.startOffset);lastSelected=this._createSelectionModel(lastSelected,(range.endContainer),range.endOffset);}
  43. const topOverlap=range.intersectsNode(this._topGapElement)&&this._topGapElement._active;const bottomOverlap=range.intersectsNode(this._bottomGapElement)&&this._bottomGapElement._active;if(!topOverlap&&!bottomOverlap&&!hasVisibleSelection){this._headSelection=null;this._anchorSelection=null;return false;}
  44. if(!this._anchorSelection||!this._headSelection){this._anchorSelection=this._createSelectionModel(0,this.element,0);this._headSelection=this._createSelectionModel(this._itemCount-1,this.element,this.element.children.length);this._selectionIsBackward=false;}
  45. const isBackward=this._isSelectionBackwards(selection);const startSelection=this._selectionIsBackward?this._headSelection:this._anchorSelection;const endSelection=this._selectionIsBackward?this._anchorSelection:this._headSelection;if(topOverlap&&bottomOverlap&&hasVisibleSelection){firstSelected=firstSelected.item<startSelection.item?firstSelected:startSelection;lastSelected=lastSelected.item>endSelection.item?lastSelected:endSelection;}else if(!hasVisibleSelection){firstSelected=startSelection;lastSelected=endSelection;}else if(topOverlap){firstSelected=isBackward?this._headSelection:this._anchorSelection;}else if(bottomOverlap){lastSelected=isBackward?this._anchorSelection:this._headSelection;}
  46. if(isBackward){this._anchorSelection=lastSelected;this._headSelection=firstSelected;}else{this._anchorSelection=firstSelected;this._headSelection=lastSelected;}
  47. this._selectionIsBackward=isBackward;return true;}
  48. _restoreSelection(selection){let anchorElement=null;let anchorOffset;if(this._firstActiveIndex<=this._anchorSelection.item&&this._anchorSelection.item<=this._lastActiveIndex){anchorElement=this._anchorSelection.node;anchorOffset=this._anchorSelection.offset;}else{if(this._anchorSelection.item<this._firstActiveIndex){anchorElement=this._topGapElement;}else if(this._anchorSelection.item>this._lastActiveIndex){anchorElement=this._bottomGapElement;}
  49. anchorOffset=this._selectionIsBackward?1:0;}
  50. let headElement=null;let headOffset;if(this._firstActiveIndex<=this._headSelection.item&&this._headSelection.item<=this._lastActiveIndex){headElement=this._headSelection.node;headOffset=this._headSelection.offset;}else{if(this._headSelection.item<this._firstActiveIndex){headElement=this._topGapElement;}else if(this._headSelection.item>this._lastActiveIndex){headElement=this._bottomGapElement;}
  51. headOffset=this._selectionIsBackward?0:1;}
  52. selection.setBaseAndExtent(anchorElement,anchorOffset,headElement,headOffset);}
  53. refresh(){this._observer.disconnect();this._innerRefresh();if(this._stickToBottom){this._observer.observe(this._contentElement,this._observerConfig);}}
  54. _innerRefresh(){if(!this._visibleHeight()){return;}
  55. if(!this._itemCount){for(let i=0;i<this._renderedItems.length;++i){this._renderedItems[i].willHide();}
  56. this._renderedItems=[];this._contentElement.removeChildren();this._topGapElement.style.height='0px';this._bottomGapElement.style.height='0px';this._firstActiveIndex=-1;this._lastActiveIndex=-1;this._updateFocusedItem();return;}
  57. const selection=this.element.getComponentSelection();const shouldRestoreSelection=this._updateSelectionModel(selection);const visibleFrom=this.element.scrollTop;const visibleHeight=this._visibleHeight();const activeHeight=visibleHeight*2;this._rebuildCumulativeHeightsIfNeeded();if(this._stickToBottom){this._firstActiveIndex=Math.max(this._itemCount-Math.ceil(activeHeight/this._provider.minimumRowHeight()),0);this._lastActiveIndex=this._itemCount-1;}else{this._firstActiveIndex=Math.max(this._cumulativeHeights.lowerBound(visibleFrom+1-(activeHeight-visibleHeight)/2),0);this._lastActiveIndex=this._firstActiveIndex+Math.ceil(activeHeight/this._provider.minimumRowHeight())-1;this._lastActiveIndex=Math.min(this._lastActiveIndex,this._itemCount-1);}
  58. const topGapHeight=this._cumulativeHeights[this._firstActiveIndex-1]||0;const bottomGapHeight=this._cumulativeHeights[this._cumulativeHeights.length-1]-this._cumulativeHeights[this._lastActiveIndex];function prepare(){this._topGapElement.style.height=topGapHeight+'px';this._bottomGapElement.style.height=bottomGapHeight+'px';this._topGapElement._active=!!topGapHeight;this._bottomGapElement._active=!!bottomGapHeight;this._contentElement.style.setProperty('height','10000000px');}
  59. this._partialViewportUpdate(prepare.bind(this));this._contentElement.style.removeProperty('height');if(shouldRestoreSelection){this._restoreSelection(selection);}
  60. if(this._stickToBottom){this.element.scrollTop=10000000;}}
  61. _partialViewportUpdate(prepare){const itemsToRender=new Set();for(let i=this._firstActiveIndex;i<=this._lastActiveIndex;++i){itemsToRender.add(this._providerElement(i));}
  62. const willBeHidden=this._renderedItems.filter(item=>!itemsToRender.has(item));for(let i=0;i<willBeHidden.length;++i){willBeHidden[i].willHide();}
  63. prepare();let hadFocus=false;for(let i=0;i<willBeHidden.length;++i){hadFocus=hadFocus||willBeHidden[i].element().hasFocus();willBeHidden[i].element().remove();}
  64. const wasShown=[];let anchor=this._contentElement.firstChild;for(const viewportElement of itemsToRender){const element=viewportElement.element();if(element!==anchor){const shouldCallWasShown=!element.parentElement;if(shouldCallWasShown){wasShown.push(viewportElement);}
  65. this._contentElement.insertBefore(element,anchor);}else{anchor=anchor.nextSibling;}}
  66. for(let i=0;i<wasShown.length;++i){wasShown[i].wasShown();}
  67. this._renderedItems=Array.from(itemsToRender);if(hadFocus){this._contentElement.focus();}
  68. this._updateFocusedItem();}
  69. _selectedText(){this._updateSelectionModel(this.element.getComponentSelection());if(!this._headSelection||!this._anchorSelection){return null;}
  70. let startSelection=null;let endSelection=null;if(this._selectionIsBackward){startSelection=this._headSelection;endSelection=this._anchorSelection;}else{startSelection=this._anchorSelection;endSelection=this._headSelection;}
  71. const textLines=[];for(let i=startSelection.item;i<=endSelection.item;++i){const element=this._providerElement(i).element();const lineContent=element.childTextNodes().map(Components.Linkifier.untruncatedNodeText).join('');textLines.push(lineContent);}
  72. const endSelectionElement=this._providerElement(endSelection.item).element();if(endSelection.node&&endSelection.node.isSelfOrDescendant(endSelectionElement)){const itemTextOffset=this._textOffsetInNode(endSelectionElement,endSelection.node,endSelection.offset);textLines[textLines.length-1]=textLines.peekLast().substring(0,itemTextOffset);}
  73. const startSelectionElement=this._providerElement(startSelection.item).element();if(startSelection.node&&startSelection.node.isSelfOrDescendant(startSelectionElement)){const itemTextOffset=this._textOffsetInNode(startSelectionElement,startSelection.node,startSelection.offset);textLines[0]=textLines[0].substring(itemTextOffset);}
  74. return textLines.join('\n');}
  75. _textOffsetInNode(itemElement,selectionNode,offset){if(selectionNode.nodeType!==Node.TEXT_NODE){if(offset<selectionNode.childNodes.length){selectionNode=(selectionNode.childNodes.item(offset));offset=0;}else{offset=selectionNode.textContent.length;}}
  76. let chars=0;let node=itemElement;while((node=node.traverseNextNode(itemElement))&&node!==selectionNode){if(node.nodeType!==Node.TEXT_NODE||node.parentElement.nodeName==='STYLE'||node.parentElement.nodeName==='SCRIPT'){continue;}
  77. chars+=Components.Linkifier.untruncatedNodeText(node).length;}
  78. const untruncatedContainerLength=Components.Linkifier.untruncatedNodeText(selectionNode).length;if(offset>0&&untruncatedContainerLength!==selectionNode.textContent.length){offset=untruncatedContainerLength;}
  79. return chars+offset;}
  80. _onScroll(event){this.refresh();}
  81. firstVisibleIndex(){if(!this._cumulativeHeights.length){return-1;}
  82. this._rebuildCumulativeHeightsIfNeeded();return this._cumulativeHeights.lowerBound(this.element.scrollTop+1);}
  83. lastVisibleIndex(){if(!this._cumulativeHeights.length){return-1;}
  84. this._rebuildCumulativeHeightsIfNeeded();const scrollBottom=this.element.scrollTop+this.element.clientHeight;const right=this._itemCount-1;return this._cumulativeHeights.lowerBound(scrollBottom,undefined,undefined,right);}
  85. renderedElementAt(index){if(index===-1||index<this._firstActiveIndex||index>this._lastActiveIndex){return null;}
  86. return this._renderedItems[index-this._firstActiveIndex].element();}
  87. scrollItemIntoView(index,makeLast){const firstVisibleIndex=this.firstVisibleIndex();const lastVisibleIndex=this.lastVisibleIndex();if(index>firstVisibleIndex&&index<lastVisibleIndex){return;}
  88. if(index===lastVisibleIndex&&this._cumulativeHeights[index]<=this.element.scrollTop+this._visibleHeight()){return;}
  89. if(makeLast){this.forceScrollItemToBeLast(index);}else if(index<=firstVisibleIndex){this.forceScrollItemToBeFirst(index);}else if(index>=lastVisibleIndex){this.forceScrollItemToBeLast(index);}}
  90. forceScrollItemToBeFirst(index){console.assert(index>=0&&index<this._itemCount,'Cannot scroll item at invalid index');this.setStickToBottom(false);this._rebuildCumulativeHeightsIfNeeded();this.element.scrollTop=index>0?this._cumulativeHeights[index-1]:0;if(this.element.isScrolledToBottom()){this.setStickToBottom(true);}
  91. this.refresh();this.renderedElementAt(index).scrollIntoView(true);}
  92. forceScrollItemToBeLast(index){console.assert(index>=0&&index<this._itemCount,'Cannot scroll item at invalid index');this.setStickToBottom(false);this._rebuildCumulativeHeightsIfNeeded();this.element.scrollTop=this._cumulativeHeights[index]-this._visibleHeight();if(this.element.isScrolledToBottom()){this.setStickToBottom(true);}
  93. this.refresh();this.renderedElementAt(index).scrollIntoView(false);}
  94. _visibleHeight(){return this.element.offsetHeight;}}
  95. class ConsoleViewportProvider{fastHeight(index){return 0;}
  96. itemCount(){return 0;}
  97. minimumRowHeight(){return 0;}
  98. itemElement(index){return null;}}
  99. export class ConsoleViewportElement{willHide(){}
  100. wasShown(){}
  101. element(){}}
  102. self.Console=self.Console||{};Console=Console||{};Console.ConsoleViewport=ConsoleViewport;Console.ConsoleViewportProvider=ConsoleViewportProvider;Console.ConsoleViewportElement=ConsoleViewportElement;