commit d2e4ceaada3dc35ae7b1c3e0a00df4cdae1e1497 Author: 宇天 Date: Sat Aug 4 16:26:50 2018 +0800 init project diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d56fc3d --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.DS_Store +.AppleDouble +.LSOverride +.idea +.vscode +node_modules/ +dist/ + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ab60297 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 + +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. diff --git a/pack.config.js b/pack.config.js new file mode 100644 index 0000000..a8b504b --- /dev/null +++ b/pack.config.js @@ -0,0 +1,364 @@ +/** + * + * @authors yutent (yutent@doui.cc) + * @date 2018-08-04 01:00:06 + */ + +'use strict' + +require('es.shim') +const fs = require('iofs') +const path = require('path') +const chokidar = require('chokidar') +const uglify = require('uglify-es') +const chalk = require('chalk') +const log = console.log +const VERSION = '1.0.0' +const PACK_QUEUE = [ + 'anot.js', + 'anot.shim.js', + 'anot-touch.js', + 'anot-touch.shim.js' +] +const PACK_DIR = path.resolve('./dist') +const SOURCE_DIR = path.resolve('./src/') + +const PAD_START = Buffer.from(` +var _Anot = (function() { +`) +const PAD_END = Buffer.from(` +/********************************************************************* + * DOMReady * + **********************************************************************/ + + var readyList = [] + var isReady + var fireReady = function(fn) { + isReady = true + while ((fn = readyList.shift())) { + fn(Anot) + } + } + + if (DOC.readyState === 'complete') { + setTimeout(fireReady) //如果在domReady之外加载 + } else { + DOC.addEventListener('DOMContentLoaded', fireReady) + } + window.addEventListener('load', fireReady) + Anot.ready = function(fn) { + if (!isReady) { + readyList.push(fn) + } else { + fn(Anot) + } + } + return Anot +})() +module.exports = _Anot +`) +const PAD_END_NEXT = Buffer.from(` + +/********************************************************************* + * css import * + **********************************************************************/ + + var CSS_DEPS = {} + function importCss(url) { + url = url.replace(/^\\/+/, '/') + if (window.LIBS_BASE_URL) { + url = window.LIBS_BASE_URL + url + } + if (CSS_DEPS[url]) { + return + } + head.insertAdjacentHTML( + 'afterBegin', + '' + ) + CSS_DEPS[url] = 1 + } + + +/********************************************************************* + * DOMReady * + **********************************************************************/ + + var readyList = [] + var isReady + var fireReady = function(fn) { + isReady = true + while ((fn = readyList.shift())) { + fn(Anot) + } + } + + if (DOC.readyState === 'complete') { + setTimeout(fireReady) //如果在domReady之外加载 + } else { + DOC.addEventListener('DOMContentLoaded', fireReady) + } + window.addEventListener('load', fireReady) + Anot.ready = function(fn) { + if (!isReady) { + readyList.push(fn) + } else { + fn(Anot) + } + } + window.importCss = importCss + return Anot +})() +export default _Anot +`) +const PAD_START_SHIM = Buffer.from(` +;(function() { +`) +const PAD_END_SHIM = Buffer.from(` +/********************************************************************* + * DOMReady * + **********************************************************************/ + + var readyList = [] + var isReady + var fireReady = function(fn) { + isReady = true + var require = Anot.require + if (require && require.checkDeps) { + modules['domReady!'].state = 4 + require.checkDeps() + } + while ((fn = readyList.shift())) { + fn(Anot) + } + } + + if (DOC.readyState === 'complete') { + setTimeout(fireReady) //如果在domReady之外加载 + } else { + DOC.addEventListener('DOMContentLoaded', fireReady) + } + window.addEventListener('load', fireReady) + Anot.ready = function(fn) { + if (!isReady) { + readyList.push(fn) + } else { + fn(Anot) + } + } + // Map over Anot in case of overwrite + var _Anot = window.Anot + Anot.noConflict = function(deep) { + if (deep && window.Anot === Anot) { + window.Anot = _Anot + } + return Anot + } + + window.Anot = Anot +})() +`) + +function comment({ amd, touch, next } = {}) { + return `/*================================================== + * Anot ${touch ? 'touch' : 'normal'} version ${amd ? 'with AMD loader' : ''} ${ + next ? 'for future browsers' : '' + } + * @authors yutent (yutent@doui.cc) + * @date 2017-03-21 21:05:57 + * support IE10+ and modern browsers + * + ==================================================*/ + ` +} + +/***************************************************************************/ +/********************* 华丽丽的分割线 ****************************/ +/***************************************************************************/ + +const BUFFER_CACHE = {} +const LIB_QUEUE = [] + +function loadFiles() { + let files = fs.ls('./src/') + for (let it of files) { + if (fs.isdir(it)) { + continue + } + BUFFER_CACHE[it] = fs.cat(it) + LIB_QUEUE.push(it) + } +} + +function updateBuffer(file) { + BUFFER_CACHE[file] = fs.cat(file) +} + +// 打包,但不压缩 +function packNoCompress(file) { + if (file) { + updateBuffer(file) + } + let libs = LIB_QUEUE.map(it => { + return BUFFER_CACHE[it] + }) + let touchModule = fs.cat('./src/lib/touch.js') + let amdModule = fs.cat('./src/lib/amd.js') + + let shim = Buffer.concat(libs.concat(amdModule)) + let touchShim = Buffer.concat([shim, touchModule]) + + fs.echo( + Buffer.concat([PAD_START_SHIM, shim, PAD_END_SHIM]), + './dist/anot.shim.js' + ) + log(chalk.green('anot.shim.js 打包完成...')) + fs.echo( + Buffer.concat([PAD_START_SHIM, touchShim, PAD_END_SHIM]), + './dist/anot-touch.shim.js' + ) + log(chalk.green('anot-touch.shim.js 打包完成...')) +} + +// 打包并压缩 +function packAndCompress() { + let libs = LIB_QUEUE.map(it => { + return BUFFER_CACHE[it] + }) + let touchModule = fs.cat('./src/lib/touch.js') + let amdModule = fs.cat('./src/lib/amd.js') + + let normal = Buffer.concat(libs) + let touchNormal = Buffer.concat([normal, touchModule]) + let shim = Buffer.concat([normal, amdModule]) + let touchShim = Buffer.concat([shim, touchModule]) + + /** + * -------------------------------------------------------- + * 打包普通版 anot + * -------------------------------------------------------- + */ + log('正在打包 anot.js...') + let normalVer = Buffer.concat([PAD_START, normal, PAD_END]).toString() + fs.echo(comment() + uglify.minify(normalVer).code, './dist/anot.js') + log(chalk.green('anot.js 打包压缩完成!')) + + /** + * -------------------------------------------------------- + * 打包带触摸事件的普通版 anot + * -------------------------------------------------------- + */ + log('正在打包 anot-touch.js...') + let touchNormalVer = Buffer.concat([ + PAD_START, + touchNormal, + PAD_END + ]).toString() + + fs.echo( + comment({ touch: true }) + uglify.minify(touchNormalVer).code, + './dist/anot-touch.js' + ) + + log(chalk.green('anot-touch.js 打包压缩完成...')) + + /** + * -------------------------------------------------------- + * 打包自带AMD加载器的 anot + * -------------------------------------------------------- + */ + + log('正在打包 anot.shim.js...') + let shimVer = Buffer.concat([PAD_START_SHIM, shim, PAD_END_SHIM]).toString() + fs.echo( + comment({ amd: true }) + uglify.minify(shimVer).code, + './dist/anot.shim.js' + ) + log(chalk.green('anot.shim.js 打包压缩完成!')) + + /** + * -------------------------------------------------------- + * 打包自带AMD加载器及触摸事件的 anot + * -------------------------------------------------------- + */ + + log('正在打包 anot-touch.shim.js...') + let touchShimVer = Buffer.concat([ + PAD_START_SHIM, + touchShim, + PAD_END_SHIM + ]).toString() + fs.echo( + comment({ amd: true, touch: true }) + uglify.minify(touchShimVer).code, + './dist/anot-touch.shim.js' + ) + log(chalk.green('anot-touch.shim.js 打包压缩完成...')) + + /** + * -------------------------------------------------------- + * 打包未来版的 anot + * -------------------------------------------------------- + */ + + log('正在打包 anot.next.js...') + let nextVer = Buffer.concat([PAD_START, normal, PAD_END_NEXT]).toString() + fs.echo( + comment({ next: true }) + uglify.minify(nextVer).code, + './dist/anot.next.js' + ) + log(chalk.green('anot.next.js 打包压缩完成!')) + + /** + * -------------------------------------------------------- + * 打包带触摸事件的未来版的 anot + * -------------------------------------------------------- + */ + + log('正在打包 anot-touch.next.js...') + let touchNextVer = Buffer.concat([ + PAD_START, + touchNormal, + PAD_END_NEXT + ]).toString() + fs.echo( + comment({ touch: true, next: true }) + uglify.minify(touchNextVer).code, + './dist/anot-touch.next.js' + ) + log(chalk.green('anot-touch.next.js 打包压缩完成!')) +} + +let args = process.argv.slice(2) +let mode = args.shift() +let ready = false + +switch (mode) { + case 'dev': + chokidar + .watch(path.resolve('./src/')) + .on('all', (act, file) => { + if (!ready) { + log(act, file) + return + } + if (act === 'add' || act === 'change') { + packNoCompress(file) + } + }) + .on('ready', () => { + log('正在执行首次打包...') + loadFiles() + packNoCompress() + log(chalk.red('预处理完成,监听文件变化中,请勿关闭本窗口...')) + ready = true + }) + break + case 'prod': + loadFiles() + packAndCompress() + break + default: + log(chalk.red('无效编译参数!')) + let buf = Buffer.concat(loadFiles()) + log(buf.toString()) + break +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..dd4088f --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "anot", + "version": "1.0.0", + "description": "Anot - 迷你mvvm框架", + "main": "dist/anot.js", + "files": [ + "dist" + ], + "scripts": { + "start": "npm run dev", + "dev": "node ./pack.config.js dev", + "prod": "node ./pack.config.js prod" + }, + "keywords": [ + "anot", + "avalon", + "mvvm", + "doui", + "yutent" + ], + "dependencies": {}, + "devDependencies": { + "chalk": "^2.4.1", + "chokidar": "^2.0.4", + "es.shim": "^1.1.2", + "iofs": "^1.1.0", + "uglify-es": "^3.3.9" + }, + "author": "yutent", + "license": "MIT" +} diff --git a/src/00-generate.js b/src/00-generate.js new file mode 100644 index 0000000..cedf88e --- /dev/null +++ b/src/00-generate.js @@ -0,0 +1,88 @@ +/********************************************************************* + * 全局变量及方法 * + **********************************************************************/ +var bindingID = 1024 +var IEVersion = 0 +if (window.VBArray) { + IEVersion = document.documentMode || (window.XMLHttpRequest ? 7 : 6) +} +var expose = generateID() +//http://stackoverflow.com/questions/7290086/javascript-use-strict-and-nicks-find-global-function +var DOC = window.document +var head = DOC.head //HEAD元素 +head.insertAdjacentHTML( + 'afterBegin', + '' +) +var ifGroup = head.firstChild + +function log() { + // http://stackoverflow.com/questions/8785624/how-to-safely-wrap-console-log + console.log.apply(console, arguments) +} + +/** + * Creates a new object without a prototype. This object is useful for lookup without having to + * guard against prototypically inherited properties via hasOwnProperty. + * + * Related micro-benchmarks: + * - http://jsperf.com/object-create2 + * - http://jsperf.com/proto-map-lookup/2 + * - http://jsperf.com/for-in-vs-object-keys2 + */ +function createMap() { + return Object.create(null) +} + +var subscribers = '$' + expose + +var nullObject = {} //作用类似于noop,只用于代码防御,千万不要在它上面添加属性 +var rword = /[^, ]+/g //切割字符串为一个个小块,以空格或豆号分开它们,结合replace实现字符串的forEach +var rw20g = /\w+/g +var rsvg = /^\[object SVG\w*Element\]$/ +var oproto = Object.prototype +var ohasOwn = oproto.hasOwnProperty +var serialize = oproto.toString +var ap = Array.prototype +var aslice = ap.slice +var W3C = window.dispatchEvent +var root = DOC.documentElement +var anotFragment = DOC.createDocumentFragment() +var cinerator = DOC.createElement('div') +var class2type = { + '[object Boolean]': 'boolean', + '[object Number]': 'number', + '[object String]': 'string', + '[object Function]': 'function', + '[object Array]': 'array', + '[object Date]': 'date', + '[object RegExp]': 'regexp', + '[object Object]': 'object', + '[object Error]': 'error', + '[object AsyncFunction]': 'asyncfunction', + '[object Promise]': 'promise', + '[object Generator]': 'generator', + '[object GeneratorFunction]': 'generatorfunction' +} + +function noop() {} +function scpCompile(array) { + return Function.apply(noop, array) +} + +function oneObject(array, val) { + if (typeof array === 'string') { + array = array.match(rword) || [] + } + var result = {}, + value = val !== void 0 ? val : 1 + for (var i = 0, n = array.length; i < n; i++) { + result[array[i]] = value + } + return result +} + +function generateID(mark) { + mark = (mark && mark + '-') || 'anot-' + return mark + (++bindingID).toString(16) +} diff --git a/src/01-profill.js b/src/01-profill.js new file mode 100644 index 0000000..56590e4 --- /dev/null +++ b/src/01-profill.js @@ -0,0 +1,463 @@ +/*-----------------部分ES6的JS实现 start---------------*/ + +// =============================== +// ========== Promise ============ +// =============================== +;(function(nativePromise) { + function _yes(val) { + return val + } + + function _no(err) { + throw err + } + + function done(callback) { + return this.then(callback, _no) + } + + function fail(callback) { + return this.then(_yes, callback) + } + + function defer() { + var obj = {} + obj.promise = new _Promise(function(yes, no) { + obj.resolve = yes + obj.reject = no + }) + return obj + } + + //成功的回调 + function _resolve(obj, val) { + if (obj._state !== 'pending') { + return + } + + if (val && typeof val.then === 'function') { + var method = val instanceof _Promise ? '_then' : 'then' + val[method]( + function(v) { + _transmit(obj, v, true) + }, + function(v) { + _transmit(obj, v, false) + } + ) + } else { + _transmit(obj, val, true) + } + } + + //失败的回调 + function _reject(obj, val) { + if (obj._state !== 'pending') { + return + } + + _transmit(obj, val, false) + } + + // 改变Promise的_fired值,并保持用户传参,触发所有回调 + function _transmit(obj, val, isResolved) { + obj._fired = true + obj._val = val + obj._state = isResolved ? 'fulfilled' : 'rejected' + + fireCallback(obj, function() { + for (var i in obj.callback) { + obj._fire(obj.callback[i].yes, obj.callback[i].no) + } + }) + } + + function fireCallback(obj, callback) { + var isAsync = false + + if (typeof obj.async === 'boolean') { + isAsync = obj.async + } else { + isAsync = obj.async = true + } + + if (isAsync) { + setTimeout(callback, 0) + } else { + callback() + } + } + + function _some(bool, iterable) { + iterable = Array.isArray(iterable) ? iterable : [] + + var n = 0 + var res = [] + var end = false + + return new _Promise(function(yes, no) { + if (!iterable.length) no(res) + + function loop(obj, idx) { + obj.then( + function(val) { + if (!end) { + res[idx] = val + n++ + if (bool || n >= iterable.length) { + yes(bool ? val : res) + end = true + } + } + }, + function(val) { + end = true + no(val) + } + ) + } + + for (var i = 0, len = iterable.length; i < len; i++) { + loop(iterable[i], i) + } + }) + } + + //--------------------------- + var _Promise = function(callback) { + this.callback = [] + var _this = this + + if (typeof this !== 'object') { + throw new TypeError('Promises must be constructed via new') + } + + if (typeof callback !== 'function') { + throw new TypeError('Argument must be a function') + } + + callback( + function(val) { + _resolve(_this, val) + }, + function(val) { + _reject(_this, val) + } + ) + } + var self = { + _state: 1, + _fired: 1, + _val: 1, + callback: 1 + } + + _Promise.prototype = { + constructor: _Promise, + _state: 'pending', + _fired: false, + _fire: function(yes, no) { + if (this._state === 'rejected') { + if (typeof no === 'function') no(this._val) + else throw this._val + } else { + if (typeof yes === 'function') yes(this._val) + } + }, + _then: function(yes, no) { + if (this._fired) { + var _this = this + fireCallback(_this, function() { + _this._fire(yes, no) + }) + } else { + this.callback.push({ yes: yes, no: no }) + } + }, + then: function(yes, no) { + yes = typeof yes === 'function' ? yes : _yes + no = typeof no === 'function' ? no : _no + var _this = this + var next = new _Promise(function(resolve, reject) { + _this._then( + function(val) { + try { + val = yes(val) + } catch (err) { + return reject(err) + } + resolve(val) + }, + function(val) { + try { + val = no(val) + } catch (err) { + return reject(err) + } + resolve(val) + } + ) + }) + for (var i in _this) { + if (!self[i]) next[i] = _this[i] + } + return next + }, + done: done, + catch: fail, + fail: fail + } + + _Promise.all = function(arr) { + return _some(false, arr) + } + + _Promise.race = function(arr) { + return _some(true, arr) + } + + _Promise.defer = defer + + _Promise.resolve = function(val) { + var obj = this.defer() + obj.resolve(val) + return obj.promise + } + + _Promise.reject = function(val) { + var obj = this.defer() + obj.reject(val) + return obj.promise + } + if (/native code/.test(nativePromise)) { + nativePromise.prototype.done = done + nativePromise.prototype.fail = fail + if (!nativePromise.defer) { + nativePromise.defer = defer + } + } + window.Promise = nativePromise || _Promise +})(window.Promise) + +if (!Object.assign) { + Object.defineProperty(Object, 'assign', { + enumerable: false, + value: function(target, first) { + 'use strict' + if (target === undefined || target === null) + throw new TypeError('Can not convert first argument to object') + + var to = Object(target) + for (var i = 0, len = arguments.length; i < len; i++) { + var next = arguments[i] + if (next === undefined || next === null) continue + + var keys = Object.keys(Object(next)) + for (var j = 0, n = keys.length; j < n; j++) { + var key = keys[j] + var desc = Object.getOwnPropertyDescriptor(next, key) + if (desc !== undefined && desc.enumerable) to[key] = next[key] + } + } + return to + } + }) +} + +if (!Array.from) { + Object.defineProperty(Array, 'from', { + enumerable: false, + value: (function() { + var toStr = Object.prototype.toString + var isCallable = function(fn) { + return ( + typeof fn === 'function' || toStr.call(fn) === '[object Function]' + ) + } + + var toInt = function(val) { + var num = val - 0 + if (isNaN(num)) return 0 + + if (num === 0 || isFinite(num)) return num + + return (num > 0 ? 1 : -1) * Math.floor(Math.abs(num)) + } + var maxInt = Math.pow(2, 53) - 1 + var toLen = function(val) { + var len = toInt(val) + return Math.min(Math.max(len, 0), maxInt) + } + + return function(arrLike) { + var _this = this + var items = Object(arrLike) + if (arrLike === null) + throw new TypeError( + 'Array.from requires an array-like object - not null or undefined' + ) + + var mapFn = arguments.length > 1 ? arguments[1] : undefined + var other + if (mapFn !== undefined) { + if (!isCallable(mapFn)) + throw new TypeError( + 'Array.from: when provided, the second argument must be a function' + ) + + if (arguments.length > 2) other = arguments[2] + } + + var len = toLen(items.length) + var arr = isCallable(_this) ? Object(new _this(len)) : new Array(len) + var k = 0 + var kVal + while (k < len) { + kVal = items[k] + if (mapFn) + arr[k] = + other === 'undefined' + ? mapFn(kVal, k) + : mapFn.call(other, kVal, k) + else arr[k] = kVal + + k++ + } + arr.length = len + return arr + } + })() + }) +} + +// 判断数组是否包含指定元素 +if (!Array.prototype.includes) { + Object.defineProperty(Array.prototype, 'includes', { + value: function(val) { + for (var i in this) { + if (this[i] === val) return true + } + return false + }, + enumerable: false + }) +} + +//类似于Array 的splice方法 +if (!String.prototype.splice) { + Object.defineProperty(String.prototype, 'splice', { + value: function(start, len, fill) { + var length = this.length, + argLen = arguments.length + + fill = fill === undefined ? '' : fill + + if (argLen < 1) { + return this + } + + //处理负数 + if (start < 0) { + if (Math.abs(start) >= length) start = 0 + else start = length + start + } + + if (argLen === 1) { + return this.slice(0, start) + } else { + len -= 0 + + var strl = this.slice(0, start), + strr = this.slice(start + len) + + return strl + fill + strr + } + }, + enumerable: false + }) +} + +if (!Date.prototype.getFullWeek) { + //获取当天是本年度第几周 + Object.defineProperty(Date.prototype, 'getFullWeek', { + value: function() { + var thisYear = this.getFullYear(), + that = new Date(thisYear, 0, 1), + firstDay = that.getDay() || 1, + numsOfToday = (this - that) / 86400000 + return Math.ceil((numsOfToday + firstDay) / 7) + }, + enumerable: false + }) + + //获取当天是本月第几周 + Object.defineProperty(Date.prototype, 'getWeek', { + value: function() { + var today = this.getDate(), + thisMonth = this.getMonth(), + thisYear = this.getFullYear(), + firstDay = new Date(thisYear, thisMonth, 1).getDay() + return Math.ceil((today + firstDay) / 7) + }, + enumerable: false + }) +} + +if (!Date.isDate) { + Object.defineProperty(Date, 'isDate', { + value: function(obj) { + return typeof obj === 'object' && obj.getTime ? true : false + }, + enumerable: false + }) +} + +//时间格式化 +if (!Date.prototype.format) { + Object.defineProperty(Date.prototype, 'format', { + value: function(str) { + str = str || 'Y-m-d H:i:s' + var week = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], + dt = { + fullyear: this.getFullYear(), + year: this.getYear(), + fullweek: this.getFullWeek(), + week: this.getWeek(), + month: this.getMonth() + 1, + date: this.getDate(), + day: week[this.getDay()], + hours: this.getHours(), + minutes: this.getMinutes(), + seconds: this.getSeconds() + }, + re + + dt.g = dt.hours > 12 ? dt.hours - 12 : dt.hours + + re = { + Y: dt.fullyear, + y: dt.year, + m: dt.month < 10 ? '0' + dt.month : dt.month, + n: dt.month, + d: dt.date < 10 ? '0' + dt.date : dt.date, + j: dt.date, + H: dt.hours < 10 ? '0' + dt.hours : dt.hours, + h: dt.g < 10 ? '0' + dt.g : dt.g, + G: dt.hours, + g: dt.g, + i: dt.minutes < 10 ? '0' + dt.minutes : dt.minutes, + s: dt.seconds < 10 ? '0' + dt.seconds : dt.seconds, + W: dt.fullweek, + w: dt.week, + D: dt.day + } + + for (var i in re) { + str = str.replace(new RegExp(i, 'g'), re[i]) + } + return str + }, + enumerable: false + }) +} +/*-----------------部分ES6的JS实现 ending---------------*/ diff --git a/src/02-core.js b/src/02-core.js new file mode 100644 index 0000000..7e870a3 --- /dev/null +++ b/src/02-core.js @@ -0,0 +1,540 @@ +var Anot = function(el) { + //创建jQuery式的无new 实例化结构 + return new Anot.init(el) +} + +/*视浏览器情况采用最快的异步回调*/ +Anot.nextTick = new function() { + // jshint ignore:line + var tickImmediate = window.setImmediate + var tickObserver = window.MutationObserver + if (tickImmediate) { + return tickImmediate.bind(window) + } + + var queue = [] + function callback() { + var n = queue.length + for (var i = 0; i < n; i++) { + queue[i]() + } + queue = queue.slice(n) + } + + if (tickObserver) { + var node = document.createTextNode('anot') + new tickObserver(callback).observe(node, { characterData: true }) // jshint ignore:line + var bool = false + return function(fn) { + queue.push(fn) + bool = !bool + node.data = bool + } + } + + return function(fn) { + setTimeout(fn, 4) + } +}() // jshint ignore:line + +/********************************************************************* + * Anot的静态方法定义区 * + **********************************************************************/ + +Anot.type = function(obj) { + //取得目标的类型 + if (obj == null) { + return String(obj) + } + // 早期的webkit内核浏览器实现了已废弃的ecma262v4标准,可以将正则字面量当作函数使用,因此typeof在判定正则时会返回function + return typeof obj === 'object' || typeof obj === 'function' + ? class2type[serialize.call(obj)] || 'object' + : typeof obj +} + +Anot.PropsTypes = function(type) { + this.type = 'PropsTypes' + this.checkType = type +} + +Anot.PropsTypes.prototype = { + toString: function() { + return '' + }, + check: function(val) { + this.result = Anot.type(val) + return this.result === this.checkType + }, + call: function() { + return this.toString() + } +} + +Anot.PropsTypes.isString = function() { + return new this('string') +} + +Anot.PropsTypes.isNumber = function() { + return new this('number') +} + +Anot.PropsTypes.isFunction = function() { + return new this('function') +} + +Anot.PropsTypes.isArray = function() { + return new this('array') +} + +Anot.PropsTypes.isObject = function() { + return new this('object') +} + +Anot.PropsTypes.isBoolean = function() { + return new this('boolean') +} + +/*判定是否是一个朴素的javascript对象(Object),不是DOM对象,不是BOM对象,不是自定义类的实例*/ +Anot.isPlainObject = function(obj) { + // 简单的 typeof obj === "object"检测,会致使用isPlainObject(window)在opera下通不过 + return ( + serialize.call(obj) === '[object Object]' && + Object.getPrototypeOf(obj) === oproto + ) +} + +var VMODELS = (Anot.vmodels = {}) //所有vmodel都储存在这里 +Anot.init = function(source) { + if (Anot.isPlainObject(source)) { + var $id = source.$id + var vm = null + if (!$id) { + log('warning: vm必须指定id') + } + vm = modelFactory(Object.assign({ props: {} }, source)) + vm.$id = $id + VMODELS[$id] = vm + + Anot.nextTick(function() { + var $elem = document.querySelector('[anot=' + vm.$id + ']') + if ($elem) { + if ($elem === DOC.body) { + scanTag($elem, []) + } else { + var _parent = $elem + while ((_parent = _parent.parentNode)) { + if (_parent.__VM__) { + break + } + } + scanTag($elem.parentNode, _parent ? [_parent.__VM__] : []) + } + } + }) + + return vm + } else { + this[0] = this.element = source + } +} +Anot.fn = Anot.prototype = Anot.init.prototype + +//与jQuery.extend方法,可用于浅拷贝,深拷贝 +Anot.mix = Anot.fn.mix = function() { + var options, + name, + src, + copy, + copyIsArray, + clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false + + // 如果第一个参数为布尔,判定是否深拷贝 + if (typeof target === 'boolean') { + deep = target + target = arguments[1] || {} + i++ + } + + //确保接受方为一个复杂的数据类型 + if (typeof target !== 'object' && Anot.type(target) !== 'function') { + target = {} + } + + //如果只有一个参数,那么新成员添加于mix所在的对象上 + if (i === length) { + target = this + i-- + } + + for (; i < length; i++) { + //只处理非空参数 + if ((options = arguments[i]) != null) { + for (name in options) { + src = target[name] + copy = options[name] + // 防止环引用 + if (target === copy) { + continue + } + if ( + deep && + copy && + (Anot.isPlainObject(copy) || (copyIsArray = Array.isArray(copy))) + ) { + if (copyIsArray) { + copyIsArray = false + clone = src && Array.isArray(src) ? src : [] + } else { + clone = src && Anot.isPlainObject(src) ? src : {} + } + + target[name] = Anot.mix(deep, clone, copy) + } else if (copy !== void 0) { + target[name] = copy + } + } + } + } + return target +} + +function cacheStore(tpye, key, val) { + if (!window[tpye]) { + return log('该浏览器不支持本地储存' + tpye) + } + + if (this.type(key) === 'object') { + for (var i in key) { + window[tpye].setItem(i, key[i]) + } + return + } + switch (arguments.length) { + case 2: + return window[tpye].getItem(key) + case 3: + if ((this.type(val) == 'string' && val.trim() === '') || val === null) { + window[tpye].removeItem(key) + return + } + if (this.type(val) !== 'object' && this.type(val) !== 'array') { + window[tpye].setItem(key, val.toString()) + } else { + window[tpye].setItem(key, JSON.stringify(val)) + } + break + } +} + +/*判定是否类数组,如节点集合,纯数组,arguments与拥有非负整数的length属性的纯JS对象*/ +function isArrayLike(obj) { + if (obj && typeof obj === 'object') { + var n = obj.length, + str = serialize.call(obj) + if (/(Array|List|Collection|Map|Arguments)\]$/.test(str)) { + return true + } else if (str === '[object Object]' && n === n >>> 0) { + return true //由于ecma262v5能修改对象属性的enumerable,因此不能用propertyIsEnumerable来判定了 + } + } + return false +} + +Anot.mix({ + rword: rword, + subscribers: subscribers, + version: '1.0.0', + log: log, + ui: {}, //仅用于存放组件版本信息等 + slice: function(nodes, start, end) { + return aslice.call(nodes, start, end) + }, + noop: noop, + /*如果不用Error对象封装一下,str在控制台下可能会乱码*/ + error: function(str, e) { + throw new (e || Error)(str) // jshint ignore:line + }, + /* Anot.range(10) + => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + Anot.range(1, 11) + => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + Anot.range(0, 30, 5) + => [0, 5, 10, 15, 20, 25] + Anot.range(0, -10, -1) + => [0, -1, -2, -3, -4, -5, -6, -7, -8, -9] + Anot.range(0) + => []*/ + range: function(start, end, step) { + // 用于生成整数数组 + step || (step = 1) + if (end == null) { + end = start || 0 + start = 0 + } + var index = -1, + length = Math.max(0, Math.ceil((end - start) / step)), + result = new Array(length) + while (++index < length) { + result[index] = start + start += step + } + return result + }, + deepCopy: toJson, + eventHooks: {}, + /*绑定事件*/ + bind: function(el, type, fn, phase) { + var hooks = Anot.eventHooks + type = type.split(',') + Anot.each(type, function(i, t) { + t = t.trim() + var hook = hooks[t] + if (typeof hook === 'object') { + type = hook.type || type + phase = hook.phase || !!phase + fn = hook.fix ? hook.fix(el, fn) : fn + } + el.addEventListener(t, fn, phase) + }) + return fn + }, + /*卸载事件*/ + unbind: function(el, type, fn, phase) { + var hooks = Anot.eventHooks + type = type.split(',') + fn = fn || noop + Anot.each(type, function(i, t) { + t = t.trim() + var hook = hooks[t] + if (typeof hook === 'object') { + type = hook.type || type + phase = hook.phase || !!phase + } + el.removeEventListener(t, fn, phase) + }) + }, + /*读写删除元素节点的样式*/ + css: function(node, name, value) { + if (node instanceof Anot) { + node = node[0] + } + var prop = /[_-]/.test(name) ? camelize(name) : name, + fn + name = Anot.cssName(prop) || prop + if (value === void 0 || typeof value === 'boolean') { + //获取样式 + fn = cssHooks[prop + ':get'] || cssHooks['@:get'] + if (name === 'background') { + name = 'backgroundColor' + } + var val = fn(node, name) + return value === true ? parseFloat(val) || 0 : val + } else if (value === '') { + //请除样式 + node.style[name] = '' + } else { + //设置样式 + if (value == null || value !== value) { + return + } + if (isFinite(value) && !Anot.cssNumber[prop]) { + value += 'px' + } + fn = cssHooks[prop + ':set'] || cssHooks['@:set'] + fn(node, name, value) + } + }, + /*遍历数组与对象,回调的第一个参数为索引或键名,第二个或元素或键值*/ + each: function(obj, fn) { + if (obj) { + //排除null, undefined + var i = 0 + if (isArrayLike(obj)) { + for (var n = obj.length; i < n; i++) { + if (fn(i, obj[i]) === false) break + } + } else { + for (i in obj) { + if (obj.hasOwnProperty(i) && fn(i, obj[i]) === false) { + break + } + } + } + } + }, + Array: { + /*只有当前数组不存在此元素时只添加它*/ + ensure: function(target, item) { + if (target.indexOf(item) === -1) { + return target.push(item) + } + }, + /*移除数组中指定位置的元素,返回布尔表示成功与否*/ + removeAt: function(target, index) { + return !!target.splice(index, 1).length + }, + /*移除数组中第一个匹配传参的那个元素,返回布尔表示成功与否*/ + remove: function(target, item) { + var index = target.indexOf(item) + if (~index) return Anot.Array.removeAt(target, index) + return false + } + }, + /** + * [ls localStorage操作] + * @param {[type]} key [键名] + * @param {[type]} val [键值,为空时删除] + * @return + */ + ls: function() { + var args = aslice.call(arguments, 0) + args.unshift('localStorage') + return cacheStore.apply(this, args) + }, + ss: function() { + var args = aslice.call(arguments, 0) + args.unshift('sessionStorage') + return cacheStore.apply(this, args) + }, + /** + * [cookie cookie 操作 ] + * @param key [cookie名] + * @param val [cookie值] + * @param {[json]} opt [有效期,域名,路径等] + * @return {[boolean]} [读取时返回对应的值,写入时返回true] + */ + cookie: function(key, val, opt) { + if (arguments.length > 1) { + if (!key) { + return + } + + //设置默认的参数 + opt = opt || {} + opt = Object.assign( + { + expires: '', + path: '/', + domain: document.domain, + secure: '' + }, + opt + ) + + if ((this.type(val) == 'string' && val.trim() === '') || val === null) { + document.cookie = + encodeURIComponent(key) + + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=' + + opt.domain + + '; path=' + + opt.path + return true + } + if (opt.expires) { + switch (opt.expires.constructor) { + case Number: + opt.expires = + opt.expires === Infinity + ? '; expires=Fri, 31 Dec 9999 23:59:59 GMT' + : '; max-age=' + opt.expires + break + case String: + opt.expires = '; expires=' + opt.expires + break + case Date: + opt.expires = '; expires=' + opt.expires.toUTCString() + break + } + } + document.cookie = + encodeURIComponent(key) + + '=' + + encodeURIComponent(val) + + opt.expires + + '; domain=' + + opt.domain + + '; path=' + + opt.path + + '; ' + + opt.secure + return true + } else { + if (!key) { + return document.cookie + } + return ( + decodeURIComponent( + document.cookie.replace( + new RegExp( + '(?:(?:^|.*;)\\s*' + + encodeURIComponent(key).replace(/[\-\.\+\*]/g, '\\$&') + + '\\s*\\=\\s*([^;]*).*$)|^.*$' + ), + '$1' + ) + ) || null + ) + } + }, + //获取url的参数 + search: function(key) { + key += '' + var uri = location.search + + if (!key || !uri) return null + + uri = uri.slice(1) + uri = uri.split('&') + + var obj = {} + for (var i = 0, item; (item = uri[i++]); ) { + var tmp = item.split('=') + tmp[1] = tmp.length < 2 ? null : tmp[1] + tmp[1] = decodeURIComponent(tmp[1]) + if (obj.hasOwnProperty(tmp[0])) { + if (typeof obj[tmp[0]] === 'object') { + obj[tmp[0]].push(tmp[1]) + } else { + obj[tmp[0]] = [obj[tmp[0]]] + obj[tmp[0]].push(tmp[1]) + } + } else { + obj[tmp[0]] = tmp[1] + } + } + return obj.hasOwnProperty(key) ? obj[key] : null + }, + //复制文本到粘贴板 + copy: function(txt) { + if (!DOC.queryCommandSupported || !DOC.queryCommandSupported('copy')) { + return log('该浏览器不支持复制到粘贴板') + } + + var ta = DOC.createElement('textarea') + ta.textContent = txt + ta.style.position = 'fixed' + ta.style.bottom = '-1000px' + DOC.body.appendChild(ta) + ta.select() + try { + DOC.execCommand('copy') + } catch (err) { + log('复制到粘贴板失败') + } + DOC.body.removeChild(ta) + } +}) + +var bindingHandlers = (Anot.bindingHandlers = {}) +var bindingExecutors = (Anot.bindingExecutors = {}) + +var directives = (Anot.directives = {}) +Anot.directive = function(name, obj) { + bindingHandlers[name] = obj.init = obj.init || noop + bindingExecutors[name] = obj.update = obj.update || noop + return (directives[name] = obj) +} diff --git a/src/03-cache.js b/src/03-cache.js new file mode 100644 index 0000000..fc01d26 --- /dev/null +++ b/src/03-cache.js @@ -0,0 +1,72 @@ +// https://github.com/rsms/js-lru +var Cache = new function() { + // jshint ignore:line + function LRU(maxLength) { + this.size = 0 + this.limit = maxLength + this.head = this.tail = void 0 + this._keymap = {} + } + + var p = LRU.prototype + + p.put = function(key, value) { + var entry = { + key: key, + value: value + } + this._keymap[key] = entry + if (this.tail) { + this.tail.newer = entry + entry.older = this.tail + } else { + this.head = entry + } + this.tail = entry + if (this.size === this.limit) { + this.shift() + } else { + this.size++ + } + return value + } + + p.shift = function() { + var entry = this.head + if (entry) { + this.head = this.head.newer + this.head.older = entry.newer = entry.older = this._keymap[ + entry.key + ] = void 0 + delete this._keymap[entry.key] //#1029 + } + } + p.get = function(key) { + var entry = this._keymap[key] + if (entry === void 0) return + if (entry === this.tail) { + return entry.value + } + // HEAD--------------TAIL + // <.older .newer> + // <--- add direction -- + // A B C E + if (entry.newer) { + if (entry === this.head) { + this.head = entry.newer + } + entry.newer.older = entry.older // C <-- E. + } + if (entry.older) { + entry.older.newer = entry.newer // C. --> E + } + entry.newer = void 0 // D --x + entry.older = this.tail // D. --> E + if (this.tail) { + this.tail.newer = entry // E. <-- D + } + this.tail = entry + return entry.value + } + return LRU +}() // jshint ignore:line diff --git a/src/04-dom.patch.js b/src/04-dom.patch.js new file mode 100644 index 0000000..05df89b --- /dev/null +++ b/src/04-dom.patch.js @@ -0,0 +1,156 @@ +/********************************************************************* + * DOM 底层补丁 * + **********************************************************************/ + +//safari5+是把contains方法放在Element.prototype上而不是Node.prototype +if (!DOC.contains) { + Node.prototype.contains = function(arg) { + return !!(this.compareDocumentPosition(arg) & 16) + } +} +Anot.contains = function(root, el) { + try { + while ((el = el.parentNode)) if (el === root) return true + return false + } catch (e) { + return false + } +} + +if (window.SVGElement) { + var svgns = 'http://www.w3.org/2000/svg' + var svg = DOC.createElementNS(svgns, 'svg') + svg.innerHTML = '' + if (!rsvg.test(svg.firstChild)) { + // #409 + /* jshint ignore:start */ + function enumerateNode(node, targetNode) { + if (node && node.childNodes) { + var nodes = node.childNodes + for (var i = 0, el; (el = nodes[i++]); ) { + if (el.tagName) { + var svg = DOC.createElementNS(svgns, el.tagName.toLowerCase()) + // copy attrs + ap.forEach.call(el.attributes, function(attr) { + svg.setAttribute(attr.name, attr.value) + }) + // 递归处理子节点 + enumerateNode(el, svg) + targetNode.appendChild(svg) + } + } + } + } + /* jshint ignore:end */ + Object.defineProperties(SVGElement.prototype, { + outerHTML: { + //IE9-11,firefox不支持SVG元素的innerHTML,outerHTML属性 + enumerable: true, + configurable: true, + get: function() { + return new XMLSerializer().serializeToString(this) + }, + set: function(html) { + var tagName = this.tagName.toLowerCase(), + par = this.parentNode, + frag = Anot.parseHTML(html) + // 操作的svg,直接插入 + if (tagName === 'svg') { + par.insertBefore(frag, this) + // svg节点的子节点类似 + } else { + var newFrag = DOC.createDocumentFragment() + enumerateNode(frag, newFrag) + par.insertBefore(newFrag, this) + } + par.removeChild(this) + } + }, + innerHTML: { + enumerable: true, + configurable: true, + get: function() { + var s = this.outerHTML + var ropen = new RegExp( + '<' + this.nodeName + '\\b(?:(["\'])[^"]*?(\\1)|[^>])*>', + 'i' + ) + var rclose = new RegExp('$', 'i') + return s.replace(ropen, '').replace(rclose, '') + }, + set: function(html) { + if (Anot.clearHTML) { + Anot.clearHTML(this) + var frag = Anot.parseHTML(html) + enumerateNode(frag, this) + } + } + } + }) + } +} + +//========================= event binding ==================== + +var eventHooks = Anot.eventHooks + +//针对firefox, chrome修正mouseenter, mouseleave(chrome30+) +if (!('onmouseenter' in root)) { + Anot.each( + { + mouseenter: 'mouseover', + mouseleave: 'mouseout' + }, + function(origType, fixType) { + eventHooks[origType] = { + type: fixType, + fix: function(elem, fn) { + return function(e) { + var t = e.relatedTarget + if (!t || (t !== elem && !(elem.compareDocumentPosition(t) & 16))) { + delete e.type + e.type = origType + return fn.call(elem, e) + } + } + } + } + } + ) +} + +//针对IE9+, w3c修正animationend +Anot.each( + { + AnimationEvent: 'animationend', + WebKitAnimationEvent: 'webkitAnimationEnd' + }, + function(construct, fixType) { + if (window[construct] && !eventHooks.animationend) { + eventHooks.animationend = { + type: fixType + } + } + } +) + +if (DOC.onmousewheel === void 0) { + /* IE6-11 chrome mousewheel wheelDetla 下 -120 上 120 + firefox DOMMouseScroll detail 下3 上-3 + firefox wheel detlaY 下3 上-3 + IE9-11 wheel deltaY 下40 上-40 + chrome wheel deltaY 下100 上-100 */ + eventHooks.mousewheel = { + type: 'wheel', + fix: function(elem, fn) { + return function(e) { + e.wheelDeltaY = e.wheelDelta = e.deltaY > 0 ? -120 : 120 + e.wheelDeltaX = 0 + Object.defineProperty(e, 'type', { + value: 'mousewheel' + }) + fn.call(elem, e) + } + } + } +} diff --git a/src/05-kernel.js b/src/05-kernel.js new file mode 100644 index 0000000..cc00f65 --- /dev/null +++ b/src/05-kernel.js @@ -0,0 +1,65 @@ +/********************************************************************* + * 配置系统 * + **********************************************************************/ + +function kernel(settings) { + for (var p in settings) { + if (!ohasOwn.call(settings, p)) continue + var val = settings[p] + if (typeof kernel.plugins[p] === 'function') { + kernel.plugins[p](val) + } else if (typeof kernel[p] === 'object') { + Anot.mix(kernel[p], val) + } else { + kernel[p] = val + } + } + return this +} +Anot.config = kernel + +var openTag, + closeTag, + rexpr, + rexprg, + rbind, + rregexp = /[-.*+?^${}()|[\]\/\\]/g + +function escapeRegExp(target) { + //http://stevenlevithan.com/regex/xregexp/ + //将字符串安全格式化为正则表达式的源码 + return (target + '').replace(rregexp, '\\$&') +} + +var plugins = { + interpolate: function(array) { + openTag = array[0] + closeTag = array[1] + if (openTag === closeTag) { + throw new SyntaxError('openTag!==closeTag') + var test = openTag + 'test' + closeTag + cinerator.innerHTML = test + if ( + cinerator.innerHTML !== test && + cinerator.innerHTML.indexOf('<') > -1 + ) { + throw new SyntaxError('此定界符不合法') + } + cinerator.innerHTML = '' + } + kernel.openTag = openTag + kernel.closeTag = closeTag + var o = escapeRegExp(openTag), + c = escapeRegExp(closeTag) + rexpr = new RegExp(o + '([\\s\\S]*)' + c) + rexprg = new RegExp(o + '([\\s\\S]*)' + c, 'g') + rbind = new RegExp(o + '[\\s\\S]*' + c + '|\\s:') //此处有疑问 + } +} +kernel.plugins = plugins +kernel.plugins['interpolate'](['{{', '}}']) + +kernel.async = true +kernel.paths = {} +kernel.shim = {} +kernel.maxRepeatSize = 100 diff --git a/src/06-vm.js b/src/06-vm.js new file mode 100644 index 0000000..7237337 --- /dev/null +++ b/src/06-vm.js @@ -0,0 +1,556 @@ +function $watch(expr, binding) { + var $events = this.$events || (this.$events = {}), + queue = $events[expr] || ($events[expr] = []) + + if (typeof binding === 'function') { + var backup = binding + backup.uuid = '_' + ++bindingID + binding = { + element: root, + type: 'user-watcher', + handler: noop, + vmodels: [this], + expr: expr, + uuid: backup.uuid + } + binding.wildcard = /\*/.test(expr) + } + + if (!binding.update) { + if (/\w\.*\B/.test(expr) || expr === '*') { + binding.getter = noop + var host = this + binding.update = function() { + var args = this.fireArgs || [] + if (args[2]) binding.handler.apply(host, args) + delete this.fireArgs + } + queue.sync = true + Anot.Array.ensure(queue, binding) + } else { + Anot.injectBinding(binding) + } + if (backup) { + binding.handler = backup + } + } else if (!binding.oneTime) { + Anot.Array.ensure(queue, binding) + } + + return function() { + binding.update = binding.getter = binding.handler = noop + binding.element = DOC.createElement('a') + } +} + +function $emit(key, args) { + var event = this.$events + var _parent = null + if (event && event[key]) { + if (args) { + args[2] = key + } + var arr = event[key] + notifySubscribers(arr, args) + if (args && event['*'] && !/\./.test(key)) { + for (var sub, k = 0; (sub = event['*'][k++]); ) { + try { + sub.handler.apply(this, args) + } catch (e) {} + } + } + _parent = this.$up + if (_parent) { + if (this.$pathname) { + $emit.call(_parent, this.$pathname + '.' + key, args) //以确切的值往上冒泡 + } + $emit.call(_parent, '*.' + key, args) //以模糊的值往上冒泡 + } + } else { + _parent = this.$up + if (this.$ups) { + for (var i in this.$ups) { + $emit.call(this.$ups[i], i + '.' + key, args) //以确切的值往上冒泡 + } + return + } + if (_parent) { + var p = this.$pathname + if (p === '') p = '*' + var path = p + '.' + key + arr = path.split('.') + + args = (args && args.concat([path, key])) || [path, key] + + if (arr.indexOf('*') === -1) { + $emit.call(_parent, path, args) //以确切的值往上冒泡 + arr[1] = '*' + $emit.call(_parent, arr.join('.'), args) //以模糊的值往上冒泡 + } else { + $emit.call(_parent, path, args) //以确切的值往上冒泡 + } + } + } +} + +function collectDependency(el, key) { + do { + if (el.$watch) { + var e = el.$events || (el.$events = {}) + var array = e[key] || (e[key] = []) + dependencyDetection.collectDependency(array) + return + } + el = el.$up + if (el) { + key = el.$pathname + '.' + key + } else { + break + } + } while (true) +} + +function notifySubscribers(subs, args) { + if (!subs) return + if (new Date() - beginTime > 444 && typeof subs[0] === 'object') { + rejectDisposeQueue() + } + var users = [], + renders = [] + for (var i = 0, sub; (sub = subs[i++]); ) { + if (sub.type === 'user-watcher') { + users.push(sub) + } else { + renders.push(sub) + } + } + if (kernel.async) { + buffer.render() //1 + for (i = 0; (sub = renders[i++]); ) { + if (sub.update) { + sub.uuid = sub.uuid || '_' + ++bindingID + var uuid = sub.uuid + if (!buffer.queue[uuid]) { + buffer.queue[uuid] = '__' + buffer.queue.push(sub) + } + } + } + } else { + for (i = 0; (sub = renders[i++]); ) { + if (sub.update) { + sub.update() //最小化刷新DOM树 + } + } + } + for (i = 0; (sub = users[i++]); ) { + if ((args && args[2] === sub.expr) || sub.wildcard) { + sub.fireArgs = args + } + sub.update() + } +} + +//一些不需要被监听的属性 +var kernelProps = oneObject( + '$id,$watch,$fire,$events,$model,$active,$pathname,$up,$ups,$track,$accessors' +) + +//如果浏览器不支持ecma262v5的Object.defineProperties或者存在BUG,比如IE8 +//标准浏览器使用__defineGetter__, __defineSetter__实现 + +function modelFactory(source, options) { + options = options || {} + options.watch = true + return observeObject(source, options) +} + +function isSkip(k) { + return + k.charAt(0) === '$' || k.slice(0, 2) === '__' || kernelProps[k] +} + +//监听对象属性值的变化(注意,数组元素不是数组的属性),通过对劫持当前对象的访问器实现 +//监听对象或数组的结构变化, 对对象的键值对进行增删重排, 或对数组的进行增删重排,都属于这范畴 +// 通过比较前后代理VM顺序实现 +function Component() {} + +function observeObject(source, options) { + if ( + !source || + (source.$id && source.$accessors) || + (source.nodeName && source.nodeType > 0) + ) { + return source + } + //source为原对象,不能是元素节点或null + //options,可选,配置对象,里面有old, force, watch这三个属性 + options = options || nullObject + var force = options.force || nullObject + var old = options.old + var oldAccessors = (old && old.$accessors) || nullObject + var $vmodel = new Component() //要返回的对象, 它在IE6-8下可能被偷龙转凤 + var accessors = {} //监控属性 + var hasOwn = {} + var skip = [] + var simple = [] + var userSkip = {} + // 提取 source中的配置项, 并删除相应字段 + var state = source.state + var computed = source.computed + var methods = source.methods + var props = source.props + var watches = source.watch + var mounted = source.mounted + + delete source.state + delete source.computed + delete source.methods + delete source.props + delete source.watch + + if (source.skip) { + userSkip = oneObject(source.skip) + delete source.skip + } + + // 基础数据 + if (state) { + if (source.$id) { + // 直接删除名为props的 字段, 对于主VM对象, props将作为保留关键字 + // 下面的计算属性,方法等, 作同样的逻辑处理 + delete state.props + } + for (name in state) { + var value = state[name] + if (!kernelProps[name]) { + hasOwn[name] = true + } + if ( + typeof value === 'function' || + (value && value.nodeName && value.nodeType > 0) || + (!force[name] && (isSkip(name) || userSkip[name])) + ) { + skip.push(name) + } else if (isComputed(value)) { + log('warning:计算属性建议放在[computed]对象中统一定义') + // 转给下一步处理 + computed[name] = value + } else { + simple.push(name) + if (oldAccessors[name]) { + accessors[name] = oldAccessors[name] + } else { + accessors[name] = makeGetSet(name, value) + } + } + } + } + + //处理计算属性 + if (computed) { + delete computed.props + for (var name in computed) { + hasOwn[name] = true + ;(function(key, value) { + var old + if (typeof value === 'function') { + value = { get: value, set: noop } + } + if (typeof value.set !== 'function') { + value.set = noop + } + accessors[key] = { + get: function() { + return (old = value.get.call(this)) + }, + set: function(x) { + var older = old, + newer + value.set.call(this, x) + newer = this[key] + if (this.$fire && newer !== older) { + this.$fire(key, newer, older) + } + }, + enumerable: true, + configurable: true + } + })(name, computed[name]) // jshint ignore:line + } + } + + // 方法 + if (methods) { + delete methods.props + for (var name in methods) { + hasOwn[name] = true + skip.push(name) + } + } + + if (props) { + hideProperty($vmodel, 'props', {}) + hasOwn.props = !!source.$id + for (var name in props) { + $vmodel.props[name] = props[name] + } + } + + Object.assign(source, state, methods) + + accessors['$model'] = $modelDescriptor + $vmodel = Object.defineProperties($vmodel, accessors, source) + function trackBy(name) { + return hasOwn[name] === true + } + skip.forEach(function(name) { + $vmodel[name] = source[name] + }) + + /* jshint ignore:start */ + // hideProperty($vmodel, '$ups', null) + hideProperty($vmodel, '$id', 'anonymous') + hideProperty($vmodel, '$up', old ? old.$up : null) + hideProperty($vmodel, '$track', Object.keys(hasOwn)) + hideProperty($vmodel, '$active', false) + hideProperty($vmodel, '$pathname', old ? old.$pathname : '') + hideProperty($vmodel, '$accessors', accessors) + hideProperty($vmodel, '$events', {}) + hideProperty($vmodel, '$refs', {}) + hideProperty($vmodel, '$children', []) + hideProperty($vmodel, '$components', []) + hideProperty($vmodel, 'hasOwnProperty', trackBy) + hideProperty($vmodel, '$mounted', mounted) + if (options.watch) { + hideProperty($vmodel, '$watch', function() { + return $watch.apply($vmodel, arguments) + }) + hideProperty($vmodel, '$fire', function(path, a) { + if (path.indexOf('all!') === 0) { + var ee = path.slice(4) + for (var i in Anot.vmodels) { + var v = Anot.vmodels[i] + v.$fire && v.$fire.apply(v, [ee, a]) + } + } else if (path.indexOf('child!') === 0) { + var ee = 'props.' + path.slice(6) + for (var i in $vmodel.$children) { + var v = $vmodel.$children[i] + v.$fire && v.$fire.apply(v, [ee, a]) + } + } else { + $emit.call($vmodel, path, [a]) + } + }) + } + /* jshint ignore:end */ + + //必须设置了$active,$events + simple.forEach(function(name) { + var oldVal = old && old[name] + var val = ($vmodel[name] = state[name]) + if (val && typeof val === 'object' && !Date.isDate(val)) { + val.$up = $vmodel + val.$pathname = name + } + $emit.call($vmodel, name, [val, oldVal]) + }) + + // 属性的监听, 必须放在上一步$emit后处理, 否则会在初始时就已经触发一次 监听回调 + if (watches) { + delete watches.props + for (var key in watches) { + if (Array.isArray(watches[key])) { + var tmp + while ((tmp = watches[key].pop())) { + $watch.call($vmodel, key, tmp) + } + } else { + $watch.call($vmodel, key, watches[key]) + } + } + } + + $vmodel.$active = true + + if (old && old.$up && old.$up.$children) { + old.$up.$children.push($vmodel) + } + + return $vmodel +} + +/* + 新的VM拥有如下私有属性 + $id: vm.id + $events: 放置$watch回调与绑定对象 + $watch: 增强版$watch + $fire: 触发$watch回调 + $track:一个数组,里面包含用户定义的所有键名 + $active:boolean,false时防止依赖收集 + $model:返回一个纯净的JS对象 + $accessors:放置所有读写器的数据描述对象 + $pathname:返回此对象在上级对象的名字,注意,数组元素的$pathname为空字符串 + ============================= + skip:用于指定不可监听的属性,但VM生成是没有此属性的 + */ +function isComputed(val) { + //speed up! + if (val && typeof val === 'object') { + for (var i in val) { + if (i !== 'get' && i !== 'set') { + return false + } + } + return typeof val.get === 'function' + } +} +function makeGetSet(key, value) { + var childVm, + value = NaN + return { + get: function() { + if (this.$active) { + collectDependency(this, key) + } + return value + }, + set: function(newVal) { + if (value === newVal) return + var oldValue = value + childVm = observe(newVal, value) + if (childVm) { + value = childVm + } else { + childVm = void 0 + value = newVal + } + + if (Object(childVm) === childVm) { + childVm.$pathname = key + childVm.$up = this + } + if (this.$active) { + $emit.call(this, key, [value, oldValue]) + } + }, + enumerable: true, + configurable: true + } +} + +function observe(obj, old, hasReturn, watch) { + if (Array.isArray(obj)) { + return observeArray(obj, old, watch) + } else if (Anot.isPlainObject(obj)) { + if (old && typeof old === 'object') { + var keys = Object.keys(obj) + var keys2 = Object.keys(old) + if (keys.join(';') === keys2.join(';')) { + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + old[i] = obj[i] + } + } + return old + } + old.$active = false + } + return observeObject( + { state: obj }, + { + old: old, + watch: watch + } + ) + } + if (hasReturn) { + return obj + } +} + +function observeArray(array, old, watch) { + if (old && old.splice) { + var args = [0, old.length].concat(array) + old.splice.apply(old, args) + return old + } else { + for (var i in newProto) { + array[i] = newProto[i] + } + hideProperty(array, '$up', null) + hideProperty(array, '$pathname', '') + hideProperty(array, '$track', createTrack(array.length)) + + array._ = observeObject( + { + state: { length: NaN } + }, + { + watch: true + } + ) + array._.length = array.length + array._.$watch('length', function(a, b) { + $emit.call(array.$up, array.$pathname + '.length', [a, b]) + }) + if (watch) { + hideProperty(array, '$watch', function() { + return $watch.apply(array, arguments) + }) + } + + Object.defineProperty(array, '$model', $modelDescriptor) + + for (var j = 0, n = array.length; j < n; j++) { + var el = (array[j] = observe(array[j], 0, 1, 1)) + if (Object(el) === el) { + //#1077 + el.$up = array + } + } + + return array + } +} + +function hideProperty(host, name, value) { + Object.defineProperty(host, name, { + value: value, + writable: true, + enumerable: false, + configurable: true + }) +} +Anot.hideProperty = hideProperty + +function toJson(val) { + var xtype = Anot.type(val) + if (xtype === 'array') { + var array = [] + for (var i = 0; i < val.length; i++) { + array[i] = toJson(val[i]) + } + return array + } else if (xtype === 'object') { + var obj = {} + for (i in val) { + if (val.hasOwnProperty(i)) { + var value = val[i] + obj[i] = value && value.nodeType ? value : toJson(value) + } + } + return obj + } + return val +} + +var $modelDescriptor = { + get: function() { + return toJson(this) + }, + set: noop, + enumerable: false, + configurable: true +} diff --git a/src/07-collection.js b/src/07-collection.js new file mode 100644 index 0000000..8539b26 --- /dev/null +++ b/src/07-collection.js @@ -0,0 +1,172 @@ +/********************************************************************* + * 监控数组(:for配合使用) * + **********************************************************************/ + +var arrayMethods = ['push', 'pop', 'shift', 'unshift', 'splice'] +var arrayProto = Array.prototype +var newProto = { + notify: function() { + $emit.call(this.$up, this.$pathname) + }, + set: function(index, val) { + index = index >>> 0 + if (index > this.length) { + throw Error(index + 'set方法的第一个参数不能大于原数组长度') + } + if (this[index] !== val) { + var old = this[index] + this.splice(index, 1, val) + $emit.call(this.$up, this.$pathname + '.*', [val, old, null, index]) + } + }, + contains: function(el) { + //判定是否包含 + return this.indexOf(el) > -1 + }, + ensure: function(el) { + if (!this.contains(el)) { + //只有不存在才push + this.push(el) + } + return this + }, + pushArray: function(arr) { + return this.push.apply(this, arr) + }, + remove: function(el) { + //移除第一个等于给定值的元素 + return this.removeAt(this.indexOf(el)) + }, + removeAt: function(index) { + index = index >>> 0 + //移除指定索引上的元素 + return this.splice(index, 1) + }, + size: function() { + //取得数组长度,这个函数可以同步视图,length不能 + return this._.length + }, + removeAll: function(all) { + //移除N个元素 + if (Array.isArray(all)) { + for (var i = this.length - 1; i >= 0; i--) { + if (all.indexOf(this[i]) !== -1) { + _splice.call(this.$track, i, 1) + _splice.call(this, i, 1) + } + } + } else if (typeof all === 'function') { + for (i = this.length - 1; i >= 0; i--) { + var el = this[i] + if (all(el, i)) { + _splice.call(this.$track, i, 1) + _splice.call(this, i, 1) + } + } + } else { + _splice.call(this.$track, 0, this.length) + _splice.call(this, 0, this.length) + } + if (!W3C) { + this.$model = toJson(this) + } + this.notify() + this._.length = this.length + }, + clear: function() { + this.removeAll() + } +} + +var _splice = arrayProto.splice +arrayMethods.forEach(function(method) { + var original = arrayProto[method] + newProto[method] = function() { + // 继续尝试劫持数组元素的属性 + var args = [] + for (var i = 0, n = arguments.length; i < n; i++) { + args[i] = observe(arguments[i], 0, 1, 1) + } + var result = original.apply(this, args) + addTrack(this.$track, method, args) + if (!W3C) { + this.$model = toJson(this) + } + this.notify() + this._.length = this.length + return result + } +}) + +'sort,reverse'.replace(rword, function(method) { + newProto[method] = function() { + var oldArray = this.concat() //保持原来状态的旧数组 + var newArray = this + var mask = Math.random() + var indexes = [] + var hasSort = false + arrayProto[method].apply(newArray, arguments) //排序 + for (var i = 0, n = oldArray.length; i < n; i++) { + var neo = newArray[i] + var old = oldArray[i] + if (neo === old) { + indexes.push(i) + } else { + var index = oldArray.indexOf(neo) + indexes.push(index) //得到新数组的每个元素在旧数组对应的位置 + oldArray[index] = mask //屏蔽已经找过的元素 + hasSort = true + } + } + if (hasSort) { + sortByIndex(this.$track, indexes) + if (!W3C) { + this.$model = toJson(this) + } + this.notify() + } + return this + } +}) + +function sortByIndex(array, indexes) { + var map = {} + for (var i = 0, n = indexes.length; i < n; i++) { + map[i] = array[i] + var j = indexes[i] + if (j in map) { + array[i] = map[j] + delete map[j] + } else { + array[i] = array[j] + } + } +} + +function createTrack(n) { + var ret = [] + for (var i = 0; i < n; i++) { + ret[i] = generateID('proxy-each') + } + return ret +} + +function addTrack(track, method, args) { + switch (method) { + case 'push': + case 'unshift': + args = createTrack(args.length) + break + case 'splice': + if (args.length > 2) { + // 0, 5, a, b, c --> 0, 2, 0 + // 0, 5, a, b, c, d, e, f, g--> 0, 0, 3 + var del = args[1] + var add = args.length - 2 + // args = [args[0], Math.max(del - add, 0)].concat(createTrack(Math.max(add - del, 0))) + args = [args[0], args[1]].concat(createTrack(args.length - 2)) + } + break + } + Array.prototype[method].apply(track, args) +} diff --git a/src/08-dependency.js b/src/08-dependency.js new file mode 100644 index 0000000..5169329 --- /dev/null +++ b/src/08-dependency.js @@ -0,0 +1,129 @@ +/********************************************************************* + * 依赖调度系统 * + **********************************************************************/ + +//检测两个对象间的依赖关系 +var dependencyDetection = (function() { + var outerFrames = [] + var currentFrame + return { + begin: function(binding) { + //accessorObject为一个拥有callback的对象 + outerFrames.push(currentFrame) + currentFrame = binding + }, + end: function() { + currentFrame = outerFrames.pop() + }, + collectDependency: function(array) { + if (currentFrame) { + //被dependencyDetection.begin调用 + currentFrame.callback(array) + } + } + } +})() + +//将绑定对象注入到其依赖项的订阅数组中 +var roneval = /^on$/ + +function returnRandom() { + return new Date() - 0 +} + +Anot.injectBinding = function(binding) { + binding.handler = binding.handler || directives[binding.type].update || noop + binding.update = function() { + var begin = false + if (!binding.getter) { + begin = true + dependencyDetection.begin({ + callback: function(array) { + injectDependency(array, binding) + } + }) + + binding.getter = parseExpr(binding.expr, binding.vmodels, binding) + binding.observers.forEach(function(a) { + a.v.$watch(a.p, binding) + }) + delete binding.observers + } + try { + var args = binding.fireArgs, + a, + b + delete binding.fireArgs + if (!args) { + if (binding.type === 'on') { + a = binding.getter + '' + } else { + try { + a = binding.getter.apply(0, binding.args) + } catch (e) { + a = null + } + } + } else { + a = args[0] + b = args[1] + } + b = typeof b === 'undefined' ? binding.oldValue : b + if (binding._filters) { + a = filters.$filter.apply(0, [a].concat(binding._filters)) + } + if (binding.signature) { + var xtype = Anot.type(a) + if (xtype !== 'array' && xtype !== 'object') { + throw Error('warning:' + binding.expr + '只能是对象或数组') + } + binding.xtype = xtype + var vtrack = getProxyIds(binding.proxies || [], xtype) + var mtrack = + a.$track || + (xtype === 'array' ? createTrack(a.length) : Object.keys(a)) + binding.track = mtrack + if (vtrack !== mtrack.join(';')) { + binding.handler(a, b) + binding.oldValue = 1 + } + } else if (Array.isArray(a) ? a.length !== (b && b.length) : false) { + binding.handler(a, b) + binding.oldValue = a.concat() + } else if (!('oldValue' in binding) || a !== b) { + binding.handler(a, b) + binding.oldValue = Array.isArray(a) ? a.concat() : a + } + } catch (e) { + delete binding.getter + log('warning:exception throwed in [Anot.injectBinding] ', e) + var node = binding.element + if (node && node.nodeType === 3) { + node.nodeValue = + openTag + (binding.oneTime ? '::' : '') + binding.expr + closeTag + } + } finally { + begin && dependencyDetection.end() + } + } + binding.update() +} + +//将依赖项(比它高层的访问器或构建视图刷新函数的绑定对象)注入到订阅者数组 +function injectDependency(list, binding) { + if (binding.oneTime) return + if (list && Anot.Array.ensure(list, binding) && binding.element) { + injectDisposeQueue(binding, list) + if (new Date() - beginTime > 444) { + rejectDisposeQueue() + } + } +} + +function getProxyIds(a, isArray) { + var ret = [] + for (var i = 0, el; (el = a[i++]); ) { + ret.push(isArray ? el.$id : el.$key) + } + return ret.join(';') +} diff --git a/src/09-gc.js b/src/09-gc.js new file mode 100644 index 0000000..6138214 --- /dev/null +++ b/src/09-gc.js @@ -0,0 +1,90 @@ +/********************************************************************* + * 定时GC回收机制 (基于1.6基于频率的GC) * + **********************************************************************/ + +var disposeQueue = (Anot.$$subscribers = []) +var beginTime = new Date() + +//添加到回收列队中 +function injectDisposeQueue(data, list) { + data.list = list + data.i = ~~data.i + if (!data.uuid) { + data.uuid = '_' + ++bindingID + } + if (!disposeQueue[data.uuid]) { + disposeQueue[data.uuid] = '__' + disposeQueue.push(data) + } +} + +var lastGCIndex = 0 +function rejectDisposeQueue(data) { + var i = lastGCIndex || disposeQueue.length + var threshold = 0 + while ((data = disposeQueue[--i])) { + if (data.i < 7) { + if (data.element === null) { + disposeQueue.splice(i, 1) + if (data.list) { + Anot.Array.remove(data.list, data) + delete disposeQueue[data.uuid] + } + continue + } + if (shouldDispose(data.element)) { + //如果它的虚拟DOM不在VTree上或其属性不在VM上 + disposeQueue.splice(i, 1) + Anot.Array.remove(data.list, data) + disposeData(data) + //Anot会在每次全量更新时,比较上次执行时间, + //假若距离上次有半秒,就会发起一次GC,并且只检测当中的500个绑定 + //而一个正常的页面不会超过2000个绑定(500即取其4分之一) + //用户频繁操作页面,那么2,3秒内就把所有绑定检测一遍,将无效的绑定移除 + if (threshold++ > 500) { + lastGCIndex = i + break + } + continue + } + data.i++ + //基于检测频率,如果检测过7次,可以认为其是长久存在的节点,那么以后每7次才检测一次 + if (data.i === 7) { + data.i = 14 + } + } else { + data.i-- + } + } + beginTime = new Date() +} + +function disposeData(data) { + delete disposeQueue[data.uuid] // 先清除,不然无法回收了 + data.element = null + data.rollback && data.rollback() + for (var key in data) { + data[key] = null + } +} + +function shouldDispose(el) { + try { + //IE下,如果文本节点脱离DOM树,访问parentNode会报错 + var fireError = el.parentNode.nodeType + } catch (e) { + return true + } + if (el.ifRemove) { + // 如果节点被放到ifGroup,才移除 + if (!root.contains(el.ifRemove) && ifGroup === el.parentNode) { + el.parentNode && el.parentNode.removeChild(el) + return true + } + } + return el.msRetain + ? 0 + : el.nodeType === 1 + ? !root.contains(el) + : !Anot.contains(root, el) +} diff --git a/src/10-html.js b/src/10-html.js new file mode 100644 index 0000000..92c771e --- /dev/null +++ b/src/10-html.js @@ -0,0 +1,89 @@ +/************************************************************************ + * HTML处理(parseHTML, innerHTML, clearHTML) * + *************************************************************************/ + +//parseHTML的辅助变量 +var tagHooks = new function() { + // jshint ignore:line + Anot.mix(this, { + option: DOC.createElement('select'), + thead: DOC.createElement('table'), + td: DOC.createElement('tr'), + area: DOC.createElement('map'), + tr: DOC.createElement('tbody'), + col: DOC.createElement('colgroup'), + legend: DOC.createElement('fieldset'), + _default: DOC.createElement('div'), + g: DOC.createElementNS('http://www.w3.org/2000/svg', 'svg') + }) + this.optgroup = this.option + this.tbody = this.tfoot = this.colgroup = this.caption = this.thead + this.th = this.td +}() // jshint ignore:line +String( + 'circle,defs,ellipse,image,line,path,polygon,polyline,rect,symbol,text,use' +).replace(rword, function(tag) { + tagHooks[tag] = tagHooks.g //处理SVG +}) + +var rtagName = /<([\w:]+)/ +var rxhtml = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi +var scriptTypes = oneObject([ + '', + 'text/javascript', + 'text/ecmascript', + 'application/ecmascript', + 'application/javascript' +]) +var script = DOC.createElement('script') +var rhtml = /<|&#?\w+;/ + +Anot.parseHTML = function(html) { + var fragment = anotFragment.cloneNode(false) + if (typeof html !== 'string') { + return fragment + } + if (!rhtml.test(html)) { + fragment.appendChild(DOC.createTextNode(html)) + return fragment + } + html = html.replace(rxhtml, '<$1>').trim() + var tag = (rtagName.exec(html) || ['', ''])[1].toLowerCase(), + //取得其标签名 + wrapper = tagHooks[tag] || tagHooks._default, + firstChild + wrapper.innerHTML = html + var els = wrapper.getElementsByTagName('script') + if (els.length) { + //使用innerHTML生成的script节点不会发出请求与执行text属性 + for (var i = 0, el; (el = els[i++]); ) { + if (scriptTypes[el.type]) { + var neo = script.cloneNode(false) //FF不能省略参数 + ap.forEach.call(el.attributes, function(attr) { + neo.setAttribute(attr.name, attr.value) + }) // jshint ignore:line + neo.text = el.text + el.parentNode.replaceChild(neo, el) + } + } + } + + while ((firstChild = wrapper.firstChild)) { + // 将wrapper上的节点转移到文档碎片上! + fragment.appendChild(firstChild) + } + return fragment +} + +Anot.innerHTML = function(node, html) { + var a = this.parseHTML(html) + this.clearHTML(node).appendChild(a) +} + +Anot.clearHTML = function(node) { + node.textContent = '' + while (node.firstChild) { + node.removeChild(node.firstChild) + } + return node +} diff --git a/src/11-dom.instance.js b/src/11-dom.instance.js new file mode 100644 index 0000000..d40b1df --- /dev/null +++ b/src/11-dom.instance.js @@ -0,0 +1,484 @@ +/********************************************************************* + * Anot的原型方法定义区 * + **********************************************************************/ + +function hyphen(target) { + //转换为连字符线风格 + return target.replace(/([a-z\d])([A-Z]+)/g, '$1-$2').toLowerCase() +} + +function camelize(target) { + //转换为驼峰风格 + if (target.indexOf('-') < 0 && target.indexOf('_') < 0) { + return target //提前判断,提高getStyle等的效率 + } + return target.replace(/[-_][^-_]/g, function(match) { + return match.charAt(1).toUpperCase() + }) +} + +'add,remove'.replace(rword, function(method) { + Anot.fn[method + 'Class'] = function(cls) { + var el = this[0] + //https://developer.mozilla.org/zh-CN/docs/Mozilla/Firefox/Releases/26 + if (cls && typeof cls === 'string' && el && el.nodeType === 1) { + cls.replace(/\S+/g, function(c) { + el.classList[method](c) + }) + } + return this + } +}) + +Anot.fn.mix({ + hasClass: function(cls) { + var el = this[0] || {} //IE10+, chrome8+, firefox3.6+, safari5.1+,opera11.5+支持classList,chrome24+,firefox26+支持classList2.0 + return el.nodeType === 1 && el.classList.contains(cls) + }, + toggleClass: function(value, stateVal) { + var className, + i = 0 + var classNames = String(value).match(/\S+/g) || [] + var isBool = typeof stateVal === 'boolean' + while ((className = classNames[i++])) { + var state = isBool ? stateVal : !this.hasClass(className) + this[state ? 'addClass' : 'removeClass'](className) + } + return this + }, + attr: function(name, value) { + if (arguments.length === 2) { + this[0].setAttribute(name, value) + return this + } else { + return this[0].getAttribute(name) + } + }, + data: function(name, value) { + name = 'data-' + hyphen(name || '') + switch (arguments.length) { + case 2: + this.attr(name, value) + return this + case 1: + var val = this.attr(name) + return parseData(val) + case 0: + var ret = {} + ap.forEach.call(this[0].attributes, function(attr) { + if (attr) { + name = attr.name + if (!name.indexOf('data-')) { + name = camelize(name.slice(5)) + ret[name] = parseData(attr.value) + } + } + }) + return ret + } + }, + removeData: function(name) { + name = 'data-' + hyphen(name) + this[0].removeAttribute(name) + return this + }, + css: function(name, value) { + if (Anot.isPlainObject(name)) { + for (var i in name) { + Anot.css(this, i, name[i]) + } + } else { + var ret = Anot.css(this, name, value) + } + return ret !== void 0 ? ret : this + }, + position: function() { + var offsetParent, + offset, + elem = this[0], + parentOffset = { + top: 0, + left: 0 + } + if (!elem) { + return + } + if (this.css('position') === 'fixed') { + offset = elem.getBoundingClientRect() + } else { + offsetParent = this.offsetParent() //得到真正的offsetParent + offset = this.offset() // 得到正确的offsetParent + if (offsetParent[0].tagName !== 'HTML') { + parentOffset = offsetParent.offset() + } + parentOffset.top += Anot.css(offsetParent[0], 'borderTopWidth', true) + parentOffset.left += Anot.css(offsetParent[0], 'borderLeftWidth', true) + // Subtract offsetParent scroll positions + parentOffset.top -= offsetParent.scrollTop() + parentOffset.left -= offsetParent.scrollLeft() + } + return { + top: offset.top - parentOffset.top - Anot.css(elem, 'marginTop', true), + left: offset.left - parentOffset.left - Anot.css(elem, 'marginLeft', true) + } + }, + offsetParent: function() { + var offsetParent = this[0].offsetParent + while (offsetParent && Anot.css(offsetParent, 'position') === 'static') { + offsetParent = offsetParent.offsetParent + } + return Anot(offsetParent || root) + }, + bind: function(type, fn, phase) { + if (this[0]) { + //此方法不会链 + return Anot.bind(this[0], type, fn, phase) + } + }, + unbind: function(type, fn, phase) { + if (this[0]) { + Anot.unbind(this[0], type, fn, phase) + } + return this + }, + val: function(value) { + var node = this[0] + if (node && node.nodeType === 1) { + var get = arguments.length === 0 + var access = get ? ':get' : ':set' + var fn = valHooks[getValType(node) + access] + if (fn) { + var val = fn(node, value) + } else if (get) { + return (node.value || '').replace(/\r/g, '') + } else { + node.value = value + } + } + return get ? val : this + } +}) + +if (root.dataset) { + Anot.fn.data = function(name, val) { + name = name && camelize(name) + var dataset = this[0].dataset + switch (arguments.length) { + case 2: + dataset[name] = val + return this + case 1: + val = dataset[name] + return parseData(val) + case 0: + var ret = createMap() + for (name in dataset) { + ret[name] = parseData(dataset[name]) + } + return ret + } + } +} + +Anot.parseJSON = JSON.parse + +var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/ +function parseData(data) { + try { + if (typeof data === 'object') return data + data = + data === 'true' + ? true + : data === 'false' + ? false + : data === 'null' + ? null + : +data + '' === data + ? +data + : rbrace.test(data) + ? JSON.parse(data) + : data + } catch (e) {} + return data +} + +Anot.fireDom = function(elem, type, opts) { + var hackEvent = DOC.createEvent('Events') + hackEvent.initEvent(type, true, true) + Anot.mix(hackEvent, opts) + elem.dispatchEvent(hackEvent) +} + +Anot.each( + { + scrollLeft: 'pageXOffset', + scrollTop: 'pageYOffset' + }, + function(method, prop) { + Anot.fn[method] = function(val) { + var node = this[0] || {}, + win = getWindow(node), + top = method === 'scrollTop' + if (!arguments.length) { + return win ? win[prop] : node[method] + } else { + if (win) { + win.scrollTo(!top ? val : win[prop], top ? val : win[prop]) + } else { + node[method] = val + } + } + } + } +) + +function getWindow(node) { + return node.window && node.document + ? node + : node.nodeType === 9 + ? node.defaultView + : false +} + +//=============================css相关================================== + +var cssHooks = (Anot.cssHooks = createMap()) +var prefixes = ['', '-webkit-', '-moz-', '-ms-'] //去掉opera-15的支持 +var cssMap = { + float: 'cssFloat' +} + +Anot.cssNumber = oneObject( + 'animationIterationCount,animationIterationCount,columnCount,order,flex,flexGrow,flexShrink,fillOpacity,fontWeight,lineHeight,opacity,orphans,widows,zIndex,zoom' +) + +Anot.cssName = function(name, host, camelCase) { + if (cssMap[name]) { + return cssMap[name] + } + host = host || root.style + for (var i = 0, n = prefixes.length; i < n; i++) { + camelCase = camelize(prefixes[i] + name) + if (camelCase in host) { + return (cssMap[name] = camelCase) + } + } + return null +} + +cssHooks['@:set'] = function(node, name, value) { + node.style[name] = value +} + +cssHooks['@:get'] = function(node, name) { + if (!node || !node.style) { + throw new Error('getComputedStyle要求传入一个节点 ' + node) + } + var ret, + computed = getComputedStyle(node) + if (computed) { + ret = name === 'filter' ? computed.getPropertyValue(name) : computed[name] + if (ret === '') { + ret = node.style[name] //其他浏览器需要我们手动取内联样式 + } + } + return ret +} +cssHooks['opacity:get'] = function(node) { + var ret = cssHooks['@:get'](node, 'opacity') + return ret === '' ? '1' : ret +} + +'top,left'.replace(rword, function(name) { + cssHooks[name + ':get'] = function(node) { + var computed = cssHooks['@:get'](node, name) + return /px$/.test(computed) ? computed : Anot(node).position()[name] + 'px' + } +}) + +var cssShow = { + position: 'absolute', + visibility: 'hidden', + display: 'block' +} +var rdisplayswap = /^(none|table(?!-c[ea]).+)/ +function showHidden(node, array) { + //http://www.cnblogs.com/rubylouvre/archive/2012/10/27/2742529.html + if (node.offsetWidth <= 0) { + //opera.offsetWidth可能小于0 + var styles = getComputedStyle(node, null) + if (rdisplayswap.test(styles['display'])) { + var obj = { + node: node + } + for (var name in cssShow) { + obj[name] = styles[name] + node.style[name] = cssShow[name] + } + array.push(obj) + } + var _parent = node.parentNode + if (_parent && _parent.nodeType === 1) { + showHidden(_parent, array) + } + } +} + +'Width,Height'.replace(rword, function(name) { + //fix 481 + var method = name.toLowerCase(), + clientProp = 'client' + name, + scrollProp = 'scroll' + name, + offsetProp = 'offset' + name + cssHooks[method + ':get'] = function(node, which, override) { + var boxSizing = -4 + if (typeof override === 'number') { + boxSizing = override + } + which = name === 'Width' ? ['Left', 'Right'] : ['Top', 'Bottom'] + var ret = node[offsetProp] // border-box 0 + if (boxSizing === 2) { + // margin-box 2 + return ( + ret + + Anot.css(node, 'margin' + which[0], true) + + Anot.css(node, 'margin' + which[1], true) + ) + } + if (boxSizing < 0) { + // padding-box -2 + ret = + ret - + Anot.css(node, 'border' + which[0] + 'Width', true) - + Anot.css(node, 'border' + which[1] + 'Width', true) + } + if (boxSizing === -4) { + // content-box -4 + ret = + ret - + Anot.css(node, 'padding' + which[0], true) - + Anot.css(node, 'padding' + which[1], true) + } + return ret + } + cssHooks[method + '&get'] = function(node) { + var hidden = [] + showHidden(node, hidden) + var val = cssHooks[method + ':get'](node) + for (var i = 0, obj; (obj = hidden[i++]); ) { + node = obj.node + for (var n in obj) { + if (typeof obj[n] === 'string') { + node.style[n] = obj[n] + } + } + } + return val + } + Anot.fn[method] = function(value) { + //会忽视其display + var node = this[0] + if (arguments.length === 0) { + if (node.setTimeout) { + //取得窗口尺寸,IE9后可以用node.innerWidth /innerHeight代替 + return node['inner' + name] + } + if (node.nodeType === 9) { + //取得页面尺寸 + var doc = node.documentElement + //FF chrome html.scrollHeight< body.scrollHeight + //IE 标准模式 : html.scrollHeight> body.scrollHeight + //IE 怪异模式 : html.scrollHeight 最大等于可视窗口多一点? + return Math.max( + node.body[scrollProp], + doc[scrollProp], + node.body[offsetProp], + doc[offsetProp], + doc[clientProp] + ) + } + return cssHooks[method + '&get'](node) + } else { + return this.css(method, value) + } + } + Anot.fn['inner' + name] = function() { + return cssHooks[method + ':get'](this[0], void 0, -2) + } + Anot.fn['outer' + name] = function(includeMargin) { + return cssHooks[method + ':get']( + this[0], + void 0, + includeMargin === true ? 2 : 0 + ) + } +}) + +Anot.fn.offset = function() { + //取得距离页面左右角的坐标 + var node = this[0] + try { + var rect = node.getBoundingClientRect() + // Make sure element is not hidden (display: none) or disconnected + // https://github.com/jquery/jquery/pull/2043/files#r23981494 + if (rect.width || rect.height || node.getClientRects().length) { + var doc = node.ownerDocument + var root = doc.documentElement + var win = doc.defaultView + return { + top: rect.top + win.pageYOffset - root.clientTop, + left: rect.left + win.pageXOffset - root.clientLeft + } + } + } catch (e) { + return { + left: 0, + top: 0 + } + } +} + +//=============================val相关======================= + +function getValType(elem) { + var ret = elem.tagName.toLowerCase() + return ret === 'input' && /checkbox|radio/.test(elem.type) ? 'checked' : ret +} + +var valHooks = { + 'select:get': function(node, value) { + var option, + options = node.options, + index = node.selectedIndex, + one = node.type === 'select-one' || index < 0, + values = one ? null : [], + max = one ? index + 1 : options.length, + i = index < 0 ? max : one ? index : 0 + for (; i < max; i++) { + option = options[i] + //旧式IE在reset后不会改变selected,需要改用i === index判定 + //我们过滤所有disabled的option元素,但在safari5下,如果设置select为disable,那么其所有孩子都disable + //因此当一个元素为disable,需要检测其是否显式设置了disable及其父节点的disable情况 + if ((option.selected || i === index) && !option.disabled) { + value = option.value + if (one) { + return value + } + //收集所有selected值组成数组返回 + values.push(value) + } + } + return values + }, + 'select:set': function(node, values, optionSet) { + values = [].concat(values) //强制转换为数组 + for (var i = 0, el; (el = node.options[i++]); ) { + if ((el.selected = values.indexOf(el.value) > -1)) { + optionSet = true + } + } + if (!optionSet) { + node.selectedIndex = -1 + } + } +} diff --git a/src/12-parser.js b/src/12-parser.js new file mode 100644 index 0000000..0d1591b --- /dev/null +++ b/src/12-parser.js @@ -0,0 +1,326 @@ +var keyMap = {} +var keys = [ + 'break,case,catch,continue,debugger,default,delete,do,else,false', + 'finally,for,function,if,in,instanceof,new,null,return,switch,this', + 'throw,true,try,typeof,var,void,while,with' /* 关键字*/, + 'abstract,boolean,byte,char,class,const,double,enum,export,extends', + 'final,float,goto,implements,import,int,interface,long,native', + 'package,private,protected,public,short,static,super,synchronized', + 'throws,transient,volatile' /*保留字*/, + 'arguments,let,yield,async,await,undefined' +].join(',') +keys.replace(/\w+/g, function(a) { + keyMap[a] = true +}) + +var ridentStart = /[a-z_$]/i +var rwhiteSpace = /[\s\uFEFF\xA0]/ +function getIdent(input, lastIndex) { + var result = [] + var subroutine = !!lastIndex + lastIndex = lastIndex || 0 + //将表达式中的标识符抽取出来 + var state = 'unknown' + var variable = '' + for (var i = 0; i < input.length; i++) { + var c = input.charAt(i) + if (c === "'" || c === '"') { + //字符串开始 + if (state === 'unknown') { + state = c + } else if (state === c) { + //字符串结束 + state = 'unknown' + } + } else if (c === '\\') { + if (state === "'" || state === '"') { + i++ + } + } else if (ridentStart.test(c)) { + //碰到标识符 + if (state === 'unknown') { + state = 'variable' + variable = c + } else if (state === 'maybePath') { + variable = result.pop() + variable += '.' + c + state = 'variable' + } else if (state === 'variable') { + variable += c + } + } else if (/\w/.test(c)) { + if (state === 'variable') { + variable += c + } + } else if (c === '.') { + if (state === 'variable') { + if (variable) { + result.push(variable) + variable = '' + state = 'maybePath' + } + } + } else if (c === '[') { + if (state === 'variable' || state === 'maybePath') { + if (variable) { + //如果前面存在变量,收集它 + result.push(variable) + variable = '' + } + var lastLength = result.length + var last = result[lastLength - 1] + var innerResult = getIdent(input.slice(i), i) + if (innerResult.length) { + //如果括号中存在变量,那么这里添加通配符 + result[lastLength - 1] = last + '.*' + result = innerResult.concat(result) + } else { + //如果括号中的东西是确定的,直接转换为其子属性 + var content = input.slice(i + 1, innerResult.i) + try { + var text = scpCompile(['return ' + content])() + result[lastLength - 1] = last + '.' + text + } catch (e) {} + } + state = 'maybePath' //]后面可能还接东西 + i = innerResult.i + } + } else if (c === ']') { + if (subroutine) { + result.i = i + lastIndex + addVar(result, variable) + return result + } + } else if (rwhiteSpace.test(c) && c !== '\r' && c !== '\n') { + if (state === 'variable') { + if (addVar(result, variable)) { + state = 'maybePath' // aaa . bbb 这样的情况 + } + variable = '' + } + } else { + addVar(result, variable) + state = 'unknown' + variable = '' + } + } + addVar(result, variable) + return result +} + +function addVar(array, element) { + if (element && !keyMap[element]) { + array.push(element) + return true + } +} + +function addAssign(vars, vmodel, name, binding) { + var ret = [] + var prefix = ' = ' + name + '.' + for (var i = vars.length, prop; (prop = vars[--i]); ) { + var arr = prop.split('.') + var first = arr[0] + + if (vmodel.hasOwnProperty(first)) { + // log(first, prop, prefix, vmodel) + ret.push(first + prefix + first) + binding.observers.push({ + v: vmodel, + p: prop, + type: Anot.type(vmodel[first]) + }) + vars.splice(i, 1) + } + } + return ret +} + +var rproxy = /(proxy\-[a-z]+)\-[\-0-9a-f]+$/ +var variablePool = new Cache(218) +//缓存求值函数,以便多次利用 +var evaluatorPool = new Cache(128) + +function getVars(expr) { + expr = expr.trim() + var ret = variablePool.get(expr) + if (ret) { + return ret.concat() + } + var array = getIdent(expr) + var uniq = {} + var result = [] + for (var i = 0, el; (el = array[i++]); ) { + if (!uniq[el]) { + uniq[el] = 1 + result.push(el) + } + } + return variablePool.put(expr, result).concat() +} + +function parseExpr(expr, vmodels, binding) { + var filters = binding.filters + if (typeof filters === 'string' && filters.trim() && !binding._filters) { + binding._filters = parseFilter(filters.trim()) + } + + var vars = getVars(expr) + var expose = new Date() - 0 + var assigns = [] + var names = [] + var args = [] + binding.observers = [] + + for (var i = 0, sn = vmodels.length; i < sn; i++) { + if (vars.length) { + var name = 'vm' + expose + '_' + i + names.push(name) + args.push(vmodels[i]) + assigns.push.apply(assigns, addAssign(vars, vmodels[i], name, binding)) + } + } + binding.args = args + var dataType = binding.type + var exprId = + vmodels.map(function(el) { + return String(el.$id).replace(rproxy, '$1') + }) + + expr + + dataType + // log(expr, '---------------', assigns) + var getter = evaluatorPool.get(exprId) //直接从缓存,免得重复生成 + if (getter) { + if (dataType === 'duplex') { + var setter = evaluatorPool.get(exprId + 'setter') + binding.setter = setter.apply(setter, binding.args) + } + return (binding.getter = getter) + } + + // expr的字段不可枚举时,补上一个随机变量, 避免抛出异常 + if (!assigns.length) { + assigns.push('fix' + expose) + } + + if (dataType === 'duplex') { + var nameOne = {} + assigns.forEach(function(a) { + var arr = a.split('=') + nameOne[arr[0].trim()] = arr[1].trim() + }) + expr = expr.replace(/[\$\w]+/, function(a) { + return nameOne[a] ? nameOne[a] : a + }) + /* jshint ignore:start */ + var fn2 = scpCompile( + names.concat( + '"use strict";\n return function(vvv){' + expr + ' = vvv\n}\n' + ) + ) + /* jshint ignore:end */ + evaluatorPool.put(exprId + 'setter', fn2) + binding.setter = fn2.apply(fn2, binding.args) + } + + if (dataType === 'on') { + //事件绑定 + if (expr.indexOf('(') === -1) { + expr += '.call(' + names[names.length - 1] + ', $event)' + } else { + expr = expr.replace('(', '.call(' + names[names.length - 1] + ', ') + } + names.push('$event') + expr = '\nreturn ' + expr + ';' //IE全家 Function("return ")出错,需要Function("return ;") + var lastIndex = expr.lastIndexOf('\nreturn') + var header = expr.slice(0, lastIndex) + var footer = expr.slice(lastIndex) + expr = header + '\n' + footer + } else { + // 对于非事件绑定的方法, 同样绑定到vm上 + binding.observers.forEach(function(it) { + if (it.type === 'function') { + // log(it, expr) + var reg = new RegExp(it.p + '\\(([^)]*)\\)', 'g') + expr = expr.replace(reg, function(s, m) { + m = m.trim() + return ( + it.p + + '.call(' + + names[names.length - 1] + + (m ? ', ' + m : '') + + ')' + ) + }) + } + }) + expr = '\nreturn ' + expr + ';' //IE全家 Function("return ")出错,需要Function("return ;") + } + + /* jshint ignore:start */ + getter = scpCompile( + names.concat( + "'use strict';\ntry{\n var " + + assigns.join(',\n ') + + expr + + '\n}catch(e){console.log(e)}' + ) + ) + /* jshint ignore:end */ + + return evaluatorPool.put(exprId, getter) +} + +function normalizeExpr(code) { + var hasExpr = rexpr.test(code) //比如:class="width{{w}}"的情况 + if (hasExpr) { + var array = scanExpr(code) + if (array.length === 1) { + return array[0].expr + } + return array + .map(function(el) { + return el.type ? '(' + el.expr + ')' : quote(el.expr) + }) + .join(' + ') + } else { + return code + } +} + +Anot.normalizeExpr = normalizeExpr +Anot.parseExprProxy = parseExpr + +var rthimRightParentheses = /\)\s*$/ +var rthimOtherParentheses = /\)\s*\|/g +var rquoteFilterName = /\|\s*([$\w]+)/g +var rpatchBracket = /"\s*\["/g +var rthimLeftParentheses = /"\s*\(/g +function parseFilter(filters) { + filters = + filters + .replace(rthimRightParentheses, '') //处理最后的小括号 + .replace(rthimOtherParentheses, function() { + //处理其他小括号 + return '],|' + }) + .replace(rquoteFilterName, function(a, b) { + //处理|及它后面的过滤器的名字 + return '[' + quote(b) + }) + .replace(rpatchBracket, function() { + return '"],["' + }) + .replace(rthimLeftParentheses, function() { + return '",' + }) + ']' + /* jshint ignore:start */ + return scpCompile(['return [' + filters + ']'])() + /* jshint ignore:end */ +} + +/********************************************************************* + * 编译系统 * + **********************************************************************/ + +var quote = JSON.stringify diff --git a/src/13-scan.js b/src/13-scan.js new file mode 100644 index 0000000..9e3bea0 --- /dev/null +++ b/src/13-scan.js @@ -0,0 +1,468 @@ +/********************************************************************* + * 扫描系统 * + **********************************************************************/ + +//http://www.w3.org/TR/html5/syntax.html#void-elements +var stopScan = oneObject( + 'area,base,basefont,br,col,command,embed,hr,img,input,link,meta,param,source,track,wbr,noscript,script,style,textarea'.toUpperCase() +) +function isWidget(el) { + //如果是组件,则返回组件的名字 + var name = el.nodeName.toLowerCase() + if (/^anot-([a-z][a-z0-9\-]*)$/.test(name)) { + return RegExp.$1 + } + return null +} + +function isRef(el) { + return el.hasAttribute('ref') ? el.getAttribute('ref') : null +} + +function checkScan(elem, callback, innerHTML) { + var id = setTimeout(function() { + var currHTML = elem.innerHTML + clearTimeout(id) + if (currHTML === innerHTML) { + callback() + } else { + checkScan(elem, callback, currHTML) + } + }) +} + +function getBindingCallback(elem, name, vmodels) { + var callback = elem.getAttribute(name) + if (callback) { + for (var i = 0, vm; (vm = vmodels[i++]); ) { + if (vm.hasOwnProperty(callback) && typeof vm[callback] === 'function') { + return vm[callback] + } + } + } +} + +function executeBindings(bindings, vmodels) { + for (var i = 0, binding; (binding = bindings[i++]); ) { + binding.vmodels = vmodels + directives[binding.type].init(binding) + + Anot.injectBinding(binding) + if (binding.getter && binding.element.nodeType === 1) { + //移除数据绑定,防止被二次解析 + //chrome使用removeAttributeNode移除不存在的特性节点时会报错 + binding.element.removeAttribute(binding.name) + } + } + bindings.length = 0 +} + +//https://github.com/RubyLouvre/Anot/issues/636 +var mergeTextNodes = + IEVersion && window.MutationObserver + ? function(elem) { + var node = elem.firstChild, + text + while (node) { + var aaa = node.nextSibling + if (node.nodeType === 3) { + if (text) { + text.nodeValue += node.nodeValue + elem.removeChild(node) + } else { + text = node + } + } else { + text = null + } + node = aaa + } + } + : 0 +var roneTime = /^\s*::/ +var rmsAttr = /:(\w+)-?(.*)/ + +var events = oneObject( + 'animationend,blur,change,input,click,dblclick,focus,keydown,keypress,keyup,mousedown,mouseenter,mouseleave,mousemove,mouseout,mouseover,mouseup,scan,scroll,submit' +) +var obsoleteAttrs = oneObject( + 'value,title,alt,checked,selected,disabled,readonly,enabled,href,src' +) +function bindingSorter(a, b) { + return a.priority - b.priority +} + +var rnoCollect = /^(:\S+|data-\S+|on[a-z]+|style|class)$/ +var ronattr = '__fn__' +var specifiedVars = [':disabled', ':loading', ':value'] +var filterTypes = ['html', 'text', 'attr', 'data'] +function getOptionsFromTag(elem, vmodels) { + var attributes = aslice.call(elem.attributes, 0) + var ret = {} + var vm = vmodels[0] || {} + + for (var i = 0, attr; (attr = attributes[i++]); ) { + var name = attr.name + if ( + (attr.specified && !rnoCollect.test(name)) || + specifiedVars.includes(name) + ) { + elem.removeAttribute(name) + if (name.indexOf(ronattr) === 0) { + name = attr.value.slice(6) + ret[name] = elem[attr.value] + delete elem[attr.value] + } else { + var camelizeName = camelize(name) + if (camelizeName.indexOf('@') === 0) { + camelizeName = camelizeName.slice(1) + attr.value = attr.value.replace(/\(.*\)$/, '') + if (vm.$id.slice(0, 10) === 'proxy-each') { + vm = vm.$up + } + if ( + vm.hasOwnProperty(attr.value) && + typeof vm[attr.value] === 'function' + ) { + ret[camelizeName] = vm[attr.value].bind(vm) + } + } else { + ret[camelizeName] = parseData(attr.value) + } + } + } + } + return ret +} + +function scanAttr(elem, vmodels, match) { + var scanNode = true + if (vmodels.length) { + var attributes = elem.attributes + var bindings = [] + var uniq = {} + for (var i = 0, attr; (attr = attributes[i++]); ) { + var name = attr.name + if (uniq[name]) { + //IE8下:for BUG + continue + } + uniq[name] = 1 + if (attr.specified) { + if ((match = name.match(rmsAttr))) { + //如果是以指定前缀命名的 + var type = match[1] + var param = match[2] || '' + var value = attr.value + if (events[type]) { + param = type + type = 'on' + } + if (directives[type]) { + var newValue = value.replace(roneTime, '') + var oneTime = value !== newValue + var binding = { + type: type, + param: param, + element: elem, + name: name, + expr: newValue, + oneTime: oneTime, + uuid: '_' + ++bindingID, + priority: + (directives[type].priority || type.charCodeAt(0) * 10) + + (Number(param.replace(/\D/g, '')) || 0) + } + if (filterTypes.includes(type)) { + var filters = getToken(value).filters + binding.expr = binding.expr.replace(filters, '') + binding.filters = filters + .replace(rhasHtml, function() { + binding.type = 'html' + binding.group = 1 + return '' + }) + .trim() // jshint ignore:line + } else if (type === 'duplex') { + var hasDuplex = name + } else if (name === ':if-loop') { + binding.priority += 100 + } else if (name === ':attr-value') { + var hasAttrValue = name + } + bindings.push(binding) + } + } + } + } + if (bindings.length) { + bindings.sort(bindingSorter) + + if (hasDuplex && hasAttrValue && elem.type === 'text') { + log('warning!一个控件不能同时定义:attr-value与' + hasDuplex) + } + + for (i = 0; (binding = bindings[i]); i++) { + type = binding.type + if (rnoscanAttrBinding.test(type)) { + return executeBindings(bindings.slice(0, i + 1), vmodels) + } else if (scanNode) { + scanNode = !rnoscanNodeBinding.test(type) + } + } + executeBindings(bindings, vmodels) + } + } + if ( + scanNode && + !stopScan[elem.tagName] && + (isWidget(elem) ? elem.msResolved : 1) + ) { + mergeTextNodes && mergeTextNodes(elem) + scanNodeList(elem, vmodels) //扫描子孙元素 + } +} + +var rnoscanAttrBinding = /^if|for$/ +var rnoscanNodeBinding = /^html|include$/ + +function scanNodeList(elem, vmodels) { + var nodes = Anot.slice(elem.childNodes) + scanNodeArray(nodes, vmodels) +} + +function scanNodeArray(nodes, vmodels) { + function _delay_component(name) { + setTimeout(function() { + Anot.component(name) + }) + } + for (var i = 0, node; (node = nodes[i++]); ) { + switch (node.nodeType) { + case 1: + var elem = node + if ( + !elem.msResolved && + elem.parentNode && + elem.parentNode.nodeType === 1 + ) { + var widget = isWidget(elem) + + if (widget) { + elem.setAttribute('is-widget', '') + elem.removeAttribute(':if') + elem.removeAttribute(':if-loop') + componentQueue.push({ + element: elem, + vmodels: vmodels, + name: widget + }) + if (Anot.components[widget]) { + // log(widget, Anot.components) + //确保所有:attr-name扫描完再处理 + _delay_component(widget) + } + } else { + // 非组件才检查 ref属性 + var ref = isRef(elem) + if (ref && vmodels.length) { + vmodels[0].$refs[ref] = elem + } + } + } + + scanTag(node, vmodels) //扫描元素节点 + + if (node.msHasEvent) { + Anot.fireDom(node, 'datasetchanged', { + bubble: node.msHasEvent + }) + } + + break + case 3: + if (rexpr.test(node.nodeValue)) { + scanText(node, vmodels, i) //扫描文本节点 + } + break + } + } +} + +function scanTag(elem, vmodels) { + //扫描顺序 skip(0) --> anot(1) --> :if(10) --> :for(90) + //--> :if-loop(110) --> :attr(970) ...--> :duplex(2000)垫后 + var skip = elem.getAttribute('skip') + var node = elem.getAttributeNode('anot') + var vm = vmodels.concat() + if (typeof skip === 'string') { + return + } else if (node) { + var newVmodel = Anot.vmodels[node.value] + var attrs = aslice.call(elem.attributes, 0) + + if (!newVmodel) { + return + } + + vm = [newVmodel] + + elem.removeAttribute(node.name) //removeAttributeNode不会刷新xx[anot]样式规则 + // 挂载VM对象到相应的元素上 + elem.__VM__ = newVmodel + hideProperty(newVmodel, '$elem', elem) + + if (vmodels.length) { + newVmodel.$up = vmodels[0] + vmodels[0].$children.push(newVmodel) + var props = {} + attrs.forEach(function(attr) { + if (/^:/.test(attr.name)) { + var name = attr.name.match(rmsAttr)[1] + var value = null + if (!name || Anot.directives[name] || events[name]) { + return + } + try { + value = parseExpr(attr.value, vmodels, {}).apply(0, vmodels) + value = toJson(value) + elem.removeAttribute(attr.name) + props[name] = value + } catch (error) { + log( + 'Props parse faild on (%s[class=%s]),', + elem.nodeName, + elem.className, + attr, + error + '' + ) + } + } + }) + // 一旦设定了 props的类型, 就必须传入正确的值 + for (var k in newVmodel.props) { + if (newVmodel.props[k] && newVmodel.props[k].type === 'PropsTypes') { + if (newVmodel.props[k].check(props[k])) { + newVmodel.props[k] = props[k] + delete props[k] + } else { + console.error( + new TypeError( + 'props.' + + k + + ' needs [' + + newVmodel.props[k].checkType + + '], but [' + + newVmodel.props[k].result + + '] given.' + ) + ) + } + } + } + Object.assign(newVmodel.props, props) + props = undefined + } + } + scanAttr(elem, vm) //扫描特性节点 + + if (newVmodel) { + setTimeout(function() { + if (typeof newVmodel.$mounted === 'function') { + newVmodel.$mounted() + } + delete newVmodel.$mounted + }) + } +} +var rhasHtml = /\|\s*html(?:\b|$)/, + r11a = /\|\|/g, + rlt = /</g, + rgt = />/g, + rstringLiteral = /(['"])(\\\1|.)+?\1/g, + rline = /\r?\n/g +function getToken(value) { + if (value.indexOf('|') > 0) { + var scapegoat = value.replace(rstringLiteral, function(_) { + return Array(_.length + 1).join('1') // jshint ignore:line + }) + var index = scapegoat.replace(r11a, '\u1122\u3344').indexOf('|') //干掉所有短路或 + if (index > -1) { + return { + type: 'text', + filters: value.slice(index).trim(), + expr: value.slice(0, index) + } + } + } + return { + type: 'text', + expr: value, + filters: '' + } +} + +function scanExpr(str) { + var tokens = [], + value, + start = 0, + stop + do { + stop = str.indexOf(openTag, start) + if (stop === -1) { + break + } + value = str.slice(start, stop) + if (value) { + // {{ 左边的文本 + tokens.push({ + expr: value + }) + } + start = stop + openTag.length + stop = str.indexOf(closeTag, start) + if (stop === -1) { + break + } + value = str.slice(start, stop) + if (value) { + //处理{{ }}插值表达式 + tokens.push(getToken(value.replace(rline, ''))) + } + start = stop + closeTag.length + } while (1) + value = str.slice(start) + if (value) { + //}} 右边的文本 + tokens.push({ + expr: value + }) + } + return tokens +} + +function scanText(textNode, vmodels, index) { + var bindings = [], + tokens = scanExpr(textNode.data) + if (tokens.length) { + for (var i = 0, token; (token = tokens[i++]); ) { + var node = DOC.createTextNode(token.expr) //将文本转换为文本节点,并替换原来的文本节点 + if (token.type) { + token.expr = token.expr.replace(roneTime, function() { + token.oneTime = true + return '' + }) // jshint ignore:line + token.element = node + token.filters = token.filters.replace(rhasHtml, function() { + token.type = 'html' + return '' + }) // jshint ignore:line + token.pos = index * 1000 + i + bindings.push(token) //收集带有插值表达式的文本 + } + anotFragment.appendChild(node) + } + textNode.parentNode.replaceChild(anotFragment, textNode) + if (bindings.length) executeBindings(bindings, vmodels) + } +} diff --git a/src/14-buffer.js b/src/14-buffer.js new file mode 100644 index 0000000..9b3b48b --- /dev/null +++ b/src/14-buffer.js @@ -0,0 +1,26 @@ +//使用来自游戏界的双缓冲技术,减少对视图的冗余刷新 +var Buffer = function() { + this.queue = [] +} +Buffer.prototype = { + render: function(isAnimate) { + if (!this.locked) { + this.locked = isAnimate ? root.offsetHeight + 10 : 1 + var me = this + Anot.nextTick(function() { + me.flush() + }) + } + }, + flush: function() { + for (var i = 0, sub; (sub = this.queue[i++]); ) { + sub.update && sub.update() + } + this.locked = 0 + this.queue = [] + } +} + +var buffer = new Buffer() + + diff --git a/src/15-component.js b/src/15-component.js new file mode 100644 index 0000000..7987f8e --- /dev/null +++ b/src/15-component.js @@ -0,0 +1,339 @@ +var componentQueue = [] +var widgetList = [] +var componentHooks = { + __init__: noop, + componentWillMount: noop, + componentDidMount: noop, + childComponentDidMount: noop, + componentWillUnmount: noop, + render: function() { + return null + } +} + +function parseSlot(collections, vms) { + var arr = aslice.call(collections, 0) + var obj = { __extra__: [] } + arr.forEach(function(elem) { + switch (elem.nodeType) { + case 1: + var slot = elem.getAttribute('slot') + + if (slot) { + obj[slot] = obj[slot] || [] + elem.removeAttribute('slot') + obj[slot].push(elem.outerHTML) + } else { + var txt = elem.outerHTML + if (isWidget(elem) || /:[\w-]*=".*"/.test(txt)) { + break + } + if (rexpr.test(txt)) { + var expr = normalizeExpr(txt) + txt = parseExpr(expr, vms, {}).apply(0, vms) + } + + obj.__extra__.push(txt) + } + + break + case 3: + var txt = elem.textContent.trim() + if (txt) { + obj.__extra__.push(txt) + } + break + default: + break + } + elem.parentNode.removeChild(elem) + }) + return obj +} + +function parseVmValue(vm, key, val) { + if (arguments.length === 2) { + var oval = Function('o', 'return o.' + key)(vm) + if (oval && typeof oval === 'object') { + try { + return oval.$model + } catch (err) {} + } + return oval + } else if (arguments.length === 3) { + Function('o', 'v', 'return o.' + key + ' = v')(vm, val) + } +} + +Anot.components = {} +Anot.component = function(name, opts) { + if (opts) { + Anot.components[name] = Anot.mix({}, componentHooks, opts) + } + for (var i = 0, obj; (obj = componentQueue[i]); i++) { + if (name === obj.name) { + componentQueue.splice(i, 1) + i-- + // (obj, Anot.components[name], obj.element, obj.name) + ;(function(host, hooks, elem, widget) { + //如果elem已从Document里移除,直接返回 + if (!Anot.contains(DOC, elem) || elem.msResolved) { + Anot.Array.remove(componentQueue, host) + return + } + + var dependencies = 1 + + //===========收集各种配置======= + if (elem.getAttribute(':attr-uuid')) { + //如果还没有解析完,就延迟一下 #1155 + return + } + hooks.watch = hooks.watch || {} + var parentVm = host.vmodels.concat().pop() + var state = {} + var props = getOptionsFromTag(elem, host.vmodels) + var $id = props.uuid || generateID(widget) + var slots = { __extra__: [] } + + // 对象组件的子父vm关系, 只存最顶层的$components对象中, + while (parentVm.$up && parentVm.$up.__WIDGET__ === name) { + parentVm = parentVm.$up + } + + if (elem.childNodes.length) { + slots = parseSlot(elem.childNodes, host.vmodels) + } + var txtContent = slots.__extra__.join('') + delete slots.__extra__ + elem.text = function() { + return txtContent + } + + if (props.hasOwnProperty(':disabled')) { + var disabledKey = props[':disabled'] + var disabledKeyReverse = false + if (disabledKey.indexOf('!') === 0) { + disabledKey = disabledKey.slice(1) + disabledKeyReverse = true + } + state.disabled = parseVmValue(parentVm, disabledKey) + if (disabledKeyReverse) { + state.disabled = !state.disabled + } + + parentVm.$watch(disabledKey, function(val) { + if (disabledKeyReverse) { + val = !val + } + Anot.vmodels[$id].disabled = val + }) + + delete props[':disabled'] + } + if (props.hasOwnProperty(':loading')) { + var loadingKey = props[':loading'] + var loadingKeyReverse = false + if (loadingKey.indexOf('!') === 0) { + loadingKey = loadingKey.slice(1) + loadingKeyReverse = true + } + state.loading = parseVmValue(parentVm, loadingKey) + if (loadingKeyReverse) { + state.loading = !state.loading + } + parentVm.$watch(loadingKey, function(val) { + if (loadingKeyReverse) { + val = !val + } + Anot.vmodels[$id].loading = val + }) + delete props[':loading'] + } + + // :value可实现双向同步值 + if (props.hasOwnProperty(':value')) { + var valueKey = props[':value'] + var valueWatcher = function() { + var val = parseVmValue(parentVm, valueKey) + Anot.vmodels[$id].value = val + } + var childValueWatcher = function() { + var val = this.value + if (val && typeof val === 'object') { + val = val.$model + } + parseVmValue(parentVm, valueKey, val) + } + state.value = parseVmValue(parentVm, valueKey) + + if (hooks.watch.value) { + hooks.watch.value = [hooks.watch.value] + } else { + hooks.watch.value = [] + } + if (hooks.watch['value.length']) { + hooks.watch['value.length'] = [hooks.watch['value.length']] + } else { + hooks.watch['value.length'] = [] + } + if (hooks.watch['value.*']) { + hooks.watch['value.*'] = [hooks.watch['value.*']] + } else { + hooks.watch['value.*'] = [] + } + + parentVm.$watch(valueKey, valueWatcher) + if (Array.isArray(state.value)) { + parentVm.$watch(valueKey + '.*', valueWatcher) + parentVm.$watch(valueKey + '.length', valueWatcher) + hooks.watch['value.*'].push(childValueWatcher) + hooks.watch['value.length'].push(childValueWatcher) + } else { + hooks.watch.value.push(childValueWatcher) + } + + delete props[':value'] + } + + delete props.uuid + delete props.name + delete props.isWidget + + hooks.props = hooks.props || {} + hooks.state = hooks.state || {} + + Object.assign(hooks.props, props) + Object.assign(hooks.state, state) + + var __READY__ = false + + hooks.__init__.call(elem, hooks.props, hooks.state, function next() { + __READY__ = true + + delete elem.text + }) + + if (!__READY__) { + return + } + + hooks.$id = $id + + //==========构建VM========= + var { + componentWillMount, + componentDidMount, + childComponentDidMount, + componentWillUnmount, + render + } = hooks + + delete hooks.__init__ + delete hooks.componentWillMount + delete hooks.componentDidMount + delete hooks.childComponentDidMount + delete hooks.componentWillUnmount + + var vmodel = Anot(hooks) + Anot.vmodels[vmodel.$id] = vmodel + hideProperty(vmodel, '__WIDGET__', name) + hideProperty(vmodel, '$recycle', function() { + for (var i in this.$events) { + var ev = this.$events[i] || [] + var len = ev.length + while (len--) { + if (ev[len].type === null || ev[len].type === 'user-watcher') { + ev.splice(len, 1) + } + } + } + }) + delete vmodel.$mounted + + // 对象组件的子父vm关系, 只存最顶层的$components对象中, + // 而子vm, 无论向下多少级, 他们的$up对象也只存最顶层的组件vm + parentVm.$components.push(vmodel) + if (parentVm.__WIDGET__ === name) { + vmodel.$up = parentVm + } + + elem.msResolved = 1 //防止二进扫描此元素 + + componentWillMount.call(vmodel) + + Anot.clearHTML(elem) + var html = render.call(vmodel, slots) || '' + + html = html.replace(/<\w+[^>]*>/g, function(m, s) { + return m.replace(/[\n\t\s]{1,}/g, ' ') + }) + + elem.innerHTML = html + + hideProperty(vmodel, '$elem', elem) + elem.__VM__ = vmodel + + Anot.fireDom(elem, 'datasetchanged', { + vm: vmodel, + childReady: 1 + }) + + var children = 0 + var removeFn = Anot.bind(elem, 'datasetchanged', function(ev) { + if (ev.childReady) { + dependencies += ev.childReady + if (vmodel.$id !== ev.vm.$id) { + if (ev.childReady === -1) { + children++ + childComponentDidMount.call(vmodel, ev.vm) + } + ev.stopPropagation() + } + } + if (dependencies === 0) { + var timer = setTimeout(function() { + clearTimeout(timer) + elem.removeAttribute('is-widget') + componentDidMount.call(vmodel) + }, children ? Math.max(children * 17, 100) : 17) + + Anot.unbind(elem, 'datasetchanged', removeFn) + //================== + host.rollback = function() { + try { + componentWillUnmount.call(vmodel) + } catch (e) {} + parentVm.$recycle && parentVm.$recycle() + Anot.Array.remove(parentVm.$components, vmodel) + delete Anot.vmodels[vmodel.$id] + } + injectDisposeQueue(host, widgetList) + if (window.chrome) { + elem.addEventListener('DOMNodeRemovedFromDocument', function() { + setTimeout(rejectDisposeQueue) + }) + } + } + }) + + scanTag(elem, [vmodel]) + + if (!elem.childNodes.length) { + Anot.fireDom(elem, 'datasetchanged', { + vm: vmodel, + childReady: -1 + }) + } else { + var id2 = setTimeout(function() { + clearTimeout(id2) + Anot.fireDom(elem, 'datasetchanged', { + vm: vmodel, + childReady: -1 + }) + }, 17) + } + })(obj, toJson(Anot.components[name]), obj.element, obj.name) // jshint ignore:line + } + } +} diff --git a/src/16-:attr.js b/src/16-:attr.js new file mode 100644 index 0000000..8bcffb1 --- /dev/null +++ b/src/16-:attr.js @@ -0,0 +1,169 @@ +var bools = [ + 'autofocus,autoplay,async,allowTransparency,checked,controls', + 'declare,disabled,defer,defaultChecked,defaultSelected', + 'contentEditable,isMap,loop,multiple,noHref,noResize,noShade', + 'open,readOnly,selected' +].join(',') +var boolMap = {} +bools.replace(rword, function(name) { + boolMap[name.toLowerCase()] = name +}) + +var propMap = { + //属性名映射 + 'accept-charset': 'acceptCharset', + char: 'ch', + charoff: 'chOff', + class: 'className', + for: 'htmlFor', + 'http-equiv': 'httpEquiv' +} + +var anomaly = [ + 'accessKey,bgColor,cellPadding,cellSpacing,codeBase,codeType,colSpan', + 'dateTime,defaultValue,frameBorder,longDesc,maxLength,marginWidth,marginHeight', + 'rowSpan,tabIndex,useMap,vSpace,valueType,vAlign' +].join(',') +anomaly.replace(rword, function(name) { + propMap[name.toLowerCase()] = name +}) + +var attrDir = Anot.directive('attr', { + init: function(binding) { + //{{aaa}} --> aaa + //{{aaa}}/bbb.html --> (aaa) + "/bbb.html" + binding.expr = normalizeExpr(binding.expr.trim()) + if (binding.type === 'include') { + var elem = binding.element + effectBinding(elem, binding) + binding.includeRendered = getBindingCallback( + elem, + 'data-rendered', + binding.vmodels + ) + binding.includeLoaded = getBindingCallback( + elem, + 'data-loaded', + binding.vmodels + ) + var outer = (binding.includeReplace = !!Anot(elem).data('includeReplace')) + if (Anot(elem).data('cache')) { + binding.templateCache = {} + } + binding.start = DOC.createComment(':include') + binding.end = DOC.createComment(':include-end') + if (outer) { + binding.element = binding.end + binding._element = elem + elem.parentNode.insertBefore(binding.start, elem) + elem.parentNode.insertBefore(binding.end, elem.nextSibling) + } else { + elem.insertBefore(binding.start, elem.firstChild) + elem.appendChild(binding.end) + } + } + }, + update: function(val) { + var elem = this.element + var obj = {} + var vm = this.vmodels[0] + + val = toJson(val) + + if (this.param) { + if (typeof val === 'object' && val !== null) { + if (Array.isArray(val)) { + obj[this.param] = val + } else { + if (Date.isDate(val)) { + obj[this.param] = val.toUTCString() + } else { + obj[this.param] = val + } + } + } else { + obj[this.param] = val + } + } else { + if (!val || typeof val !== 'object' || Array.isArray(val)) { + return + } + if (Date.isDate(val)) { + return + } + + obj = val + } + + for (var i in obj) { + if (i === 'style') { + console.error('设置style样式, 请改用 :css指令') + continue + } + // 通过属性设置回调,必须以@符号开头 + if (i.indexOf('@') === 0) { + if (typeof obj[i] !== 'function') { + continue + } + } + if (i === 'href' || i === 'src') { + //处理IE67自动转义的问题 + if (!root.hasAttribute) obj[i] = obj[i].replace(/&/g, '&') + + elem[i] = obj[i] + + //chrome v37- 下embed标签动态设置的src,无法发起请求 + if (window.chrome && elem.tagName === 'EMBED') { + var _parent = elem.parentNode + var com = DOC.createComment(':src') + _parent.replaceChild(com, elem) + _parent.replaceChild(elem, com) + } + } else { + var k = i + //古董IE下,部分属性名字要进行映射 + if (!W3C && propMap[k]) { + k = propMap[k] + } + if (obj[i] === false || obj[i] === null || obj[i] === undefined) { + obj[i] = '' + } + + if (typeof elem[boolMap[k]] === 'boolean') { + //布尔属性必须使用el.xxx = true|false方式设值 + elem[boolMap[k]] = !!obj[i] + + //如果为false, IE全系列下相当于setAttribute(xxx, ''),会影响到样式,需要进一步处理 + if (!obj[i]) { + obj[i] = !!obj[i] + } + if (obj[i] === false) { + elem.removeAttribute(k) + continue + } + } + + //SVG只能使用setAttribute(xxx, yyy), VML只能使用elem.xxx = yyy ,HTML的固有属性必须elem.xxx = yyy + var isInnate = rsvg.test(elem) + ? false + : DOC.namespaces && isVML(elem) + ? true + : k in elem.cloneNode(false) + if (isInnate) { + elem[k] = obj[i] + } else { + if (typeof obj[i] === 'object') { + obj[i] = Date.isDate(obj[i]) + ? obj[i].toUTCString() + : JSON.stringify(obj[i]) + } else if (typeof obj[i] === 'function') { + k = ronattr + camelize(k.slice(1)) + elem[k] = obj[i].bind(vm) + obj[i] = k + } + elem.setAttribute(k, obj[i]) + } + } + } + } +}) diff --git a/src/17-:class.js b/src/17-:class.js new file mode 100644 index 0000000..916dd47 --- /dev/null +++ b/src/17-:class.js @@ -0,0 +1,84 @@ +//类名定义, :class="xx:yy" :class="{xx: yy}" :class="xx" :class="{{xx}}" +Anot.directive('class', { + init: function(binding) { + var expr = [] + if (!/^\{.*\}$/.test(binding.expr)) { + expr = binding.expr.split(':') + expr[1] = (expr[1] && expr[1].trim()) || 'true' + var arr = expr[0].split(/\s+/) + binding.expr = + '{' + + arr + .map(function(it) { + return it + ': ' + expr[1] + }) + .join(', ') + + '}' + } else if (/^\{\{.*\}\}$/.test(binding.expr)) { + binding.expr = binding.expr.slice(2, -2) + } + + if (binding.type === 'hover' || binding.type === 'active') { + //确保只绑定一次 + if (!binding.hasBindEvent) { + var elem = binding.element + var $elem = Anot(elem) + var activate = 'mouseenter' //在移出移入时切换类名 + var abandon = 'mouseleave' + if (binding.type === 'active') { + //在聚焦失焦中切换类名 + elem.tabIndex = elem.tabIndex || -1 + activate = 'mousedown' + abandon = 'mouseup' + var fn0 = $elem.bind('mouseleave', function() { + $elem.removeClass(expr[0]) + }) + } + } + + var fn1 = $elem.bind(activate, function() { + $elem.addClass(expr[0]) + }) + var fn2 = $elem.bind(abandon, function() { + $elem.removeClass(expr[0]) + }) + binding.rollback = function() { + $elem.unbind('mouseleave', fn0) + $elem.unbind(activate, fn1) + $elem.unbind(abandon, fn2) + } + binding.hasBindEvent = true + } + }, + update: function(val) { + if (this.type !== 'class') { + return + } + var obj = val + if (!obj || this.param) + return log( + 'class指令语法错误 %c %s="%s"', + 'color:#f00', + this.name, + this.expr + ) + + if (typeof obj === 'string') { + obj = {} + obj[val] = true + } + + if (!Anot.isPlainObject(obj)) { + obj = obj.$model + } + + var $elem = Anot(this.element) + for (var i in obj) { + $elem.toggleClass(i, !!obj[i]) + } + } +}) + +'hover,active'.replace(rword, function(name) { + directives[name] = directives['class'] +}) diff --git a/src/18-:css.js b/src/18-:css.js new file mode 100644 index 0000000..51629c8 --- /dev/null +++ b/src/18-:css.js @@ -0,0 +1,25 @@ +//样式定义 :css-width="200" +//:css="{width: 200}" +Anot.directive('css', { + init: directives.attr.init, + update: function(val) { + var $elem = Anot(this.element) + if (!this.param) { + var obj = val + try { + if (typeof val === 'object') { + if (!Anot.isPlainObject(val)) obj = val.$model + } else { + obj = new Function('return ' + val)() + } + for (var i in obj) { + $elem.css(i, obj[i]) + } + } catch (err) { + log('样式格式错误 %c %s="%s"', 'color:#f00', this.name, this.expr) + } + } else { + $elem.css(this.param, val) + } + } +}) diff --git a/src/19-:data.js b/src/19-:data.js new file mode 100644 index 0000000..4db7811 --- /dev/null +++ b/src/19-:data.js @@ -0,0 +1,19 @@ +//兼容2种写法 :data-xx="yy", :data="{xx: yy}" +Anot.directive('data', { + priority: 100, + init: directives.attr.init, + update: function(val) { + var obj = val + if (typeof obj === 'object' && obj !== null) { + if (!Anot.isPlainObject(obj)) obj = val.$model + + for (var i in obj) { + this.element.setAttribute('data-' + i, obj[i]) + } + } else { + if (!this.param) return + + this.element.setAttribute('data-' + this.param, obj) + } + } +}) diff --git a/src/20-:rule.js b/src/20-:rule.js new file mode 100644 index 0000000..d53ff7e --- /dev/null +++ b/src/20-:rule.js @@ -0,0 +1,129 @@ +/*------ 表单验证 -------*/ +var __rules = {} +Anot.validate = function(key, cb) { + if (!__rules[key]) { + throw new Error('validate [' + key + '] not exists.') + } + if (typeof cb === 'function') { + __rules[key].event = cb + } + var result = __rules[key].result + for (var k in result) { + if (!result[k].passed) { + return result[k] + } + } + return true +} +Anot.directive('rule', { + priority: 2010, + init: function(binding) { + if (binding.param && !__rules[binding.param]) { + __rules[binding.param] = { + event: noop, + result: {} + } + } + binding.target = __rules[binding.param] + }, + update: function(opt) { + var _this = this + var elem = this.element + if (!['INPUT', 'TEXTAREA'].includes(elem.nodeName)) { + return + } + if (elem.msBinded) { + return + } + if (this.target) { + this.target.result[elem.expr] = { key: elem.expr } + } + var target = this.target + + // 0: 验证通过 + // 10001: 不能为空 + // 10002: 必须为合法数字 + // 10003: Email格式错误 + // 10004: 手机格式错误 + // 10005: 必须为纯中文 + // 10006: 格式匹配错误(正则) + // 10011: 输入值超过指定最大长度 + // 10012: 输入值短于指定最小长度 + // 10021: 输入值大于指定最大数值 + // 10022: 输入值小于指定最小数值 + // 10031: 与指定的表单的值不一致 + function checked(ev) { + var val = elem.value + var code = 0 + + if (opt.require && (val === '' || val === null)) { + code = 10001 + } + + if (code === 0 && opt.isNumeric) { + code = !isFinite(val) ? 10002 : 0 + } + + if (code === 0 && opt.isEmail) + code = !/^[\w\.\-]+@\w+([\.\-]\w+)*\.\w+$/.test(val) ? 10003 : 0 + + if (code === 0 && opt.isPhone) { + code = !/^1[34578]\d{9}$/.test(val) ? 10004 : 0 + } + + if (code === 0 && opt.isCN) { + code = !/^[\u4e00-\u9fa5]+$/.test(val) ? 10005 : 0 + } + + if (code === 0 && opt.exp) { + code = !opt.exp.test(val) ? 10006 : 0 + } + + if (code === 0 && opt.maxLen) { + code = val.length > opt.maxLen ? 10011 : 0 + } + + if (code === 0 && opt.minLen) { + code = val.length < opt.minLen ? 10012 : 0 + } + + if (code === 0 && opt.hasOwnProperty('max')) { + code = val > opt.max ? 10021 : 0 + } + + if (code === 0 && opt.hasOwnProperty('min')) { + code = val < opt.min ? 10022 : 0 + } + + if (code === 0 && opt.eq) { + var eqVal = parseVmValue(_this.vmodels[0], opt.eq) + code = val !== eqVal ? 10031 : 0 + } + + target.result[elem.expr].code = code + target.result[elem.expr].passed = opt.require + ? code === 0 + : val + ? code === 0 + : true + + var passed = true + for (var k in target.result) { + if (!target.result[k].passed) { + passed = false + target.event(target.result[k]) + break + } + } + if (passed) { + target.event(true) + } + } + Anot(elem).bind('blur', checked) + this.rollback = function() { + Anot(elem).unbind('blur', checked) + } + elem.msBinded = true + checked() + } +}) diff --git a/src/21-:duplex.js b/src/21-:duplex.js new file mode 100644 index 0000000..3bcc687 --- /dev/null +++ b/src/21-:duplex.js @@ -0,0 +1,320 @@ +//双工绑定 +var rduplexType = /^(?:checkbox|radio)$/ +var rduplexParam = /^(?:radio|checked)$/ +var rnoduplexInput = /^(file|button|reset|submit|checkbox|radio|range)$/ +var duplexBinding = Anot.directive('duplex', { + priority: 2000, + init: function(binding, hasCast) { + var elem = binding.element + var vmodels = binding.vmodels + binding.changed = getBindingCallback(elem, 'data-changed', vmodels) || noop + var params = [] + var casting = oneObject('string,number,boolean,checked') + if (elem.type === 'radio' && binding.param === '') { + binding.param = 'checked' + } + + binding.param.replace(rw20g, function(name) { + if (rduplexType.test(elem.type) && rduplexParam.test(name)) { + name = 'checked' + binding.isChecked = true + binding.xtype = 'radio' + } + + if (casting[name]) { + hasCast = true + } + Anot.Array.ensure(params, name) + }) + if (!hasCast) { + params.push('string') + } + binding.param = params.join('-') + if (!binding.xtype) { + binding.xtype = + elem.tagName === 'SELECT' + ? 'select' + : elem.type === 'checkbox' + ? 'checkbox' + : elem.type === 'radio' + ? 'radio' + : /^change/.test(elem.getAttribute('data-event')) + ? 'change' + : 'input' + } + elem.expr = binding.expr + //===================绑定事件====================== + var bound = (binding.bound = function(type, callback) { + elem.addEventListener(type, callback, false) + var old = binding.rollback + binding.rollback = function() { + elem.anotSetter = null + Anot.unbind(elem, type, callback) + old && old() + } + }) + function callback(value) { + binding.changed.call(this, value) + } + var composing = false + function compositionStart() { + composing = true + } + function compositionEnd() { + composing = false + setTimeout(updateVModel) + } + var updateVModel = function(e) { + var val = elem.value + //防止递归调用形成死循环 + //处理中文输入法在minlengh下引发的BUG + if (composing || val === binding.oldValue || binding.pipe === null) { + return + } + + var lastValue = binding.pipe(val, binding, 'get') + binding.oldValue = val + binding.setter(lastValue) + + callback.call(elem, lastValue) + Anot.fireDom(elem, 'change') + } + switch (binding.xtype) { + case 'radio': + bound('click', function() { + var lastValue = binding.pipe(elem.value, binding, 'get') + binding.setter(lastValue) + callback.call(elem, lastValue) + }) + break + case 'checkbox': + bound('change', function() { + var method = elem.checked ? 'ensure' : 'remove' + var array = binding.getter.apply(0, binding.vmodels) + if (!Array.isArray(array)) { + log(':duplex应用于checkbox上要对应一个数组') + array = [array] + } + var val = binding.pipe(elem.value, binding, 'get') + Anot.Array[method](array, val) + callback.call(elem, array) + }) + break + case 'change': + bound('change', updateVModel) + break + case 'input': + bound('input', updateVModel) + bound('keyup', updateVModel) + if (!IEVersion) { + bound('compositionstart', compositionStart) + bound('compositionend', compositionEnd) + bound('DOMAutoComplete', updateVModel) + } + break + case 'select': + bound('change', function() { + var val = Anot(elem).val() //字符串或字符串数组 + if (Array.isArray(val)) { + val = val.map(function(v) { + return binding.pipe(v, binding, 'get') + }) + } else { + val = binding.pipe(val, binding, 'get') + } + if (val + '' !== binding.oldValue) { + try { + binding.setter(val) + } catch (ex) { + log(ex) + } + } + }) + bound('datasetchanged', function(e) { + if (e.bubble === 'selectDuplex') { + var value = binding._value + var curValue = Array.isArray(value) ? value.map(String) : value + '' + Anot(elem).val(curValue) + elem.oldValue = curValue + '' + callback.call(elem, curValue) + } + }) + break + } + if (binding.xtype === 'input' && !rnoduplexInput.test(elem.type)) { + if (elem.type !== 'hidden') { + bound('focus', function() { + elem.msFocus = true + }) + bound('blur', function() { + elem.msFocus = false + }) + } + elem.anotSetter = updateVModel //#765 + watchValueInTimer(function() { + if (root.contains(elem)) { + if (!elem.msFocus) { + updateVModel() + } + } else if (!elem.msRetain) { + return false + } + }) + } + }, + update: function(value) { + var elem = this.element, + binding = this, + curValue + if (!this.init) { + var cpipe = binding.pipe || (binding.pipe = pipe) + cpipe(null, binding, 'init') + this.init = 1 + } + switch (this.xtype) { + case 'input': + elem.value = value + break + case 'change': + curValue = this.pipe(value, this, 'set') //fix #673 + if (curValue !== this.oldValue) { + var fixCaret = false + if (elem.msFocus) { + try { + var start = elem.selectionStart + var end = elem.selectionEnd + if (start === end) { + var pos = start + fixCaret = true + } + } catch (e) {} + } + elem.value = this.oldValue = curValue + if (fixCaret && !elem.readOnly) { + elem.selectionStart = elem.selectionEnd = pos + } + } + break + case 'radio': + curValue = binding.isChecked ? !!value : value + '' === elem.value + elem.checked = curValue + break + case 'checkbox': + var array = [].concat(value) //强制转换为数组 + curValue = this.pipe(elem.value, this, 'get') + elem.checked = array.indexOf(curValue) > -1 + break + case 'select': + //必须变成字符串后才能比较 + binding._value = value + if (!elem.msHasEvent) { + elem.msHasEvent = 'selectDuplex' + //必须等到其孩子准备好才触发 + } else { + Anot.fireDom(elem, 'datasetchanged', { + bubble: elem.msHasEvent + }) + } + break + } + } +}) + +function fixNull(val) { + return val == null ? '' : val +} +Anot.duplexHooks = { + checked: { + get: function(val, binding) { + return !binding.oldValue + } + }, + string: { + get: function(val) { + //同步到VM + return val + }, + set: fixNull + }, + boolean: { + get: function(val) { + return val === 'true' + }, + set: fixNull + }, + number: { + get: function(val, binding) { + var number = +val + if (+val === number) { + return number + } + return 0 + }, + set: fixNull + } +} + +function pipe(val, binding, action, e) { + binding.param.replace(rw20g, function(name) { + var hook = Anot.duplexHooks[name] + if (hook && typeof hook[action] === 'function') { + val = hook[action](val, binding) + } + }) + return val +} + +var TimerID, + ribbon = [] + +Anot.tick = function(fn) { + if (ribbon.push(fn) === 1) { + TimerID = setInterval(ticker, 60) + } +} + +function ticker() { + for (var n = ribbon.length - 1; n >= 0; n--) { + var el = ribbon[n] + if (el() === false) { + ribbon.splice(n, 1) + } + } + if (!ribbon.length) { + clearInterval(TimerID) + } +} + +var watchValueInTimer = noop +new function() { + // jshint ignore:line + try { + //#272 IE9-IE11, firefox + var setters = {} + var aproto = HTMLInputElement.prototype + var bproto = HTMLTextAreaElement.prototype + function newSetter(value) { + // jshint ignore:line + setters[this.tagName].call(this, value) + if (!this.msFocus && this.anotSetter) { + this.anotSetter() + } + } + var inputProto = HTMLInputElement.prototype + Object.getOwnPropertyNames(inputProto) //故意引发IE6-8等浏览器报错 + setters['INPUT'] = Object.getOwnPropertyDescriptor(aproto, 'value').set + + Object.defineProperty(aproto, 'value', { + set: newSetter + }) + setters['TEXTAREA'] = Object.getOwnPropertyDescriptor(bproto, 'value').set + Object.defineProperty(bproto, 'value', { + set: newSetter + }) + } catch (e) { + //在chrome 43中 :duplex终于不需要使用定时器实现双向绑定了 + // http://updates.html5rocks.com/2015/04/DOM-attributes-now-on-the-prototype + // https://docs.google.com/document/d/1jwA8mtClwxI-QJuHT7872Z0pxpZz8PBkf2bGAbsUtqs/edit?pli=1 + watchValueInTimer = Anot.tick + } +}() // jshint ignore:line diff --git a/src/22-:effect.js b/src/22-:effect.js new file mode 100644 index 0000000..4a29bd0 --- /dev/null +++ b/src/22-:effect.js @@ -0,0 +1,335 @@ +/*-------------动画------------*/ + +Anot.directive('effect', { + priority: 5, + init: function(binding) { + var text = binding.expr, + className, + rightExpr + var colonIndex = text + .replace(rexprg, function(a) { + return a.replace(/./g, '0') + }) + .indexOf(':') //取得第一个冒号的位置 + if (colonIndex === -1) { + // 比如 :class/effect="aaa bbb ccc" 的情况 + className = text + rightExpr = true + } else { + // 比如 :class/effect-1="ui-state-active:checked" 的情况 + className = text.slice(0, colonIndex) + rightExpr = text.slice(colonIndex + 1) + } + if (!rexpr.test(text)) { + className = quote(className) + } else { + className = normalizeExpr(className) + } + binding.expr = '[' + className + ',' + rightExpr + ']' + }, + update: function(arr) { + var name = arr[0] + var elem = this.element + if (elem.getAttribute('data-effect-name') === name) { + return + } else { + elem.removeAttribute('data-effect-driver') + } + var inlineStyles = elem.style + var computedStyles = window.getComputedStyle + ? window.getComputedStyle(elem) + : null + var useAni = false + if (computedStyles && (supportTransition || supportAnimation)) { + //如果支持CSS动画 + var duration = + inlineStyles[transitionDuration] || computedStyles[transitionDuration] + if (duration && duration !== '0s') { + elem.setAttribute('data-effect-driver', 't') + useAni = true + } + + if (!useAni) { + duration = + inlineStyles[animationDuration] || computedStyles[animationDuration] + if (duration && duration !== '0s') { + elem.setAttribute('data-effect-driver', 'a') + useAni = true + } + } + } + + if (!useAni) { + if (Anot.effects[name]) { + elem.setAttribute('data-effect-driver', 'j') + useAni = true + } + } + if (useAni) { + elem.setAttribute('data-effect-name', name) + } + } +}) + +Anot.effects = {} +Anot.effect = function(name, callbacks) { + Anot.effects[name] = callbacks +} + +var supportTransition = false +var supportAnimation = false + +var transitionEndEvent +var animationEndEvent +var transitionDuration = Anot.cssName('transition-duration') +var animationDuration = Anot.cssName('animation-duration') +new function() { + // jshint ignore:line + var checker = { + TransitionEvent: 'transitionend', + WebKitTransitionEvent: 'webkitTransitionEnd', + OTransitionEvent: 'oTransitionEnd', + otransitionEvent: 'otransitionEnd' + } + var tran + //有的浏览器同时支持私有实现与标准写法,比如webkit支持前两种,Opera支持1、3、4 + for (var name in checker) { + if (window[name]) { + tran = checker[name] + break + } + try { + var a = document.createEvent(name) + tran = checker[name] + break + } catch (e) {} + } + if (typeof tran === 'string') { + supportTransition = true + transitionEndEvent = tran + } + + //大致上有两种选择 + //IE10+, Firefox 16+ & Opera 12.1+: animationend + //Chrome/Safari: webkitAnimationEnd + //http://blogs.msdn.com/b/davrous/archive/2011/12/06/introduction-to-css3-animat ions.aspx + //IE10也可以使用MSAnimationEnd监听,但是回调里的事件 type依然为animationend + // el.addEventListener("MSAnimationEnd", function(e) { + // alert(e.type)// animationend!!! + // }) + checker = { + AnimationEvent: 'animationend', + WebKitAnimationEvent: 'webkitAnimationEnd' + } + var ani + for (name in checker) { + if (window[name]) { + ani = checker[name] + break + } + } + if (typeof ani === 'string') { + supportTransition = true + animationEndEvent = ani + } +}() + +var effectPool = [] //重复利用动画实例 +function effectFactory(el, opts) { + if (!el || el.nodeType !== 1) { + return null + } + if (opts) { + var name = opts.effectName + var driver = opts.effectDriver + } else { + name = el.getAttribute('data-effect-name') + driver = el.getAttribute('data-effect-driver') + } + if (!name || !driver) { + return null + } + + var instance = effectPool.pop() || new Effect() + instance.el = el + instance.driver = driver + instance.useCss = driver !== 'j' + if (instance.useCss) { + opts && Anot(el).addClass(opts.effectClass) + instance.cssEvent = driver === 't' ? transitionEndEvent : animationEndEvent + } + instance.name = name + instance.callbacks = Anot.effects[name] || {} + + return instance +} + +function effectBinding(elem, binding) { + var name = elem.getAttribute('data-effect-name') + if (name) { + binding.effectName = name + binding.effectDriver = elem.getAttribute('data-effect-driver') + var stagger = +elem.getAttribute('data-effect-stagger') + binding.effectLeaveStagger = + +elem.getAttribute('data-effect-leave-stagger') || stagger + binding.effectEnterStagger = + +elem.getAttribute('data-effect-enter-stagger') || stagger + binding.effectClass = elem.className || NaN + } +} +function upperFirstChar(str) { + return str.replace(/^[\S]/g, function(m) { + return m.toUpperCase() + }) +} +var effectBuffer = new Buffer() +function Effect() {} //动画实例,做成类的形式,是为了共用所有原型方法 + +Effect.prototype = { + contrustor: Effect, + enterClass: function() { + return getEffectClass(this, 'enter') + }, + leaveClass: function() { + return getEffectClass(this, 'leave') + }, + // 共享一个函数 + actionFun: function(name, before, after) { + if (document.hidden) { + return + } + var me = this + var el = me.el + var isLeave = name === 'leave' + name = isLeave ? 'leave' : 'enter' + var oppositeName = isLeave ? 'enter' : 'leave' + callEffectHook(me, 'abort' + upperFirstChar(oppositeName)) + callEffectHook(me, 'before' + upperFirstChar(name)) + if (!isLeave) before(el) //这里可能做插入DOM树的操作,因此必须在修改类名前执行 + var cssCallback = function(cancel) { + el.removeEventListener(me.cssEvent, me.cssCallback) + if (isLeave) { + before(el) //这里可能做移出DOM树操作,因此必须位于动画之后 + Anot(el).removeClass(me.cssClass) + } else { + if (me.driver === 'a') { + Anot(el).removeClass(me.cssClass) + } + } + if (cancel !== true) { + callEffectHook(me, 'after' + upperFirstChar(name)) + after && after(el) + } + me.dispose() + } + if (me.useCss) { + if (me.cssCallback) { + //如果leave动画还没有完成,立即完成 + me.cssCallback(true) + } + + me.cssClass = getEffectClass(me, name) + me.cssCallback = cssCallback + + me.update = function() { + el.addEventListener(me.cssEvent, me.cssCallback) + if (!isLeave && me.driver === 't') { + //transtion延迟触发 + Anot(el).removeClass(me.cssClass) + } + } + Anot(el).addClass(me.cssClass) //animation会立即触发 + + effectBuffer.render(true) + effectBuffer.queue.push(me) + } else { + callEffectHook(me, name, cssCallback) + } + }, + enter: function(before, after) { + this.actionFun.apply(this, ['enter'].concat(Anot.slice(arguments))) + }, + leave: function(before, after) { + this.actionFun.apply(this, ['leave'].concat(Anot.slice(arguments))) + }, + dispose: function() { + //销毁与回收到池子中 + this.update = this.cssCallback = null + if (effectPool.unshift(this) > 100) { + effectPool.pop() + } + } +} + +function getEffectClass(instance, type) { + var a = instance.callbacks[type + 'Class'] + if (typeof a === 'string') return a + if (typeof a === 'function') return a() + return instance.name + '-' + type +} + +function callEffectHook(effect, name, cb) { + var hook = effect.callbacks[name] + if (hook) { + hook.call(effect, effect.el, cb) + } +} + +var applyEffect = function(el, dir /*[before, [after, [opts]]]*/) { + var args = aslice.call(arguments, 0) + if (typeof args[2] !== 'function') { + args.splice(2, 0, noop) + } + if (typeof args[3] !== 'function') { + args.splice(3, 0, noop) + } + var before = args[2] + var after = args[3] + var opts = args[4] + var effect = effectFactory(el, opts) + if (!effect) { + before() + after() + return false + } else { + var method = dir ? 'enter' : 'leave' + effect[method](before, after) + } +} + +Anot.mix(Anot.effect, { + apply: applyEffect, + append: function(el, _parent, after, opts) { + return applyEffect( + el, + 1, + function() { + _parent.appendChild(el) + }, + after, + opts + ) + }, + before: function(el, target, after, opts) { + return applyEffect( + el, + 1, + function() { + target.parentNode.insertBefore(el, target) + }, + after, + opts + ) + }, + remove: function(el, _parent, after, opts) { + return applyEffect( + el, + 0, + function() { + if (el.parentNode === _parent) _parent.removeChild(el) + }, + after, + opts + ) + } +}) diff --git a/src/23-:html.js b/src/23-:html.js new file mode 100644 index 0000000..cff4861 --- /dev/null +++ b/src/23-:html.js @@ -0,0 +1,49 @@ +Anot.directive('html', { + update: function(val) { + var binding = this + var elem = this.element + var isHtmlFilter = elem.nodeType !== 1 + var _parent = isHtmlFilter ? elem.parentNode : elem + if (!_parent) return + val = val == null ? '' : val + + if (elem.nodeType === 3) { + var signature = generateID('html') + _parent.insertBefore(DOC.createComment(signature), elem) + binding.element = DOC.createComment(signature + ':end') + _parent.replaceChild(binding.element, elem) + elem = binding.element + } + if (typeof val !== 'object') { + //string, number, boolean + var fragment = Anot.parseHTML(String(val)) + } else if (val.nodeType === 11) { + //将val转换为文档碎片 + fragment = val + } else if (val.nodeType === 1 || val.item) { + var nodes = val.nodeType === 1 ? val.childNodes : val.item + fragment = anotFragment.cloneNode(true) + while (nodes[0]) { + fragment.appendChild(nodes[0]) + } + } + + nodes = Anot.slice(fragment.childNodes) + //插入占位符, 如果是过滤器,需要有节制地移除指定的数量,如果是html指令,直接清空 + if (isHtmlFilter) { + var endValue = elem.nodeValue.slice(0, -4) + while (true) { + var node = elem.previousSibling + if (!node || (node.nodeType === 8 && node.nodeValue === endValue)) { + break + } else { + _parent.removeChild(node) + } + } + _parent.insertBefore(fragment, elem) + } else { + Anot.clearHTML(elem).appendChild(fragment) + } + scanNodeArray(nodes, binding.vmodels) + } +}) diff --git a/src/24-:text.js b/src/24-:text.js new file mode 100644 index 0000000..eb95383 --- /dev/null +++ b/src/24-:text.js @@ -0,0 +1,16 @@ +Anot.directive('text', { + update: function(val) { + var elem = this.element + val = val == null ? '' : val //不在页面上显示undefined null + if (elem.nodeType === 3) { + //绑定在文本节点上 + try { + //IE对游离于DOM树外的节点赋值会报错 + elem.data = val + } catch (e) {} + } else { + //绑定在特性节点上 + elem.textContent = val + } + } +}) diff --git a/src/25-:if.js b/src/25-:if.js new file mode 100644 index 0000000..2ac055c --- /dev/null +++ b/src/25-:if.js @@ -0,0 +1,103 @@ +Anot.directive('if', { + priority: 10, + update: function(val) { + var binding = this + var elem = this.element + var stamp = (binding.stamp = +new Date()) + var par + var after = function() { + if (stamp !== binding.stamp) return + binding.recoverNode = null + } + if (binding.recoverNode) binding.recoverNode() // 还原现场,有移动节点的都需要还原现场 + try { + if (!elem.parentNode) return + par = elem.parentNode + } catch (e) { + return + } + if (val) { + //插回DOM树 + function alway() { + // jshint ignore:line + if (elem.getAttribute(binding.name)) { + elem.removeAttribute(binding.name) + scanAttr(elem, binding.vmodels) + } + binding.rollback = null + } + if (elem.nodeType === 8) { + var keep = binding.keep + var hasEffect = Anot.effect.apply( + keep, + 1, + function() { + if (stamp !== binding.stamp) return + elem.parentNode.replaceChild(keep, elem) + elem = binding.element = keep //这时可能为null + if (keep.getAttribute('_required')) { + //#1044 + elem.required = true + elem.removeAttribute('_required') + } + if (elem.querySelectorAll) { + Anot.each(elem.querySelectorAll('[_required=true]'), function( + el + ) { + el.required = true + el.removeAttribute('_required') + }) + } + alway() + }, + after + ) + hasEffect = hasEffect === false + } + if (!hasEffect) alway() + } else { + //移出DOM树,并用注释节点占据原位置 + if (elem.nodeType === 1) { + if (elem.required === true) { + elem.required = false + elem.setAttribute('_required', 'true') + } + try { + //如果不支持querySelectorAll或:required,可以直接无视 + Anot.each(elem.querySelectorAll(':required'), function(el) { + elem.required = false + el.setAttribute('_required', 'true') + }) + } catch (e) {} + + var node = (binding.element = DOC.createComment(':if')), + pos = elem.nextSibling + binding.recoverNode = function() { + binding.recoverNode = null + if (node.parentNode !== par) { + par.insertBefore(node, pos) + binding.keep = elem + } + } + + Anot.effect.apply( + elem, + 0, + function() { + binding.recoverNode = null + if (stamp !== binding.stamp) return + elem.parentNode.replaceChild(node, elem) + binding.keep = elem //元素节点 + ifGroup.appendChild(elem) + binding.rollback = function() { + if (elem.parentNode === ifGroup) { + ifGroup.removeChild(elem) + } + } + }, + after + ) + } + } + } +}) diff --git a/src/26-:include.js b/src/26-:include.js new file mode 100644 index 0000000..9666327 --- /dev/null +++ b/src/26-:include.js @@ -0,0 +1,183 @@ +var getXHR = function() { + return new window.XMLHttpRequest() // jshint ignore:line +} +//将所有远程加载的模板,以字符串形式存放到这里 +var templatePool = (Anot.templateCache = {}) + +function getTemplateContainer(binding, id, text) { + var div = binding.templateCache && binding.templateCache[id] + if (div) { + var dom = DOC.createDocumentFragment(), + firstChild + while ((firstChild = div.firstChild)) { + dom.appendChild(firstChild) + } + return dom + } + return Anot.parseHTML(text) +} +function nodesToFrag(nodes) { + var frag = DOC.createDocumentFragment() + for (var i = 0, len = nodes.length; i < len; i++) { + frag.appendChild(nodes[i]) + } + return frag +} +Anot.directive('include', { + init: directives.attr.init, + update: function(val) { + var binding = this + var elem = this.element + var vmodels = binding.vmodels + var rendered = binding.includeRendered + var effectClass = binding.effectName && binding.effectClass // 是否开启动画 + var templateCache = binding.templateCache // 是否data-include-cache + var outer = binding.includeReplace // 是否data-include-replace + var loaded = binding.includeLoaded + var target = outer ? elem.parentNode : elem + var _ele = binding._element // data-include-replace binding.element === binding.end + + binding.recoverNodes = binding.recoverNodes || Anot.noop + + var scanTemplate = function(text) { + var _stamp = (binding._stamp = +new Date()) // 过滤掉频繁操作 + if (loaded) { + var newText = loaded.apply(target, [text].concat(vmodels)) + if (typeof newText === 'string') text = newText + } + if (rendered) { + checkScan( + target, + function() { + rendered.call(target) + }, + NaN + ) + } + var lastID = binding.includeLastID || '_default' // 默认 + + binding.includeLastID = val + var leaveEl = + (templateCache && templateCache[lastID]) || + DOC.createElement(elem.tagName || binding._element.tagName) // 创建一个离场元素 + + if (effectClass) { + leaveEl.className = effectClass + target.insertBefore(leaveEl, binding.start) // 插入到start之前,防止被错误的移动 + } + + // cache or animate,移动节点 + ;(templateCache || {})[lastID] = leaveEl + var fragOnDom = binding.recoverNodes() // 恢复动画中的节点 + if (fragOnDom) { + target.insertBefore(fragOnDom, binding.end) + } + while (true) { + var node = binding.start.nextSibling + if (node && node !== leaveEl && node !== binding.end) { + leaveEl.appendChild(node) + } else { + break + } + } + + // 元素退场 + Anot.effect.remove( + leaveEl, + target, + function() { + if (templateCache) { + // write cache + if (_stamp === binding._stamp) ifGroup.appendChild(leaveEl) + } + }, + binding + ) + + var enterEl = target, + before = Anot.noop, + after = Anot.noop + + var fragment = getTemplateContainer(binding, val, text) + var nodes = Anot.slice(fragment.childNodes) + + if (outer && effectClass) { + enterEl = _ele + enterEl.innerHTML = '' // 清空 + enterEl.setAttribute(':skip', 'true') + target.insertBefore(enterEl, binding.end.nextSibling) // 插入到bingding.end之后避免被错误的移动 + before = function() { + enterEl.insertBefore(fragment, null) // 插入节点 + } + after = function() { + binding.recoverNodes = Anot.noop + if (_stamp === binding._stamp) { + fragment = nodesToFrag(nodes) + target.insertBefore(fragment, binding.end) // 插入真实element + scanNodeArray(nodes, vmodels) + } + if (enterEl.parentNode === target) target.removeChild(enterEl) // 移除入场动画元素 + } + binding.recoverNodes = function() { + binding.recoverNodes = Anot.noop + return nodesToFrag(nodes) + } + } else { + before = function() { + //新添加元素的动画 + target.insertBefore(fragment, binding.end) + scanNodeArray(nodes, vmodels) + } + } + + Anot.effect.apply(enterEl, 'enter', before, after) + } + + if (!val) return + + var el = val + + if (typeof el === 'object') { + if (el.nodeType !== 1) return log('include 不支持非DOM对象') + } else { + el = DOC.getElementById(val) + if (!el) { + if (typeof templatePool[val] === 'string') { + Anot.nextTick(function() { + scanTemplate(templatePool[val]) + }) + } else if (Array.isArray(templatePool[val])) { + //#805 防止在循环绑定中发出许多相同的请求 + templatePool[val].push(scanTemplate) + } else { + var xhr = getXHR() + xhr.onload = function() { + if (xhr.status !== 200) + return log('获取网络资源出错, httpError[' + xhr.status + ']') + + var text = xhr.responseText + for (var f = 0, fn; (fn = templatePool[val][f++]); ) { + fn(text) + } + templatePool[val] = text + } + xhr.onerror = function() { + log(':include load [' + val + '] error') + } + templatePool[val] = [scanTemplate] + xhr.open('GET', val, true) + if ('withCredentials' in xhr) { + xhr.withCredentials = true + } + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest') + xhr.send(null) + } + return + } + } + + Anot.nextTick(function() { + scanTemplate(el.value || el.innerText || el.innerHTML) + }) + } +}) diff --git a/src/27-:on.js b/src/27-:on.js new file mode 100644 index 0000000..684bb1c --- /dev/null +++ b/src/27-:on.js @@ -0,0 +1,46 @@ +var rdash = /\(([^)]*)\)/ +var onDir = Anot.directive('on', { + priority: 3000, + init: function(binding) { + var value = binding.expr + binding.type = 'on' + var eventType = binding.param.replace(/-\d+$/, '') // :on-mousemove-10 + if (typeof onDir[eventType + 'Hook'] === 'function') { + onDir[eventType + 'Hook'](binding) + } + if (value.indexOf('(') > 0 && value.indexOf(')') > -1) { + var matched = (value.match(rdash) || ['', ''])[1].trim() + if (matched === '' || matched === '$event') { + // aaa() aaa($event)当成aaa处理 + value = value.replace(rdash, '') + } + } + binding.expr = value + }, + update: function(callback) { + var binding = this + var elem = this.element + callback = function(e) { + var fn = binding.getter || noop + return fn.apply(binding.args[0], binding.args.concat(e)) + } + + var eventType = binding.param.replace(/-\d+$/, '') // :on-mousemove-10 + if (eventType === 'scan') { + callback.call(elem, { + type: eventType + }) + } else if (typeof binding.specialBind === 'function') { + binding.specialBind(elem, callback) + } else { + var removeFn = Anot.bind(elem, eventType, callback) + } + binding.rollback = function() { + if (typeof binding.specialUnbind === 'function') { + binding.specialUnbind() + } else { + Anot.unbind(elem, eventType, removeFn) + } + } + } +}) diff --git a/src/28-:for.js b/src/28-:for.js new file mode 100644 index 0000000..5b38286 --- /dev/null +++ b/src/28-:for.js @@ -0,0 +1,476 @@ +Anot.directive('for', { + priority: 90, + init: function(binding) { + var type = binding.type + binding.cache = {} //用于存放代理VM + binding.enterCount = 0 + + var elem = binding.element + if (elem.nodeType === 1) { + var vars = binding.expr.split(' in ') + binding.expr = vars.pop() + if (vars.length) { + vars = vars.pop().split(/\s+/) + } + binding.vars = vars + elem.removeAttribute(binding.name) + effectBinding(elem, binding) + var rendered = getBindingCallback(elem, 'data-rendered', binding.vmodels) + + var signature = generateID(type) + var start = DOC.createComment(signature + ':start') + var end = (binding.element = DOC.createComment(signature + ':end')) + binding.signature = signature + binding.start = start + binding.template = anotFragment.cloneNode(false) + + var _parent = elem.parentNode + _parent.replaceChild(end, elem) + _parent.insertBefore(start, end) + binding.template.appendChild(elem) + + binding.element = end + + if (rendered) { + var removeFn = Anot.bind(_parent, 'datasetchanged', function() { + rendered.apply(_parent, _parent.args) + Anot.unbind(_parent, 'datasetchanged', removeFn) + _parent.msRendered = rendered + }) + } + } + }, + update: function(value, oldValue) { + var binding = this + var xtype = this.xtype + + if (xtype === 'array') { + if (!this.vars.length) { + this.vars.push('$index', 'el') + } else if (this.vars.length === 1) { + this.vars.unshift('$index') + } + this.param = this.vars[1] + } else { + this.param = '__el__' + if (!this.vars.length) { + this.vars.push('$key', '$val') + } else if (this.vars.length === 1) { + this.vars.push('$val') + } + } + + this.enterCount += 1 + var init = !oldValue + if (init) { + binding.$outer = {} + var check0 = this.vars[0] + var check1 = this.vars[1] + if (xtype === 'array') { + check0 = '$first' + check1 = '$last' + } + for (var i = 0, v; (v = binding.vmodels[i++]); ) { + if (v.hasOwnProperty(check0) && v.hasOwnProperty(check1)) { + binding.$outer = v + break + } + } + } + var track = this.track + var action = 'move' + binding.$repeat = value + var fragments = [] + var transation = init && anotFragment.cloneNode(false) + var proxies = [] + var param = this.param + var retain = Anot.mix({}, this.cache) + var elem = this.element + var length = track.length + + var _parent = elem.parentNode + + //检查新元素数量 + var newCount = 0 + for (i = 0; i < length; i++) { + var keyOrId = track[i] + if (!retain[keyOrId]) newCount++ + } + var oldCount = 0 + for (i in retain) { + oldCount++ + } + var clear = (!length || newCount === length) && oldCount > 10 //当全部是新元素,且移除元素较多(10)时使用clear + + var kill = elem.previousSibling + var start = binding.start + + if (clear) { + while (kill !== start) { + _parent.removeChild(kill) + kill = elem.previousSibling + } + } + + for (i = 0; i < length; i++) { + keyOrId = track[i] //array为随机数, object 为keyName + var proxy = retain[keyOrId] + if (!proxy) { + // log(this) + proxy = getProxyVM(this) + proxy.$up = this.vmodels[0] + if (xtype === 'array') { + action = 'add' + proxy.$id = keyOrId + var valueItem = value[i] + proxy[param] = valueItem //index + if (Object(valueItem) === valueItem) { + valueItem.$ups = valueItem.$ups || {} + valueItem.$ups[param] = proxy + } + } else { + action = 'append' + proxy[check0] = keyOrId + proxy[check1] = value[keyOrId] //key + var tmp = {} + tmp[check0] = proxy[check0] + tmp[check1] = proxy[check1] + proxy[param] = tmp + } + this.cache[keyOrId] = proxy + var node = proxy.$anchor || (proxy.$anchor = elem.cloneNode(false)) + node.nodeValue = this.signature + shimController( + binding, + transation, + proxy, + fragments, + init && !binding.effectDriver + ) + decorateProxy(proxy, binding, xtype) + } else { + fragments.push({}) + retain[keyOrId] = true + } + + //重写proxy + if (this.enterCount === 1) { + //防止多次进入,导致位置不对 + proxy.$active = false + proxy.$oldIndex = proxy.$index + proxy.$active = true + proxy.$index = i + } + + if (xtype === 'array') { + proxy.$first = i === 0 + proxy.$last = i === length - 1 + proxy[this.vars[0]] = proxy.$index + } else { + proxy[check1] = toJson(value[keyOrId]) //这里是处理vm.object = newObject的情况 + } + proxies.push(proxy) + } + this.proxies = proxies + if (init && !binding.effectDriver) { + _parent.insertBefore(transation, elem) + fragments.forEach(function(fragment) { + scanNodeArray(fragment.nodes || [], fragment.vmodels) + //if(fragment.vmodels.length > 2) + fragment.nodes = fragment.vmodels = null + }) // jshint ignore:line + } else { + var staggerIndex = (binding.staggerIndex = 0) + for (keyOrId in retain) { + if (retain[keyOrId] !== true) { + action = 'del' + !clear && removeItem(retain[keyOrId].$anchor, binding, true) + // 相当于delete binding.cache[key] + proxyRecycler(this.cache, keyOrId, param) + retain[keyOrId] = null + } + } + + for (i = 0; i < length; i++) { + proxy = proxies[i] + keyOrId = xtype === 'array' ? proxy.$id : proxy.$key + var pre = proxies[i - 1] + var preEl = pre ? pre.$anchor : binding.start + if (!retain[keyOrId]) { + //如果还没有插入到DOM树,进行插入动画 + ;(function(fragment, preElement) { + var nodes = fragment.nodes + var vmodels = fragment.vmodels + if (nodes) { + staggerIndex = mayStaggerAnimate( + binding.effectEnterStagger, + function() { + _parent.insertBefore(fragment.content, preElement.nextSibling) + scanNodeArray(nodes, vmodels) + !init && animateRepeat(nodes, 1, binding) + }, + staggerIndex + ) + } + fragment.nodes = fragment.vmodels = null + })(fragments[i], preEl) // jshint ignore:line + } else if (proxy.$index !== proxy.$oldIndex) { + //进行移动动画 + ;(function(proxy2, preElement) { + staggerIndex = mayStaggerAnimate( + binding.effectEnterStagger, + function() { + var curNode = removeItem(proxy2.$anchor) + var inserted = Anot.slice(curNode.childNodes) + _parent.insertBefore(curNode, preElement.nextSibling) + animateRepeat(inserted, 1, binding) + }, + staggerIndex + ) + })(proxy, preEl) // jshint ignore:line + } + } + } + if (!value.$track) { + //如果是非监控对象,那么就将其$events清空,阻止其持续监听 + for (keyOrId in this.cache) { + proxyRecycler(this.cache, keyOrId, param) + } + } + + // :for --> duplex + ;(function(args) { + _parent.args = args + if (_parent.msRendered) { + //第一次事件触发,以后直接调用 + _parent.msRendered.apply(_parent, args) + } + })(kernel.newWatch ? arguments : [action]) + var id = setTimeout(function() { + clearTimeout(id) + //触发上层的select回调及自己的rendered回调 + Anot.fireDom(_parent, 'datasetchanged', { + bubble: _parent.msHasEvent + }) + }) + this.enterCount -= 1 + } +}) + +function animateRepeat(nodes, isEnter, binding) { + for (var i = 0, node; (node = nodes[i++]); ) { + if (node.className === binding.effectClass) { + Anot.effect.apply(node, isEnter, noop, noop, binding) + } + } +} + +function mayStaggerAnimate(staggerTime, callback, index) { + if (staggerTime) { + setTimeout(callback, ++index * staggerTime) + } else { + callback() + } + return index +} + +function removeItem(node, binding, flagRemove) { + var fragment = anotFragment.cloneNode(false) + var last = node + var breakText = last.nodeValue + var staggerIndex = binding && Math.max(+binding.staggerIndex, 0) + var nodes = Anot.slice(last.parentNode.childNodes) + var index = nodes.indexOf(last) + while (true) { + var pre = nodes[--index] //node.previousSibling + if (!pre || String(pre.nodeValue).indexOf(breakText) === 0) { + break + } + if (!flagRemove && binding && pre.className === binding.effectClass) { + node = pre + ;(function(cur) { + binding.staggerIndex = mayStaggerAnimate( + binding.effectLeaveStagger, + function() { + Anot.effect.apply( + cur, + 0, + noop, + function() { + fragment.appendChild(cur) + }, + binding + ) + }, + staggerIndex + ) + })(pre) // jshint ignore:line + } else { + fragment.insertBefore(pre, fragment.firstChild) + } + } + fragment.appendChild(last) + return fragment +} + +function shimController(data, transation, proxy, fragments, init) { + var content = data.template.cloneNode(true) + var nodes = Anot.slice(content.childNodes) + content.appendChild(proxy.$anchor) + init && transation.appendChild(content) + var itemName = data.param || 'el' + var valueItem = proxy[itemName], + nv + + nv = [proxy].concat(data.vmodels) + + var fragment = { + nodes: nodes, + vmodels: nv, + content: content + } + fragments.push(fragment) +} +// {} --> {xx: 0, yy: 1, zz: 2} add +// {xx: 0, yy: 1, zz: 2} --> {xx: 0, yy: 1, zz: 2, uu: 3} +// [xx: 0, yy: 1, zz: 2} --> {xx: 0, zz: 1, yy: 2} + +function getProxyVM(binding) { + var agent = binding.xtype === 'object' ? withProxyAgent : eachProxyAgent + var proxy = agent(binding) + var node = proxy.$anchor || (proxy.$anchor = binding.element.cloneNode(false)) + node.nodeValue = binding.signature + proxy.$outer = binding.$outer + return proxy +} + +function decorateProxy(proxy, binding, type) { + if (type === 'array') { + proxy.$remove = function() { + binding.$repeat.removeAt(proxy.$index) + } + var param = binding.param + proxy.$watch(param, function(val) { + var index = proxy.$index + binding.$repeat[index] = val + }) + } else { + var __k__ = binding.vars[0] + var __v__ = binding.vars[1] + proxy.$up.$watch(binding.expr + '.' + proxy[__k__], function(val) { + proxy[binding.param][__v__] = val + proxy[__v__] = val + }) + } +} + +var eachProxyPool = [] + +function eachProxyAgent(data, proxy) { + var itemName = data.param || 'el' + for (var i = 0, n = eachProxyPool.length; i < n; i++) { + var candidate = eachProxyPool[i] + if (candidate && candidate.hasOwnProperty(itemName)) { + eachProxyPool.splice(i, 1) + proxy = candidate + break + } + } + if (!proxy) { + proxy = eachProxyFactory(data) + } + return proxy +} + +function eachProxyFactory(data) { + var itemName = data.param || 'el' + var __k__ = data.vars[0] + var source = { + $outer: {}, + $index: 0, + $oldIndex: 0, + $anchor: null, + //----- + $first: false, + $last: false, + $remove: Anot.noop + } + source[__k__] = 0 + source[itemName] = NaN + var force = { + $last: 1, + $first: 1, + $index: 1 + } + force[__k__] = 1 + force[itemName] = 1 + var proxy = modelFactory( + { state: source }, + { + force: force + } + ) + proxy.$id = generateID('proxy-each') + return proxy +} + +var withProxyPool = [] + +function withProxyAgent(data) { + return withProxyPool.pop() || withProxyFactory(data) +} + +function withProxyFactory(data) { + var itemName = data.param || '__el__' + var __k__ = data.vars[0] + var __v__ = data.vars[1] + var source = { + $index: 0, + $oldIndex: 0, + $outer: {}, + $anchor: null + } + source[__k__] = '' + source[__v__] = NaN + source[itemName] = NaN + var force = { + __el__: 1, + $index: 1 + } + force[__k__] = 1 + force[__v__] = 1 + var proxy = modelFactory( + { state: source }, + { + force: force + } + ) + proxy.$id = generateID('proxy-with') + return proxy +} + +function proxyRecycler(cache, key, param) { + var proxy = cache[key] + if (proxy) { + var proxyPool = + proxy.$id.indexOf('proxy-each') === 0 ? eachProxyPool : withProxyPool + proxy.$outer = {} + + for (var i in proxy.$events) { + var a = proxy.$events[i] + if (Array.isArray(a)) { + a.length = 0 + if (i === param) { + proxy[param] = NaN + } else if (i === '$val') { + proxy.$val = NaN + } + } + } + + if (proxyPool.unshift(proxy) > kernel.maxRepeatSize) { + proxyPool.pop() + } + delete cache[key] + } +} diff --git a/src/29-:visible.js b/src/29-:visible.js new file mode 100644 index 0000000..4768851 --- /dev/null +++ b/src/29-:visible.js @@ -0,0 +1,65 @@ +function parseDisplay(nodeName, val) { + //用于取得此类标签的默认display值 + var key = '_' + nodeName + if (!parseDisplay[key]) { + var node = DOC.createElement(nodeName) + root.appendChild(node) + if (W3C) { + val = getComputedStyle(node, null).display + } else { + val = node.currentStyle.display + } + root.removeChild(node) + parseDisplay[key] = val + } + return parseDisplay[key] +} + +Anot.parseDisplay = parseDisplay + +Anot.directive('visible', { + init: function(binding) { + effectBinding(binding.element, binding) + }, + update: function(val) { + var binding = this, + elem = this.element, + stamp + var noEffect = !this.effectName + if (!this.stamp) { + stamp = this.stamp = +new Date() + if (val) { + elem.style.display = binding.display || '' + if (Anot(elem).css('display') === 'none') { + elem.style.display = binding.display = parseDisplay(elem.nodeName) + } + } else { + elem.style.display = 'none' + } + return + } + stamp = this.stamp = +new Date() + if (val) { + Anot.effect.apply(elem, 1, function() { + if (stamp !== binding.stamp) return + var driver = elem.getAttribute('data-effect-driver') || 'a' + + if (noEffect) { + //不用动画时走这里 + elem.style.display = binding.display || '' + } + // "a", "t" + if (driver === 'a' || driver === 't') { + if (Anot(elem).css('display') === 'none') { + elem.style.display = binding.display || parseDisplay(elem.nodeName) + } + } + }) + } else { + Anot.effect.apply(elem, 0, function() { + if (stamp !== binding.stamp) return + elem.style.display = 'none' + }) + } + } +}) diff --git a/src/30-filters.js b/src/30-filters.js new file mode 100644 index 0000000..7e4b47d --- /dev/null +++ b/src/30-filters.js @@ -0,0 +1,151 @@ +/********************************************************************* + * 自带过滤器 * + **********************************************************************/ + +var rscripts = /]*>([\S\s]*?)<\/script\s*>/gim +var ron = /\s+(on[^=\s]+)(?:=("[^"]*"|'[^']*'|[^\s>]+))?/g +var ropen = /<\w+\b(?:(["'])[^"]*?(\1)|[^>])*>/gi +var rsanitize = { + a: /\b(href)\=("javascript[^"]*"|'javascript[^']*')/gi, + img: /\b(src)\=("javascript[^"]*"|'javascript[^']*')/gi, + form: /\b(action)\=("javascript[^"]*"|'javascript[^']*')/gi +} +var rsurrogate = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g +var rnoalphanumeric = /([^\#-~| |!])/g + +function numberFormat(number, decimals, point, thousands) { + //form http://phpjs.org/functions/number_format/ + //number 必需,要格式化的数字 + //decimals 可选,规定多少个小数位。 + //point 可选,规定用作小数点的字符串(默认为 . )。 + //thousands 可选,规定用作千位分隔符的字符串(默认为 , ),如果设置了该参数,那么所有其他参数都是必需的。 + number = (number + '').replace(/[^0-9+\-Ee.]/g, '') + var n = !isFinite(+number) ? 0 : +number, + prec = !isFinite(+decimals) ? 3 : Math.abs(decimals), + sep = thousands || ',', + dec = point || '.', + s = '', + toFixedFix = function(n, prec) { + var k = Math.pow(10, prec) + return '' + (Math.round(n * k) / k).toFixed(prec) + } + // Fix for IE parseFloat(0.55).toFixed(0) = 0; + s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.') + if (s[0].length > 3) { + s[0] = s[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, sep) + } + if ((s[1] || '').length < prec) { + s[1] = s[1] || '' + s[1] += new Array(prec - s[1].length + 1).join('0') + } + return s.join(dec) +} + +var filters = (Anot.filters = { + uppercase: function(str) { + return str.toUpperCase() + }, + lowercase: function(str) { + return str.toLowerCase() + }, + //字符串截取,超过指定长度以mark标识接上 + truncate: function(str, len, mark) { + len = len || 30 + mark = typeof mark === 'string' ? mark : '...' + return str.slice(0, len) + (str.length <= len ? '' : mark) + }, + //小值秒数转化为 时间格式 + time: function(str) { + str = str >> 0 + var s = str % 60 + var m = Math.floor(str / 60) + var h = Math.floor(m / 60) + m = m % 60 + m = m < 10 ? '0' + m : m + s = s < 10 ? '0' + s : s + + if (h > 0) { + h = h < 10 ? '0' + h : h + return h + ':' + m + ':' + s + } + return m + ':' + s + }, + $filter: function(val) { + for (var i = 1, n = arguments.length; i < n; i++) { + var array = arguments[i] + var fn = Anot.filters[array[0]] + if (typeof fn === 'function') { + var arr = [val].concat(array.slice(1)) + val = fn.apply(null, arr) + } + } + return val + }, + camelize: camelize, + //https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet + // chrome + // chrome + // IE67chrome + // IE67chrome + // IE67chrome + sanitize: function(str) { + return str.replace(rscripts, '').replace(ropen, function(a, b) { + var match = a.toLowerCase().match(/<(\w+)\s/) + if (match) { + //处理a标签的href属性,img标签的src属性,form标签的action属性 + var reg = rsanitize[match[1]] + if (reg) { + a = a.replace(reg, function(s, name, value) { + var quote = value.charAt(0) + return name + '=' + quote + 'javascript:void(0)' + quote // jshint ignore:line + }) + } + } + return a.replace(ron, ' ').replace(/\s+/g, ' ') //移除onXXX事件 + }) + }, + escape: function(str) { + //将字符串经过 str 转义得到适合在页面中显示的内容, 例如替换 < 为 < + return String(str) + .replace(/&/g, '&') + .replace(rsurrogate, function(value) { + var hi = value.charCodeAt(0) + var low = value.charCodeAt(1) + return '&#' + ((hi - 0xd800) * 0x400 + (low - 0xdc00) + 0x10000) + ';' + }) + .replace(rnoalphanumeric, function(value) { + return '&#' + value.charCodeAt(0) + ';' + }) + .replace(//g, '>') + }, + currency: function(amount, symbol, fractionSize) { + return ( + (symbol || '\u00a5') + + numberFormat(amount, isFinite(fractionSize) ? fractionSize : 2) + ) + }, + number: numberFormat, + //日期格式化,类似php的date函数, + date: function(stamp, str, second) { + second = second === undefined ? false : true + var oDate + if (!Date.isDate(stamp)) { + if (!/[^\d]/.test(stamp)) { + stamp -= 0 + if (second) { + stamp *= 1000 + } + } + + oDate = new Date(stamp) + if (oDate + '' === 'Invalid Date') { + return 'Invalid Date' + } + } else { + oDate = stamp + } + return oDate.format(str) + } +}) + diff --git a/src/lib/amd.js b/src/lib/amd.js new file mode 100644 index 0000000..ca2bc69 --- /dev/null +++ b/src/lib/amd.js @@ -0,0 +1,677 @@ +/********************************************************************* + * AMD加载器 * + **********************************************************************/ + +//https://www.devbridge.com/articles/understanding-amd-requirejs/ +//http://maxogden.com/nested-dependencies.html +var modules = (Anot.modules = { + 'domReady!': { + exports: Anot, + state: 3 + }, + Anot: { + exports: Anot, + state: 4 + } +}) +//Object(modules[id]).state拥有如下值 +// undefined 没有定义 +// 1(send) 已经发出请求 +// 2(loading) 已经被执行但还没有执行完成,在这个阶段define方法会被执行 +// 3(loaded) 执行完毕,通过onload/onreadystatechange回调判定,在这个阶段checkDeps方法会执行 +// 4(execute) 其依赖也执行完毕, 值放到exports对象上,在这个阶段fireFactory方法会执行 +modules.exports = modules.Anot +var otherRequire = window.require +var otherDefine = window.define +var innerRequire +plugins.loader = function(builtin) { + var flag = innerRequire && builtin + window.require = flag ? innerRequire : otherRequire + window.define = flag ? innerRequire.define : otherDefine +} +new function() { + // jshint ignore:line + var loadings = [] //正在加载中的模块列表 + var factorys = [] //放置define方法的factory函数 + var rjsext = /\.js$/i + + function makeRequest(name, config) { + //1. 去掉querystring, hash + var query = '' + name = name.replace(rquery, function(match) { + query = match + return '' + }) + + //2. 去掉扩展名 + var ext = '.js' //默认拓展名 + var res = 'js' // 默认资源类型 + var suffix = ['.js', '.css'] + name = name.replace(/\.[a-z0-9]+$/g, function(match) { + ext = match + res = suffix.indexOf(match) > -1 ? match.slice(1) : 'text' + return '' + }) + + //补上协议, 避免引入依赖时判断不正确 + if (/^\/\//.test(name)) { + name = location.protocol + name + } + var req = Anot.mix( + { + query: query, + ext: ext, + res: res, + name: name, + toUrl: toUrl + }, + config + ) + req.toUrl(name) + return req + } + + function fireRequest(req) { + var name = req.name + var res = req.res + //1. 如果该模块已经发出请求,直接返回 + var module = modules[name] + var urlNoQuery = name && req.urlNoQuery + if (module && module.state >= 1) { + return name + } + module = modules[urlNoQuery] + if (module && module.state >= 3) { + innerRequire(module.deps || [], module.factory, urlNoQuery) + return urlNoQuery + } + if (name && !module) { + module = modules[urlNoQuery] = { + id: urlNoQuery, + state: 1 //send + } + var wrap = function(obj) { + resources[res] = obj + obj.load(name, req, function(a) { + if (arguments.length && a !== void 0) { + module.exports = a + } + module.state = 4 + checkDeps() + }) + } + + if (!resources[res]) { + innerRequire([res], wrap) + } else { + wrap(resources[res]) + } + } + return name ? urlNoQuery : res + '!' + } + + //核心API之一 require + var requireQueue = [] + var isUserFirstRequire = false + innerRequire = Anot.require = function( + array, + factory, + parentUrl, + defineConfig + ) { + if (!isUserFirstRequire) { + requireQueue.push(Anot.slice(arguments)) + if (arguments.length <= 2) { + isUserFirstRequire = true + var queue = requireQueue.splice(0, requireQueue.length), + args + while ((args = queue.shift())) { + innerRequire.apply(null, args) + } + } + return + } + if (!Array.isArray(array)) { + Anot.error('require方法的第一个参数应为数组 ' + array) + } + var deps = [] // 放置所有依赖项的完整路径 + var uniq = createMap() + var id = parentUrl || 'callback' + setTimeout('1') // jshint ignore:line + + defineConfig = defineConfig || createMap() + defineConfig.baseUrl = kernel.baseUrl + var isBuilt = !!defineConfig.built + if (parentUrl) { + defineConfig.parentUrl = parentUrl.substr(0, parentUrl.lastIndexOf('/')) + defineConfig.mapUrl = parentUrl.replace(rjsext, '') + } + if (isBuilt) { + var req = makeRequest(defineConfig.defineName, defineConfig) + id = req.urlNoQuery + } else { + array.forEach(function(name) { + if (!name) { + return + } + var req = makeRequest(name, defineConfig) + var url = fireRequest(req) //加载资源,并返回该资源的完整地址 + + if (url) { + if (!uniq[url]) { + deps.push(url) + uniq[url] = !0 + } + } + }) + } + + var module = modules[id] + if (!module || module.state !== 4) { + modules[id] = { + id: id, + deps: isBuilt ? array.concat() : deps, + factory: factory || noop, + state: 3 + } + } + if (!module) { + //如果此模块是定义在另一个JS文件中, 那必须等该文件加载完毕, 才能放到检测列队中 + loadings.push(id) + } + checkDeps() + } + + //核心API之二 require + innerRequire.define = function(name, deps, factory) { + //模块名,依赖列表,模块本身 + if (typeof name !== 'string') { + factory = deps + deps = name + name = 'anonymous' + } + if (!Array.isArray(deps)) { + factory = deps + deps = [] + } + var config = { + built: !isUserFirstRequire, //用r.js打包后,所有define会放到requirejs之前 + defineName: name + } + var args = [deps, factory, config] + factory.require = function(url) { + args.splice(2, 0, url) + if (modules[url]) { + modules[url].state = 3 //loaded + var isCycle = false + try { + isCycle = checkCycle(modules[url].deps, url) + } catch (e) {} + if (isCycle) { + Anot.error( + url + + '模块与之前的模块存在循环依赖,请不要直接用script标签引入' + + url + + '模块' + ) + } + } + delete factory.require //释放内存 + innerRequire.apply(null, args) //0,1,2 --> 1,2,0 + } + + //根据标准,所有遵循W3C标准的浏览器,script标签会按标签的出现顺序执行。 + //老的浏览器中,加载也是按顺序的:一个文件下载完成后,才开始下载下一个文件。 + //较新的浏览器中(IE8+ 、FireFox3.5+ 、Chrome4+ 、Safari4+),为了减小请求时间以优化体验, + //下载可以是并行的,但是执行顺序还是按照标签出现的顺序。 + //但如果script标签是动态插入的, 就未必按照先请求先执行的原则了,目测只有firefox遵守 + //唯一比较一致的是,IE10+及其他标准浏览器,一旦开始解析脚本, 就会一直堵在那里,直接脚本解析完毕 + //亦即,先进入loading阶段的script标签(模块)必然会先进入loaded阶段 + var url = config.built ? 'unknown' : getCurrentScript() + if (url) { + var module = modules[url] + if (module) { + module.state = 2 + } + factory.require(url) + } else { + //合并前后的safari,合并后的IE6-9走此分支 + factorys.push(factory) + } + } + //核心API之三 require.config(settings) + innerRequire.config = kernel + //核心API之四 define.amd 标识其符合AMD规范 + innerRequire.define.amd = modules + + //==========================对用户配置项进行再加工========================== + var allpaths = (kernel['orig.paths'] = createMap()) + var allmaps = (kernel['orig.map'] = createMap()) + var allpackages = (kernel['packages'] = []) + var allargs = (kernel['orig.args'] = createMap()) + Anot.mix(plugins, { + paths: function(hash) { + Anot.mix(allpaths, hash) + kernel.paths = makeIndexArray(allpaths) + }, + map: function(hash) { + Anot.mix(allmaps, hash) + var list = makeIndexArray(allmaps, 1, 1) + Anot.each(list, function(_, item) { + item.val = makeIndexArray(item.val) + }) + kernel.map = list + }, + packages: function(array) { + array = array.concat(allpackages) + var uniq = createMap() + var ret = [] + for (var i = 0, pkg; (pkg = array[i++]); ) { + pkg = typeof pkg === 'string' ? { name: pkg } : pkg + var name = pkg.name + if (!uniq[name]) { + var url = joinPath(pkg.location || name, pkg.main || 'main') + url = url.replace(rjsext, '') + ret.push(pkg) + uniq[name] = pkg.location = url + pkg.reg = makeMatcher(name) + } + } + kernel.packages = ret.sort() + }, + urlArgs: function(hash) { + if (typeof hash === 'string') { + hash = { '*': hash } + } + Anot.mix(allargs, hash) + kernel.urlArgs = makeIndexArray(allargs, 1) + }, + baseUrl: function(url) { + if (!isAbsUrl(url)) { + var baseElement = head.getElementsByTagName('base')[0] + if (baseElement) { + head.removeChild(baseElement) + } + var node = DOC.createElement('a') + node.href = url + url = node.href + if (baseElement) { + head.insertBefore(baseElement, head.firstChild) + } + } + if (url.length > 3) kernel.baseUrl = url + }, + shim: function(obj) { + for (var i in obj) { + var value = obj[i] + if (Array.isArray(value)) { + value = obj[i] = { + deps: value + } + } + if (!value.exportsFn && (value.exports || value.init)) { + value.exportsFn = makeExports(value) + } + } + kernel.shim = obj + } + }) + + //==============================内部方法================================= + function checkCycle(deps, nick) { + //检测是否存在循环依赖 + for (var i = 0, id; (id = deps[i++]); ) { + if ( + modules[id].state !== 4 && + (id === nick || checkCycle(modules[id].deps, nick)) + ) { + return true + } + } + } + + function checkFail(node, onError) { + var id = trimQuery(node.src) //检测是否死链 + node.onload = node.onerror = null + if (onError) { + setTimeout(function() { + head.removeChild(node) + node = null // 处理旧式IE下的循环引用问题 + }) + log('加载 ' + id + ' 失败') + } else { + return true + } + } + + function checkDeps() { + //检测此JS模块的依赖是否都已安装完毕,是则安装自身 + loop: for (var i = loadings.length, id; (id = loadings[--i]); ) { + var obj = modules[id], + deps = obj.deps + + if (!deps) continue + for (var j = 0, key; (key = deps[j]); j++) { + if (Object(modules[key]).state !== 4) { + continue loop + } + } + //如果deps是空对象或者其依赖的模块的状态都是4 + if (obj.state !== 4) { + loadings.splice(i, 1) //必须先移除再安装,防止在IE下DOM树建完后手动刷新页面,会多次执行它 + fireFactory(obj.id, obj.deps, obj.factory) + checkDeps() //如果成功,则再执行一次,以防有些模块就差本模块没有安装好 + } + } + } + + function loadJS(url, id, callback) { + //通过script节点加载目标模块 + var node = DOC.createElement('script') + node.className = subscribers //让getCurrentScript只处理类名为subscribers的script节点 + node.onload = function() { + var factory = factorys.pop() + factory && factory.require(id) + if (callback) { + callback() + } + id && loadings.push(id) + checkDeps() + } + node.onerror = function() { + checkFail(node, true) + } + + head.insertBefore(node, head.firstChild) //chrome下第二个参数不能为null + node.src = url //插入到head的第一个节点前,防止IE6下head标签没闭合前使用appendChild抛错,更重要的是IE6下可以收窄getCurrentScript的寻找范围 + } + + var resources = (innerRequire.plugins = { + //三大常用资源插件 js!, css!, text!, domReady! + domReady: { + load: noop + }, + js: { + load: function(name, req, onLoad) { + var url = req.url + var id = req.urlNoQuery + var shim = kernel.shim[name.replace(rjsext, '')] + if (shim) { + //shim机制 + innerRequire(shim.deps || [], function() { + var args = Anot.slice(arguments) + loadJS(url, id, function() { + onLoad(shim.exportsFn ? shim.exportsFn.apply(0, args) : void 0) + }) + }) + } else { + loadJS(url, id) + } + } + }, + css: { + load: function(name, req, onLoad) { + var url = req.url + head.insertAdjacentHTML( + 'afterBegin', + '' + ) + onLoad() + } + }, + text: { + load: function(name, req, onLoad) { + var xhr = getXHR() + xhr.onload = function() { + var status = xhr.status + if (status > 399 && status < 600) { + Anot.error(url + ' 对应资源不存在或没有开启 CORS') + } else { + onLoad(xhr.responseText) + } + } + xhr.open('GET', req.url, true) + xhr.send() + } + } + }) + innerRequire.checkDeps = checkDeps + + var rquery = /(\?[^#]*)$/ + function trimQuery(url) { + return (url || '').replace(rquery, '') + } + + function isAbsUrl(path) { + //http://stackoverflow.com/questions/10687099/how-to-test-if-a-url-string-is-absolute-or-relative + return /^(?:[a-z\-]+:)?\/\//i.test(String(path)) + } + + function getCurrentScript() { + // inspireb by https://github.com/samyk/jiagra/blob/master/jiagra.js + var stack + try { + a.b.c() //强制报错,以便捕获e.stack + } catch (e) { + //safari5的sourceURL,firefox的fileName,它们的效果与e.stack不一样 + stack = e.stack + } + if (stack) { + /**e.stack最后一行在所有支持的浏览器大致如下: + *chrome23: + * at http://113.93.50.63/data.js:4:1 + *firefox17: + *@http://113.93.50.63/query.js:4 + *opera12:http://www.oldapps.com/opera.php?system=Windows_XP + *@http://113.93.50.63/data.js:4 + *IE10: + * at Global code (http://113.93.50.63/data.js:4:1) + * //firefox4+ 可以用document.currentScript + */ + stack = stack.split(/[@ ]/g).pop() //取得最后一行,最后一个空格或@之后的部分 + stack = stack[0] === '(' ? stack.slice(1, -1) : stack.replace(/\s/, '') //去掉换行符 + return trimQuery(stack.replace(/(:\d+)?:\d+$/i, '')) //去掉行号与或许存在的出错字符起始位置 + } + var nodes = head.getElementsByTagName('script') //只在head标签中寻找 + for (var i = nodes.length, node; (node = nodes[--i]); ) { + if (node.className === subscribers && node.readyState === 'interactive') { + var url = node.src + return (node.className = trimQuery(url)) + } + } + } + + var rcallback = /^callback\d+$/ + function fireFactory(id, deps, factory) { + var module = Object(modules[id]) + module.state = 4 + for (var i = 0, array = [], d; (d = deps[i++]); ) { + if (d === 'exports') { + var obj = module.exports || (module.exports = createMap()) + array.push(obj) + } else { + array.push(modules[d].exports) + } + } + try { + var ret = factory.apply(window, array) + } catch (e) { + log('执行[' + id + ']模块的factory抛错: ', e) + } + if (ret !== void 0) { + module.exports = ret + } + if (rcallback.test(id)) { + delete modules[id] + } + delete module.factory + return ret + } + function toUrl(id) { + if (id.indexOf(this.res + '!') === 0) { + id = id.slice(this.res.length + 1) //处理define("css!style",[], function(){})的情况 + } + var url = id + //1. 是否命中paths配置项 + var usePath = 0 + var baseUrl = this.baseUrl + var rootUrl = this.parentUrl || baseUrl + eachIndexArray(id, kernel.paths, function(value, key) { + url = url.replace(key, value) + usePath = 1 + }) + //2. 是否命中packages配置项 + if (!usePath) { + eachIndexArray(id, kernel.packages, function(value, key, item) { + url = url.replace(item.name, item.location) + }) + } + //3. 是否命中map配置项 + if (this.mapUrl) { + eachIndexArray(this.mapUrl, kernel.map, function(array) { + eachIndexArray(url, array, function(mdValue, mdKey) { + url = url.replace(mdKey, mdValue) + rootUrl = baseUrl + }) + }) + } + var ext = this.ext + if (ext && usePath && url.slice(-ext.length) === ext) { + url = url.slice(0, -ext.length) + } + //4. 转换为绝对路径 + if (!isAbsUrl(url)) { + rootUrl = this.built || /^\w/.test(url) ? baseUrl : rootUrl + url = joinPath(rootUrl, url) + } + //5. 还原扩展名,query + var urlNoQuery = url + ext + url = urlNoQuery + this.query + urlNoQuery = url.replace(rquery, function(a) { + this.query = a + return '' + }) + //6. 处理urlArgs + eachIndexArray(id, kernel.urlArgs, function(value) { + url += (url.indexOf('?') === -1 ? '?' : '&') + value + }) + this.url = url + return (this.urlNoQuery = urlNoQuery) + } + + function makeIndexArray(hash, useStar, part) { + //创建一个经过特殊算法排好序的数组 + var index = hash2array(hash, useStar, part) + index.sort(descSorterByName) + return index + } + + function makeMatcher(prefix) { + return new RegExp('^' + prefix + '(/|$)') + } + + function makeExports(value) { + return function() { + var ret + if (value.init) { + ret = value.init.apply(window, arguments) + } + return ret || (value.exports && getGlobal(value.exports)) + } + } + + function hash2array(hash, useStar, part) { + var array = [] + for (var key in hash) { + // if (hash.hasOwnProperty(key)) {//hash是由createMap创建没有hasOwnProperty + var item = { + name: key, + val: hash[key] + } + array.push(item) + item.reg = key === '*' && useStar ? /^/ : makeMatcher(key) + if (part && key !== '*') { + item.reg = new RegExp('/' + key.replace(/^\//, '') + '(/|$)') + } + // } + } + return array + } + + function eachIndexArray(moduleID, array, matcher) { + array = array || [] + for (var i = 0, el; (el = array[i++]); ) { + if (el.reg.test(moduleID)) { + matcher(el.val, el.name, el) + return false + } + } + } + // 根据元素的name项进行数组字符数逆序的排序函数 + function descSorterByName(a, b) { + var aaa = a.name + var bbb = b.name + if (bbb === '*') { + return -1 + } + if (aaa === '*') { + return 1 + } + return bbb.length - aaa.length + } + + var rdeuce = /\/\w+\/\.\./ + function joinPath(a, b) { + if (a.charAt(a.length - 1) !== '/') { + a += '/' + } + if (b.slice(0, 2) === './') { + //相对于兄弟路径 + return a + b.slice(2) + } + if (b.slice(0, 2) === '..') { + //相对于父路径 + a += b + while (rdeuce.test(a)) { + a = a.replace(rdeuce, '') + } + return a + } + if (b.slice(0, 1) === '/') { + return a + b.slice(1) + } + return a + b + } + + function getGlobal(value) { + if (!value) { + return value + } + var g = window + value.split('.').forEach(function(part) { + g = g[part] + }) + return g + } + + var mainNode = DOC.scripts[DOC.scripts.length - 1] + var dataMain = mainNode.getAttribute('data-main') + if (dataMain) { + plugins.baseUrl(dataMain) + var href = kernel.baseUrl + kernel.baseUrl = href.slice(0, href.lastIndexOf('/') + 1) + loadJS(href.replace(rjsext, '') + '.js') + } else { + var loaderUrl = trimQuery(mainNode.src) + kernel.baseUrl = loaderUrl.slice(0, loaderUrl.lastIndexOf('/') + 1) + } +}() // jshint ignore:line + +Anot.config({ + loader: true +}) + +if (typeof define === 'function' && define.amd) { + define('Anot', [], function() { + return Anot + }) +} diff --git a/src/lib/touch.js b/src/lib/touch.js new file mode 100644 index 0000000..a444471 --- /dev/null +++ b/src/lib/touch.js @@ -0,0 +1,584 @@ +var ua = navigator.userAgent.toLowerCase() +//http://stackoverflow.com/questions/9038625/detect-if-device-is-ios +function iOSversion() { + //https://developer.apple.com/library/prerelease/mac/releasenotes/General/WhatsNewInSafari/Articles/Safari_9.html + //http://mp.weixin.qq.com/s?__biz=MzA3MDQ4MzQzMg==&mid=256900619&idx=1&sn=b29f84cff0b8d7b9742e5d8b3cd8f218&scene=1&srcid=1009F9l4gh9nZ7rcQJEhmf7Q#rd + if (/ipad|iphone|ipod/.test(ua) && !window.MSStream) { + if ('backdropFilter' in document.documentElement.style) { + return 9 + } + if (!!window.indexedDB) { + return 8 + } + if (!!window.SpeechSynthesisUtterance) { + return 7 + } + if (!!window.webkitAudioContext) { + return 6 + } + if (!!window.matchMedia) { + return 5 + } + if (!!window.history && 'pushState' in window.history) { + return 4 + } + return 3 + } + return NaN +} + +var deviceIsAndroid = ua.indexOf('android') > 0 +var deviceIsIOS = iOSversion() + +var Recognizer = (Anot.gestureHooks = { + pointers: {}, + //以AOP切入touchstart, touchmove, touchend, touchcancel回调 + start: function(event, callback) { + //touches是当前屏幕上所有触摸点的列表; + //targetTouches是当前对象上所有触摸点的列表; + //changedTouches是涉及当前事件的触摸点的列表。 + for (var i = 0; i < event.changedTouches.length; i++) { + var touch = event.changedTouches[i], + id = touch.identifier, + pointer = { + startTouch: mixLocations({}, touch), + startTime: Date.now(), + status: 'tapping', + element: event.target, + pressingHandler: + Recognizer.pointers[id] && Recognizer.pointers[id].pressingHandler + } + Recognizer.pointers[id] = pointer + callback(pointer, touch) + } + }, + move: function(event, callback) { + for (var i = 0; i < event.changedTouches.length; i++) { + var touch = event.changedTouches[i] + var pointer = Recognizer.pointers[touch.identifier] + if (!pointer) { + return + } + + if (!('lastTouch' in pointer)) { + pointer.lastTouch = pointer.startTouch + pointer.lastTime = pointer.startTime + pointer.deltaX = pointer.deltaY = pointer.duration = pointer.distance = 0 + } + + var time = Date.now() - pointer.lastTime + + if (time > 0) { + var RECORD_DURATION = 70 + if (time > RECORD_DURATION) { + time = RECORD_DURATION + } + if (pointer.duration + time > RECORD_DURATION) { + pointer.duration = RECORD_DURATION - time + } + + pointer.duration += time + pointer.lastTouch = mixLocations({}, touch) + + pointer.lastTime = Date.now() + + pointer.deltaX = touch.clientX - pointer.startTouch.clientX + pointer.deltaY = touch.clientY - pointer.startTouch.clientY + var x = pointer.deltaX * pointer.deltaX + var y = pointer.deltaY * pointer.deltaY + pointer.distance = Math.sqrt(x + y) + pointer.isVertical = x < y + + callback(pointer, touch) + } + } + }, + end: function(event, callback) { + for (var i = 0; i < event.changedTouches.length; i++) { + var touch = event.changedTouches[i], + id = touch.identifier, + pointer = Recognizer.pointers[id] + + if (!pointer) continue + + callback(pointer, touch) + + delete Recognizer.pointers[id] + } + }, + //人工触发合成事件 + fire: function(elem, type, props) { + if (elem) { + var event = document.createEvent('Events') + event.initEvent(type, true, true) + Anot.mix(event, props) + elem.dispatchEvent(event) + } + }, + //添加各种识别器 + add: function(name, recognizer) { + function move(event) { + recognizer.touchmove(event) + } + + function end(event) { + recognizer.touchend(event) + + document.removeEventListener('touchmove', move) + + document.removeEventListener('touchend', end) + + document.removeEventListener('touchcancel', cancel) + } + + function cancel(event) { + recognizer.touchcancel(event) + + document.removeEventListener('touchmove', move) + + document.removeEventListener('touchend', end) + + document.removeEventListener('touchcancel', cancel) + } + + recognizer.events.forEach(function(eventName) { + Anot.eventHooks[eventName] = { + fix: function(el, fn) { + if (!el['touch-' + name]) { + el['touch-' + name] = '1' + el.addEventListener('touchstart', function(event) { + recognizer.touchstart(event) + + document.addEventListener('touchmove', move) + + document.addEventListener('touchend', end) + + document.addEventListener('touchcancel', cancel) + }) + } + return fn + } + } + }) + } +}) + +var locations = ['screenX', 'screenY', 'clientX', 'clientY', 'pageX', 'pageY'] + +// 复制 touch 对象上的有用属性到固定对象上 +function mixLocations(target, source) { + if (source) { + locations.forEach(function(key) { + target[key] = source[key] + }) + } + return target +} + +var supportPointer = !!navigator.pointerEnabled || !!navigator.msPointerEnabled + +if (supportPointer) { + // 支持pointer的设备可用样式来取消click事件的300毫秒延迟 + root.style.msTouchAction = root.style.touchAction = 'none' +} +var tapRecognizer = { + events: ['tap'], + touchBoundary: 10, + tapDelay: 200, + needClick: function(target) { + //判定是否使用原生的点击事件, 否则使用sendClick方法手动触发一个人工的点击事件 + switch (target.nodeName.toLowerCase()) { + case 'button': + case 'select': + case 'textarea': + if (target.disabled) { + return true + } + + break + case 'input': + // IOS6 pad 上选择文件,如果不是原生的click,弹出的选择界面尺寸错误 + if ((deviceIsIOS && target.type === 'file') || target.disabled) { + return true + } + + break + case 'label': + case 'iframe': + case 'video': + return true + } + + return false + }, + needFocus: function(target) { + switch (target.nodeName.toLowerCase()) { + case 'textarea': + case 'select': //实测android下select也需要 + return true + case 'input': + switch (target.type) { + case 'button': + case 'checkbox': + case 'file': + case 'image': + case 'radio': + case 'submit': + return false + } + //如果是只读或disabled状态,就无须获得焦点了 + return !target.disabled && !target.readOnly + default: + return false + } + }, + focus: function(targetElement) { + var length + //在iOS7下, 对一些新表单元素(如date, datetime, time, month)调用focus方法会抛错, + //幸好的是,我们可以改用setSelectionRange获取焦点, 将光标挪到文字的最后 + var type = targetElement.type + if ( + deviceIsIOS && + targetElement.setSelectionRange && + type.indexOf('date') !== 0 && + type !== 'time' && + type !== 'month' + ) { + length = targetElement.value.length + targetElement.setSelectionRange(length, length) + } else { + targetElement.focus() + } + }, + findControl: function(labelElement) { + // 获取label元素所对应的表单元素 + // 可以能过control属性, getElementById, 或用querySelector直接找其内部第一表单元素实现 + if (labelElement.control !== undefined) { + return labelElement.control + } + + if (labelElement.htmlFor) { + return document.getElementById(labelElement.htmlFor) + } + + return labelElement.querySelector( + 'button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea' + ) + }, + fixTarget: function(target) { + if (target.nodeType === 3) { + return target.parentNode + } + if (window.SVGElementInstance && target instanceof SVGElementInstance) { + return target.correspondingUseElement + } + + return target + }, + updateScrollParent: function(targetElement) { + //如果事件源元素位于某一个有滚动条的祖父元素中,那么保持其scrollParent与scrollTop值 + var scrollParent = targetElement.tapScrollParent + + if (!scrollParent || !scrollParent.contains(targetElement)) { + var parentElement = targetElement + do { + if (parentElement.scrollHeight > parentElement.offsetHeight) { + scrollParent = parentElement + targetElement.tapScrollParent = parentElement + break + } + + parentElement = parentElement.parentElement + } while (parentElement) + } + + if (scrollParent) { + scrollParent.lastScrollTop = scrollParent.scrollTop + } + }, + touchHasMoved: function(event) { + //判定是否发生移动,其阀值是10px + var touch = event.changedTouches[0], + boundary = tapRecognizer.touchBoundary + return ( + Math.abs(touch.pageX - tapRecognizer.pageX) > boundary || + Math.abs(touch.pageY - tapRecognizer.pageY) > boundary + ) + }, + + findType: function(targetElement) { + // 安卓chrome浏览器上,模拟的 click 事件不能让 select 打开,故使用 mousedown 事件 + return deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select' + ? 'mousedown' + : 'click' + }, + sendClick: function(targetElement, event) { + // 在click之前触发tap事件 + Recognizer.fire(targetElement, 'tap', { + touchEvent: event + }) + var clickEvent, touch + //某些安卓设备必须先移除焦点,之后模拟的click事件才能让新元素获取焦点 + if (document.activeElement && document.activeElement !== targetElement) { + document.activeElement.blur() + } + + touch = event.changedTouches[0] + // 手动触发点击事件,此时必须使用document.createEvent('MouseEvents')来创建事件 + // 及使用initMouseEvent来初始化它 + clickEvent = document.createEvent('MouseEvents') + clickEvent.initMouseEvent( + tapRecognizer.findType(targetElement), + true, + true, + window, + 1, + touch.screenX, + touch.screenY, + touch.clientX, + touch.clientY, + false, + false, + false, + false, + 0, + null + ) + clickEvent.touchEvent = event + targetElement.dispatchEvent(clickEvent) + }, + touchstart: function(event) { + //忽略多点触摸 + if (event.targetTouches.length !== 1) { + return true + } + //修正事件源对象 + var targetElement = tapRecognizer.fixTarget(event.target) + var touch = event.targetTouches[0] + if (deviceIsIOS) { + // 判断是否是点击文字,进行选择等操作,如果是,不需要模拟click + var selection = window.getSelection() + if (selection.rangeCount && !selection.isCollapsed) { + return true + } + var id = touch.identifier + //当 alert 或 confirm 时,点击其他地方,会触发touch事件,identifier相同,此事件应该被忽略 + if ( + id && + isFinite(tapRecognizer.lastTouchIdentifier) && + tapRecognizer.lastTouchIdentifier === id + ) { + event.preventDefault() + return false + } + + tapRecognizer.lastTouchIdentifier = id + + tapRecognizer.updateScrollParent(targetElement) + } + //收集触摸点的信息 + tapRecognizer.status = 'tapping' + tapRecognizer.startTime = Date.now() + tapRecognizer.element = targetElement + tapRecognizer.pageX = touch.pageX + tapRecognizer.pageY = touch.pageY + // 如果点击太快,阻止双击带来的放大收缩行为 + if ( + tapRecognizer.startTime - tapRecognizer.lastTime < + tapRecognizer.tapDelay + ) { + event.preventDefault() + } + }, + touchmove: function(event) { + if (tapRecognizer.status !== 'tapping') { + return true + } + // 如果事件源元素发生改变,或者发生了移动,那么就取消触发点击事件 + if ( + tapRecognizer.element !== tapRecognizer.fixTarget(event.target) || + tapRecognizer.touchHasMoved(event) + ) { + tapRecognizer.status = tapRecognizer.element = 0 + } + }, + touchend: function(event) { + var targetElement = tapRecognizer.element + var now = Date.now() + //如果是touchstart与touchend相隔太久,可以认为是长按,那么就直接返回 + //或者是在touchstart, touchmove阶段,判定其不该触发点击事件,也直接返回 + if ( + !targetElement || + now - tapRecognizer.startTime > tapRecognizer.tapDelay + ) { + return true + } + + tapRecognizer.lastTime = now + + var startTime = tapRecognizer.startTime + tapRecognizer.status = tapRecognizer.startTime = 0 + + targetTagName = targetElement.tagName.toLowerCase() + if (targetTagName === 'label') { + //尝试触发label上可能绑定的tap事件 + Recognizer.fire(targetElement, 'tap', { + touchEvent: event + }) + var forElement = tapRecognizer.findControl(targetElement) + if (forElement) { + tapRecognizer.focus(targetElement) + targetElement = forElement + } + } else if (tapRecognizer.needFocus(targetElement)) { + // 如果元素从touchstart到touchend经历时间过长,那么不应该触发点击事 + // 或者此元素是iframe中的input元素,那么它也无法获点焦点 + if ( + now - startTime > 100 || + (deviceIsIOS && window.top !== window && targetTagName === 'input') + ) { + tapRecognizer.element = 0 + return false + } + + tapRecognizer.focus(targetElement) + deviceIsAndroid && tapRecognizer.sendClick(targetElement, event) + + return false + } + + if (deviceIsIOS) { + //如果它的父容器的滚动条发生改变,那么应该识别为划动或拖动事件,不应该触发点击事件 + var scrollParent = targetElement.tapScrollParent + if ( + scrollParent && + scrollParent.lastScrollTop !== scrollParent.scrollTop + ) { + return true + } + } + //如果这不是一个需要使用原生click的元素,则屏蔽原生事件,避免触发两次click + if (!tapRecognizer.needClick(targetElement)) { + event.preventDefault() + // 触发一次模拟的click + tapRecognizer.sendClick(targetElement, event) + } + }, + touchcancel: function() { + tapRecognizer.startTime = tapRecognizer.element = 0 + } +} + +Recognizer.add('tap', tapRecognizer) + +var pressRecognizer = { + events: ['longtap', 'doubletap'], + cancelPress: function(pointer) { + clearTimeout(pointer.pressingHandler) + pointer.pressingHandler = null + }, + touchstart: function(event) { + Recognizer.start(event, function(pointer, touch) { + pointer.pressingHandler = setTimeout(function() { + if (pointer.status === 'tapping') { + Recognizer.fire(event.target, 'longtap', { + touch: touch, + touchEvent: event + }) + } + pressRecognizer.cancelPress(pointer) + }, 800) + if (event.changedTouches.length !== 1) { + pointer.status = 0 + } + }) + }, + touchmove: function(event) { + Recognizer.move(event, function(pointer) { + if (pointer.distance > 10 && pointer.pressingHandler) { + pressRecognizer.cancelPress(pointer) + if (pointer.status === 'tapping') { + pointer.status = 'panning' + } + } + }) + }, + touchend: function(event) { + Recognizer.end(event, function(pointer, touch) { + pressRecognizer.cancelPress(pointer) + if (pointer.status === 'tapping') { + pointer.lastTime = Date.now() + if ( + pressRecognizer.lastTap && + pointer.lastTime - pressRecognizer.lastTap.lastTime < 300 + ) { + Recognizer.fire(pointer.element, 'doubletap', { + touch: touch, + touchEvent: event + }) + } + + pressRecognizer.lastTap = pointer + } + }) + }, + touchcancel: function(event) { + Recognizer.end(event, function(pointer) { + pressRecognizer.cancelPress(pointer) + }) + } +} +Recognizer.add('press', pressRecognizer) + +var swipeRecognizer = { + events: ['swipe', 'swipeleft', 'swiperight', 'swipeup', 'swipedown'], + getAngle: function(x, y) { + return Math.atan2(y, x) * 180 / Math.PI + }, + getDirection: function(x, y) { + var angle = swipeRecognizer.getAngle(x, y) + if (angle < -45 && angle > -135) { + return 'up' + } else if (angle >= 45 && angle < 315) { + return 'down' + } else if (angle > -45 && angle <= 45) { + return 'right' + } else { + return 'left' + } + }, + touchstart: function(event) { + Recognizer.start(event, noop) + }, + touchmove: function(event) { + Recognizer.move(event, noop) + }, + touchend: function(event) { + if (event.changedTouches.length !== 1) { + return + } + Recognizer.end(event, function(pointer, touch) { + var isflick = + pointer.distance > 30 && pointer.distance / pointer.duration > 0.65 + if (isflick) { + var extra = { + deltaX: pointer.deltaX, + deltaY: pointer.deltaY, + touch: touch, + touchEvent: event, + direction: swipeRecognizer.getDirection( + pointer.deltaX, + pointer.deltaY + ), + isVertical: pointer.isVertical + } + var target = pointer.element + Recognizer.fire(target, 'swipe', extra) + Recognizer.fire(target, 'swipe' + extra.direction, extra) + } + }) + } +} + +swipeRecognizer.touchcancel = swipeRecognizer.touchend +Recognizer.add('swipe', swipeRecognizer)