blob: 409bf29387fc49bf97fa7a0a4ce2ed07032ddfb6 [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';
Simon Zündda7058f2020-02-28 13:57:286import * as Platform from '../platform/platform.js';
Tim van der Lippe97611c92020-02-12 16:56:587import * as SDK from '../sdk/sdk.js';
8import * as UI from '../ui/ui.js';
9
Tim van der Lippeaabc8302019-12-10 15:34:4510import {ElementsPanel} from './ElementsPanel.js';
11
Blink Reformat4c46d092018-04-07 15:32:3712/**
13 * @unrestricted
14 */
Tim van der Lippe97611c92020-02-12 16:56:5815export class ClassesPaneWidget extends UI.Widget.Widget {
Blink Reformat4c46d092018-04-07 15:32:3716 constructor() {
17 super(true);
18 this.registerRequiredCSS('elements/classesPaneWidget.css');
19 this.contentElement.className = 'styles-element-classes-pane';
20 const container = this.contentElement.createChild('div', 'title-container');
21 this._input = container.createChild('div', 'new-class-input monospace');
22 this.setDefaultFocusedElement(this._input);
23 this._classesContainer = this.contentElement.createChild('div', 'source-code');
24 this._classesContainer.classList.add('styles-element-classes-container');
Tim van der Lippe13f71fb2019-11-29 11:17:3925 this._prompt = new ClassNamePrompt(this._nodeClasses.bind(this));
Blink Reformat4c46d092018-04-07 15:32:3726 this._prompt.setAutocompletionTimeout(0);
27 this._prompt.renderAsBlock();
28
29 const proxyElement = this._prompt.attach(this._input);
Tim van der Lippe97611c92020-02-12 16:56:5830 this._prompt.setPlaceholder(Common.UIString.UIString('Add new class'));
Blink Reformat4c46d092018-04-07 15:32:3731 this._prompt.addEventListener(UI.TextPrompt.Events.TextChanged, this._onTextChanged, this);
32 proxyElement.addEventListener('keydown', this._onKeyDown.bind(this), false);
33
Paul Lewisdaac1062020-03-05 14:37:1034 SDK.SDKModel.TargetManager.instance().addModelListener(
Tim van der Lippe97611c92020-02-12 16:56:5835 SDK.DOMModel.DOMModel, SDK.DOMModel.Events.DOMMutated, this._onDOMMutated, this);
36 /** @type {!Set<!SDK.DOMModel.DOMNode>} */
Blink Reformat4c46d092018-04-07 15:32:3737 this._mutatingNodes = new Set();
Tim van der Lippe97611c92020-02-12 16:56:5838 /** @type {!Map<!SDK.DOMModel.DOMNode, string>} */
Blink Reformat4c46d092018-04-07 15:32:3739 this._pendingNodeClasses = new Map();
Tim van der Lippe97611c92020-02-12 16:56:5840 this._updateNodeThrottler = new Common.Throttler.Throttler(0);
41 /** @type {?SDK.DOMModel.DOMNode} */
Blink Reformat4c46d092018-04-07 15:32:3742 this._previousTarget = null;
Tim van der Lippe97611c92020-02-12 16:56:5843 self.UI.context.addFlavorChangeListener(SDK.DOMModel.DOMNode, this._onSelectedNodeChanged, this);
Blink Reformat4c46d092018-04-07 15:32:3744 }
45
46 /**
47 * @param {string} text
48 * @return {!Array.<string>}
49 */
50 _splitTextIntoClasses(text) {
Mathias Bynens3abc0952020-04-20 14:15:5251 return text.split(/[,\s]/).map(className => className.trim()).filter(className => className.length);
Blink Reformat4c46d092018-04-07 15:32:3752 }
53
54 /**
55 * @param {!Event} event
56 */
57 _onKeyDown(event) {
Tim van der Lippe1d6e57a2019-09-30 11:55:3458 if (!isEnterKey(event) && !isEscKey(event)) {
Blink Reformat4c46d092018-04-07 15:32:3759 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3460 }
Blink Reformat4c46d092018-04-07 15:32:3761
62 if (isEnterKey(event)) {
63 event.consume();
Tim van der Lippe1d6e57a2019-09-30 11:55:3464 if (this._prompt.acceptAutoComplete()) {
Blink Reformat4c46d092018-04-07 15:32:3765 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3466 }
Blink Reformat4c46d092018-04-07 15:32:3767 }
68
69 let text = event.target.textContent;
70 if (isEscKey(event)) {
Simon Zündda7058f2020-02-28 13:57:2871 if (!Platform.StringUtilities.isWhitespace(text)) {
Blink Reformat4c46d092018-04-07 15:32:3772 event.consume(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:3473 }
Blink Reformat4c46d092018-04-07 15:32:3774 text = '';
75 }
76
77 this._prompt.clearAutocomplete();
78 event.target.textContent = '';
79
Tim van der Lippe97611c92020-02-12 16:56:5880 const node = self.UI.context.flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:3481 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:3782 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3483 }
Blink Reformat4c46d092018-04-07 15:32:3784
85 const classNames = this._splitTextIntoClasses(text);
Michael Liaocafccfd2020-04-02 17:11:5986 if (!classNames.length) {
87 return;
88 }
89
Tim van der Lippe1d6e57a2019-09-30 11:55:3490 for (const className of classNames) {
Blink Reformat4c46d092018-04-07 15:32:3791 this._toggleClass(node, className, true);
Tim van der Lippe1d6e57a2019-09-30 11:55:3492 }
Michael Liaocafccfd2020-04-02 17:11:5993
94 // annoucementString is used for screen reader to announce that the class(es) has been added successfully.
95 const joinClassString = classNames.join(' ');
96 const announcementString =
97 classNames.length > 1 ? ls`Classes ${joinClassString} added.` : ls`Class ${joinClassString} added.`;
98 UI.ARIAUtils.alert(announcementString, this.contentElement);
99
Blink Reformat4c46d092018-04-07 15:32:37100 this._installNodeClasses(node);
101 this._update();
102 }
103
104 _onTextChanged() {
Tim van der Lippe97611c92020-02-12 16:56:58105 const node = self.UI.context.flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34106 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:37107 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34108 }
Blink Reformat4c46d092018-04-07 15:32:37109 this._installNodeClasses(node);
110 }
111
112 /**
Tim van der Lippec02a97c2020-02-14 14:39:27113 * @param {!Common.EventTarget.EventTargetEvent} event
Blink Reformat4c46d092018-04-07 15:32:37114 */
115 _onDOMMutated(event) {
Tim van der Lippe97611c92020-02-12 16:56:58116 const node = /** @type {!SDK.DOMModel.DOMNode} */ (event.data);
Tim van der Lippe1d6e57a2019-09-30 11:55:34117 if (this._mutatingNodes.has(node)) {
Blink Reformat4c46d092018-04-07 15:32:37118 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34119 }
Tim van der Lippe13f71fb2019-11-29 11:17:39120 delete node[ClassesPaneWidget._classesSymbol];
Blink Reformat4c46d092018-04-07 15:32:37121 this._update();
122 }
123
124 /**
Tim van der Lippec02a97c2020-02-14 14:39:27125 * @param {!Common.EventTarget.EventTargetEvent} event
Blink Reformat4c46d092018-04-07 15:32:37126 */
127 _onSelectedNodeChanged(event) {
128 if (this._previousTarget && this._prompt.text()) {
129 this._input.textContent = '';
130 this._installNodeClasses(this._previousTarget);
131 }
Tim van der Lippe97611c92020-02-12 16:56:58132 this._previousTarget = /** @type {?SDK.DOMModel.DOMNode} */ (event.data);
Blink Reformat4c46d092018-04-07 15:32:37133 this._update();
134 }
135
136 /**
137 * @override
138 */
139 wasShown() {
140 this._update();
141 }
142
143 _update() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34144 if (!this.isShowing()) {
Blink Reformat4c46d092018-04-07 15:32:37145 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34146 }
Blink Reformat4c46d092018-04-07 15:32:37147
Tim van der Lippe97611c92020-02-12 16:56:58148 let node = self.UI.context.flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34149 if (node) {
Blink Reformat4c46d092018-04-07 15:32:37150 node = node.enclosingElementOrSelf();
Tim van der Lippe1d6e57a2019-09-30 11:55:34151 }
Blink Reformat4c46d092018-04-07 15:32:37152
153 this._classesContainer.removeChildren();
154 this._input.disabled = !node;
155
Tim van der Lippe1d6e57a2019-09-30 11:55:34156 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:37157 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34158 }
Blink Reformat4c46d092018-04-07 15:32:37159
160 const classes = this._nodeClasses(node);
Simon Zündf27be3d2020-02-11 14:46:27161 const keys = [...classes.keys()];
Blink Reformat4c46d092018-04-07 15:32:37162 keys.sort(String.caseInsensetiveComparator);
163 for (let i = 0; i < keys.length; ++i) {
164 const className = keys[i];
Tim van der Lippe97611c92020-02-12 16:56:58165 const label = UI.UIUtils.CheckboxLabel.create(className, classes.get(className));
Blink Reformat4c46d092018-04-07 15:32:37166 label.classList.add('monospace');
167 label.checkboxElement.addEventListener('click', this._onClick.bind(this, className), false);
168 this._classesContainer.appendChild(label);
169 }
170 }
171
172 /**
173 * @param {string} className
174 * @param {!Event} event
175 */
176 _onClick(className, event) {
Tim van der Lippe97611c92020-02-12 16:56:58177 const node = self.UI.context.flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34178 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:37179 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34180 }
Blink Reformat4c46d092018-04-07 15:32:37181 const enabled = event.target.checked;
182 this._toggleClass(node, className, enabled);
183 this._installNodeClasses(node);
184 }
185
186 /**
Tim van der Lippe97611c92020-02-12 16:56:58187 * @param {!SDK.DOMModel.DOMNode} node
Blink Reformat4c46d092018-04-07 15:32:37188 * @return {!Map<string, boolean>}
189 */
190 _nodeClasses(node) {
Tim van der Lippe13f71fb2019-11-29 11:17:39191 let result = node[ClassesPaneWidget._classesSymbol];
Blink Reformat4c46d092018-04-07 15:32:37192 if (!result) {
193 const classAttribute = node.getAttribute('class') || '';
194 const classes = classAttribute.split(/\s/);
195 result = new Map();
196 for (let i = 0; i < classes.length; ++i) {
197 const className = classes[i].trim();
Tim van der Lippe1d6e57a2019-09-30 11:55:34198 if (!className.length) {
Blink Reformat4c46d092018-04-07 15:32:37199 continue;
Tim van der Lippe1d6e57a2019-09-30 11:55:34200 }
Blink Reformat4c46d092018-04-07 15:32:37201 result.set(className, true);
202 }
Tim van der Lippe13f71fb2019-11-29 11:17:39203 node[ClassesPaneWidget._classesSymbol] = result;
Blink Reformat4c46d092018-04-07 15:32:37204 }
205 return result;
206 }
207
208 /**
Tim van der Lippe97611c92020-02-12 16:56:58209 * @param {!SDK.DOMModel.DOMNode} node
Blink Reformat4c46d092018-04-07 15:32:37210 * @param {string} className
211 * @param {boolean} enabled
212 */
213 _toggleClass(node, className, enabled) {
214 const classes = this._nodeClasses(node);
215 classes.set(className, enabled);
216 }
217
218 /**
Tim van der Lippe97611c92020-02-12 16:56:58219 * @param {!SDK.DOMModel.DOMNode} node
Blink Reformat4c46d092018-04-07 15:32:37220 */
221 _installNodeClasses(node) {
222 const classes = this._nodeClasses(node);
223 const activeClasses = new Set();
224 for (const className of classes.keys()) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34225 if (classes.get(className)) {
Blink Reformat4c46d092018-04-07 15:32:37226 activeClasses.add(className);
Tim van der Lippe1d6e57a2019-09-30 11:55:34227 }
Blink Reformat4c46d092018-04-07 15:32:37228 }
229
230 const additionalClasses = this._splitTextIntoClasses(this._prompt.textWithCurrentSuggestion());
Tim van der Lippe1d6e57a2019-09-30 11:55:34231 for (const className of additionalClasses) {
Blink Reformat4c46d092018-04-07 15:32:37232 activeClasses.add(className);
Tim van der Lippe1d6e57a2019-09-30 11:55:34233 }
Blink Reformat4c46d092018-04-07 15:32:37234
Simon Zünda0d40622020-02-12 13:16:42235 const newClasses = [...activeClasses.values()].sort();
Blink Reformat4c46d092018-04-07 15:32:37236
237 this._pendingNodeClasses.set(node, newClasses.join(' '));
238 this._updateNodeThrottler.schedule(this._flushPendingClasses.bind(this));
239 }
240
241 /**
242 * @return {!Promise}
243 */
244 _flushPendingClasses() {
245 const promises = [];
246 for (const node of this._pendingNodeClasses.keys()) {
247 this._mutatingNodes.add(node);
248 const promise = node.setAttributeValuePromise('class', this._pendingNodeClasses.get(node))
249 .then(onClassValueUpdated.bind(this, node));
250 promises.push(promise);
251 }
252 this._pendingNodeClasses.clear();
253 return Promise.all(promises);
254
255 /**
Tim van der Lippe97611c92020-02-12 16:56:58256 * @param {!SDK.DOMModel.DOMNode} node
Tim van der Lippe13f71fb2019-11-29 11:17:39257 * @this {ClassesPaneWidget}
Blink Reformat4c46d092018-04-07 15:32:37258 */
259 function onClassValueUpdated(node) {
260 this._mutatingNodes.delete(node);
261 }
262 }
Tim van der Lippe13f71fb2019-11-29 11:17:39263}
Blink Reformat4c46d092018-04-07 15:32:37264
Tim van der Lippe13f71fb2019-11-29 11:17:39265ClassesPaneWidget._classesSymbol = Symbol('ClassesPaneWidget._classesSymbol');
Blink Reformat4c46d092018-04-07 15:32:37266
267/**
Tim van der Lippe97611c92020-02-12 16:56:58268 * @implements {UI.Toolbar.Provider}
Blink Reformat4c46d092018-04-07 15:32:37269 * @unrestricted
270 */
Tim van der Lippe13f71fb2019-11-29 11:17:39271export class ButtonProvider {
Blink Reformat4c46d092018-04-07 15:32:37272 constructor() {
Tim van der Lippe97611c92020-02-12 16:56:58273 this._button = new UI.Toolbar.ToolbarToggle(Common.UIString.UIString('Element Classes'), '');
Blink Reformat4c46d092018-04-07 15:32:37274 this._button.setText('.cls');
275 this._button.element.classList.add('monospace');
Tim van der Lippe97611c92020-02-12 16:56:58276 this._button.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._clicked, this);
Tim van der Lippe13f71fb2019-11-29 11:17:39277 this._view = new ClassesPaneWidget();
Blink Reformat4c46d092018-04-07 15:32:37278 }
279
280 _clicked() {
Tim van der Lippeaabc8302019-12-10 15:34:45281 ElementsPanel.instance().showToolbarPane(!this._view.isShowing() ? this._view : null, this._button);
Blink Reformat4c46d092018-04-07 15:32:37282 }
283
284 /**
285 * @override
Tim van der Lippe97611c92020-02-12 16:56:58286 * @return {!UI.Toolbar.ToolbarItem}
Blink Reformat4c46d092018-04-07 15:32:37287 */
288 item() {
289 return this._button;
290 }
Tim van der Lippe13f71fb2019-11-29 11:17:39291}
Blink Reformat4c46d092018-04-07 15:32:37292
293/**
294 * @unrestricted
295 */
Tim van der Lippe97611c92020-02-12 16:56:58296export class ClassNamePrompt extends UI.TextPrompt.TextPrompt {
Blink Reformat4c46d092018-04-07 15:32:37297 /**
Tim van der Lippe97611c92020-02-12 16:56:58298 * @param {function(!SDK.DOMModel.DOMNode):!Map<string, boolean>} nodeClasses
Blink Reformat4c46d092018-04-07 15:32:37299 */
300 constructor(nodeClasses) {
301 super();
302 this._nodeClasses = nodeClasses;
303 this.initialize(this._buildClassNameCompletions.bind(this), ' ');
304 this.disableDefaultSuggestionForEmptyInput();
305 this._selectedFrameId = '';
306 this._classNamesPromise = null;
307 }
308
309 /**
Tim van der Lippe97611c92020-02-12 16:56:58310 * @param {!SDK.DOMModel.DOMNode} selectedNode
Blink Reformat4c46d092018-04-07 15:32:37311 * @return {!Promise.<!Array.<string>>}
312 */
Mathias Bynens3abc0952020-04-20 14:15:52313 async _getClassNames(selectedNode) {
Blink Reformat4c46d092018-04-07 15:32:37314 const promises = [];
315 const completions = new Set();
316 this._selectedFrameId = selectedNode.frameId();
317
318 const cssModel = selectedNode.domModel().cssModel();
319 const allStyleSheets = cssModel.allStyleSheets();
320 for (const stylesheet of allStyleSheets) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34321 if (stylesheet.frameId !== this._selectedFrameId) {
Blink Reformat4c46d092018-04-07 15:32:37322 continue;
Tim van der Lippe1d6e57a2019-09-30 11:55:34323 }
Mathias Bynens3abc0952020-04-20 14:15:52324 const cssPromise = cssModel.classNamesPromise(stylesheet.id).then(classes => {
325 for (const className of classes) {
326 completions.add(className);
327 }
328 });
Blink Reformat4c46d092018-04-07 15:32:37329 promises.push(cssPromise);
330 }
331
Mathias Bynens3abc0952020-04-20 14:15:52332 const domPromise = selectedNode.domModel().classNamesPromise(selectedNode.ownerDocument.id).then(classes => {
333 for (const className of classes) {
334 completions.add(className);
335 }
336 });
Blink Reformat4c46d092018-04-07 15:32:37337 promises.push(domPromise);
Mathias Bynens3abc0952020-04-20 14:15:52338 await Promise.all(promises);
339 return [...completions];
Blink Reformat4c46d092018-04-07 15:32:37340 }
341
342 /**
343 * @param {string} expression
344 * @param {string} prefix
345 * @param {boolean=} force
346 * @return {!Promise<!UI.SuggestBox.Suggestions>}
347 */
348 _buildClassNameCompletions(expression, prefix, force) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34349 if (!prefix || force) {
Blink Reformat4c46d092018-04-07 15:32:37350 this._classNamesPromise = null;
Tim van der Lippe1d6e57a2019-09-30 11:55:34351 }
Blink Reformat4c46d092018-04-07 15:32:37352
Tim van der Lippe97611c92020-02-12 16:56:58353 const selectedNode = self.UI.context.flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34354 if (!selectedNode || (!prefix && !force && !expression.trim())) {
Blink Reformat4c46d092018-04-07 15:32:37355 return Promise.resolve([]);
Tim van der Lippe1d6e57a2019-09-30 11:55:34356 }
Blink Reformat4c46d092018-04-07 15:32:37357
Tim van der Lippe1d6e57a2019-09-30 11:55:34358 if (!this._classNamesPromise || this._selectedFrameId !== selectedNode.frameId()) {
Blink Reformat4c46d092018-04-07 15:32:37359 this._classNamesPromise = this._getClassNames(selectedNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34360 }
Blink Reformat4c46d092018-04-07 15:32:37361
362 return this._classNamesPromise.then(completions => {
Tim van der Lippe97611c92020-02-12 16:56:58363 const classesMap = this._nodeClasses(/** @type {!SDK.DOMModel.DOMNode} */ (selectedNode));
Blink Reformat4c46d092018-04-07 15:32:37364 completions = completions.filter(value => !classesMap.get(value));
365
Tim van der Lippe1d6e57a2019-09-30 11:55:34366 if (prefix[0] === '.') {
Blink Reformat4c46d092018-04-07 15:32:37367 completions = completions.map(value => '.' + value);
Tim van der Lippe1d6e57a2019-09-30 11:55:34368 }
Blink Reformat4c46d092018-04-07 15:32:37369 return completions.filter(value => value.startsWith(prefix)).sort().map(completion => ({text: completion}));
370 });
371 }
Tim van der Lippe13f71fb2019-11-29 11:17:39372}