UNPKG

8.19 kBJavaScriptView Raw
1let { list } = require('postcss')
2let parser = require('postcss-value-parser')
3
4let Browsers = require('./browsers')
5let vendor = require('./vendor')
6
7class Transition {
8 constructor(prefixes) {
9 this.props = ['transition', 'transition-property']
10 this.prefixes = prefixes
11 }
12
13 /**
14 * Process transition and add prefixes for all necessary properties
15 */
16 add(decl, result) {
17 let prefix, prop
18 let add = this.prefixes.add[decl.prop]
19 let vendorPrefixes = this.ruleVendorPrefixes(decl)
20 let declPrefixes = vendorPrefixes || (add && add.prefixes) || []
21
22 let params = this.parse(decl.value)
23 let names = params.map(i => this.findProp(i))
24 let added = []
25
26 if (names.some(i => i[0] === '-')) {
27 return
28 }
29
30 for (let param of params) {
31 prop = this.findProp(param)
32 if (prop[0] === '-') continue
33
34 let prefixer = this.prefixes.add[prop]
35 if (!prefixer || !prefixer.prefixes) continue
36
37 for (prefix of prefixer.prefixes) {
38 if (vendorPrefixes && !vendorPrefixes.some(p => prefix.includes(p))) {
39 continue
40 }
41
42 let prefixed = this.prefixes.prefixed(prop, prefix)
43 if (prefixed !== '-ms-transform' && !names.includes(prefixed)) {
44 if (!this.disabled(prop, prefix)) {
45 added.push(this.clone(prop, prefixed, param))
46 }
47 }
48 }
49 }
50
51 params = params.concat(added)
52 let value = this.stringify(params)
53
54 let webkitClean = this.stringify(
55 this.cleanFromUnprefixed(params, '-webkit-')
56 )
57 if (declPrefixes.includes('-webkit-')) {
58 this.cloneBefore(decl, `-webkit-${decl.prop}`, webkitClean)
59 }
60 this.cloneBefore(decl, decl.prop, webkitClean)
61 if (declPrefixes.includes('-o-')) {
62 let operaClean = this.stringify(this.cleanFromUnprefixed(params, '-o-'))
63 this.cloneBefore(decl, `-o-${decl.prop}`, operaClean)
64 }
65
66 for (prefix of declPrefixes) {
67 if (prefix !== '-webkit-' && prefix !== '-o-') {
68 let prefixValue = this.stringify(
69 this.cleanOtherPrefixes(params, prefix)
70 )
71 this.cloneBefore(decl, prefix + decl.prop, prefixValue)
72 }
73 }
74
75 if (value !== decl.value && !this.already(decl, decl.prop, value)) {
76 this.checkForWarning(result, decl)
77 decl.cloneBefore()
78 decl.value = value
79 }
80 }
81
82 /**
83 * Find property name
84 */
85 findProp(param) {
86 let prop = param[0].value
87 if (/^\d/.test(prop)) {
88 for (let [i, token] of param.entries()) {
89 if (i !== 0 && token.type === 'word') {
90 return token.value
91 }
92 }
93 }
94 return prop
95 }
96
97 /**
98 * Does we already have this declaration
99 */
100 already(decl, prop, value) {
101 return decl.parent.some(i => i.prop === prop && i.value === value)
102 }
103
104 /**
105 * Add declaration if it is not exist
106 */
107 cloneBefore(decl, prop, value) {
108 if (!this.already(decl, prop, value)) {
109 decl.cloneBefore({ prop, value })
110 }
111 }
112
113 /**
114 * Show transition-property warning
115 */
116 checkForWarning(result, decl) {
117 if (decl.prop !== 'transition-property') {
118 return
119 }
120
121 let isPrefixed = false
122 let hasAssociatedProp = false
123
124 decl.parent.each(i => {
125 if (i.type !== 'decl') {
126 return undefined
127 }
128 if (i.prop.indexOf('transition-') !== 0) {
129 return undefined
130 }
131 let values = list.comma(i.value)
132 // check if current Rule's transition-property comma separated value list needs prefixes
133 if (i.prop === 'transition-property') {
134 values.forEach(value => {
135 let lookup = this.prefixes.add[value]
136 if (lookup && lookup.prefixes && lookup.prefixes.length > 0) {
137 isPrefixed = true
138 }
139 })
140 return undefined
141 }
142 // check if another transition-* prop in current Rule has comma separated value list
143 hasAssociatedProp = hasAssociatedProp || values.length > 1
144 return false
145 })
146
147 if (isPrefixed && hasAssociatedProp) {
148 decl.warn(
149 result,
150 'Replace transition-property to transition, ' +
151 'because Autoprefixer could not support ' +
152 'any cases of transition-property ' +
153 'and other transition-*'
154 )
155 }
156 }
157
158 /**
159 * Process transition and remove all unnecessary properties
160 */
161 remove(decl) {
162 let params = this.parse(decl.value)
163 params = params.filter(i => {
164 let prop = this.prefixes.remove[this.findProp(i)]
165 return !prop || !prop.remove
166 })
167 let value = this.stringify(params)
168
169 if (decl.value === value) {
170 return
171 }
172
173 if (params.length === 0) {
174 decl.remove()
175 return
176 }
177
178 let double = decl.parent.some(i => {
179 return i.prop === decl.prop && i.value === value
180 })
181 let smaller = decl.parent.some(i => {
182 return i !== decl && i.prop === decl.prop && i.value.length > value.length
183 })
184
185 if (double || smaller) {
186 decl.remove()
187 return
188 }
189
190 decl.value = value
191 }
192
193 /**
194 * Parse properties list to array
195 */
196 parse(value) {
197 let ast = parser(value)
198 let result = []
199 let param = []
200 for (let node of ast.nodes) {
201 param.push(node)
202 if (node.type === 'div' && node.value === ',') {
203 result.push(param)
204 param = []
205 }
206 }
207 result.push(param)
208 return result.filter(i => i.length > 0)
209 }
210
211 /**
212 * Return properties string from array
213 */
214 stringify(params) {
215 if (params.length === 0) {
216 return ''
217 }
218 let nodes = []
219 for (let param of params) {
220 if (param[param.length - 1].type !== 'div') {
221 param.push(this.div(params))
222 }
223 nodes = nodes.concat(param)
224 }
225 if (nodes[0].type === 'div') {
226 nodes = nodes.slice(1)
227 }
228 if (nodes[nodes.length - 1].type === 'div') {
229 nodes = nodes.slice(0, +-2 + 1 || undefined)
230 }
231 return parser.stringify({ nodes })
232 }
233
234 /**
235 * Return new param array with different name
236 */
237 clone(origin, name, param) {
238 let result = []
239 let changed = false
240 for (let i of param) {
241 if (!changed && i.type === 'word' && i.value === origin) {
242 result.push({ type: 'word', value: name })
243 changed = true
244 } else {
245 result.push(i)
246 }
247 }
248 return result
249 }
250
251 /**
252 * Find or create separator
253 */
254 div(params) {
255 for (let param of params) {
256 for (let node of param) {
257 if (node.type === 'div' && node.value === ',') {
258 return node
259 }
260 }
261 }
262 return { type: 'div', value: ',', after: ' ' }
263 }
264
265 cleanOtherPrefixes(params, prefix) {
266 return params.filter(param => {
267 let current = vendor.prefix(this.findProp(param))
268 return current === '' || current === prefix
269 })
270 }
271
272 /**
273 * Remove all non-webkit prefixes and unprefixed params if we have prefixed
274 */
275 cleanFromUnprefixed(params, prefix) {
276 let remove = params
277 .map(i => this.findProp(i))
278 .filter(i => i.slice(0, prefix.length) === prefix)
279 .map(i => this.prefixes.unprefixed(i))
280
281 let result = []
282 for (let param of params) {
283 let prop = this.findProp(param)
284 let p = vendor.prefix(prop)
285 if (!remove.includes(prop) && (p === prefix || p === '')) {
286 result.push(param)
287 }
288 }
289 return result
290 }
291
292 /**
293 * Check property for disabled by option
294 */
295 disabled(prop, prefix) {
296 let other = ['order', 'justify-content', 'align-self', 'align-content']
297 if (prop.includes('flex') || other.includes(prop)) {
298 if (this.prefixes.options.flexbox === false) {
299 return true
300 }
301
302 if (this.prefixes.options.flexbox === 'no-2009') {
303 return prefix.includes('2009')
304 }
305 }
306 return undefined
307 }
308
309 /**
310 * Check if transition prop is inside vendor specific rule
311 */
312 ruleVendorPrefixes(decl) {
313 let { parent } = decl
314
315 if (parent.type !== 'rule') {
316 return false
317 } else if (!parent.selector.includes(':-')) {
318 return false
319 }
320
321 let selectors = Browsers.prefixes().filter(s =>
322 parent.selector.includes(':' + s)
323 )
324
325 return selectors.length > 0 ? selectors : false
326 }
327}
328
329module.exports = Transition