blob: 1be8862409f5022f41b73d0da3b6a8902987f7aa [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.
4/**
5 * @unrestricted
6 */
Tim van der Lippe13f71fb2019-11-29 11:17:397export default class ClassesPaneWidget extends UI.Widget {
Blink Reformat4c46d092018-04-07 15:32:378 constructor() {
9 super(true);
10 this.registerRequiredCSS('elements/classesPaneWidget.css');
11 this.contentElement.className = 'styles-element-classes-pane';
12 const container = this.contentElement.createChild('div', 'title-container');
13 this._input = container.createChild('div', 'new-class-input monospace');
14 this.setDefaultFocusedElement(this._input);
15 this._classesContainer = this.contentElement.createChild('div', 'source-code');
16 this._classesContainer.classList.add('styles-element-classes-container');
Tim van der Lippe13f71fb2019-11-29 11:17:3917 this._prompt = new ClassNamePrompt(this._nodeClasses.bind(this));
Blink Reformat4c46d092018-04-07 15:32:3718 this._prompt.setAutocompletionTimeout(0);
19 this._prompt.renderAsBlock();
20
21 const proxyElement = this._prompt.attach(this._input);
22 this._prompt.setPlaceholder(Common.UIString('Add new class'));
23 this._prompt.addEventListener(UI.TextPrompt.Events.TextChanged, this._onTextChanged, this);
24 proxyElement.addEventListener('keydown', this._onKeyDown.bind(this), false);
25
26 SDK.targetManager.addModelListener(SDK.DOMModel, SDK.DOMModel.Events.DOMMutated, this._onDOMMutated, this);
27 /** @type {!Set<!SDK.DOMNode>} */
28 this._mutatingNodes = new Set();
29 /** @type {!Map<!SDK.DOMNode, string>} */
30 this._pendingNodeClasses = new Map();
31 this._updateNodeThrottler = new Common.Throttler(0);
32 /** @type {?SDK.DOMNode} */
33 this._previousTarget = null;
34 UI.context.addFlavorChangeListener(SDK.DOMNode, this._onSelectedNodeChanged, this);
35 }
36
37 /**
38 * @param {string} text
39 * @return {!Array.<string>}
40 */
41 _splitTextIntoClasses(text) {
42 return text.split(/[.,\s]/)
43 .map(className => className.trim())
44 .filter(className => className.length);
45 }
46
47 /**
48 * @param {!Event} event
49 */
50 _onKeyDown(event) {
Tim van der Lippe1d6e57a2019-09-30 11:55:3451 if (!isEnterKey(event) && !isEscKey(event)) {
Blink Reformat4c46d092018-04-07 15:32:3752 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3453 }
Blink Reformat4c46d092018-04-07 15:32:3754
55 if (isEnterKey(event)) {
56 event.consume();
Tim van der Lippe1d6e57a2019-09-30 11:55:3457 if (this._prompt.acceptAutoComplete()) {
Blink Reformat4c46d092018-04-07 15:32:3758 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3459 }
Blink Reformat4c46d092018-04-07 15:32:3760 }
61
62 let text = event.target.textContent;
63 if (isEscKey(event)) {
Tim van der Lippe1d6e57a2019-09-30 11:55:3464 if (!text.isWhitespace()) {
Blink Reformat4c46d092018-04-07 15:32:3765 event.consume(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:3466 }
Blink Reformat4c46d092018-04-07 15:32:3767 text = '';
68 }
69
70 this._prompt.clearAutocomplete();
71 event.target.textContent = '';
72
73 const node = UI.context.flavor(SDK.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:3474 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:3775 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3476 }
Blink Reformat4c46d092018-04-07 15:32:3777
78 const classNames = this._splitTextIntoClasses(text);
Tim van der Lippe1d6e57a2019-09-30 11:55:3479 for (const className of classNames) {
Blink Reformat4c46d092018-04-07 15:32:3780 this._toggleClass(node, className, true);
Tim van der Lippe1d6e57a2019-09-30 11:55:3481 }
Blink Reformat4c46d092018-04-07 15:32:3782 this._installNodeClasses(node);
83 this._update();
84 }
85
86 _onTextChanged() {
87 const node = UI.context.flavor(SDK.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:3488 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:3789 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3490 }
Blink Reformat4c46d092018-04-07 15:32:3791 this._installNodeClasses(node);
92 }
93
94 /**
95 * @param {!Common.Event} event
96 */
97 _onDOMMutated(event) {
98 const node = /** @type {!SDK.DOMNode} */ (event.data);
Tim van der Lippe1d6e57a2019-09-30 11:55:3499 if (this._mutatingNodes.has(node)) {
Blink Reformat4c46d092018-04-07 15:32:37100 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34101 }
Tim van der Lippe13f71fb2019-11-29 11:17:39102 delete node[ClassesPaneWidget._classesSymbol];
Blink Reformat4c46d092018-04-07 15:32:37103 this._update();
104 }
105
106 /**
107 * @param {!Common.Event} event
108 */
109 _onSelectedNodeChanged(event) {
110 if (this._previousTarget && this._prompt.text()) {
111 this._input.textContent = '';
112 this._installNodeClasses(this._previousTarget);
113 }
114 this._previousTarget = /** @type {?SDK.DOMNode} */ (event.data);
115 this._update();
116 }
117
118 /**
119 * @override
120 */
121 wasShown() {
122 this._update();
123 }
124
125 _update() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34126 if (!this.isShowing()) {
Blink Reformat4c46d092018-04-07 15:32:37127 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34128 }
Blink Reformat4c46d092018-04-07 15:32:37129
130 let node = UI.context.flavor(SDK.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34131 if (node) {
Blink Reformat4c46d092018-04-07 15:32:37132 node = node.enclosingElementOrSelf();
Tim van der Lippe1d6e57a2019-09-30 11:55:34133 }
Blink Reformat4c46d092018-04-07 15:32:37134
135 this._classesContainer.removeChildren();
136 this._input.disabled = !node;
137
Tim van der Lippe1d6e57a2019-09-30 11:55:34138 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:37139 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34140 }
Blink Reformat4c46d092018-04-07 15:32:37141
142 const classes = this._nodeClasses(node);
143 const keys = classes.keysArray();
144 keys.sort(String.caseInsensetiveComparator);
145 for (let i = 0; i < keys.length; ++i) {
146 const className = keys[i];
147 const label = UI.CheckboxLabel.create(className, classes.get(className));
148 label.classList.add('monospace');
149 label.checkboxElement.addEventListener('click', this._onClick.bind(this, className), false);
150 this._classesContainer.appendChild(label);
151 }
152 }
153
154 /**
155 * @param {string} className
156 * @param {!Event} event
157 */
158 _onClick(className, event) {
159 const node = UI.context.flavor(SDK.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34160 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:37161 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34162 }
Blink Reformat4c46d092018-04-07 15:32:37163 const enabled = event.target.checked;
164 this._toggleClass(node, className, enabled);
165 this._installNodeClasses(node);
166 }
167
168 /**
169 * @param {!SDK.DOMNode} node
170 * @return {!Map<string, boolean>}
171 */
172 _nodeClasses(node) {
Tim van der Lippe13f71fb2019-11-29 11:17:39173 let result = node[ClassesPaneWidget._classesSymbol];
Blink Reformat4c46d092018-04-07 15:32:37174 if (!result) {
175 const classAttribute = node.getAttribute('class') || '';
176 const classes = classAttribute.split(/\s/);
177 result = new Map();
178 for (let i = 0; i < classes.length; ++i) {
179 const className = classes[i].trim();
Tim van der Lippe1d6e57a2019-09-30 11:55:34180 if (!className.length) {
Blink Reformat4c46d092018-04-07 15:32:37181 continue;
Tim van der Lippe1d6e57a2019-09-30 11:55:34182 }
Blink Reformat4c46d092018-04-07 15:32:37183 result.set(className, true);
184 }
Tim van der Lippe13f71fb2019-11-29 11:17:39185 node[ClassesPaneWidget._classesSymbol] = result;
Blink Reformat4c46d092018-04-07 15:32:37186 }
187 return result;
188 }
189
190 /**
191 * @param {!SDK.DOMNode} node
192 * @param {string} className
193 * @param {boolean} enabled
194 */
195 _toggleClass(node, className, enabled) {
196 const classes = this._nodeClasses(node);
197 classes.set(className, enabled);
198 }
199
200 /**
201 * @param {!SDK.DOMNode} node
202 */
203 _installNodeClasses(node) {
204 const classes = this._nodeClasses(node);
205 const activeClasses = new Set();
206 for (const className of classes.keys()) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34207 if (classes.get(className)) {
Blink Reformat4c46d092018-04-07 15:32:37208 activeClasses.add(className);
Tim van der Lippe1d6e57a2019-09-30 11:55:34209 }
Blink Reformat4c46d092018-04-07 15:32:37210 }
211
212 const additionalClasses = this._splitTextIntoClasses(this._prompt.textWithCurrentSuggestion());
Tim van der Lippe1d6e57a2019-09-30 11:55:34213 for (const className of additionalClasses) {
Blink Reformat4c46d092018-04-07 15:32:37214 activeClasses.add(className);
Tim van der Lippe1d6e57a2019-09-30 11:55:34215 }
Blink Reformat4c46d092018-04-07 15:32:37216
217 const newClasses = activeClasses.valuesArray();
218 newClasses.sort();
219
220 this._pendingNodeClasses.set(node, newClasses.join(' '));
221 this._updateNodeThrottler.schedule(this._flushPendingClasses.bind(this));
222 }
223
224 /**
225 * @return {!Promise}
226 */
227 _flushPendingClasses() {
228 const promises = [];
229 for (const node of this._pendingNodeClasses.keys()) {
230 this._mutatingNodes.add(node);
231 const promise = node.setAttributeValuePromise('class', this._pendingNodeClasses.get(node))
232 .then(onClassValueUpdated.bind(this, node));
233 promises.push(promise);
234 }
235 this._pendingNodeClasses.clear();
236 return Promise.all(promises);
237
238 /**
239 * @param {!SDK.DOMNode} node
Tim van der Lippe13f71fb2019-11-29 11:17:39240 * @this {ClassesPaneWidget}
Blink Reformat4c46d092018-04-07 15:32:37241 */
242 function onClassValueUpdated(node) {
243 this._mutatingNodes.delete(node);
244 }
245 }
Tim van der Lippe13f71fb2019-11-29 11:17:39246}
Blink Reformat4c46d092018-04-07 15:32:37247
Tim van der Lippe13f71fb2019-11-29 11:17:39248ClassesPaneWidget._classesSymbol = Symbol('ClassesPaneWidget._classesSymbol');
Blink Reformat4c46d092018-04-07 15:32:37249
250/**
251 * @implements {UI.ToolbarItem.Provider}
252 * @unrestricted
253 */
Tim van der Lippe13f71fb2019-11-29 11:17:39254export class ButtonProvider {
Blink Reformat4c46d092018-04-07 15:32:37255 constructor() {
256 this._button = new UI.ToolbarToggle(Common.UIString('Element Classes'), '');
257 this._button.setText('.cls');
258 this._button.element.classList.add('monospace');
259 this._button.addEventListener(UI.ToolbarButton.Events.Click, this._clicked, this);
Tim van der Lippe13f71fb2019-11-29 11:17:39260 this._view = new ClassesPaneWidget();
Blink Reformat4c46d092018-04-07 15:32:37261 }
262
263 _clicked() {
264 Elements.ElementsPanel.instance().showToolbarPane(!this._view.isShowing() ? this._view : null, this._button);
265 }
266
267 /**
268 * @override
269 * @return {!UI.ToolbarItem}
270 */
271 item() {
272 return this._button;
273 }
Tim van der Lippe13f71fb2019-11-29 11:17:39274}
Blink Reformat4c46d092018-04-07 15:32:37275
276/**
277 * @unrestricted
278 */
Tim van der Lippe13f71fb2019-11-29 11:17:39279export class ClassNamePrompt extends UI.TextPrompt {
Blink Reformat4c46d092018-04-07 15:32:37280 /**
281 * @param {function(!SDK.DOMNode):!Map<string, boolean>} nodeClasses
282 */
283 constructor(nodeClasses) {
284 super();
285 this._nodeClasses = nodeClasses;
286 this.initialize(this._buildClassNameCompletions.bind(this), ' ');
287 this.disableDefaultSuggestionForEmptyInput();
288 this._selectedFrameId = '';
289 this._classNamesPromise = null;
290 }
291
292 /**
293 * @param {!SDK.DOMNode} selectedNode
294 * @return {!Promise.<!Array.<string>>}
295 */
296 _getClassNames(selectedNode) {
297 const promises = [];
298 const completions = new Set();
299 this._selectedFrameId = selectedNode.frameId();
300
301 const cssModel = selectedNode.domModel().cssModel();
302 const allStyleSheets = cssModel.allStyleSheets();
303 for (const stylesheet of allStyleSheets) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34304 if (stylesheet.frameId !== this._selectedFrameId) {
Blink Reformat4c46d092018-04-07 15:32:37305 continue;
Tim van der Lippe1d6e57a2019-09-30 11:55:34306 }
Blink Reformat4c46d092018-04-07 15:32:37307 const cssPromise = cssModel.classNamesPromise(stylesheet.id).then(classes => completions.addAll(classes));
308 promises.push(cssPromise);
309 }
310
311 const domPromise = selectedNode.domModel()
312 .classNamesPromise(selectedNode.ownerDocument.id)
313 .then(classes => completions.addAll(classes));
314 promises.push(domPromise);
315 return Promise.all(promises).then(() => completions.valuesArray());
316 }
317
318 /**
319 * @param {string} expression
320 * @param {string} prefix
321 * @param {boolean=} force
322 * @return {!Promise<!UI.SuggestBox.Suggestions>}
323 */
324 _buildClassNameCompletions(expression, prefix, force) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34325 if (!prefix || force) {
Blink Reformat4c46d092018-04-07 15:32:37326 this._classNamesPromise = null;
Tim van der Lippe1d6e57a2019-09-30 11:55:34327 }
Blink Reformat4c46d092018-04-07 15:32:37328
329 const selectedNode = UI.context.flavor(SDK.DOMNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34330 if (!selectedNode || (!prefix && !force && !expression.trim())) {
Blink Reformat4c46d092018-04-07 15:32:37331 return Promise.resolve([]);
Tim van der Lippe1d6e57a2019-09-30 11:55:34332 }
Blink Reformat4c46d092018-04-07 15:32:37333
Tim van der Lippe1d6e57a2019-09-30 11:55:34334 if (!this._classNamesPromise || this._selectedFrameId !== selectedNode.frameId()) {
Blink Reformat4c46d092018-04-07 15:32:37335 this._classNamesPromise = this._getClassNames(selectedNode);
Tim van der Lippe1d6e57a2019-09-30 11:55:34336 }
Blink Reformat4c46d092018-04-07 15:32:37337
338 return this._classNamesPromise.then(completions => {
339 const classesMap = this._nodeClasses(/** @type {!SDK.DOMNode} */ (selectedNode));
340 completions = completions.filter(value => !classesMap.get(value));
341
Tim van der Lippe1d6e57a2019-09-30 11:55:34342 if (prefix[0] === '.') {
Blink Reformat4c46d092018-04-07 15:32:37343 completions = completions.map(value => '.' + value);
Tim van der Lippe1d6e57a2019-09-30 11:55:34344 }
Blink Reformat4c46d092018-04-07 15:32:37345 return completions.filter(value => value.startsWith(prefix)).sort().map(completion => ({text: completion}));
346 });
347 }
Tim van der Lippe13f71fb2019-11-29 11:17:39348}
349
350/* Legacy exported object */
351self.Elements = self.Elements || {};
352
353/* Legacy exported object */
354Elements = Elements || {};
355
356/** @constructor */
357Elements.ClassesPaneWidget = ClassesPaneWidget;
358
359/** @constructor */
360Elements.ClassesPaneWidget.ButtonProvider = ButtonProvider;
361
362/** @constructor */
363Elements.ClassesPaneWidget.ClassNamePrompt = ClassNamePrompt;