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