blob: cac1cf923fcb1789f087abcd88fe242dbab3482f [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 Lippe97611c92020-02-12 16:56:584
5import * as Common from '../common/common.js';
6import * as SDK from '../sdk/sdk.js';
7import * as UI from '../ui/ui.js';
8
Tim van der Lippeaabc8302019-12-10 15:34:459import {ElementsPanel} from './ElementsPanel.js';
10
Blink Reformat4c46d092018-04-07 15:32:3711/**
12 * @unrestricted
13 */
Tim van der Lippe97611c92020-02-12 16:56:5814export class ClassesPaneWidget extends UI.Widget.Widget {
Blink Reformat4c46d092018-04-07 15:32:3715 constructor() {
16 super(true);
17 this.registerRequiredCSS('elements/classesPaneWidget.css');
18 this.contentElement.className = 'styles-element-classes-pane';
19 const container = this.contentElement.createChild('div', 'title-container');
20 this._input = container.createChild('div', 'new-class-input monospace');
21 this.setDefaultFocusedElement(this._input);
22 this._classesContainer = this.contentElement.createChild('div', 'source-code');
23 this._classesContainer.classList.add('styles-element-classes-container');
Tim van der Lippe13f71fb2019-11-29 11:17:3924 this._prompt = new ClassNamePrompt(this._nodeClasses.bind(this));
Blink Reformat4c46d092018-04-07 15:32:3725 this._prompt.setAutocompletionTimeout(0);
26 this._prompt.renderAsBlock();
27
28 const proxyElement = this._prompt.attach(this._input);
Tim van der Lippe97611c92020-02-12 16:56:5829 this._prompt.setPlaceholder(Common.UIString.UIString('Add new class'));
Blink Reformat4c46d092018-04-07 15:32:3730 this._prompt.addEventListener(UI.TextPrompt.Events.TextChanged, this._onTextChanged, this);
31 proxyElement.addEventListener('keydown', this._onKeyDown.bind(this), false);
32
Tim van der Lippe97611c92020-02-12 16:56:5833 self.SDK.targetManager.addModelListener(
34 SDK.DOMModel.DOMModel, SDK.DOMModel.Events.DOMMutated, this._onDOMMutated, this);
35 /** @type {!Set<!SDK.DOMModel.DOMNode>} */
Blink Reformat4c46d092018-04-07 15:32:3736 this._mutatingNodes = new Set();
Tim van der Lippe97611c92020-02-12 16:56:5837 /** @type {!Map<!SDK.DOMModel.DOMNode, string>} */
Blink Reformat4c46d092018-04-07 15:32:3738 this._pendingNodeClasses = new Map();
Tim van der Lippe97611c92020-02-12 16:56:5839 this._updateNodeThrottler = new Common.Throttler.Throttler(0);
40 /** @type {?SDK.DOMModel.DOMNode} */
Blink Reformat4c46d092018-04-07 15:32:3741 this._previousTarget = null;
Tim van der Lippe97611c92020-02-12 16:56:5842 self.UI.context.addFlavorChangeListener(SDK.DOMModel.DOMNode, this._onSelectedNodeChanged, this);
Blink Reformat4c46d092018-04-07 15:32:3743 }
44
45 /**
46 * @param {string} text
47 * @return {!Array.<string>}
48 */
49 _splitTextIntoClasses(text) {
50 return text.split(/[.,\s]/)
51 .map(className => className.trim())
52 .filter(className => className.length);
53 }
54
55 /**
56 * @param {!Event} event
57 */
58 _onKeyDown(event) {
Tim van der Lippe1d6e57a2019-09-30 11:55:3459 if (!isEnterKey(event) && !isEscKey(event)) {
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 if (isEnterKey(event)) {
64 event.consume();
Tim van der Lippe1d6e57a2019-09-30 11:55:3465 if (this._prompt.acceptAutoComplete()) {
Blink Reformat4c46d092018-04-07 15:32:3766 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3467 }
Blink Reformat4c46d092018-04-07 15:32:3768 }
69
70 let text = event.target.textContent;
71 if (isEscKey(event)) {
Tim van der Lippe1d6e57a2019-09-30 11:55:3472 if (!text.isWhitespace()) {
Blink Reformat4c46d092018-04-07 15:32:3773 event.consume(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:3474 }
Blink Reformat4c46d092018-04-07 15:32:3775 text = '';
76 }
77
78 this._prompt.clearAutocomplete();
79 event.target.textContent = '';
80
Tim van der Lippe97611c92020-02-12 16:56:5881 const node = self.UI.context.flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:3482 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:3783 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3484 }
Blink Reformat4c46d092018-04-07 15:32:3785
86 const classNames = this._splitTextIntoClasses(text);
Tim van der Lippe1d6e57a2019-09-30 11:55:3487 for (const className of classNames) {
Blink Reformat4c46d092018-04-07 15:32:3788 this._toggleClass(node, className, true);
Tim van der Lippe1d6e57a2019-09-30 11:55:3489 }
Blink Reformat4c46d092018-04-07 15:32:3790 this._installNodeClasses(node);
91 this._update();
92 }
93
94 _onTextChanged() {
Tim van der Lippe97611c92020-02-12 16:56:5895 const node = self.UI.context.flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:3496 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:3797 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3498 }
Blink Reformat4c46d092018-04-07 15:32:3799 this._installNodeClasses(node);
100 }
101
102 /**
103 * @param {!Common.Event} event
104 */
105 _onDOMMutated(event) {
Tim van der Lippe97611c92020-02-12 16:56:58106 const node = /** @type {!SDK.DOMModel.DOMNode} */ (event.data);
Tim van der Lippe1d6e57a2019-09-30 11:55:34107 if (this._mutatingNodes.has(node)) {
Blink Reformat4c46d092018-04-07 15:32:37108 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34109 }
Tim van der Lippe13f71fb2019-11-29 11:17:39110 delete node[ClassesPaneWidget._classesSymbol];
Blink Reformat4c46d092018-04-07 15:32:37111 this._update();
112 }
113
114 /**
115 * @param {!Common.Event} event
116 */
117 _onSelectedNodeChanged(event) {
118 if (this._previousTarget && this._prompt.text()) {
119 this._input.textContent = '';
120 this._installNodeClasses(this._previousTarget);
121 }
Tim van der Lippe97611c92020-02-12 16:56:58122 this._previousTarget = /** @type {?SDK.DOMModel.DOMNode} */ (event.data);
Blink Reformat4c46d092018-04-07 15:32:37123 this._update();
124 }
125
126 /**
127 * @override
128 */
129 wasShown() {
130 this._update();
131 }
132
133 _update() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34134 if (!this.isShowing()) {
Blink Reformat4c46d092018-04-07 15:32:37135 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34136 }
Blink Reformat4c46d092018-04-07 15:32:37137
Tim van der Lippe97611c92020-02-12 16:56:58138 let node = self.UI.context.flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34139 if (node) {
Blink Reformat4c46d092018-04-07 15:32:37140 node = node.enclosingElementOrSelf();
Tim van der Lippe1d6e57a2019-09-30 11:55:34141 }
Blink Reformat4c46d092018-04-07 15:32:37142
143 this._classesContainer.removeChildren();
144 this._input.disabled = !node;
145
Tim van der Lippe1d6e57a2019-09-30 11:55:34146 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:37147 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34148 }
Blink Reformat4c46d092018-04-07 15:32:37149
150 const classes = this._nodeClasses(node);
Simon Zündf27be3d2020-02-11 14:46:27151 const keys = [...classes.keys()];
Blink Reformat4c46d092018-04-07 15:32:37152 keys.sort(String.caseInsensetiveComparator);
153 for (let i = 0; i < keys.length; ++i) {
154 const className = keys[i];
Tim van der Lippe97611c92020-02-12 16:56:58155 const label = UI.UIUtils.CheckboxLabel.create(className, classes.get(className));
Blink Reformat4c46d092018-04-07 15:32:37156 label.classList.add('monospace');
157 label.checkboxElement.addEventListener('click', this._onClick.bind(this, className), false);
158 this._classesContainer.appendChild(label);
159 }
160 }
161
162 /**
163 * @param {string} className
164 * @param {!Event} event
165 */
166 _onClick(className, event) {
Tim van der Lippe97611c92020-02-12 16:56:58167 const node = self.UI.context.flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34168 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:37169 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34170 }
Blink Reformat4c46d092018-04-07 15:32:37171 const enabled = event.target.checked;
172 this._toggleClass(node, className, enabled);
173 this._installNodeClasses(node);
174 }
175
176 /**
Tim van der Lippe97611c92020-02-12 16:56:58177 * @param {!SDK.DOMModel.DOMNode} node
Blink Reformat4c46d092018-04-07 15:32:37178 * @return {!Map<string, boolean>}
179 */
180 _nodeClasses(node) {
Tim van der Lippe13f71fb2019-11-29 11:17:39181 let result = node[ClassesPaneWidget._classesSymbol];
Blink Reformat4c46d092018-04-07 15:32:37182 if (!result) {
183 const classAttribute = node.getAttribute('class') || '';
184 const classes = classAttribute.split(/\s/);
185 result = new Map();
186 for (let i = 0; i < classes.length; ++i) {
187 const className = classes[i].trim();
Tim van der Lippe1d6e57a2019-09-30 11:55:34188 if (!className.length) {
Blink Reformat4c46d092018-04-07 15:32:37189 continue;
Tim van der Lippe1d6e57a2019-09-30 11:55:34190 }
Blink Reformat4c46d092018-04-07 15:32:37191 result.set(className, true);
192 }
Tim van der Lippe13f71fb2019-11-29 11:17:39193 node[ClassesPaneWidget._classesSymbol] = result;
Blink Reformat4c46d092018-04-07 15:32:37194 }
195 return result;
196 }
197
198 /**
Tim van der Lippe97611c92020-02-12 16:56:58199 * @param {!SDK.DOMModel.DOMNode} node
Blink Reformat4c46d092018-04-07 15:32:37200 * @param {string} className
201 * @param {boolean} enabled
202 */
203 _toggleClass(node, className, enabled) {
204 const classes = this._nodeClasses(node);
205 classes.set(className, enabled);
206 }
207
208 /**
Tim van der Lippe97611c92020-02-12 16:56:58209 * @param {!SDK.DOMModel.DOMNode} node
Blink Reformat4c46d092018-04-07 15:32:37210 */
211 _installNodeClasses(node) {
212 const classes = this._nodeClasses(node);
213 const activeClasses = new Set();
214 for (const className of classes.keys()) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34215 if (classes.get(className)) {
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 }
219
220 const additionalClasses = this._splitTextIntoClasses(this._prompt.textWithCurrentSuggestion());
Tim van der Lippe1d6e57a2019-09-30 11:55:34221 for (const className of additionalClasses) {
Blink Reformat4c46d092018-04-07 15:32:37222 activeClasses.add(className);
Tim van der Lippe1d6e57a2019-09-30 11:55:34223 }
Blink Reformat4c46d092018-04-07 15:32:37224
Simon Zünda0d40622020-02-12 13:16:42225 const newClasses = [...activeClasses.values()].sort();
Blink Reformat4c46d092018-04-07 15:32:37226
227 this._pendingNodeClasses.set(node, newClasses.join(' '));
228 this._updateNodeThrottler.schedule(this._flushPendingClasses.bind(this));
229 }
230
231 /**
232 * @return {!Promise}
233 */
234 _flushPendingClasses() {
235 const promises = [];
236 for (const node of this._pendingNodeClasses.keys()) {
237 this._mutatingNodes.add(node);
238 const promise = node.setAttributeValuePromise('class', this._pendingNodeClasses.get(node))
239 .then(onClassValueUpdated.bind(this, node));
240 promises.push(promise);
241 }
242 this._pendingNodeClasses.clear();
243 return Promise.all(promises);
244
245 /**
Tim van der Lippe97611c92020-02-12 16:56:58246 * @param {!SDK.DOMModel.DOMNode} node
Tim van der Lippe13f71fb2019-11-29 11:17:39247 * @this {ClassesPaneWidget}
Blink Reformat4c46d092018-04-07 15:32:37248 */
249 function onClassValueUpdated(node) {
250 this._mutatingNodes.delete(node);
251 }
252 }
Tim van der Lippe13f71fb2019-11-29 11:17:39253}
Blink Reformat4c46d092018-04-07 15:32:37254
Tim van der Lippe13f71fb2019-11-29 11:17:39255ClassesPaneWidget._classesSymbol = Symbol('ClassesPaneWidget._classesSymbol');
Blink Reformat4c46d092018-04-07 15:32:37256
257/**
Tim van der Lippe97611c92020-02-12 16:56:58258 * @implements {UI.Toolbar.Provider}
Blink Reformat4c46d092018-04-07 15:32:37259 * @unrestricted
260 */
Tim van der Lippe13f71fb2019-11-29 11:17:39261export class ButtonProvider {
Blink Reformat4c46d092018-04-07 15:32:37262 constructor() {
Tim van der Lippe97611c92020-02-12 16:56:58263 this._button = new UI.Toolbar.ToolbarToggle(Common.UIString.UIString('Element Classes'), '');
Blink Reformat4c46d092018-04-07 15:32:37264 this._button.setText('.cls');
265 this._button.element.classList.add('monospace');
Tim van der Lippe97611c92020-02-12 16:56:58266 this._button.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._clicked, this);
Tim van der Lippe13f71fb2019-11-29 11:17:39267 this._view = new ClassesPaneWidget();
Blink Reformat4c46d092018-04-07 15:32:37268 }
269
270 _clicked() {
Tim van der Lippeaabc8302019-12-10 15:34:45271 ElementsPanel.instance().showToolbarPane(!this._view.isShowing() ? this._view : null, this._button);
Blink Reformat4c46d092018-04-07 15:32:37272 }
273
274 /**
275 * @override
Tim van der Lippe97611c92020-02-12 16:56:58276 * @return {!UI.Toolbar.ToolbarItem}
Blink Reformat4c46d092018-04-07 15:32:37277 */
278 item() {
279 return this._button;
280 }
Tim van der Lippe13f71fb2019-11-29 11:17:39281}
Blink Reformat4c46d092018-04-07 15:32:37282
283/**
284 * @unrestricted
285 */
Tim van der Lippe97611c92020-02-12 16:56:58286export class ClassNamePrompt extends UI.TextPrompt.TextPrompt {
Blink Reformat4c46d092018-04-07 15:32:37287 /**
Tim van der Lippe97611c92020-02-12 16:56:58288 * @param {function(!SDK.DOMModel.DOMNode):!Map<string, boolean>} nodeClasses
Blink Reformat4c46d092018-04-07 15:32:37289 */
290 constructor(nodeClasses) {
291 super();
292 this._nodeClasses = nodeClasses;
293 this.initialize(this._buildClassNameCompletions.bind(this), ' ');
294 this.disableDefaultSuggestionForEmptyInput();
295 this._selectedFrameId = '';
296 this._classNamesPromise = null;
297 }
298
299 /**
Tim van der Lippe97611c92020-02-12 16:56:58300 * @param {!SDK.DOMModel.DOMNode} selectedNode
Blink Reformat4c46d092018-04-07 15:32:37301 * @return {!Promise.<!Array.<string>>}
302 */
303 _getClassNames(selectedNode) {
304 const promises = [];
305 const completions = new Set();
306 this._selectedFrameId = selectedNode.frameId();
307
308 const cssModel = selectedNode.domModel().cssModel();
309 const allStyleSheets = cssModel.allStyleSheets();
310 for (const stylesheet of allStyleSheets) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34311 if (stylesheet.frameId !== this._selectedFrameId) {
Blink Reformat4c46d092018-04-07 15:32:37312 continue;
Tim van der Lippe1d6e57a2019-09-30 11:55:34313 }
Blink Reformat4c46d092018-04-07 15:32:37314 const cssPromise = cssModel.classNamesPromise(stylesheet.id).then(classes => completions.addAll(classes));
315 promises.push(cssPromise);
316 }
317
318 const domPromise = selectedNode.domModel()
319 .classNamesPromise(selectedNode.ownerDocument.id)
320 .then(classes => completions.addAll(classes));
321 promises.push(domPromise);
Simon Zünda0d40622020-02-12 13:16:42322 return Promise.all(promises).then(() => [...completions]);
Blink Reformat4c46d092018-04-07 15:32:37323 }
324
325 /**
326 * @param {string} expression
327 * @param {string} prefix
328 * @param {boolean=} force
329 * @return {!Promise<!UI.SuggestBox.Suggestions>}
330 */
331 _buildClassNameCompletions(expression, prefix, force) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34332 if (!prefix || force) {
Blink Reformat4c46d092018-04-07 15:32:37333 this._classNamesPromise = null;
Tim van der Lippe1d6e57a2019-09-30 11:55:34334 }
Blink Reformat4c46d092018-04-07 15:32:37335
Tim van der Lippe97611c92020-02-12 16:56:58336 const selectedNode = self.UI.context.flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34337 if (!selectedNode || (!prefix && !force && !expression.trim())) {
Blink Reformat4c46d092018-04-07 15:32:37338 return Promise.resolve([]);
Tim van der Lippe1d6e57a2019-09-30 11:55:34339 }
Blink Reformat4c46d092018-04-07 15:32:37340
Tim van der Lippe1d6e57a2019-09-30 11:55:34341 if (!this._classNamesPromise || this._selectedFrameId !== selectedNode.frameId()) {
Blink Reformat4c46d092018-04-07 15:32:37342 this._classNamesPromise = this._getClassNames(selectedNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34343 }
Blink Reformat4c46d092018-04-07 15:32:37344
345 return this._classNamesPromise.then(completions => {
Tim van der Lippe97611c92020-02-12 16:56:58346 const classesMap = this._nodeClasses(/** @type {!SDK.DOMModel.DOMNode} */ (selectedNode));
Blink Reformat4c46d092018-04-07 15:32:37347 completions = completions.filter(value => !classesMap.get(value));
348
Tim van der Lippe1d6e57a2019-09-30 11:55:34349 if (prefix[0] === '.') {
Blink Reformat4c46d092018-04-07 15:32:37350 completions = completions.map(value => '.' + value);
Tim van der Lippe1d6e57a2019-09-30 11:55:34351 }
Blink Reformat4c46d092018-04-07 15:32:37352 return completions.filter(value => value.startsWith(prefix)).sort().map(completion => ({text: completion}));
353 });
354 }
Tim van der Lippe13f71fb2019-11-29 11:17:39355}