metrojs
Version:
Metro JS is a plugin to help make Metro ( also known as flat or modern ) interfaces on the web. Example features include Live Tiles that can fade, flip, slide at custom points or in a carousel. Theme and accent colors are supported along with custom tile
1,156 lines (1,144 loc) • 120 kB
JavaScript
/*!
* Metro JS for jQuery
* http://drewgreenwell.com/
* For details and usage info see: http://drewgreenwell.com/projects/metrojs
Copyright (C) 2013, Drew Greenwell
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
;(function ($) {
// the metrojs object contains helper methods and theme settings
$.fn.metrojs = {
capabilities: null,
checkCapabilities: function(stgs, recheck){
if($.fn.metrojs.capabilities == null || (typeof(recheck) != "undefined" && recheck == true))
$.fn.metrojs.capabilities = new $.fn.metrojs.MetroModernizr(stgs);
return $.fn.metrojs.capabilities;
}
};
var metrojs = $.fn.metrojs,
console = window.console;
if (typeof console !== "object") {
console = {};
console.log = function() {};
console.error=function() {};
}
var throwError = typeof ($.error) === "function" ? $.error : console.error;
var MAX_LOOP_COUNT = 99000;
// .liveTile
$.fn.liveTile = function (method) {
if (pubMethods[method]) {
var args = [];
for (var i = 1; i <= arguments.length; i++) {
args[i - 1] = arguments[i];
}
return pubMethods[method].apply(this, args);
} else if (typeof method === 'object' || !method) {
return pubMethods.init.apply(this, arguments);
} else {
$.error('Method ' + method + ' does not exist on jQuery.liveTile');
return null;
}
};
$.fn.liveTile.contentModules = {
modules: [],
/* the default module layout
[
defaultModules.imageSwap,
defaultModules.htmlSwap
],*/
addContentModule: function (moduleName, module) {
if (!(this.modules instanceof Array))
this.modules = [];
this.modules.push(module);
},
hasContentModule: function (moduleName) {
if (typeof (moduleName) === "undefined" || !(this.modules instanceof Array))
return -1;
for (var i = 0; i < this.modules.length; i++) {
if (typeof (this.modules[i].moduleName) != "undefined" && this.modules[i].moduleName == moduleName)
return i;
}
return -1;
}
};
// default option values for .liveTile
$.fn.liveTile.defaults = {
mode: 'slide', // 'fade', 'slide', 'flip', 'flip-list', carousel
speed: 500, // how fast should animations be performed, in milliseconds
initDelay: -1, // how long to wait before the initial animation
delay: 5000, // how long to wait between animations
stops: "100%", // how much of the back tile should 'slide' reveal before starting a delay
stack: false, // should tiles in slide mode appear stacked (e.g Me tile)
direction: 'vertical', // which direction should animations be performed(horizontal | vertical)
animationDirection: 'forward', // the direction that carousel mode uses to determine which way to slide in tiles
tileSelector: '>div,>li,>p,>img,>a', // the selector used by carousel mode and flip-list to choose tile containers
tileFaceSelector: '>div,>li,>p,>img,>a',// the selector used to choose the front and back containers
ignoreDataAttributes: false, // should data attributes be ignored
click: null, // function ($tile, tdata) { return true; }
link: '', // a url to go to when clicked
newWindow: false, // should the link be opened in a new window
bounce: false, // should the tile shrink when tapped
bounceDirections: 'all', // which direction the tile will tile 'all', 'edges, 'corners'
bounceFollowsMove: true, // should a tile in bounce state tilt in the direction of the mouse as it moves
pauseOnHover: false, // should tile animations be paused on hover in and restarted on hover out
pauseOnHoverEvent: 'both', // pause is called on mouseover, mouseout, or both
playOnHover: false, // should "play" be called on hover
playOnHoverEvent: 'both', // play is called on mouseover, mouseout, or both
onHoverDelay: 0, // the amount of time to wait before the onHover event is fired
onHoverOutDelay: 200, // the amount of time in addition to the speed to wait before the onHoverOut event is fired
repeatCount: -1, // number of times to repeat the animation
appendBack: true, // appends the .last tile if one doesnt exist (slide and flip only)
alwaysTrigger: false, // should every item in a flip list trigger every time a delay passes
flipListOnHover: false, // should items in flip-list flip and stop when hovered
flipListOnHoverEvent: 'mouseout', // which event should be used to trigger the flip-list faces
noHAflipOpacity: '1', // the opacity level set for the backside of the flip animation on unaccelerated browsers
haTransFunc: 'ease', // the tranisiton-timing function to use in hardware accelerated mode
noHaTransFunc: 'linear', // the tranisiton-timing function to use in non hardware accelerated mode
currentIndex: 0, // what is the current stop index for slide mode or slide index for carousel mode
startNow: true, // should the tile immediately start or wait util play or restart has been called
useModernizr: (typeof (window.Modernizr) !== "undefined"), // checks to see if modernizer is already in use
useHardwareAccel: true, // should css animations, transitions and transforms be used when available
useTranslate: true,
faces: {
$front: null, // the jQuery element to use as the front face of the tile; this will bypass tileCssSelector
$back: null // the jQuery element to use as the back face of the tile; this will bypass tileCssSelector
},
animationStarting: function (tileData, $front, $back) {
// returning false will cancel the animation
},
animationComplete: function (tileData, $front, $back) {
},
triggerDelay: function (idx) { // used by flip-list to decide how random the tile flipping should be
return Math.random() * 3000;
},
swap: '', // which swap modules are active for this tile (image, html)
swapFront: '-', // override the available swap modules for the front face
swapBack: '-', // override the available swap modules for the back face
contentModules: [],
rebindMessage: "tile data is missing. Are you missing a call to rebind or destroy? You may also be able to avoid this error by calling stop or pause"
};
// public methods that can be called via .liveTile(method name)
var pubMethods = {
init: function (options) {
// Setup the public options for the livetile
var settings = $.extend({}, $.fn.liveTile.defaults, options);
// checks for browser feature support to enable hardware acceleration
metrojs.checkCapabilities(settings);
helperMethods.getBrowserPrefix();
// setup the default content modules
if ($.fn.liveTile.contentModules.hasContentModule("image") == -1)
$.fn.liveTile.contentModules.addContentModule("image", defaultModules.imageSwap);
if ($.fn.liveTile.contentModules.hasContentModule("html") == -1)
$.fn.liveTile.contentModules.addContentModule("html", defaultModules.htmlSwap);
// this is where the magic happens
return $(this).each(function (tileIndex, ele) {
var $this = $(ele),
data = privMethods.initTileData($this, settings);
// append back tiles and add appropriate classes to prepare tiles
data.faces = privMethods.prepTile($this, data);
// action methods
data.fade = function (count) { privMethods.fade($this, count); };
data.slide = function (count) { privMethods.slide($this, count); };
data.carousel = function (count) { privMethods.carousel($this, count); };
data.flip = function (count) { privMethods.flip($this, count); };
data.flipList = function (count) { privMethods.flipList($this, count); };
var actions = {
fade: data.fade,
slide: data.slide,
carousel: data.carousel,
flip: data.flip,
'flip-list': data.flipList
};
data.doAction = function (count) {
// get the action for the current mode
var action = actions[data.mode];
if (typeof (action) === "function") {
action(count);
data.hasRun = true;
}
// prevent pauseOnHover from resuming a tile that has finished
if (count == data.timer.repeatCount)
data.runEvents = false;
};
// create a new tile timer
data.timer = new $.fn.metrojs.TileTimer(data.delay, data.doAction, data.repeatCount);
// apply the data
$this.data("LiveTile", data);
// handle events
// only bind pause / play on hover if we are not using a fliplist or flipListOnHover isn't set set
if (data.mode !== "flip-list" || data.flipListOnHover == false) {
if (data.pauseOnHover) {
privMethods.bindPauseOnHover($this);
} else if (data.playOnHover) {
privMethods.bindPlayOnHover($this, data);
}
}
// add a click / link to the tile
if (data.link.length > 0 || typeof (data.click) === "function") {
$this.css({ cursor: 'pointer' }).bind("click.liveTile", function (e) {
var proceed = true;
if (typeof (data.click) === "function") {
proceed = data.click($this, data) || false;
}
if (proceed && data.link.length > 0) {
e.preventDefault();
if (data.newWindow)
window.open(data.link);
else
window.location = data.link;
}
});
}
// add bounce if set
privMethods.bindBounce($this, data);
// start timer
if (data.startNow && data.mode != "none") {
data.runEvents = true;
data.timer.start(data.initDelay);
}
});
},
// goto is a future reserved word
'goto': function (options) {
var opts, t = typeof (options);
if (t === "undefined") {
opts = {
index: -99, // same as next
delay: 0,
autoAniDirection: false
};
}
if (t === "number" || !isNaN(options)) {
opts = {
index: parseInt(options, 10),
delay: 0
};
} else if (t === "string") {
if (options == "next") {
opts = {
index: -99,
delay: 0
};
} else if (options.indexOf("prev") === 0) {
opts = {
index: -100,
delay: 0
};
} else {
$.error(options + " is not a recognized action for .liveTile(\"goto\")");
return $(this);
}
} else if (t === "object") {
if (typeof (options.delay) === "undefined") {
options.delay = 0;
}
var ti = typeof (options.index);
if (ti === "undefined") {
options.index = 0;
} else if (ti === "string") {
if (options.index === "next")
options.index = -99;
else if (options.index.indexOf("prev") === 0)
options.index = -100;
}
opts = options;
}
return $(this).each(function (tileIndex, ele) {
var $tile = $(ele),
data = $tile.data("LiveTile"),
aniData = $tile.data("metrojs.tile"),
goTo = opts.index;
if (aniData.animating === true)
return $(this);
if (data.mode === "carousel") {
// get the index based off of the active carousel slide
var $cur = data.faces.$listTiles.filter(".active");
var curIdx = data.faces.$listTiles.index($cur);
// carousel will look for these values as triggers
if (goTo === -100) { // prev
// autoAniDirection determines if a forward or backward animation should be used based on the goTo index
if (typeof (opts.autoAniDirection) === "undefined" || opts.autoAniDirection == true)
data.tempValues.animationDirection = typeof (opts.animationDirection) === "undefined" ? "backward" : opts.animationDirection;
goTo = curIdx === 0 ? data.faces.$listTiles.length - 1 : curIdx - 1;
} else if (goTo === -99) { // next
if (typeof (opts.autoAniDirection) === "undefined" || opts.autoAniDirection == true)
data.tempValues.animationDirection = typeof (opts.animationDirection) === "undefined" ? "forward" : opts.animationDirection;
goTo = curIdx + 1;
}
if (curIdx == goTo) {
return;
}
if (typeof (opts.direction) !== "undefined") {
data.tempValues.direction = opts.direction;
}
if (typeof (opts.animationDirection) !== "undefined") {
data.tempValues.animationDirection = opts.animationDirection;
}
// the index is offset by 1 and incremented on animate
if (goTo == 0)
data.currentIndex = data.faces.$listTiles.length;
else
data.currentIndex = goTo - 1;
} else // slide mode will use the index directly
data.currentIndex = goTo;
// start the timer
data.runEvents = true;
data.timer.start(opts.delay >= 0 ? opts.delay : data.delay);
});
},
play: function (options) {
var opts, t = typeof (options);
if (t === "undefined") {
opts = {
delay: -1
};
} else if (t === "number") {
opts = {
delay: options
};
} else if (t === "object") {
if (typeof (options.delay) === "undefined") {
options.delay = -1;
}
opts = options;
}
return $(this).each(function (tileIndex, ele) {
var $tile = $(ele),
data = $tile.data("LiveTile");
data.runEvents = true;
if (opts.delay < 0 && !data.hasRun)
opts.delay = data.initDelay;
data.timer.start(opts.delay >= 0 ? opts.delay : data.delay);
});
},
animate: function () { // this is really only useful for certain edge cases in slide mode, use 'play' to toggle animations
return $(this).each(function (tileIndex, ele) {
var $tile = $(ele),
data = $tile.data("LiveTile");
data.doAction();
});
},
stop: function () {
return $(this).each(function (tileIndex, ele) {
var $tile = $(ele),
data = $tile.data("LiveTile");
data.hasRun = false;
data.runEvents = false;
data.timer.stop();
window.clearTimeout(data.eventTimeout);
window.clearTimeout(data.flCompleteTimeout);
window.clearTimeout(data.completeTimeout);
if (data.mode === "flip-list") {
data.faces.$listTiles.each(function (idx, li) {
var ldata = $(li).data("metrojs.tile");
window.clearTimeout(ldata.eventTimeout);
window.clearTimeout(ldata.flCompleteTimeout);
window.clearTimeout(ldata.completeTimeout);
});
}
});
},
pause: function () {
return $(this).each(function (tileIndex, ele) {
var $tile = $(ele),
data = $tile.data("LiveTile");
data.timer.pause();
data.runEvents = false;
window.clearTimeout(data.eventTimeout);
window.clearTimeout(data.flCompleteTimeout);
window.clearTimeout(data.completeTimeout);
if (data.mode === "flip-list") {
data.faces.$listTiles.each(function (idx, li) {
var ldata = $(li).data("metrojs.tile");
window.clearTimeout(ldata.eventTimeout);
window.clearTimeout(ldata.flCompleteTimeout);
window.clearTimeout(ldata.completeTimeout);
});
}
});
},
restart: function (options) {
var opts, t = typeof (options);
if (t === "undefined") {
opts = {
delay: -1
};
} else if (t === "number") {
opts = {
delay: options
};
} else if (t === "object") {
if (typeof (options.delay) === "undefined") {
options.delay = -1;
}
opts = options;
}
return $(this).each(function (tileIndex, ele) {
var $tile = $(ele),
data = $tile.data("LiveTile");
if (opts.delay < 0 && !data.hasRun)
opts.delay = data.initDelay;
data.hasRun = false;
data.runEvents = true;
data.timer.restart(opts.delay >= 0 ? opts.delay : data.delay);
});
},
rebind: function (options) {
return $(this).each(function (tileIndex, ele) {
if (typeof (options) !== "undefined") {
if (typeof (options.timer) !== "undefined" && options.timer != null) {
options.timer.stop();
}
options.hasRun = false;
pubMethods["init"].apply(ele, [options]);
} else {
pubMethods["init"].apply(ele, [{}]);
}
});
},
destroy: function (options) {
var t = typeof (options), opts;
if (t === "undefined") {
opts = {
removeCss: false
};
} else if (t === "boolean") {
opts = {
removeCss: options
};
} else if (t === "object") {
if (typeof (options.removeCss) === "undefined") {
options.removeCss = false;
}
opts = options;
}
return $(this).each(function (tileIndex, ele) {
var $tile = $(ele);
var data = $tile.data("LiveTile");
if (typeof (data) === "undefined")
return;
$tile.unbind(".liveTile");
var resetCss = helperMethods.appendStyleProperties({ margin: '', cursor: '' }, ['transform', 'transition'], ['', '']);
data.timer.stop();
window.clearTimeout(data.eventTimeout);
window.clearTimeout(data.flCompleteTimeout);
window.clearTimeout(data.completeTimeout);
if (data.faces.$listTiles != null) {
data.faces.$listTiles.each(function (idx, li) {
var $li = $(li);
if (data.mode === "flip-list") {
var ldata = $li.data("metrojs.tile");
window.clearTimeout(ldata.eventTimeout);
window.clearTimeout(ldata.flCompleteTimeout);
window.clearTimeout(ldata.completeTimeout);
} else if (data.mode === "carousel") {
var sdata = data.listData[idx];
if (sdata.bounce) {
privMethods.unbindMsBounce($li, sdata);
}
}
if (opts.removeCss) {
$li.removeClass("ha");
$li.find(data.tileFaceSelector)
.unbind(".liveTile")
.removeClass("bounce flip-front flip-back ha slide slide-front slide-back")
.css(resetCss);
} else {
$li.find(data.tileFaceSelector).unbind(".liveTile");
}
$li.removeData("metrojs.tile");
}).unbind(".liveTile");
}
if (data.faces.$front != null && opts.removeCss) {
data.faces.$front.removeClass("flip-front flip-back ha slide slide-front slide-back")
.css(resetCss);
}
if (data.faces.$back != null && opts.removeCss) {
data.faces.$back.removeClass("flip-front flip-back ha slide slide-front slide-back")
.css(resetCss);
}
// remove the bounce and hover methods
// todo: combine all mouse/touch events (down, move, up)
if (data.bounce) {
privMethods.unbindMsBounce($tile, data);
}
if (data.playOnHover) {
privMethods.unbindMsPlayOnHover($tile, data);
}
if (data.pauseOnhover) {
privMethods.unbindMsPauseOnHover($tile, data);
}
$tile.removeClass("ha");
$tile.removeData("LiveTile");
$tile.removeData("metrojs.tile");
data = null;
});
}
};
// private methods that are called by .liveTile
var privMethods = {
//getDataOrDefault for older versions of jQuery that dont look for 'data-' properties
dataAtr: function ($ele, name, def) {
return typeof ($ele.attr('data-' + name)) !== "undefined" ? $ele.attr('data-' + name) : def;
},
dataMethod: function ($ele, name, def) {
return typeof ($ele.data(name)) !== "undefined" ? $ele.data(name) : def;
},
getDataOrDefault: null,
initTileData: function ($tile, stgs) {
var useData = stgs.ignoreDataAttributes == false,
tdata = null;
if (this.getDataOrDefault == null)
this.getDataOrDefault = metrojs.capabilities.isOldJQuery ? this.dataAtr : this.dataMethod;
if (useData) {
tdata = { //an object to store settings for later access
speed: this.getDataOrDefault($tile, "speed", stgs.speed),
delay: this.getDataOrDefault($tile, "delay", stgs.delay),
stops: this.getDataOrDefault($tile, "stops", stgs.stops),
stack: this.getDataOrDefault($tile, "stack", stgs.stack),
mode: this.getDataOrDefault($tile, "mode", stgs.mode),
direction: this.getDataOrDefault($tile, "direction", stgs.direction),
useHardwareAccel: this.getDataOrDefault($tile, "ha", stgs.useHardwareAccel),
repeatCount: this.getDataOrDefault($tile, "repeat", stgs.repeatCount),
swap: this.getDataOrDefault($tile, "swap", stgs.swap),
appendBack: this.getDataOrDefault($tile, "appendback", stgs.appendBack),
currentIndex: this.getDataOrDefault($tile, "start-index", stgs.currentIndex),
animationDirection: this.getDataOrDefault($tile, "ani-direction", stgs.animationDirection),
startNow: this.getDataOrDefault($tile, "start-now", stgs.startNow),
tileSelector: this.getDataOrDefault($tile, "tile-selector", stgs.tileSelector),
tileFaceSelector: this.getDataOrDefault($tile, "face-selector", stgs.tileFaceSelector),
bounce: this.getDataOrDefault($tile, "bounce", stgs.bounce),
bounceDirections: this.getDataOrDefault($tile, "bounce-dir", stgs.bounceDirections),
bounceFollowsMove: this.getDataOrDefault($tile, "bounce-follows", stgs.bounceFollowsMove),
click: this.getDataOrDefault($tile, "click", stgs.click),
link: this.getDataOrDefault($tile, "link", stgs.link),
newWindow: this.getDataOrDefault($tile, "new-window", stgs.newWindow),
alwaysTrigger: this.getDataOrDefault($tile, "always-trigger", stgs.alwaysTrigger),
flipListOnHover: this.getDataOrDefault($tile, "flip-onhover", stgs.flipListOnHover),
pauseOnHover: this.getDataOrDefault($tile, "pause-onhover", stgs.pauseOnHover),
playOnHover: this.getDataOrDefault($tile, "play-onhover", stgs.playOnHover),
onHoverDelay: this.getDataOrDefault($tile, "hover-delay", stgs.onHoverDelay),
onHoverOutDelay: this.getDataOrDefault($tile, "hoverout-delay", stgs.onHoverOutDelay),
noHAflipOpacity: this.getDataOrDefault($tile, "flip-opacity", stgs.noHAflipOpacity),
useTranslate: this.getDataOrDefault($tile, "use-translate", stgs.useTranslate),
runEvents: false,
isReversed: false,
loopCount: 0,
contentModules: [],
listData: [],
height: $tile.height(),
width: $tile.width(),
tempValues: {}
};
} else {
tdata = $.extend(true, {
runEvents: false,
isReversed: false,
loopCount: 0,
contentModules: [],
listData: [],
height: $tile.height(),
width: $tile.width(),
tempValues: {}
}, stgs);
}
tdata.useTranslate = tdata.useTranslate && tdata.useHardwareAccel && metrojs.capabilities.canTransform && metrojs.capabilities.canTransition;
// set the margin to half of the height or width based on the direction
tdata.margin = (tdata.direction === "vertical") ? tdata.height / 2 : tdata.width / 2;
// convert stops if needed
tdata.stops = (typeof (stgs.stops) === "object" && (stgs.stops instanceof Array)) ? stgs.stops : ("" + tdata.stops).split(",");
// add a return stop
if (tdata.stops.length === 1)
tdata.stops.push("0px");
// add content modules, start with global swaps
var swaps = tdata.swap instanceof Array ? tdata.swap : tdata.swap.replace(' ', '').split(",");
// get the front and back swap data
var sf = useData ? this.getDataOrDefault($tile, "swap-front", stgs.swapFront) : stgs.swapFront;
var sb = useData ? this.getDataOrDefault($tile, "swap-back", stgs.swapBack) : stgs.swapBack;
// set the data to the global value if its still the default
if (sf instanceof Array) {
tdata.swapFront = sf;
} else {
tdata.swapFront = sf === '-' ? swaps : sf.replace(' ', '').split(",");
}
if (sb instanceof Array) {
tdata.swapBack = sb;
} else {
tdata.swapBack = sb === '-' ? swaps : sb.replace(' ', '').split(",");
}
// make sure the swaps includes all front and back swaps
var i;
for (i = 0; i < tdata.swapFront.length; i++) {
if (tdata.swapFront[i].length > 0 && $.inArray(tdata.swapFront[i], swaps) === -1)
swaps.push(tdata.swapFront[i]);
}
for (i = 0; i < tdata.swapBack.length; i++) {
if (tdata.swapBack[i].length > 0 && $.inArray(tdata.swapBack[i], swaps) === -1)
swaps.push(tdata.swapBack[i]);
}
tdata.swap = swaps;
// add all required content modules for the swaps
for (i = 0; i < swaps.length; i++) {
if (swaps[i].length > 0) {
var moduleIdx = $.fn.liveTile.contentModules.hasContentModule(swaps[i]);
if (moduleIdx > -1) {
tdata.contentModules.push($.fn.liveTile.contentModules.modules[moduleIdx]);
}
}
}
// set the initDelay value to the delay if it's not set
tdata.initDelay = useData ? this.getDataOrDefault($tile, "initdelay", stgs.initDelay) : stgs.initDelay;
// if the delay is -1 call the triggerDelay function to get a value
if (tdata.delay < -1)
tdata.delay = stgs.triggerDelay(1);
else if (tdata.delay < 0)
tdata.delay = 3500 + (Math.random() * 4501);
// match the delay value if less than 0
if (tdata.initDelay < 0)
tdata.initDelay = tdata.delay;
// merge the objects
var mergedData = {};
for (i = 0; i < tdata.contentModules.length; i++)
$.extend(mergedData, tdata.contentModules[i].data);
$.extend(mergedData, stgs, tdata);
// add flip-list / carousel data
var $tiles;
if (mergedData.mode === "flip-list") {
$tiles = $tile.find(mergedData.tileSelector).not(".tile-title");
$tiles.each(function (idx, ele) {
var $li = $(ele);
var ldata = {
direction: useData ? privMethods.getDataOrDefault($li, "direction", mergedData.direction) : mergedData.direction,
newWindow: useData ? privMethods.getDataOrDefault($li, "new-window", false) : false,
link: useData ? privMethods.getDataOrDefault($li, "link", "") : "",
faces: { $front: null, $back: null },
height: $li.height(),
width: $li.width(),
isReversed: false
};
ldata.margin = ldata.direction === "vertical" ? ldata.height / 2 : ldata.width / 2;
mergedData.listData.push(ldata);
});
} else if (mergedData.mode === "carousel") {
mergedData.stack = true;
$tiles = $tile.find(mergedData.tileSelector).not(".tile-title");
$tiles.each(function (idx, ele) {
var $slide = $(ele);
var sdata = {
bounce: useData ? privMethods.getDataOrDefault($slide, "bounce", false) : false,
bounceDirections: useData ? privMethods.getDataOrDefault($slide, "bounce-dir", "all") : "all",
link: useData ? privMethods.getDataOrDefault($slide, "link", "") : "",
newWindow: useData ? privMethods.getDataOrDefault($slide, "new-window", false) : false,
animationDirection: useData ? privMethods.getDataOrDefault($slide, "ani-direction", "") : "",
direction: useData ? privMethods.getDataOrDefault($slide, "direction", "") : ""
};
mergedData.listData.push(sdata);
});
}
// get any additional options from the modules
for (i = 0; i < tdata.contentModules.length; i++) {
if (typeof (mergedData.contentModules[i].initData) === "function")
mergedData.contentModules[i].initData(mergedData, $tile);
}
tdata = null;
return mergedData;
},
prepTile: function ($tile, tdata) {
//add the mode to the tile if it's not already there.
$tile.addClass(tdata.mode);
var ret = {
$tileFaces: null, // all possible tile faces in a liveTile in a non list mode
$listTiles: null, // all possible tiles in a liveTile in a list mode
$front: null, // the front face of a tile in a non list mode
$back: null // the back face of a tile in a non list mode
};
var rotateDir, frontCss, backCss, tileCss;
// prepare the tile based on the current mode
switch (tdata.mode) {
case "fade":
// front and back tile faces
ret.$tileFaces = $tile.find(tdata.tileFaceSelector).not(".tile-title");
ret.$front = (tdata.faces.$front != null && tdata.faces.$front.length > 0) ?
tdata.faces.$front.addClass('fade-front') :
ret.$tileFaces.filter(":first").addClass('fade-front');
// get back face from settings, via selector, or append it if necessary
if (tdata.faces.$back != null && tdata.faces.$back.length > 0) // use $back option
ret.$back = tdata.faces.$back.addClass('fade-back');
else if (ret.$tileFaces.length > 1) // get the last tile face
ret.$back = ret.$tileFaces.filter(":last").addClass('fade-back');
else if (tdata.appendBack) // append the back tile
ret.$back = $('<div class="fade-back"></div>').appendTo($tile);
else // just keep an empty placeholder
ret.$back = $('<div></div>');
break;
case "slide":
// front and back tile faces
ret.$tileFaces = $tile.find(tdata.tileFaceSelector).not(".tile-title");
// get front face from settings or via selector
ret.$front = (tdata.faces.$front != null && tdata.faces.$front.length > 0) ?
tdata.faces.$front.addClass('slide-front') :
ret.$tileFaces.filter(":first").addClass('slide-front'); // using :first for pre jQuery 1.4
// get back face from settings, via selector, or append it if necessary
if (tdata.faces.$back != null && tdata.faces.$back.length > 0) // use $back option
ret.$back = tdata.faces.$back.addClass('slide-back');
else if (ret.$tileFaces.length > 1) // get the last tile face
ret.$back = ret.$tileFaces.filter(":last").addClass('slide-back');
else if (tdata.appendBack) // append the back tile
ret.$back = $('<div class="slide-back"></div>').appendTo($tile);
else // just keep an empty placeholder
ret.$back = $('<div></div>');
// stack mode
if (tdata.stack == true) {
var prop,
translate;
if (tdata.direction === "vertical") {
prop = "top",
translate = 'translate(0%, -100%) translateZ(0)';
} else {
prop = "left",
translate = 'translate(-100%, 0%) translateZ(0)';
}
backCss = {};
if (tdata.useTranslate)
helperMethods.appendStyleProperties(backCss, ['transform'], [translate]);
else
backCss[prop] = "-100%";
ret.$back.css(backCss);
}
$tile.data("metrojs.tile", { animating: false });
if (metrojs.capabilities.canTransition && tdata.useHardwareAccel) { // hardware accelerated :)
$tile.addClass("ha");
ret.$front.addClass("ha");
ret.$back.addClass("ha");
}
break;
case "carousel":
ret.$listTiles = $tile.find(tdata.tileSelector).not(".tile-title");
var numberOfSlides = ret.$listTiles.length;
$tile.data("metrojs.tile", { animating: false });
tdata.currentIndex = Math.min(tdata.currentIndex, numberOfSlides - 1);
ret.$listTiles.each(function (idx, ele) {
var $slide = $(ele).addClass("slide");
var sdata = tdata.listData[idx],
aniDir = typeof (sdata.animationDirection) === "string" && sdata.animationDirection.length > 0 ? sdata.animationDirection : tdata.animationDirection,
dir = typeof (sdata.direction) === "string" && sdata.direction.length > 0 ? sdata.direction : tdata.direction;
if (idx == tdata.currentIndex) {
$slide.addClass("active");
} else if (aniDir === "forward") {
if (dir === "vertical") {
tileCss = tdata.useTranslate ? helperMethods.appendStyleProperties({}, ['transform'], ['translate(0%, 100%) translateZ(0)']) :
{ left: '0%', top: '100%' };
$slide.css(tileCss);
} else {
tileCss = tdata.useTranslate ? helperMethods.appendStyleProperties({}, ['transform'], ['translate(100%, 0%) translateZ(0)']) :
{ left: '100%', top: '0%' };
$slide.css(tileCss);
}
} else if (aniDir === "backward") {
if (dir === "vertical") {
tileCss = tdata.useTranslate ? helperMethods.appendStyleProperties({}, ['transform'], ['translate(0%, -100%) translateZ(0)']) :
{ left: '0%', top: '-100%' };
$slide.css(tileCss);
} else {
tileCss = tdata.useTranslate ? helperMethods.appendStyleProperties({}, ['transform'], ['translate(-100%, 0%) translateZ(0)']) :
{ left: '-100%', top: '0%' };
$slide.css(tileCss);
}
}
// link and bounce can be bound per slide
// add the click handler and link property
privMethods.bindLink($slide, sdata);
// add the bounce effect
if (tdata.useHardwareAccel && metrojs.capabilities.canTransition)
privMethods.bindBounce($slide, sdata);
$slide = null;
sdata = null;
});
// hardware accelerated :)
if (metrojs.capabilities.canFlip3d && tdata.useHardwareAccel) {
$tile.addClass("ha");
ret.$listTiles.addClass("ha");
}
break;
case "flip-list":
// the tile containers inside the list
ret.$listTiles = $tile.find(tdata.tileSelector).not(".tile-title");
ret.$listTiles.each(function (idx, ele) {
var $li = $(ele).addClass("tile-" + (idx + 1));
// add the flip class to the front face
var $lFront = $li.find(tdata.tileFaceSelector).filter(":first").addClass("flip-front").css({ margin: "0px" });
// append a back tile face if one isnt present
if ($li.find(tdata.tileFaceSelector).length === 1 && tdata.appendBack == true)
$li.append("<div></div>");
// add the flip class to the back face
var $lBack = $li.find(tdata.tileFaceSelector).filter(":last").addClass("flip-back").css({ margin: "0px" });
// update the tdata object with the faces
tdata.listData[idx].faces.$front = $lFront;
tdata.listData[idx].faces.$back = $lBack;
// set data for overrides and easy access
$li.data("metrojs.tile", {
animating: false,
count: 1,
completeTimeout: null,
flCompleteTimeout: null,
index: idx
});
var ldata = $li.data("metrojs.tile");
// add the hardware accelerated classes
if (metrojs.capabilities.canFlip3d && tdata.useHardwareAccel) { // hardware accelerated :)
$li.addClass("ha");
$lFront.addClass("ha");
$lBack.addClass("ha");
rotateDir = tdata.listData[idx].direction === "vertical" ? "rotateX(180deg)" : "rotateY(180deg)";
backCss = helperMethods.appendStyleProperties({}, ["transform"], [rotateDir]);
$lBack.css(backCss);
} else { // not hardware accelerated :(
// the front tile face will take up the entire tile
frontCss = (tdata.listData[idx].direction === "vertical") ?
{ height: '100%', width: '100%', marginTop: '0px', opacity: '1' } :
{ height: '100%', width: '100%', marginLeft: '0px', opacity: '1' };
// the back tile face is hidden by default and expanded halfway through a flip
backCss = (tdata.listData[idx].direction === "vertical") ?
{ height: '0px', width: '100%', marginTop: tdata.listData[idx].margin + 'px', opacity: tdata.noHAflipOpacity } :
{ height: '100%', width: '0px', marginLeft: tdata.listData[idx].margin + 'px', opacity: tdata.noHAflipOpacity };
$lFront.css(frontCss);
$lBack.css(backCss);
}
var flipEnded = function () {
ldata.count++;
if (ldata.count >= MAX_LOOP_COUNT)
ldata.count = 1;
};
if (tdata.flipListOnHover) {
var event = tdata.flipListOnHoverEvent + ".liveTile";
$lFront.bind(event, function () {
privMethods.flip($li, ldata.count, tdata, flipEnded);
});
$lBack.bind(event, function () {
privMethods.flip($li, ldata.count, tdata, flipEnded);
});
}
if (tdata.listData[idx].link.length > 0) {
$li.css({ cursor: 'pointer' }).bind("click.liveTile", function () {
if (tdata.listData[idx].newWindow)
window.open(tdata.listData[idx].link);
else
window.location = tdata.listData[idx].link;
});
}
});
break;
case "flip":
// front and back tile faces
ret.$tileFaces = $tile.find(tdata.tileFaceSelector).not(".tile-title");
// get front face from settings or via selector
ret.$front = (tdata.faces.$front != null && tdata.faces.$front.length > 0) ?
tdata.faces.$front.addClass('flip-front') :
ret.$tileFaces.filter(":first").addClass('flip-front');
// get back face from settings, via selector, or append it if necessary
if (tdata.faces.$back != null && tdata.faces.$back.length > 0) {
// use $back option
ret.$back = tdata.faces.$back.addClass('flip-back');
} else if (ret.$tileFaces.length > 1) {
// get the last tile face
ret.$back = ret.$tileFaces.filter(":last").addClass('flip-back');
} else if (tdata.appendBack) {
// append the back tile
ret.$back = $('<div class="flip-back"></div>').appendTo($tile);
} else {
// just keep an empty placeholder
ret.$back = $('<div></div>');
}
$tile.data("metrojs.tile", { animating: false });
if (metrojs.capabilities.canFlip3d && tdata.useHardwareAccel) {
// hardware accelerated :)
$tile.addClass("ha");
ret.$front.addClass("ha");
ret.$back.addClass("ha");
rotateDir = tdata.direction === "vertical" ? "rotateX(180deg)" : "rotateY(180deg)";
backCss = helperMethods.appendStyleProperties({}, ["transform"], [rotateDir]);
ret.$back.css(backCss);
} else {
// not hardware accelerated :(
// the front tile face will take up the entire tile
frontCss = (tdata.direction === "vertical") ?
{ height: '100%', width: '100%', marginTop: '0px', opacity: '1' } :
{ height: '100%', width: '100%', marginLeft: '0px', opacity: '1' };
// the back tile face is hidden by default and expanded halfway through a flip
backCss = (tdata.direction === "vertical") ?
{ height: '0%', width: '100%', marginTop: tdata.margin + 'px', opacity: '0' } :
{ height: '100%', width: '0%', marginLeft: tdata.margin + 'px', opacity: '0' };
ret.$front.css(frontCss);
ret.$back.css(backCss);
}
break;
}
return ret;
},
bindPauseOnHover: function ($tile) {
// stop the tile when hovered and resume after a delay
(function () {
var data = $tile.data("LiveTile"),
isOver = false,
isPending = false,
touchStartedOver = false,
touchStartedOut = false,
pauseIn = (data.pauseOnHoverEvent == "both" || data.pauseOnHoverEvent == "mouseover" || data.pauseOnHoverEvent == "mouseenter"),
pauseOut = (data.pauseOnHoverEvent == "both" || data.pauseOnHoverEvent == "mouseout" || data.pauseOnHoverEvent == "mouseleave");
data.pOnHoverMethods = {
pause: function () {
data.timer.pause();
if (data.mode === "flip-list") {
data.faces.$listTiles.each(function (idx, li) {
window.clearTimeout($(li).data("metrojs.tile").completeTimeout);
});
}
},
over: function (e, isTouch) {
isTouch = typeof (isTouch) == "undefined" ? false : isTouch;
if (!isTouch && touchStartedOver) {
return;
}
if (isOver || isPending)
return;
if (data.runEvents) {
isPending = true;
data.eventTimeout = window.setTimeout(function () {
isPending = false;
if (pauseOut)
isOver = true;
touchStartedOver = false;
data.pOnHoverMethods.pause();
}, data.onHoverDelay);
}
},
out: function (e, isTouch) {
isTouch = typeof (isTouch) == "undefined" ? false : isTouch;
if (!isTouch && touchStartedOut == true) {
return;
}
if (isPending) {
window.clearTimeout(data.eventTimeout);
isPending = false;
return;
}
if (pauseIn) {
if (!isOver && !isPending)
return;
if (data.runEvents) {
// todo: use a custom value if provided
data.timer.start(data.hasRun ? data.delay : data.initDelay);
}
} else {
data.pOnHoverMethods.pause();
}
isOver = false;
touchStartedOut = false;
}
};
if (!metrojs.capabilities.canTouch) {
if (pauseIn)
$tile.bind("mouseover.liveTile", data.pOnHoverMethods.over);
if (pauseOut)
$tile.bind("mouseout.liveTile", data.pOnHoverMethods.out);
} else {
if (window.PointerEvent || window.MSPointerEvent) { // pointer
var eventPrefix = window.MSPointerEvent ? "MS" : "";
if (pauseIn) {
$tile[0].addEventListener(eventPrefix + 'PointerOver', data.pOnHoverMethods.over, false);
}
if (pauseOut) {
$tile[0].addEventListener(eventPrefix + 'PointerOut', data.pOnHoverMethods.out, false);
}
} else { // touch events
if (pauseIn) {
$tile.bind("mouseover.liveTile", data.pOnHoverMethods.over);
$tile.bind("touchstart.liveTile", function (event) {
touchStartedOver = false;
data.pOnHoverMethods.over.apply($tile[0], [event, true]);
});
}
if (pauseOut) {
$tile.bind("mouseout.liveTile", data.pOnHoverMethods.out);
$tile.bind("touchend.liveTile", function (event) {
touchStartedOut = false;
data.pOnHoverMethods.out.apply($tile[0], [event, true]);
});
}
}
}
})();
},
unbindMsPauseOnHover: function ($tile, data) {
if (typeof (data.pOnHoverMethods) !== "undefined" && (window.PointerEvent || window.MSPointerEvent)) {
var eventPrefix = window.MSPointerEvent ? "MS" : "";
$tile[0].removeEventListener(eventPrefix + 'PointerOver', data.pOnHoverMethods.over, false);
$tile[0].removeEventListener(eventPrefix + 'PointerOut', data.pOnHoverMethods.out, false);
}
},
bindPlayOnHover: function ($tile, data) {
// play the tile immediately when hovered
(function () {
var isOver = false,
isPending = false,
touchStartedOver = false,
touchStartedOut = false,
playIn = (data.playOnHoverEvent == "both" || data.playOnHoverEvent == "mouseover" || data.playOnHoverEvent == "mouseenter"),
playOut = (data.playOnHoverEvent == "both" || data.playOnHoverEvent == "mouseout" || data.playOnHoverEvent == "mouseleave");
data.onHoverMethods = {
over: function (event, isTouch) {
isTouch = typeof (isTouch) == "undefined" ? false : isTouch;
if (!isTouch && touchStartedOver) {
return;
}
if (isOver || isPending || (data.bounce && data.bounceMethods.down != "no"))
return;
// if startNow is set use the opposite of isReversed so we're in sync
var rev = (data.mode == "flip") || (data.startNow ? !data.isReversed : data.isReversed);
window.clearTimeout(data.eventTimeout);
if ((data.runEvents && rev) || !data.hasRun) {
isPending = true;
data.eventTimeout = window.setTimeout(function () {
isPending = false;
if (playOut)
isOver = true;
pubMethods["play"].apply($tile[0], [0]);
touchStartedOver = false;
}, data.onHoverDelay);
}
},
out: function (event, isTouch) {
isTouch = typeof(isTouch) == "undefined" ? false : isTouch;
if (!isTouch && touchStartedOut == true) {
return;
}
if (isPending) {
window.clearTimeout(data.eventTimeout);
isPending = false;
return;
}
if (playIn) {
if (!isOver && !isPending) {
return;
}
}
window.clearTimeout(data.eventTimeout);
data.eventTimeout = window.setTimeout(function () {
var rev = (data.mode == "flip") || (data.startNow ? data.isReversed : !data.isReversed);
if (data.runEvents && rev) {
pubMethods["play"].apply($tile[0], [0]);
}
touchStartedOut = false;
isOver = false;
}, data.speed + data.onHoverOutDelay);
}
};
if (!metrojs.capabilities.canTouch) {
if (playIn)
$tile.bind('mouseenter.liveTile', data.onHoverMethods.over);
if (playOut)
$tile.bind('mouseleave.liveTile', data.onHoverMethods.out);
} else {
if (window.PointerEvent || window.MSPointerEvent) { // pointer
var eventPrefix = window.MSPointerEvent ? "MS" : "";
if (playIn) {
//$tile[0].addEventListener(eventPrefix + 'PointerDown', data.onHoverMethods.over, false);
$tile[0].addEventListener(eventPrefix + 'PointerEnter', data.onHoverMethods.over, false);
}
// mouseleave gives a more consistent effect than out when the children are transformed
if (playOut)
$tile[0].addEventListener(eventPrefix + 'PointerLeave', data.onHoverMethods.out, false);
} else { // touch events
if (playIn) {
$tile.bind("touchstart.liveTile", function (event) {
touchStartedOver = true;
data.onHoverMethods.over.apply($tile[0], [event, true]);
});
$tile.bind("mouseenter.liveTile", data.onHoverMethods.over);
}
if (playOut) {
$tile.bind("touchend.liveTile,touchcancel.liveTile", function (event) {
touchStartedOut = false;
data.onHoverMethods.out.apply($tile[0], [event, true]);
});
$tile.bind("mouseleave.liveTile", data.onHoverMethods.out);
}
}
}
})();
},
unbindMsPlayOnHover: function ($tile, data) {
if (typeof (data.onHoverMethods) !== "undefined" && (window.PointerEvent || window.MSPointerEvent)) {
var eventPrefix = window.MSPointerEvent ? "MS" : "";
$tile[0].removeEventListener(eventPrefix + 'PointerOver', data.onHoverMethods.over, false);
}
},
bindBounce: function ($tile, data) {
// add bounce
if (data.bounce) {
$tile.addClass("bounce");
$tile.addClass("noselect");
(function () {
data.bounceMethods = {
down: "no",
threshold: 30,
zeroPos: { x: 0, y: 0 },
eventPos: { x: 0, y: 0 },
inTilePos: { x: 0, y: 0 },
pointPos: { x: 0, y: 0 },
regions: {
c: [0, 0], // center
tl: [-1, -1], // top left
tr: [1, -1], // top right
bl: [-1, 1], // bottom left
br: [1, 1], // bottom right
t: [null, -1], // top
r: [1, null], // right
b: [null, 1], // bottom
l: [-1, null] // left
},
targets: {
all: ['c', 't', 'r', 'b', 'l', 'tl', 'tr', 'bl', 'br'],
edges: ['c', 't', 'r', 'b', 'l'],
corners: ['c', 'tl', 'tr', 'bl', 'br']
},
hitTest: function ($el, pos, targetRegions, omegaC) {
var regions = data.bounceMethods.regions,
checkFor = data.bounceMethods.targets[targetRegions],
i = 0,
strictMatch = null,
looseMatch = null,
defResult = { hit: [0, 0], name: 'c' };
// scale only for android 2.x and old ie
if (metrojs.capabilities.isOldAndroid || !metrojs.capabilities.canTransition)
return defResult;
if (typeof (checkFor) == "undefined") {
if (typeof (targetRegions) === "string")
checkFor = targetRegions.split(',');
// only default to center if explicitly requested
if ($.isArray(checkFor) && $.inArray('c') == -1) {
omegaC = 0;
defResult = null;
}
}
// check for a matching region
var w = $el.width(),
h = $el.height(),
// center threshold - maximum amount from center
ct = [w * omegaC, h * omegaC],
// how far from the center is the point
diffX = pos.x - (w * 0.5),
diffY = pos.y - (h * 0.5),
// if we're beyond the center threshold, set -1 or 1 else 0
hit = [
diffX > 0 ? (Math.abs(diffX) <= ct[0] ? 0 : 1) : (Math.abs(diffX) <= ct[0] ? 0 : -1),
diffY > 0 ? (Math.abs(diffY) <= ct[1] ? 0 : 1) : (Math.abs(diffY) <= ct[1] ? 0 : -1)
];
for (; i < checkFor.length; i++) {
if (strictMatch != null)