v5
Version:
V5 Mobile Web Framework
673 lines (612 loc) • 19 kB
JavaScript
/*global $, _, getStorage, Scape, EventProxy, history*/
/**
* ```
* V5.js 0.2.0
* http://html5ify.com
* (c) 2011-2013 Jackson Tian
* V5 may be freely distributed under the MIT license
* ```
*/
(function (global) {
/**
* The Framework's top object. all components will be register under it.
* Why named as V5, we salute the V8 project.
* The voice means it contains power in Chinese also.
*/
var V5 = global.V5 = new EventProxy();
var $ = window.jQuery || window.Zepto;
/**
* 默认选项
*/
V5.options = {
// Debug mode. If debug is true, don't cache anything
debug: false,
// 资源版本,如果服务端静态资源有更改,修改它可以用于客户端静态资源的更新
// 如果debug模式开启,静态资源将会每次更新
version: '',
// Assets resources path prefix
prefix: '',
// 是否预加载
preload: true,
// 主卡片页,用于启用应用时打开的页面
main: "index"
};
/**
* ## Card定义
*/
/**
* 卡片页定义,每个卡片页具有声明周期,在初始化、收缩、重打开、销毁时分别调用。
*/
var Card = function (module) {
/**
* The Initialize method.
*/
this.initialize = function () {};
/**
* The Shrink method, will be invoked when hide current card.
*/
this.shrink = function () {};
/**
* The Reappear method, when card reappear after shrink, this function will be invoked.
*/
this.reappear = function () {};
/**
* The Destroy method, should be invoked manually when necessary.
*/
this.destroy = function () {};
/**
* Parameters, store the parameters, for check the card whether changed.
*/
this.parameters = null;
/**
* Flag whether enable localization.
*/
this.enableL10N = false;
// Merge the module's methods
_.extend(this, module);
};
// Mixin the eventproxy's prototype
_.extend(Card.prototype, EventProxy.prototype);
/**
* Open an another card from current column or next column.
* @param blank Indicate whether open another card from next column.
*/
Card.prototype.openCard = function (hash, blank) {
var effectColumn;
if (blank) {
effectColumn = this.columnIndex + 1;
} else {
effectColumn = this.columnIndex;
}
var args = hash.split("/");
var cardName = args.shift();
V5.trigger("openCard", cardName, effectColumn, args, this.viewport);
};
/**
* Open a viewport and display a card.
*/
Card.prototype.openViewport = function (hash) {
var args = hash.split("/");
var cardName = args.shift();
var viewport = $("<div></div>").addClass("viewport");
body.append(viewport);
V5.trigger("openCard", cardName, 0, args, viewport);
};
/**
* Destroy current card and close current viewport.
*/
Card.prototype.closeViewport = function () {
this.destroy();
this.unbind();
this.node.remove();
delete this.node;
this.initialized = false;
this.viewport.remove();
delete this.viewport;
};
/**
* Post message to current Card.
*/
Card.prototype.postMessage = function (event, data) {
return this.trigger("card:" + event, data);
};
/**
* Bind message event.
*/
Card.prototype.onMessage = function (event, callback) {
return this.bind("card:" + event, callback);
};
/**
* Define a card component. Card will be displayed in a view colomn.
* @param {Function} module Module object.
*/
V5.Card = Card;
/**
* Lets callback execute at a safely moment.
* @param {function} callback The callback method that will execute when document is ready.
*/
V5.ready = function (callback) {
if (document.readyState === "complete") {
callback();
} else {
var ready = function () {
if (document.readyState === "complete") {
callback();
// Remove the callback listener from readystatechange event.
document.removeEventListener("readystatechange", ready);
}
};
// Bind callback to readystatechange
document.addEventListener("readystatechange", ready);
}
};
/**
* Gets the V5 mode, detects the V5 runing in which devices.
* There are two modes current, phone or tablet.
*/
V5.mode = window.innerWidth < 768 ? "phone" : "tablet";
/**
* Default viewport reference. Viewport could contains many view columns, it's detected by mode.
*/
V5.viewport = null;
/**
* Startups V5 framework.
*/
V5.init = function (options) {
// 更新选项设置
_.extend(V5.options, options);
V5.ready(function () {
V5.viewport = $("#container");
V5.setOrientation();
// Disable touch move events for integrate with iScroll.
window.addEventListener("touchmove", function (e) {e.preventDefault(); }, false);
// Use popstate to handle history go/back.
window.addEventListener('popstate', function (event) {
var params = event.state;
if (params) {
var args = params.split("/");
var currentHash = args.shift();
console.log("Hash Changed: " + currentHash);
if (V5.hashHistory.length) {
console.log(V5.hashHistory);
if (currentHash !== undefined) {
var topHash = _.map(V5.hashMap, function (val, key) {
return _.last(val);
});
if (_.include(topHash, params)) {
console.log("changed, but no action.");
} else {
var hashStack = _.compact(_.map(V5.hashMap, function (val, key) {
return _.include(val, currentHash) ? key : null;
}));
console.log(hashStack);
V5.hashMap[hashStack[0]].pop();
V5.trigger("openCard", currentHash, V5.columns.indexOf(hashStack[0]));
console.log("Forward or back");
}
}
}
}
}, false);
// Handle refresh case or first visit.
// if (V5.hashHistory.length === 0) {
// var map = V5.hashMap;
// if (_.size(map)) {
// // Restore view from session.
// console.log("Restore from session.");
// V5.restoreViews();
// } else {
// Init card.
console.log("Init card.");
V5.initCard();
// }
// }
});
// preload card file
if (V5.options.preload) {
setTimeout(function () {
V5.preloadCard();
}, 100);
}
};
// 全局body对象
var body = $("body");
/**
* Handle orient change events.
*/
V5.setOrientation = function () {
var _setOrientation = function (event) {
var orient = Math.abs(window.orientation) === 90 ? 'landscape' : 'portrait';
var aspect = orient === 'landscape' ? 'portrait' : 'landscape';
body.removeClass(aspect).addClass(orient);
};
_setOrientation();
window.addEventListener('orientationchange', _setOrientation, false);
};
// Hide address bar
// When ready...
V5.ready(function () {
window.scrollTo(0, 0);
});
/**
* Cache the card html.
*/
V5._cardCache = {};
/**
* Predefined view columns.
*/
V5.columns = ["alpha", "beta", "gamma"];
/**
* Predefined viewport's state.
*/
V5.columnModes = ["single", "double", "triple"];
V5.bind("openCard", function (cardName, effectColumn, args, viewport) {
if (V5.mode === "phone") {
effectColumn = 0;
}
args = args || [];
viewport = viewport || V5.viewport;
V5.displayCard(cardName, effectColumn, args, viewport);
var hash = [cardName].concat(args).join("/");
history.pushState(hash, cardName, "#" + hash);
});
/**
* Initializes views when first time visit.
*/
V5.initCard = function () {
V5.trigger("openCard", V5.options.main, 0);
};
/**
* Restores views from session storage.
*/
V5.restoreViews = function () {
var map = V5.hashMap;
console.log(map);
_.each(map, function (viewNames, columnName) {
var hash = viewNames.pop();
var args = hash.split("/");
V5.trigger("openCard", args.shift(), _.indexOf(V5.columns, columnName), args, V5.viewport);
});
};
/**
* Gets Card from cache or server. If the card file comes from server,
* the callback will be executed async, and cache it.
* @param {string} cardName Card name.
* @param {function} callback Callback function, will be called after got the card from cache or server.
*/
V5.getCard = function (cardName, callback) {
var _cardCache = V5._cardCache;
var card = V5._cards[cardName];
var proxy = new EventProxy();
proxy.all("l10n", "card", function (l10n, card) {
var html = l10n ? V5.localize(card, l10n) : card;
card.resources = l10n;
callback($($.trim(html)));
});
if (_cardCache[cardName]) {
proxy.trigger("card", _cardCache[cardName]);
} else {
var url = V5.options.prefix + "cards/" + cardName + ".html?_=";
url += V5.options.debug ? new Date().getTime() : V5.options.version;
$.get(url, function (text) {
// Save into cache.
_cardCache[cardName] = text;
proxy.trigger("card", _cardCache[cardName]);
});
}
// Fetch the localize resources.
// enableL10N Flag whether this card's localization enabled.
// If true, will generate card with localization resources.
if (card.enableL10N) {
V5.fetchL10N(cardName, function () {
proxy.trigger("l10n", V5.L10N[V5.langCode][cardName]);
});
} else {
proxy.trigger("l10n", null);
}
};
/**
* Preload all card files
*/
V5.preloadCard = function () {
var loaded = V5._cardCache;
var all = V5._cards;
var unloaded = [];
for (var name in all) {
if (!loaded.hasOwnProperty(name)) {
unloaded.push(name);
}
}
var continueLoad = function () {
var name = unloaded.shift();
if (name) {
V5.getCard(name, continueLoad);
}
};
continueLoad();
};
/**
* Display card in view column.
* @private
* @param {string} hash Card hash, card name.
* @param {number} effectColumn View column's index.
* @param {array} args Parameters of view.
* @param {object} viewport Which viewport, if don't set, will use default viewport.
*/
V5.displayCard = function (hash, effectColumn, args, viewport) {
var columnName = V5.columns[effectColumn];
var column = viewport.find("." + columnName);
if (column.size() < 1) {
// 在新的viewport中打开时
column = $("<div><div class='column_loading'><div class='loading_animation'></div></div></div>");
column.addClass(columnName);
viewport.append(column);
}
// 记录到历史中,供重新打开应用时恢复
if (viewport === V5.viewport) {
if (V5.hashMap[columnName]) {
V5.hashMap[columnName].push([hash].concat(args).join("/"));
} else {
V5.hashMap[columnName] = [[hash].concat(args).join("/")];
}
V5.hashHistory.push([hash].concat(args));
}
var card = V5._cards[hash];
if (!card) {
throw new Error(hash + " module doesn't be defined.");
}
var previousCard = column.find("section.card.active");
if (previousCard.length) {
var id = previousCard.attr('id');
var previous = V5._cards[id];
// 如果前一张card与要打开的不是同一张card,收起它
if (previous && id !== hash) {
previous.shrink();
}
}
var loadingNode = column.find(".column_loading").removeClass("hidden");
V5.getCard(hash, function (node) {
// 隐藏前一张卡片
previousCard.removeClass("active");
loadingNode.addClass("hidden");
if (viewport === V5.viewport) {
viewport.attr("class", V5.columnModes[_.size(V5.hashMap) - 1]);
}
card.columnIndex = _.indexOf(V5.columns, columnName);
if (!card.initialized) {
column.append(node);
card.node = node;
card.node.addClass("active");
card.initialize.apply(card, args);
card.initialized = true;
} else if (card.parameters.toString() !== args.toString()) {
card.destroy();
card.node.remove();
column.append(node);
card.node = node;
card.node.addClass("active");
card.initialize.apply(card, args);
} else {
card.node.addClass("active");
card.reappear();
}
// 传递
card.parameters = args;
card.viewport = viewport;
});
};
/**
* ## History
*/
/**
* History implementation. Stores history actions.
*/
V5.hashHistory = [];
/**
* Store hash and keep in session storage.
*/
V5.hashMap = (function () {
var session = getStorage("session");
var hashMap = session.get("hashMap");
if (!hashMap) {
hashMap = {};
} else {
session.remove("hashMap");
}
// Save hash state into session storeage when unload page
$(window).bind("unload", function () {
session.put("hashMap", V5.hashMap);
});
return hashMap;
}());
/**
* ## View
*/
/**
*
*/
var View = function (el) {
this.el = $(el);
};
_.extend(View.prototype, EventProxy.prototype);
// Cached regex to split keys for `delegate`.
var eventSplitter = /^(\S+)\s*(.*)$/;
V5.supportTouch = "ontouchend" in document;
View.prototype.method = function (eventName) {
var that = this;
return function () {
that.emit.apply(that, [eventName].concat([].slice.call(arguments, 0)));
};
};
View.prototype.$ = function (selector) {
return this.el.find(selector);
};
// Set callbacks, where `this.callbacks` is a hash of
/**
* ```
* {"event selector": "callback"}
*
* {
* 'mousedown .title': 'edit',
* 'click .button': 'save'
* }
* ```
* pairs. Callbacks will be bound to the view, with `this` set properly.
* Uses event delegation for efficiency.
* Omitting the selector binds the event to `this.el`.
* This only works for delegate-able events: not `focus`, `blur`, and
* not `change`, `submit`, and `reset` in Internet Explorer.
*/
View.prototype.delegateEvents = function (events) {
if (!(events || (events = this.events))) {
return;
}
if (_.isFunction(events)) {
events = events.call(this);
}
var that = this;
this.el.unbind('.delegateEvents');
var ignores = {
'swipe': 'click',
'swipeLeft': 'click',
'swipeRight': 'click',
'swipeUp': 'click',
'swipeDown': 'click'
};
var fallback = {
'tap': 'click',
'singleTap': 'click',
'doubleTap': 'dblclick',
'longTap': 'click'
};
for (var key in events) {
var match = key.match(eventSplitter);
var eventName = match[1], selector = match[2];
if (!V5.supportTouch) {
eventName = fallback[eventName] || eventName;
if (ignores.hasOwnProperty(eventName)) {
// 在PC上忽略掉不支持的事件
continue;
}
}
var method = that.method(events[key]);
eventName += '.delegateEvents';
if (selector === '') {
this.el.bind(eventName, method);
} else {
this.el.delegate(selector, eventName, method);
}
}
};
/**
* undelegate all events
*/
View.prototype.undelegateEvents = function () {
$(this.el).unbind();
};
/**
* A factory method to generate View object. Packaged on Backbone.View.
* @param {node} node a $(Zepto/jQuery) element node.
* @returns {View} View object, based on Backbone.View.
*/
V5.View = function (el) {
return new View(el);
};
// Card defined
/**
* Card namespace. All card module will be stored at here.
* @private
*/
V5._cards = {};
/**
* Register a card to V5.
* @param {string} name Card id, used as key/path, V5 framework find card element by this name
* @param {function} The module object.
*/
V5.registerCard = function (name, module) {
if (typeof module === "function") {
V5._cards[name] = new V5.Card(module());
}
};
/**
* ## Common Module
*/
V5._modules = {};
/**
* Register a common module.
*/
V5.registerModule = function (moduleId, module) {
V5._modules[moduleId] = module;
};
/**
* Call a common module.
*/
V5.Card.prototype.invoke = function (moduleId) {
var module = V5._modules[moduleId];
if (module) {
var args = [].slice.call(arguments, 1);
return module.apply(this, args);
} else {
throw new Error(moduleId + " Module doesn't exist");
}
};
/**
* ## Localization
*/
/**
* Local code.
*/
V5.langCode = "en-US";
/**
* All localization resources will be stored at here by locale code.
* Localization resources namespace.
*/
V5.L10N = {};
/**
* Gets localization resources by card name.
* @param {string} cardName Card name
* @param {function} callback Callback that will be invoked when sources is got.
*/
V5.fetchL10N = function (cardName, callback) {
var code = V5.langCode;
var url = V5.options.prefix + "languages/" + cardName + "_" + code + ".lang?_=";
url += V5.options.debug ? new Date().getTime() : V5.options.version;
$.getJSON(url, function (data) {
// Sets l10n resources to V5.L10N
V5.L10N[code] = V5.L10N[code] || {};
_.extend(V5.L10N[code], data);
callback(V5.L10N[code]);
});
};
/**
* A wrapper method to localize template with the resources
* @param {String} tpl template string.
* @param {Object} resources resources object.
* @returns rendered html string.
*/
V5.localize = function (tpl, resources) {
var settings = {
interpolate : /\{\{(.+?)\}\}/g
};
return _.template(tpl, resources, settings);
};
/**
* ## Message mechanism
*/
/**
* V5 message mechanism.
*/
V5.postMessage = function (hash, event, data) {
var card = V5._cards[hash];
if (card) {
card.postMessage(event, data);
}
};
/**
* ## Model
*/
/**
* V5 model layer.
*/
V5.Model = new Scape();
}(window));