Skip to content

Instantly share code, notes, and snippets.

@publickeating
Created July 12, 2012 16:42
Show Gist options
  • Save publickeating/3099233 to your computer and use it in GitHub Desktop.
Save publickeating/3099233 to your computer and use it in GitHub Desktop.
SC.StackableChildViews Draft
/**
This mixin automatically positions the view's child views in a stack either
vertically or horizontally and adjusts the View's height or width respectively
to fit. It does this by checking the height or width of each child view
(depending on the direction of layout) and positioning the following child view
accordingly.
If the child view's frame changes, then the parent view will re-layout all of
the others to fit and re-adjust its width or height to fit as well.
A possible usage scenario is a long "form" view made of unique subsection
views. If we want to adjust the height of a subsection, to make space for an
error message for example, it would be a lot of work to manually reposition
all the other sections below it.
For example,
MyApp.MyView = SC.View.extend(SC.StackableChildViews, {
// Laid out from left to right.
direction: SC.LAYOUT_HORIZONTAL,
// Actual layout will be { left: 10, bottom: 20, top: 20, width: 270 }
layout: { left: 10, bottom: 20, top: 20 },
// Keep the child views ordered!
childViews: ['sectionA', 'sectionB', 'sectionC'],
sectionA: SC.View.design({
// Actual layout will be { left: 0, bottom: 0, top: 0, width: 100 }
layout: { width: 100 }
}),
sectionB: SC.View.design({
// Actual layout will be { left: 100, bottom: 0, top: 0, width: 50 }
layout: { width: 50 }
}),
sectionC: SC.View.design({
// Actual layout will be { left: 150, bottom: 0, top: 0, width: 120 }
layout: { width: 120 }
})
});
You can also specify values for the padding before the first child view,
`paddingBefore`, for the padding after the last child view, `paddingAfter` and
for the spacing between each child view, `spacing`.
For more control over the spacing between child views, you can provide
relevant margin properties on each child view. When the layout direction
is SC.LAYOUT_HORIZONTAL, then child views can adjust their automatic
position from left to right by providing marginLeft or marginRight. Likewise,
when the direction is SC.LAYOUT_VERTICAL, child views can override the
default spacing by providing marginTop or marginBottom.
For example,
MyApp.MyView = SC.View.extend(SC.StackableChildViews, {
// Laid out from left to right.
direction: SC.LAYOUT_HORIZONTAL,
// Actual layout will be { left: 10, bottom: 20, top: 20, width: 570 }
layout: { left: 10, bottom: 20, top: 20 },
// Keep the child views ordered!
childViews: ['sectionA', 'sectionB', 'sectionC'],
sectionA: SC.View.design({
// Actual layout will be { left: 0, bottom: 0, top: 0, width: 100 }
layout: { width: 100 },
// Force the following child view to be 200px further to the right.
marginRight: 200
}),
sectionB: SC.View.design({
// Actual layout will be { left: 200, bottom: 0, top: 0, width: 50 }
layout: { width: 50 }
}),
sectionC: SC.View.design({
// Actual layout will be { left: 450, bottom: 0, top: 0, width: 120 }
layout: { width: 120 },
// Force this child view to be 200px further from the previous.
marginLeft: 200
})
});
Finally, you can leave a child view out of automatic stacking by explicitly
specifying `isStackable` is false on the child view.
*/
SC.StackableChildViews = {
/**
The direction of layout, either SC.LAYOUT_HORIZONTAL or SC.LAYOUT_VERTICAL.
@default: SC.LAYOUT_VERTICAL
*/
direction: SC.LAYOUT_VERTICAL,
/**
Ignores changes to child views heights and visibilities when false.
If your child views are not going to change height or visibility, you
can improve performance by setting this to false in order to prevent the
view from observing its child views for changes.
@default true
*/
liveAdjust: true,
/**
Padding after the last child view.
@default: 0
*/
paddingAfter: 0,
/**
Padding before the first child view.
@default: 0
*/
paddingBefore: 0,
/**
The spacing between child views.
This is essentially the margins between each child view. It can be
overridden as needed by setting `marginBottom` and `marginTop` on a
child view when using SC.LAYOUT_VERTICAL direction or by setting
`marginLeft` and `marginRight` on a child view when using
SC.LAYOUT_HORIZONTAL direction.
Note that the spacing specified becomes the minimum margin between child
views, without explicitly overriding it from both sides. For example,
if `spacing` is 25, setting `marginBottom` to 10 on a child view will not
result in the next child view being 10px below it, unless the next child
view also specified `marginTop` as 10.
What this means is that it takes less configuration if you set `spacing` to
be the smallest margin you wish to exist and then use the overrides to
expand it. For example, if `spacing` is 5, setting `marginBottom` to 10
on a child view will result in the next child view being 10px below it,
without having to specify `marginTop` on the next child view.
@default: 0
*/
spacing: 0,
/** @private Whenever the childViews array changes, we need to change each layout. */
childViewsDidChange: function() {
this.layoutChildren();
},
/** @private */
destroyMixin: function() {
this.removeObserver('liveAdjust', this, this.liveAdjustDidChange);
this.removeObserver('childViews', this, this.childViewsDidChange);
},
/** @private */
initMixin: function() {
this.addObserver('liveAdjust', this, this.liveAdjustDidChange);
this.addObserver('childViews', this, this.childViewsDidChange);
this.childViewsDidChange();
},
/** @private */
layoutChildren: function() {
var childViews = this.get('childViews'),
direction = this.get('direction'),
marginBottom,
marginRight,
paddingAfter = this.get('paddingAfter'),
position = 0,
spacing = this.get('spacing');
marginBottom = marginRight = this.get('paddingBefore');
for (var i = 0, len = childViews.get('length'); i < len; i++) {
var childView = childViews.objectAt(i),
isStackable,
marginLeft,
marginTop;
// Ignore child views with isStackable false or that are not visible.
isStackable = childView.get('isStackable');
if (!SC.none(isStackable) && !isStackable) continue;
if (!childView.get('isVisible')) continue;
//@if(debug)
// Add some developer support.
var layout = childView.get('layout');
if (direction === SC.LAYOUT_VERTICAL && !SC.none(layout.bottom)) {
SC.warn('Developer Warning: Views that mix in SC.StackableChildViews for vertical layout may only define childView layouts for height with left and right or height with width and centerX!');
} else if (direction === SC.LAYOUT_HORIZONTAL && !SC.none(layout.right)) {
SC.warn('Developer Warning: Views that mix in SC.StackableChildViews for horizontal layout may only define childView layouts for width with top and bottom or width with height and centerY!');
}
//@endif
// Add observers on frame and isVisible if liveAdjust is set. If
// liveAdjust is unset, these observers will be removed in
// liveAdjustDidChange().
if (this.get('liveAdjust') && !childView.hasObserverFor('frame')) {
childView.addObserver('frame', this, this.layoutChildren);
childView.addObserver('isVisible', this, this.layoutChildren);
}
if (direction === SC.LAYOUT_VERTICAL) {
// Determine the top margin.
marginTop = childView.get('marginTop') || spacing;
position += Math.max(marginBottom, marginTop);
childView.adjust('top', position);
position += childView.getPath('frame.height');
// Determine the bottom margin.
marginBottom = childView.get('marginBottom') || spacing;
} else {
// Determine the left margin.
marginLeft = childView.get('marginLeft') || spacing;
position += Math.max(marginRight, marginLeft);
childView.adjust('left', position);
position += childView.getPath('frame.width');
// Determine the right margin.
marginRight = childView.get('marginRight') || spacing;
}
}
// Adjust our frame to fit as well, this ensures that SC.ScrollView works.
if (direction === SC.LAYOUT_VERTICAL) {
paddingAfter = Math.max(marginBottom, paddingAfter);
this.adjust('height', position + paddingAfter);
} else {
paddingAfter = Math.max(marginRight, paddingAfter);
this.adjust('width', position + paddingAfter);
}
},
/** @private */
liveAdjustDidChange: function() {
if (this.get('liveAdjust')) {
// If liveAdjust gets set to true, auto adjust child views.
this.layoutChildren();
} else {
// Else, remove live adjust observers from the child views.
var childViews = this.get('childViews');
for (var i = 0, len = childViews.get('length'); i < len; i++) {
var childView = childViews.objectAt(i);
if (childView.hasObserverFor('frame')) {
childView.removeObserver('frame', this, this.layoutChildren);
childView.removeObserver('isVisible', this, this.layoutChildren);
}
}
}
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment