blob: ff33a6d13fa3c8d6b3493f0c6ba9f3f888b04a7e [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 */
7Elements.ClassesPaneWidget = class extends UI.Widget {
8 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');
17 this._prompt = new Elements.ClassesPaneWidget.ClassNamePrompt(this._nodeClasses.bind(this));
18 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) {
51 if (!isEnterKey(event) && !isEscKey(event))
52 return;
53
54 if (isEnterKey(event)) {
55 event.consume();
56 if (this._prompt.acceptAutoComplete())
57 return;
58 }
59
60 let text = event.target.textContent;
61 if (isEscKey(event)) {
62 if (!text.isWhitespace())
63 event.consume(true);
64 text = '';
65 }
66
67 this._prompt.clearAutocomplete();
68 event.target.textContent = '';
69
70 const node = UI.context.flavor(SDK.DOMNode);
71 if (!node)
72 return;
73
74 const classNames = this._splitTextIntoClasses(text);
75 for (const className of classNames)
76 this._toggleClass(node, className, true);
77 this._installNodeClasses(node);
78 this._update();
79 }
80
81 _onTextChanged() {
82 const node = UI.context.flavor(SDK.DOMNode);
83 if (!node)
84 return;
85 this._installNodeClasses(node);
86 }
87
88 /**
89 * @param {!Common.Event} event
90 */
91 _onDOMMutated(event) {
92 const node = /** @type {!SDK.DOMNode} */ (event.data);
93 if (this._mutatingNodes.has(node))
94 return;
95 delete node[Elements.ClassesPaneWidget._classesSymbol];
96 this._update();
97 }
98
99 /**
100 * @param {!Common.Event} event
101 */
102 _onSelectedNodeChanged(event) {
103 if (this._previousTarget && this._prompt.text()) {
104 this._input.textContent = '';
105 this._installNodeClasses(this._previousTarget);
106 }
107 this._previousTarget = /** @type {?SDK.DOMNode} */ (event.data);
108 this._update();
109 }
110
111 /**
112 * @override
113 */
114 wasShown() {
115 this._update();
116 }
117
118 _update() {
119 if (!this.isShowing())
120 return;
121
122 let node = UI.context.flavor(SDK.DOMNode);
123 if (node)
124 node = node.enclosingElementOrSelf();
125
126 this._classesContainer.removeChildren();
127 this._input.disabled = !node;
128
129 if (!node)
130 return;
131
132 const classes = this._nodeClasses(node);
133 const keys = classes.keysArray();
134 keys.sort(String.caseInsensetiveComparator);
135 for (let i = 0; i < keys.length; ++i) {
136 const className = keys[i];
137 const label = UI.CheckboxLabel.create(className, classes.get(className));
138 label.classList.add('monospace');
139 label.checkboxElement.addEventListener('click', this._onClick.bind(this, className), false);
140 this._classesContainer.appendChild(label);
141 }
142 }
143
144 /**
145 * @param {string} className
146 * @param {!Event} event
147 */
148 _onClick(className, event) {
149 const node = UI.context.flavor(SDK.DOMNode);
150 if (!node)
151 return;
152 const enabled = event.target.checked;
153 this._toggleClass(node, className, enabled);
154 this._installNodeClasses(node);
155 }
156
157 /**
158 * @param {!SDK.DOMNode} node
159 * @return {!Map<string, boolean>}
160 */
161 _nodeClasses(node) {
162 let result = node[Elements.ClassesPaneWidget._classesSymbol];
163 if (!result) {
164 const classAttribute = node.getAttribute('class') || '';
165 const classes = classAttribute.split(/\s/);
166 result = new Map();
167 for (let i = 0; i < classes.length; ++i) {
168 const className = classes[i].trim();
169 if (!className.length)
170 continue;
171 result.set(className, true);
172 }
173 node[Elements.ClassesPaneWidget._classesSymbol] = result;
174 }
175 return result;
176 }
177
178 /**
179 * @param {!SDK.DOMNode} node
180 * @param {string} className
181 * @param {boolean} enabled
182 */
183 _toggleClass(node, className, enabled) {
184 const classes = this._nodeClasses(node);
185 classes.set(className, enabled);
186 }
187
188 /**
189 * @param {!SDK.DOMNode} node
190 */
191 _installNodeClasses(node) {
192 const classes = this._nodeClasses(node);
193 const activeClasses = new Set();
194 for (const className of classes.keys()) {
195 if (classes.get(className))
196 activeClasses.add(className);
197 }
198
199 const additionalClasses = this._splitTextIntoClasses(this._prompt.textWithCurrentSuggestion());
200 for (const className of additionalClasses)
201 activeClasses.add(className);
202
203 const newClasses = activeClasses.valuesArray();
204 newClasses.sort();
205
206 this._pendingNodeClasses.set(node, newClasses.join(' '));
207 this._updateNodeThrottler.schedule(this._flushPendingClasses.bind(this));
208 }
209
210 /**
211 * @return {!Promise}
212 */
213 _flushPendingClasses() {
214 const promises = [];
215 for (const node of this._pendingNodeClasses.keys()) {
216 this._mutatingNodes.add(node);
217 const promise = node.setAttributeValuePromise('class', this._pendingNodeClasses.get(node))
218 .then(onClassValueUpdated.bind(this, node));
219 promises.push(promise);
220 }
221 this._pendingNodeClasses.clear();
222 return Promise.all(promises);
223
224 /**
225 * @param {!SDK.DOMNode} node
226 * @this {Elements.ClassesPaneWidget}
227 */
228 function onClassValueUpdated(node) {
229 this._mutatingNodes.delete(node);
230 }
231 }
232};
233
234Elements.ClassesPaneWidget._classesSymbol = Symbol('Elements.ClassesPaneWidget._classesSymbol');
235
236/**
237 * @implements {UI.ToolbarItem.Provider}
238 * @unrestricted
239 */
240Elements.ClassesPaneWidget.ButtonProvider = class {
241 constructor() {
242 this._button = new UI.ToolbarToggle(Common.UIString('Element Classes'), '');
243 this._button.setText('.cls');
244 this._button.element.classList.add('monospace');
245 this._button.addEventListener(UI.ToolbarButton.Events.Click, this._clicked, this);
246 this._view = new Elements.ClassesPaneWidget();
247 }
248
249 _clicked() {
250 Elements.ElementsPanel.instance().showToolbarPane(!this._view.isShowing() ? this._view : null, this._button);
251 }
252
253 /**
254 * @override
255 * @return {!UI.ToolbarItem}
256 */
257 item() {
258 return this._button;
259 }
260};
261
262/**
263 * @unrestricted
264 */
265Elements.ClassesPaneWidget.ClassNamePrompt = class extends UI.TextPrompt {
266 /**
267 * @param {function(!SDK.DOMNode):!Map<string, boolean>} nodeClasses
268 */
269 constructor(nodeClasses) {
270 super();
271 this._nodeClasses = nodeClasses;
272 this.initialize(this._buildClassNameCompletions.bind(this), ' ');
273 this.disableDefaultSuggestionForEmptyInput();
274 this._selectedFrameId = '';
275 this._classNamesPromise = null;
276 }
277
278 /**
279 * @param {!SDK.DOMNode} selectedNode
280 * @return {!Promise.<!Array.<string>>}
281 */
282 _getClassNames(selectedNode) {
283 const promises = [];
284 const completions = new Set();
285 this._selectedFrameId = selectedNode.frameId();
286
287 const cssModel = selectedNode.domModel().cssModel();
288 const allStyleSheets = cssModel.allStyleSheets();
289 for (const stylesheet of allStyleSheets) {
290 if (stylesheet.frameId !== this._selectedFrameId)
291 continue;
292 const cssPromise = cssModel.classNamesPromise(stylesheet.id).then(classes => completions.addAll(classes));
293 promises.push(cssPromise);
294 }
295
296 const domPromise = selectedNode.domModel()
297 .classNamesPromise(selectedNode.ownerDocument.id)
298 .then(classes => completions.addAll(classes));
299 promises.push(domPromise);
300 return Promise.all(promises).then(() => completions.valuesArray());
301 }
302
303 /**
304 * @param {string} expression
305 * @param {string} prefix
306 * @param {boolean=} force
307 * @return {!Promise<!UI.SuggestBox.Suggestions>}
308 */
309 _buildClassNameCompletions(expression, prefix, force) {
310 if (!prefix || force)
311 this._classNamesPromise = null;
312
313 const selectedNode = UI.context.flavor(SDK.DOMNode);
314 if (!selectedNode || (!prefix && !force && !expression.trim()))
315 return Promise.resolve([]);
316
317 if (!this._classNamesPromise || this._selectedFrameId !== selectedNode.frameId())
318 this._classNamesPromise = this._getClassNames(selectedNode);
319
320 return this._classNamesPromise.then(completions => {
321 const classesMap = this._nodeClasses(/** @type {!SDK.DOMNode} */ (selectedNode));
322 completions = completions.filter(value => !classesMap.get(value));
323
324 if (prefix[0] === '.')
325 completions = completions.map(value => '.' + value);
326 return completions.filter(value => value.startsWith(prefix)).sort().map(completion => ({text: completion}));
327 });
328 }
329};