Skip to content

Commit

Permalink
flexbox status: successy!
Browse files Browse the repository at this point in the history
  • Loading branch information
AprilArcus committed Mar 10, 2015
1 parent 010465a commit c2ec3c6
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 100 deletions.
9 changes: 7 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
<head>
<title>Carousel Demo for Patreon</title>
<style>
html, body {
height: 100%;
}
body {
background: linear-gradient(160deg, #009ee3 0%,#5e4e9c 100%);
margin: 0;
background: linear-gradient(160deg, #009ee3 0%,#5e4e9c 100%);
}
#carousel {
height: 100%;
height: 70%;
width: 80%;
margin: auto;
}
</style>
</head>
Expand Down
203 changes: 109 additions & 94 deletions scripts/components/Carousel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default React.createClass({
function rangeCheck(props, propName, componentName) {
if (props[propName] > props.numItems) {
return new Error('Carousel must be initialized with ' +
'enough items to fill every slot.')
'enough items to fill every slot.');
}
}

Expand All @@ -31,16 +31,8 @@ export default React.createClass({
return error;

},
slideDuration: React.PropTypes.number,
fullscreen: React.PropTypes.bool
},

validateSlots(props, propName, componentName) {
React.PropTypes.number(props, propName, componentName);
if (props[propName] > props.numItems) {
return new Error('Carousel may not be initialized with more slots' +
'than items')
}
respawnThreshold: React.PropTypes.number,
slideDuration: React.PropTypes.number
},

getDefaultProps() {
Expand Down Expand Up @@ -124,20 +116,22 @@ export default React.createClass({
},

handleGenericInteraction() {
this.setState({genericInteractions: this.state.genericInteractions + 1})
// don't respawn if the user's first interaction is with the
// 'clear' button!
if ((this.state.genericInteractions + 1) % this.props.respawnThreshold === 0) {
this.setState({genericInteractions: this.state.genericInteractions + 1});
if (this.state.genericInteractions % this.props.respawnThreshold === 0) {
CarouselActions.respawn();
}
event.stopPropagation();
},

handleReset() {
CarouselActions.reset(this.props.numItems);
},

handleClear() {
handleClear(event) {
CarouselActions.clear();
// don't let clicks on the 'clear' button bubble up to the generic
// interaction handler.
event.stopPropagation();
},

slideBackward() {
Expand Down Expand Up @@ -184,36 +178,44 @@ export default React.createClass({
// https://vimeo.com/channels/684289/116209150
staticStyles: {
container: {
display: 'table-row' // flexbox is the modern way to build
}, // this type of layout, but display: table
verticalAligner: { // and friends work adequately for this
display: 'table-cell', // case and enjoy ubiquitous support.
verticalAlign: 'middle'
},
overflowConcealer: {
display: 'table-cell', // 100% of container width after
width: '100%', // accounting for the navigational buttons
overflowX: 'hidden',
overflowY: 'hidden'
position: 'relative', // we'll position the reset and clear
display: 'flex', // buttons relative to the outer div
alignItems: 'center',

This comment has been minimized.

Copy link
@jimmylee

jimmylee Mar 15, 2015

Contributor

I also really like using vertical align: middle with display: inline-block to center objects as well. You can use :before to create an element of height: 100%; and then apply vertical-align: middle; to each element.

In addition, with inline-blocks you can also apply text-align: center; to center the child elements.

What I'm also trying to say is I'm glad you used flex haha

This comment has been minimized.

Copy link
@AprilArcus

AprilArcus Mar 15, 2015

Author Owner

Thanks for the feedback! The problem I set myself was to create an element whose width would be 100% the width of the enclosing box, minus allowances for the fixed-width endcaps:

 <----------------------100%--------------------->
|---endcap---|---------stock---------|---endcap---|
 <---70px--->                         <---70px--->

The reasoning for this is that the endcaps only need to be big enough to present an easy click target to the user, but after accounting for this, the images should be as large as possible for maximum visual appeal.

This particular responsive layout can't be achieved with display: inline-block; except by resorting to width: calc(100% - 140px);, since an inline-block's percentage width is always calculated as a fraction of its parent's width. The display: table-cell; method works fine for this, as does display: flex, but the latter has the additional advantage that it can also be a positioned element, which is important for positioning the left edge of the slider, its child div. IE received support for calc() in version 9 and flexbox in version 10, but since I also really wanted to make use of CSS transitions, another IE 10 feature, I felt that embracing flexbox and the clean markup that goes along with it would be the best decision.

This comment has been minimized.

Copy link
@jimmylee

jimmylee Mar 15, 2015

Contributor

I think you made the right choices! I totally follow what you were going for.

I hacked up the thought for fun on codepen using just HTML/CSS so I could play with the idea. Regardless I was just trying to say there are a lot of fun ways to experiment with the positioning.

I'm really enjoying going through the code you submitted. One interesting part being this: https://github.com/AprilArcus/carousel/blob/gh-pages/scripts/utils/bootstrapVariables.js - In the case where you don't get to start with a clean document, I think this technique can be an excellent way for new features to exist within legacy documents with a lot of legacy CSS (since all of the styles end up being inline styles).

I also imagine it providing people who have to create HTML/CSS in different contexts a lot of help, I know Marcos has to inline styles for his E-mails, if we had our styles in this technique + JSX this could make his life a lot easier.

This comment has been minimized.

Copy link
@AprilArcus

AprilArcus Mar 15, 2015

Author Owner

Yeah! In particular, since render() is a pure function, we can use these React classes to generate static HTML in Node. This obviously wouldn't work with the particular approach I took to handling vendor prefixing, but I have some ideas about patching React itself to handle vendor prefixing automatically that I'm planning to experiment with. If it works out I'll submit a pull request, and we'll have a new tool.

This comment has been minimized.

Copy link
@jimmylee

jimmylee Mar 15, 2015

Contributor

Thats definitely a win for server side rendering! I am a huge proponent of isomorphic applications. Having a Node/IO backend could help us do this easier.

If you got a pull request in with the solution to vendor prefixing that would be so rad, I imagine there is a wealth of attempts to solve the issue with vendor prefixing (both in JavaScript and CSS preprocessor land)

Thinking off of the top of my head for automatic vendor prefixing...

/* aligned to how you do this now, I imagine its all about getting 
a list of vendor prefixes we want. */
var objectProperties = {
    boxModel: {
        boxSizing: 'border-box',
        borderRadius: '3px'
    }
}

/* where we know we need to create these cases. 
CSS preprocessors have a great list of all of them (SASS, LESS) */
/*
    -webkit-box-sizing: *;
    -moz-box-sizing: *;
    box-sizing: *;
*/
var _prefixTable = {
    boxModel: {
        boxSizing: [
            '-webkit-box-sizing',
            '-moz-box-sizing',
            'box-sizing' 
        ],
        borderRadius: [
            '-webkit-border-radius',
            '-moz-border-radius',
            'border-radius'
        ]
    }
}

/* having some method to merge the objects!
sorry not the most performant or safe code here */
function mergePrefixes(properties) {
    var propertiesWithPrefixes = {};

    for (var property in properties) {
        var each = properties[property];
        propertiesWithPrefixes[property] = {};

        for (var type in each) {
            var value = each[type],
                prefixes = _prefixTable[property][type];

            propertiesWithPrefixes[property][type] = [];

            for (var i = 0; i < prefixes.length; i++) {
                propertiesWithPrefixes[property][type].push(prefixes[i] + ": " + value);
            }
        }
    }

    return propertiesWithPrefixes;
}

/* To produce all of the style properties to add to the style attribute. */
{
    boxModel: {
        boxSizing: [
            '-webkit-box-sizing: border-box',
            '-moz-box-sizing: border-box,
            'box-sizing: border-box'
        ],
        borderRadius: [
            '-webkit-box-radius: 3px',
            '-moz-box-radius: 3px',
            'box-radius: 3px'            
        ]
    }
}

Let me know how your experiments go, can't wait to talk more on Tuesday!

height: '100%', // occupy the full height of our parent
overflow: 'hidden' // rather than the natural height of the
}, // tallest carousel item
endCap: {
flexShrink: 0,
zIndex: 100
},
stock: {
flexGrow: 1,
display: 'flex',
alignItems: 'center',
position: 'relative' // the stock is the reference for
}, // the slider and the 'game over' message
slider: {
position: 'relative', // later, we will calculate how much to
whiteSpace: 'nowrap' // shift the slider relative to its parent
flexGrow: 1,
position: 'relative', // we will calculate how much to offset
whiteSpace: 'nowrap' // the slider in render()
},
item: {
display: 'inline-block'
messageContainer: {
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
},
gameOverVerticalAligner: { // flexbox would have obviated the
position: 'absolute', // 1 need for this non-semantic wrapper
top: '50%', // 2 div and these five numbered style
width: '100%' // 3 properties
},
gameOverText: {
position: 'absolute', // 4
top: '-0.5em', // 5
width: '100%',
message: {
textAlign: 'center',
fontSize: 50
fontSize: 50,
fontWeight: 100
},
item: {
display: 'inline-block'
},
leftArrow: {
width: 0,
Expand All @@ -236,8 +238,11 @@ export default React.createClass({
borderLeft: '15px solid black'
},
buttonGroup: {
textAlign: 'right'
},
position: 'absolute',
bottom: 4,
right: 4,
zIndex: 100 // setting negative zIndex on the carousel items
}, // breaks their onClick handlers.
button: { // fake Twitter Bootstrap button
display: 'inlineBlock',
webkitAppearance: 'button',
Expand All @@ -253,26 +258,33 @@ export default React.createClass({
},

render() {
// suppress render until the backing store is initialized
if (this.state.items === undefined) return <div />

const dynamicStyles = {};
const itemWidth = 100 / this.props.numSlots;
dynamicStyles.slider = Object.assign({},
this.staticStyles.slider
// {height: 0,
// paddingBottom: `${itemWidth}%`}
)
// allow our caller to set styles on us, provided they don't
// conflict with the ones we neeed.
dynamicStyles.container = Object.assign({},
this.props.style,
this.staticStyles.container)

// } else if (this.state.gameOver) {
// items = <div style={this.staticStyles.gameOverVerticalAligner}>
// <div style={this.staticStyles.gameOverText}>
// Game Over
// </div>
// </div>
// suppress render until the backing store is initialized
if (this.state.items === undefined) {
return <div style={dynamicStyles.container}/>;
}

let messageStyle;
if (this.state.gameOver) {
messageStyle = {transform: 'scale(1,1)',
transition: 'transform 450ms cubic-bezier(.4,1.4,.4,1)'};
} else {
messageStyle = {transform: 'scale(0,0)',
transition: 'none'};
}
dynamicStyles.message = Object.assign({},
this.staticStyles.message,
messageStyle);

// calculate the slider's left offset and supply an appropriate
// transition: ease while sliding, snap before re-render.
const itemWidth = 100 / this.props.numSlots;
let slidingStyle;
switch(this.state.sliding) {
case this.enums.sliding.FORWARD:
Expand All @@ -284,12 +296,10 @@ export default React.createClass({
transition: `left ${this.props.slideDuration}ms ease`};
break;
default:
slidingStyle = {left: `-${itemWidth}%`,
transition: 'none'};
slidingStyle = {left: `-${itemWidth}%`, transition: 'none'};
}
Object.assign(dynamicStyles.slider, slidingStyle);

// get the items we need and render them.
dynamicStyles.slider = Object.assign({}, this.staticStyles.slider, slidingStyle);
// get the items we need
dynamicStyles.item = Object.assign({},
this.staticStyles.item,
{width: `${itemWidth}%`});
Expand All @@ -299,10 +309,11 @@ export default React.createClass({
const circularized = Immutable.Repeat(withIndices).flatten(1);
const slice = circularized.slice(this.state.offsetIndex,
this.state.offsetIndex + this.props.numSlots + 2);
// render them
let items = slice.map( ([shape, storeIndex], sliceIndex) =>
<CarouselItem key={storeIndex + // a unique and
Math.floor(sliceIndex / // stable key saves
this.state.items.size)} // DOM operations
Math.floor(sliceIndex / // stable key
this.state.items.size)} // avoids redraws
index={storeIndex}
style={dynamicStyles.item}
hp={shape.hp}
Expand All @@ -311,41 +322,45 @@ export default React.createClass({
// iterables in JSX, but for now we must
// convert to the built-in Array type.

return <div style={{position: 'relative'}}
return <div style={this.staticStyles.container}
onClick={this.handleGenericInteraction}>
<div style={this.staticStyles.container}>
<div style={this.staticStyles.verticalAligner}>
<input type="button"
style={this.staticStyles.leftArrow}
disabled={this.state.sliding !==
this.enums.sliding.STOPPED}
onClick={this.slideBackward} />
<div style={this.staticStyles.endCap}>
<input type="button"
style={this.staticStyles.leftArrow}
disabled={this.state.sliding !==
this.enums.sliding.STOPPED}
onClick={this.slideBackward} />
</div>
<div style={this.staticStyles.stock}>
<div style={dynamicStyles.slider}>
{items}
</div>
<div style={this.staticStyles.overflowConcealer}>
<div style={dynamicStyles.slider}>
{items}
</div>
Game Over
</div>
<div style={this.staticStyles.verticalAligner}>
<input type="button"
style={this.staticStyles.rightArrow}
disabled={this.state.sliding !==
this.enums.sliding.STOPPED}
onClick={this.slideForward} />
<div style={this.staticStyles.messageContainer}>
<span style={dynamicStyles.message}>
{'Thanks for Playing!'}
<br />
{'April Arcus <3 Patreon'}
</span>
</div>
</div>
<div style={this.staticStyles.buttonGroup}>
<div style={this.staticStyles.endCap}>
<input type="button"
style={this.staticStyles.button}
value="Reset"
onClick={this.handleReset} />
<input type="button"
style={this.staticStyles.button}
value="Clear"
disabled={this.state.gameOver}
onClick={this.handleClear} />
style={this.staticStyles.rightArrow}
disabled={this.state.sliding !==
this.enums.sliding.STOPPED}
onClick={this.slideForward} />
</div>
<div style={this.staticStyles.buttonGroup}>
<input type="button"
style={this.staticStyles.button}
value="Reset"
onClick={this.handleReset} />
<input type="button"
style={this.staticStyles.button}
value="Clear"
disabled={this.state.gameOver}
onClick={this.handleClear} />
</div>
</div>;
}
});
5 changes: 3 additions & 2 deletions scripts/components/CarouselItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export default React.createClass({
generateRandomPolygonFromSeed(seed) {
const rng = seedrandom(seed);
const hue = Math.floor(rng() * 360);
const zIndex = Math.floor(rng() * 20);
const zIndex = Math.floor(rng() * 100);
const irregularity = rng() * 0.8;
const spikeyness = rng() * 0.6;
const numVerts = Math.floor(3 + rng() * 5);
Expand All @@ -121,7 +121,8 @@ export default React.createClass({
const outerStyle = Object.assign({},
this.props.style,
{position: 'relative',
zIndex: this.state.zIndex})
zIndex: this.state.zIndex
})

let shrink;
if (this.props.hp > 0) {
Expand Down
3 changes: 1 addition & 2 deletions scripts/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,4 @@ const React = require('react'),
React.render(<Carousel numSlots={6}
numItems={8}
respawnThreshold={12}
slideDuration={150}
fullscreen={true} />, document.getElementById('carousel'));
slideDuration={150} />, document.getElementById('carousel'));

0 comments on commit c2ec3c6

Please sign in to comment.