1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 /** 9 @type String 10 @constant 11 */ 12 SC.ALIGN_JUSTIFY = "justify"; 13 14 /** 15 @namespace 16 17 Normal SproutCore views are absolutely positioned--parent views have relatively 18 little input on where their child views are placed. 19 20 This mixin makes a view layout its child views itself, flowing left-to-right 21 or up-to-down, and, optionally, wrapping. 22 23 Child views with useAbsoluteLayout===YES will be ignored in the layout process. 24 This mixin detects when child views have changed their size, and will adjust accordingly. 25 It also observes child views' isVisible and calculatedWidth/Height properties, and, as a 26 flowedlayout-specific extension, isHidden. 27 28 These properties are observed through `#js:observeChildLayout` and `#js:unobserveChildLayout`; 29 you can override the method to add your own properties. To customize isVisible behavior, 30 you will also want to override shouldIncludeChildInFlow. 31 32 This relies on the children's frames or, if specified, calculatedWidth and calculatedHeight 33 properties. 34 35 This view mixes very well with animation. Further, it is able to automatically mix 36 in to child views it manages, created or not yet created, allowing you to specify 37 settings such as animation once only, and have everything "just work". 38 39 Like normal views, you simply specify child views--everything will "just work." 40 41 @since SproutCore 1.0 42 */ 43 SC.FlowedLayout = { 44 isFlowedLayout: YES, 45 /** 46 The direction of flow. Possible values: 47 48 - SC.LAYOUT_HORIZONTAL 49 - SC.LAYOUT_VERTICAL 50 51 @type String 52 @default SC.LAYOUT_HORIZONTAL 53 */ 54 layoutDirection: SC.LAYOUT_HORIZONTAL, 55 56 /** 57 Whether the view should automatically resize (to allow scrolling, for instance) 58 59 @type Boolean 60 @default YES 61 */ 62 autoResize: YES, 63 64 /** 65 @type Boolean 66 @default YES 67 */ 68 shouldResizeWidth: YES, 69 70 /** 71 @type Boolean 72 @default YES 73 */ 74 shouldResizeHeight: YES, 75 76 /** 77 The alignment of items within rows or columns. Possible values: 78 79 - SC.ALIGN_LEFT 80 - SC.ALIGN_CENTER 81 - SC.ALIGN_RIGHT 82 - SC.ALIGN_JUSTIFY 83 84 @type String 85 @default SC.ALIGN_LEFT 86 */ 87 align: SC.ALIGN_LEFT, 88 89 /** 90 If YES, flowing child views are allowed to wrap to new rows or columns. 91 92 @type Boolean 93 @default YES 94 */ 95 canWrap: YES, 96 97 /** 98 A set of spacings (left, top, right, bottom) for subviews. Defaults to 0s all around. 99 This is the amount of space that will be before, after, above, and below the view. These 100 spacings do not collapse into each other. 101 102 You can also set flowSpacing on any child view, or implement flowSpacingForView. 103 104 @type Hash 105 @default `{ left: 0, bottom: 0, top: 0, right: 0 }` 106 */ 107 defaultFlowSpacing: { left: 0, bottom: 0, top: 0, right: 0 }, 108 109 /** 110 @type Hash 111 112 Padding around the edges of this flow layout view. This is useful for 113 situations where you don't control the layout of the FlowedLayout view; 114 for instance, when the view is the contentView for a SC.ScrollView. 115 116 @type Hash 117 @default `{ left: 0, bottom: 0, top: 0, right: 0 }` 118 */ 119 flowPadding: { left: 0, bottom: 0, right: 0, top: 0 }, 120 121 /** 122 @private 123 124 If the flowPadding somehow misses a property (one of the sides), 125 we need to make sure a default value of 0 is still there. 126 */ 127 _scfl_validFlowPadding: function() { 128 var padding = this.get('flowPadding') || {}, ret = {}; 129 ret.left = padding.left || 0; 130 ret.top = padding.top || 0; 131 ret.bottom = padding.bottom || 0; 132 ret.right = padding.right || 0; 133 return ret; 134 }.property('flowPadding').cacheable(), 135 136 concatenatedProperties: ['childMixins'], 137 138 /** @private */ 139 initMixin: function() { 140 this._scfl_tileOnce(); 141 // register observer to detect the childViews changes 142 this.addObserver( 'childViews.[]', this, this._scfl_childViewsDidChange ); 143 }, 144 145 /** @private 146 Detects when the child views change. 147 */ 148 _scfl_childViewsDidChange: function(c) { 149 this._scfl_tileOnce(); 150 }, 151 152 /** @private */ 153 _scfl_layoutPropertyDidChange: function(childView) { 154 this._scfl_tileOnce(); 155 }.observes('layoutDirection', 'align', 'flowPadding', 'canWrap', 'defaultFlowSpacing', 'isVisibleInWindow'), 156 157 /** @private 158 Overridden to only update if it is a view we do not manage, or the width or height has changed 159 since our last record of it. 160 */ 161 layoutDidChangeFor: function(c) { 162 // now, check if anything has changed 163 var l = c._scfl_lastLayout, cl = c.get('layout'), f = c.get('frame'); 164 if (!l) return sc_super(); 165 166 var same = YES; 167 168 // in short, if anything interfered with the layout, we need to 169 // do something about it. 170 if (l.left && l.left !== cl.left) same = NO; 171 else if (l.top && l.top !== cl.top) same = NO; 172 else if (!c.get('fillWidth') && l.width && l.width !== cl.width) same = NO; 173 else if (!l.width && !c.get('fillWidth') && f.width !== c._scfl_lastFrame.width) same = NO; 174 else if (!c.get('fillHeight') && l.height && l.height !== cl.height) same = NO; 175 else if (!l.height && !c.get('fillHeight') && f.height !== c._scfl_lastFrame.height) same = NO; 176 177 if (same) { 178 return sc_super(); 179 } 180 181 // nothing has changed. This is where we do something 182 this._scfl_tileOnce(); 183 sc_super(); 184 }, 185 186 /** @private 187 Sets up layout observers on child view. We observe three things: 188 - isVisible 189 - calculatedWidth 190 - calculatedHeight 191 192 Actual layout changes are detected through layoutDidChangeFor. 193 */ 194 observeChildLayout: function(c) { 195 if (c._scfl_isBeingObserved) return; 196 c._scfl_isBeingObserved = YES; 197 c.addObserver('flowSpacing', this, '_scfl_tileOnce'); 198 c.addObserver('isVisible', this, '_scfl_tileOnce'); 199 c.addObserver('useAbsoluteLayout', this, '_scfl_tileOnce'); 200 c.addObserver('calculatedWidth', this, '_scfl_tileOnce'); 201 c.addObserver('calculatedHeight', this, '_scfl_tileOnce'); 202 c.addObserver('startsNewRow', this, '_scfl_tileOnce'); 203 c.addObserver('isSpacer', this, '_scfl_tileOnce'); 204 c.addObserver('maxSpacerLength', this, '_scfl_tileOnce'); 205 c.addObserver('fillWidth', this, '_scfl_tileOnce'); 206 c.addObserver('fillHeight', this, '_scfl_tileOnce'); 207 }, 208 209 /** @private 210 Removes observers on child view. 211 */ 212 unobserveChildLayout: function(c) { 213 c._scfl_isBeingObserved = NO; 214 c.removeObserver('flowSpacing', this, '_scfl_tileOnce'); 215 c.removeObserver('isVisible', this, '_scfl_tileOnce'); 216 c.removeObserver('useAbsoluteLayout', this, '_scfl_tileOnce'); 217 c.removeObserver('calculatedWidth', this, '_scfl_tileOnce'); 218 c.removeObserver('calculatedHeight', this, '_scfl_tileOnce'); 219 c.removeObserver('startsNewRow', this, '_scfl_tileOnce'); 220 c.removeObserver('isSpacer', this, '_scfl_tileOnce'); 221 c.removeObserver('maxSpacerLength', this, '_scfl_tileOnce'); 222 c.removeObserver('fillWidth', this, '_scfl_tileOnce'); 223 c.removeObserver('fillHeight', this, '_scfl_tileOnce'); 224 }, 225 226 /** 227 Determines whether the specified child view should be included in the flow layout. 228 By default, if it has isVisible: NO or useAbsoluteLayout: YES, it will not be included. 229 230 @field 231 @type Boolean 232 @default NO 233 */ 234 shouldIncludeChildInFlow: function(idx, c) { 235 return c.get('isVisible') && !c.get('useAbsoluteLayout'); 236 }, 237 238 /** 239 Returns the flow spacings for a given view. By default, returns the view's flowSpacing, 240 and if they don't exist, the defaultFlowSpacing for this view. 241 242 @field 243 @type Hash 244 */ 245 flowSpacingForChild: function(idx, view) { 246 var spacing = view.get('flowSpacing'); 247 if (SC.none(spacing)) spacing = this.get('defaultFlowSpacing'); 248 if (SC.none(spacing)) spacing = 0; 249 250 if (SC.typeOf(spacing) === SC.T_NUMBER) { 251 spacing = { left: spacing, right: spacing, bottom: spacing, top: spacing }; 252 } else { 253 spacing['left'] = spacing['left'] || 0; 254 spacing['right'] = spacing['right'] || 0; 255 spacing['top'] = spacing['top'] || 0; 256 spacing['bottom'] = spacing['bottom'] || 0; 257 } 258 259 return spacing; 260 }, 261 262 /** 263 Returns the flow size for a given view, excluding spacing. The default version 264 checks the view's calculatedWidth/Height, then its frame. 265 266 For spacers, this returns an empty size. 267 268 @field 269 @type Hash 270 @default {width: 0, height: 0} 271 */ 272 flowSizeForChild: function(idx, view) { 273 var cw = view.get('calculatedWidth'), ch = view.get('calculatedHeight'), 274 layoutDirection = this.get('layoutDirection'), 275 calc = {}, f = view.get('frame'), l = view.get('layout'); 276 view._scfl_lastFrame = f; 277 278 // if there is a calculated width, use that. NOTE: if calculatedWidth === 0, 279 // it is invalid. This is the practice in other views. 280 if (cw) { 281 calc.width = cw; 282 } else { 283 // we should use the layout width if available to avoid breaking layouts 284 // that have borders 285 calc.width = l.width || f.width; 286 } 287 288 // same for calculated height 289 if (ch) { 290 calc.height = ch; 291 } else { 292 // we should use the layout width if available to avoid breaking layouts 293 // that have borders 294 calc.height = l.height || f.height; 295 } 296 297 // if it is a spacer, we must set the dimension that it 298 // expands in to 0. 299 if (view.get('isSpacer')) { 300 calc.maxSpacerLength = view.get('maxSpacerLength'); 301 302 if (layoutDirection === SC.LAYOUT_HORIZONTAL) { 303 calc.width = l.minWidth || 0; 304 } else { 305 calc.height = l.minHeight || 0; 306 } 307 } 308 309 // if it has a fillWidth/Height, clear it for later 310 if (layoutDirection === SC.LAYOUT_HORIZONTAL && view.get('fillHeight')) { 311 calc.height = l.minHeight || 0; 312 } else if (layoutDirection === SC.LAYOUT_VERTICAL && view.get('fillWidth')) { 313 calc.width = l.minWidth || 0; 314 } 315 316 return calc; 317 }, 318 319 /** @private */ 320 clippingFrame: function() { 321 return { left: 0, top: 0, width: this.get('calculatedWidth'), height: this.get('calculatedHeight') }; 322 }.property('calculatedWidth', 'calculatedHeight'), 323 324 /** @private */ 325 326 // the maximum row length when all flexible items are collapsed. 327 _scfl_maxCollapsedRowLength: 0, 328 329 // the total row size when all flexible rows are collapsed. 330 _scfl_totalCollapsedRowSize: 0, 331 332 333 _scfl_calculatedSizeDidChange: function() { 334 if(this.get('autoResize')) { 335 if (this.get('layoutDirection') == SC.LAYOUT_VERTICAL) { 336 if (this.get('shouldResizeHeight')) { 337 this.adjust('minHeight', this.get('_scfl_maximumCollapsedRowLength')); 338 } 339 340 if (this.get('shouldResizeWidth')) { 341 this.adjust('minWidth', this.get('_scfl_totalCollapsedRowSize')); 342 } 343 } else { 344 if (this.get('shouldResizeWidth')) { 345 this.adjust('minWidth', this.get('_scfl_maximumCollapsedRowLength')); 346 } 347 if (this.get('shouldResizeHeight')) { 348 this.adjust('minHeight', this.get('_scfl_totalCollapsedRowSize')); 349 } 350 } 351 } 352 }.observes('autoResize', 'shouldResizeWidth', '_scfl_maximumCollapsedRowLength', '_scfl_totalCollapsedRowSize', 'shouldResizeHeight'), 353 354 /** 355 @private 356 Creates a plan, initializing all of the basic properties in it, but not 357 doing anything further. 358 359 Other methods should be called to do this: 360 361 - _scfl_distributeChildrenIntoRows distributes children into rows. 362 - _scfl_positionChildrenInRows positions the children within the rows. 363 - this calls _scfl_positionChildrenInRow 364 - _scfl_positionRows positions and sizes rows within the plan. 365 366 The plan's structure is defined inside the method. 367 368 Some of these methods may eventually be made public and/or delegate methods. 369 */ 370 _scfl_createPlan: function() { 371 var layoutDirection = this.get('layoutDirection'), 372 flowPadding = this.get('_scfl_validFlowPadding'), 373 frame = this.get('frame'); 374 375 var isVertical = (layoutDirection === SC.LAYOUT_VERTICAL); 376 377 // A plan hash contains general information about the layout, and also, 378 // the collection of rows. 379 // 380 // This method only fills out a subset of the properties in a plan. 381 // 382 var plan = { 383 // The rows array starts empty. It will get filled out by the method 384 // _scfl_distributeChildrenIntoRows. 385 rows: undefined, 386 387 388 // the maximum row length where all collapsible items are collapsed. 389 maximumCollapsedRowLength: 0, 390 391 // the total sizes of all rows when collapsed (With flex-height rows 392 // at minimum size) 393 totalCollapsedRowSize: 0, 394 395 // These properties are calculated once here, but later used by 396 // the various methods. 397 isVertical: layoutDirection === SC.LAYOUT_VERTICAL, 398 isHorizontal: layoutDirection === SC.LAYOUT_HORIZONTAL, 399 400 flowPadding: flowPadding, 401 402 planStartPadding: flowPadding[isVertical ? 'left' : 'top'], 403 planEndPadding: flowPadding[isVertical ? 'right' : 'bottom'], 404 405 rowStartPadding: flowPadding[isVertical ? 'top' : 'left'], 406 rowEndPadding: flowPadding[isVertical ? 'bottom' : 'right'], 407 408 maximumRowLength: undefined, // to be calculated below 409 410 // if any rows need to fit to fill, this is the size to fill 411 fitToPlanSize: undefined, 412 413 414 align: this.get('align') 415 }; 416 417 if (isVertical) { 418 plan.maximumRowLength = frame.height - plan.rowStartPadding - plan.rowEndPadding; 419 plan.fitToPlanSize = frame.width - plan.planStartPadding - plan.planEndPadding; 420 } else { 421 plan.maximumRowLength = frame.width - plan.rowStartPadding - plan.rowEndPadding; 422 plan.fitToPlanSize = frame.height - plan.planStartPadding - plan.planEndPadding; 423 } 424 425 return plan; 426 }, 427 428 /** @private */ 429 _scfl_distributeChildrenIntoRows: function(plan) { 430 var children = this.get('childViews'), child, idx, len = children.length, 431 isVertical = plan.isVertical, rows = [], lastIdx; 432 433 lastIdx = -1; idx = 0; 434 while (idx < len && idx !== lastIdx) { 435 lastIdx = idx; 436 437 var row = { 438 // always a reference to the plan 439 plan: plan, 440 441 // the combined size of the items in the row. This is used, for instance, 442 // in justification or right-alignment. 443 rowLength: undefined, 444 445 // the size of the row. When flowing horizontally, this is the height; 446 // it is the opposite dimension of rowLength. It is calculated 447 // both while positioning items in the row and while positioning the rows 448 // themselves. 449 rowSize: undefined, 450 451 // whether this row should expand to fit any available space. In this case, 452 // the size is the row's minimum size. 453 shouldExpand: undefined, 454 455 // to be decided by _scfl_distributeItemsIntoRows 456 items: undefined, 457 458 // to be decided by _scfl_positionRows 459 position: undefined 460 }; 461 462 idx = this._scfl_distributeChildrenIntoRow(children, idx, row); 463 rows.push(row); 464 } 465 466 plan.rows = rows; 467 }, 468 469 /** 470 @private 471 Distributes as many children as possible into a single row, stating 472 at the given index, and returning the index of the next item, if any. 473 */ 474 _scfl_distributeChildrenIntoRow: function(children, startingAt, row) { 475 var idx, len = children.length, plan = row.plan, child, childSize, spacing, 476 items = [], itemOffset = 0, isVertical = plan.isVertical, itemSize, itemLength, 477 maxSpacerLength, 478 canWrap = this.get('canWrap'), 479 newRowPending = NO, 480 maxItemLength = 0, 481 max = row.plan.maximumRowLength; 482 483 for (idx = startingAt; idx < len; idx++) { 484 child = children[idx]; 485 486 // this must be set before we check if the child is included because even 487 // if it isn't included, we need to remember that there is a line break 488 // for later 489 newRowPending = newRowPending || (items.length > 0 && child.get('startsNewRow')); 490 491 if (!this.shouldIncludeChildInFlow(idx, child)) continue; 492 493 childSize = this.flowSizeForChild(idx, child); 494 spacing = this.flowSpacingForChild(idx, child); 495 496 childSize.width += spacing.left + spacing.right; 497 childSize.height += spacing.top + spacing.bottom; 498 499 itemLength = childSize[isVertical ? 'height' : 'width']; 500 if(!SC.none(childSize.maxSpacerLength)) maxSpacerLength = childSize.maxSpacerLength + (isVertical ? spacing.top + spacing.bottom : spacing.left + spacing.right); 501 itemSize = childSize[isVertical ? 'width' : 'height']; 502 503 // there are two cases where we must start a new row: if the child or a 504 // previous child in the row that wasn't included has 505 // startsNewRow === YES, and if the item cannot fit. Neither applies if there 506 // is nothing in the row yet. 507 if ((newRowPending || (canWrap && itemOffset + itemLength > max)) && items.length > 0) { 508 break; 509 } 510 511 var item = { 512 child: child, 513 514 itemLength: itemLength, 515 maxSpacerLength: maxSpacerLength, 516 itemSize: itemSize, 517 518 spacing: spacing, 519 520 // The position in the row. 521 // 522 // note: in one process or another, this becomes left or top. 523 // but before that, it is calculated. 524 position: undefined, 525 526 // whether this item should attempt to fill to the row's size 527 fillRow: isVertical ? child.get('fillWidth') : child.get('fillHeight'), 528 529 // whether this item is a spacer, and thus should be resized to its itemLength 530 isSpacer: child.get('isSpacer'), 531 532 // these will get set if necessary during the positioning code 533 left: undefined, top: undefined, 534 width: undefined, height: undefined 535 }; 536 537 538 items.push(item); 539 itemOffset += itemLength; 540 maxItemLength = Math.max(itemLength, maxItemLength); 541 } 542 543 row.rowLength = itemOffset; 544 545 // if the row cannot wrap, then the minimum size for the row (and therefore collapsed size) 546 // is the same as the current row length: it consists of the minimum size of all items. 547 // 548 // If the row can wrap, then the longest item will determine the size of a fully 549 // collapsed (one item per row) layout. 550 var minRowLength = canWrap ? maxItemLength : row.rowLength; 551 row.plan.maximumCollapsedRowLength = Math.max(minRowLength, row.plan.maximumCollapsedRowLength); 552 row.items = items; 553 return idx; 554 }, 555 556 /** @private */ 557 _scfl_positionChildrenInRows: function(plan) { 558 var rows = plan.rows, len = rows.length, idx; 559 560 for (idx = 0; idx < len; idx++) { 561 this._scfl_positionChildrenInRow(rows[idx]); 562 } 563 }, 564 565 /** 566 @private 567 Positions items within a row. The items are already in the row, this just 568 modifies the 'position' property. 569 570 This also marks a tentative size of the row, and whether it should be expanded 571 to fit in any available extra space. Note the term 'size' rather than 'length'... 572 */ 573 _scfl_positionChildrenInRow: function(row) { 574 var items = row.items, len = items.length, idx, item, position, rowSize = 0, 575 spacerCount = 0, spacerSize, align = row.plan.align, shouldExpand = YES, 576 leftOver = 0, noMaxWidth = NO; 577 578 // 579 // STEP ONE: DETERMINE SPACER SIZE + COUNT 580 // 581 for (idx = 0; idx < len; idx++) { 582 item = items[idx]; 583 if (item.isSpacer) { 584 spacerCount += item.child.get('spaceUnits') || 1; 585 } 586 } 587 588 // justification is like adding a spacer between every item. We'll actually account for 589 // that later, but for now... 590 if (align === SC.ALIGN_JUSTIFY) spacerCount += len - 1; 591 592 // calculate spacer size 593 spacerSize = Math.max(0, row.plan.maximumRowLength - row.rowLength) / spacerCount; 594 595 // determine individual spacer sizes using spacerSize and limited by 596 // each spacer's maxWidth (if they have one) 597 while(spacerSize > 0) { 598 for (idx = 0; idx < len; idx++) { 599 item = items[idx]; 600 601 if (item.isSpacer) { 602 item.itemLength += spacerSize * (item.child.get('spaceUnits') || 1); 603 if(item.itemLength > item.maxSpacerLength) { 604 leftOver += item.itemLength - item.maxSpacerLength; 605 item.itemLength = item.maxSpacerLength; 606 } 607 else { 608 noMaxWidth = YES; 609 } 610 } 611 } 612 613 // if none of the spacers can expand further, stop 614 if(!noMaxWidth) break; 615 616 spacerSize = Math.round(leftOver / spacerCount); 617 leftOver = 0; 618 } 619 620 // 621 // STEP TWO: ADJUST FOR ALIGNMENT 622 // Note: if there are spacers, this has no effect, because they fill all available 623 // space. 624 // 625 position = 0; 626 if (spacerCount === 0 && (align === SC.ALIGN_RIGHT || align === SC.ALIGN_BOTTOM)) { 627 position = row.plan.maximumRowLength - row.rowLength; 628 } else if (spacerCount === 0 && (align === SC.ALIGN_CENTER || align === SC.ALIGN_MIDDLE)) { 629 position = (row.plan.maximumRowLength / 2) - (row.rowLength / 2); 630 } 631 632 position += row.plan.rowStartPadding; 633 // 634 // STEP TWO: LOOP + POSITION 635 // 636 for (idx = 0; idx < len; idx++) { 637 item = items[idx]; 638 639 // if this item has fillWidth or fillHeight set, the row should expand 640 // laterally 641 if(!item.fillRow) shouldExpand = NO; 642 643 // if the item is not a fill-row item, this row has a size that all fill-row 644 // items should expand to 645 rowSize = Math.max(item.itemSize, rowSize); 646 647 item.position = position; 648 649 position += item.itemLength; 650 651 // if justification is on, we have one more spacer 652 // note that we check idx because position is used to determine the new rowLength. 653 if (align === SC.ALIGN_JUSTIFY && idx < len - 1) position += spacerSize; 654 } 655 656 row.shouldExpand = len > 0 ? shouldExpand : NO; 657 row.rowLength = position - row.plan.rowStartPadding; // row length does not include padding 658 row.rowSize = rowSize; 659 660 row.plan.totalCollapsedRowSize += row.rowSize; 661 662 }, 663 664 /** @private */ 665 _scfl_positionRows: function(plan) { 666 var rows = plan.rows, len = rows.length, idx, row, position, 667 fillRowCount = 0, planSize = 0, fillSpace; 668 669 // first, we need a count of rows that need to fill, and the size they 670 // are filling to (the combined size of all _other_ rows). 671 for (idx = 0; idx < len; idx++) { 672 if (rows[idx].shouldExpand) fillRowCount++; 673 planSize += rows[idx].rowSize; 674 } 675 676 fillSpace = plan.fitToPlanSize - planSize; 677 678 // now, position+size the rows 679 position = plan.planStartPadding; 680 for (idx = 0; idx < len; idx++) { 681 row = rows[idx]; 682 683 if (row.shouldExpand && fillSpace > 0) { 684 row.rowSize += fillSpace / fillRowCount; 685 fillRowCount--; 686 } 687 688 row.position = position; 689 position += row.rowSize; 690 } 691 }, 692 693 /** 694 @private 695 Positions all of the child views according to the plan. 696 */ 697 _scfl_applyPlan: function(plan) { 698 var rows = plan.rows, rowIdx, rowsLen, row, longestRow = 0, totalSize = 0, 699 items, itemIdx, itemsLen, item, layout, itemSize, 700 701 isVertical = plan.isVertical; 702 703 rowsLen = rows.length; 704 for (rowIdx = 0; rowIdx < rowsLen; rowIdx++) { 705 row = rows[rowIdx]; 706 longestRow = Math.max(longestRow, row.rowLength); 707 totalSize += row.rowSize; 708 709 items = row.items; itemsLen = items.length; 710 711 for (itemIdx = 0; itemIdx < itemsLen; itemIdx++) { 712 item = items[itemIdx]; 713 item.child.beginPropertyChanges(); 714 715 itemSize = item.fillRow ? row.rowSize : item.itemSize; 716 717 layout = { 718 left: item.spacing.left + (isVertical ? row.position : item.position), 719 top: item.spacing.top + (isVertical ? item.position : row.position), 720 width: isVertical ? itemSize : item.itemLength, 721 height: isVertical ? item.itemLength : itemSize 722 }; 723 724 layout.width -= item.spacing.left + item.spacing.right; 725 layout.height -= item.spacing.top + item.spacing.bottom; 726 727 this.applyPlanToView(item.child, layout); 728 item.child._scfl_lastLayout = layout; 729 730 item.child.endPropertyChanges(); 731 } 732 } 733 734 totalSize += plan.planStartPadding + plan.planEndPadding; 735 longestRow += plan.rowStartPadding + plan.rowEndPadding; 736 737 this.beginPropertyChanges(); 738 739 this.set('calculatedHeight', isVertical ? longestRow : totalSize); 740 this.set('calculatedWidth', isVertical ? totalSize : longestRow); 741 this.set('_scfl_maximumCollapsedRowLength', plan.maximumCollapsedRowLength); 742 this.set('_scfl_totalCollapsedRowSize', plan.totalCollapsedRowSize); 743 744 this.endPropertyChanges(); 745 }, 746 747 /** 748 Applies the given layout to the view. 749 Override this if you would like your view to, for example, animate to a new position. 750 */ 751 applyPlanToView: function(view, layout) { 752 view.adjust(layout); 753 }, 754 755 /** @private */ 756 _scfl_tileOnce: function() { 757 this.invokeLast(this._scfl_tile); 758 }, 759 760 _scfl_tile: function() { 761 // short circuit when hidden 762 if(!this.get('isVisibleInWindow')) return; 763 764 // first, do the plan 765 var plan = this._scfl_createPlan(); 766 this._scfl_distributeChildrenIntoRows(plan); 767 this._scfl_positionChildrenInRows(plan); 768 this._scfl_positionRows(plan); 769 this._scfl_applyPlan(plan); 770 771 // save so it can be observed 772 this.setIfChanged('numberOfRows', plan.rows.length); 773 774 // second, observe all children, and stop observing any children we no longer 775 // should be observing. 776 var previouslyObserving = this._scfl_isObserving || SC.CoreSet.create(), 777 nowObserving = this._scfl_isObserving = SC.CoreSet.create(); 778 779 var children = this.get('childViews'), len = children.length, idx, child; 780 for (idx = 0; idx < len; idx++) { 781 child = children[idx]; 782 783 if (!previouslyObserving.contains(child)) { 784 this.observeChildLayout(child); 785 } else { 786 previouslyObserving.remove(child); 787 } 788 789 nowObserving.add(child); 790 } 791 792 len = previouslyObserving.length; 793 for (idx = 0; idx < len; idx++) { 794 this.unobserveChildLayout(previouslyObserving[idx]); 795 } 796 }, 797 798 /** @private */ 799 _scfl_frameDidChange: function() { 800 var frame = this.get("frame"), lf = this._scfl_lastFrameSize || {}; 801 this._scfl_lastFrameSize = SC.clone(frame); 802 803 if (lf.width == frame.width && lf.height == frame.height) { 804 return; 805 } 806 807 this._scfl_tileOnce(); 808 }.observes('frame'), 809 810 /** @private */ 811 destroyMixin: function() { 812 this.removeObserver( 'childViews.[]', this, this._scfl_childViewsDidChange ); 813 814 var isObserving = this._scfl_isObserving; 815 if (!isObserving) return; 816 817 var len = isObserving.length, idx; 818 for (idx = 0; idx < len; idx++) { 819 this.unobserveChildLayout(isObserving[idx]); 820 } 821 }, 822 823 /** @private 824 Reorders childViews so that the passed views are at the beginning in the order they are passed. Needed because childViews are laid out in the order they appear in childViews. 825 */ 826 reorder: function(views) { 827 if(!SC.typeOf(views) === SC.T_ARRAY) views = arguments; 828 829 var i = views.length, childViews = this.childViews, view; 830 831 // childViews.[] should be observed 832 this.beginPropertyChanges(); 833 834 while(i-- > 0) { 835 view = views[i]; 836 837 if(SC.typeOf(view) === SC.T_STRING) view = this.get(view); 838 839 childViews.removeObject(view); 840 childViews.unshiftObject(view); 841 } 842 843 this.endPropertyChanges(); 844 845 this._scfl_childViewsDidChange(); 846 847 return this; 848 } 849 }; 850 851