UNPKG

avalon2

Version:

an elegant efficient express mvvm framework

732 lines (671 loc) 27.7 kB
/******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ if (installedModules[moduleId]) /******/ return installedModules[moduleId].exports; /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ exports: {}, /******/ id: moduleId, /******/ loaded: false /******/ }; /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ // Flag the module as loaded /******/ module.loaded = true; /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ // Load entry module and return exports /******/ return __webpack_require__(0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ function(module, exports, __webpack_require__) { /* * * version 1.0 * built in 2015.11.19 */ var mmHistory = __webpack_require__(6) var storage = __webpack_require__(7) function Router() { this.rules = [] } var placeholder = /([:*])(\w+)|\{(\w+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g Router.prototype = storage avalon.mix(storage, { error: function(callback) { this.errorback = callback }, _pathToRegExp: function(pattern, opts) { var keys = opts.keys = [], // segments = opts.segments = [], compiled = '^', last = 0, m, name, regexp, segment; while ((m = placeholder.exec(pattern))) { name = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null regexp = m[4] || (m[1] == '*' ? '.*' : 'string') segment = pattern.substring(last, m.index); var type = this.$types[regexp] var key = { name: name } if (type) { regexp = type.pattern key.decode = type.decode } keys.push(key) compiled += quoteRegExp(segment, regexp, false) // segments.push(segment) last = placeholder.lastIndex } segment = pattern.substring(last); compiled += quoteRegExp(segment) + (opts.strict ? opts.last : "\/?") + '$'; var sensitive = typeof opts.caseInsensitive === "boolean" ? opts.caseInsensitive : true // segments.push(segment); opts.regexp = new RegExp(compiled, sensitive ? 'i' : undefined); return opts }, //添加一个路由规则 add: function(path, callback, opts) { var array = this.rules if (path.charAt(0) !== "/") { avalon.error("avalon.router.add的第一个参数必须以/开头") } opts = opts || {} opts.callback = callback if (path.length > 2 && path.charAt(path.length - 1) === "/") { path = path.slice(0, -1) opts.last = "/" } avalon.Array.ensure(array, this._pathToRegExp(path, opts)) }, //判定当前URL与已有状态对象的路由规则是否符合 route: function(path, query) { path = path.trim() var rules = this.rules for (var i = 0, el; el = rules[i++];) { var args = path.match(el.regexp) if (args) { el.query = query || {} el.path = path el.params = {} var keys = el.keys args.shift() if (keys.length) { this._parseArgs(args, el) } return el.callback.apply(el, args) } } if (this.errorback) { this.errorback() } }, _parseArgs: function(match, stateObj) { var keys = stateObj.keys for (var j = 0, jn = keys.length; j < jn; j++) { var key = keys[j] var value = match[j] || '' if (typeof key.decode === 'function') { //在这里尝试转换参数的类型 var val = key.decode(value) } else { try { val = JSON.parse(value) } catch (e) { val = value } } match[j] = stateObj.params[key.name] = val } }, /* * @interface avalon.router.navigate 设置历史(改变URL) * @param hash 访问的url hash */ navigate: function(hash, mode) { var parsed = parseQuery(hash) var newHash = this.route(parsed.path, parsed.query) if (isLegalPath(newHash)) { hash = newHash } //保存到本地储存或cookie avalon.router.setLastPath(hash) // 模式0, 不改变URL, 不产生历史实体, 执行回调 // 模式1, 改变URL, 不产生历史实体, 执行回调 // 模式2, 改变URL, 产生历史实体, 执行回调 if (mode === 1) { avalon.history.setHash(hash, true) } else if (mode === 2) { avalon.history.setHash(hash) } return hash }, /* * @interface avalon.router.when 配置重定向规则 * @param path 被重定向的表达式,可以是字符串或者数组 * @param redirect 重定向的表示式或者url */ when: function(path, redirect) { var me = this, path = path instanceof Array ? path : [path] avalon.each(path, function(index, p) { me.add(p, function() { var info = me.urlFormate(redirect, this.params, this.query) me.navigate(info.path + info.query) }) }) return this }, urlFormate: function(url, params, query) { var query = query ? queryToString(query) : "", hash = url.replace(placeholder, function(mat) { var key = mat.replace(/[\{\}]/g, '').split(":") key = key[0] ? key[0] : key[1] return params[key] !== undefined ? params[key] : '' }).replace(/^\//g, '') return { path: hash, query: query } }, /* * `'/hello/'` - 匹配'/hello/'或'/hello' `'/user/:id'` - 匹配 '/user/bob' 或 '/user/1234!!!' 或 '/user/' 但不匹配 '/user' 与 '/user/bob/details' `'/user/{id}'` - 同上 `'/user/{id:[^/]*}'` - 同上 `'/user/{id:[0-9a-fA-F]{1,8}}'` - 要求ID匹配/[0-9a-fA-F]{1,8}/这个子正则 `'/files/{path:.*}'` - Matches any URL starting with '/files/' and captures the rest of the path into the parameter 'path'. `'/files/*path'` - ditto. */ // avalon.router.get("/ddd/:dddID/",callback) // avalon.router.get("/ddd/{dddID}/",callback) // avalon.router.get("/ddd/{dddID:[0-9]{4}}/",callback) // avalon.router.get("/ddd/{dddID:int}/",callback) // 我们甚至可以在这里添加新的类型,avalon.router.$type.d4 = { pattern: '[0-9]{4}', decode: Number} // avalon.router.get("/ddd/{dddID:d4}/",callback) $types: { date: { pattern: "[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])", decode: function(val) { return new Date(val.replace(/\-/g, "/")) } }, string: { pattern: "[^\\/]*", decode: function(val) { return val; } }, bool: { decode: function(val) { return parseInt(val, 10) === 0 ? false : true; }, pattern: "0|1" }, 'int': { decode: function(val) { return parseInt(val, 10); }, pattern: "\\d+" } } }) module.exports = avalon.router = new Router function parseQuery(url) { var array = url.split("?"), query = {}, path = array[0], querystring = array[1] if (querystring) { var seg = querystring.split("&"), len = seg.length, i = 0, s; for (; i < len; i++) { if (!seg[i]) { continue } s = seg[i].split("=") query[decodeURIComponent(s[0])] = decodeURIComponent(s[1]) } } return { path: path, query: query } } function isLegalPath(path) { if (path === '/') return true if (typeof path === 'string' && path.length > 1 && path.charAt(0) === '/') { return true } } function queryToString(obj) { if (typeof obj === 'string') return obj var str = [] for (var i in obj) { if (i === "query") continue str.push(i + '=' + encodeURIComponent(obj[i])) } return str.length ? '?' + str.join("&") : '' } function quoteRegExp(string, pattern, isOptional) { var result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&"); if (!pattern) return result; var flag = isOptional ? '?' : ''; return result + flag + '(' + pattern + ')' + flag; } /***/ }, /* 1 */ , /* 2 */ , /* 3 */ , /* 4 */ , /* 5 */ , /* 6 */ /***/ function(module, exports) { /*! * mmHistory * 用于监听地址栏的变化 * https://github.com/flatiron/director/blob/master/lib/director/browser.js * https://github.com/visionmedia/page.js/blob/master/page.js */ var location = document.location var oldIE = avalon.msie <= 7 var supportPushState = !!(window.history.pushState) var supportHashChange = !!("onhashchange" in window && (!window.VBArray || !oldIE)) var defaults = { root: "/", html5: false, hashPrefix: "!", iframeID: null, //IE6-7,如果有在页面写死了一个iframe,这样似乎刷新的时候不会丢掉之前的历史 interval: 50, //IE6-7,使用轮询,这是其时间时隔, autoScroll: false } var mmHistory = { hash: getHash(location.href), check: function() { var h = getHash(location.href) if (h !== this.hash) { this.hash = h this.onHashChanged() } }, start: function(options) { if (this.started) throw new Error('avalon.history has already been started') this.started = true //监听模式 if (typeof options === 'boolean') { options = { html5: options } } options = avalon.mix({}, defaults, options || {}) if (options.fireAnchor) { options.autoScroll = true } var rootPath = options.root if (!/^\//.test(rootPath)) { avalon.error('root配置项必须以/字符开始, 以非/字符结束') } if (rootPath.length > 1) { options.root = rootPath.replace(/\/$/, '') } var html5Mode = options.html5 this.options = options this.mode = html5Mode ? "popstate" : "hashchange" if (!supportPushState) { if (html5Mode) { avalon.warn("浏览器不支持HTML5 pushState,平稳退化到onhashchange!") } this.mode = "hashchange" } if (!supportHashChange) { this.mode = "iframepoll" } avalon.log('avalon run mmHistory in the ', this.mode, 'mode') // 支持popstate 就监听popstate // 支持hashchange 就监听hashchange(IE8,IE9,FF3) // 否则的话只能每隔一段时间进行检测了(IE6, IE7) switch (this.mode) { case "popstate": // At least for now HTML5 history is available for 'modern' browsers only // There is an old bug in Chrome that causes onpopstate to fire even // upon initial page load. Since the handler is run manually in init(), // this would cause Chrome to run it twise. Currently the only // workaround seems to be to set the handler after the initial page load // http://code.google.com/p/chromium/issues/detail?id=63040 setTimeout(function() { window.onpopstate = mmHistory.onHashChanged }, 500) break case "hashchange": window.onhashchange = mmHistory.onHashChanged break case "iframepoll": //也有人这样玩 http://www.cnblogs.com/meteoric_cry/archive/2011/01/11/1933164.html avalon.ready(function() { var iframe = document.createElement('iframe') iframe.id = options.iframeID iframe.style.display = 'none' document.body.appendChild(iframe) mmHistory.iframe = iframe mmHistory.writeFrame('') if (avalon.msie) { function onPropertyChange() { if (event.propertyName === 'location') { mmHistory.check() } } document.attachEvent('onpropertychange', onPropertyChange) mmHistory.onPropertyChange = onPropertyChange } mmHistory.intervalID = window.setInterval(function() { mmHistory.check() }, options.interval) }) break } //页面加载时触发onHashChanged this.onHashChanged() }, stop: function() { switch (this.mode) { case "popstate": window.onpopstate = avalon.noop break case "hashchange": window.onhashchange = avalon.noop break case "iframepoll": if (this.iframe) { document.body.removeChild(this.iframe) this.iframe = null } if (this.onPropertyChange) { document.detachEvent('onpropertychange', this.onPropertyChange) } clearInterval(this.intervalID) break } this.started = false }, setHash: function(s, replace) { switch (this.mode) { case 'iframepoll': if (replace) { var iframe = this.iframe if (iframe) { //contentWindow 兼容各个浏览器,可取得子窗口的 window 对象。 //contentDocument Firefox 支持,> ie8 的ie支持。可取得子窗口的 document 对象。 iframe.contentWindow._hash = s } } else { this.writeFrame(s) } break case 'popstate': var path = (this.options.root + '/' + s).replace(/\/+/g, '/') var method = replace ? 'replaceState' : 'pushState' history[method]({}, document.title, path) // 手动触发onpopstate event this.onHashChanged() break default: //http://stackoverflow.com/questions/9235304/how-to-replace-the-location-hash-and-only-keep-the-last-history-entry var newHash = this.options.hashPrefix + s if (replace && location.hash !== newHash) { history.back() } location.hash = newHash break } }, writeFrame: function(s) { // IE support... var f = mmHistory.iframe var d = f.contentDocument || f.contentWindow.document d.open() d.write("<script>_hash = '" + s + "'; onload = parent.avalon.history.syncHash;<script>") d.close() }, syncHash: function() { // IE support... var s = this._hash if (s !== getHash(location.href)) { location.hash = s } return this }, getPath: function() { var path = location.pathname var path = path.split(this.options.root)[1] if (path.charAt(0) !== '/') { path = '/' + path } return path }, onHashChanged: function(hash, clickMode) { if (!clickMode) { hash = mmHistory.mode === 'popstate' ? mmHistory.getPath() : location.href.replace(/.*#!?/, '') } hash = decodeURIComponent(hash) hash = hash.charAt(0) === '/' ? hash : '/' + hash if (hash !== mmHistory.hash) { mmHistory.hash = hash if (avalon.router) { //即mmRouter hash = avalon.router.navigate(hash, 0) } if (clickMode) { mmHistory.setHash(hash) } if (clickMode && mmHistory.options.autoScroll) { autoScroll(hash.slice(1)) } } } } function getHash(path) { // IE6直接用location.hash取hash,可能会取少一部分内容 // 比如 http://www.cnblogs.com/rubylouvre#stream/xxxxx?lang=zh_c // ie6 => location.hash = #stream/xxxxx // 其他浏览器 => location.hash = #stream/xxxxx?lang=zh_c // firefox 会自作多情对hash进行decodeURIComponent // 又比如 http://www.cnblogs.com/rubylouvre/#!/home/q={%22thedate%22:%2220121010~20121010%22} // firefox 15 => #!/home/q={"thedate":"20121010~20121010"} // 其他浏览器 => #!/home/q={%22thedate%22:%2220121010~20121010%22} var index = path.indexOf("#") if (index === -1) { return '' } return decodeURI(path.slice(index)) } //劫持页面上所有点击事件,如果事件源来自链接或其内部, //并且它不会跳出本页,并且以"#/"或"#!/"开头,那么触发updateLocation方法 avalon.bind(document, "click", function(e) { //https://github.com/asual/jquery-address/blob/master/src/jquery.address.js //https://github.com/angular/angular.js/blob/master/src/ng/location.js //下面十种情况将阻止进入路由系列 //1. 路由器没有启动 if (!mmHistory.started) { return } //2. 不是左键点击或使用组合键 if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2 || e.button === 2) { return } //3. 此事件已经被阻止 if (e.returnValue === false) { return } //4. 目标元素不A标签,或不在A标签之内 var el = e.path ? e.path[0] : e.target || e.srcElement || {} while (el.nodeName !== "A") { el = el.parentNode if (!el || el.tagName === "BODY") { return } } //5. 没有定义href属性或在hash模式下,只有一个# //IE6/7直接用getAttribute返回完整路径 var href = el.getAttribute('href', 2) || el.getAttribute("xlink:href") || '' if (href.slice(0, 2) !== '#!') { return } //6. 目标链接是用于下载资源或指向外部 if (el.getAttribute('download') || el.getAttribute('rel') === 'external') return //7. 只是邮箱地址 if (href.indexOf('mailto:') > -1) { return } //8. 目标链接要新开窗口 if (el.target && el.target !== '_self') { return } e.preventDefault() //终于达到目的地 mmHistory.onHashChanged(href.replace('#!', ''), true) }) //得到页面第一个符合条件的A标签 function getFirstAnchor(name) { var list = document.getElementsByTagName('A') for (var i = 0, el; el = list[i++];) { if (el.name === name) { return el } } } function getOffset(elem) { var position = avalon(elem).css('position'), offset if (position !== 'fixed') { offset = 0 } else { offset = elem.getBoundingClientRect().bottom } return offset } function autoScroll(hash) { //取得页面拥有相同ID的元素 var elem = document.getElementById(hash) if (!elem) { //取得页面拥有相同name的A元素 elem = getFirstAnchor(hash) } if (elem) { elem.scrollIntoView() var offset = getOffset(elem) if (offset) { var elemTop = elem.getBoundingClientRect().top window.scrollBy(0, elemTop - offset.top) } } else { window.scrollTo(0, 0) } } module.exports = avalon.history = mmHistory /***/ }, /* 7 */ /***/ function(module, exports) { function supportLocalStorage() { try { //看是否支持localStorage localStorage.setItem("avalon", 1) localStorage.removeItem("avalon") return true } catch (e) { return false } } function escapeCookie(value) { return String(value).replace(/[,;"\\=\s%]/g, function(character) { return encodeURIComponent(character) }); } var ret = {} if (supportLocalStorage()) { ret.getLastPath = function() { return localStorage.getItem('msLastPath') } var cookieID ret.setLastPath = function(path) { if (cookieID) { clearTimeout(cookieID) cookieID = null } localStorage.setItem("msLastPath", path) cookieID = setTimeout(function() { //模拟过期时间 localStorage.removItem("msLastPath") }, 1000 * 60 * 60 * 24) } } else { ret.getLastPath = function() { return getCookie.getItem('msLastPath') } ret.setLastPath = function(path) { setCookie('msLastPath', path) } function setCookie(key, value) { var date = new Date() //将date设置为1天以后的时间 date.setTime(date.getTime() + 1000 * 60 * 60 * 24) document.cookie = escapeCookie(key) + '=' + escapeCookie(value) + ';expires=' + date.toGMTString() } function getCookie(name) { var m = String(document.cookie).match(new RegExp('(?:^| )' + name + '(?:(?:=([^;]*))|;|$)')) || ["", ""] return decodeURIComponent(m[1]) } } module.exports = ret /***/ } /******/ ]);