UNPKG

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,170 lines (1,160 loc) 97.2 kB
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) return strictMatch; var r = checkFor[i], region = regions[r]; if (r == "*") { r = checkFor[i + 1]; return { region: regions[r], name: r }; } if (hit[0] == region[0] && hit[1] == region[1]) { // found the region with a strict lookup strictMatch = { hit: region, name: r }; } else if ((hit[0] == region[0] || region[0] == null) && (hit[1] == region[1] || region[1] == null)) { // found the region with a loose lookup looseMatch = { hit: region, name: r }; } } // prefer a strict match if (strictMatch != null) return strictMatch; else if (looseMatch != null) return looseMatch; else // no matches were found, return center return defResult; }, bounceDown: function (e) { if (e.target.tagName == "A" && !$(e).is(".bounce")) return; var point = e.originalEvent && e.originalEvent.touches ? e.originalEvent.touches[0] : e, offsetOfTile = $tile.offset(), scrollX = window.pageXOffset, scrollY = window.pageYOffset; data.bounceMethods.pointPos = { x: point.pageX, y: point.pageY }; data.bounceMethods.inTilePos = { x: point.pageX - offsetOfTile.left, y: point.pageY - offsetOfTile.top }; if (!data.$tileParent) { data.$tileParent = $tile.parent(); } var offsetOfParent = data.$tileParent.offset(); data.bounceMethods.eventPos = { x: (offsetOfTile.left - offsetOfParent.left) + ($tile.width() / 2), y: (offsetOfTile.top - offsetOfParent.top) + ($tile.height() / 2) }; var hit = data.bounceMethods.hitTest($tile, data.bounceMethods.inTilePos, data.bounceDirections, 0.25);