OLD | NEW |
1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | |
5 /** | 4 /** |
6 * @constructor | 5 * @unrestricted |
7 * @param {!WebInspector.CSSStyleDeclaration} ownerStyle | |
8 * @param {number} index | |
9 * @param {string} name | |
10 * @param {string} value | |
11 * @param {boolean} important | |
12 * @param {boolean} disabled | |
13 * @param {boolean} parsedOk | |
14 * @param {boolean} implicit | |
15 * @param {?string=} text | |
16 * @param {!CSSAgent.SourceRange=} range | |
17 */ | 6 */ |
18 WebInspector.CSSProperty = function(ownerStyle, index, name, value, important, d
isabled, parsedOk, implicit, text, range) | 7 WebInspector.CSSProperty = class { |
19 { | 8 /** |
| 9 * @param {!WebInspector.CSSStyleDeclaration} ownerStyle |
| 10 * @param {number} index |
| 11 * @param {string} name |
| 12 * @param {string} value |
| 13 * @param {boolean} important |
| 14 * @param {boolean} disabled |
| 15 * @param {boolean} parsedOk |
| 16 * @param {boolean} implicit |
| 17 * @param {?string=} text |
| 18 * @param {!CSSAgent.SourceRange=} range |
| 19 */ |
| 20 constructor(ownerStyle, index, name, value, important, disabled, parsedOk, imp
licit, text, range) { |
20 this.ownerStyle = ownerStyle; | 21 this.ownerStyle = ownerStyle; |
21 this.index = index; | 22 this.index = index; |
22 this.name = name; | 23 this.name = name; |
23 this.value = value; | 24 this.value = value; |
24 this.important = important; | 25 this.important = important; |
25 this.disabled = disabled; | 26 this.disabled = disabled; |
26 this.parsedOk = parsedOk; | 27 this.parsedOk = parsedOk; |
27 this.implicit = implicit; // A longhand, implicitly set by missing values of
shorthand. | 28 this.implicit = implicit; // A longhand, implicitly set by missing values o
f shorthand. |
28 this.text = text; | 29 this.text = text; |
29 this.range = range ? WebInspector.TextRange.fromObject(range) : null; | 30 this.range = range ? WebInspector.TextRange.fromObject(range) : null; |
30 this._active = true; | 31 this._active = true; |
31 this._nameRange = null; | 32 this._nameRange = null; |
32 this._valueRange = null; | 33 this._valueRange = null; |
33 }; | 34 } |
34 | 35 |
35 /** | 36 /** |
36 * @param {!WebInspector.CSSStyleDeclaration} ownerStyle | 37 * @param {!WebInspector.CSSStyleDeclaration} ownerStyle |
37 * @param {number} index | 38 * @param {number} index |
38 * @param {!CSSAgent.CSSProperty} payload | 39 * @param {!CSSAgent.CSSProperty} payload |
39 * @return {!WebInspector.CSSProperty} | 40 * @return {!WebInspector.CSSProperty} |
40 */ | 41 */ |
41 WebInspector.CSSProperty.parsePayload = function(ownerStyle, index, payload) | 42 static parsePayload(ownerStyle, index, payload) { |
42 { | |
43 // The following default field values are used in the payload: | 43 // The following default field values are used in the payload: |
44 // important: false | 44 // important: false |
45 // parsedOk: true | 45 // parsedOk: true |
46 // implicit: false | 46 // implicit: false |
47 // disabled: false | 47 // disabled: false |
48 var result = new WebInspector.CSSProperty( | 48 var result = new WebInspector.CSSProperty( |
49 ownerStyle, index, payload.name, payload.value, payload.important || fal
se, payload.disabled || false, ("parsedOk" in payload) ? !!payload.parsedOk : tr
ue, !!payload.implicit, payload.text, payload.range); | 49 ownerStyle, index, payload.name, payload.value, payload.important || fal
se, payload.disabled || false, |
| 50 ('parsedOk' in payload) ? !!payload.parsedOk : true, !!payload.implicit,
payload.text, payload.range); |
50 return result; | 51 return result; |
| 52 } |
| 53 |
| 54 _ensureRanges() { |
| 55 if (this._nameRange && this._valueRange) |
| 56 return; |
| 57 var range = this.range; |
| 58 var text = this.text ? new WebInspector.Text(this.text) : null; |
| 59 if (!range || !text) |
| 60 return; |
| 61 |
| 62 var nameIndex = text.value().indexOf(this.name); |
| 63 var valueIndex = text.value().lastIndexOf(this.value); |
| 64 if (nameIndex === -1 || valueIndex === -1 || nameIndex > valueIndex) |
| 65 return; |
| 66 |
| 67 var nameSourceRange = new WebInspector.SourceRange(nameIndex, this.name.leng
th); |
| 68 var valueSourceRange = new WebInspector.SourceRange(valueIndex, this.value.l
ength); |
| 69 |
| 70 this._nameRange = rebase(text.toTextRange(nameSourceRange), range.startLine,
range.startColumn); |
| 71 this._valueRange = rebase(text.toTextRange(valueSourceRange), range.startLin
e, range.startColumn); |
| 72 |
| 73 /** |
| 74 * @param {!WebInspector.TextRange} oneLineRange |
| 75 * @param {number} lineOffset |
| 76 * @param {number} columnOffset |
| 77 * @return {!WebInspector.TextRange} |
| 78 */ |
| 79 function rebase(oneLineRange, lineOffset, columnOffset) { |
| 80 if (oneLineRange.startLine === 0) { |
| 81 oneLineRange.startColumn += columnOffset; |
| 82 oneLineRange.endColumn += columnOffset; |
| 83 } |
| 84 oneLineRange.startLine += lineOffset; |
| 85 oneLineRange.endLine += lineOffset; |
| 86 return oneLineRange; |
| 87 } |
| 88 } |
| 89 |
| 90 /** |
| 91 * @return {?WebInspector.TextRange} |
| 92 */ |
| 93 nameRange() { |
| 94 this._ensureRanges(); |
| 95 return this._nameRange; |
| 96 } |
| 97 |
| 98 /** |
| 99 * @return {?WebInspector.TextRange} |
| 100 */ |
| 101 valueRange() { |
| 102 this._ensureRanges(); |
| 103 return this._valueRange; |
| 104 } |
| 105 |
| 106 /** |
| 107 * @param {!WebInspector.CSSModel.Edit} edit |
| 108 */ |
| 109 rebase(edit) { |
| 110 if (this.ownerStyle.styleSheetId !== edit.styleSheetId) |
| 111 return; |
| 112 if (this.range) |
| 113 this.range = this.range.rebaseAfterTextEdit(edit.oldRange, edit.newRange); |
| 114 } |
| 115 |
| 116 /** |
| 117 * @param {boolean} active |
| 118 */ |
| 119 _setActive(active) { |
| 120 this._active = active; |
| 121 } |
| 122 |
| 123 get propertyText() { |
| 124 if (this.text !== undefined) |
| 125 return this.text; |
| 126 |
| 127 if (this.name === '') |
| 128 return ''; |
| 129 return this.name + ': ' + this.value + (this.important ? ' !important' : '')
+ ';'; |
| 130 } |
| 131 |
| 132 /** |
| 133 * @return {boolean} |
| 134 */ |
| 135 activeInStyle() { |
| 136 return this._active; |
| 137 } |
| 138 |
| 139 /** |
| 140 * @param {string} propertyText |
| 141 * @param {boolean} majorChange |
| 142 * @param {boolean} overwrite |
| 143 * @return {!Promise.<boolean>} |
| 144 */ |
| 145 setText(propertyText, majorChange, overwrite) { |
| 146 if (!this.ownerStyle) |
| 147 return Promise.reject(new Error('No ownerStyle for property')); |
| 148 |
| 149 if (!this.ownerStyle.styleSheetId) |
| 150 return Promise.reject(new Error('No owner style id')); |
| 151 |
| 152 if (!this.range || !this.ownerStyle.range) |
| 153 return Promise.reject(new Error('Style not editable')); |
| 154 |
| 155 if (majorChange) |
| 156 WebInspector.userMetrics.actionTaken(WebInspector.UserMetrics.Action.Style
RuleEdited); |
| 157 |
| 158 if (overwrite && propertyText === this.propertyText) { |
| 159 if (majorChange) |
| 160 this.ownerStyle.cssModel().domModel().markUndoableState(); |
| 161 return Promise.resolve(true); |
| 162 } |
| 163 |
| 164 var range = this.range.relativeTo(this.ownerStyle.range.startLine, this.owne
rStyle.range.startColumn); |
| 165 var indentation = this.ownerStyle.cssText ? this._detectIndentation(this.own
erStyle.cssText) : |
| 166 WebInspector.moduleSetting('text
EditorIndent').get(); |
| 167 var endIndentation = this.ownerStyle.cssText ? indentation.substring(0, this
.ownerStyle.range.endColumn) : ''; |
| 168 var text = new WebInspector.Text(this.ownerStyle.cssText || ''); |
| 169 var newStyleText = text.replaceRange(range, String.sprintf(';%s;', propertyT
ext)); |
| 170 |
| 171 return self.runtime.extension(WebInspector.TokenizerFactory) |
| 172 .instance() |
| 173 .then(this._formatStyle.bind(this, newStyleText, indentation, endIndenta
tion)) |
| 174 .then(setStyleText.bind(this)); |
| 175 |
| 176 /** |
| 177 * @param {string} styleText |
| 178 * @this {WebInspector.CSSProperty} |
| 179 * @return {!Promise.<boolean>} |
| 180 */ |
| 181 function setStyleText(styleText) { |
| 182 return this.ownerStyle.setText(styleText, majorChange); |
| 183 } |
| 184 } |
| 185 |
| 186 /** |
| 187 * @param {string} styleText |
| 188 * @param {string} indentation |
| 189 * @param {string} endIndentation |
| 190 * @param {!WebInspector.TokenizerFactory} tokenizerFactory |
| 191 * @return {string} |
| 192 */ |
| 193 _formatStyle(styleText, indentation, endIndentation, tokenizerFactory) { |
| 194 if (indentation) |
| 195 indentation = '\n' + indentation; |
| 196 var result = ''; |
| 197 var propertyText; |
| 198 var insideProperty = false; |
| 199 var tokenize = tokenizerFactory.createTokenizer('text/css'); |
| 200 |
| 201 tokenize('*{' + styleText + '}', processToken); |
| 202 if (insideProperty) |
| 203 result += propertyText; |
| 204 result = result.substring(2, result.length - 1).trimRight(); |
| 205 return result + (indentation ? '\n' + endIndentation : ''); |
| 206 |
| 207 /** |
| 208 * @param {string} token |
| 209 * @param {?string} tokenType |
| 210 * @param {number} column |
| 211 * @param {number} newColumn |
| 212 */ |
| 213 function processToken(token, tokenType, column, newColumn) { |
| 214 if (!insideProperty) { |
| 215 var disabledProperty = tokenType && tokenType.includes('css-comment') &&
isDisabledProperty(token); |
| 216 var isPropertyStart = tokenType && (tokenType.includes('css-string') ||
tokenType.includes('css-meta') || |
| 217 tokenType.includes('css-property') |
| tokenType.includes('css-variable-2')); |
| 218 if (disabledProperty) { |
| 219 result = result.trimRight() + indentation + token; |
| 220 } else if (isPropertyStart) { |
| 221 insideProperty = true; |
| 222 propertyText = token; |
| 223 } else if (token !== ';') { |
| 224 result += token; |
| 225 } |
| 226 return; |
| 227 } |
| 228 |
| 229 if (token === '}' || token === ';') { |
| 230 result = result.trimRight() + indentation + propertyText.trim() + ';'; |
| 231 insideProperty = false; |
| 232 if (token === '}') |
| 233 result += '}'; |
| 234 } else { |
| 235 propertyText += token; |
| 236 } |
| 237 } |
| 238 |
| 239 /** |
| 240 * @param {string} text |
| 241 * @return {boolean} |
| 242 */ |
| 243 function isDisabledProperty(text) { |
| 244 var colon = text.indexOf(':'); |
| 245 if (colon === -1) |
| 246 return false; |
| 247 var propertyName = text.substring(2, colon).trim(); |
| 248 return WebInspector.cssMetadata().isCSSPropertyName(propertyName); |
| 249 } |
| 250 } |
| 251 |
| 252 /** |
| 253 * @param {string} text |
| 254 * @return {string} |
| 255 */ |
| 256 _detectIndentation(text) { |
| 257 var lines = text.split('\n'); |
| 258 if (lines.length < 2) |
| 259 return ''; |
| 260 return WebInspector.TextUtils.lineIndent(lines[1]); |
| 261 } |
| 262 |
| 263 /** |
| 264 * @param {string} newValue |
| 265 * @param {boolean} majorChange |
| 266 * @param {boolean} overwrite |
| 267 * @param {function(boolean)=} userCallback |
| 268 */ |
| 269 setValue(newValue, majorChange, overwrite, userCallback) { |
| 270 var text = this.name + ': ' + newValue + (this.important ? ' !important' : '
') + ';'; |
| 271 this.setText(text, majorChange, overwrite).then(userCallback); |
| 272 } |
| 273 |
| 274 /** |
| 275 * @param {boolean} disabled |
| 276 * @return {!Promise.<boolean>} |
| 277 */ |
| 278 setDisabled(disabled) { |
| 279 if (!this.ownerStyle) |
| 280 return Promise.resolve(false); |
| 281 if (disabled === this.disabled) |
| 282 return Promise.resolve(true); |
| 283 var propertyText = this.text.trim(); |
| 284 var text = disabled ? '/* ' + propertyText + ' */' : this.text.substring(2,
propertyText.length - 2).trim(); |
| 285 return this.setText(text, true, true); |
| 286 } |
51 }; | 287 }; |
52 | 288 |
53 WebInspector.CSSProperty.prototype = { | 289 |
54 _ensureRanges: function() | |
55 { | |
56 if (this._nameRange && this._valueRange) | |
57 return; | |
58 var range = this.range; | |
59 var text = this.text ? new WebInspector.Text(this.text) : null; | |
60 if (!range || !text) | |
61 return; | |
62 | |
63 var nameIndex = text.value().indexOf(this.name); | |
64 var valueIndex = text.value().lastIndexOf(this.value); | |
65 if (nameIndex === -1 || valueIndex === -1 || nameIndex > valueIndex) | |
66 return; | |
67 | |
68 var nameSourceRange = new WebInspector.SourceRange(nameIndex, this.name.
length); | |
69 var valueSourceRange = new WebInspector.SourceRange(valueIndex, this.val
ue.length); | |
70 | |
71 this._nameRange = rebase(text.toTextRange(nameSourceRange), range.startL
ine, range.startColumn); | |
72 this._valueRange = rebase(text.toTextRange(valueSourceRange), range.star
tLine, range.startColumn); | |
73 | |
74 /** | |
75 * @param {!WebInspector.TextRange} oneLineRange | |
76 * @param {number} lineOffset | |
77 * @param {number} columnOffset | |
78 * @return {!WebInspector.TextRange} | |
79 */ | |
80 function rebase(oneLineRange, lineOffset, columnOffset) | |
81 { | |
82 if (oneLineRange.startLine === 0) { | |
83 oneLineRange.startColumn += columnOffset; | |
84 oneLineRange.endColumn += columnOffset; | |
85 } | |
86 oneLineRange.startLine += lineOffset; | |
87 oneLineRange.endLine += lineOffset; | |
88 return oneLineRange; | |
89 } | |
90 }, | |
91 | |
92 /** | |
93 * @return {?WebInspector.TextRange} | |
94 */ | |
95 nameRange: function() | |
96 { | |
97 this._ensureRanges(); | |
98 return this._nameRange; | |
99 }, | |
100 | |
101 /** | |
102 * @return {?WebInspector.TextRange} | |
103 */ | |
104 valueRange: function() | |
105 { | |
106 this._ensureRanges(); | |
107 return this._valueRange; | |
108 }, | |
109 | |
110 /** | |
111 * @param {!WebInspector.CSSModel.Edit} edit | |
112 */ | |
113 rebase: function(edit) | |
114 { | |
115 if (this.ownerStyle.styleSheetId !== edit.styleSheetId) | |
116 return; | |
117 if (this.range) | |
118 this.range = this.range.rebaseAfterTextEdit(edit.oldRange, edit.newR
ange); | |
119 }, | |
120 | |
121 /** | |
122 * @param {boolean} active | |
123 */ | |
124 _setActive: function(active) | |
125 { | |
126 this._active = active; | |
127 }, | |
128 | |
129 get propertyText() | |
130 { | |
131 if (this.text !== undefined) | |
132 return this.text; | |
133 | |
134 if (this.name === "") | |
135 return ""; | |
136 return this.name + ": " + this.value + (this.important ? " !important" :
"") + ";"; | |
137 }, | |
138 | |
139 /** | |
140 * @return {boolean} | |
141 */ | |
142 activeInStyle: function() | |
143 { | |
144 return this._active; | |
145 }, | |
146 | |
147 /** | |
148 * @param {string} propertyText | |
149 * @param {boolean} majorChange | |
150 * @param {boolean} overwrite | |
151 * @return {!Promise.<boolean>} | |
152 */ | |
153 setText: function(propertyText, majorChange, overwrite) | |
154 { | |
155 if (!this.ownerStyle) | |
156 return Promise.reject(new Error("No ownerStyle for property")); | |
157 | |
158 if (!this.ownerStyle.styleSheetId) | |
159 return Promise.reject(new Error("No owner style id")); | |
160 | |
161 if (!this.range || !this.ownerStyle.range) | |
162 return Promise.reject(new Error("Style not editable")); | |
163 | |
164 if (majorChange) | |
165 WebInspector.userMetrics.actionTaken(WebInspector.UserMetrics.Action
.StyleRuleEdited); | |
166 | |
167 if (overwrite && propertyText === this.propertyText) { | |
168 if (majorChange) | |
169 this.ownerStyle.cssModel().domModel().markUndoableState(); | |
170 return Promise.resolve(true); | |
171 } | |
172 | |
173 var range = this.range.relativeTo(this.ownerStyle.range.startLine, this.
ownerStyle.range.startColumn); | |
174 var indentation = this.ownerStyle.cssText ? this._detectIndentation(this
.ownerStyle.cssText) : WebInspector.moduleSetting("textEditorIndent").get(); | |
175 var endIndentation = this.ownerStyle.cssText ? indentation.substring(0,
this.ownerStyle.range.endColumn) : ""; | |
176 var text = new WebInspector.Text(this.ownerStyle.cssText || ""); | |
177 var newStyleText = text.replaceRange(range, String.sprintf(";%s;", prope
rtyText)); | |
178 | |
179 return self.runtime.extension(WebInspector.TokenizerFactory).instance() | |
180 .then(this._formatStyle.bind(this, newStyleText, indentation, endInd
entation)) | |
181 .then(setStyleText.bind(this)); | |
182 | |
183 /** | |
184 * @param {string} styleText | |
185 * @this {WebInspector.CSSProperty} | |
186 * @return {!Promise.<boolean>} | |
187 */ | |
188 function setStyleText(styleText) | |
189 { | |
190 return this.ownerStyle.setText(styleText, majorChange); | |
191 } | |
192 }, | |
193 | |
194 /** | |
195 * @param {string} styleText | |
196 * @param {string} indentation | |
197 * @param {string} endIndentation | |
198 * @param {!WebInspector.TokenizerFactory} tokenizerFactory | |
199 * @return {string} | |
200 */ | |
201 _formatStyle: function(styleText, indentation, endIndentation, tokenizerFact
ory) | |
202 { | |
203 if (indentation) | |
204 indentation = "\n" + indentation; | |
205 var result = ""; | |
206 var propertyText; | |
207 var insideProperty = false; | |
208 var tokenize = tokenizerFactory.createTokenizer("text/css"); | |
209 | |
210 tokenize("*{" + styleText + "}", processToken); | |
211 if (insideProperty) | |
212 result += propertyText; | |
213 result = result.substring(2, result.length - 1).trimRight(); | |
214 return result + (indentation ? "\n" + endIndentation : ""); | |
215 | |
216 /** | |
217 * @param {string} token | |
218 * @param {?string} tokenType | |
219 * @param {number} column | |
220 * @param {number} newColumn | |
221 */ | |
222 function processToken(token, tokenType, column, newColumn) | |
223 { | |
224 if (!insideProperty) { | |
225 var disabledProperty = tokenType && tokenType.includes("css-comm
ent") && isDisabledProperty(token); | |
226 var isPropertyStart = tokenType && (tokenType.includes("css-stri
ng") || tokenType.includes("css-meta") || tokenType.includes("css-property") ||
tokenType.includes("css-variable-2")); | |
227 if (disabledProperty) { | |
228 result = result.trimRight() + indentation + token; | |
229 } else if (isPropertyStart) { | |
230 insideProperty = true; | |
231 propertyText = token; | |
232 } else if (token !== ";") { | |
233 result += token; | |
234 } | |
235 return; | |
236 } | |
237 | |
238 if (token === "}" || token === ";") { | |
239 result = result.trimRight() + indentation + propertyText.trim()
+ ";"; | |
240 insideProperty = false; | |
241 if (token === "}") | |
242 result += "}"; | |
243 } else { | |
244 propertyText += token; | |
245 } | |
246 } | |
247 | |
248 /** | |
249 * @param {string} text | |
250 * @return {boolean} | |
251 */ | |
252 function isDisabledProperty(text) | |
253 { | |
254 var colon = text.indexOf(":"); | |
255 if (colon === -1) | |
256 return false; | |
257 var propertyName = text.substring(2, colon).trim(); | |
258 return WebInspector.cssMetadata().isCSSPropertyName(propertyName); | |
259 } | |
260 }, | |
261 | |
262 /** | |
263 * @param {string} text | |
264 * @return {string} | |
265 */ | |
266 _detectIndentation: function(text) | |
267 { | |
268 var lines = text.split("\n"); | |
269 if (lines.length < 2) | |
270 return ""; | |
271 return WebInspector.TextUtils.lineIndent(lines[1]); | |
272 }, | |
273 | |
274 /** | |
275 * @param {string} newValue | |
276 * @param {boolean} majorChange | |
277 * @param {boolean} overwrite | |
278 * @param {function(boolean)=} userCallback | |
279 */ | |
280 setValue: function(newValue, majorChange, overwrite, userCallback) | |
281 { | |
282 var text = this.name + ": " + newValue + (this.important ? " !important"
: "") + ";"; | |
283 this.setText(text, majorChange, overwrite).then(userCallback); | |
284 }, | |
285 | |
286 /** | |
287 * @param {boolean} disabled | |
288 * @return {!Promise.<boolean>} | |
289 */ | |
290 setDisabled: function(disabled) | |
291 { | |
292 if (!this.ownerStyle) | |
293 return Promise.resolve(false); | |
294 if (disabled === this.disabled) | |
295 return Promise.resolve(true); | |
296 var propertyText = this.text.trim(); | |
297 var text = disabled ? "/* " + propertyText + " */" : this.text.substring
(2, propertyText.length - 2).trim(); | |
298 return this.setText(text, true, true); | |
299 } | |
300 }; | |
OLD | NEW |