| OLD | NEW |
| 1 // Copyright (c) 2015 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | |
| 5 /** | 4 /** |
| 6 * @constructor | 5 * @unrestricted |
| 7 * @extends {WebInspector.Widget} | |
| 8 */ | 6 */ |
| 9 WebInspector.ClassesPaneWidget = function() | 7 WebInspector.ClassesPaneWidget = class extends WebInspector.Widget { |
| 10 { | 8 constructor() { |
| 11 WebInspector.Widget.call(this); | 9 super(); |
| 12 this.element.className = "styles-element-classes-pane"; | 10 this.element.className = 'styles-element-classes-pane'; |
| 13 var container = this.element.createChild("div", "title-container"); | 11 var container = this.element.createChild('div', 'title-container'); |
| 14 this._input = container.createChild("div", "new-class-input monospace"); | 12 this._input = container.createChild('div', 'new-class-input monospace'); |
| 15 this._input.setAttribute("placeholder", WebInspector.UIString("Add new class
")); | 13 this._input.setAttribute('placeholder', WebInspector.UIString('Add new class
')); |
| 16 this.setDefaultFocusedElement(this._input); | 14 this.setDefaultFocusedElement(this._input); |
| 17 this._classesContainer = this.element.createChild("div", "source-code"); | 15 this._classesContainer = this.element.createChild('div', 'source-code'); |
| 18 this._classesContainer.classList.add("styles-element-classes-container"); | 16 this._classesContainer.classList.add('styles-element-classes-container'); |
| 19 this._prompt = new WebInspector.ClassesPaneWidget.ClassNamePrompt(); | 17 this._prompt = new WebInspector.ClassesPaneWidget.ClassNamePrompt(); |
| 20 this._prompt.setAutocompletionTimeout(0); | 18 this._prompt.setAutocompletionTimeout(0); |
| 21 this._prompt.renderAsBlock(); | 19 this._prompt.renderAsBlock(); |
| 22 | 20 |
| 23 var proxyElement = this._prompt.attach(this._input); | 21 var proxyElement = this._prompt.attach(this._input); |
| 24 proxyElement.addEventListener("keydown", this._onKeyDown.bind(this), false); | 22 proxyElement.addEventListener('keydown', this._onKeyDown.bind(this), false); |
| 25 | 23 |
| 26 WebInspector.targetManager.addModelListener(WebInspector.DOMModel, WebInspec
tor.DOMModel.Events.DOMMutated, this._onDOMMutated, this); | 24 WebInspector.targetManager.addModelListener( |
| 25 WebInspector.DOMModel, WebInspector.DOMModel.Events.DOMMutated, this._on
DOMMutated, this); |
| 27 /** @type {!Set<!WebInspector.DOMNode>} */ | 26 /** @type {!Set<!WebInspector.DOMNode>} */ |
| 28 this._mutatingNodes = new Set(); | 27 this._mutatingNodes = new Set(); |
| 29 WebInspector.context.addFlavorChangeListener(WebInspector.DOMNode, this._upd
ate, this); | 28 WebInspector.context.addFlavorChangeListener(WebInspector.DOMNode, this._upd
ate, this); |
| 29 } |
| 30 |
| 31 /** |
| 32 * @param {!Event} event |
| 33 */ |
| 34 _onKeyDown(event) { |
| 35 var text = event.target.textContent; |
| 36 if (isEscKey(event)) { |
| 37 event.target.textContent = ''; |
| 38 if (!text.isWhitespace()) |
| 39 event.consume(true); |
| 40 return; |
| 41 } |
| 42 |
| 43 if (!isEnterKey(event)) |
| 44 return; |
| 45 var node = WebInspector.context.flavor(WebInspector.DOMNode); |
| 46 if (!node) |
| 47 return; |
| 48 |
| 49 this._prompt.clearAutocomplete(); |
| 50 event.target.textContent = ''; |
| 51 var classNames = text.split(/[.,\s]/); |
| 52 for (var className of classNames) { |
| 53 var className = className.trim(); |
| 54 if (!className.length) |
| 55 continue; |
| 56 this._toggleClass(node, className, true); |
| 57 } |
| 58 this._installNodeClasses(node); |
| 59 this._update(); |
| 60 event.consume(true); |
| 61 } |
| 62 |
| 63 /** |
| 64 * @param {!WebInspector.Event} event |
| 65 */ |
| 66 _onDOMMutated(event) { |
| 67 var node = /** @type {!WebInspector.DOMNode} */ (event.data); |
| 68 if (this._mutatingNodes.has(node)) |
| 69 return; |
| 70 delete node[WebInspector.ClassesPaneWidget._classesSymbol]; |
| 71 this._update(); |
| 72 } |
| 73 |
| 74 /** |
| 75 * @override |
| 76 */ |
| 77 wasShown() { |
| 78 this._update(); |
| 79 } |
| 80 |
| 81 _update() { |
| 82 if (!this.isShowing()) |
| 83 return; |
| 84 |
| 85 var node = WebInspector.context.flavor(WebInspector.DOMNode); |
| 86 if (node) |
| 87 node = node.enclosingElementOrSelf(); |
| 88 |
| 89 this._classesContainer.removeChildren(); |
| 90 this._input.disabled = !node; |
| 91 |
| 92 if (!node) |
| 93 return; |
| 94 |
| 95 var classes = this._nodeClasses(node); |
| 96 var keys = classes.keysArray(); |
| 97 keys.sort(String.caseInsensetiveComparator); |
| 98 for (var i = 0; i < keys.length; ++i) { |
| 99 var className = keys[i]; |
| 100 var label = createCheckboxLabel(className, classes.get(className)); |
| 101 label.visualizeFocus = true; |
| 102 label.classList.add('monospace'); |
| 103 label.checkboxElement.addEventListener('click', this._onClick.bind(this, c
lassName), false); |
| 104 this._classesContainer.appendChild(label); |
| 105 } |
| 106 } |
| 107 |
| 108 /** |
| 109 * @param {string} className |
| 110 * @param {!Event} event |
| 111 */ |
| 112 _onClick(className, event) { |
| 113 var node = WebInspector.context.flavor(WebInspector.DOMNode); |
| 114 if (!node) |
| 115 return; |
| 116 var enabled = event.target.checked; |
| 117 this._toggleClass(node, className, enabled); |
| 118 this._installNodeClasses(node); |
| 119 } |
| 120 |
| 121 /** |
| 122 * @param {!WebInspector.DOMNode} node |
| 123 * @return {!Map<string, boolean>} |
| 124 */ |
| 125 _nodeClasses(node) { |
| 126 var result = node[WebInspector.ClassesPaneWidget._classesSymbol]; |
| 127 if (!result) { |
| 128 var classAttribute = node.getAttribute('class') || ''; |
| 129 var classes = classAttribute.split(/\s/); |
| 130 result = new Map(); |
| 131 for (var i = 0; i < classes.length; ++i) { |
| 132 var className = classes[i].trim(); |
| 133 if (!className.length) |
| 134 continue; |
| 135 result.set(className, true); |
| 136 } |
| 137 node[WebInspector.ClassesPaneWidget._classesSymbol] = result; |
| 138 } |
| 139 return result; |
| 140 } |
| 141 |
| 142 /** |
| 143 * @param {!WebInspector.DOMNode} node |
| 144 * @param {string} className |
| 145 * @param {boolean} enabled |
| 146 */ |
| 147 _toggleClass(node, className, enabled) { |
| 148 var classes = this._nodeClasses(node); |
| 149 classes.set(className, enabled); |
| 150 } |
| 151 |
| 152 /** |
| 153 * @param {!WebInspector.DOMNode} node |
| 154 */ |
| 155 _installNodeClasses(node) { |
| 156 var classes = this._nodeClasses(node); |
| 157 var activeClasses = new Set(); |
| 158 for (var className of classes.keys()) { |
| 159 if (classes.get(className)) |
| 160 activeClasses.add(className); |
| 161 } |
| 162 |
| 163 var newClasses = activeClasses.valuesArray(); |
| 164 newClasses.sort(); |
| 165 this._mutatingNodes.add(node); |
| 166 node.setAttributeValue('class', newClasses.join(' '), onClassNameUpdated.bin
d(this)); |
| 167 |
| 168 /** |
| 169 * @this {WebInspector.ClassesPaneWidget} |
| 170 */ |
| 171 function onClassNameUpdated() { |
| 172 this._mutatingNodes.delete(node); |
| 173 } |
| 174 } |
| 30 }; | 175 }; |
| 31 | 176 |
| 32 WebInspector.ClassesPaneWidget._classesSymbol = Symbol("WebInspector.ClassesPane
Widget._classesSymbol"); | 177 WebInspector.ClassesPaneWidget._classesSymbol = Symbol('WebInspector.ClassesPane
Widget._classesSymbol'); |
| 33 | 178 |
| 34 WebInspector.ClassesPaneWidget.prototype = { | 179 /** |
| 35 /** | 180 * @implements {WebInspector.ToolbarItem.Provider} |
| 36 * @param {!Event} event | 181 * @unrestricted |
| 37 */ | 182 */ |
| 38 _onKeyDown: function(event) | 183 WebInspector.ClassesPaneWidget.ButtonProvider = class { |
| 39 { | 184 constructor() { |
| 40 var text = event.target.textContent; | 185 this._button = new WebInspector.ToolbarToggle(WebInspector.UIString('Element
Classes'), ''); |
| 41 if (isEscKey(event)) { | 186 this._button.setText('.cls'); |
| 42 event.target.textContent = ""; | 187 this._button.element.classList.add('monospace'); |
| 43 if (!text.isWhitespace()) | 188 this._button.addEventListener('click', this._clicked, this); |
| 44 event.consume(true); | 189 this._view = new WebInspector.ClassesPaneWidget(); |
| 45 return; | 190 } |
| 46 } | 191 |
| 47 | 192 _clicked() { |
| 48 if (!isEnterKey(event)) | 193 WebInspector.ElementsPanel.instance().showToolbarPane(!this._view.isShowing(
) ? this._view : null, this._button); |
| 49 return; | 194 } |
| 50 var node = WebInspector.context.flavor(WebInspector.DOMNode); | 195 |
| 51 if (!node) | 196 /** |
| 52 return; | 197 * @override |
| 53 | 198 * @return {!WebInspector.ToolbarItem} |
| 54 this._prompt.clearAutocomplete(); | 199 */ |
| 55 event.target.textContent = ""; | 200 item() { |
| 56 var classNames = text.split(/[.,\s]/); | 201 return this._button; |
| 57 for (var className of classNames) { | 202 } |
| 58 var className = className.trim(); | |
| 59 if (!className.length) | |
| 60 continue; | |
| 61 this._toggleClass(node, className, true); | |
| 62 } | |
| 63 this._installNodeClasses(node); | |
| 64 this._update(); | |
| 65 event.consume(true); | |
| 66 }, | |
| 67 | |
| 68 /** | |
| 69 * @param {!WebInspector.Event} event | |
| 70 */ | |
| 71 _onDOMMutated: function(event) | |
| 72 { | |
| 73 var node = /** @type {!WebInspector.DOMNode} */(event.data); | |
| 74 if (this._mutatingNodes.has(node)) | |
| 75 return; | |
| 76 delete node[WebInspector.ClassesPaneWidget._classesSymbol]; | |
| 77 this._update(); | |
| 78 }, | |
| 79 | |
| 80 /** | |
| 81 * @override | |
| 82 */ | |
| 83 wasShown: function() | |
| 84 { | |
| 85 this._update(); | |
| 86 }, | |
| 87 | |
| 88 _update: function() | |
| 89 { | |
| 90 if (!this.isShowing()) | |
| 91 return; | |
| 92 | |
| 93 var node = WebInspector.context.flavor(WebInspector.DOMNode); | |
| 94 if (node) | |
| 95 node = node.enclosingElementOrSelf(); | |
| 96 | |
| 97 this._classesContainer.removeChildren(); | |
| 98 this._input.disabled = !node; | |
| 99 | |
| 100 if (!node) | |
| 101 return; | |
| 102 | |
| 103 var classes = this._nodeClasses(node); | |
| 104 var keys = classes.keysArray(); | |
| 105 keys.sort(String.caseInsensetiveComparator); | |
| 106 for (var i = 0; i < keys.length; ++i) { | |
| 107 var className = keys[i]; | |
| 108 var label = createCheckboxLabel(className, classes.get(className)); | |
| 109 label.visualizeFocus = true; | |
| 110 label.classList.add("monospace"); | |
| 111 label.checkboxElement.addEventListener("click", this._onClick.bind(t
his, className), false); | |
| 112 this._classesContainer.appendChild(label); | |
| 113 } | |
| 114 }, | |
| 115 | |
| 116 /** | |
| 117 * @param {string} className | |
| 118 * @param {!Event} event | |
| 119 */ | |
| 120 _onClick: function(className, event) | |
| 121 { | |
| 122 var node = WebInspector.context.flavor(WebInspector.DOMNode); | |
| 123 if (!node) | |
| 124 return; | |
| 125 var enabled = event.target.checked; | |
| 126 this._toggleClass(node, className, enabled); | |
| 127 this._installNodeClasses(node); | |
| 128 }, | |
| 129 | |
| 130 /** | |
| 131 * @param {!WebInspector.DOMNode} node | |
| 132 * @return {!Map<string, boolean>} | |
| 133 */ | |
| 134 _nodeClasses: function(node) | |
| 135 { | |
| 136 var result = node[WebInspector.ClassesPaneWidget._classesSymbol]; | |
| 137 if (!result) { | |
| 138 var classAttribute = node.getAttribute("class") || ""; | |
| 139 var classes = classAttribute.split(/\s/); | |
| 140 result = new Map(); | |
| 141 for (var i = 0; i < classes.length; ++i) { | |
| 142 var className = classes[i].trim(); | |
| 143 if (!className.length) | |
| 144 continue; | |
| 145 result.set(className, true); | |
| 146 } | |
| 147 node[WebInspector.ClassesPaneWidget._classesSymbol] = result; | |
| 148 } | |
| 149 return result; | |
| 150 }, | |
| 151 | |
| 152 /** | |
| 153 * @param {!WebInspector.DOMNode} node | |
| 154 * @param {string} className | |
| 155 * @param {boolean} enabled | |
| 156 */ | |
| 157 _toggleClass: function(node, className, enabled) | |
| 158 { | |
| 159 var classes = this._nodeClasses(node); | |
| 160 classes.set(className, enabled); | |
| 161 }, | |
| 162 | |
| 163 /** | |
| 164 * @param {!WebInspector.DOMNode} node | |
| 165 */ | |
| 166 _installNodeClasses: function(node) | |
| 167 { | |
| 168 var classes = this._nodeClasses(node); | |
| 169 var activeClasses = new Set(); | |
| 170 for (var className of classes.keys()) { | |
| 171 if (classes.get(className)) | |
| 172 activeClasses.add(className); | |
| 173 } | |
| 174 | |
| 175 var newClasses = activeClasses.valuesArray(); | |
| 176 newClasses.sort(); | |
| 177 this._mutatingNodes.add(node); | |
| 178 node.setAttributeValue("class", newClasses.join(" "), onClassNameUpdated
.bind(this)); | |
| 179 | |
| 180 /** | |
| 181 * @this {WebInspector.ClassesPaneWidget} | |
| 182 */ | |
| 183 function onClassNameUpdated() | |
| 184 { | |
| 185 this._mutatingNodes.delete(node); | |
| 186 } | |
| 187 }, | |
| 188 | |
| 189 __proto__: WebInspector.Widget.prototype | |
| 190 }; | 203 }; |
| 191 | 204 |
| 192 /** | 205 /** |
| 193 * @constructor | 206 * @unrestricted |
| 194 * @implements {WebInspector.ToolbarItem.Provider} | |
| 195 */ | 207 */ |
| 196 WebInspector.ClassesPaneWidget.ButtonProvider = function() | 208 WebInspector.ClassesPaneWidget.ClassNamePrompt = class extends WebInspector.Text
Prompt { |
| 197 { | 209 constructor() { |
| 198 this._button = new WebInspector.ToolbarToggle(WebInspector.UIString("Element
Classes"), ""); | 210 super(); |
| 199 this._button.setText(".cls"); | 211 this.initialize(this._buildClassNameCompletions.bind(this), ' '); |
| 200 this._button.element.classList.add("monospace"); | |
| 201 this._button.addEventListener("click", this._clicked, this); | |
| 202 this._view = new WebInspector.ClassesPaneWidget(); | |
| 203 }; | |
| 204 | |
| 205 WebInspector.ClassesPaneWidget.ButtonProvider.prototype = { | |
| 206 _clicked: function() | |
| 207 { | |
| 208 WebInspector.ElementsPanel.instance().showToolbarPane(!this._view.isShow
ing() ? this._view : null, this._button); | |
| 209 }, | |
| 210 | |
| 211 /** | |
| 212 * @override | |
| 213 * @return {!WebInspector.ToolbarItem} | |
| 214 */ | |
| 215 item: function() | |
| 216 { | |
| 217 return this._button; | |
| 218 } | |
| 219 }; | |
| 220 | |
| 221 /** | |
| 222 * @constructor | |
| 223 * @extends {WebInspector.TextPrompt} | |
| 224 */ | |
| 225 WebInspector.ClassesPaneWidget.ClassNamePrompt = function() | |
| 226 { | |
| 227 WebInspector.TextPrompt.call(this); | |
| 228 this.initialize(this._buildClassNameCompletions.bind(this), " "); | |
| 229 this.setSuggestBoxEnabled(true); | 212 this.setSuggestBoxEnabled(true); |
| 230 this.disableDefaultSuggestionForEmptyInput(); | 213 this.disableDefaultSuggestionForEmptyInput(); |
| 231 this._selectedFrameId = ""; | 214 this._selectedFrameId = ''; |
| 232 this._classNamesPromise = null; | 215 this._classNamesPromise = null; |
| 216 } |
| 217 |
| 218 /** |
| 219 * @param {!WebInspector.DOMNode} selectedNode |
| 220 * @return {!Promise.<!Array.<string>>} |
| 221 */ |
| 222 _getClassNames(selectedNode) { |
| 223 var promises = []; |
| 224 var completions = new Set(); |
| 225 this._selectedFrameId = selectedNode.frameId(); |
| 226 |
| 227 var cssModel = WebInspector.CSSModel.fromTarget(selectedNode.target()); |
| 228 var allStyleSheets = cssModel.allStyleSheets(); |
| 229 for (var stylesheet of allStyleSheets) { |
| 230 if (stylesheet.frameId !== this._selectedFrameId) |
| 231 continue; |
| 232 var cssPromise = cssModel.classNamesPromise(stylesheet.id).then(classes =>
completions.addAll(classes)); |
| 233 promises.push(cssPromise); |
| 234 } |
| 235 |
| 236 var domPromise = selectedNode.domModel() |
| 237 .classNamesPromise(selectedNode.ownerDocument.id) |
| 238 .then(classes => completions.addAll(classes)); |
| 239 promises.push(domPromise); |
| 240 return Promise.all(promises).then(() => completions.valuesArray()); |
| 241 } |
| 242 |
| 243 /** |
| 244 * @param {!Element} proxyElement |
| 245 * @param {!Range} wordRange |
| 246 * @param {boolean} force |
| 247 * @param {function(!Array.<string>, number=)} completionsReadyCallback |
| 248 */ |
| 249 _buildClassNameCompletions(proxyElement, wordRange, force, completionsReadyCal
lback) { |
| 250 var prefix = wordRange.toString(); |
| 251 if (!prefix || force) |
| 252 this._classNamesPromise = null; |
| 253 |
| 254 var selectedNode = WebInspector.context.flavor(WebInspector.DOMNode); |
| 255 if (!selectedNode || (!prefix && !force && !proxyElement.textContent.length)
) { |
| 256 completionsReadyCallback([]); |
| 257 return; |
| 258 } |
| 259 |
| 260 if (!this._classNamesPromise || this._selectedFrameId !== selectedNode.frame
Id()) |
| 261 this._classNamesPromise = this._getClassNames(selectedNode); |
| 262 |
| 263 this._classNamesPromise.then(completions => { |
| 264 if (prefix[0] === '.') |
| 265 completions = completions.map(value => '.' + value); |
| 266 var results = completions.filter(value => value.startsWith(prefix)); |
| 267 completionsReadyCallback(results, 0); |
| 268 }); |
| 269 } |
| 233 }; | 270 }; |
| 234 | |
| 235 WebInspector.ClassesPaneWidget.ClassNamePrompt.prototype = { | |
| 236 /** | |
| 237 * @param {!WebInspector.DOMNode} selectedNode | |
| 238 * @return {!Promise.<!Array.<string>>} | |
| 239 */ | |
| 240 _getClassNames: function(selectedNode) | |
| 241 { | |
| 242 var promises = []; | |
| 243 var completions = new Set(); | |
| 244 this._selectedFrameId = selectedNode.frameId(); | |
| 245 | |
| 246 var cssModel = WebInspector.CSSModel.fromTarget(selectedNode.target()); | |
| 247 var allStyleSheets = cssModel.allStyleSheets(); | |
| 248 for (var stylesheet of allStyleSheets) { | |
| 249 if (stylesheet.frameId !== this._selectedFrameId) | |
| 250 continue; | |
| 251 var cssPromise = cssModel.classNamesPromise(stylesheet.id).then(clas
ses => completions.addAll(classes)); | |
| 252 promises.push(cssPromise); | |
| 253 } | |
| 254 | |
| 255 var domPromise = selectedNode.domModel().classNamesPromise(selectedNode.
ownerDocument.id).then(classes => completions.addAll(classes)); | |
| 256 promises.push(domPromise); | |
| 257 return Promise.all(promises).then(() => completions.valuesArray()); | |
| 258 }, | |
| 259 | |
| 260 /** | |
| 261 * @param {!Element} proxyElement | |
| 262 * @param {!Range} wordRange | |
| 263 * @param {boolean} force | |
| 264 * @param {function(!Array.<string>, number=)} completionsReadyCallback | |
| 265 */ | |
| 266 _buildClassNameCompletions: function(proxyElement, wordRange, force, complet
ionsReadyCallback) | |
| 267 { | |
| 268 var prefix = wordRange.toString(); | |
| 269 if (!prefix || force) | |
| 270 this._classNamesPromise = null; | |
| 271 | |
| 272 var selectedNode = WebInspector.context.flavor(WebInspector.DOMNode); | |
| 273 if (!selectedNode || (!prefix && !force && !proxyElement.textContent.len
gth)) { | |
| 274 completionsReadyCallback([]); | |
| 275 return; | |
| 276 } | |
| 277 | |
| 278 if (!this._classNamesPromise || this._selectedFrameId !== selectedNode.f
rameId()) | |
| 279 this._classNamesPromise = this._getClassNames(selectedNode); | |
| 280 | |
| 281 this._classNamesPromise.then(completions => { | |
| 282 if (prefix[0] === ".") | |
| 283 completions = completions.map(value => "." + value); | |
| 284 var results = completions.filter(value => value.startsWith(prefix)); | |
| 285 completionsReadyCallback(results, 0); | |
| 286 }); | |
| 287 }, | |
| 288 | |
| 289 __proto__: WebInspector.TextPrompt.prototype | |
| 290 }; | |
| OLD | NEW |