HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
Any URLs added here will be added as <link>
s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Search for and use JavaScript packages from npm here. By selecting a package, an import
statement will be added to the top of the JavaScript editor for this package.
Using packages here is powered by esm.sh, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ESM usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
.container(style="margin-bottom:40px;")
.row
.col-sm-12
h2 Stacked card shuffle
.card-shuffle
.relative
.card-shuffle__card(data-position="0")
.container.height-inherit
.row.height-inherit
.col-sm-12.height-inherit
.card-shuffle__card-inner.green.height-inherit.grab
.card-shuffle__card(data-position="1")
.container.height-inherit
.row.height-inherit
.col-sm-12.height-inherit
.card-shuffle__card-inner.red.height-inherit.grab
.card-shuffle__card(data-position="2")
.container.height-inherit
.row.height-inherit
.col-sm-12.height-inherit
.card-shuffle__card-inner.blue.height-inherit.grab
.card-shuffle__card(data-position="3")
.container.height-inherit
.row.height-inherit
.col-sm-12.height-inherit
.card-shuffle__card-inner.orange.height-inherit.grab
body {
background-color: #ECF0F1;
}
// Easing
$easeOutQuart: cubic-bezier(0.165, 0.840, 0.440, 1.000);
$easeOutExpo: cubic-bezier(0.190, 1.000, 0.220, 1.000);
.blue { background-color: #3498DB; }
.red { background-color: #E74C3C; }
.orange { background-color: #E67E22; }
.green { background-color: #2ECC71; }
.relative { position: relative; }
.height-inherit { height: inherit; }
.grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.grab.grabbing,
.grabbing,
.grabbing .grab,
.gxRangeHandle:active {
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
.container {
max-width: 1200px;
}
@media (min-width: 768px) {
.container {
width: 93%;
}
}
$stackedAdvanceDuration: .1s;
$stackedOffDeckDuration: .3s;
$stackedToBackDuration: .15s;
.card-shuffle {
position: relative;
padding-bottom: 20px;
padding-top: 20px;
height: 320px;
width: 100%;
overflow: hidden;
}
.card-shuffle__card {
position: absolute;
z-index: 0;
top: 0;
left: 0;
width: 100%;
height: 275px;
transform: translateZ(0);
transition: transform $stackedAdvanceDuration $easeOutQuart;
}
.card-shuffle__card-inner {
border-radius: 2px;
box-shadow: 0 0 4px 1px rgba(0, 0, 0, 0.2);
}
$skews: (-1.0deg, 1.5deg, -0.6deg, 0.8deg, -1.5deg);
@for $i from 1 through length($skews) {
.card-shuffle__card[data-position="#{$i - 1}"] {
z-index: length($skews) - $i;
}
.card-shuffle__card:nth-child(#{$i}) {
transform: rotate( nth($skews, $i) );
}
}
.card-shuffle__card[data-position="0"] {
transition-duration: $stackedToBackDuration;
}
.card-shuffle__card.card-shuffle__card--front {
transform: rotate(0);
}
// To bottom of the deck.
.card-shuffle__card.card-shuffle__card--to-bottom {
z-index: 10;
transition-duration: $stackedOffDeckDuration;
transition-timing-function: $easeOutExpo;
}
.card-shuffle__card.card-shuffle--no-transition {
transition-duration: 0ms;
}
@media (max-width: 568px) {
$skews: (-2.4deg, 2.5deg, -3.0deg, 2.8deg, -2.5deg);
@for $i from 1 through length($skews) {
.card-shuffle__card:nth-child(#{$i}) {
transform: rotate( nth($skews, $i) );
}
}
.card-shuffle__card.card-shuffle__card--front {
transform: rotate(0);
}
}
@media (max-width: 767px) {
.card-shuffle__card.card-shuffle__card--off-deck-end {
transform: translate( calc(100% - 30px), 0 );
}
.card-shuffle__card.card-shuffle__card--off-deck-start {
transform: translate( calc(-100% + 30px), 0 );
}
}
@media (min-width: 768px) {
.card-shuffle__card.card-shuffle__card--off-deck-end {
transform: translate( calc(93% - 30px), 0 );
}
.card-shuffle__card.card-shuffle__card--off-deck-start {
transform: translate( calc(-93% + 30px), 0 );
}
}
@media (min-width: (1200px / 0.93)) {
.card-shuffle__card.card-shuffle__card--off-deck-end {
transform: translate( calc(1200px - 30px), 0 );
}
.card-shuffle__card.card-shuffle__card--off-deck-start {
transform: translate( calc(-1200px + 30px), 0 );
}
}
define('app/device',['require','modernizr'],function(require) {
var Modernizr = require('modernizr');
var Device = {};
/**
* Hyphenates a javascript style string to a css one. For example:
* MozBoxSizing -> -moz-box-sizing.
*
* @param {string|boolean} str The string to hyphenate.
* @return {string} The hyphenated string.
*/
Device.hyphenate = function(str) {
// Catch booleans.
if (!str) {
return '';
}
// Turn MozBoxSizing into -moz-box-sizing.
return str.replace(/([A-Z])/g, function(str, m1) {
return '-' + m1.toLowerCase();
}).replace(/^ms-/, '-ms-');
};
/**
* Object Model prefixes.
* @type {string}
* @private
*/
Device.omPrefixes_ = 'Webkit Moz O ms';
/**
* CSS Object Model prefixes.
* @type {Array.<string>}
* @private
*/
Device.cssomPrefixes_ = Device.omPrefixes_.split(' ');
/**
* Document Object Model prefixes.
* @type {Array.<string>}
* @private
*/
Device.domPrefixes_ = Device.omPrefixes_.toLowerCase().split(' ');
/**
* Prefix lookup cache.
* @type {Object}
* @private
*/
Device.prefixCache_ = {};
/**
* @param {*} obj Anything.
* @return {boolean}
*/
var isString = function(obj) {
return typeof obj === 'string';
};
/**
* Capitalize a string.
* @param {string} str String to capitalize.
* @return {string} Capitalized string.
*/
var capitalize = function(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
};
/**
* Returns the prefixed style property if it exists.
* Mimics of Modernizr.prefixed. So Device.prefixed('transform') should
* return 'WebkitTransform' for Chrome and 'transform' for FF and IE10.
* {@link http://perfectionkills.com/feature-testing-css-properties/}
*
* @param {string} propName The property name.
* @param {Element=} opt_element Element to test. Defaults to html element.
* @return {string|boolean} The style property or false.
*/
Device.prefixed = function(propName, opt_element) {
opt_element = opt_element || document.documentElement;
var style = opt_element.style,
cache = Device.prefixCache_,
prefixes = Device.cssomPrefixes_,
prefixed,
uPropName;
// check cache only when no element is given.
if (arguments.length === 1 && isString(cache[propName])) {
return cache[propName];
}
// test standard property first.
if (isString(style[propName])) {
return (cache[propName] = propName);
}
// capitalize.
uPropName = capitalize(propName);
// test vendor specific properties.
for (var i = 0, l = prefixes.length; i < l; i++) {
prefixed = prefixes[i] + uPropName;
if (isString(style[prefixed])) {
return (cache[propName] = prefixed);
}
}
return false;
};
/**
* Prefixed style properties.
* @enum {string|boolean}
*/
Device.Js = {
TRANSFORM: Device.prefixed('transform'),
TRANSITION: Device.prefixed('transition'),
TRANSITION_PROPERTY: Device.prefixed('transitionProperty'),
TRANSITION_DURATION: Device.prefixed('transitionDuration'),
TRANSITION_TIMING_FUNCTION: Device.prefixed('transitionTimingFunction'),
TRANSITION_DELAY: Device.prefixed('transitionDelay')
};
/**
* Prefixed css properties.
* @enum {string}
*/
Device.Css = {
TRANSFORM: Device.hyphenate(Device.Js.TRANSFORM),
TRANSITION: Device.hyphenate(Device.Js.TRANSITION),
TRANSITION_PROPERTY: Device.hyphenate(Device.Js.TRANSITION_PROPERTY),
TRANSITION_DURATION: Device.hyphenate(Device.Js.TRANSITION_DURATION),
TRANSITION_TIMING_FUNCTION: Device.hyphenate(
Device.Js.TRANSITION_TIMING_FUNCTION),
TRANSITION_DELAY: Device.hyphenate(Device.Js.TRANSITION_DELAY)
};
/**
* Whether the browser has css transitions.
* @type {boolean}
*/
Device.HAS_TRANSITIONS = Modernizr.csstransitions;
/**
* Whether the browser has css transitions.
* @type {boolean}
*/
Device.HAS_TRANSFORMS = Modernizr.csstransforms;
/**
* The browser can use css transitions and transforms.
* @type {boolean}
*/
Device.CAN_TRANSITION_TRANSFORMS = Device.HAS_TRANSITIONS &&
Device.HAS_TRANSFORMS;
/**
* The browser can use 3d css transforms.
* https://github.com/Modernizr/Modernizr/blob/master/feature-detects/css/transforms3d.js
* @type {boolean}
*/
Device.HAS_3D_TRANSFORMS = Modernizr.csstransforms3d;
/**
* Whether the browser supports touch events.
* @type {boolean}
*/
Device.HAS_TOUCH_EVENTS = ('ontouchstart' in window) ||
!!window.DocumentTouch && document instanceof DocumentTouch;
/**
* Whether the browser supports pointer events.
* http://blogs.windows.com/windows_phone/b/wpdev/archive/2012/11/15/adapting-your-webkit-optimized-site-for-internet-explorer-10.aspx
* @type {boolean}
*/
Device.HAS_POINTER_EVENTS = !!navigator.pointerEnabled ||
!!navigator.msPointerEnabled;
return Device;
});
define('app/helpers',['require','jquery','app/device', 'modernizr', 'app/event-type'],function(require) {
var $ = require('jquery');
var Device = require('app/device');
var Modernizr = require('modernizr');
var EventType = require('app/event-type');
var Helpers = {};
/** @enum {string} */
Helpers.ClassName = {
HIDDEN: 'hidden',
FADE: 'fade',
IN: 'in',
INVISIBLE: 'invisible',
ACTIVE: 'active',
GRAB: 'grab',
GRABBING: 'grabbing'
};
// Polyfill Object.create.
if (!Object.create) {
Object.create = (function() {
function F(){}
return function(o) {
if (arguments.length !== 1) {
throw new Error(
'Object.create implementation only accepts one parameter.');
}
F.prototype = o;
return new F();
};
})();
}
Helpers.inherits = function(child, parent) {
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
/**
* Taken from Closure :)
*
* Calls superclass constructor/method.
*
* This function is only available if you use goog.inherits to
* express inheritance relationships between classes.
*
* NOTE: This is a replacement for goog.base and for superClass_
* property defined in child.
*
* @param {!Object} me Should always be "this".
* @param {string} methodName The method name to call. Calling
* superclass constructor can be done with the special string
* 'constructor'.
* @param {...*} var_args The arguments to pass to superclass
* method/constructor.
* @return {*} The return value of the superclass method/constructor.
*/
child.base = function(me, methodName, var_args) {
var args = Array.prototype.slice.call(arguments, 2);
return parent.prototype[methodName].apply(me, args);
};
};
/**
* Capitalize a string.
* @param {string} str String to capitalize.
* @return {string} Capitalized string.
*/
Helpers.capitalize = function(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
};
// Math utilities.
/**
* Clone of jQuery.isNumeric {@link https://api.jquery.com/jQuery.isNumeric}
* Because goog.isNumber only checks if the type is a number, so NaN passes.
* @param {*} obj Object to test.
* @return {!boolean} Whether the given object is a number.
*/
Helpers.isNumeric = function(obj) {
return !isNaN(parseFloat(obj)) && isFinite(obj);
};
/**
* Takes a number and clamps it to within the provided bounds.
* @param {number} value The input number.
* @param {number} min The minimum value to return.
* @param {number} max The maximum value to return.
* @return {number} The input number if it is within bounds, or the nearest
* number within the bounds.
*/
Helpers.clamp = function(value, min, max) {
return Math.min(Math.max(value, min), max);
};
/**
* Class for representing a box. A box is specified as a top, right, bottom,
* and left. A box is useful for representing margins and padding.
*
* @param {number} top Top.
* @param {number} right Right.
* @param {number} bottom Bottom.
* @param {number} left Left.
* @constructor
*/
Helpers.Box = function(top, right, bottom, left) {
/**
* Top
* @type {number}
*/
this.top = top;
/**
* Right
* @type {number}
*/
this.right = right;
/**
* Bottom
* @type {number}
*/
this.bottom = bottom;
/**
* Left
* @type {number}
*/
this.left = left;
};
/**
* Class for representing rectangular regions.
* @param {number} x Left.
* @param {number} y Top.
* @param {number} w Width.
* @param {number} h Height.
* @constructor
*/
Helpers.Rect = function(x, y, w, h) {
/**
* Left
* @type {number}
*/
this.left = x;
/**
* Top
* @type {number}
*/
this.top = y;
/**
* Width
* @type {number}
*/
this.width = w;
/**
* Height
* @type {number}
*/
this.height = h;
};
// Style utilities
/**
* Gets the height and with of an element when the display is not none.
* @param {Element} element Element to get size of.
* @return {!{width: number, height: number}} Object with width/height.
*/
Helpers.getSize = function(element) {
var offsetWidth = element.offsetWidth;
var offsetHeight = element.offsetHeight;
if (offsetWidth === undefined && element.getBoundingClientRect) {
// Fall back to calling getBoundingClientRect when offsetWidth or
// offsetHeight are not defined.
var clientRect = element.getBoundingClientRect();
return {
width: clientRect.right - clientRect.left,
height: clientRect.bottom - clientRect.top
};
}
return {
width: offsetWidth,
height: offsetHeight
};
};
Helpers._getBox = function(element, property) {
var props = $(element).css([
property + 'Top',
property + 'Right',
property + 'Left',
property + 'Bottom'
]);
return new Helpers.Box(
Helpers.getFloat(props[property + 'Top']),
Helpers.getFloat(props[property + 'Right']),
Helpers.getFloat(props[property + 'Left']),
Helpers.getFloat(props[property + 'Bottom']));
};
Helpers.getFloat = function(value) {
return parseFloat(value) || 0;
};
Helpers.getMarginBox = function(element) {
return Helpers._getBox(element, 'margin');
};
/**
* Returns a string to be used with transforms. Uses 3d translates
* when available.
* @param {string=} opt_x The x position value with units. Default is zero.
* @param {string=} opt_y The y position value with units. Default is zero.
* @return {string} The css value for transform.
*/
Helpers.getTranslateString = function(opt_x, opt_y) {
var x = opt_x !== undefined ? opt_x : 0;
var y = opt_y !== undefined ? opt_y : 0;
var prefix = 'translate';
var suffix = ')';
if (Device.HAS_3D_TRANSFORMS) {
prefix += '3d(';
suffix = ',0' + suffix;
} else {
prefix += '(';
}
return prefix + x + ',' + y + suffix;
};
Helpers.onTransitionEnd = function( elem, fn, context, opt_property ) {
// transitioned and ignore others.
if ( elem.jquery ) {
elem = elem[0];
}
var callback = $.proxy(fn, context || window);
var fakeEvent = {
target: elem,
currentTarget: elem,
fake: true
};
/**
* @param {$.Event|{target: Element, currentTarget: Element}} evt Event object.
*/
function transitionEnded(evt) {
var source = evt.currentTarget;
// Some other element's transition event could have bubbled up to this.
if (!source || source !== evt.target) {
return;
}
// If the browser has transitions, there will be a listener bound to the
// `transitionend` event which needs to be removed. `listenOnce` is not used
// because transition events can bubble up to the parent.
if (Modernizr.csstransitions) {
// If the optional property exists and it's not the property which was
// transitioned, exit out of the function and continue waiting for the
// right transition property.
if (opt_property && !evt.fake && evt.originalEvent.propertyName !== opt_property) {
return;
}
$(source).off(EventType.TRANSITIONEND, transitionEnded);
}
// Done!
callback(evt);
}
if (Modernizr.csstransitions) {
$(elem).on(EventType.TRANSITIONEND, transitionEnded);
// TODO(glen): Get length of transition and set a timeout as a backup.
// The transition will not happen if the values don't change on the element,
// the timeout would be a failsafe for that.
} else {
// Push to the end of the queue with a fake event which will pass the checks
// inside the callback function.
setTimeout($.proxy(transitionEnded, window, fakeEvent), 0);
}
};
return Helpers;
});
/**
* @fileoverview The base abstract component providing easy-to-use things for
* working with dom and other services.
*/
define('app/base-component',['require','jquery','modernizr','app/helpers'],function(require) {
var $ = require('jquery');
var Modernizr = require('modernizr');
var Helpers = require('app/helpers');
/**
* The base class for modules.
* @param {Element} element Main element of the module.
* @param {boolean} addListener Whether to listen for the 767|768 breakpoint.
* @constructor
*/
function BaseComponent( element, addListener ) {
if ( element ) {
this.$el = $( element );
this.element = element;
}
if ( addListener ) {
/**
* Whether the screen is smaller than 768px or not.
* @type {boolean}
*/
this.isSmallScreen = false;
if ( Modernizr.mediaqueries ) {
this._mql = window.matchMedia('(max-width: 47.9375em)');
this._mqlListener = this.handleMediaQueryChange.bind( this );
this.isSmallScreen = this._mql.matches;
}
}
}
BaseComponent.prototype.decorateInternal = function() {};
BaseComponent.prototype.enterDocument = function() {};
BaseComponent.prototype.getElement = function() {
return this.element;
};
/**
* Listen for events.
*/
BaseComponent.prototype.listen = function() {
// Listen for changes across 767|768.
if ( this._mql ) {
this._mql.addListener( this._mqlListener );
}
};
/**
* Finds an element within this class' main element.
* @param {string} selector Selector.
* @param {jQuery|Element} [context] Optionally provide the context (scope)
* for the query. Default is the main element of the class.
* @return {jQuery} A jQuery object which may or may not contain the element
* which was searched for.
*/
BaseComponent.prototype.findBySelector = function( selector, context ) {
return $( selector, context || this.$el );
};
/**
* Finds an element within this class' main element.
* @param {string} className Class name to search for.
* @param {jQuery|Element} [context] Optionally provide the context (scope)
* for the query. Default is the main element of the class.
* @return {jQuery} A jQuery object which may or may not contain the element
* which was searched for.
*/
BaseComponent.prototype.findByClass = function( className, context ) {
return this.findBySelector( '.' + className, context );
};
BaseComponent.prototype.getElementByClass = function(className, context) {
return this.findByClass(className, context).get(0) || null;
};
BaseComponent.prototype.getElementsByClass = function(className, context) {
return this.findByClass(className, context).get();
};
/**
* Retieves elements from the children of a parent which match the given
* class. This function is useful when an element has the same class name
* nested deeper within the element that is needed.
* @param {string} className The classname to match against.
* @param {Element} parent The element whose children will be filtered.
* @return {!Array.<Element>} Direct descendants of the parent by class.
*/
BaseComponent.prototype.getDirectDescendantsByClass = function(
className, parent) {
return $(parent).children('.' + className).get();
};
BaseComponent.prototype.getParentByClass = function(className, child,
opt_context) {
var context = opt_context || this.element;
return $(child).closest('.' + className, context).get(0) || null;
};
/**
* Cleanup DOM references and event listeners.
*/
BaseComponent.prototype.dispose = function() {
if ( this._mql ) {
this._mql.removeListener( this._mqlListener );
this._mql = null;
}
if ( this.$el ) {
this.$el = null;
this.element = null;
}
};
/**
* Triggers an event on the class instance.
* @param {string|jQuery.Event} eventName Name of the event to trigger or
* an event instance.
* @param {Array.<*>} [args] Optional arguments to send with the event.
* @return {?boolean} If the event name is an event instance, this function
* returns whether or not the event was prevented using preventDefault().
*/
BaseComponent.prototype.dispatchEvent = function(eventName, args) {
if ($.type(eventName) === 'string') {
$(this).trigger( eventName, args && args.length ? args : [ this ] );
return null;
} else {
$(this).trigger(eventName); // undefined not a function :(
return eventName.isDefaultPrevented();
}
};
/**
* Return the media query list.
* @return {MediaQueryList}
* @protected
*/
BaseComponent.prototype.getMediaQueryList = function() {
return this._mql;
};
/**
* Media query changed. Ideally this should be on some kind of base component which
* all modules inherit from.
* @param {MediaQueryList} mediaQueryList The media query list object.
* @protected
*/
BaseComponent.prototype.handleMediaQueryChange = function( mediaQueryList ) {
this.isSmallScreen = mediaQueryList.matches;
};
return BaseComponent;
});
/**
* @fileoverview A utility class for representing two-dimensional positions.
*/
define('app/coordinate',[],function() {
/**
* Class for representing coordinates and positions.
* @param {number=} opt_x Left, defaults to 0.
* @param {number=} opt_y Top, defaults to 0.
* @constructor
*/
var Coordinate = function(opt_x, opt_y) {
/**
* X-value
* @type {number}
*/
this.x = opt_x !== undefined ? opt_x : 0;
/**
* Y-value
* @type {number}
*/
this.y = opt_y !== undefined ? opt_y : 0;
};
/**
* Returns the distance between two coordinates.
* @param {!Coordinate} a A Coordinate.
* @param {!Coordinate} b A Coordinate.
* @return {number} The distance between {@code a} and {@code b}.
*/
Coordinate.distance = function(a, b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
};
return Coordinate;
});
define('app/event-type',['app/device'], function(Device) {
// https://github.com/Modernizr/Modernizr/blob/master/src/prefixed.js
var transEndEventNames = {
'WebkitTransition': 'webkitTransitionEnd', // Saf 6, Android Browser
'MozTransition': 'transitionend', // only for FF < 15
'transition': 'transitionend' // IE10, Opera, Chrome, FF 15+, Saf 7+
};
var hasUnprefixedPointerEvents = !!navigator.pointerEnabled;
function getPointerEvent(event) {
if (Device.HAS_POINTER_EVENTS) {
if (hasUnprefixedPointerEvents) {
return event.toLowerCase();
} else {
return 'MS' + event;
}
} else {
return null;
}
}
return {
// Mouse events
CLICK: 'click',
MOUSEDOWN: 'mousedown',
MOUSEUP: 'mouseup',
MOUSEOVER: 'mouseover',
MOUSEOUT: 'mouseout',
MOUSEMOVE: 'mousemove',
// WebKit touch events.
TOUCHSTART: 'touchstart',
TOUCHMOVE: 'touchmove',
TOUCHEND: 'touchend',
TOUCHCANCEL: 'touchcancel',
POINTERCANCEL: getPointerEvent('PointerCancel'),
POINTERDOWN: getPointerEvent('PointerDown'),
POINTERMOVE: getPointerEvent('PointerMove'),
POINTEROVER: getPointerEvent('PointerOver'),
POINTEROUT: getPointerEvent('PointerOut'),
POINTERUP: getPointerEvent('PointerUp'),
TRANSITIONEND: transEndEventNames[Device.Js.TRANSITION]
};
});
/**
* @fileoverview A draggable class which uses translate when
* available and can use a containing element instead of specific boundaries.
* It also emits events with more useful information for swipes, like velocity,
* distance, and direction.
*/
define('app/draggable',['require','jquery','app/helpers','app/base-component','app/coordinate','app/device','app/event-type'],function(require) {
var $ = require('jquery');
var Helpers = require('app/helpers');
var BaseComponent = require('app/base-component');
var Coordinate = require('app/coordinate');
var Device = require('app/device');
var EventType = require('app/event-type');
/**
* A class that allows mouse or touch-based dragging (moving) of an element.
*
* @param {Element} dragElement The element that will be dragged.
* @param {Element} containment The element which will contain the draggable
* element.
* @param {string=} opt_axis The axis to drag on. Defaults to both. Should be
* "x" or "y" to constrain to an axis.
* @extends {BaseComponent}
* @constructor
*/
var Draggable = function(dragElement, containment, opt_axis, opt_limits) {
Draggable.base(this, 'constructor', dragElement);
/**
* The draggable element.
* @type {Element}
* @private
*/
this.el_ = dragElement;
/**
* The element which contains the target.
* @type {Element}
* @private
*/
this.containerEl_ = containment;
/**
* Object to keep track of the current position of the handle/target.
* @type {Coordinate}
* @private
*/
this.currentPosition_ = new Coordinate();
/**
* Object to keep track of the where the starting location for dragging was.
* Relative to the draggable element.
* @type {Coordinate}
* @private
*/
this.startPosition_ = new Coordinate();
/**
* Current x position of mouse or touch relative to the document.
* @type {number}
*/
this.pageX = 0;
/**
* Current y position of mouse or touch relative to the document.
* @type {number}
*/
this.pageY = 0;
/**
* The x position where the first mousedown or touchstart occurred.
* @type {number}
*/
this.startX = 0;
/**
* The y position where the first mousedown or touchstart occurred.
* @type {number}
*/
this.startY = 0;
/**
* Current x position of drag relative to target's parent.
* @type {number}
*/
this.deltaX = 0;
/**
* Current y position of drag relative to target's parent.
* @type {number}
*/
this.deltaY = 0;
/**
* Limits of how far the draggable element can be dragged.
* @type {Helpers.Rect}
*/
this.limits = opt_limits || new Helpers.Rect(NaN, NaN, NaN, NaN);
/**
* Friction to apply to dragging. A value of zero would result in no dragging,
* 0.5 would result in the draggable element moving half as far as the user
* dragged, and 1 is a 1:1 ratio with user movement.
* @type {number}
*/
this.friction_ = 1;
/**
* Draggable axis.
* @type {string}
* @private
*/
this.axis_ = opt_axis || Draggable.Axis.BOTH;
/**
* Flag indicating dragging has happened. It is set on dragmove and reset
* after the draggableend event has been dispatched.
* @type {boolean}
*/
this.hasDragged = false;
/**
* Whether the user is locked in place within the draggable element. This
* is set to true when `preventDefault` is called on the move event.
* @type {boolean}
* @private
*/
this.isLocked_ = false;
/**
* Whether dragging is enabled internally. If the user attempts to scroll
* in the opposite direction of the draggable element, this is set to true
* and no more drag move events are counted until the user releases and
* starts dragging again.
* @type {boolean}
* @private
*/
this.isDeactivated_ = false;
/**
* Whether dragging is currently enabled.
* @type {boolean}
* @private
*/
this.enabled_ = true;
var $el = $(this.el_);
// Namespace
var ns = '.draggable';
if (Device.HAS_POINTER_EVENTS) {
$el.on(EventType.POINTERDOWN + ns, this.handleDragStart_.bind(this));
} else {
$el.on(EventType.MOUSEDOWN + ns, this.handleDragStart_.bind(this));
if (Device.HAS_TOUCH_EVENTS) {
$el.on(EventType.TOUCHSTART + ns, this.handleDragStart_.bind(this));
}
}
};
Helpers.inherits(Draggable, BaseComponent);
/** @enum {string} */
Draggable.EventType = {
START: 'draggablestart',
MOVE: 'draggablemove',
END: 'draggableend'
};
/** @enum {string} */
Draggable.Direction = {
RIGHT: 'right',
LEFT: 'left',
UP: 'up',
DOWN: 'down',
NONE: 'no_movement'
};
/** @enum {string} */
Draggable.Axis = {
X: 'x',
Y: 'y',
BOTH: 'xy'
};
/**
* The scroll/drag amount (pixels) required on the draggable axis before
* stopping further page scrolling/movement.
* @const {number}
*/
Draggable.LOCK_THRESHOLD = 6;
/**
* The scroll/drag amount (pixels) required on the opposite draggable axis
* before dragging is deactivated for the rest of the interaction.
* @const {number}
*/
Draggable.DRAG_THRESHOLD = 5;
/**
* Calculate the velocity between two points.
*
* @param {number} deltaTime Change in time.
* @param {number} deltaX Change in x.
* @param {number} deltaY Change in y.
* @return {Object} Velocity of the drag.
*/
Draggable.getVelocity = function(deltaTime, deltaX, deltaY) {
var velocityX = Math.abs(deltaX / deltaTime);
var velocityY = Math.abs(deltaY / deltaTime);
return {
x: isFinite(velocityX) ? velocityX : 0,
y: isFinite(velocityY) ? velocityY : 0
};
};
/**
* angle to direction define.
* @param {Coordinate} coord1 The starting coordinate.
* @param {Coordinate} coord2 The ending coordinate.
* @return {string} Direction constant.
*/
Draggable.getDirection = function(coord1, coord2) {
var x = Math.abs(coord1.x - coord2.x);
var y = Math.abs(coord1.y - coord2.y);
if (x >= y) {
return coord1.x - coord2.x > 0 ?
Draggable.Direction.LEFT :
coord1.x - coord2.x < 0 ?
Draggable.Direction.RIGHT :
Draggable.Direction.NONE;
} else {
return coord1.y - coord2.y > 0 ?
Draggable.Direction.UP :
coord1.y - coord2.y < 0 ?
Draggable.Direction.DOWN :
Draggable.Direction.NONE;
}
};
/**
* Saves the containment element's width and height and scrubber position.
* @private
*/
Draggable.prototype.setDimensions_ = function() {
var containmentRect = this.containerEl_.getBoundingClientRect();
var elRect = this.el_.getBoundingClientRect();
var relativeElement = Device.CAN_TRANSITION_TRANSFORMS ?
this.el_ :
this.containerEl_;
// getBoundingClientRect does not include margins. They must be accounted for.
var margins = Helpers.getMarginBox(this.el_);
var offsetCorrectionX = margins.left;
var offsetCorrectionY = margins.top;
offsetCorrectionX += containmentRect.left;
offsetCorrectionY += containmentRect.top;
this.containmentWidth_ = relativeElement.offsetWidth;
this.containmentHeight_ = relativeElement.offsetHeight;
if (this.containmentWidth_ === 0) {
throw new Error('containing element\'s width is zero');
} else if (this.containmentHeight_ === 0) {
throw new Error('containing element\'s height is zero');
}
this.startPosition_.x = elRect.left - offsetCorrectionX;
this.startPosition_.y = elRect.top - offsetCorrectionY;
};
/**
* Get whether dragger is enabled
* @return {boolean} Whether dragger is enabled.
*/
Draggable.prototype.getEnabled = function() {
return this.enabled_;
};
/**
* Set whether dragger is enabled
* @param {boolean} enabled Whether dragger is enabled.
*/
Draggable.prototype.setEnabled = function(enabled) {
this.enabled_ = enabled;
};
/**
* Sets (or reset) the Drag limits after a Dragger is created.
* @param {Helpers.Rect} limits Object containing left, top, width,
* height for new Dragger limits.
*/
Draggable.prototype.setLimits = function(limits) {
this.limits = limits || new Helpers.Rect(NaN, NaN, NaN, NaN);
};
/**
* Returns the 'real' x after limits are applied (allows for some
* limits to be undefined).
* @param {number} x X-coordinate to limit.
* @return {number} The 'real' X-coordinate after limits are applied.
*/
Draggable.prototype.limitX = function(x) {
var rect = this.limits;
var left = isNaN(rect.left) ? null : rect.left;
var width = isNaN(rect.width) ? 0 : rect.width;
var maxX = left !== null ? left + width : Infinity;
var minX = left !== null ? left : -Infinity;
return Helpers.clamp(x, minX, maxX);
};
/**
* Returns the 'real' y after limits are applied (allows for some
* limits to be undefined).
* @param {number} y Y-coordinate to limit.
* @return {number} The 'real' Y-coordinate after limits are applied.
*/
Draggable.prototype.limitY = function(y) {
var rect = this.limits;
var top = isNaN(rect.top) ? null : rect.top;
var height = isNaN(rect.height) ? 0 : rect.height;
var maxY = top !== null ? top + height : Infinity;
var minY = top !== null ? top : -Infinity;
return Helpers.clamp(y, minY, maxY);
};
/**
* Returns the x and y positions the draggable element should be set to.
* @param {Coordinate=} opt_position Position to set the draggable
* element. This will optionally override calculating the position
* from a drag.
* @return {!Coordinate} The x and y coordinates.
* @private
*/
Draggable.prototype.getElementPosition_ = function(opt_position) {
var outputX = 0;
var outputY = 0;
if (opt_position) {
var scrubberX = (opt_position.x / 100) * this.containerEl_.offsetWidth;
var scrubberY = (opt_position.y / 100) * this.containerEl_.offsetHeight;
this.currentPosition_.x = this.limitX(scrubberX);
this.currentPosition_.y = this.limitY(scrubberY);
}
var newX = (this.currentPosition_.x / this.containmentWidth_) * 100;
var newY = (this.currentPosition_.y / this.containmentHeight_) * 100;
// Drag horizontal only.
if (this.axis_ === Draggable.Axis.X) {
outputX = newX;
// Drag vertical only.
} else if (this.axis_ === Draggable.Axis.Y) {
outputY = newY;
// Drag both directions.
} else {
outputX = newX;
outputY = newY;
}
return new Coordinate(outputX, outputY);
};
/**
* Apply a friction value to an pixel position, reducing its value.
* @param {number} position X or Y position.
* @return {number} Position multiplied by friction.
* @private
*/
Draggable.prototype.applyFriction_ = function(position) {
return position * this.friction_;
};
/**
* Drag start handler.
* @param {jQuery.Event} evt The drag event object.
* @private
*/
Draggable.prototype.handleDragStart_ = function(evt) {
var browserEvent = evt.originalEvent;
var isTouchEvent = !!browserEvent.changedTouches;
var isPointerEvent = !!browserEvent.pointerId;
// Must be left click to drag.
if (!this.enabled_ || !isTouchEvent && evt.which !== 1) {
return;
}
// Use the first touch for the pageX and pageY.
if (isTouchEvent) {
this.startX = browserEvent.changedTouches[0].pageX;
this.startY = browserEvent.changedTouches[0].pageY;
// Pointer events have trusted pageX and pageY values, but jQuery doesn't
// normalize it for us because it doesn't know what pointer events are yet.
} else if (isPointerEvent) {
this.startX = browserEvent.pageX;
this.startY = browserEvent.pageY;
} else {
this.startX = evt.pageX; // Normalized by jQuery.
this.startY = evt.pageY; // Normalized by jQuery.
}
this.pageX = this.startX;
this.pageY = this.startY;
this.deltaX = 0;
this.deltaY = 0;
this.timestamp = $.now();
this.deltaTime = 0;
this.setDimensions_();
this.currentPosition_ = new Coordinate(
this.startPosition_.x,
this.startPosition_.y);
// Give a hook to others
var isPrevented = this.dispatchEvent(new DraggableEvent(
Draggable.EventType.START, this, this.startPosition_,
this.startPosition_));
if (!isPrevented) {
this.setupDragHandlers();
}
};
/**
* Drag move, after applyDraggableElementPosition has happened
* @param {jQuery.Event} evt The dragger event.
* @private
*/
Draggable.prototype.handleDragMove_ = function(evt) {
if (!this.enabled_ || this.isDeactivated_) {
return;
}
var browserEvent = evt.originalEvent;
var isTouchEvent = !!browserEvent.changedTouches;
var isPointerEvent = !!browserEvent.pointerId;
if (isTouchEvent) {
this.pageX = browserEvent.changedTouches[0].pageX;
this.pageY = browserEvent.changedTouches[0].pageY;
} else if (isPointerEvent) {
this.pageX = browserEvent.pageX;
this.pageY = browserEvent.pageY;
} else {
this.pageX = evt.pageX; // Normalized by jQuery.
this.pageY = evt.pageY; // Normalized by jQuery.
}
this.deltaX = this.pageX - this.startX;
this.deltaY = this.pageY - this.startY;
var newX = this.startPosition_.x + this.applyFriction_(this.deltaX);
var newY = this.startPosition_.y + this.applyFriction_(this.deltaY);
this.currentPosition_.x = this.limitX(newX);
this.currentPosition_.y = this.limitY(newY);
this.deltaTime = $.now() - this.timestamp;
// Emit an event.
var isPrevented = this.dispatchEvent(new DraggableEvent(
Draggable.EventType.MOVE,
this, this.startPosition_, this.currentPosition_));
// Abort if the developer prevented default on the custom event.
if (isPrevented) {
return;
}
this.hasDragged = true;
var absX = Math.abs(this.deltaX);
var absY = Math.abs(this.deltaY);
// Prevent scrolling if the user has moved past the locking threshold.
if ((this.axis_ === Draggable.Axis.X && absX > Draggable.LOCK_THRESHOLD) ||
(this.axis_ === Draggable.Axis.Y && absY > Draggable.LOCK_THRESHOLD)) {
this.isLocked_ = true;
evt.preventDefault();
}
// Disable dragging if the user is attempting to go the opposite direction
// of the draggable element.
if (!this.isLocked_ && (
(this.axis_ === Draggable.Axis.X && absY > Draggable.DRAG_THRESHOLD) ||
(this.axis_ === Draggable.Axis.Y && absX > Draggable.DRAG_THRESHOLD))) {
this.isDeactivated_ = true;
}
if (!this.isDeactivated_) {
this.applyDraggableElementPosition();
}
};
/**
* Dragging ended.
* @private
*/
Draggable.prototype.handleDragEnd_ = function() {
this.cleanupDragHandlers();
var start = this.startPosition_;
var end = this.currentPosition_;
this.deltaTime = $.now() - this.timestamp;
// Give a hook to others
this.dispatchEvent(new DraggableEvent(
Draggable.EventType.END, this, start, end));
this.hasDragged = false;
this.isDeactivated_ = false;
this.isLocked_ = false;
};
/**
* Sets the position of thd draggable element.
* @param {Coordinate=} opt_position Position to set the draggable
* element. This will optionally override calculating the position
* from a drag.
* @return {Coordinate} The position the draggable element was set to.
*/
Draggable.prototype.applyDraggableElementPosition = function(opt_position) {
var pos = this.getElementPosition_(opt_position);
// Add percentage unit
var outputX = pos.x + '%';
var outputY = pos.y + '%';
if (Device.CAN_TRANSITION_TRANSFORMS) {
this.el_.style[Device.Js.TRANSFORM] =
Helpers.getTranslateString(outputX, outputY);
} else {
this.el_.style.left = outputX;
this.el_.style.top = outputY;
}
return this.currentPosition_;
};
/**
* Binds events to the document for move, end, and cancel (if cancel events
* exist for the device).
*/
Draggable.prototype.setupDragHandlers = function() {
var $doc = $(document);
var ns = '.draggable';
if (Device.HAS_POINTER_EVENTS) {
$doc.on(EventType.POINTERMOVE + ns, this.handleDragMove_.bind(this));
$doc.on(EventType.POINTERUP + ns, this.handleDragEnd_.bind(this));
// Touch and pointers have cancel events for when the user goes into
// something like the browser chrome.
$doc.on(EventType.POINTERCANCEL + ns, this.handleDragEnd_.bind(this));
} else {
$doc.on(EventType.MOUSEMOVE + ns, this.handleDragMove_.bind(this));
$doc.on(EventType.MOUSEUP + ns, this.handleDragEnd_.bind(this));
if (Device.HAS_TOUCH_EVENTS) {
$doc.on(EventType.TOUCHMOVE + ns, this.handleDragMove_.bind(this));
$doc.on(EventType.TOUCHEND + ns, this.handleDragEnd_.bind(this));
// Touch and pointers have cancel events for when the user goes into
// something like the browser chrome.
$doc.on(EventType.TOUCHCANCEL + ns, this.handleDragEnd_.bind(this));
}
}
};
/**
* Removes the events bound during drag start. The draggable namespace can be
* used to remove all of them because the drag start event is still bound
* to the actual element.
*/
Draggable.prototype.cleanupDragHandlers = function() {
var $doc = $(document);
$doc.off('.draggable');
};
/**
* Returns the current position of the draggable element.
* @param {boolean} opt_asPercent Optionally retrieve percentage values instead
* of pixel values.
* @return {Coordinate} X and Y coordinates of the draggable element.
*/
Draggable.prototype.getPosition = function(opt_asPercent) {
if (opt_asPercent) {
return new Coordinate(
(this.currentPosition_.x / this.containmentWidth_) * 100,
(this.currentPosition_.y / this.containmentHeight_) * 100);
} else {
return this.currentPosition_;
}
};
/**
* Set the position of the draggable element.
* @param {number} x X position as a percentage. Eg. 50 for "50%".
* @param {number} y Y position as a percentage. Eg. 50 for "50%".
* @return {Coordinate} The position the draggable element was set to.
*/
Draggable.prototype.setPosition = function(x, y) {
return this.applyDraggableElementPosition(new Coordinate(x, y));
};
/**
* Set the friction value.
* @param {number} friction A number between [1, 0].
*/
Draggable.prototype.setFriction = function(friction) {
if (friction !== this.friction_) {
this.friction_ = friction;
}
};
/**
* Easy way to trigger setting dimensions. Useful for doing things after this
* class has been initialized, but no dragging has occurred yet.
* @return {Draggable} The instance.
*/
Draggable.prototype.update = function() {
this.setDimensions_();
return this;
};
/** @override */
Draggable.prototype.dispose = function() {
Draggable.base(this, 'dispose');
this.containerEl_ = null;
this.el_ = null;
// Remove pointer/mouse/touch events by namespace.
$(this.el_).off('.draggable');
};
/**
* Object representing a drag event.
* @param {string} type Event type.
* @param {Draggable} draggable The draggable instance.
* @param {Coordinate} start The starting coordinate.
* @param {Coordinate} end The ending coordinate.
* @constructor
* @extends {jQuery.Event}
*/
var DraggableEvent = function(type, draggable, start, end) {
$.Event.call(this, type);
/**
* @type {Element}
*/
this.target = draggable.el_;
/**
* The change in x from drag start to end.
* @type {number}
*/
this.deltaX = end.x - start.x;
/**
* The change in y from drag start to end.
* @type {number}
*/
this.deltaY = end.y - start.y;
/**
* Time elapsed from mouse/touch down to mouse/touch up.
* @type {number}
*/
this.deltaTime = draggable.deltaTime;
/**
* Reference to the drag object for this event.
* @type {Draggable}
*/
this.draggable = draggable;
/**
* Velocity in drag.
* @type {number}
*/
this.velocity = Draggable.getVelocity(this.deltaTime,
this.deltaX, this.deltaY);
/**
* Distance dragged.
* @type {number}
*/
this.distance = Coordinate.distance(start, end);
/**
* Direction of drag.
* @type {string}
*/
this.direction = Draggable.getDirection(start, end);
var onAxis = false;
// Is X and direction is right or left.
if (draggable.axis_ === Draggable.Axis.X &&
(this.direction === Draggable.Direction.LEFT ||
this.direction === Draggable.Direction.RIGHT)) {
onAxis = true;
// Is Y and direction is down or up.
} else if (draggable.axis_ === Draggable.Axis.Y &&
(this.direction === Draggable.Direction.UP ||
this.direction === Draggable.Direction.DOWN)) {
onAxis = true;
// Is both and direction is not none.
} else if (draggable.axis_ === Draggable.Axis.BOTH &&
this.direction !== Draggable.Direction.NONE) {
onAxis = true;
}
/**
* Whether the drag direction is on the axis of the draggable element.
* @type {boolean}
*/
this.isDirectionOnAxis = onAxis;
/** @type {Coordinate} */
this.position = draggable.currentPosition_;
};
Helpers.inherits(DraggableEvent, $.Event);
return Draggable;
});
define('card-shuffle', ['require', 'jquery', 'app/device', 'app/helpers', 'app/base-component', 'app/draggable'], function(require) {
var $ = require('jquery');
var Device = require('app/device');
var Helpers = require('app/helpers');
var BaseComponent = require('app/base-component');
var Draggable = require('app/draggable');
var CardShuffle = function(element, isReverseStacked, isDraggable) {
CardShuffle.base(this, 'constructor', element, true);
this.currentIndex = 0;
this.$cards = this.findByClass(CardShuffle.ClassName.CARD);
this.totalCards = this.$cards.length;
this.positions = $.map(this.$cards, function(card) {
return parseFloat($(card).attr('data-position'));
});
this.isTransitioning = false;
this.isReverseStacked = isReverseStacked;
this.isDraggable = isDraggable;
this.draggables = null;
this.timerId = null;
this.init();
};
Helpers.inherits(CardShuffle, BaseComponent);
CardShuffle.ClassName = {
CARD: 'card-shuffle__card',
NO_TRANSITION: 'card-shuffle--no-transition',
TO_BOTTOM: 'card-shuffle__card--to-bottom',
FRONT: 'card-shuffle__card--front',
START: 'card-shuffle__card--off-deck-start',
END: 'card-shuffle__card--off-deck-end',
INNER: 'card-shuffle__card-inner'
};
/**
* When the card pops off the top of the deck, it can go in different
* directions.
* @type {Object}
*/
CardShuffle.Direction = {
START: CardShuffle.ClassName.START,
END: CardShuffle.ClassName.END
};
CardShuffle.prototype.init = function() {
var frontIndex = this.isReverseStacked ? 0 : this.totalCards - 1;
this.$cards.filter('[data-position=' + frontIndex + ']')
.addClass(CardShuffle.ClassName.FRONT);
if (this.isDraggable) {
this.draggables = new Array(this.totalCards);
var self = this;
this.$cards.each(function(i, card) {
self.draggables[i] = new Draggable(card, card.parentNode, Draggable.Axis.X);
if (i > 0) {
self.draggables[i].setEnabled(false);
}
});
}
this.listen();
};
CardShuffle.prototype.listen = function() {
CardShuffle.base(this, 'listen');
if (this.isDraggable) {
this.draggables.forEach(function(draggable) {
$(draggable).on(Draggable.EventType.START, this._handleDraggableStart.bind(this));
$(draggable).on(Draggable.EventType.END, this._handleDraggableEnd.bind(this));
}, this);
}
};
CardShuffle.prototype.dispose = function() {
CardShuffle.base(this, 'dispose');
if (this.draggables) {
this.draggables.forEach(function(draggable) {
$(draggable).off(Draggable.EventType.END);
}, this);
}
this.$cards = null;
};
/**
* Returns the logical index of a neighbor from a given relative number.
* Non looped carousels at 0 and length - 1 don't have previous and next
* neighbors, respectively, so 0 or length - 1 will be return, respectively.
*
* @param {number} logicalIndex The logical index which the second parameter
* is relative to.
* @param {number} relativePos The relative position to the first parameter.
* For example, -2 or 2.
* @return {number} The logical index of the neighbor.
* @private
*/
CardShuffle.prototype._getRelativeIndex = function(logicalIndex, relativePos) {
var index = logicalIndex + relativePos;
if (index < 0) {
return this.totalCards + index;
} else if (index > this.totalCards - 1) {
return index - this.totalCards;
} else {
return index;
}
};
CardShuffle.prototype.advance = function(opt_direction) {
var previousIndex = this.currentIndex;
var selectedIndex = previousIndex + 1;
var direction = opt_direction ? opt_direction : CardShuffle.Direction.END;
if (selectedIndex >= this.totalCards) {
selectedIndex = 0;
}
this.currentIndex = selectedIndex;
this._updateCardPositions();
this._popOffStack(previousIndex, direction);
};
CardShuffle.prototype._slideBack = function(draggable) {
this.isTransitioning = true;
this.$cards.eq(this.currentIndex).removeClass(CardShuffle.ClassName.NO_TRANSITION);
var done = function() {
clearTimeout(this.timerId);
this.isTransitioning = false;
}.bind(this);
Helpers.onTransitionEnd(draggable.getElement(), done);
this.timerId = setTimeout(done, 500);
draggable.setPosition(0, 0);
};
CardShuffle.prototype._popOffStack = function(cardIndex, direction) {
var $previousFront = this.$cards.eq(cardIndex);
this.isTransitioning = true;
// Animate card out of container
$previousFront
.removeClass()
.addClass([
CardShuffle.ClassName.CARD,
CardShuffle.ClassName.TO_BOTTOM,
direction
].join(' '));
// When done going to the bottom, put it behind the other cards and
// go back to the top of the container.
Helpers.onTransitionEnd($previousFront, function($card) {
$card.removeClass([
CardShuffle.ClassName.TO_BOTTOM,
CardShuffle.ClassName.START,
CardShuffle.ClassName.END,
].join(' '));
Helpers.onTransitionEnd($card, function() {
this.isTransitioning = false;
// Toggle dragging.
this.draggables[cardIndex].setEnabled(false);
this.draggables[this.currentIndex].setEnabled(true);
}, this, Device.Css.TRANSFORM);
}.bind(this, $previousFront));
};
CardShuffle.prototype._updateCardPositions = function() {
this.$cards.each(function(i, el) {
var currentPosition = this.positions[i];
var nextPosition = this._getRelativeIndex(currentPosition, this.isReverseStacked ? -1 : 1);
var $card = $(el);
$card.attr('data-position', nextPosition);
this.positions[i] = nextPosition;
var isFront = false;
if (this.isReverseStacked) {
if (nextPosition === 0) {
isFront = true;
}
} else {
if (nextPosition === this.totalCards - 1) {
isFront = true;
}
}
$card.toggleClass(CardShuffle.ClassName.FRONT, isFront);
}.bind(this));
};
CardShuffle.prototype._handleTap = function(evt) {
var target = evt.target;
var hasCardParent = $(target).closest('.' + CardShuffle.ClassName.INNER).length > 0;
if (hasCardParent && !this.isTransitioning) {
this.advance();
}
};
CardShuffle.prototype._handleDraggableStart = function(evt) {
if (this.isTransitioning) {
evt.preventDefault();
return;
}
var draggable = evt.draggable;
var dragger = draggable.getElement();
$(dragger).addClass([
CardShuffle.ClassName.NO_TRANSITION,
Helpers.ClassName.GRABBING
].join(' '));
};
CardShuffle.prototype._handleDraggableEnd = function(evt) {
var velocityThreshold = 0.7;
var velocity = evt.velocity;
var direction = evt.direction;
var draggable = evt.draggable;
var position = draggable.getPosition(true);
var dragger = evt.target;
if (
// Velocity throw
(velocity.x > velocityThreshold && direction === Draggable.Direction.LEFT) ||
// Dragged a quarter of the way
(direction === Draggable.Direction.LEFT && position.x <= -25)) {
dragger.style[Device.Js.TRANSFORM] = '';
this.advance(CardShuffle.Direction.START);
} else if (
// Velocity throw
(velocity.x > velocityThreshold && direction === Draggable.Direction.RIGHT) ||
// Dragged a quarter of the way
(direction === Draggable.Direction.RIGHT && position.x >= 25)) {
dragger.style[Device.Js.TRANSFORM] = '';
this.advance(CardShuffle.Direction.END);
// As long as it has moved more than 1 pixel, it can be transitioned back to
// the resting state. If it's zero, the transition end event will never
// be triggered and the draggable will continue to be disabled.
} else if (evt.isDirectionOnAxis) {
this._slideBack(draggable);
}
$(draggable.getElement()).removeClass(Helpers.ClassName.GRABBING);
};
return CardShuffle;
});
requirejs.config({
baseUrl: 'js',
paths: {
jquery: '//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery'
}
});
define('modernizr', [], window.Modernizr);
require(['card-shuffle'], function(CardShuffle) {
window.cards = new CardShuffle($('.card-shuffle--stacked')[0], true, true);
});
define("app/main", function(){});
Also see: Tab Triggers