ClassesPaneWidget.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445
  1. export default class ClassesPaneWidget extends UI.Widget{constructor(){super(true);this.registerRequiredCSS('elements/classesPaneWidget.css');this.contentElement.className='styles-element-classes-pane';const container=this.contentElement.createChild('div','title-container');this._input=container.createChild('div','new-class-input monospace');this.setDefaultFocusedElement(this._input);this._classesContainer=this.contentElement.createChild('div','source-code');this._classesContainer.classList.add('styles-element-classes-container');this._prompt=new ClassNamePrompt(this._nodeClasses.bind(this));this._prompt.setAutocompletionTimeout(0);this._prompt.renderAsBlock();const proxyElement=this._prompt.attach(this._input);this._prompt.setPlaceholder(Common.UIString('Add new class'));this._prompt.addEventListener(UI.TextPrompt.Events.TextChanged,this._onTextChanged,this);proxyElement.addEventListener('keydown',this._onKeyDown.bind(this),false);SDK.targetManager.addModelListener(SDK.DOMModel,SDK.DOMModel.Events.DOMMutated,this._onDOMMutated,this);this._mutatingNodes=new Set();this._pendingNodeClasses=new Map();this._updateNodeThrottler=new Common.Throttler(0);this._previousTarget=null;UI.context.addFlavorChangeListener(SDK.DOMNode,this._onSelectedNodeChanged,this);}
  2. _splitTextIntoClasses(text){return text.split(/[.,\s]/).map(className=>className.trim()).filter(className=>className.length);}
  3. _onKeyDown(event){if(!isEnterKey(event)&&!isEscKey(event)){return;}
  4. if(isEnterKey(event)){event.consume();if(this._prompt.acceptAutoComplete()){return;}}
  5. let text=event.target.textContent;if(isEscKey(event)){if(!text.isWhitespace()){event.consume(true);}
  6. text='';}
  7. this._prompt.clearAutocomplete();event.target.textContent='';const node=UI.context.flavor(SDK.DOMNode);if(!node){return;}
  8. const classNames=this._splitTextIntoClasses(text);for(const className of classNames){this._toggleClass(node,className,true);}
  9. this._installNodeClasses(node);this._update();}
  10. _onTextChanged(){const node=UI.context.flavor(SDK.DOMNode);if(!node){return;}
  11. this._installNodeClasses(node);}
  12. _onDOMMutated(event){const node=(event.data);if(this._mutatingNodes.has(node)){return;}
  13. delete node[ClassesPaneWidget._classesSymbol];this._update();}
  14. _onSelectedNodeChanged(event){if(this._previousTarget&&this._prompt.text()){this._input.textContent='';this._installNodeClasses(this._previousTarget);}
  15. this._previousTarget=(event.data);this._update();}
  16. wasShown(){this._update();}
  17. _update(){if(!this.isShowing()){return;}
  18. let node=UI.context.flavor(SDK.DOMNode);if(node){node=node.enclosingElementOrSelf();}
  19. this._classesContainer.removeChildren();this._input.disabled=!node;if(!node){return;}
  20. const classes=this._nodeClasses(node);const keys=classes.keysArray();keys.sort(String.caseInsensetiveComparator);for(let i=0;i<keys.length;++i){const className=keys[i];const label=UI.CheckboxLabel.create(className,classes.get(className));label.classList.add('monospace');label.checkboxElement.addEventListener('click',this._onClick.bind(this,className),false);this._classesContainer.appendChild(label);}}
  21. _onClick(className,event){const node=UI.context.flavor(SDK.DOMNode);if(!node){return;}
  22. const enabled=event.target.checked;this._toggleClass(node,className,enabled);this._installNodeClasses(node);}
  23. _nodeClasses(node){let result=node[ClassesPaneWidget._classesSymbol];if(!result){const classAttribute=node.getAttribute('class')||'';const classes=classAttribute.split(/\s/);result=new Map();for(let i=0;i<classes.length;++i){const className=classes[i].trim();if(!className.length){continue;}
  24. result.set(className,true);}
  25. node[ClassesPaneWidget._classesSymbol]=result;}
  26. return result;}
  27. _toggleClass(node,className,enabled){const classes=this._nodeClasses(node);classes.set(className,enabled);}
  28. _installNodeClasses(node){const classes=this._nodeClasses(node);const activeClasses=new Set();for(const className of classes.keys()){if(classes.get(className)){activeClasses.add(className);}}
  29. const additionalClasses=this._splitTextIntoClasses(this._prompt.textWithCurrentSuggestion());for(const className of additionalClasses){activeClasses.add(className);}
  30. const newClasses=activeClasses.valuesArray();newClasses.sort();this._pendingNodeClasses.set(node,newClasses.join(' '));this._updateNodeThrottler.schedule(this._flushPendingClasses.bind(this));}
  31. _flushPendingClasses(){const promises=[];for(const node of this._pendingNodeClasses.keys()){this._mutatingNodes.add(node);const promise=node.setAttributeValuePromise('class',this._pendingNodeClasses.get(node)).then(onClassValueUpdated.bind(this,node));promises.push(promise);}
  32. this._pendingNodeClasses.clear();return Promise.all(promises);function onClassValueUpdated(node){this._mutatingNodes.delete(node);}}}
  33. ClassesPaneWidget._classesSymbol=Symbol('ClassesPaneWidget._classesSymbol');export class ButtonProvider{constructor(){this._button=new UI.ToolbarToggle(Common.UIString('Element Classes'),'');this._button.setText('.cls');this._button.element.classList.add('monospace');this._button.addEventListener(UI.ToolbarButton.Events.Click,this._clicked,this);this._view=new ClassesPaneWidget();}
  34. _clicked(){Elements.ElementsPanel.instance().showToolbarPane(!this._view.isShowing()?this._view:null,this._button);}
  35. item(){return this._button;}}
  36. export class ClassNamePrompt extends UI.TextPrompt{constructor(nodeClasses){super();this._nodeClasses=nodeClasses;this.initialize(this._buildClassNameCompletions.bind(this),' ');this.disableDefaultSuggestionForEmptyInput();this._selectedFrameId='';this._classNamesPromise=null;}
  37. _getClassNames(selectedNode){const promises=[];const completions=new Set();this._selectedFrameId=selectedNode.frameId();const cssModel=selectedNode.domModel().cssModel();const allStyleSheets=cssModel.allStyleSheets();for(const stylesheet of allStyleSheets){if(stylesheet.frameId!==this._selectedFrameId){continue;}
  38. const cssPromise=cssModel.classNamesPromise(stylesheet.id).then(classes=>completions.addAll(classes));promises.push(cssPromise);}
  39. const domPromise=selectedNode.domModel().classNamesPromise(selectedNode.ownerDocument.id).then(classes=>completions.addAll(classes));promises.push(domPromise);return Promise.all(promises).then(()=>completions.valuesArray());}
  40. _buildClassNameCompletions(expression,prefix,force){if(!prefix||force){this._classNamesPromise=null;}
  41. const selectedNode=UI.context.flavor(SDK.DOMNode);if(!selectedNode||(!prefix&&!force&&!expression.trim())){return Promise.resolve([]);}
  42. if(!this._classNamesPromise||this._selectedFrameId!==selectedNode.frameId()){this._classNamesPromise=this._getClassNames(selectedNode);}
  43. return this._classNamesPromise.then(completions=>{const classesMap=this._nodeClasses((selectedNode));completions=completions.filter(value=>!classesMap.get(value));if(prefix[0]==='.'){completions=completions.map(value=>'.'+value);}
  44. return completions.filter(value=>value.startsWith(prefix)).sort().map(completion=>({text:completion}));});}}
  45. self.Elements=self.Elements||{};Elements=Elements||{};Elements.ClassesPaneWidget=ClassesPaneWidget;Elements.ClassesPaneWidget.ButtonProvider=ButtonProvider;Elements.ClassesPaneWidget.ClassNamePrompt=ClassNamePrompt;