blob: b5e6becb0e46426dc917a50bf2f3d6d500260745 [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
Tim van der Lippe97611c92020-02-12 16:56:5812export class ClassesPaneWidget extends UI.Widget.Widget {
Blink Reformat4c46d092018-04-07 15:32:3713 constructor() {
14 super(true);
Jack Franklin71519f82020-11-03 12:08:5915 this.registerRequiredCSS('elements/classesPaneWidget.css', {enableLegacyPatching: true});
Blink Reformat4c46d092018-04-07 15:32:3716 this.contentElement.className = 'styles-element-classes-pane';
17 const container = this.contentElement.createChild('div', 'title-container');
18 this._input = container.createChild('div', 'new-class-input monospace');
19 this.setDefaultFocusedElement(this._input);
20 this._classesContainer = this.contentElement.createChild('div', 'source-code');
21 this._classesContainer.classList.add('styles-element-classes-container');
Tim van der Lippe13f71fb2019-11-29 11:17:3922 this._prompt = new ClassNamePrompt(this._nodeClasses.bind(this));
Blink Reformat4c46d092018-04-07 15:32:3723 this._prompt.setAutocompletionTimeout(0);
24 this._prompt.renderAsBlock();
25
26 const proxyElement = this._prompt.attach(this._input);
Tim van der Lippe97611c92020-02-12 16:56:5827 this._prompt.setPlaceholder(Common.UIString.UIString('Add new class'));
Blink Reformat4c46d092018-04-07 15:32:3728 this._prompt.addEventListener(UI.TextPrompt.Events.TextChanged, this._onTextChanged, this);
29 proxyElement.addEventListener('keydown', this._onKeyDown.bind(this), false);
30
Paul Lewisdaac1062020-03-05 14:37:1031 SDK.SDKModel.TargetManager.instance().addModelListener(
Tim van der Lippe97611c92020-02-12 16:56:5832 SDK.DOMModel.DOMModel, SDK.DOMModel.Events.DOMMutated, this._onDOMMutated, this);
33 /** @type {!Set<!SDK.DOMModel.DOMNode>} */
Blink Reformat4c46d092018-04-07 15:32:3734 this._mutatingNodes = new Set();
Tim van der Lippe97611c92020-02-12 16:56:5835 /** @type {!Map<!SDK.DOMModel.DOMNode, string>} */
Blink Reformat4c46d092018-04-07 15:32:3736 this._pendingNodeClasses = new Map();
Tim van der Lippe97611c92020-02-12 16:56:5837 this._updateNodeThrottler = new Common.Throttler.Throttler(0);
38 /** @type {?SDK.DOMModel.DOMNode} */
Blink Reformat4c46d092018-04-07 15:32:3739 this._previousTarget = null;
Tim van der Lipped1a00aa2020-08-19 16:03:5640 UI.Context.Context.instance().addFlavorChangeListener(SDK.DOMModel.DOMNode, this._onSelectedNodeChanged, this);
Blink Reformat4c46d092018-04-07 15:32:3741 }
42
43 /**
44 * @param {string} text
45 * @return {!Array.<string>}
46 */
47 _splitTextIntoClasses(text) {
Mathias Bynens3abc0952020-04-20 14:15:5248 return text.split(/[,\s]/).map(className => className.trim()).filter(className => className.length);
Blink Reformat4c46d092018-04-07 15:32:3749 }
50
51 /**
52 * @param {!Event} event
53 */
54 _onKeyDown(event) {
Tim van der Lippe1d6e57a2019-09-30 11:55:3455 if (!isEnterKey(event) && !isEscKey(event)) {
Blink Reformat4c46d092018-04-07 15:32:3756 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3457 }
Blink Reformat4c46d092018-04-07 15:32:3758
59 if (isEnterKey(event)) {
60 event.consume();
Tim van der Lippe1d6e57a2019-09-30 11:55:3461 if (this._prompt.acceptAutoComplete()) {
Blink Reformat4c46d092018-04-07 15:32:3762 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3463 }
Blink Reformat4c46d092018-04-07 15:32:3764 }
65
Tim van der Lippe32f760f2020-10-01 10:52:1566 const eventTarget = /** @type {!HTMLElement} */ (event.target);
67 let text = /** @type {string} */ (eventTarget.textContent);
Blink Reformat4c46d092018-04-07 15:32:3768 if (isEscKey(event)) {
Simon Zündda7058f2020-02-28 13:57:2869 if (!Platform.StringUtilities.isWhitespace(text)) {
Blink Reformat4c46d092018-04-07 15:32:3770 event.consume(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:3471 }
Blink Reformat4c46d092018-04-07 15:32:3772 text = '';
73 }
74
75 this._prompt.clearAutocomplete();
Tim van der Lippe32f760f2020-10-01 10:52:1576 eventTarget.textContent = '';
Blink Reformat4c46d092018-04-07 15:32:3777
Tim van der Lipped1a00aa2020-08-19 16:03:5678 const node = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:3479 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:3780 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3481 }
Blink Reformat4c46d092018-04-07 15:32:3782
83 const classNames = this._splitTextIntoClasses(text);
Michael Liaocafccfd2020-04-02 17:11:5984 if (!classNames.length) {
Patrick Brosset78fa1f52020-08-17 14:33:4885 this._installNodeClasses(node);
Michael Liaocafccfd2020-04-02 17:11:5986 return;
87 }
88
Tim van der Lippe1d6e57a2019-09-30 11:55:3489 for (const className of classNames) {
Blink Reformat4c46d092018-04-07 15:32:3790 this._toggleClass(node, className, true);
Tim van der Lippe1d6e57a2019-09-30 11:55:3491 }
Michael Liaocafccfd2020-04-02 17:11:5992
93 // annoucementString is used for screen reader to announce that the class(es) has been added successfully.
94 const joinClassString = classNames.join(' ');
95 const announcementString =
96 classNames.length > 1 ? ls`Classes ${joinClassString} added.` : ls`Class ${joinClassString} added.`;
97 UI.ARIAUtils.alert(announcementString, this.contentElement);
98
Blink Reformat4c46d092018-04-07 15:32:3799 this._installNodeClasses(node);
100 this._update();
101 }
102
103 _onTextChanged() {
Tim van der Lipped1a00aa2020-08-19 16:03:56104 const node = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34105 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:37106 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34107 }
Blink Reformat4c46d092018-04-07 15:32:37108 this._installNodeClasses(node);
109 }
110
111 /**
Tim van der Lippec02a97c2020-02-14 14:39:27112 * @param {!Common.EventTarget.EventTargetEvent} event
Blink Reformat4c46d092018-04-07 15:32:37113 */
114 _onDOMMutated(event) {
Tim van der Lippe97611c92020-02-12 16:56:58115 const node = /** @type {!SDK.DOMModel.DOMNode} */ (event.data);
Tim van der Lippe1d6e57a2019-09-30 11:55:34116 if (this._mutatingNodes.has(node)) {
Blink Reformat4c46d092018-04-07 15:32:37117 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34118 }
Tim van der Lippe32f760f2020-10-01 10:52:15119 cachedClassesMap.delete(node);
Blink Reformat4c46d092018-04-07 15:32:37120 this._update();
121 }
122
123 /**
Tim van der Lippec02a97c2020-02-14 14:39:27124 * @param {!Common.EventTarget.EventTargetEvent} event
Blink Reformat4c46d092018-04-07 15:32:37125 */
126 _onSelectedNodeChanged(event) {
127 if (this._previousTarget && this._prompt.text()) {
128 this._input.textContent = '';
129 this._installNodeClasses(this._previousTarget);
130 }
Tim van der Lippe97611c92020-02-12 16:56:58131 this._previousTarget = /** @type {?SDK.DOMModel.DOMNode} */ (event.data);
Blink Reformat4c46d092018-04-07 15:32:37132 this._update();
133 }
134
135 /**
136 * @override
137 */
138 wasShown() {
139 this._update();
140 }
141
142 _update() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34143 if (!this.isShowing()) {
Blink Reformat4c46d092018-04-07 15:32:37144 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34145 }
Blink Reformat4c46d092018-04-07 15:32:37146
Tim van der Lipped1a00aa2020-08-19 16:03:56147 let node = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34148 if (node) {
Blink Reformat4c46d092018-04-07 15:32:37149 node = node.enclosingElementOrSelf();
Tim van der Lippe1d6e57a2019-09-30 11:55:34150 }
Blink Reformat4c46d092018-04-07 15:32:37151
152 this._classesContainer.removeChildren();
Tim van der Lippe32f760f2020-10-01 10:52:15153 // @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:37154 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()];
Tim van der Lippe32f760f2020-10-01 10:52:15162 keys.sort(Platform.StringUtilities.caseInsensetiveComparator);
163 for (const className of keys) {
Tim van der Lippe97611c92020-02-12 16:56:58164 const label = UI.UIUtils.CheckboxLabel.create(className, classes.get(className));
Blink Reformat4c46d092018-04-07 15:32:37165 label.classList.add('monospace');
166 label.checkboxElement.addEventListener('click', this._onClick.bind(this, className), false);
167 this._classesContainer.appendChild(label);
168 }
169 }
170
171 /**
172 * @param {string} className
173 * @param {!Event} event
174 */
175 _onClick(className, event) {
Tim van der Lipped1a00aa2020-08-19 16:03:56176 const node = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34177 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:37178 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34179 }
Tim van der Lippe32f760f2020-10-01 10:52:15180 const enabled = /** @type {!HTMLInputElement} */ (event.target).checked;
Blink Reformat4c46d092018-04-07 15:32:37181 this._toggleClass(node, className, enabled);
182 this._installNodeClasses(node);
183 }
184
185 /**
Tim van der Lippe97611c92020-02-12 16:56:58186 * @param {!SDK.DOMModel.DOMNode} node
Blink Reformat4c46d092018-04-07 15:32:37187 * @return {!Map<string, boolean>}
188 */
189 _nodeClasses(node) {
Tim van der Lippe32f760f2020-10-01 10:52:15190 let result = cachedClassesMap.get(node);
Blink Reformat4c46d092018-04-07 15:32:37191 if (!result) {
192 const classAttribute = node.getAttribute('class') || '';
193 const classes = classAttribute.split(/\s/);
194 result = new Map();
195 for (let i = 0; i < classes.length; ++i) {
196 const className = classes[i].trim();
Tim van der Lippe1d6e57a2019-09-30 11:55:34197 if (!className.length) {
Blink Reformat4c46d092018-04-07 15:32:37198 continue;
Tim van der Lippe1d6e57a2019-09-30 11:55:34199 }
Blink Reformat4c46d092018-04-07 15:32:37200 result.set(className, true);
201 }
Tim van der Lippe32f760f2020-10-01 10:52:15202 cachedClassesMap.set(node, result);
Blink Reformat4c46d092018-04-07 15:32:37203 }
204 return result;
205 }
206
207 /**
Tim van der Lippe97611c92020-02-12 16:56:58208 * @param {!SDK.DOMModel.DOMNode} node
Blink Reformat4c46d092018-04-07 15:32:37209 * @param {string} className
210 * @param {boolean} enabled
211 */
212 _toggleClass(node, className, enabled) {
213 const classes = this._nodeClasses(node);
214 classes.set(className, enabled);
215 }
216
217 /**
Tim van der Lippe97611c92020-02-12 16:56:58218 * @param {!SDK.DOMModel.DOMNode} node
Blink Reformat4c46d092018-04-07 15:32:37219 */
220 _installNodeClasses(node) {
221 const classes = this._nodeClasses(node);
222 const activeClasses = new Set();
223 for (const className of classes.keys()) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34224 if (classes.get(className)) {
Blink Reformat4c46d092018-04-07 15:32:37225 activeClasses.add(className);
Tim van der Lippe1d6e57a2019-09-30 11:55:34226 }
Blink Reformat4c46d092018-04-07 15:32:37227 }
228
229 const additionalClasses = this._splitTextIntoClasses(this._prompt.textWithCurrentSuggestion());
Tim van der Lippe1d6e57a2019-09-30 11:55:34230 for (const className of additionalClasses) {
Blink Reformat4c46d092018-04-07 15:32:37231 activeClasses.add(className);
Tim van der Lippe1d6e57a2019-09-30 11:55:34232 }
Blink Reformat4c46d092018-04-07 15:32:37233
Simon Zünda0d40622020-02-12 13:16:42234 const newClasses = [...activeClasses.values()].sort();
Blink Reformat4c46d092018-04-07 15:32:37235
236 this._pendingNodeClasses.set(node, newClasses.join(' '));
237 this._updateNodeThrottler.schedule(this._flushPendingClasses.bind(this));
238 }
239
240 /**
Tim van der Lippe32f760f2020-10-01 10:52:15241 * @return {!Promise<void>}
Blink Reformat4c46d092018-04-07 15:32:37242 */
Tim van der Lippe32f760f2020-10-01 10:52:15243 async _flushPendingClasses() {
Blink Reformat4c46d092018-04-07 15:32:37244 const promises = [];
245 for (const node of this._pendingNodeClasses.keys()) {
246 this._mutatingNodes.add(node);
Tim van der Lippe32f760f2020-10-01 10:52:15247 const promise = node.setAttributeValuePromise('class', /** @type {string} */ (this._pendingNodeClasses.get(node)))
Blink Reformat4c46d092018-04-07 15:32:37248 .then(onClassValueUpdated.bind(this, node));
249 promises.push(promise);
250 }
251 this._pendingNodeClasses.clear();
Tim van der Lippe32f760f2020-10-01 10:52:15252 await Promise.all(promises);
Blink Reformat4c46d092018-04-07 15:32:37253
254 /**
Tim van der Lippe97611c92020-02-12 16:56:58255 * @param {!SDK.DOMModel.DOMNode} node
Tim van der Lippe13f71fb2019-11-29 11:17:39256 * @this {ClassesPaneWidget}
Blink Reformat4c46d092018-04-07 15:32:37257 */
258 function onClassValueUpdated(node) {
259 this._mutatingNodes.delete(node);
260 }
261 }
Tim van der Lippe13f71fb2019-11-29 11:17:39262}
Blink Reformat4c46d092018-04-07 15:32:37263
Tim van der Lippe32f760f2020-10-01 10:52:15264/** @type {!WeakMap<!SDK.DOMModel.DOMNode, !Map<string, boolean>>} */
265const cachedClassesMap = new WeakMap();
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 */
Tim van der Lippe13f71fb2019-11-29 11:17:39270export class ButtonProvider {
Blink Reformat4c46d092018-04-07 15:32:37271 constructor() {
Tim van der Lippe97611c92020-02-12 16:56:58272 this._button = new UI.Toolbar.ToolbarToggle(Common.UIString.UIString('Element Classes'), '');
Blink Reformat4c46d092018-04-07 15:32:37273 this._button.setText('.cls');
274 this._button.element.classList.add('monospace');
Tim van der Lippe97611c92020-02-12 16:56:58275 this._button.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._clicked, this);
Tim van der Lippe13f71fb2019-11-29 11:17:39276 this._view = new ClassesPaneWidget();
Blink Reformat4c46d092018-04-07 15:32:37277 }
278
279 _clicked() {
Tim van der Lippeaabc8302019-12-10 15:34:45280 ElementsPanel.instance().showToolbarPane(!this._view.isShowing() ? this._view : null, this._button);
Blink Reformat4c46d092018-04-07 15:32:37281 }
282
283 /**
284 * @override
Tim van der Lippe97611c92020-02-12 16:56:58285 * @return {!UI.Toolbar.ToolbarItem}
Blink Reformat4c46d092018-04-07 15:32:37286 */
287 item() {
288 return this._button;
289 }
Tim van der Lippe13f71fb2019-11-29 11:17:39290}
Blink Reformat4c46d092018-04-07 15:32:37291
Tim van der Lippe97611c92020-02-12 16:56:58292export class ClassNamePrompt extends UI.TextPrompt.TextPrompt {
Blink Reformat4c46d092018-04-07 15:32:37293 /**
Tim van der Lippe97611c92020-02-12 16:56:58294 * @param {function(!SDK.DOMModel.DOMNode):!Map<string, boolean>} nodeClasses
Blink Reformat4c46d092018-04-07 15:32:37295 */
296 constructor(nodeClasses) {
297 super();
298 this._nodeClasses = nodeClasses;
299 this.initialize(this._buildClassNameCompletions.bind(this), ' ');
300 this.disableDefaultSuggestionForEmptyInput();
Tim van der Lippe32f760f2020-10-01 10:52:15301 /** @type {?string} */
Blink Reformat4c46d092018-04-07 15:32:37302 this._selectedFrameId = '';
303 this._classNamesPromise = null;
304 }
305
306 /**
Tim van der Lippe97611c92020-02-12 16:56:58307 * @param {!SDK.DOMModel.DOMNode} selectedNode
Blink Reformat4c46d092018-04-07 15:32:37308 * @return {!Promise.<!Array.<string>>}
309 */
Mathias Bynens3abc0952020-04-20 14:15:52310 async _getClassNames(selectedNode) {
Blink Reformat4c46d092018-04-07 15:32:37311 const promises = [];
312 const completions = new Set();
313 this._selectedFrameId = selectedNode.frameId();
314
315 const cssModel = selectedNode.domModel().cssModel();
316 const allStyleSheets = cssModel.allStyleSheets();
317 for (const stylesheet of allStyleSheets) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34318 if (stylesheet.frameId !== this._selectedFrameId) {
Blink Reformat4c46d092018-04-07 15:32:37319 continue;
Tim van der Lippe1d6e57a2019-09-30 11:55:34320 }
Mathias Bynens3abc0952020-04-20 14:15:52321 const cssPromise = cssModel.classNamesPromise(stylesheet.id).then(classes => {
322 for (const className of classes) {
323 completions.add(className);
324 }
325 });
Blink Reformat4c46d092018-04-07 15:32:37326 promises.push(cssPromise);
327 }
328
Tim van der Lippe32f760f2020-10-01 10:52:15329 const ownerDocumentId = /** @type {number} */ (
330 /** @type {!SDK.DOMModel.DOMDocument} */ (selectedNode.ownerDocument).id);
331
332 const domPromise = selectedNode.domModel().classNamesPromise(ownerDocumentId).then(classes => {
Mathias Bynens3abc0952020-04-20 14:15:52333 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 */
Mathias Bynens41ea2632020-12-24 05:52:49348 async _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 Lipped1a00aa2020-08-19 16:03:56353 const selectedNode = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34354 if (!selectedNode || (!prefix && !force && !expression.trim())) {
Mathias Bynens41ea2632020-12-24 05:52:49355 return [];
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
Mathias Bynens41ea2632020-12-24 05:52:49362 let completions = await this._classNamesPromise;
363 const classesMap = this._nodeClasses(/** @type {!SDK.DOMModel.DOMNode} */ (selectedNode));
364 completions = completions.filter(value => !classesMap.get(value));
Blink Reformat4c46d092018-04-07 15:32:37365
Mathias Bynens41ea2632020-12-24 05:52:49366 if (prefix[0] === '.') {
367 completions = completions.map(value => '.' + value);
368 }
369 return completions.filter(value => value.startsWith(prefix)).sort().map(completion => {
370 return {
371 text: completion,
372 title: undefined,
373 subtitle: undefined,
374 iconType: undefined,
375 priority: undefined,
376 isSecondary: undefined,
377 subtitleRenderer: undefined,
378 selectionRange: undefined,
379 hideGhostText: undefined,
380 iconElement: undefined,
381 };
Blink Reformat4c46d092018-04-07 15:32:37382 });
383 }
Tim van der Lippe13f71fb2019-11-29 11:17:39384}