blob: 078030041e8b56d8f5497d781a618e03d404953e [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) {
51 return text.split(/[.,\s]/)
52 .map(className => className.trim())
53 .filter(className => className.length);
54 }
55
56 /**
57 * @param {!Event} event
58 */
59 _onKeyDown(event) {
Tim van der Lippe1d6e57a2019-09-30 11:55:3460 if (!isEnterKey(event) && !isEscKey(event)) {
Blink Reformat4c46d092018-04-07 15:32:3761 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3462 }
Blink Reformat4c46d092018-04-07 15:32:3763
64 if (isEnterKey(event)) {
65 event.consume();
Tim van der Lippe1d6e57a2019-09-30 11:55:3466 if (this._prompt.acceptAutoComplete()) {
Blink Reformat4c46d092018-04-07 15:32:3767 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3468 }
Blink Reformat4c46d092018-04-07 15:32:3769 }
70
71 let text = event.target.textContent;
72 if (isEscKey(event)) {
Simon Zündda7058f2020-02-28 13:57:2873 if (!Platform.StringUtilities.isWhitespace(text)) {
Blink Reformat4c46d092018-04-07 15:32:3774 event.consume(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:3475 }
Blink Reformat4c46d092018-04-07 15:32:3776 text = '';
77 }
78
79 this._prompt.clearAutocomplete();
80 event.target.textContent = '';
81
Tim van der Lippe97611c92020-02-12 16:56:5882 const node = self.UI.context.flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:3483 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:3784 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3485 }
Blink Reformat4c46d092018-04-07 15:32:3786
87 const classNames = this._splitTextIntoClasses(text);
Tim van der Lippe1d6e57a2019-09-30 11:55:3488 for (const className of classNames) {
Blink Reformat4c46d092018-04-07 15:32:3789 this._toggleClass(node, className, true);
Tim van der Lippe1d6e57a2019-09-30 11:55:3490 }
Blink Reformat4c46d092018-04-07 15:32:3791 this._installNodeClasses(node);
92 this._update();
93 }
94
95 _onTextChanged() {
Tim van der Lippe97611c92020-02-12 16:56:5896 const node = self.UI.context.flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:3497 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:3798 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3499 }
Blink Reformat4c46d092018-04-07 15:32:37100 this._installNodeClasses(node);
101 }
102
103 /**
Tim van der Lippec02a97c2020-02-14 14:39:27104 * @param {!Common.EventTarget.EventTargetEvent} event
Blink Reformat4c46d092018-04-07 15:32:37105 */
106 _onDOMMutated(event) {
Tim van der Lippe97611c92020-02-12 16:56:58107 const node = /** @type {!SDK.DOMModel.DOMNode} */ (event.data);
Tim van der Lippe1d6e57a2019-09-30 11:55:34108 if (this._mutatingNodes.has(node)) {
Blink Reformat4c46d092018-04-07 15:32:37109 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34110 }
Tim van der Lippe13f71fb2019-11-29 11:17:39111 delete node[ClassesPaneWidget._classesSymbol];
Blink Reformat4c46d092018-04-07 15:32:37112 this._update();
113 }
114
115 /**
Tim van der Lippec02a97c2020-02-14 14:39:27116 * @param {!Common.EventTarget.EventTargetEvent} event
Blink Reformat4c46d092018-04-07 15:32:37117 */
118 _onSelectedNodeChanged(event) {
119 if (this._previousTarget && this._prompt.text()) {
120 this._input.textContent = '';
121 this._installNodeClasses(this._previousTarget);
122 }
Tim van der Lippe97611c92020-02-12 16:56:58123 this._previousTarget = /** @type {?SDK.DOMModel.DOMNode} */ (event.data);
Blink Reformat4c46d092018-04-07 15:32:37124 this._update();
125 }
126
127 /**
128 * @override
129 */
130 wasShown() {
131 this._update();
132 }
133
134 _update() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34135 if (!this.isShowing()) {
Blink Reformat4c46d092018-04-07 15:32:37136 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34137 }
Blink Reformat4c46d092018-04-07 15:32:37138
Tim van der Lippe97611c92020-02-12 16:56:58139 let node = self.UI.context.flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34140 if (node) {
Blink Reformat4c46d092018-04-07 15:32:37141 node = node.enclosingElementOrSelf();
Tim van der Lippe1d6e57a2019-09-30 11:55:34142 }
Blink Reformat4c46d092018-04-07 15:32:37143
144 this._classesContainer.removeChildren();
145 this._input.disabled = !node;
146
Tim van der Lippe1d6e57a2019-09-30 11:55:34147 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:37148 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34149 }
Blink Reformat4c46d092018-04-07 15:32:37150
151 const classes = this._nodeClasses(node);
Simon Zündf27be3d2020-02-11 14:46:27152 const keys = [...classes.keys()];
Blink Reformat4c46d092018-04-07 15:32:37153 keys.sort(String.caseInsensetiveComparator);
154 for (let i = 0; i < keys.length; ++i) {
155 const className = keys[i];
Tim van der Lippe97611c92020-02-12 16:56:58156 const label = UI.UIUtils.CheckboxLabel.create(className, classes.get(className));
Blink Reformat4c46d092018-04-07 15:32:37157 label.classList.add('monospace');
158 label.checkboxElement.addEventListener('click', this._onClick.bind(this, className), false);
159 this._classesContainer.appendChild(label);
160 }
161 }
162
163 /**
164 * @param {string} className
165 * @param {!Event} event
166 */
167 _onClick(className, event) {
Tim van der Lippe97611c92020-02-12 16:56:58168 const node = self.UI.context.flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34169 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:37170 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34171 }
Blink Reformat4c46d092018-04-07 15:32:37172 const enabled = event.target.checked;
173 this._toggleClass(node, className, enabled);
174 this._installNodeClasses(node);
175 }
176
177 /**
Tim van der Lippe97611c92020-02-12 16:56:58178 * @param {!SDK.DOMModel.DOMNode} node
Blink Reformat4c46d092018-04-07 15:32:37179 * @return {!Map<string, boolean>}
180 */
181 _nodeClasses(node) {
Tim van der Lippe13f71fb2019-11-29 11:17:39182 let result = node[ClassesPaneWidget._classesSymbol];
Blink Reformat4c46d092018-04-07 15:32:37183 if (!result) {
184 const classAttribute = node.getAttribute('class') || '';
185 const classes = classAttribute.split(/\s/);
186 result = new Map();
187 for (let i = 0; i < classes.length; ++i) {
188 const className = classes[i].trim();
Tim van der Lippe1d6e57a2019-09-30 11:55:34189 if (!className.length) {
Blink Reformat4c46d092018-04-07 15:32:37190 continue;
Tim van der Lippe1d6e57a2019-09-30 11:55:34191 }
Blink Reformat4c46d092018-04-07 15:32:37192 result.set(className, true);
193 }
Tim van der Lippe13f71fb2019-11-29 11:17:39194 node[ClassesPaneWidget._classesSymbol] = result;
Blink Reformat4c46d092018-04-07 15:32:37195 }
196 return result;
197 }
198
199 /**
Tim van der Lippe97611c92020-02-12 16:56:58200 * @param {!SDK.DOMModel.DOMNode} node
Blink Reformat4c46d092018-04-07 15:32:37201 * @param {string} className
202 * @param {boolean} enabled
203 */
204 _toggleClass(node, className, enabled) {
205 const classes = this._nodeClasses(node);
206 classes.set(className, enabled);
207 }
208
209 /**
Tim van der Lippe97611c92020-02-12 16:56:58210 * @param {!SDK.DOMModel.DOMNode} node
Blink Reformat4c46d092018-04-07 15:32:37211 */
212 _installNodeClasses(node) {
213 const classes = this._nodeClasses(node);
214 const activeClasses = new Set();
215 for (const className of classes.keys()) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34216 if (classes.get(className)) {
Blink Reformat4c46d092018-04-07 15:32:37217 activeClasses.add(className);
Tim van der Lippe1d6e57a2019-09-30 11:55:34218 }
Blink Reformat4c46d092018-04-07 15:32:37219 }
220
221 const additionalClasses = this._splitTextIntoClasses(this._prompt.textWithCurrentSuggestion());
Tim van der Lippe1d6e57a2019-09-30 11:55:34222 for (const className of additionalClasses) {
Blink Reformat4c46d092018-04-07 15:32:37223 activeClasses.add(className);
Tim van der Lippe1d6e57a2019-09-30 11:55:34224 }
Blink Reformat4c46d092018-04-07 15:32:37225
Simon Zünda0d40622020-02-12 13:16:42226 const newClasses = [...activeClasses.values()].sort();
Blink Reformat4c46d092018-04-07 15:32:37227
228 this._pendingNodeClasses.set(node, newClasses.join(' '));
229 this._updateNodeThrottler.schedule(this._flushPendingClasses.bind(this));
230 }
231
232 /**
233 * @return {!Promise}
234 */
235 _flushPendingClasses() {
236 const promises = [];
237 for (const node of this._pendingNodeClasses.keys()) {
238 this._mutatingNodes.add(node);
239 const promise = node.setAttributeValuePromise('class', this._pendingNodeClasses.get(node))
240 .then(onClassValueUpdated.bind(this, node));
241 promises.push(promise);
242 }
243 this._pendingNodeClasses.clear();
244 return Promise.all(promises);
245
246 /**
Tim van der Lippe97611c92020-02-12 16:56:58247 * @param {!SDK.DOMModel.DOMNode} node
Tim van der Lippe13f71fb2019-11-29 11:17:39248 * @this {ClassesPaneWidget}
Blink Reformat4c46d092018-04-07 15:32:37249 */
250 function onClassValueUpdated(node) {
251 this._mutatingNodes.delete(node);
252 }
253 }
Tim van der Lippe13f71fb2019-11-29 11:17:39254}
Blink Reformat4c46d092018-04-07 15:32:37255
Tim van der Lippe13f71fb2019-11-29 11:17:39256ClassesPaneWidget._classesSymbol = Symbol('ClassesPaneWidget._classesSymbol');
Blink Reformat4c46d092018-04-07 15:32:37257
258/**
Tim van der Lippe97611c92020-02-12 16:56:58259 * @implements {UI.Toolbar.Provider}
Blink Reformat4c46d092018-04-07 15:32:37260 * @unrestricted
261 */
Tim van der Lippe13f71fb2019-11-29 11:17:39262export class ButtonProvider {
Blink Reformat4c46d092018-04-07 15:32:37263 constructor() {
Tim van der Lippe97611c92020-02-12 16:56:58264 this._button = new UI.Toolbar.ToolbarToggle(Common.UIString.UIString('Element Classes'), '');
Blink Reformat4c46d092018-04-07 15:32:37265 this._button.setText('.cls');
266 this._button.element.classList.add('monospace');
Tim van der Lippe97611c92020-02-12 16:56:58267 this._button.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._clicked, this);
Tim van der Lippe13f71fb2019-11-29 11:17:39268 this._view = new ClassesPaneWidget();
Blink Reformat4c46d092018-04-07 15:32:37269 }
270
271 _clicked() {
Tim van der Lippeaabc8302019-12-10 15:34:45272 ElementsPanel.instance().showToolbarPane(!this._view.isShowing() ? this._view : null, this._button);
Blink Reformat4c46d092018-04-07 15:32:37273 }
274
275 /**
276 * @override
Tim van der Lippe97611c92020-02-12 16:56:58277 * @return {!UI.Toolbar.ToolbarItem}
Blink Reformat4c46d092018-04-07 15:32:37278 */
279 item() {
280 return this._button;
281 }
Tim van der Lippe13f71fb2019-11-29 11:17:39282}
Blink Reformat4c46d092018-04-07 15:32:37283
284/**
285 * @unrestricted
286 */
Tim van der Lippe97611c92020-02-12 16:56:58287export class ClassNamePrompt extends UI.TextPrompt.TextPrompt {
Blink Reformat4c46d092018-04-07 15:32:37288 /**
Tim van der Lippe97611c92020-02-12 16:56:58289 * @param {function(!SDK.DOMModel.DOMNode):!Map<string, boolean>} nodeClasses
Blink Reformat4c46d092018-04-07 15:32:37290 */
291 constructor(nodeClasses) {
292 super();
293 this._nodeClasses = nodeClasses;
294 this.initialize(this._buildClassNameCompletions.bind(this), ' ');
295 this.disableDefaultSuggestionForEmptyInput();
296 this._selectedFrameId = '';
297 this._classNamesPromise = null;
298 }
299
300 /**
Tim van der Lippe97611c92020-02-12 16:56:58301 * @param {!SDK.DOMModel.DOMNode} selectedNode
Blink Reformat4c46d092018-04-07 15:32:37302 * @return {!Promise.<!Array.<string>>}
303 */
304 _getClassNames(selectedNode) {
305 const promises = [];
306 const completions = new Set();
307 this._selectedFrameId = selectedNode.frameId();
308
309 const cssModel = selectedNode.domModel().cssModel();
310 const allStyleSheets = cssModel.allStyleSheets();
311 for (const stylesheet of allStyleSheets) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34312 if (stylesheet.frameId !== this._selectedFrameId) {
Blink Reformat4c46d092018-04-07 15:32:37313 continue;
Tim van der Lippe1d6e57a2019-09-30 11:55:34314 }
Blink Reformat4c46d092018-04-07 15:32:37315 const cssPromise = cssModel.classNamesPromise(stylesheet.id).then(classes => completions.addAll(classes));
316 promises.push(cssPromise);
317 }
318
319 const domPromise = selectedNode.domModel()
320 .classNamesPromise(selectedNode.ownerDocument.id)
321 .then(classes => completions.addAll(classes));
322 promises.push(domPromise);
Simon Zünda0d40622020-02-12 13:16:42323 return Promise.all(promises).then(() => [...completions]);
Blink Reformat4c46d092018-04-07 15:32:37324 }
325
326 /**
327 * @param {string} expression
328 * @param {string} prefix
329 * @param {boolean=} force
330 * @return {!Promise<!UI.SuggestBox.Suggestions>}
331 */
332 _buildClassNameCompletions(expression, prefix, force) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34333 if (!prefix || force) {
Blink Reformat4c46d092018-04-07 15:32:37334 this._classNamesPromise = null;
Tim van der Lippe1d6e57a2019-09-30 11:55:34335 }
Blink Reformat4c46d092018-04-07 15:32:37336
Tim van der Lippe97611c92020-02-12 16:56:58337 const selectedNode = self.UI.context.flavor(SDK.DOMModel.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34338 if (!selectedNode || (!prefix && !force && !expression.trim())) {
Blink Reformat4c46d092018-04-07 15:32:37339 return Promise.resolve([]);
Tim van der Lippe1d6e57a2019-09-30 11:55:34340 }
Blink Reformat4c46d092018-04-07 15:32:37341
Tim van der Lippe1d6e57a2019-09-30 11:55:34342 if (!this._classNamesPromise || this._selectedFrameId !== selectedNode.frameId()) {
Blink Reformat4c46d092018-04-07 15:32:37343 this._classNamesPromise = this._getClassNames(selectedNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34344 }
Blink Reformat4c46d092018-04-07 15:32:37345
346 return this._classNamesPromise.then(completions => {
Tim van der Lippe97611c92020-02-12 16:56:58347 const classesMap = this._nodeClasses(/** @type {!SDK.DOMModel.DOMNode} */ (selectedNode));
Blink Reformat4c46d092018-04-07 15:32:37348 completions = completions.filter(value => !classesMap.get(value));
349
Tim van der Lippe1d6e57a2019-09-30 11:55:34350 if (prefix[0] === '.') {
Blink Reformat4c46d092018-04-07 15:32:37351 completions = completions.map(value => '.' + value);
Tim van der Lippe1d6e57a2019-09-30 11:55:34352 }
Blink Reformat4c46d092018-04-07 15:32:37353 return completions.filter(value => value.startsWith(prefix)).sort().map(completion => ({text: completion}));
354 });
355 }
Tim van der Lippe13f71fb2019-11-29 11:17:39356}