blob: 33930ef6f6628616059fdc018196a554d078b507 [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);
Jack Franklin71519f82020-11-03 12:08:5918 this.registerRequiredCSS('elements/classesPaneWidget.css', {enableLegacyPatching: true});
Blink Reformat4c46d092018-04-07 15:32:3719 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 Lipped1a00aa2020-08-19 16:03:5643 UI.Context.Context.instance().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
Tim van der Lippe32f760f2020-10-01 10:52:1569 const eventTarget = /** @type {!HTMLElement} */ (event.target);
70 let text = /** @type {string} */ (eventTarget.textContent);
Blink Reformat4c46d092018-04-07 15:32:3771 if (isEscKey(event)) {
Simon Zündda7058f2020-02-28 13:57:2872 if (!Platform.StringUtilities.isWhitespace(text)) {
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();
Tim van der Lippe32f760f2020-10-01 10:52:1579 eventTarget.textContent = '';
Blink Reformat4c46d092018-04-07 15:32:3780
Tim van der Lipped1a00aa2020-08-19 16:03:5681 const node = UI.Context.Context.instance().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);
Michael Liaocafccfd2020-04-02 17:11:5987 if (!classNames.length) {
Patrick Brosset78fa1f52020-08-17 14:33:4888 this._installNodeClasses(node);
Michael Liaocafccfd2020-04-02 17:11:5989 return;
90 }
91
Tim van der Lippe1d6e57a2019-09-30 11:55:3492 for (const className of classNames) {
Blink Reformat4c46d092018-04-07 15:32:3793 this._toggleClass(node, className, true);
Tim van der Lippe1d6e57a2019-09-30 11:55:3494 }
Michael Liaocafccfd2020-04-02 17:11:5995
96 // annoucementString is used for screen reader to announce that the class(es) has been added successfully.
97 const joinClassString = classNames.join(' ');
98 const announcementString =
99 classNames.length > 1 ? ls`Classes ${joinClassString} added.` : ls`Class ${joinClassString} added.`;
100 UI.ARIAUtils.alert(announcementString, this.contentElement);
101
Blink Reformat4c46d092018-04-07 15:32:37102 this._installNodeClasses(node);
103 this._update();
104 }
105
106 _onTextChanged() {
Tim van der Lipped1a00aa2020-08-19 16:03:56107 const node = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34108 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:37109 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34110 }
Blink Reformat4c46d092018-04-07 15:32:37111 this._installNodeClasses(node);
112 }
113
114 /**
Tim van der Lippec02a97c2020-02-14 14:39:27115 * @param {!Common.EventTarget.EventTargetEvent} event
Blink Reformat4c46d092018-04-07 15:32:37116 */
117 _onDOMMutated(event) {
Tim van der Lippe97611c92020-02-12 16:56:58118 const node = /** @type {!SDK.DOMModel.DOMNode} */ (event.data);
Tim van der Lippe1d6e57a2019-09-30 11:55:34119 if (this._mutatingNodes.has(node)) {
Blink Reformat4c46d092018-04-07 15:32:37120 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34121 }
Tim van der Lippe32f760f2020-10-01 10:52:15122 cachedClassesMap.delete(node);
Blink Reformat4c46d092018-04-07 15:32:37123 this._update();
124 }
125
126 /**
Tim van der Lippec02a97c2020-02-14 14:39:27127 * @param {!Common.EventTarget.EventTargetEvent} event
Blink Reformat4c46d092018-04-07 15:32:37128 */
129 _onSelectedNodeChanged(event) {
130 if (this._previousTarget && this._prompt.text()) {
131 this._input.textContent = '';
132 this._installNodeClasses(this._previousTarget);
133 }
Tim van der Lippe97611c92020-02-12 16:56:58134 this._previousTarget = /** @type {?SDK.DOMModel.DOMNode} */ (event.data);
Blink Reformat4c46d092018-04-07 15:32:37135 this._update();
136 }
137
138 /**
139 * @override
140 */
141 wasShown() {
142 this._update();
143 }
144
145 _update() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34146 if (!this.isShowing()) {
Blink Reformat4c46d092018-04-07 15:32:37147 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34148 }
Blink Reformat4c46d092018-04-07 15:32:37149
Tim van der Lipped1a00aa2020-08-19 16:03:56150 let node = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34151 if (node) {
Blink Reformat4c46d092018-04-07 15:32:37152 node = node.enclosingElementOrSelf();
Tim van der Lippe1d6e57a2019-09-30 11:55:34153 }
Blink Reformat4c46d092018-04-07 15:32:37154
155 this._classesContainer.removeChildren();
Tim van der Lippe32f760f2020-10-01 10:52:15156 // @ts-ignore this._input is a div, not an input element. So this line makes no sense at all
Blink Reformat4c46d092018-04-07 15:32:37157 this._input.disabled = !node;
158
Tim van der Lippe1d6e57a2019-09-30 11:55:34159 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:37160 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34161 }
Blink Reformat4c46d092018-04-07 15:32:37162
163 const classes = this._nodeClasses(node);
Simon Zündf27be3d2020-02-11 14:46:27164 const keys = [...classes.keys()];
Tim van der Lippe32f760f2020-10-01 10:52:15165 keys.sort(Platform.StringUtilities.caseInsensetiveComparator);
166 for (const className of keys) {
Tim van der Lippe97611c92020-02-12 16:56:58167 const label = UI.UIUtils.CheckboxLabel.create(className, classes.get(className));
Blink Reformat4c46d092018-04-07 15:32:37168 label.classList.add('monospace');
169 label.checkboxElement.addEventListener('click', this._onClick.bind(this, className), false);
170 this._classesContainer.appendChild(label);
171 }
172 }
173
174 /**
175 * @param {string} className
176 * @param {!Event} event
177 */
178 _onClick(className, event) {
Tim van der Lipped1a00aa2020-08-19 16:03:56179 const node = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34180 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:37181 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34182 }
Tim van der Lippe32f760f2020-10-01 10:52:15183 const enabled = /** @type {!HTMLInputElement} */ (event.target).checked;
Blink Reformat4c46d092018-04-07 15:32:37184 this._toggleClass(node, className, enabled);
185 this._installNodeClasses(node);
186 }
187
188 /**
Tim van der Lippe97611c92020-02-12 16:56:58189 * @param {!SDK.DOMModel.DOMNode} node
Blink Reformat4c46d092018-04-07 15:32:37190 * @return {!Map<string, boolean>}
191 */
192 _nodeClasses(node) {
Tim van der Lippe32f760f2020-10-01 10:52:15193 let result = cachedClassesMap.get(node);
Blink Reformat4c46d092018-04-07 15:32:37194 if (!result) {
195 const classAttribute = node.getAttribute('class') || '';
196 const classes = classAttribute.split(/\s/);
197 result = new Map();
198 for (let i = 0; i < classes.length; ++i) {
199 const className = classes[i].trim();
Tim van der Lippe1d6e57a2019-09-30 11:55:34200 if (!className.length) {
Blink Reformat4c46d092018-04-07 15:32:37201 continue;
Tim van der Lippe1d6e57a2019-09-30 11:55:34202 }
Blink Reformat4c46d092018-04-07 15:32:37203 result.set(className, true);
204 }
Tim van der Lippe32f760f2020-10-01 10:52:15205 cachedClassesMap.set(node, result);
Blink Reformat4c46d092018-04-07 15:32:37206 }
207 return result;
208 }
209
210 /**
Tim van der Lippe97611c92020-02-12 16:56:58211 * @param {!SDK.DOMModel.DOMNode} node
Blink Reformat4c46d092018-04-07 15:32:37212 * @param {string} className
213 * @param {boolean} enabled
214 */
215 _toggleClass(node, className, enabled) {
216 const classes = this._nodeClasses(node);
217 classes.set(className, enabled);
218 }
219
220 /**
Tim van der Lippe97611c92020-02-12 16:56:58221 * @param {!SDK.DOMModel.DOMNode} node
Blink Reformat4c46d092018-04-07 15:32:37222 */
223 _installNodeClasses(node) {
224 const classes = this._nodeClasses(node);
225 const activeClasses = new Set();
226 for (const className of classes.keys()) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34227 if (classes.get(className)) {
Blink Reformat4c46d092018-04-07 15:32:37228 activeClasses.add(className);
Tim van der Lippe1d6e57a2019-09-30 11:55:34229 }
Blink Reformat4c46d092018-04-07 15:32:37230 }
231
232 const additionalClasses = this._splitTextIntoClasses(this._prompt.textWithCurrentSuggestion());
Tim van der Lippe1d6e57a2019-09-30 11:55:34233 for (const className of additionalClasses) {
Blink Reformat4c46d092018-04-07 15:32:37234 activeClasses.add(className);
Tim van der Lippe1d6e57a2019-09-30 11:55:34235 }
Blink Reformat4c46d092018-04-07 15:32:37236
Simon Zünda0d40622020-02-12 13:16:42237 const newClasses = [...activeClasses.values()].sort();
Blink Reformat4c46d092018-04-07 15:32:37238
239 this._pendingNodeClasses.set(node, newClasses.join(' '));
240 this._updateNodeThrottler.schedule(this._flushPendingClasses.bind(this));
241 }
242
243 /**
Tim van der Lippe32f760f2020-10-01 10:52:15244 * @return {!Promise<void>}
Blink Reformat4c46d092018-04-07 15:32:37245 */
Tim van der Lippe32f760f2020-10-01 10:52:15246 async _flushPendingClasses() {
Blink Reformat4c46d092018-04-07 15:32:37247 const promises = [];
248 for (const node of this._pendingNodeClasses.keys()) {
249 this._mutatingNodes.add(node);
Tim van der Lippe32f760f2020-10-01 10:52:15250 const promise = node.setAttributeValuePromise('class', /** @type {string} */ (this._pendingNodeClasses.get(node)))
Blink Reformat4c46d092018-04-07 15:32:37251 .then(onClassValueUpdated.bind(this, node));
252 promises.push(promise);
253 }
254 this._pendingNodeClasses.clear();
Tim van der Lippe32f760f2020-10-01 10:52:15255 await Promise.all(promises);
Blink Reformat4c46d092018-04-07 15:32:37256
257 /**
Tim van der Lippe97611c92020-02-12 16:56:58258 * @param {!SDK.DOMModel.DOMNode} node
Tim van der Lippe13f71fb2019-11-29 11:17:39259 * @this {ClassesPaneWidget}
Blink Reformat4c46d092018-04-07 15:32:37260 */
261 function onClassValueUpdated(node) {
262 this._mutatingNodes.delete(node);
263 }
264 }
Tim van der Lippe13f71fb2019-11-29 11:17:39265}
Blink Reformat4c46d092018-04-07 15:32:37266
Tim van der Lippe32f760f2020-10-01 10:52:15267/** @type {!WeakMap<!SDK.DOMModel.DOMNode, !Map<string, boolean>>} */
268const cachedClassesMap = new WeakMap();
Blink Reformat4c46d092018-04-07 15:32:37269
270/**
Tim van der Lippe97611c92020-02-12 16:56:58271 * @implements {UI.Toolbar.Provider}
Blink Reformat4c46d092018-04-07 15:32:37272 * @unrestricted
273 */
Tim van der Lippe13f71fb2019-11-29 11:17:39274export class ButtonProvider {
Blink Reformat4c46d092018-04-07 15:32:37275 constructor() {
Tim van der Lippe97611c92020-02-12 16:56:58276 this._button = new UI.Toolbar.ToolbarToggle(Common.UIString.UIString('Element Classes'), '');
Blink Reformat4c46d092018-04-07 15:32:37277 this._button.setText('.cls');
278 this._button.element.classList.add('monospace');
Tim van der Lippe97611c92020-02-12 16:56:58279 this._button.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._clicked, this);
Tim van der Lippe13f71fb2019-11-29 11:17:39280 this._view = new ClassesPaneWidget();
Blink Reformat4c46d092018-04-07 15:32:37281 }
282
283 _clicked() {
Tim van der Lippeaabc8302019-12-10 15:34:45284 ElementsPanel.instance().showToolbarPane(!this._view.isShowing() ? this._view : null, this._button);
Blink Reformat4c46d092018-04-07 15:32:37285 }
286
287 /**
288 * @override
Tim van der Lippe97611c92020-02-12 16:56:58289 * @return {!UI.Toolbar.ToolbarItem}
Blink Reformat4c46d092018-04-07 15:32:37290 */
291 item() {
292 return this._button;
293 }
Tim van der Lippe13f71fb2019-11-29 11:17:39294}
Blink Reformat4c46d092018-04-07 15:32:37295
296/**
297 * @unrestricted
298 */
Tim van der Lippe97611c92020-02-12 16:56:58299export class ClassNamePrompt extends UI.TextPrompt.TextPrompt {
Blink Reformat4c46d092018-04-07 15:32:37300 /**
Tim van der Lippe97611c92020-02-12 16:56:58301 * @param {function(!SDK.DOMModel.DOMNode):!Map<string, boolean>} nodeClasses
Blink Reformat4c46d092018-04-07 15:32:37302 */
303 constructor(nodeClasses) {
304 super();
305 this._nodeClasses = nodeClasses;
306 this.initialize(this._buildClassNameCompletions.bind(this), ' ');
307 this.disableDefaultSuggestionForEmptyInput();
Tim van der Lippe32f760f2020-10-01 10:52:15308 /** @type {?string} */
Blink Reformat4c46d092018-04-07 15:32:37309 this._selectedFrameId = '';
310 this._classNamesPromise = null;
311 }
312
313 /**
Tim van der Lippe97611c92020-02-12 16:56:58314 * @param {!SDK.DOMModel.DOMNode} selectedNode
Blink Reformat4c46d092018-04-07 15:32:37315 * @return {!Promise.<!Array.<string>>}
316 */
Mathias Bynens3abc0952020-04-20 14:15:52317 async _getClassNames(selectedNode) {
Blink Reformat4c46d092018-04-07 15:32:37318 const promises = [];
319 const completions = new Set();
320 this._selectedFrameId = selectedNode.frameId();
321
322 const cssModel = selectedNode.domModel().cssModel();
323 const allStyleSheets = cssModel.allStyleSheets();
324 for (const stylesheet of allStyleSheets) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34325 if (stylesheet.frameId !== this._selectedFrameId) {
Blink Reformat4c46d092018-04-07 15:32:37326 continue;
Tim van der Lippe1d6e57a2019-09-30 11:55:34327 }
Mathias Bynens3abc0952020-04-20 14:15:52328 const cssPromise = cssModel.classNamesPromise(stylesheet.id).then(classes => {
329 for (const className of classes) {
330 completions.add(className);
331 }
332 });
Blink Reformat4c46d092018-04-07 15:32:37333 promises.push(cssPromise);
334 }
335
Tim van der Lippe32f760f2020-10-01 10:52:15336 const ownerDocumentId = /** @type {number} */ (
337 /** @type {!SDK.DOMModel.DOMDocument} */ (selectedNode.ownerDocument).id);
338
339 const domPromise = selectedNode.domModel().classNamesPromise(ownerDocumentId).then(classes => {
Mathias Bynens3abc0952020-04-20 14:15:52340 for (const className of classes) {
341 completions.add(className);
342 }
343 });
Blink Reformat4c46d092018-04-07 15:32:37344 promises.push(domPromise);
Mathias Bynens3abc0952020-04-20 14:15:52345 await Promise.all(promises);
346 return [...completions];
Blink Reformat4c46d092018-04-07 15:32:37347 }
348
349 /**
350 * @param {string} expression
351 * @param {string} prefix
352 * @param {boolean=} force
353 * @return {!Promise<!UI.SuggestBox.Suggestions>}
354 */
355 _buildClassNameCompletions(expression, prefix, force) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34356 if (!prefix || force) {
Blink Reformat4c46d092018-04-07 15:32:37357 this._classNamesPromise = null;
Tim van der Lippe1d6e57a2019-09-30 11:55:34358 }
Blink Reformat4c46d092018-04-07 15:32:37359
Tim van der Lipped1a00aa2020-08-19 16:03:56360 const selectedNode = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34361 if (!selectedNode || (!prefix && !force && !expression.trim())) {
Blink Reformat4c46d092018-04-07 15:32:37362 return Promise.resolve([]);
Tim van der Lippe1d6e57a2019-09-30 11:55:34363 }
Blink Reformat4c46d092018-04-07 15:32:37364
Tim van der Lippe1d6e57a2019-09-30 11:55:34365 if (!this._classNamesPromise || this._selectedFrameId !== selectedNode.frameId()) {
Blink Reformat4c46d092018-04-07 15:32:37366 this._classNamesPromise = this._getClassNames(selectedNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34367 }
Blink Reformat4c46d092018-04-07 15:32:37368
369 return this._classNamesPromise.then(completions => {
Tim van der Lippe97611c92020-02-12 16:56:58370 const classesMap = this._nodeClasses(/** @type {!SDK.DOMModel.DOMNode} */ (selectedNode));
Blink Reformat4c46d092018-04-07 15:32:37371 completions = completions.filter(value => !classesMap.get(value));
372
Tim van der Lippe1d6e57a2019-09-30 11:55:34373 if (prefix[0] === '.') {
Blink Reformat4c46d092018-04-07 15:32:37374 completions = completions.map(value => '.' + value);
Tim van der Lippe1d6e57a2019-09-30 11:55:34375 }
Tim van der Lippe32f760f2020-10-01 10:52:15376 return completions.filter(value => value.startsWith(prefix)).sort().map(completion => ({
377 text: completion,
378 title: undefined,
379 subtitle: undefined,
380 iconType: undefined,
381 priority: undefined,
382 isSecondary: undefined,
383 subtitleRenderer: undefined,
384 selectionRange: undefined,
Alex Rudenko4504e3a2020-10-23 06:21:22385 hideGhostText: undefined,
386 iconElement: undefined,
Tim van der Lippe32f760f2020-10-01 10:52:15387 }));
Blink Reformat4c46d092018-04-07 15:32:37388 });
389 }
Tim van der Lippe13f71fb2019-11-29 11:17:39390}