blob: 62f758d4364de054fb1bf2816df479429b0658c5 [file] [log] [blame]
Blink Reformat4c46d092018-04-07 15:32:371// 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
3// found in the LICENSE file.
Tim van der Lippeaabc8302019-12-10 15:34:454import {ElementsPanel} from './ElementsPanel.js';
5
Blink Reformat4c46d092018-04-07 15:32:376/**
7 * @unrestricted
8 */
Tim van der Lippeaabc8302019-12-10 15:34:459export class ClassesPaneWidget extends UI.Widget {
Blink Reformat4c46d092018-04-07 15:32:3710 constructor() {
11 super(true);
12 this.registerRequiredCSS('elements/classesPaneWidget.css');
13 this.contentElement.className = 'styles-element-classes-pane';
14 const container = this.contentElement.createChild('div', 'title-container');
15 this._input = container.createChild('div', 'new-class-input monospace');
16 this.setDefaultFocusedElement(this._input);
17 this._classesContainer = this.contentElement.createChild('div', 'source-code');
18 this._classesContainer.classList.add('styles-element-classes-container');
Tim van der Lippe13f71fb2019-11-29 11:17:3919 this._prompt = new ClassNamePrompt(this._nodeClasses.bind(this));
Blink Reformat4c46d092018-04-07 15:32:3720 this._prompt.setAutocompletionTimeout(0);
21 this._prompt.renderAsBlock();
22
23 const proxyElement = this._prompt.attach(this._input);
24 this._prompt.setPlaceholder(Common.UIString('Add new class'));
25 this._prompt.addEventListener(UI.TextPrompt.Events.TextChanged, this._onTextChanged, this);
26 proxyElement.addEventListener('keydown', this._onKeyDown.bind(this), false);
27
Paul Lewis4ae5f4f2020-01-23 10:19:3328 self.SDK.targetManager.addModelListener(SDK.DOMModel, SDK.DOMModel.Events.DOMMutated, this._onDOMMutated, this);
Blink Reformat4c46d092018-04-07 15:32:3729 /** @type {!Set<!SDK.DOMNode>} */
30 this._mutatingNodes = new Set();
31 /** @type {!Map<!SDK.DOMNode, string>} */
32 this._pendingNodeClasses = new Map();
33 this._updateNodeThrottler = new Common.Throttler(0);
34 /** @type {?SDK.DOMNode} */
35 this._previousTarget = null;
Paul Lewisd9907342020-01-24 13:49:4736 self.UI.context.addFlavorChangeListener(SDK.DOMNode, this._onSelectedNodeChanged, this);
Blink Reformat4c46d092018-04-07 15:32:3737 }
38
39 /**
40 * @param {string} text
41 * @return {!Array.<string>}
42 */
43 _splitTextIntoClasses(text) {
44 return text.split(/[.,\s]/)
45 .map(className => className.trim())
46 .filter(className => className.length);
47 }
48
49 /**
50 * @param {!Event} event
51 */
52 _onKeyDown(event) {
Tim van der Lippe1d6e57a2019-09-30 11:55:3453 if (!isEnterKey(event) && !isEscKey(event)) {
Blink Reformat4c46d092018-04-07 15:32:3754 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3455 }
Blink Reformat4c46d092018-04-07 15:32:3756
57 if (isEnterKey(event)) {
58 event.consume();
Tim van der Lippe1d6e57a2019-09-30 11:55:3459 if (this._prompt.acceptAutoComplete()) {
Blink Reformat4c46d092018-04-07 15:32:3760 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3461 }
Blink Reformat4c46d092018-04-07 15:32:3762 }
63
64 let text = event.target.textContent;
65 if (isEscKey(event)) {
Tim van der Lippe1d6e57a2019-09-30 11:55:3466 if (!text.isWhitespace()) {
Blink Reformat4c46d092018-04-07 15:32:3767 event.consume(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:3468 }
Blink Reformat4c46d092018-04-07 15:32:3769 text = '';
70 }
71
72 this._prompt.clearAutocomplete();
73 event.target.textContent = '';
74
Paul Lewisd9907342020-01-24 13:49:4775 const node = self.UI.context.flavor(SDK.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:3476 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:3777 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3478 }
Blink Reformat4c46d092018-04-07 15:32:3779
80 const classNames = this._splitTextIntoClasses(text);
Tim van der Lippe1d6e57a2019-09-30 11:55:3481 for (const className of classNames) {
Blink Reformat4c46d092018-04-07 15:32:3782 this._toggleClass(node, className, true);
Tim van der Lippe1d6e57a2019-09-30 11:55:3483 }
Blink Reformat4c46d092018-04-07 15:32:3784 this._installNodeClasses(node);
85 this._update();
86 }
87
88 _onTextChanged() {
Paul Lewisd9907342020-01-24 13:49:4789 const node = self.UI.context.flavor(SDK.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:3490 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:3791 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3492 }
Blink Reformat4c46d092018-04-07 15:32:3793 this._installNodeClasses(node);
94 }
95
96 /**
97 * @param {!Common.Event} event
98 */
99 _onDOMMutated(event) {
100 const node = /** @type {!SDK.DOMNode} */ (event.data);
Tim van der Lippe1d6e57a2019-09-30 11:55:34101 if (this._mutatingNodes.has(node)) {
Blink Reformat4c46d092018-04-07 15:32:37102 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34103 }
Tim van der Lippe13f71fb2019-11-29 11:17:39104 delete node[ClassesPaneWidget._classesSymbol];
Blink Reformat4c46d092018-04-07 15:32:37105 this._update();
106 }
107
108 /**
109 * @param {!Common.Event} event
110 */
111 _onSelectedNodeChanged(event) {
112 if (this._previousTarget && this._prompt.text()) {
113 this._input.textContent = '';
114 this._installNodeClasses(this._previousTarget);
115 }
116 this._previousTarget = /** @type {?SDK.DOMNode} */ (event.data);
117 this._update();
118 }
119
120 /**
121 * @override
122 */
123 wasShown() {
124 this._update();
125 }
126
127 _update() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34128 if (!this.isShowing()) {
Blink Reformat4c46d092018-04-07 15:32:37129 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34130 }
Blink Reformat4c46d092018-04-07 15:32:37131
Paul Lewisd9907342020-01-24 13:49:47132 let node = self.UI.context.flavor(SDK.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34133 if (node) {
Blink Reformat4c46d092018-04-07 15:32:37134 node = node.enclosingElementOrSelf();
Tim van der Lippe1d6e57a2019-09-30 11:55:34135 }
Blink Reformat4c46d092018-04-07 15:32:37136
137 this._classesContainer.removeChildren();
138 this._input.disabled = !node;
139
Tim van der Lippe1d6e57a2019-09-30 11:55:34140 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:37141 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34142 }
Blink Reformat4c46d092018-04-07 15:32:37143
144 const classes = this._nodeClasses(node);
Simon Zündf27be3d2020-02-11 14:46:27145 const keys = [...classes.keys()];
Blink Reformat4c46d092018-04-07 15:32:37146 keys.sort(String.caseInsensetiveComparator);
147 for (let i = 0; i < keys.length; ++i) {
148 const className = keys[i];
149 const label = UI.CheckboxLabel.create(className, classes.get(className));
150 label.classList.add('monospace');
151 label.checkboxElement.addEventListener('click', this._onClick.bind(this, className), false);
152 this._classesContainer.appendChild(label);
153 }
154 }
155
156 /**
157 * @param {string} className
158 * @param {!Event} event
159 */
160 _onClick(className, event) {
Paul Lewisd9907342020-01-24 13:49:47161 const node = self.UI.context.flavor(SDK.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34162 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:37163 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34164 }
Blink Reformat4c46d092018-04-07 15:32:37165 const enabled = event.target.checked;
166 this._toggleClass(node, className, enabled);
167 this._installNodeClasses(node);
168 }
169
170 /**
171 * @param {!SDK.DOMNode} node
172 * @return {!Map<string, boolean>}
173 */
174 _nodeClasses(node) {
Tim van der Lippe13f71fb2019-11-29 11:17:39175 let result = node[ClassesPaneWidget._classesSymbol];
Blink Reformat4c46d092018-04-07 15:32:37176 if (!result) {
177 const classAttribute = node.getAttribute('class') || '';
178 const classes = classAttribute.split(/\s/);
179 result = new Map();
180 for (let i = 0; i < classes.length; ++i) {
181 const className = classes[i].trim();
Tim van der Lippe1d6e57a2019-09-30 11:55:34182 if (!className.length) {
Blink Reformat4c46d092018-04-07 15:32:37183 continue;
Tim van der Lippe1d6e57a2019-09-30 11:55:34184 }
Blink Reformat4c46d092018-04-07 15:32:37185 result.set(className, true);
186 }
Tim van der Lippe13f71fb2019-11-29 11:17:39187 node[ClassesPaneWidget._classesSymbol] = result;
Blink Reformat4c46d092018-04-07 15:32:37188 }
189 return result;
190 }
191
192 /**
193 * @param {!SDK.DOMNode} node
194 * @param {string} className
195 * @param {boolean} enabled
196 */
197 _toggleClass(node, className, enabled) {
198 const classes = this._nodeClasses(node);
199 classes.set(className, enabled);
200 }
201
202 /**
203 * @param {!SDK.DOMNode} node
204 */
205 _installNodeClasses(node) {
206 const classes = this._nodeClasses(node);
207 const activeClasses = new Set();
208 for (const className of classes.keys()) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34209 if (classes.get(className)) {
Blink Reformat4c46d092018-04-07 15:32:37210 activeClasses.add(className);
Tim van der Lippe1d6e57a2019-09-30 11:55:34211 }
Blink Reformat4c46d092018-04-07 15:32:37212 }
213
214 const additionalClasses = this._splitTextIntoClasses(this._prompt.textWithCurrentSuggestion());
Tim van der Lippe1d6e57a2019-09-30 11:55:34215 for (const className of additionalClasses) {
Blink Reformat4c46d092018-04-07 15:32:37216 activeClasses.add(className);
Tim van der Lippe1d6e57a2019-09-30 11:55:34217 }
Blink Reformat4c46d092018-04-07 15:32:37218
Simon Zünda0d40622020-02-12 13:16:42219 const newClasses = [...activeClasses.values()].sort();
Blink Reformat4c46d092018-04-07 15:32:37220
221 this._pendingNodeClasses.set(node, newClasses.join(' '));
222 this._updateNodeThrottler.schedule(this._flushPendingClasses.bind(this));
223 }
224
225 /**
226 * @return {!Promise}
227 */
228 _flushPendingClasses() {
229 const promises = [];
230 for (const node of this._pendingNodeClasses.keys()) {
231 this._mutatingNodes.add(node);
232 const promise = node.setAttributeValuePromise('class', this._pendingNodeClasses.get(node))
233 .then(onClassValueUpdated.bind(this, node));
234 promises.push(promise);
235 }
236 this._pendingNodeClasses.clear();
237 return Promise.all(promises);
238
239 /**
240 * @param {!SDK.DOMNode} node
Tim van der Lippe13f71fb2019-11-29 11:17:39241 * @this {ClassesPaneWidget}
Blink Reformat4c46d092018-04-07 15:32:37242 */
243 function onClassValueUpdated(node) {
244 this._mutatingNodes.delete(node);
245 }
246 }
Tim van der Lippe13f71fb2019-11-29 11:17:39247}
Blink Reformat4c46d092018-04-07 15:32:37248
Tim van der Lippe13f71fb2019-11-29 11:17:39249ClassesPaneWidget._classesSymbol = Symbol('ClassesPaneWidget._classesSymbol');
Blink Reformat4c46d092018-04-07 15:32:37250
251/**
252 * @implements {UI.ToolbarItem.Provider}
253 * @unrestricted
254 */
Tim van der Lippe13f71fb2019-11-29 11:17:39255export class ButtonProvider {
Blink Reformat4c46d092018-04-07 15:32:37256 constructor() {
257 this._button = new UI.ToolbarToggle(Common.UIString('Element Classes'), '');
258 this._button.setText('.cls');
259 this._button.element.classList.add('monospace');
260 this._button.addEventListener(UI.ToolbarButton.Events.Click, this._clicked, this);
Tim van der Lippe13f71fb2019-11-29 11:17:39261 this._view = new ClassesPaneWidget();
Blink Reformat4c46d092018-04-07 15:32:37262 }
263
264 _clicked() {
Tim van der Lippeaabc8302019-12-10 15:34:45265 ElementsPanel.instance().showToolbarPane(!this._view.isShowing() ? this._view : null, this._button);
Blink Reformat4c46d092018-04-07 15:32:37266 }
267
268 /**
269 * @override
270 * @return {!UI.ToolbarItem}
271 */
272 item() {
273 return this._button;
274 }
Tim van der Lippe13f71fb2019-11-29 11:17:39275}
Blink Reformat4c46d092018-04-07 15:32:37276
277/**
278 * @unrestricted
279 */
Tim van der Lippe13f71fb2019-11-29 11:17:39280export class ClassNamePrompt extends UI.TextPrompt {
Blink Reformat4c46d092018-04-07 15:32:37281 /**
282 * @param {function(!SDK.DOMNode):!Map<string, boolean>} nodeClasses
283 */
284 constructor(nodeClasses) {
285 super();
286 this._nodeClasses = nodeClasses;
287 this.initialize(this._buildClassNameCompletions.bind(this), ' ');
288 this.disableDefaultSuggestionForEmptyInput();
289 this._selectedFrameId = '';
290 this._classNamesPromise = null;
291 }
292
293 /**
294 * @param {!SDK.DOMNode} selectedNode
295 * @return {!Promise.<!Array.<string>>}
296 */
297 _getClassNames(selectedNode) {
298 const promises = [];
299 const completions = new Set();
300 this._selectedFrameId = selectedNode.frameId();
301
302 const cssModel = selectedNode.domModel().cssModel();
303 const allStyleSheets = cssModel.allStyleSheets();
304 for (const stylesheet of allStyleSheets) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34305 if (stylesheet.frameId !== this._selectedFrameId) {
Blink Reformat4c46d092018-04-07 15:32:37306 continue;
Tim van der Lippe1d6e57a2019-09-30 11:55:34307 }
Blink Reformat4c46d092018-04-07 15:32:37308 const cssPromise = cssModel.classNamesPromise(stylesheet.id).then(classes => completions.addAll(classes));
309 promises.push(cssPromise);
310 }
311
312 const domPromise = selectedNode.domModel()
313 .classNamesPromise(selectedNode.ownerDocument.id)
314 .then(classes => completions.addAll(classes));
315 promises.push(domPromise);
Simon Zünda0d40622020-02-12 13:16:42316 return Promise.all(promises).then(() => [...completions]);
Blink Reformat4c46d092018-04-07 15:32:37317 }
318
319 /**
320 * @param {string} expression
321 * @param {string} prefix
322 * @param {boolean=} force
323 * @return {!Promise<!UI.SuggestBox.Suggestions>}
324 */
325 _buildClassNameCompletions(expression, prefix, force) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34326 if (!prefix || force) {
Blink Reformat4c46d092018-04-07 15:32:37327 this._classNamesPromise = null;
Tim van der Lippe1d6e57a2019-09-30 11:55:34328 }
Blink Reformat4c46d092018-04-07 15:32:37329
Paul Lewisd9907342020-01-24 13:49:47330 const selectedNode = self.UI.context.flavor(SDK.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34331 if (!selectedNode || (!prefix && !force && !expression.trim())) {
Blink Reformat4c46d092018-04-07 15:32:37332 return Promise.resolve([]);
Tim van der Lippe1d6e57a2019-09-30 11:55:34333 }
Blink Reformat4c46d092018-04-07 15:32:37334
Tim van der Lippe1d6e57a2019-09-30 11:55:34335 if (!this._classNamesPromise || this._selectedFrameId !== selectedNode.frameId()) {
Blink Reformat4c46d092018-04-07 15:32:37336 this._classNamesPromise = this._getClassNames(selectedNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34337 }
Blink Reformat4c46d092018-04-07 15:32:37338
339 return this._classNamesPromise.then(completions => {
340 const classesMap = this._nodeClasses(/** @type {!SDK.DOMNode} */ (selectedNode));
341 completions = completions.filter(value => !classesMap.get(value));
342
Tim van der Lippe1d6e57a2019-09-30 11:55:34343 if (prefix[0] === '.') {
Blink Reformat4c46d092018-04-07 15:32:37344 completions = completions.map(value => '.' + value);
Tim van der Lippe1d6e57a2019-09-30 11:55:34345 }
Blink Reformat4c46d092018-04-07 15:32:37346 return completions.filter(value => value.startsWith(prefix)).sort().map(completion => ({text: completion}));
347 });
348 }
Tim van der Lippe13f71fb2019-11-29 11:17:39349}