Widget.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. export default class Widget extends Common.Object{constructor(isWebComponent,delegatesFocus){super();this.contentElement=createElementWithClass('div','widget');if(isWebComponent){this.element=createElementWithClass('div','vbox flex-auto');this._shadowRoot=UI.createShadowRootWithCoreStyles(this.element,undefined,delegatesFocus);this._shadowRoot.appendChild(this.contentElement);}else{this.element=this.contentElement;}
  2. this._isWebComponent=isWebComponent;this.element.__widget=this;this._visible=false;this._isRoot=false;this._isShowing=false;this._children=[];this._hideOnDetach=false;this._notificationDepth=0;this._invalidationsSuspended=0;this._defaultFocusedChild=null;}
  3. static _incrementWidgetCounter(parentElement,childElement){const count=(childElement.__widgetCounter||0)+(childElement.__widget?1:0);if(!count){return;}
  4. while(parentElement){parentElement.__widgetCounter=(parentElement.__widgetCounter||0)+count;parentElement=parentElement.parentElementOrShadowHost();}}
  5. static _decrementWidgetCounter(parentElement,childElement){const count=(childElement.__widgetCounter||0)+(childElement.__widget?1:0);if(!count){return;}
  6. while(parentElement){parentElement.__widgetCounter-=count;parentElement=parentElement.parentElementOrShadowHost();}}
  7. static __assert(condition,message){if(!condition){throw new Error(message);}}
  8. static focusWidgetForNode(node){while(node){if(node.__widget){break;}
  9. node=node.parentNodeOrShadowHost();}
  10. if(!node){return;}
  11. let widget=node.__widget;while(widget._parentWidget){widget._parentWidget._defaultFocusedChild=widget;widget=widget._parentWidget;}}
  12. markAsRoot(){Widget.__assert(!this.element.parentElement,'Attempt to mark as root attached node');this._isRoot=true;}
  13. parentWidget(){return this._parentWidget;}
  14. children(){return this._children;}
  15. childWasDetached(widget){}
  16. isShowing(){return this._isShowing;}
  17. shouldHideOnDetach(){if(!this.element.parentElement){return false;}
  18. if(this._hideOnDetach){return true;}
  19. for(const child of this._children){if(child.shouldHideOnDetach()){return true;}}
  20. return false;}
  21. setHideOnDetach(){this._hideOnDetach=true;}
  22. _inNotification(){return!!this._notificationDepth||(this._parentWidget&&this._parentWidget._inNotification());}
  23. _parentIsShowing(){if(this._isRoot){return true;}
  24. return!!this._parentWidget&&this._parentWidget.isShowing();}
  25. _callOnVisibleChildren(method){const copy=this._children.slice();for(let i=0;i<copy.length;++i){if(copy[i]._parentWidget===this&&copy[i]._visible){method.call(copy[i]);}}}
  26. _processWillShow(){this._callOnVisibleChildren(this._processWillShow);this._isShowing=true;}
  27. _processWasShown(){if(this._inNotification()){return;}
  28. this.restoreScrollPositions();this._notify(this.wasShown);this._callOnVisibleChildren(this._processWasShown);}
  29. _processWillHide(){if(this._inNotification()){return;}
  30. this.storeScrollPositions();this._callOnVisibleChildren(this._processWillHide);this._notify(this.willHide);this._isShowing=false;}
  31. _processWasHidden(){this._callOnVisibleChildren(this._processWasHidden);}
  32. _processOnResize(){if(this._inNotification()){return;}
  33. if(!this.isShowing()){return;}
  34. this._notify(this.onResize);this._callOnVisibleChildren(this._processOnResize);}
  35. _notify(notification){++this._notificationDepth;try{notification.call(this);}finally{--this._notificationDepth;}}
  36. wasShown(){}
  37. willHide(){}
  38. onResize(){}
  39. onLayout(){}
  40. ownerViewDisposed(){}
  41. show(parentElement,insertBefore){Widget.__assert(parentElement,'Attempt to attach widget with no parent element');if(!this._isRoot){let currentParent=parentElement;while(currentParent&&!currentParent.__widget){currentParent=currentParent.parentElementOrShadowHost();}
  42. Widget.__assert(currentParent,'Attempt to attach widget to orphan node');this._attach(currentParent.__widget);}
  43. this._showWidget(parentElement,insertBefore);}
  44. _attach(parentWidget){if(parentWidget===this._parentWidget){return;}
  45. if(this._parentWidget){this.detach();}
  46. this._parentWidget=parentWidget;this._parentWidget._children.push(this);this._isRoot=false;}
  47. showWidget(){if(this._visible){return;}
  48. Widget.__assert(this.element.parentElement,'Attempt to show widget that is not hidden using hideWidget().');this._showWidget((this.element.parentElement),this.element.nextSibling);}
  49. _showWidget(parentElement,insertBefore){let currentParent=parentElement;while(currentParent&&!currentParent.__widget){currentParent=currentParent.parentElementOrShadowHost();}
  50. if(this._isRoot){Widget.__assert(!currentParent,'Attempt to show root widget under another widget');}else{Widget.__assert(currentParent&&currentParent.__widget===this._parentWidget,'Attempt to show under node belonging to alien widget');}
  51. const wasVisible=this._visible;if(wasVisible&&this.element.parentElement===parentElement){return;}
  52. this._visible=true;if(!wasVisible&&this._parentIsShowing()){this._processWillShow();}
  53. this.element.classList.remove('hidden');if(this.element.parentElement!==parentElement){if(!this._externallyManaged){Widget._incrementWidgetCounter(parentElement,this.element);}
  54. if(insertBefore){Widget._originalInsertBefore.call(parentElement,this.element,insertBefore);}else{Widget._originalAppendChild.call(parentElement,this.element);}}
  55. if(!wasVisible&&this._parentIsShowing()){this._processWasShown();}
  56. if(this._parentWidget&&this._hasNonZeroConstraints()){this._parentWidget.invalidateConstraints();}else{this._processOnResize();}}
  57. hideWidget(){if(!this._visible){return;}
  58. this._hideWidget(false);}
  59. _hideWidget(removeFromDOM){this._visible=false;const parentElement=this.element.parentElement;if(this._parentIsShowing()){this._processWillHide();}
  60. if(removeFromDOM){Widget._decrementWidgetCounter(parentElement,this.element);Widget._originalRemoveChild.call(parentElement,this.element);}else{this.element.classList.add('hidden');}
  61. if(this._parentIsShowing()){this._processWasHidden();}
  62. if(this._parentWidget&&this._hasNonZeroConstraints()){this._parentWidget.invalidateConstraints();}}
  63. detach(overrideHideOnDetach){if(!this._parentWidget&&!this._isRoot){return;}
  64. const removeFromDOM=overrideHideOnDetach||!this.shouldHideOnDetach();if(this._visible){this._hideWidget(removeFromDOM);}else if(removeFromDOM&&this.element.parentElement){const parentElement=this.element.parentElement;Widget._decrementWidgetCounter(parentElement,this.element);Widget._originalRemoveChild.call(parentElement,this.element);}
  65. if(this._parentWidget){const childIndex=this._parentWidget._children.indexOf(this);Widget.__assert(childIndex>=0,'Attempt to remove non-child widget');this._parentWidget._children.splice(childIndex,1);if(this._parentWidget._defaultFocusedChild===this){this._parentWidget._defaultFocusedChild=null;}
  66. this._parentWidget.childWasDetached(this);this._parentWidget=null;}else{Widget.__assert(this._isRoot,'Removing non-root widget from DOM');}}
  67. detachChildWidgets(){const children=this._children.slice();for(let i=0;i<children.length;++i){children[i].detach();}}
  68. elementsToRestoreScrollPositionsFor(){return[this.element];}
  69. storeScrollPositions(){const elements=this.elementsToRestoreScrollPositionsFor();for(let i=0;i<elements.length;++i){const container=elements[i];container._scrollTop=container.scrollTop;container._scrollLeft=container.scrollLeft;}}
  70. restoreScrollPositions(){const elements=this.elementsToRestoreScrollPositionsFor();for(let i=0;i<elements.length;++i){const container=elements[i];if(container._scrollTop){container.scrollTop=container._scrollTop;}
  71. if(container._scrollLeft){container.scrollLeft=container._scrollLeft;}}}
  72. doResize(){if(!this.isShowing()){return;}
  73. if(!this._inNotification()){this._callOnVisibleChildren(this._processOnResize);}}
  74. doLayout(){if(!this.isShowing()){return;}
  75. this._notify(this.onLayout);this.doResize();}
  76. registerRequiredCSS(cssFile){UI.appendStyle(this._isWebComponent?this._shadowRoot:this.element,cssFile);}
  77. printWidgetHierarchy(){const lines=[];this._collectWidgetHierarchy('',lines);console.log(lines.join('\n'));}
  78. _collectWidgetHierarchy(prefix,lines){lines.push(prefix+'['+this.element.className+']'+(this._children.length?' {':''));for(let i=0;i<this._children.length;++i){this._children[i]._collectWidgetHierarchy(prefix+' ',lines);}
  79. if(this._children.length){lines.push(prefix+'}');}}
  80. setDefaultFocusedElement(element){this._defaultFocusedElement=element;}
  81. setDefaultFocusedChild(child){Widget.__assert(child._parentWidget===this,'Attempt to set non-child widget as default focused.');this._defaultFocusedChild=child;}
  82. focus(){if(!this.isShowing()){return;}
  83. const element=this._defaultFocusedElement;if(element){if(!element.hasFocus()){element.focus();}
  84. return;}
  85. if(this._defaultFocusedChild&&this._defaultFocusedChild._visible){this._defaultFocusedChild.focus();}else{for(const child of this._children){if(child._visible){child.focus();return;}}
  86. let child=this.contentElement.traverseNextNode(this.contentElement);while(child){if(child instanceof UI.XWidget){child.focus();return;}
  87. child=child.traverseNextNode(this.contentElement);}}}
  88. hasFocus(){return this.element.hasFocus();}
  89. calculateConstraints(){return new UI.Constraints();}
  90. constraints(){if(typeof this._constraints!=='undefined'){return this._constraints;}
  91. if(typeof this._cachedConstraints==='undefined'){this._cachedConstraints=this.calculateConstraints();}
  92. return this._cachedConstraints;}
  93. setMinimumAndPreferredSizes(width,height,preferredWidth,preferredHeight){this._constraints=new UI.Constraints(new UI.Size(width,height),new UI.Size(preferredWidth,preferredHeight));this.invalidateConstraints();}
  94. setMinimumSize(width,height){this._constraints=new UI.Constraints(new UI.Size(width,height));this.invalidateConstraints();}
  95. _hasNonZeroConstraints(){const constraints=this.constraints();return!!(constraints.minimum.width||constraints.minimum.height||constraints.preferred.width||constraints.preferred.height);}
  96. suspendInvalidations(){++this._invalidationsSuspended;}
  97. resumeInvalidations(){--this._invalidationsSuspended;if(!this._invalidationsSuspended&&this._invalidationsRequested){this.invalidateConstraints();}}
  98. invalidateConstraints(){if(this._invalidationsSuspended){this._invalidationsRequested=true;return;}
  99. this._invalidationsRequested=false;const cached=this._cachedConstraints;delete this._cachedConstraints;const actual=this.constraints();if(!actual.isEqual(cached)&&this._parentWidget){this._parentWidget.invalidateConstraints();}else{this.doLayout();}}
  100. markAsExternallyManaged(){Widget.__assert(!this._parentWidget,'Attempt to mark widget as externally managed after insertion to the DOM');this._externallyManaged=true;}}
  101. export const _originalAppendChild=Element.prototype.appendChild;export const _originalInsertBefore=Element.prototype.insertBefore;export const _originalRemoveChild=Element.prototype.removeChild;export const _originalRemoveChildren=Element.prototype.removeChildren;export class VBox extends Widget{constructor(isWebComponent,delegatesFocus){super(isWebComponent,delegatesFocus);this.contentElement.classList.add('vbox');}
  102. calculateConstraints(){let constraints=new UI.Constraints();function updateForChild(){const child=this.constraints();constraints=constraints.widthToMax(child);constraints=constraints.addHeight(child);}
  103. this._callOnVisibleChildren(updateForChild);return constraints;}}
  104. export class HBox extends Widget{constructor(isWebComponent){super(isWebComponent);this.contentElement.classList.add('hbox');}
  105. calculateConstraints(){let constraints=new UI.Constraints();function updateForChild(){const child=this.constraints();constraints=constraints.addWidth(child);constraints=constraints.heightToMax(child);}
  106. this._callOnVisibleChildren(updateForChild);return constraints;}}
  107. export class VBoxWithResizeCallback extends VBox{constructor(resizeCallback){super();this._resizeCallback=resizeCallback;}
  108. onResize(){this._resizeCallback();}}
  109. export class WidgetFocusRestorer{constructor(widget){this._widget=widget;this._previous=widget.element.ownerDocument.deepActiveElement();widget.focus();}
  110. restore(){if(!this._widget){return;}
  111. if(this._widget.hasFocus()&&this._previous){this._previous.focus();}
  112. this._previous=null;this._widget=null;}}
  113. Element.prototype.appendChild=function(child){Widget.__assert(!child.__widget||child.parentElement===this,'Attempt to add widget via regular DOM operation.');return Widget._originalAppendChild.call(this,child);};Element.prototype.insertBefore=function(child,anchor){Widget.__assert(!child.__widget||child.parentElement===this,'Attempt to add widget via regular DOM operation.');return Widget._originalInsertBefore.call(this,child,anchor);};Element.prototype.removeChild=function(child){Widget.__assert(!child.__widgetCounter&&!child.__widget,'Attempt to remove element containing widget via regular DOM operation');return Widget._originalRemoveChild.call(this,child);};Element.prototype.removeChildren=function(){Widget.__assert(!this.__widgetCounter,'Attempt to remove element containing widget via regular DOM operation');Widget._originalRemoveChildren.call(this);};self.UI=self.UI||{};UI=UI||{};UI.Widget=Widget;Widget._originalAppendChild=_originalAppendChild;Widget._originalInsertBefore=_originalInsertBefore;Widget._originalRemoveChild=_originalRemoveChild;Widget._originalRemoveChildren=_originalRemoveChildren;UI.HBox=HBox;UI.VBox=VBox;UI.WidgetFocusRestorer=WidgetFocusRestorer;UI.VBoxWithResizeCallback=VBoxWithResizeCallback;