1 
  2 sc_require('ext/string');
  3 sc_require('views/view');
  4 sc_require('views/view/animation');
  5 
  6 /**
  7   Map to CSS Transforms
  8 */
  9 
 10 // The scale transform must be last in order to decompose the transformation matrix.
 11 SC.CSS_TRANSFORM_NAMES = ['rotateX', 'rotateY', 'rotateZ', 'scale'];
 12 
 13 SC.CSS_TRANSFORM_MAP = {
 14 
 15   rotate: function (val) {
 16     if (SC.typeOf(val) === SC.T_NUMBER) { val += 'deg'; }
 17     return 'rotate(' + val + ')';
 18   },
 19 
 20   rotateX: function (val) {
 21     if (SC.typeOf(val) === SC.T_NUMBER) { val += 'deg'; }
 22     return 'rotateX(' + val + ')';
 23   },
 24 
 25   rotateY: function (val) {
 26     if (SC.typeOf(val) === SC.T_NUMBER) { val += 'deg'; }
 27     return 'rotateY(' + val + ')';
 28   },
 29 
 30   rotateZ: function (val) {
 31     if (SC.typeOf(val) === SC.T_NUMBER) { val += 'deg'; }
 32     return 'rotateZ(' + val + ')';
 33   },
 34 
 35   scale: function (val) {
 36     if (SC.typeOf(val) === SC.T_ARRAY) { val = val.join(', '); }
 37     return 'scale(' + val + ')';
 38   }
 39 };
 40 
 41 
 42 /** @private */
 43 SC.View.LayoutStyleCalculator = {
 44 
 45   /** @private Shared object used to avoid continually initializing/destroying objects. */
 46   _SC_STATE_MAP: null,
 47 
 48   /** @private Shared object used to avoid continually initializing/destroying objects. */
 49   _SC_TRANSFORMS_ARRAY: null,
 50 
 51   /** @private Shared object used to avoid continually initializing/destroying objects. */
 52   _SC_TRANSITIONS_ARRAY: null,
 53 
 54   /** @private If the value is undefined, make it null. */
 55   _valueOrNull: function (value) {
 56     return value === undefined ? null : value;
 57   },
 58 
 59   /** @private */
 60   _prepareStyle: function (style, layout) {
 61     /*jshint eqnull:true */
 62     // It's important to provide null defaults to reset any previous style when
 63     // this is applied.
 64     var commonBorder = this._valueOrNull(layout.border);
 65 
 66     // Reset properties that might not be set from style to style.
 67     style.marginLeft = null;
 68     style.marginTop = null;
 69 
 70     // Position and size.
 71     style.bottom = layout.bottom;
 72     style.right = layout.right;
 73     style.left = layout.left;
 74     style.top = layout.top;
 75     style.centerX = layout.centerX;
 76     style.centerY = layout.centerY;
 77     style.height = layout.height;
 78     style.width = layout.width;
 79 
 80     // Borders.
 81     style.borderTopWidth = layout.borderTop !== undefined ? layout.borderTop : commonBorder;
 82     style.borderRightWidth = layout.borderRight !== undefined ? layout.borderRight : commonBorder;
 83     style.borderBottomWidth = layout.borderBottom !== undefined ? layout.borderBottom : commonBorder;
 84     style.borderLeftWidth = layout.borderLeft !== undefined ? layout.borderLeft : commonBorder;
 85 
 86     // Minimum and maximum size.
 87     style.maxHeight = this._valueOrNull(layout.maxHeight);
 88     style.maxWidth = this._valueOrNull(layout.maxWidth);
 89     style.minWidth = this._valueOrNull(layout.minWidth);
 90     style.minHeight = this._valueOrNull(layout.minHeight);
 91 
 92     // the toString here is to ensure that it doesn't get px added to it
 93     style.zIndex  = (layout.zIndex != null) ? layout.zIndex.toString() : null;
 94     style.opacity = (layout.opacity != null) ? layout.opacity.toString() : null;
 95 
 96     style.backgroundPosition = this._valueOrNull(layout.backgroundPosition);
 97 
 98     // Handle transforms (including reset).
 99     if (SC.platform.supportsCSSTransforms) {
100       var transformAttribute = SC.browser.experimentalStyleNameFor('transform'),
101         transforms = this._SC_TRANSFORMS_ARRAY, // Shared object used to avoid continually initializing/destroying objects.
102         transformMap = SC.CSS_TRANSFORM_MAP;
103 
104       // Create the array once. Note: This is a shared array; it must be set to 0 length each time.
105       if (!transforms) { transforms = this._SC_TRANSFORMS_ARRAY = []; }
106 
107       // The order of the transforms is important so that we can decompose them
108       // from the transformation matrix later if necessary.
109       for (var i = 0, len = SC.CSS_TRANSFORM_NAMES.length; i < len; i++) {
110         var transformName = SC.CSS_TRANSFORM_NAMES[i],
111           layoutTransform = layout[transformName];
112 
113         if (layoutTransform != null) {
114           // normalizing transforms like rotateX: 5 to rotateX(5deg)
115           transforms.push(transformMap[transformName](layoutTransform));
116         }
117       }
118 
119       // Set or reset the transform style.
120       style[transformAttribute] = transforms.length > 0 ? transforms.join(' ') : null;
121 
122       // Transform origin.
123       var transformOriginAttribute = SC.browser.experimentalStyleNameFor('transformOrigin'),
124         originX = layout.transformOriginX,
125         originY = layout.transformOriginY;
126       if (originX == null && originY == null) {
127         style[transformOriginAttribute] = null;
128       } else {
129         if (originX == null) originX = 0.5;
130         if (originY == null) originY = 0.5;
131         style[transformOriginAttribute] = (originX * 100) + '% ' + (originY * 100) + '%';
132       }
133     }
134 
135     // Reset any transitions.
136     if (SC.platform.supportsCSSTransitions) {
137       style[SC.browser.experimentalStyleNameFor('transition')] = null;
138     }
139 
140     // Reset shared object!
141     this._SC_TRANSFORMS_ARRAY.length = 0;
142   },
143 
144   /** @private */
145   _prepareState: function (state, style) {
146     /*jshint eqnull:true */
147     state.hasBottom = (style.bottom != null);
148     state.hasRight = (style.right != null);
149     state.hasLeft = (style.left != null);
150     state.hasTop = (style.top != null);
151     state.hasCenterX = (style.centerX != null);
152     state.hasCenterY = (style.centerY != null);
153     state.hasHeight = (style.height != null);
154     state.hasWidth = (style.width != null);
155     state.hasMaxWidth = (style.maxWidth != null);
156     state.hasMaxHeight = (style.maxHeight != null);
157   },
158 
159 
160   // handles the case where you do width:auto or height:auto and are not using "staticLayout"
161   _invalidAutoValue: function (view, property) {
162     SC.throw("%@.layout() you cannot use %@:auto if staticLayout is disabled".fmt(view, property), "%@".fmt(view), -1);
163   },
164 
165   /** @private */
166   _calculatePosition: function (style, state, direction) {
167     var start, finish, size,
168       hasStart, hasFinish, hasSize, hasMaxSize,
169       startBorder,
170       finishBorder;
171 
172     if (direction === 'X') {
173       start      = 'left';
174       finish     = 'right';
175       size       = 'width';
176       startBorder  = 'borderLeftWidth';
177       finishBorder = 'borderRightWidth';
178       hasStart   = state.hasLeft;
179       hasFinish  = state.hasRight;
180       hasSize    = state.hasWidth;
181       hasMaxSize = state.hasMaxWidth;
182     } else {
183       start      = 'top';
184       finish     = 'bottom';
185       size       = 'height';
186       startBorder  = 'borderTopWidth';
187       finishBorder = 'borderBottomWidth';
188       hasStart   = state.hasTop;
189       hasFinish  = state.hasBottom;
190       hasSize    = state.hasHeight;
191       hasMaxSize = state.hasMaxHeight;
192     }
193 
194     style[start]  = this._cssNumber(style[start]);
195     style[finish] = this._cssNumber(style[finish]);
196 
197     var startBorderVal = this._cssNumber(style[startBorder]),
198       finishBorderVal = this._cssNumber(style[finishBorder]),
199       sizeNum = style[size];
200 
201     style[startBorder] = startBorderVal;
202     style[finishBorder] = finishBorderVal;
203 
204     // This is a normal number
205     if (sizeNum >= 1) { sizeNum -= (startBorderVal + finishBorderVal); }
206     style[size] = this._cssNumber(sizeNum);
207 
208     if (hasStart) {
209       // top, bottom, height -> top, bottom
210       if (hasFinish && hasSize)  { style[finish] = null; }
211     } else {
212       // bottom aligned
213       if (!hasFinish || (hasFinish && !hasSize && !hasMaxSize)) {
214         // no top, no bottom
215         style[start] = 0;
216       }
217     }
218 
219     if (!hasSize && !hasFinish) { style[finish] = 0; }
220   },
221 
222 
223   /** @private */
224   _calculateCenter: function (style, direction) {
225     var size, center, start, finish, margin,
226         startBorder,
227         finishBorder;
228 
229     if (direction === 'X') {
230       size   = 'width';
231       center = 'centerX';
232       start  = 'left';
233       finish = 'right';
234       margin = 'marginLeft';
235       startBorder  = 'borderLeftWidth';
236       finishBorder = 'borderRightWidth';
237     } else {
238       size   = 'height';
239       center = 'centerY';
240       start  = 'top';
241       finish = 'bottom';
242       margin = 'marginTop';
243       startBorder  = 'borderTopWidth';
244       finishBorder = 'borderBottomWidth';
245     }
246 
247     style[start] = "50%";
248 
249     var startBorderVal = this._cssNumber(style[startBorder]),
250       finishBorderVal = this._cssNumber(style[finishBorder]),
251       sizeValue   = style[size],
252       centerValue = style[center],
253       sizeIsPercent = SC.isPercentage(sizeValue),
254       value;
255 
256     style[startBorder] = startBorderVal;
257     style[finishBorder] = finishBorderVal;
258 
259     // Calculate the margin offset used to center the value along this axis.
260     if (SC.none(sizeValue)) {
261       // Invalid!
262       style[margin] = "50%";
263     } else {
264       value = centerValue - sizeValue / 2;
265       style[margin] = (sizeIsPercent) ? Math.floor(value * 100) + "%" : Math.floor(value);
266     }
267 
268     // If > 1 then it's a pixel value, in which case we shrink it to accommodate the borders.
269     if (sizeValue > 1) { sizeValue -= (startBorderVal + finishBorderVal); }
270 
271     style[size] = this._cssNumber(sizeValue) || 0;
272     style[finish] = style[center] = null;
273   },
274 
275   /** @private */
276   // return "auto" for "auto", null for null, converts 0.X into "X%".
277   // otherwise returns the original number, rounded down
278   _cssNumber: function (val) {
279     /*jshint eqnull:true*/
280     if (val == null) { return null; }
281     else if (val === SC.LAYOUT_AUTO) { return SC.LAYOUT_AUTO; }
282     else if (SC.isPercentage(val)) { return (val * 100) + "%"; }
283     else { return Math.floor(val); }
284   },
285 
286   /** @private
287     Calculate the layout style for the given view, making adjustments to allow
288     for flexible positioning, animation and accelerated transforms.
289 
290     @return {Object} Layout style hash.
291   */
292   calculate: function (view, style) {
293     var layout = view.get('layout'),
294       animations = view._activeAnimations,
295       state = this._SC_STATE_MAP, // Shared object used to avoid continually initializing/destroying objects.
296       useStaticLayout = view.get('useStaticLayout');
297 
298     // Fast path!
299     // If the developer sets useStaticLayout and doesn't provide a unique `layout` property, we
300     // should not insert the styles "left: 0px; right: 0px; top: 0px; bottom: 0px" as they could
301     // conflict with the developer's intention.  However, if they do provide a unique `layout`,
302     // use it.
303     if (useStaticLayout && layout === SC.View.prototype.layout) { return {}; }
304 
305     // Reset and prep the style object.
306     this._prepareStyle(style, layout);
307 
308     // Create the object once. Note: This is a shared object; all properties must be overwritten each time.
309     if (!state) { state = this._SC_STATE_MAP = {}; }
310 
311     // Reset and prep the state object.
312     this._prepareState(state, style);
313 
314     // handle invalid use of auto in absolute layouts
315     if (!useStaticLayout) {
316       if (style.width === SC.LAYOUT_AUTO) { this._invalidAutoValue(view, "width"); }
317       if (style.height === SC.LAYOUT_AUTO) { this._invalidAutoValue(view, "height"); }
318     }
319 
320     // X DIRECTION
321     if (state.hasLeft || state.hasRight || !state.hasCenterX) {
322       this._calculatePosition(style, state, "X");
323     } else {
324       this._calculateCenter(style, "X");
325     }
326 
327     // Y DIRECTION
328     if (state.hasTop || state.hasBottom || !state.hasCenterY) {
329       this._calculatePosition(style, state, "Y");
330     } else {
331       this._calculateCenter(style, "Y");
332     }
333 
334     this._calculateAnimations(style, animations, view.get('hasAcceleratedLayer'));
335 
336     // convert any numbers into a number + "px".
337     for (var key in style) {
338       var value = style[key];
339       if (typeof value === SC.T_NUMBER) { style[key] = (value + "px"); }
340     }
341 
342     return style;
343   },
344 
345   /** @private Calculates animation styles. */
346   _calculateAnimations: function (style, animations, hasAcceleratedLayer) {
347     /*jshint eqnull:true*/
348     var key,
349       shouldTranslate;
350 
351     // Handle transforms
352     if (hasAcceleratedLayer) {
353       shouldTranslate = YES;
354 
355       // If we're animating other transforms at different speeds, don't use acceleratedLayer
356       if (animations && (animations.top || animations.left)) {
357         for (key in animations) {
358           if (SC.CSS_TRANSFORM_MAP[key] &&
359               ((animations.top &&
360                 animations.top.duration !== animations[key].duration) ||
361                (animations.left &&
362                 animations.left.duration !== animations[key].duration))) {
363             shouldTranslate = NO;
364           }
365         }
366       }
367 
368       if (shouldTranslate) {
369         var transformAttribute = SC.browser.experimentalStyleNameFor('transform'),
370           curValue = style[transformAttribute],
371           newValue;
372 
373         newValue = 'translateX(' + style.left + 'px) translateY(' + style.top + 'px)';
374 
375         // double check to make sure this is needed
376         if (SC.platform.supportsCSS3DTransforms) { newValue += ' translateZ(0px)'; }
377 
378         // Append any current transforms.
379         // NOTE: The order of transforms is important. If we scale before translate, our translations
380         // will be scaled and incorrect.
381         if (curValue) { newValue += ' ' + curValue; }
382         style[transformAttribute] = newValue;
383 
384         // Set the absolute left & top style to 0,0 (will be translated from there).
385         style.left = 0;
386         style.top = 0;
387       }
388     }
389 
390     // Handle animations
391     if (animations) {
392       if (SC.platform.supportsCSSTransitions) {
393         var transitions = this._SC_TRANSITIONS_ARRAY; // Shared object used to avoid continually initializing/destroying objects.
394 
395         // Create the array once. Note: This is a shared array; it must be set to 0 length each time.
396         if (!transitions) { transitions = this._SC_TRANSITIONS_ARRAY = []; }
397 
398         for (key in animations) {
399           var animation = animations[key],
400             isTransformProperty = !!SC.CSS_TRANSFORM_MAP[key],
401             isTurboProperty = shouldTranslate && (key === 'top' || key === 'left');
402 
403           if (SC.platform.supportsCSSTransforms && (isTurboProperty || isTransformProperty)) {
404             // Untrack the un-transformed property name.
405             delete animations[key];
406 
407             // The key will be either 'transform' or one of '-webkit-transform', '-ms-transform', '-moz-transform', '-o-transform'
408             key = SC.browser.experimentalCSSNameFor('transform');
409 
410             var curTransformAnimation = animations[key];
411 
412             // Because multiple transforms actually share one CSS property, we can't animate multiple transforms
413             // at different speeds. So, to handle that case, we just force them to all have the same length.
414             if (curTransformAnimation) {
415               //@if(debug)
416               if (curTransformAnimation.duration !== animation.duration || curTransformAnimation.timing !== animation.timing || curTransformAnimation.delay !== animation.delay) {
417                 SC.Logger.warn("Developer Warning: Can't animate transforms with different durations, timings or delays! Using the first options specified.");
418               }
419               //@endif
420               animation = curTransformAnimation;
421             } else {
422               // Track the transformed property name.
423               animations[key] = animation;
424             }
425           }
426 
427           // Fix up the centerX & centerY properties.
428           if (key === 'centerX') { key = 'margin-left'; }
429           if (key === 'centerY') { key = 'margin-top'; }
430 
431           // We're actually storing the css for the animation on layout.animate[key].css
432           animation.css = key + " " + animation.duration + "s " + animation.timing + " " + animation.delay + "s";
433 
434           // If there are multiple transform properties, we only need to set this key once.
435           // We already checked before to make sure they have the same duration.
436           // if (!pendingAnimations[key]) {
437           if (transitions.indexOf(animation.css) < 0) {
438             transitions.push(animation.css);
439           }
440         }
441 
442         style[SC.browser.experimentalStyleNameFor('transition')] = transitions.join(", ");
443 
444         // Reset shared object!
445         this._SC_TRANSITIONS_ARRAY.length = 0;
446       } else {
447         // TODO: Do it the JS way
448       }
449     }
450   }
451 
452 };
453 
454 
455 
456 SC.View.reopen(
457   /** @scope SC.View.prototype */ {
458 
459   /** @private Shared object used to avoid continually initializing/destroying objects. */
460   _SC_STYLE_MAP: null,
461 
462   /**
463     layoutStyle describes the current styles to be written to your element
464     based on the layout you defined.  Both layoutStyle and frame reset when
465     you edit the layout property.  Both are read only.
466 
467     Computes the layout style settings needed for the current anchor.
468 
469     @type Object
470     @readOnly
471   */
472   layoutStyle: function () {
473     var style = this._SC_STYLE_MAP; // Shared object used to avoid continually initializing/destroying objects.
474 
475     // Create the object once. Note: This is a shared object; all properties must be overwritten each time.
476     if (!style) { style = this._SC_STYLE_MAP = {}; }
477 
478     return SC.View.LayoutStyleCalculator.calculate(this, style);
479 
480   // 'hasAcceleratedLayer' is dependent on 'layout' so we don't need 'layout' to be a dependency here
481   }.property('hasAcceleratedLayer', 'useStaticLayout').cacheable()
482 
483 });
484