commit 90408348bcf02bac946c96534d24b62b65833dde Author: 宇天 Date: Wed Aug 12 18:25:03 2020 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8f1f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ + +.vscode +node_modules/ +dist/ + +*.sublime-project +*.sublime-workspace +package-lock.json + + +._* + +.Spotlight-V100 +.Trashes +.DS_Store +.AppleDouble +.LSOverride \ No newline at end of file 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/README.md b/README.md new file mode 100644 index 0000000..265bc17 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +## Anot.js 迷你mvvm框架 +> 基于**司徒正美**的`avalon2.x`版精简修改而来。 \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..3ee87e8 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "@bytedo/anot", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/@bytedo/anot.git" + }, + "version": "2.2.10", + "description": "an elegant efficient express mvvm framework", + "main": "dist/anot.js", + "keywords": ["javascript", "avalon", "mvvm", "virtual dom"], + "scripts": { + "start": "rollup -w -c ./rollup.config.dev.js", + "build": "rollup -c ./rollup.config.prod.js" + }, + "dependencies": {}, + "devDependencies": { + "@bytedo/rollup-plugin-uglify": "^1.0.0", + "iofs": "^1.3.2", + "rollup": "^2.23.1" + }, + "bugs": { + "url": "https://github.com/RubyLouvre/avalon/issues" + } +} diff --git a/rollup.config.dev.js b/rollup.config.dev.js new file mode 100644 index 0000000..f160c52 --- /dev/null +++ b/rollup.config.dev.js @@ -0,0 +1,22 @@ +// const uglify = require('@bytedo/rollup-plugin-uglify') + +module.exports = [ + { + input: 'src/anot.js', + output: { + file: 'dist/anot.js', + format: 'es', + sourcemap: false + }, + plugins: [] + }, + { + input: 'src/anot.touch.js', + output: { + file: 'dist/anot.touch.js', + format: 'es', + sourcemap: false + }, + plugins: [] + } +] diff --git a/rollup.config.prod.js b/rollup.config.prod.js new file mode 100644 index 0000000..9087c71 --- /dev/null +++ b/rollup.config.prod.js @@ -0,0 +1,22 @@ +const uglify = require('@bytedo/rollup-plugin-uglify') + +module.exports = [ + { + input: 'src/anot.js', + output: { + file: 'dist/anot.js', + format: 'es', + sourcemap: false + }, + plugins: [uglify()] + }, + { + input: 'src/anot.touch.js', + output: { + file: 'dist/anot.touch.js', + format: 'es', + sourcemap: false + }, + plugins: [uglify()] + } +] diff --git a/src/anot.js b/src/anot.js new file mode 100644 index 0000000..213dc8c --- /dev/null +++ b/src/anot.js @@ -0,0 +1,20 @@ +import { Anot } from './seed/core' +import './seed/lang.modern' + +import './filters/index' +import './dom/modern' + +import './vtree/fromString' +import './vtree/fromDOM' + +import './vdom/modern' + +import './vmodel/modern' +import './vmodel/proxy' + +import './directives/modern' +import './renders/domRender' + +import './effect/index' + +export default Anot diff --git a/src/anot.touch.js b/src/anot.touch.js new file mode 100644 index 0000000..9b896e3 --- /dev/null +++ b/src/anot.touch.js @@ -0,0 +1,22 @@ +import { Anot } from './seed/core' +import './seed/lang.modern' + +import './filters/index' +import './dom/modern' + +import './vtree/fromString' +import './vtree/fromDOM' + +import './vdom/modern' + +import './vmodel/modern' +import './vmodel/proxy' + +import './directives/modern' +import './renders/domRender' + +import './effect/index' + +import './gesture/tap' + +export default Anot diff --git a/src/directives/attr.modern.js b/src/directives/attr.modern.js new file mode 100644 index 0000000..8900884 --- /dev/null +++ b/src/directives/attr.modern.js @@ -0,0 +1,21 @@ +import { Anot } from '../seed/core' +import { cssDiff } from './css' +import { updateAttrs } from '../dom/attr/modern' + +Anot.directive('attr', { + diff: cssDiff, + update: function(vdom, value) { + var props = vdom.props + for (var i in value) { + if (!!value[i] === false) { + delete props[i] + } else { + props[i] = value[i] + } + } + var dom = vdom.dom + if (dom && dom.nodeType === 1) { + updateAttrs(dom, value) + } + } +}) diff --git a/src/directives/class.hover.active.js b/src/directives/class.hover.active.js new file mode 100644 index 0000000..1b54aa8 --- /dev/null +++ b/src/directives/class.hover.active.js @@ -0,0 +1,121 @@ +//根据VM的属性值或表达式的值切换类名,ms-class='xxx yyy zzz:flag' +//http://www.cnblogs.com/rubylouvre/archive/2012/12/17/2818540.html +import { Anot, directives, getLongID as markID } from '../seed/core' + +function classNames() { + var classes = [] + for (var i = 0; i < arguments.length; i++) { + var arg = arguments[i] + var argType = typeof arg + if (argType === 'string' || argType === 'number' || arg === true) { + classes.push(arg) + } else if (Array.isArray(arg)) { + classes.push(classNames.apply(null, arg)) + } else if (argType === 'object') { + for (var key in arg) { + if (arg.hasOwnProperty(key) && arg[key]) { + classes.push(key) + } + } + } + } + + return classes.join(' ') +} + +Anot.directive('class', { + diff: function(newVal, oldVal) { + var type = this.type + var vdom = this.node + var classEvent = vdom.classEvent || {} + if (type === 'hover') { + //在移出移入时切换类名 + classEvent.mouseenter = activateClass + classEvent.mouseleave = abandonClass + } else if (type === 'active') { + //在获得焦点时切换类名 + classEvent.tabIndex = vdom.props.tabindex || -1 + classEvent.mousedown = activateClass + classEvent.mouseup = abandonClass + classEvent.mouseleave = abandonClass + } + vdom.classEvent = classEvent + + var className = classNames(newVal) + + if (typeof oldVal === void 0 || oldVal !== className) { + this.value = className + + vdom['change-' + type] = className + return true + } + }, + update: function(vdom, value) { + var dom = vdom.dom + if (dom && dom.nodeType == 1) { + var dirType = this.type + var change = 'change-' + dirType + var classEvent = vdom.classEvent + if (classEvent) { + for (var i in classEvent) { + if (i === 'tabIndex') { + dom[i] = classEvent[i] + } else { + Anot.bind(dom, i, classEvent[i]) + } + } + vdom.classEvent = {} + } + var names = ['class', 'hover', 'active'] + names.forEach(function(type) { + if (dirType !== type) return + if (type === 'class') { + dom && setClass(dom, value) + } else { + var oldClass = dom.getAttribute(change) + if (oldClass) { + Anot(dom).removeClass(oldClass) + } + var name = 'change-' + type + dom.setAttribute(name, value) + } + }) + } + } +}) + +directives.active = directives.hover = directives['class'] + +var classMap = { + mouseenter: 'change-hover', + mouseleave: 'change-hover', + mousedown: 'change-active', + mouseup: 'change-active' +} + +function activateClass(e) { + var elem = e.target + Anot(elem).addClass(elem.getAttribute(classMap[e.type]) || '') +} + +function abandonClass(e) { + var elem = e.target + var name = classMap[e.type] + Anot(elem).removeClass(elem.getAttribute(name) || '') + if (name !== 'change-active') { + Anot(elem).removeClass(elem.getAttribute('change-active') || '') + } +} + +function setClass(dom, neo) { + var old = dom.getAttribute('change-class') + if (old !== neo) { + Anot(dom) + .removeClass(old) + .addClass(neo) + dom.setAttribute('change-class', neo) + } +} + +markID(activateClass) +markID(abandonClass) diff --git a/src/directives/controller.js b/src/directives/controller.js new file mode 100644 index 0000000..d9d94c9 --- /dev/null +++ b/src/directives/controller.js @@ -0,0 +1,17 @@ +import { Anot, platform } from '../seed/core' +import { impCb } from './important' +Anot.directive('controller', { + priority: 2, + getScope: function(name, scope) { + var v = Anot.vmodels[name] + if (v) { + v.$render = this + if (scope && scope !== v) { + return platform.fuseFactory(scope, v) + } + return v + } + return scope + }, + update: impCb +}) diff --git a/src/directives/css.js b/src/directives/css.js new file mode 100644 index 0000000..a6f28b9 --- /dev/null +++ b/src/directives/css.js @@ -0,0 +1,126 @@ +import { Anot, platform } from '../seed/core' +var arrayWarn = {} +var cssDir = Anot.directive('css', { + diff: function(newVal, oldVal) { + if (Object(newVal) === newVal) { + newVal = platform.toJson(newVal) //安全的遍历VBscript + if (Array.isArray(newVal)) { + //转换成对象 + var b = {} + newVal.forEach(function(el) { + el && Anot.shadowCopy(b, el) + }) + newVal = b + if (!arrayWarn[this.type]) { + Anot.warn('ms-' + this.type + '指令的值不建议使用数组形式了!') + arrayWarn[this.type] = 1 + } + } + + var hasChange = false + var patch = {} + if (!oldVal) { + //如果一开始为空 + patch = newVal + hasChange = true + } else { + if (this.deep) { + var deep = typeof this.deep === 'number' ? this.deep : 6 + for (let i in newVal) { + //diff差异点 + if (!deepEquals(newVal[i], oldVal[i], 4)) { + this.value = newVal + return true + } + patch[i] = newVal[i] + } + } else { + for (let i in newVal) { + //diff差异点 + if (newVal[i] !== oldVal[i]) { + hasChange = true + } + patch[i] = newVal[i] + } + } + + for (let i in oldVal) { + if (!(i in patch)) { + hasChange = true + patch[i] = '' + } + } + } + if (hasChange) { + this.value = patch + return true + } + } + return false + }, + update: function(vdom, value) { + var dom = vdom.dom + if (dom && dom.nodeType === 1) { + var wrap = Anot(dom) + for (var name in value) { + wrap.css(name, value[name]) + } + } + } +}) + +export var cssDiff = cssDir.diff + +export function getEnumerableKeys(obj) { + const res = [] + for (let key in obj) res.push(key) + return res +} + +export function deepEquals(a, b, level) { + if (level === 0) return a === b + if (a === null && b === null) return true + if (a === undefined && b === undefined) return true + const aIsArray = Array.isArray(a) + if (aIsArray !== Array.isArray(b)) { + return false + } + if (aIsArray) { + return equalArray(a, b, level) + } else if (typeof a === 'object' && typeof b === 'object') { + return equalObject(a, b, level) + } + return a === b +} + +function equalArray(a, b, level) { + if (a.length !== b.length) { + return false + } + for (let i = a.length - 1; i >= 0; i--) { + try { + if (!deepEquals(a[i], b[i], level - 1)) { + return false + } + } catch (noThisPropError) { + return false + } + } + return true +} + +function equalObject(a, b, level) { + if (a === null || b === null) return false + if (getEnumerableKeys(a).length !== getEnumerableKeys(b).length) return false + for (let prop in a) { + if (!(prop in b)) return false + try { + if (!deepEquals(a[prop], b[prop], level - 1)) { + return false + } + } catch (noThisPropError) { + return false + } + } + return true +} diff --git a/src/directives/duplex/modern.js b/src/directives/duplex/modern.js new file mode 100644 index 0000000..5abcb64 --- /dev/null +++ b/src/directives/duplex/modern.js @@ -0,0 +1,23 @@ +import { Anot } from '../../seed/core' +import { + duplexBeforeInit, + duplexInit, + duplexDiff, + duplexBind, + valueHijack, + updateView +} from './share' +import { updateDataEvents } from './updateDataEvents.modern' + +Anot.directive('duplex', { + priority: 2000, + beforeInit: duplexBeforeInit, + init: duplexInit, + diff: duplexDiff, + update: function(vdom, value) { + if (!this.dom) { + duplexBind.call(this, vdom, updateDataEvents) + } + updateView[this.dtype].call(this) + } +}) diff --git a/src/directives/duplex/option.js b/src/directives/duplex/option.js new file mode 100644 index 0000000..5bac14a --- /dev/null +++ b/src/directives/duplex/option.js @@ -0,0 +1,55 @@ +export function lookupOption(vdom, values) { + vdom.children && vdom.children.forEach(function (el) { + if (el.nodeName === 'option') { + setOption(el, values) + } else { + lookupOption(el, values) + } + }) +} + +function setOption(vdom, values) { + var props = vdom.props + if (!('disabled' in props)) { + var value = getOptionValue(vdom, props) + value = String(value || '').trim() + if(typeof values === 'string'){ + props.selected = value === values + }else{ + props.selected = values.indexOf(value) !== -1; + } + + if (vdom.dom) { + vdom.dom.selected = props.selected + var v = vdom.dom.selected //必须加上这个,防止移出节点selected失效 + } + + } +} + +function getOptionValue(vdom, props) { + if (props && 'value' in props) { + return props.value+ '' + } + var arr = [] + vdom.children.forEach(function (el) { + if (el.nodeName === '#text') { + arr.push(el.nodeValue) + } else if (el.nodeName === '#document-fragment') { + arr.push(getOptionValue(el)) + } + }) + return arr.join('') +} + +export function getSelectedValue(vdom, arr) { + vdom.children.forEach(function (el) { + if (el.nodeName === 'option') { + if(el.props.selected === true) + arr.push(getOptionValue(el, el.props)) + } else if (el.children) { + getSelectedValue(el,arr) + } + }) + return arr +} \ No newline at end of file diff --git a/src/directives/duplex/share.js b/src/directives/duplex/share.js new file mode 100644 index 0000000..7330de3 --- /dev/null +++ b/src/directives/duplex/share.js @@ -0,0 +1,214 @@ +import { Anot, createFragment } from '../../seed/core' +import { rcheckedType } from '../../dom/rcheckedType' +import { lookupOption } from './option' +import { addScope, makeHandle } from '../../parser/index' +import { fromString } from '../../vtree/fromString' +import { updateModel } from './updateDataHandle' + +var rchangeFilter = /\|\s*change\b/ +var rdebounceFilter = /\|\s*debounce(?:\(([^)]+)\))?/ +export function duplexBeforeInit() { + var expr = this.expr + if (rchangeFilter.test(expr)) { + this.isChanged = true + expr = expr.replace(rchangeFilter, '') + } + var match = expr.match(rdebounceFilter) + if (match) { + expr = expr.replace(rdebounceFilter, '') + if (!this.isChanged) { + this.debounceTime = parseInt(match[1], 10) || 300 + } + } + this.expr = expr +} +export function duplexInit() { + var expr = this.expr + var node = this.node + var etype = node.props.type + this.parseValue = parseValue + //处理数据转换器 + var parsers = this.param, + dtype + var isChecked = false + parsers = parsers + ? parsers.split('-').map(function(a) { + if (a === 'checked') { + isChecked = true + } + return a + }) + : [] + node.duplex = this + if (rcheckedType.test(etype) && isChecked) { + //如果是radio, checkbox,判定用户使用了checked格式函数没有 + parsers = [] + dtype = 'radio' + this.isChecked = isChecked + } + this.parsers = parsers + if (!/input|textarea|select/.test(node.nodeName)) { + if ('contenteditable' in node.props) { + dtype = 'contenteditable' + } + } else if (!dtype) { + dtype = + node.nodeName === 'select' + ? 'select' + : etype === 'checkbox' + ? 'checkbox' + : etype === 'radio' + ? 'radio' + : 'input' + } + this.dtype = dtype + + //判定是否使用了 change debounce 过滤器 + // this.isChecked = /boolean/.test(parsers) + if (dtype !== 'input' && dtype !== 'contenteditable') { + delete this.isChanged + delete this.debounceTime + } else if (!this.isChecked) { + this.isString = true + } + + var cb = node.props['data-duplex-changed'] + if (cb) { + var arr = addScope(cb, 'xx') + var body = makeHandle(arr[0]) + this.userCb = new Function( + '$event', + 'var __vmodel__ = this\nreturn ' + body + ) + } +} +export function duplexDiff(newVal, oldVal) { + if (Array.isArray(newVal)) { + if (newVal + '' !== this.compareVal) { + this.compareVal = newVal + '' + return true + } + } else { + newVal = this.parseValue(newVal) + if (!this.isChecked) { + this.value = newVal += '' + } + if (newVal !== this.compareVal) { + this.compareVal = newVal + return true + } + } +} + +export function duplexBind(vdom, addEvent) { + var dom = vdom.dom + this.dom = dom + this.vdom = vdom + this.duplexCb = updateModel + dom._ms_duplex_ = this + //绑定事件 + addEvent(dom, this) +} + +export var valueHijack = true +try { + //#272 IE9-IE11, firefox + var setters = {} + var aproto = HTMLInputElement.prototype + var bproto = HTMLTextAreaElement.prototype + var newSetter = function(value) { + // jshint ignore:line + setters[this.tagName].call(this, value) + var data = this._ms_duplex_ + if (!this.caret && data && data.isString) { + data.duplexCb.call(this, { type: 'setter' }) + } + } + 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 + }) + valueHijack = false +} catch (e) { + //在chrome 43中 ms-duplex终于不需要使用定时器实现双向绑定了 + // http://updates.html5rocks.com/2015/04/DOM-attributes-now-on-the-prototype + // https://docs.google.com/document/d/1jwA8mtClwxI-QJuHT7872Z0pxpZz8PBkf2bGAbsUtqs/edit?pli=1 +} + +function parseValue(val) { + for (var i = 0, k; (k = this.parsers[i++]); ) { + var fn = Anot.parsers[k] + if (fn) { + val = fn.call(this, val) + } + } + return val +} + +export var updateView = { + input: function() { + //处理单个value值处理 + var vdom = this.node + var value = this.value + '' + vdom.dom.value = vdom.props.value = value + }, + updateChecked: function(vdom, checked) { + if (vdom.dom) { + vdom.dom.defaultChecked = vdom.dom.checked = checked + } + }, + radio: function() { + //处理单个checked属性 + var node = this.node + var nodeValue = node.props.value + var checked + if (this.isChecked) { + checked = !!this.value + } else { + checked = this.value + '' === nodeValue + } + node.props.checked = checked + updateView.updateChecked(node, checked) + }, + checkbox: function() { + //处理多个checked属性 + var node = this.node + var props = node.props + var value = props.value + '' + var values = [].concat(this.value) + var checked = values.some(function(el) { + return el + '' === value + }) + + props.defaultChecked = props.checked = checked + updateView.updateChecked(node, checked) + }, + select: function() { + //处理子级的selected属性 + var a = Array.isArray(this.value) ? this.value.map(String) : this.value + '' + lookupOption(this.node, a) + }, + contenteditable: function() { + //处理单个innerHTML + + var vnodes = fromString(this.value) + var fragment = createFragment() + for (var i = 0, el; (el = vnodes[i++]); ) { + var child = Anot.vdom(el, 'toDOM') + fragment.appendChild(child) + } + Anot.clearHTML(this.dom).appendChild(fragment) + var list = this.node.children + list.length = 0 + Array.prototype.push.apply(list, vnodes) + + this.duplexCb.call(this.dom) + } +} diff --git a/src/directives/duplex/updateDataActions.js b/src/directives/duplex/updateDataActions.js new file mode 100644 index 0000000..fa1507c --- /dev/null +++ b/src/directives/duplex/updateDataActions.js @@ -0,0 +1,78 @@ +import { Anot } from '../../seed/core' + +export var updateDataActions = { + input: function(prop) { + //处理单个value值处理 + var field = this + prop = prop || 'value' + var dom = field.dom + var rawValue = dom[prop] + var parsedValue = field.parseValue(rawValue) + + //有时候parse后一致,vm不会改变,但input里面的值 + field.value = rawValue + field.setValue(parsedValue) + duplexCb(field) + var pos = field.pos + /* istanbul ignore if */ + if (dom.caret) { + field.setCaret(dom, pos) + } + //vm.aaa = '1234567890' + //处理 {{@aaa}} 这种格式化同步不一致的情况 + }, + radio: function() { + var field = this + if (field.isChecked) { + var val = !field.value + field.setValue(val) + duplexCb(field) + } else { + updateDataActions.input.call(field) + field.value = NaN + } + }, + checkbox: function() { + var field = this + var array = field.value + if (!Array.isArray(array)) { + Anot.warn('ms-duplex应用于checkbox上要对应一个数组') + array = [array] + } + var method = field.dom.checked ? 'ensure' : 'remove' + if (array[method]) { + var val = field.parseValue(field.dom.value) + array[method](val) + duplexCb(field) + } + this.__test__ = array + }, + select: function() { + var field = this + var val = Anot(field.dom).val() //字符串或字符串数组 + if (val + '' !== this.value + '') { + if (Array.isArray(val)) { + //转换布尔数组或其他 + val = val.map(function(v) { + return field.parseValue(v) + }) + } else { + val = field.parseValue(val) + } + field.setValue(val) + duplexCb(field) + } + }, + contenteditable: function() { + updateDataActions.input.call(this, 'innerHTML') + } +} + +function duplexCb(field) { + if (field.userCb) { + field.userCb.call(field.vm, { + type: 'changed', + target: field.dom + }) + } +} diff --git a/src/directives/duplex/updateDataEvents.modern.js b/src/directives/duplex/updateDataEvents.modern.js new file mode 100644 index 0000000..591b556 --- /dev/null +++ b/src/directives/duplex/updateDataEvents.modern.js @@ -0,0 +1,117 @@ +/* + * 通过绑定事件同步vmodel + * 总共有三种方式同步视图 + * 1. 各种事件 input, change, click, propertychange, keydown... + * 2. value属性重写 + * 3. 定时器轮询 + */ +import { + Anot, + getShortID as markID, + window, + document, + msie +} from '../../seed/core' +import { updateModel } from './updateDataHandle' + +export function updateDataEvents(dom, data) { + var events = {} + //添加需要监听的事件 + switch (data.dtype) { + case 'radio': + case 'checkbox': + events.click = updateModel + break + case 'select': + events.change = updateModel + break + case 'contenteditable': + if (data.isChanged) { + events.blur = updateModel + } else { + if (window.webkitURL) { + // http://code.metager.de/source/xref/WebKit/LayoutTests/fast/events/ + // https://bugs.webkit.org/show_bug.cgi?id=110742 + events.webkitEditableContentChanged = updateModel + } else if (window.MutationEvent) { + events.DOMCharacterDataModified = updateModel + } + events.input = updateModel + } + break + case 'input': + if (data.isChanged) { + events.change = updateModel + } else { + events.input = updateModel + + //https://github.com/RubyLouvre/Anot/issues/1368#issuecomment-220503284 + events.compositionstart = openComposition + events.compositionend = closeComposition + if (Anot.msie) { + events.keyup = updateModelKeyDown + } + } + break + } + + if (/password|text/.test(dom.type)) { + events.focus = openCaret //判定是否使用光标修正功能 + events.blur = closeCaret + data.getCaret = getCaret + data.setCaret = setCaret + } + for (var name in events) { + Anot.bind(dom, name, events[name]) + } +} + +/* istanbul ignore next */ +function updateModelKeyDown(e) { + var key = e.keyCode + // ignore + // command modifiers arrows + if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return + updateModel.call(this, e) +} +/* istanbul ignore next */ +function openCaret() { + this.caret = true +} +/* istanbul ignore next */ +function closeCaret() { + this.caret = false +} +function openComposition() { + this.composing = true +} +/* istanbul ignore next */ +function closeComposition(e) { + this.composing = false + var elem = this + setTimeout(function() { + updateModel.call(elem, e) + }, 0) +} + +markID(openCaret) +markID(closeCaret) +markID(openComposition) +markID(closeComposition) +markID(updateModelKeyDown) +markID(updateModel) + +/* istanbul ignore next */ +function getCaret(field) { + var start = NaN + if (field.setSelectionRange) { + start = field.selectionStart + } + return start +} +/* istanbul ignore next */ +function setCaret(field, pos) { + if (!field.value || field.readOnly) return + field.selectionStart = pos + field.selectionEnd = pos +} diff --git a/src/directives/duplex/updateDataHandle.js b/src/directives/duplex/updateDataHandle.js new file mode 100644 index 0000000..341c748 --- /dev/null +++ b/src/directives/duplex/updateDataHandle.js @@ -0,0 +1,45 @@ +import { updateDataActions } from './updateDataActions' + +export function updateDataHandle(event) { + var elem = this + var field = elem._ms_duplex_ + if (elem.composing) { + //防止onpropertychange引发爆栈 + return + } + if (elem.value === field.value) { + return + } + /* istanbul ignore if*/ + if (elem.caret) { + try { + var pos = field.getCaret(elem) + field.pos = pos + } catch (e) {} + } + /* istanbul ignore if*/ + if (field.debounceTime > 4) { + var timestamp = new Date() + var left = timestamp - field.time || 0 + field.time = timestamp + /* istanbul ignore if*/ + if (left >= field.debounceTime) { + updateDataActions[field.dtype].call(field) + /* istanbul ignore else*/ + } else { + clearTimeout(field.debounceID) + field.debounceID = setTimeout(function() { + updateDataActions[field.dtype].call(field) + }, left) + } + } else if (field.isChanged) { + setTimeout(function() { + //https://github.com/RubyLouvre/Anot/issues/1908 + updateDataActions[field.dtype].call(field) + }, 4) + } else { + updateDataActions[field.dtype].call(field) + } +} + +export { updateDataHandle as updateModel } diff --git a/src/directives/expr.js b/src/directives/expr.js new file mode 100644 index 0000000..8af7308 --- /dev/null +++ b/src/directives/expr.js @@ -0,0 +1,10 @@ +import { Anot } from '../seed/core' + +Anot.directive('expr', { + update: function(vdom, value) { + value = value == null || value === '' ? '\u200b' : value + vdom.nodeValue = value + //https://github.com/RubyLouvre/Anot/issues/1834 + if (vdom.dom) vdom.dom.data = value + } +}) diff --git a/src/directives/for.js b/src/directives/for.js new file mode 100644 index 0000000..c6176c9 --- /dev/null +++ b/src/directives/for.js @@ -0,0 +1,347 @@ +import { Anot, createFragment, platform, isObject, ap } from '../seed/core' + +import { VFragment } from '../vdom/VFragment' +import { $$skipArray } from '../vmodel/reserved' + +import { addScope, makeHandle } from '../parser/index' +import { updateView } from './duplex/share' + +var rforAs = /\s+as\s+([$\w]+)/ +var rident = /^[$a-zA-Z_][$a-zA-Z0-9_]*$/ +var rinvalid = /^(null|undefined|NaN|window|this|\$index|\$id)$/ +var rargs = /[$\w_]+/g +Anot.directive('for', { + delay: true, + priority: 3, + beforeInit: function() { + var str = this.expr, + asName + str = str.replace(rforAs, function(a, b) { + /* istanbul ignore if */ + if (!rident.test(b) || rinvalid.test(b)) { + Anot.error( + 'alias ' + + b + + ' is invalid --- must be a valid JS identifier which is not a reserved name.' + ) + } else { + asName = b + } + return '' + }) + + var arr = str.split(' in ') + var kv = arr[0].match(rargs) + if (kv.length === 1) { + //确保Anot._each的回调有三个参数 + kv.unshift('$key') + } + this.expr = arr[1] + this.keyName = kv[0] + this.valName = kv[1] + this.signature = Anot.makeHashCode('for') + if (asName) { + this.asName = asName + } + + delete this.param + }, + init: function() { + var cb = this.userCb + if (typeof cb === 'string' && cb) { + var arr = addScope(cb, 'for') + var body = makeHandle(arr[0]) + this.userCb = new Function( + '$event', + 'var __vmodel__ = this\nreturn ' + body + ) + } + this.node.forDir = this //暴露给component/index.js中的resetParentChildren方法使用 + this.fragment = [ + '
', + this.fragment, + '
' + ].join('') + this.cache = {} + }, + diff: function(newVal, oldVal) { + /* istanbul ignore if */ + if (this.updating) { + return + } + this.updating = true + var traceIds = createFragments(this, newVal) + + if (this.oldTrackIds === void 0) return true + + if (this.oldTrackIds !== traceIds) { + this.oldTrackIds = traceIds + return true + } + }, + update: function() { + if (!this.preFragments) { + this.fragments = this.fragments || [] + mountList(this) + } else { + diffList(this) + updateList(this) + } + + if (this.userCb) { + var me = this + setTimeout(function() { + me.userCb.call(me.vm, { + type: 'rendered', + target: me.begin.dom, + signature: me.signature + }) + }, 0) + } + delete this.updating + }, + beforeDispose: function() { + this.fragments.forEach(function(el) { + el.dispose() + }) + } +}) + +function getTraceKey(item) { + var type = typeof item + return item && type === 'object' ? item.$hashcode : type + ':' + item +} + +//创建一组fragment的虚拟DOM +function createFragments(instance, obj) { + if (isObject(obj)) { + var array = Array.isArray(obj) + var ids = [] + var fragments = [], + i = 0 + + instance.isArray = array + if (instance.fragments) { + instance.preFragments = instance.fragments + Anot.each(obj, function(key, value) { + var k = array ? getTraceKey(value) : key + + fragments.push({ + key: k, + val: value, + index: i++ + }) + ids.push(k) + }) + instance.fragments = fragments + } else { + Anot.each(obj, function(key, value) { + if (!(key in $$skipArray)) { + var k = array ? getTraceKey(value) : key + fragments.push(new VFragment([], k, value, i++)) + ids.push(k) + } + }) + instance.fragments = fragments + } + return ids.join(';;') + } else { + return NaN + } +} + +function mountList(instance) { + var args = instance.fragments.map(function(fragment, index) { + FragmentDecorator(fragment, instance, index) + saveInCache(instance.cache, fragment) + return fragment + }) + var list = instance.parentChildren + var i = list.indexOf(instance.begin) + list.splice.apply(list, [i + 1, 0].concat(args)) +} + +function diffList(instance) { + var cache = instance.cache + var newCache = {} + var fuzzy = [] + var list = instance.preFragments + + list.forEach(function(el) { + el._dispose = true + }) + + instance.fragments.forEach(function(c, index) { + var fragment = isInCache(cache, c.key) + //取出之前的文档碎片 + if (fragment) { + delete fragment._dispose + fragment.oldIndex = fragment.index + fragment.index = index // 相当于 c.index + + resetVM(fragment.vm, instance.keyName) + fragment.vm[instance.valName] = c.val + fragment.vm[instance.keyName] = instance.isArray ? index : fragment.key + saveInCache(newCache, fragment) + } else { + //如果找不到就进行模糊搜索 + fuzzy.push(c) + } + }) + fuzzy.forEach(function(c) { + var fragment = fuzzyMatchCache(cache, c.key) + if (fragment) { + //重复利用 + fragment.oldIndex = fragment.index + fragment.key = c.key + var val = (fragment.val = c.val) + var index = (fragment.index = c.index) + + fragment.vm[instance.valName] = val + fragment.vm[instance.keyName] = instance.isArray ? index : fragment.key + delete fragment._dispose + } else { + c = new VFragment([], c.key, c.val, c.index) + fragment = FragmentDecorator(c, instance, c.index) + list.push(fragment) + } + saveInCache(newCache, fragment) + }) + + instance.fragments = list + list.sort(function(a, b) { + return a.index - b.index + }) + instance.cache = newCache +} + +function updateItemVm(vm, top) { + for (var i in top) { + if (top.hasOwnProperty(i)) { + vm[i] = top[i] + } + } +} + +function resetVM(vm, a, b) { + if (Anot.config.inProxyMode) { + vm.$accessors[a].value = NaN + } else { + vm.$accessors[a].set(NaN) + } +} + +function updateList(instance) { + var before = instance.begin.dom + var parent = before.parentNode + var list = instance.fragments + var end = instance.end.dom + + for (var i = 0, item; (item = list[i]); i++) { + if (item._dispose) { + list.splice(i, 1) + i-- + item.dispose() + continue + } + if (item.oldIndex !== item.index) { + var f = item.toFragment() + var isEnd = before.nextSibling === null + parent.insertBefore(f, before.nextSibling) + if (isEnd && !parent.contains(end)) { + parent.insertBefore(end, before.nextSibling) + } + } + before = item.split + } + var ch = instance.parentChildren + var startIndex = ch.indexOf(instance.begin) + var endIndex = ch.indexOf(instance.end) + + list.splice.apply(ch, [startIndex + 1, endIndex - startIndex].concat(list)) + if (parent.nodeName === 'SELECT' && parent._ms_duplex_) { + updateView['select'].call(parent._ms_duplex_) + } +} + +/** + * + * @param {type} fragment + * @param {type} this + * @param {type} index + * @returns { key, val, index, oldIndex, this, dom, split, vm} + */ +function FragmentDecorator(fragment, instance, index) { + var data = {} + data[instance.keyName] = instance.isArray ? index : fragment.key + data[instance.valName] = fragment.val + if (instance.asName) { + data[instance.asName] = instance.value + } + var vm = (fragment.vm = platform.itemFactory(instance.vm, { + data: data + })) + if (instance.isArray) { + vm.$watch(instance.valName, function(a) { + if (instance.value && instance.value.set) { + instance.value.set(vm[instance.keyName], a) + } + }) + } else { + vm.$watch(instance.valName, function(a) { + instance.value[fragment.key] = a + }) + } + + fragment.index = index + fragment.innerRender = Anot.scan(instance.fragment, vm, function() { + var oldRoot = this.root + ap.push.apply(fragment.children, oldRoot.children) + this.root = fragment + }) + return fragment +} +// 新位置: 旧位置 +function isInCache(cache, id) { + var c = cache[id] + if (c) { + var arr = c.arr + /* istanbul ignore if*/ + if (arr) { + var r = arr.pop() + if (!arr.length) { + c.arr = 0 + } + return r + } + delete cache[id] + return c + } +} +//[1,1,1] number1 number1_ number1__ +function saveInCache(cache, component) { + var trackId = component.key + if (!cache[trackId]) { + cache[trackId] = component + } else { + var c = cache[trackId] + var arr = c.arr || (c.arr = []) + arr.push(component) + } +} + +var rfuzzy = /^(string|number|boolean)/ +var rkfuzzy = /^_*(string|number|boolean)/ + +function fuzzyMatchCache(cache) { + var key + for (var id in cache) { + var key = id + break + } + if (key) { + return isInCache(cache, key) + } +} diff --git a/src/directives/html.js b/src/directives/html.js new file mode 100644 index 0000000..c114495 --- /dev/null +++ b/src/directives/html.js @@ -0,0 +1,25 @@ +import { Anot } from '../seed/core' + +Anot.directive('html', { + update: function(vdom, value) { + this.beforeDispose() + + this.innerRender = Anot.scan( + '
' + value + '
', + this.vm, + function() { + var oldRoot = this.root + if (vdom.children) vdom.children.length = 0 + vdom.children = oldRoot.children + this.root = vdom + if (vdom.dom) Anot.clearHTML(vdom.dom) + } + ) + }, + beforeDispose: function() { + if (this.innerRender) { + this.innerRender.dispose() + } + }, + delay: true +}) diff --git a/src/directives/if.js b/src/directives/if.js new file mode 100644 index 0000000..8652f19 --- /dev/null +++ b/src/directives/if.js @@ -0,0 +1,60 @@ +import { Anot, createAnchor } from '../seed/core' + +Anot.directive('if', { + delay: true, + priority: 5, + init: function() { + this.placeholder = createAnchor('if') + var props = this.node.props + delete props['ms-if'] + delete props[':if'] + this.fragment = Anot.vdom(this.node, 'toHTML') + }, + diff: function(newVal, oldVal) { + var n = !!newVal + if (oldVal === void 0 || n !== oldVal) { + this.value = n + return true + } + }, + update: function(vdom, value) { + if (this.isShow === void 0 && value) { + continueScan(this, vdom) + return + } + this.isShow = value + var placeholder = this.placeholder + + if (value) { + var p = placeholder.parentNode + continueScan(this, vdom) + p && p.replaceChild(vdom.dom, placeholder) + } else { + //移除DOM + this.beforeDispose() + vdom.nodeValue = 'if' + vdom.nodeName = '#comment' + delete vdom.children + var dom = vdom.dom + var p = dom && dom.parentNode + vdom.dom = placeholder + if (p) { + p.replaceChild(placeholder, dom) + } + } + }, + beforeDispose: function() { + if (this.innerRender) { + this.innerRender.dispose() + } + } +}) + +function continueScan(instance, vdom) { + var innerRender = (instance.innerRender = Anot.scan( + instance.fragment, + instance.vm + )) + Anot.shadowCopy(vdom, innerRender.root) + delete vdom.nodeValue +} diff --git a/src/directives/important.js b/src/directives/important.js new file mode 100644 index 0000000..c1a55b6 --- /dev/null +++ b/src/directives/important.js @@ -0,0 +1,27 @@ +import { Anot } from '../seed/core' + +var impDir = Anot.directive('important', { + priority: 1, + getScope: function(name, scope) { + var v = Anot.vmodels[name] + if (v) return v + throw 'error! no vmodel called ' + name + }, + update: function(node, attrName, $id) { + if (!Anot.inBrowser) return + var dom = Anot.vdom(node, 'toDOM') + if (dom.nodeType === 1) { + dom.removeAttribute(attrName) + Anot(dom).removeClass('ms-controller') + } + var vm = Anot.vmodels[$id] + if (vm) { + vm.$element = dom + vm.$render = this + vm.$fire('onReady') + delete vm.$events.onReady + } + } +}) + +export var impCb = impDir.update diff --git a/src/directives/modern.js b/src/directives/modern.js new file mode 100644 index 0000000..68aaf77 --- /dev/null +++ b/src/directives/modern.js @@ -0,0 +1,20 @@ +import './important' +import './controller' + +import './skip' +import './visible' +import './text' + +import './css' +import './expr' + +import './attr.modern' +import './html' +import './if' +import './on' +import './for' + +import './class.hover.active' +import './duplex/modern' +import './rules' +import './validate' diff --git a/src/directives/on.js b/src/directives/on.js new file mode 100644 index 0000000..86cfc46 --- /dev/null +++ b/src/directives/on.js @@ -0,0 +1,54 @@ +import { Anot, inBrowser } from '../seed/core' + +import { addScope, makeHandle } from '../parser/index' + +Anot.directive('on', { + beforeInit: function() { + this.getter = Anot.noop + }, + init: function() { + var vdom = this.node + var underline = this.name.replace('ms-on-', 'e').replace('-', '_') + var uuid = + underline + + '_' + + this.expr.replace(/\s/g, '').replace(/[^$a-z]/gi, function(e) { + return e.charCodeAt(0) + }) + var fn = Anot.eventListeners[uuid] + if (!fn) { + var arr = addScope(this.expr) + var body = arr[0], + filters = arr[1] + body = makeHandle(body) + + if (filters) { + filters = filters.replace(/__value__/g, '$event') + filters += '\nif($event.$return){\n\treturn;\n}' + } + var ret = [ + 'try{', + '\tvar __vmodel__ = this;', + '\t' + filters, + '\treturn ' + body, + '}catch(e){Anot.log(e, "in on dir")}' + ].filter(function(el) { + return /\S/.test(el) + }) + fn = new Function('$event', ret.join('\n')) + fn.uuid = uuid + Anot.eventListeners[uuid] = fn + } + + var dom = Anot.vdom(vdom, 'toDOM') + dom._ms_context_ = this.vm + + this.eventType = this.param.replace(/\-(\d)$/, '') + delete this.param + Anot(dom).bind(this.eventType, fn) + }, + + beforeDispose: function() { + Anot(this.node.dom).unbind(this.eventType) + } +}) diff --git a/src/directives/rules.js b/src/directives/rules.js new file mode 100644 index 0000000..93fbb06 --- /dev/null +++ b/src/directives/rules.js @@ -0,0 +1,154 @@ +import { Anot, isObject, platform } from '../seed/core' + +Anot.directive('rules', { + diff: function(rules) { + if (isObject(rules)) { + var vdom = this.node + vdom.rules = platform.toJson(rules) + return true + } + } +}) +function isRegExp(value) { + return Anot.type(value) === 'regexp' +} +var rmail = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/i +var rurl = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/ +function isCorrectDate(value) { + if (typeof value === 'string' && value) { + //是字符串但不能是空字符 + var arr = value.split('-') //可以被-切成3份,并且第1个是4个字符 + if (arr.length === 3 && arr[0].length === 4) { + var year = ~~arr[0] //全部转换为非负整数 + var month = ~~arr[1] - 1 + var date = ~~arr[2] + var d = new Date(year, month, date) + return ( + d.getFullYear() === year && + d.getMonth() === month && + d.getDate() === date + ) + } + } + return false +} +//https://github.com/adform/validator.js/blob/master/validator.js +Anot.shadowCopy(Anot.validators, { + pattern: { + message: '必须匹配{{pattern}}这样的格式', + get: function(value, field, next) { + var elem = field.dom + var data = field.data + if (!isRegExp(data.pattern)) { + var h5pattern = elem.getAttribute('pattern') + data.pattern = new RegExp('^(?:' + h5pattern + ')$') + } + next(data.pattern.test(value)) + return value + } + }, + digits: { + message: '必须整数', + get: function(value, field, next) { + //整数 + next(/^\-?\d+$/.test(value)) + return value + } + }, + number: { + message: '必须数字', + get: function(value, field, next) { + //数值 + next(!!value && isFinite(value)) // isFinite('') --> true + return value + } + }, + norequired: { + message: '', + get: function(value, field, next) { + next(true) + return value + } + }, + required: { + message: '必须填写', + get: function(value, field, next) { + next(value !== '') + return value + } + }, + equalto: { + message: '密码输入不一致', + get: function(value, field, next) { + var id = String(field.data.equalto) + var other = Anot(document.getElementById(id)).val() || '' + next(value === other) + return value + } + }, + date: { + message: '日期格式不正确', + get: function(value, field, next) { + var data = field.data + if (isRegExp(data.date)) { + next(data.date.test(value)) + } else { + next(isCorrectDate(value)) + } + return value + } + }, + url: { + message: 'URL格式不正确', + get: function(value, field, next) { + next(rurl.test(value)) + return value + } + }, + email: { + message: 'email格式不正确', + get: function(value, field, next) { + next(rmail.test(value)) + return value + } + }, + minlength: { + message: '最少输入{{minlength}}个字', + get: function(value, field, next) { + var num = parseInt(field.data.minlength, 10) + next(value.length >= num) + return value + } + }, + maxlength: { + message: '最多输入{{maxlength}}个字', + get: function(value, field, next) { + var num = parseInt(field.data.maxlength, 10) + next(value.length <= num) + return value + } + }, + min: { + message: '输入值不能小于{{min}}', + get: function(value, field, next) { + var num = parseInt(field.data.min, 10) + next(parseFloat(value) >= num) + return value + } + }, + max: { + message: '输入值不能大于{{max}}', + get: function(value, field, next) { + var num = parseInt(field.data.max, 10) + next(parseFloat(value) <= num) + return value + } + }, + chs: { + message: '必须是中文字符', + get: function(value, field, next) { + next(/^[\u4e00-\u9fa5]+$/.test(value)) + return value + } + } +}) diff --git a/src/directives/skip.js b/src/directives/skip.js new file mode 100644 index 0000000..3809eb2 --- /dev/null +++ b/src/directives/skip.js @@ -0,0 +1,5 @@ +import { Anot } from '../seed/core' + +Anot.directive('skip', { + delay: true +}) diff --git a/src/directives/text.js b/src/directives/text.js new file mode 100644 index 0000000..4ec13a0 --- /dev/null +++ b/src/directives/text.js @@ -0,0 +1,25 @@ +import { Anot, inBrowser } from '../seed/core' + +Anot.directive('text', { + delay: true, + init: function() { + var node = this.node + if (node.isVoidTag) { + Anot.error('自闭合元素不能使用ms-text') + } + var child = { nodeName: '#text', nodeValue: this.getValue() } + node.children.splice(0, node.children.length, child) + if (inBrowser) { + Anot.clearHTML(node.dom) + node.dom.appendChild(Anot.vdom(child, 'toDOM')) + } + this.node = child + var type = 'expr' + this.type = this.name = type + var directive = Anot.directives[type] + var me = this + this.callback = function(value) { + directive.update.call(me, me.node, value) + } + } +}) diff --git a/src/directives/validate.js b/src/directives/validate.js new file mode 100644 index 0000000..b83cddc --- /dev/null +++ b/src/directives/validate.js @@ -0,0 +1,263 @@ +import { Anot, isObject, platform } from '../seed/core' +var valiDir = Anot.directive('validate', { + diff: function(validator) { + var vdom = this.node + if (vdom.validator) { + return + } + if (isObject(validator)) { + //注意,这个Form标签的虚拟DOM有两个验证对象 + //一个是vmValidator,它是用户VM上的那个原始子对象,也是一个VM + //一个是validator,它是vmValidator.$model, 这是为了防止IE6-8添加子属性时添加的hack + //也可以称之为safeValidate + vdom.validator = validator + validator = platform.toJson(validator) + validator.vdom = vdom + validator.dom = vdom.dom + + for (var name in valiDir.defaults) { + if (!validator.hasOwnProperty(name)) { + validator[name] = valiDir.defaults[name] + } + } + validator.fields = validator.fields || [] + vdom.vmValidator = validator + return true + } + }, + update: function(vdom) { + var vmValidator = vdom.vmValidator + var validator = vdom.validator + var dom = vdom.dom + dom._ms_validate_ = vmValidator + + collectFeild(vdom.children, vmValidator.fields, vmValidator) + var type = window.netscape ? 'keypress' : 'focusin' + Anot.bind(document, type, findValidator) + //为了方便用户手动执行验证,我们需要为原始vmValidate上添加一个onManual方法 + function onManual() { + var v = this + v && valiDir.validateAll.call(v, v.onValidateAll) + } + + try { + var fn = (vmValidator.onManual = onManual.bind(vmValidator)) + validator.onManual = fn + } catch (e) { + Anot.warn( + '要想使用onManual方法,必须在validate对象预定义一个空的onManual函数' + ) + } + delete vdom.vmValidator + + dom.setAttribute('novalidate', 'novalidate') + + /* istanbul ignore if */ + if (vmValidator.validateAllInSubmit) { + Anot.bind(dom, 'submit', validateAllInSubmitFn) + } + }, + validateAll: function(callback) { + var validator = this + var vdom = this.vdom + var fields = (validator.fields = []) + collectFeild(vdom.children, fields, validator) + var fn = typeof callback === 'function' ? callback : validator.onValidateAll + var promises = validator.fields + .filter(function(field) { + var el = field.dom + return el && !el.disabled && validator.dom.contains(el) + }) + .map(function(field) { + return valiDir.validate(field, true) + }) + var uniq = {} + return Promise.all(promises).then(function(array) { + var reasons = array.concat.apply([], array) + if (validator.deduplicateInValidateAll) { + reasons = reasons.filter(function(reason) { + var el = reason.element + var uuid = el.uniqueID || (el.uniqueID = setTimeout('1')) + if (uniq[uuid]) { + return false + } else { + return (uniq[uuid] = true) + } + }) + } + fn.call(vdom.dom, reasons) //这里只放置未通过验证的组件 + }) + }, + + validate: function(field, isValidateAll, event) { + var promises = [] + var value = field.value + var elem = field.dom + /* istanbul ignore if */ + if (typeof Promise !== 'function') { + //Anot-promise不支持phantomjs + Anot.warn( + '浏览器不支持原生Promise,请下载并 + + + + +
+

+
+
+ + + * + */ diff --git a/src/filters/array.js b/src/filters/array.js new file mode 100644 index 0000000..b587a48 --- /dev/null +++ b/src/filters/array.js @@ -0,0 +1,180 @@ +import { Anot } from '../seed/core' +import { $$skipArray } from '../vmodel/reserved' + +/* +https://github.com/hufyhang/orderBy/blob/master/index.js +*/ + +export function orderBy(array, by, decend) { + var type = Anot.type(array) + if (type !== 'array' && type !== 'object') throw 'orderBy只能处理对象或数组' + var criteria = + typeof by == 'string' + ? function(el) { + return el && el[by] + } + : typeof by === 'function' + ? by + : function(el) { + return el + } + var mapping = {} + var temp = [] + __repeat(array, Array.isArray(array), function(key) { + var val = array[key] + var k = criteria(val, key) + if (k in mapping) { + mapping[k].push(key) + } else { + mapping[k] = [key] + } + temp.push(k) + }) + + temp.sort() + if (decend < 0) { + temp.reverse() + } + var _array = type === 'array' + var target = _array ? [] : {} + return recovery(target, temp, function(k) { + var key = mapping[k].shift() + if (_array) { + target.push(array[key]) + } else { + target[key] = array[key] + } + }) +} + +function __repeat(array, isArray, cb) { + if (isArray) { + array.forEach(function(val, index) { + cb(index) + }) + } else if (typeof array.$track === 'string') { + array.$track.replace(/[^☥]+/g, function(k) { + cb(k) + }) + } else { + for (var i in array) { + if (array.hasOwnProperty(i)) { + cb(i) + } + } + } +} +export function filterBy(array, search) { + var type = Anot.type(array) + if (type !== 'array' && type !== 'object') throw 'filterBy只能处理对象或数组' + var args = Anot.slice(arguments, 2) + var stype = Anot.type(search) + if (stype === 'function') { + var criteria = search._orig || search + } else if (stype === 'string' || stype === 'number') { + if (search === '') { + return array + } else { + var reg = new RegExp(Anot.escapeRegExp(search), 'i') + criteria = function(el) { + return reg.test(el) + } + } + } else { + return array + } + var isArray = type === 'array' + var target = isArray ? [] : {} + __repeat(array, isArray, function(key) { + var val = array[key] + if ( + criteria.apply( + { + key: key + }, + [val, key].concat(args) + ) + ) { + if (isArray) { + target.push(val) + } else { + target[key] = val + } + } + }) + return target +} + +export function selectBy(data, array, defaults) { + if (Anot.isObject(data) && !Array.isArray(data)) { + var target = [] + return recovery(target, array, function(name) { + target.push( + data.hasOwnProperty(name) ? data[name] : defaults ? defaults[name] : '' + ) + }) + } else { + return data + } +} + +export function limitBy(input, limit, begin) { + var type = Anot.type(input) + if (type !== 'array' && type !== 'object') throw 'limitBy只能处理对象或数组' + //必须是数值 + if (typeof limit !== 'number') { + return input + } + //不能为NaN + if (limit !== limit) { + return input + } + //将目标转换为数组 + if (type === 'object') { + input = convertArray(input, false) + } + var n = input.length + limit = Math.floor(Math.min(n, limit)) + begin = typeof begin === 'number' ? begin : 0 + if (begin < 0) { + begin = Math.max(0, n + begin) + } + var data = [] + for (var i = begin; i < n; i++) { + if (data.length === limit) { + break + } + data.push(input[i]) + } + var isArray = type === 'array' + if (isArray) { + return data + } + var target = {} + return recovery(target, data, function(el) { + target[el.key] = el.value + }) +} + +function recovery(ret, array, callback) { + for (var i = 0, n = array.length; i < n; i++) { + callback(array[i]) + } + return ret +} + +//Chrome谷歌浏览器中js代码Array.sort排序的bug乱序解决办法 +//http://www.cnblogs.com/yzeng/p/3949182.html +function convertArray(array, isArray) { + var ret = [], + i = 0 + __repeat(array, isArray, function(key) { + ret[i] = { + oldIndex: i, + value: array[key], + key: key + } + i++ + }) + return ret +} diff --git a/src/filters/date.js b/src/filters/date.js new file mode 100644 index 0000000..8163196 --- /dev/null +++ b/src/filters/date.js @@ -0,0 +1,248 @@ +/* + 'yyyy': 4 digit representation of year (e.g. AD 1 => 0001, AD 2010 => 2010) + 'yy': 2 digit representation of year, padded (00-99). (e.g. AD 2001 => 01, AD 2010 => 10) + 'y': 1 digit representation of year, e.g. (AD 1 => 1, AD 199 => 199) + 'MMMM': Month in year (January-December) + 'MMM': Month in year (Jan-Dec) + 'MM': Month in year, padded (01-12) + 'M': Month in year (1-12) + 'dd': Day in month, padded (01-31) + 'd': Day in month (1-31) + 'EEEE': Day in Week,(Sunday-Saturday) + 'EEE': Day in Week, (Sun-Sat) + 'HH': Hour in day, padded (00-23) + 'H': Hour in day (0-23) + 'hh': Hour in am/pm, padded (01-12) + 'h': Hour in am/pm, (1-12) + 'mm': Minute in hour, padded (00-59) + 'm': Minute in hour (0-59) + 'ss': Second in minute, padded (00-59) + 's': Second in minute (0-59) + 'a': am/pm marker + 'Z': 4 digit (+sign) representation of the timezone offset (-1200-+1200) + format string can also be one of the following predefined localizable formats: + + 'medium': equivalent to 'MMM d, y h:mm:ss a' for en_US locale (e.g. Sep 3, 2010 12:05:08 pm) + 'short': equivalent to 'M/d/yy h:mm a' for en_US locale (e.g. 9/3/10 12:05 pm) + 'fullDate': equivalent to 'EEEE, MMMM d,y' for en_US locale (e.g. Friday, September 3, 2010) + 'longDate': equivalent to 'MMMM d, y' for en_US locale (e.g. September 3, 2010 + 'mediumDate': equivalent to 'MMM d, y' for en_US locale (e.g. Sep 3, 2010) + 'shortDate': equivalent to 'M/d/yy' for en_US locale (e.g. 9/3/10) + 'mediumTime': equivalent to 'h:mm:ss a' for en_US locale (e.g. 12:05:08 pm) + 'shortTime': equivalent to 'h:mm a' for en_US locale (e.g. 12:05 pm) + */ + +function toInt(str) { + return parseInt(str, 10) || 0 +} + +function padNumber(num, digits, trim) { + var neg = '' + /* istanbul ignore if*/ + if (num < 0) { + neg = '-' + num = -num + } + num = '' + num + while (num.length < digits) num = '0' + num + if (trim) num = num.substr(num.length - digits) + return neg + num +} + +function dateGetter(name, size, offset, trim) { + return function(date) { + var value = date['get' + name]() + if (offset > 0 || value > -offset) value += offset + if (value === 0 && offset === -12) { + /* istanbul ignore next*/ + value = 12 + } + return padNumber(value, size, trim) + } +} + +function dateStrGetter(name, shortForm) { + return function(date, formats) { + var value = date['get' + name]() + var get = (shortForm ? 'SHORT' + name : name).toUpperCase() + return formats[get][value] + } +} + +function timeZoneGetter(date) { + var zone = -1 * date.getTimezoneOffset() + var paddedZone = zone >= 0 ? '+' : '' + paddedZone += + padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) + + padNumber(Math.abs(zone % 60), 2) + return paddedZone +} +//取得上午下午 +function ampmGetter(date, formats) { + return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1] +} +var DATE_FORMATS = { + yyyy: dateGetter('FullYear', 4), + yy: dateGetter('FullYear', 2, 0, true), + y: dateGetter('FullYear', 1), + MMMM: dateStrGetter('Month'), + MMM: dateStrGetter('Month', true), + MM: dateGetter('Month', 2, 1), + M: dateGetter('Month', 1, 1), + dd: dateGetter('Date', 2), + d: dateGetter('Date', 1), + HH: dateGetter('Hours', 2), + H: dateGetter('Hours', 1), + hh: dateGetter('Hours', 2, -12), + h: dateGetter('Hours', 1, -12), + mm: dateGetter('Minutes', 2), + m: dateGetter('Minutes', 1), + ss: dateGetter('Seconds', 2), + s: dateGetter('Seconds', 1), + sss: dateGetter('Milliseconds', 3), + EEEE: dateStrGetter('Day'), + EEE: dateStrGetter('Day', true), + a: ampmGetter, + Z: timeZoneGetter +} +var rdateFormat = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/ +var raspnetjson = /^\/Date\((\d+)\)\/$/ +export function dateFilter(date, format) { + var locate = dateFilter.locate, + text = '', + parts = [], + fn, + match + format = format || 'mediumDate' + format = locate[format] || format + if (typeof date === 'string') { + if (/^\d+$/.test(date)) { + date = toInt(date) + } else if (raspnetjson.test(date)) { + date = +RegExp.$1 + } else { + var trimDate = date.trim() + var dateArray = [0, 0, 0, 0, 0, 0, 0] + var oDate = new Date(0) + //取得年月日 + trimDate = trimDate.replace(/^(\d+)\D(\d+)\D(\d+)/, function(_, a, b, c) { + var array = c.length === 4 ? [c, a, b] : [a, b, c] + dateArray[0] = toInt(array[0]) //年 + dateArray[1] = toInt(array[1]) - 1 //月 + dateArray[2] = toInt(array[2]) //日 + return '' + }) + var dateSetter = oDate.setFullYear + var timeSetter = oDate.setHours + trimDate = trimDate.replace(/[T\s](\d+):(\d+):?(\d+)?\.?(\d)?/, function( + _, + a, + b, + c, + d + ) { + dateArray[3] = toInt(a) //小时 + dateArray[4] = toInt(b) //分钟 + dateArray[5] = toInt(c) //秒 + if (d) { + //毫秒 + dateArray[6] = Math.round(parseFloat('0.' + d) * 1000) + } + return '' + }) + var tzHour = 0 + var tzMin = 0 + trimDate = trimDate.replace(/Z|([+-])(\d\d):?(\d\d)/, function( + z, + symbol, + c, + d + ) { + dateSetter = oDate.setUTCFullYear + timeSetter = oDate.setUTCHours + if (symbol) { + tzHour = toInt(symbol + c) + tzMin = toInt(symbol + d) + } + return '' + }) + + dateArray[3] -= tzHour + dateArray[4] -= tzMin + dateSetter.apply(oDate, dateArray.slice(0, 3)) + timeSetter.apply(oDate, dateArray.slice(3)) + date = oDate + } + } + if (typeof date === 'number') { + date = new Date(date) + } + + while (format) { + match = rdateFormat.exec(format) + /* istanbul ignore else */ + if (match) { + parts = parts.concat(match.slice(1)) + format = parts.pop() + } else { + parts.push(format) + format = null + } + } + parts.forEach(function(value) { + fn = DATE_FORMATS[value] + text += fn + ? fn(date, locate) + : value.replace(/(^'|'$)/g, '').replace(/''/g, "'") + }) + return text +} + +var locate = { + AMPMS: { + 0: '上午', + 1: '下午' + }, + DAY: { + 0: '星期日', + 1: '星期一', + 2: '星期二', + 3: '星期三', + 4: '星期四', + 5: '星期五', + 6: '星期六' + }, + MONTH: { + 0: '1月', + 1: '2月', + 2: '3月', + 3: '4月', + 4: '5月', + 5: '6月', + 6: '7月', + 7: '8月', + 8: '9月', + 9: '10月', + 10: '11月', + 11: '12月' + }, + SHORTDAY: { + '0': '周日', + '1': '周一', + '2': '周二', + '3': '周三', + '4': '周四', + '5': '周五', + '6': '周六' + }, + fullDate: 'y年M月d日EEEE', + longDate: 'y年M月d日', + medium: 'yyyy-M-d H:mm:ss', + mediumDate: 'yyyy-M-d', + mediumTime: 'H:mm:ss', + short: 'yy-M-d ah:mm', + shortDate: 'yy-M-d', + shortTime: 'ah:mm' +} +locate.SHORTMONTH = locate.MONTH +dateFilter.locate = locate diff --git a/src/filters/escape.js b/src/filters/escape.js new file mode 100644 index 0000000..dfd5d9b --- /dev/null +++ b/src/filters/escape.js @@ -0,0 +1,11 @@ +//https://github.com/teppeis/htmlspecialchars +export function escapeFilter(str) { + if (str == null) return '' + + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} diff --git a/src/filters/event.js b/src/filters/event.js new file mode 100644 index 0000000..be9e524 --- /dev/null +++ b/src/filters/event.js @@ -0,0 +1,33 @@ +var eventFilters = { + stop: function(e) { + e.stopPropagation() + return e + }, + prevent: function(e) { + e.preventDefault() + return e + } +} +var keys = { + esc: 27, + tab: 9, + enter: 13, + space: 32, + del: 46, + up: 38, + left: 37, + right: 39, + down: 40 +} +for (var name in keys) { + ;(function(filter, key) { + eventFilters[filter] = function(e) { + if (e.which !== key) { + e.$return = true + } + return e + } + })(name, keys[name]) +} + +export { eventFilters } diff --git a/src/filters/index.js b/src/filters/index.js new file mode 100644 index 0000000..b1b3003 --- /dev/null +++ b/src/filters/index.js @@ -0,0 +1,72 @@ +import { Anot } from '../seed/core' + +import { numberFilter } from './number' +import { sanitizeFilter } from './sanitize' +import { dateFilter } from './date' +import { filterBy, orderBy, selectBy, limitBy } from './array' +import { eventFilters } from './event' +import { escapeFilter } from './escape' +var filters = (Anot.filters = {}) + +Anot.composeFilters = function() { + var args = arguments + return function(value) { + for (var i = 0, arr; (arr = args[i++]); ) { + var name = arr[0] + var filter = Anot.filters[name] + if (typeof filter === 'function') { + arr[0] = value + try { + value = filter.apply(0, arr) + } catch (e) {} + } + } + return value + } +} + +Anot.escapeHtml = escapeFilter + +Anot.mix( + filters, + { + uppercase(str) { + return String(str).toUpperCase() + }, + lowercase(str) { + return String(str).toLowerCase() + }, + truncate(str, length, end) { + //length,新字符串长度,truncation,新字符串的结尾的字段,返回新字符串 + if (!str) { + return '' + } + str = String(str) + if (isNaN(length)) { + length = 30 + } + end = typeof end === 'string' ? end : '...' + return str.length > length + ? str.slice(0, length - end.length) + end /* istanbul ignore else*/ + : str + }, + camelize: Anot.camelize, + date: dateFilter, + escape: escapeFilter, + sanitize: sanitizeFilter, + number: numberFilter, + currency(amount, symbol, fractionSize) { + return ( + (symbol || '\u00a5') + + numberFilter( + amount, + isFinite(fractionSize) ? /* istanbul ignore else*/ fractionSize : 2 + ) + ) + } + }, + { filterBy, orderBy, selectBy, limitBy }, + eventFilters +) + +export { Anot } diff --git a/src/filters/number.js b/src/filters/number.js new file mode 100644 index 0000000..d910d58 --- /dev/null +++ b/src/filters/number.js @@ -0,0 +1,34 @@ +import { Anot } from '../seed/core' +function toFixedFix(n, prec) { + var k = Math.pow(10, prec) + return '' + (Math.round(n * k) / k).toFixed(prec) +} +export function numberFilter(number, decimals, point, thousands) { + //https://github.com/txgruppi/number_format + //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 = typeof thousands === 'string' ? thousands : ',', + dec = point || '.', + s = '' + + // 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) + } + /** //好像没有用 + var s1 = s[1] || '' + + if (s1.length < prec) { + s1 += new Array(prec - s[1].length + 1).join('0') + s[1] = s1 + } + **/ + return s.join(dec) +} diff --git a/src/filters/sanitize.js b/src/filters/sanitize.js new file mode 100644 index 0000000..5d75427 --- /dev/null +++ b/src/filters/sanitize.js @@ -0,0 +1,31 @@ +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 +} + +//https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet +// chrome +// chrome +// IE67chrome +// IE67chrome +// IE67chrome +export function sanitizeFilter(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事件 + }) +} diff --git a/src/gesture/drag.js b/src/gesture/drag.js new file mode 100644 index 0000000..00dee30 --- /dev/null +++ b/src/gesture/drag.js @@ -0,0 +1,44 @@ +import { Recognizer } from './recognizer' + +var dragRecognizer = { + events: ['dragstart', 'drag', 'dragend'], + touchstart: function(event) { + Recognizer.start(event, Anot.noop) + }, + touchmove: function(event) { + Recognizer.move(event, function(pointer, touch) { + var extra = { + deltaX: pointer.deltaX, + deltaY: pointer.deltaY, + touch: touch, + touchEvent: event, + isVertical: pointer.isVertical + } + if (pointer.status === 'tapping' && pointer.distance > 10) { + pointer.status = 'panning' + Recognizer.fire(pointer.element, 'dragstart', extra) + } else if (pointer.status === 'panning') { + Recognizer.fire(pointer.element, 'drag', extra) + } + }) + + event.preventDefault() + }, + touchend: function(event) { + Recognizer.end(event, function(pointer, touch) { + if (pointer.status === 'panning') { + Recognizer.fire(pointer.element, 'dragend', { + deltaX: pointer.deltaX, + deltaY: pointer.deltaY, + touch: touch, + touchEvent: event, + isVertical: pointer.isVertical + }) + } + }) + Recognizer.pointers = {} + } +} +dragRecognizer.touchcancel = dragRecognizer.touchend + +Recognizer.add('drag', dragRecognizer) diff --git a/src/gesture/pinch.js b/src/gesture/pinch.js new file mode 100644 index 0000000..ad42c30 --- /dev/null +++ b/src/gesture/pinch.js @@ -0,0 +1,114 @@ +import { Recognizer } from './recognizer' +//https://github.com/manuelstofer/pinchzoom +var pinchRecognizer = { + events: ['pinchstart', 'pinch', 'pinchin', 'pinchuot', 'pinchend'], + getScale: function(x1, y1, x2, y2, x3, y3, x4, y4) { + return Math.sqrt( + (Math.pow(y4 - y3, 2) + Math.pow(x4 - x3, 2)) / + (Math.pow(y2 - y1, 2) + Math.pow(x2 - x1, 2)) + ) + }, + getCommonAncestor: function(arr) { + var el = arr[0], + el2 = arr[1] + while (el) { + if (el.contains(el2) || el === el2) { + return el + } + el = el.parentNode + } + return null + }, + touchstart: function(event) { + var pointers = Recognizer.pointers + Recognizer.start(event, Anot.noop) + var elements = [] + for (var p in pointers) { + if (pointers[p].startTime) { + elements.push(pointers[p].element) + } else { + delete pointers[p] + } + } + pointers.elements = elements + if (elements.length === 2) { + pinchRecognizer.element = pinchRecognizer.getCommonAncestor(elements) + Recognizer.fire( + pinchRecognizer.getCommonAncestor(elements), + 'pinchstart', + { + scale: 1, + touches: event.touches, + touchEvent: event + } + ) + } + }, + touchmove: function(event) { + if (pinchRecognizer.element && event.touches.length > 1) { + var position = [], + current = [] + for (var i = 0; i < event.touches.length; i++) { + var touch = event.touches[i] + var gesture = Recognizer.pointers[touch.identifier] + position.push([gesture.startTouch.clientX, gesture.startTouch.clientY]) + current.push([touch.clientX, touch.clientY]) + } + + var scale = pinchRecognizer.getScale( + position[0][0], + position[0][1], + position[1][0], + position[1][1], + current[0][0], + current[0][1], + current[1][0], + current[1][1] + ) + pinchRecognizer.scale = scale + Recognizer.fire(pinchRecognizer.element, 'pinch', { + scale: scale, + touches: event.touches, + touchEvent: event + }) + + if (scale > 1) { + Recognizer.fire(pinchRecognizer.element, 'pinchout', { + scale: scale, + touches: event.touches, + touchEvent: event + }) + } else { + Recognizer.fire(pinchRecognizer.element, 'pinchin', { + scale: scale, + touches: event.touches, + touchEvent: event + }) + } + } + event.preventDefault() + }, + touchend: function(event) { + if (pinchRecognizer.element) { + Recognizer.fire(pinchRecognizer.element, 'pinchend', { + scale: pinchRecognizer.scale, + touches: event.touches, + touchEvent: event + }) + pinchRecognizer.element = null + } + Recognizer.end(event, Anot.noop) + } +} + +pinchRecognizer.touchcancel = pinchRecognizer.touchend + +Recognizer.add('pinch', pinchRecognizer) + +/* + * + 在iOS中事件分为三类: + 触摸事件:通过触摸、手势进行触发(例如手指点击、缩放) + 运动事件:通过加速器进行触发(例如手机晃动) + 远程控制事件:通过其他远程设备触发(例如耳机控制按钮) + */ diff --git a/src/gesture/press.js b/src/gesture/press.js new file mode 100644 index 0000000..0ed08ba --- /dev/null +++ b/src/gesture/press.js @@ -0,0 +1,60 @@ +import { Recognizer } from './recognizer' + +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) + }, 500) + 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) diff --git a/src/gesture/readme.md b/src/gesture/readme.md new file mode 100644 index 0000000..bb40667 --- /dev/null +++ b/src/gesture/readme.md @@ -0,0 +1,48 @@ +请使用**webpack**打包,它们不包含在核心库里 + + +这些手势是依赖于reconginzer模块 +用到什么就包含什么,比如你想用tap模块, + +在src建立一个Anot.tap.js + +在export前引入这个模块 +```javascript +import { Anot } from './seed/core' +import './seed/lang.compact' + + +import './filters/index' +import './dom/compact' + +import './vtree/fromString' +import './vtree/fromDOM' + +import './vdom/compact' +import './vmodel/compact' +import './directives/compact' + +import './renders/domRender' + +import './effect/index.js' +import './component/index' + +import './gesture/tap' + +export default Anot +``` + + + +然后模仿buildIE6,建议一个buildTap的打包文件 + +最后`node buildtap` 就行了 + +``` + + +//你的业务代码 + +
xxxx
+ +``` \ No newline at end of file diff --git a/src/gesture/recognizer.js b/src/gesture/recognizer.js new file mode 100644 index 0000000..eef5d98 --- /dev/null +++ b/src/gesture/recognizer.js @@ -0,0 +1,178 @@ +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/i.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() +Anot.gestureEvents = {} +var Recognizer = (Anot.gestureHooks = { + isAndroid: ua.indexOf('android') > 0, + isIOS: iOSversion(), + 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] + var pointer = { + startTouch: mixLocations({}, touch), + startTime: Date.now(), + status: 'tapping', + element: event.target + } + Recognizer.pointers[touch.identifier] = 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.gestureEvents[eventName] = 1 + 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 +} + +export { Recognizer } diff --git a/src/gesture/rotate.js b/src/gesture/rotate.js new file mode 100644 index 0000000..286a776 --- /dev/null +++ b/src/gesture/rotate.js @@ -0,0 +1,86 @@ +import { Recognizer } from './recognizer' + +var rotateRecognizer = { + events: ['rotate', 'rotatestart', 'rotateend'], + getAngle180: function(p1, p2) { + // 角度, 范围在{0-180}, 用来识别旋转角度 + var agl = + Math.atan(((p2.pageY - p1.pageY) * -1) / (p2.pageX - p1.pageX)) * + (180 / Math.PI) + return parseInt(agl < 0 ? agl + 180 : agl, 10) + }, + rotate: function(event, status) { + var finger = rotateRecognizer.finger + var endAngel = rotateRecognizer.getAngle180( + rotateRecognizer.center, + finger.lastTouch + ) + var diff = rotateRecognizer.startAngel - endAngel + var direction = diff > 0 ? 'right' : 'left' + var count = 0 + var __rotation = ~~finger.element.__rotation + while (Math.abs(diff - __rotation) > 90 && count++ < 50) { + if (__rotation < 0) { + diff -= 180 + } else { + diff += 180 + } + } + var rotation = (finger.element.__rotation = __rotation = diff) + rotateRecognizer.endAngel = endAngel + var extra = { + touch: event.changedTouches[0], + touchEvent: event, + rotation: rotation, + direction: direction + } + if (status === 'end') { + Recognizer.fire(finger.element, 'rotateend', extra) + finger.element.__rotation = 0 + } else if (finger.status === 'tapping' && diff) { + finger.status = 'panning' + Recognizer.fire(finger.element, 'rotatestart', extra) + } else { + Recognizer.fire(finger.element, 'rotate', extra) + } + }, + touchstart: function(event) { + var pointers = Recognizer.pointers + Recognizer.start(event, Anot.noop) + var finger + for (var p in pointers) { + if (pointers[p].startTime) { + if (!finger) { + finger = pointers[p] + } else { + //如果超过一个指头就中止旋转 + return + } + } + } + rotateRecognizer.finger = finger + var el = finger.element + var docOff = Anot(el).offset() + rotateRecognizer.center = { + //求得元素的中心 + pageX: docOff.left + el.offsetWidth / 2, + pageY: docOff.top + el.offsetHeight / 2 + } + rotateRecognizer.startAngel = rotateRecognizer.getAngle180( + rotateRecognizer.center, + finger.startTouch + ) + }, + touchmove: function(event) { + Recognizer.move(event, Anot.noop) + rotateRecognizer.rotate(event) + }, + touchend: function(event) { + rotateRecognizer.rotate(event, 'end') + Recognizer.end(event, Anot.noop) + } +} + +rotateRecognizer.touchcancel = rotateRecognizer.touchend + +Recognizer.add('rotate', rotateRecognizer) diff --git a/src/gesture/swipe.js b/src/gesture/swipe.js new file mode 100644 index 0000000..09a839e --- /dev/null +++ b/src/gesture/swipe.js @@ -0,0 +1,48 @@ +import { Recognizer } from './recognizer' + +var swipeRecognizer = { + events: ['swipe', 'swipeleft', 'swiperight', 'swipeup', 'swipedown'], + getAngle: function(x, y) { + return (Math.atan2(y, x) * 180) / Math.PI + }, + getDirection: function(x, y) { + if (Math.abs(x) >= Math.abs(y)) { + return x < 0 ? 'left' : 'right' + } + return y < 0 ? 'up' : 'down' + }, + touchstart: function(event) { + Recognizer.start(event, Anot.noop) + }, + touchmove: function(event) { + Recognizer.move(event, Anot.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) diff --git a/src/gesture/tap.js b/src/gesture/tap.js new file mode 100644 index 0000000..4df9a98 --- /dev/null +++ b/src/gesture/tap.js @@ -0,0 +1,297 @@ +import { Recognizer } from './recognizer' +var root = Anot.root +var supportPointer = !!navigator.pointerEnabled || !!navigator.msPointerEnabled +// 支持pointer的设备可用样式来取消click事件的300毫秒延迟 +if (supportPointer) { + root.style.msTouchAction = root.style.touchAction = 'none' +} +var tapRecognizer = { + events: ['tap'], + touchBoundary: 10, + tapDelay: 90, + 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 ((Recognizer.isIOS && 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 ( + Recognizer.isIOS && + 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 Recognizer.isAndroid && + 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 (Recognizer.isIOS) { + // 判断是否是点击文字,进行选择等操作,如果是,不需要模拟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 + + var 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) + Recognizer.isAndroid && tapRecognizer.sendClick(targetElement, event) + + return false + } + + if (Recognizer.isIOS) { + //如果它的父容器的滚动条发生改变,那么应该识别为划动或拖动事件,不应该触发点击事件 + 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) diff --git a/src/parser/attributes.js b/src/parser/attributes.js new file mode 100644 index 0000000..e0e2544 --- /dev/null +++ b/src/parser/attributes.js @@ -0,0 +1,71 @@ +import { Anot, directives } from '../seed/core' +export var eventMap = Anot.oneObject( + 'animationend,blur,change,input,' + + 'click,dblclick,focus,keydown,keypress,keyup,mousedown,mouseenter,' + + 'mouseleave,mousemove,mouseout,mouseover,mouseup,scan,scroll,submit', + 'on' +) +export function parseAttributes(dirs, tuple) { + var node = tuple[0], + uniq = {}, + bindings = [] + var hasIf = false + for (var name in dirs) { + var value = dirs[name] + var arr = name.split('-') + // ms-click + if (name in node.props) { + var attrName = name + } else { + attrName = ':' + name.slice(3) + } + if (eventMap[arr[1]]) { + arr.splice(1, 0, 'on') + } + //ms-on-click + if (arr[1] === 'on') { + arr[3] = parseFloat(arr[3]) || 0 + } + + var type = arr[1] + if (type === 'controller' || type === 'important') continue + if (directives[type]) { + var binding = { + type: type, + param: arr[2], + attrName: attrName, + name: arr.join('-'), + expr: value, + priority: directives[type].priority || type.charCodeAt(0) * 100 + } + if (type === 'if') { + hasIf = true + } + if (type === 'on') { + binding.priority += arr[3] + } + if (!uniq[binding.name]) { + uniq[binding.name] = value + bindings.push(binding) + if (type === 'for') { + return [Anot.mix(binding, tuple[3])] + } + } + } + } + bindings.sort(byPriority) + + if (hasIf) { + var ret = [] + for (var i = 0, el; (el = bindings[i++]); ) { + ret.push(el) + if (el.type === 'if') { + return ret + } + } + } + return bindings +} +export function byPriority(a, b) { + return a.priority - b.priority +} diff --git a/src/parser/index.js b/src/parser/index.js new file mode 100644 index 0000000..779c099 --- /dev/null +++ b/src/parser/index.js @@ -0,0 +1,154 @@ +import { Anot, msie, Cache } from '../seed/core' +import { clearString, stringPool, fill, rfill, dig } from '../vtree/clearString' + +var keyMap = Anot.oneObject( + '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' +) + +export var skipMap = Anot.mix( + { + Math: 1, + Date: 1, + $event: 1, + window: 1, + __vmodel__: 1, + Anot: 1 + }, + keyMap +) + +var rvmKey = /(^|[^\w\u00c0-\uFFFF_])(@|##)(?=[$\w])/g +var ruselessSp = /\s*(\.|\|)\s*/g +var rshortCircuit = /\|\|/g +var brackets = /\(([^)]*)\)/ +var rpipeline = /\|(?=\?\?)/ +var rregexp = /(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\\\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})]))/g +var robjectProp = /\.[\w\.\$]+/g //对象的属性 el.xxx 中的xxx +var robjectKey = /(\{|\,)\s*([\$\w]+)\s*:/g //对象的键名与冒号 {xxx:1,yyy: 2}中的xxx, yyy +var rfilterName = /\|(\w+)/g +var rlocalVar = /[$a-zA-Z_][$a-zA-Z0-9_]*/g + +var exprCache = new Cache(300) + +function addScopeForLocal(str) { + return str.replace(robjectProp, dig).replace(rlocalVar, function(el) { + if (!skipMap[el]) { + return '__vmodel__.' + el + } + return el + }) +} + +export function addScope(expr, type) { + var cacheKey = expr + ':' + type + var cache = exprCache.get(cacheKey) + if (cache) { + return cache.slice(0) + } + + stringPool.map = {} + //https://github.com/RubyLouvre/Anot/issues/1849 + var input = expr.replace(rregexp, function(a, b) { + return b + dig(a.slice(b.length)) + }) //移除所有正则 + input = clearString(input) //移除所有字符串 + input = input + .replace(rshortCircuit, dig) //移除所有短路运算符 + .replace(ruselessSp, '$1') //移除.|两端空白 + .replace(robjectKey, function(_, a, b) { + //移除所有键名 + return a + dig(b) + ':' //比如 ms-widget="[{is:'ms-address-wrap', $id:'address'}]"这样极端的情况 + }) + .replace(rvmKey, '$1__vmodel__.') //转换@与##为__vmodel__ + .replace(rfilterName, function(a, b) { + //移除所有过滤器的名字 + return '|' + dig(b) + }) + input = addScopeForLocal(input) //在本地变量前添加__vmodel__ + + var filters = input.split(rpipeline) //根据管道符切割表达式 + var body = filters + .shift() + .replace(rfill, fill) + .trim() + if (/\?\?\d/.test(body)) { + body = body.replace(rfill, fill) + } + if (filters.length) { + filters = filters.map(function(filter) { + var bracketArgs = '' + filter = filter.replace(brackets, function(a, b) { + if (/\S/.test(b)) { + bracketArgs += ',' + b //还原字符串,正则,短路运算符 + } + return '' + }) + var arg = '[' + Anot.quote(filter.trim()) + bracketArgs + ']' + return arg + }) + filters = 'Anot.composeFilters(' + filters + ')(__value__)' + filters = filters.replace(rfill, fill) + } else { + filters = '' + } + return exprCache.put(cacheKey, [body, filters]) +} +var rhandleName = /^__vmodel__\.[$\w\.]+$/ +var rfixIE678 = /__vmodel__\.([^(]+)\(([^)]*)\)/ +export function makeHandle(body) { + if (rhandleName.test(body)) { + body = body + '($event)' + } + /* istanbul ignore if */ + if (msie < 9) { + body = body.replace(rfixIE678, function(a, b, c) { + return ( + '__vmodel__.' + + b + + '.call(__vmodel__' + + (/\S/.test(c) ? ',' + c : '') + + ')' + ) + }) + } + return body +} +export function createGetter(expr, type) { + var arr = addScope(expr, type), + body + if (!arr[1]) { + body = arr[0] + } else { + body = arr[1].replace(/__value__\)$/, arr[0] + ')') + } + try { + return new Function('__vmodel__', 'return ' + body + ';') + /* istanbul ignore next */ + } catch (e) { + Anot.log('parse getter: [', expr, body, ']error') + return Anot.noop + } +} + +/** + * 生成表达式设值函数 + * @param {String} expr + */ +export function createSetter(expr, type) { + var arr = addScope(expr, type) + var body = + 'try{ ' + arr[0] + ' = __value__}catch(e){Anot.log(e, "in on dir")}' + try { + return new Function('__vmodel__', '__value__', body + ';') + /* istanbul ignore next */ + } catch (e) { + Anot.log('parse setter: ', expr, ' error') + return Anot.noop + } +} diff --git a/src/parser/interpolate.js b/src/parser/interpolate.js new file mode 100644 index 0000000..74e0a44 --- /dev/null +++ b/src/parser/interpolate.js @@ -0,0 +1,44 @@ +import { Anot, config } from '../seed/core' +import { addScope } from './index' +var rimprovePriority = /[+-\?]/ +var rinnerValue = /__value__\)$/ +export function parseInterpolate(dir) { + var rlineSp = /\n\r?/g + var str = dir.nodeValue.trim().replace(rlineSp, '') + var tokens = [] + do { + //aaa{{@bbb}}ccc + var index = str.indexOf(config.openTag) + index = index === -1 ? str.length : index + var value = str.slice(0, index) + if (/\S/.test(value)) { + tokens.push(Anot.quote(Anot._decode(value))) + } + str = str.slice(index + config.openTag.length) + if (str) { + index = str.indexOf(config.closeTag) + var value = str.slice(0, index) + var expr = Anot.unescapeHTML(value) + if (/\|\s*\w/.test(expr)) { + //如果存在过滤器,优化干掉 + var arr = addScope(expr, 'expr') + if (arr[1]) { + expr = arr[1].replace(rinnerValue, arr[0] + ')') + } + } + if (rimprovePriority) { + expr = '(' + expr + ')' + } + tokens.push(expr) + + str = str.slice(index + config.closeTag.length) + } + } while (str.length) + return [ + { + expr: tokens.join('+'), + name: 'expr', + type: 'expr' + } + ] +} diff --git a/src/renders/Directive.js b/src/renders/Directive.js new file mode 100644 index 0000000..75654d0 --- /dev/null +++ b/src/renders/Directive.js @@ -0,0 +1,43 @@ +import { Anot, inBrowser } from '../seed/core' + +import { Action, protectedMenbers } from '../vmodel/Action' + +/** + * 一个directive装饰器 + * @returns {directive} + */ +// DirectiveDecorator(scope, binding, vdom, this) +// Decorator(vm, options, callback) +export function Directive(vm, binding, vdom, render) { + var type = binding.type + var decorator = Anot.directives[type] + if (inBrowser) { + var dom = Anot.vdom(vdom, 'toDOM') + if (dom.nodeType === 1) { + dom.removeAttribute(binding.attrName) + } + vdom.dom = dom + } + var callback = decorator.update + ? function(value) { + if (!render.mount && /css|visible|duplex/.test(type)) { + render.callbacks.push(function() { + decorator.update.call(directive, directive.node, value) + }) + } else { + decorator.update.call(directive, directive.node, value) + } + } + : Anot.noop + for (var key in decorator) { + binding[key] = decorator[key] + } + binding.node = vdom + var directive = new Action(vm, binding, callback) + if (directive.init) { + //这里可能会重写node, callback, type, name + directive.init() + } + directive.update() + return directive +} diff --git a/src/renders/domRender.js b/src/renders/domRender.js new file mode 100644 index 0000000..dddff76 --- /dev/null +++ b/src/renders/domRender.js @@ -0,0 +1,407 @@ +import { + Anot, + config, + inBrowser, + delayCompileNodes, + directives +} from '../seed/core' +import { fromDOM } from '../vtree/fromDOM' +import { fromString } from '../vtree/fromString' + +import { VFragment } from '../vdom/VFragment' +import { Directive } from './Directive' + +import { orphanTag } from '../vtree/orphanTag' +import { parseAttributes, eventMap } from '../parser/attributes' +import { parseInterpolate } from '../parser/interpolate' + +import { startWith, groupTree, dumpTree, getRange } from './share' + +/** + * 生成一个渲染器,并作为它第一个遇到的ms-controller对应的VM的$render属性 + * @param {String|DOM} node + * @param {ViewModel|Undefined} vm + * @param {Function|Undefined} beforeReady + * @returns {Render} + */ +Anot.scan = function(node, vm, beforeReady) { + return new Render(node, vm, beforeReady || Anot.noop) +} + +/** + * Anot.scan 的内部实现 + */ +function Render(node, vm, beforeReady) { + this.root = node //如果传入的字符串,确保只有一个标签作为根节点 + this.vm = vm + this.beforeReady = beforeReady + this.bindings = [] //收集待加工的绑定属性 + this.callbacks = [] + this.directives = [] + this.init() +} + +Render.prototype = { + /** + * 开始扫描指定区域 + * 收集绑定属性 + * 生成指令并建立与VM的关联 + */ + init() { + var vnodes + if (this.root && this.root.nodeType > 0) { + vnodes = fromDOM(this.root) //转换虚拟DOM + //将扫描区域的每一个节点与其父节点分离,更少指令对DOM操作时,对首屏输出造成的频繁重绘 + dumpTree(this.root) + } else if (typeof this.root === 'string') { + vnodes = fromString(this.root) //转换虚拟DOM + } else { + return Anot.warn('Anot.scan first argument must element or HTML string') + } + + this.root = vnodes[0] + this.vnodes = vnodes + this.scanChildren(vnodes, this.vm, true) + }, + + scanChildren(children, scope, isRoot) { + for (var i = 0; i < children.length; i++) { + var vdom = children[i] + switch (vdom.nodeName) { + case '#text': + scope && this.scanText(vdom, scope) + break + case '#comment': + scope && this.scanComment(vdom, scope, children) + break + case '#document-fragment': + this.scanChildren(vdom.children, scope, false) + break + default: + this.scanTag(vdom, scope, children, false) + break + } + } + if (isRoot) { + this.complete() + } + }, + + /** + * 从文本节点获取指令 + * @param {type} vdom + * @param {type} scope + * @returns {undefined} + */ + scanText(vdom, scope) { + if (config.rexpr.test(vdom.nodeValue)) { + this.bindings.push([ + vdom, + scope, + { + nodeValue: vdom.nodeValue + } + ]) + } + }, + + /** + * 从注释节点获取指令 + * @param {type} vdom + * @param {type} scope + * @param {type} parentChildren + * @returns {undefined} + */ + scanComment(vdom, scope, parentChildren) { + if (startWith(vdom.nodeValue, 'ms-for:')) { + this.getForBinding(vdom, scope, parentChildren) + } + }, + + /** + * 从元素节点的nodeName与属性中获取指令 + * @param {type} vdom + * @param {type} scope + * @param {type} parentChildren + * @param {type} isRoot 用于执行complete方法 + * @returns {undefined} + */ + scanTag(vdom, scope, parentChildren, isRoot) { + var dirs = {}, + attrs = vdom.props, + hasDir, + hasFor + for (var attr in attrs) { + var value = attrs[attr] + var oldName = attr + if (attr.charAt(0) === ':') { + attr = 'ms-' + attr.slice(1) + } + if (startWith(attr, 'ms-')) { + dirs[attr] = value + var type = attr.match(/\w+/g)[1] + type = eventMap[type] || type + if (!directives[type]) { + Anot.warn(attr + ' has not registered!') + } + hasDir = true + } + if (attr === 'ms-for') { + hasFor = value + delete attrs[oldName] + } + } + var $id = dirs['ms-important'] || dirs['ms-controller'] + if ($id) { + /** + * 后端渲染 + * serverTemplates后端给Anot添加的对象,里面都是模板, + * 将原来后端渲染好的区域再还原成原始样子,再被扫描 + */ + var templateCaches = Anot.serverTemplates + var temp = templateCaches && templateCaches[$id] + if (temp) { + Anot.log('前端再次渲染后端传过来的模板') + var node = fromString(temp)[0] + for (var i in node) { + vdom[i] = node[i] + } + delete templateCaches[$id] + this.scanTag(vdom, scope, parentChildren, isRoot) + return + } + //推算出指令类型 + var type = dirs['ms-important'] === $id ? 'important' : 'controller' + //推算出用户定义时属性名,是使用ms-属性还是:属性 + var attrName = 'ms-' + type in attrs ? 'ms-' + type : ':' + type + + if (inBrowser) { + delete attrs[attrName] + } + var dir = directives[type] + scope = dir.getScope.call(this, $id, scope) + if (!scope) { + return + } else { + var clazz = attrs['class'] + if (clazz) { + attrs['class'] = (' ' + clazz + ' ') + .replace(' ms-controller ', '') + .trim() + } + } + var render = this + scope.$render = render + this.callbacks.push(function() { + //用于删除ms-controller + dir.update.call(render, vdom, attrName, $id) + }) + } + if (hasFor) { + if (vdom.dom) { + vdom.dom.removeAttribute(oldName) + } + return this.getForBindingByElement(vdom, scope, parentChildren, hasFor) + } + + if (/^ms\-/.test(vdom.nodeName)) { + attrs.is = vdom.nodeName + } + + if (attrs['is']) { + if (!dirs['ms-widget']) { + dirs['ms-widget'] = '{}' + } + hasDir = true + } + if (hasDir) { + this.bindings.push([vdom, scope, dirs]) + } + var children = vdom.children + //如果存在子节点,并且不是容器元素(script, stype, textarea, xmp...) + if ( + !orphanTag[vdom.nodeName] && + children && + children.length && + !delayCompileNodes(dirs) + ) { + this.scanChildren(children, scope, false) + } + }, + + /** + * 将绑定属性转换为指令 + * 执行各种回调与优化指令 + * @returns {undefined} + */ + complete() { + this.yieldDirectives() + this.beforeReady() + if (inBrowser) { + var root = this.root + if (inBrowser) { + var rootDom = Anot.vdom(root, 'toDOM') + groupTree(rootDom, root.children) + } + } + + this.mount = true + var fn + while ((fn = this.callbacks.pop())) { + fn() + } + this.optimizeDirectives() + }, + + /** + * 将收集到的绑定属性进行深加工,最后转换指令 + * @returns {Array} + */ + yieldDirectives() { + var tuple + while ((tuple = this.bindings.shift())) { + var vdom = tuple[0], + scope = tuple[1], + dirs = tuple[2], + bindings = [] + if ('nodeValue' in dirs) { + bindings = parseInterpolate(dirs) + } else if (!('ms-skip' in dirs)) { + bindings = parseAttributes(dirs, tuple) + } + for (var i = 0, binding; (binding = bindings[i++]); ) { + var dir = directives[binding.type] + if (!inBrowser && /on|duplex|active|hover/.test(binding.type)) { + continue + } + if (dir.beforeInit) { + dir.beforeInit.call(binding) + } + + var directive = new Directive(scope, binding, vdom, this) + this.directives.push(directive) + } + } + }, + + /** + * 修改指令的update与callback方法,让它们以后执行时更加高效 + * @returns {undefined} + */ + optimizeDirectives() { + for (var i = 0, el; (el = this.directives[i++]); ) { + el.callback = directives[el.type].update + el.update = newUpdate + el._isScheduled = false + } + }, + update: function() { + for (var i = 0, el; (el = this.directives[i++]); ) { + el.update() + } + }, + + /** + * 销毁所有指令 + * @returns {undefined} + */ + dispose() { + var list = this.directives || [] + for (let i = 0, el; (el = list[i++]); ) { + el.dispose() + } + //防止其他地方的this.innerRender && this.innerRender.dispose报错 + for (let i in this) { + if (i !== 'dispose') delete this[i] + } + }, + + /** + * 将循环区域转换为for指令 + * @param {type} begin 注释节点 + * @param {type} scope + * @param {type} parentChildren + * @param {type} userCb 循环结束回调 + * @returns {undefined} + */ + getForBinding(begin, scope, parentChildren, userCb) { + var expr = begin.nodeValue.replace('ms-for:', '').trim() + begin.nodeValue = 'ms-for:' + expr + var nodes = getRange(parentChildren, begin) + var end = nodes.end + var fragment = Anot.vdom(nodes, 'toHTML') + parentChildren.splice(nodes.start, nodes.length) + begin.props = {} + this.bindings.push([ + begin, + scope, + { + 'ms-for': expr + }, + { + begin, + end, + expr, + userCb, + fragment, + parentChildren + } + ]) + }, + + /** + * 在带ms-for元素节点旁添加两个注释节点,组成循环区域 + * @param {type} vdom + * @param {type} scope + * @param {type} parentChildren + * @param {type} expr + * @returns {undefined} + */ + getForBindingByElement(vdom, scope, parentChildren, expr) { + var index = parentChildren.indexOf(vdom) //原来带ms-for的元素节点 + var props = vdom.props + var begin = { + nodeName: '#comment', + nodeValue: 'ms-for:' + expr + } + if (props.slot) { + begin.slot = props.slot + delete props.slot + } + var end = { + nodeName: '#comment', + nodeValue: 'ms-for-end:' + } + parentChildren.splice(index, 1, begin, vdom, end) + this.getForBinding(begin, scope, parentChildren, props['data-for-rendered']) + } +} +var viewID + +function newUpdate() { + var oldVal = this.beforeUpdate() + var newVal = (this.value = this.get()) + if (this.callback && this.diff(newVal, oldVal)) { + this.callback(this.node, this.value) + var vm = this.vm + var $render = vm.$render + var list = vm.$events['onViewChange'] + /* istanbul ignore if */ + if (list && $render && $render.root && !Anot.viewChanging) { + if (viewID) { + clearTimeout(viewID) + viewID = null + } + viewID = setTimeout(function() { + list.forEach(function(el) { + el.callback.call(vm, { + type: 'viewchange', + target: $render.root, + vmodel: vm + }) + }) + }) + } + } + this._isScheduled = false +} diff --git a/src/renders/serverRender.js b/src/renders/serverRender.js new file mode 100644 index 0000000..cb890d5 --- /dev/null +++ b/src/renders/serverRender.js @@ -0,0 +1,33 @@ +function serverRender(vm, str) { + var nodes = Anot.lexer(str) + var templates = {} + collectTemplate(nodes, templates) + var render = Anot.scan(str) + var html = Anot.vdom(render.root, 'toHTML', false) + return { + templates: templates, + html: html + } +} + +function collectTemplate(vdoms, obj) { + for (var i = 0, el; (el = vdoms[i++]); ) { + var props = el.props + if (props) { + var id = + props['ms-controller'] || + props[':controller'] || + props['ms-important'] || + props[':important'] + if (id) { + obj[id] = Anot.VElement.prototype.toHTML.call(el, true) + continue + } + } + if (el.children) { + collectTemplate(el.children, obj) + } + } +} + +if (typeof module === 'object') module.exports = serverRender diff --git a/src/renders/share.js b/src/renders/share.js new file mode 100644 index 0000000..19881a5 --- /dev/null +++ b/src/renders/share.js @@ -0,0 +1,96 @@ +import { Anot, createFragment } from '../seed/core' +import { lookupOption, getSelectedValue } from '../directives/duplex/option' + +function getChildren(arr) { + var count = 0 + for (var i = 0, el; (el = arr[i++]); ) { + if (el.nodeName === '#document-fragment') { + count += getChildren(el.children) + } else { + count += 1 + } + } + return count +} +export function groupTree(parent, children) { + children && + children.forEach(function(vdom) { + if (!vdom) return + var vlength = vdom.children && getChildren(vdom.children) + if (vdom.nodeName === '#document-fragment') { + var dom = createFragment() + } else { + dom = Anot.vdom(vdom, 'toDOM') + var domlength = dom.childNodes && dom.childNodes.length + if (domlength && vlength && domlength > vlength) { + if (!appendChildMayThrowError[dom.nodeName]) { + Anot.clearHTML(dom) + } + } + } + if (vlength) { + groupTree(dom, vdom.children) + if (vdom.nodeName === 'select') { + var values = [] + getSelectedValue(vdom, values) + lookupOption(vdom, values) + } + } + //高级版本可以尝试 querySelectorAll + + try { + if (!appendChildMayThrowError[parent.nodeName]) { + parent.appendChild(dom) + } + } catch (e) {} + }) +} + +export function dumpTree(elem) { + if (elem) { + var firstChild + while ((firstChild = elem.firstChild)) { + if (firstChild.nodeType === 1) { + dumpTree(firstChild) + } + elem.removeChild(firstChild) + } + } +} + +export function getRange(childNodes, node) { + var i = childNodes.indexOf(node) + 1 + var deep = 1, + nodes = [], + end + nodes.start = i + while ((node = childNodes[i++])) { + nodes.push(node) + if (node.nodeName === '#comment') { + if (startWith(node.nodeValue, 'ms-for:')) { + deep++ + } else if (node.nodeValue === 'ms-for-end:') { + deep-- + if (deep === 0) { + end = node + nodes.pop() + break + } + } + } + } + nodes.end = end + return nodes +} + +export function startWith(long, short) { + return long.indexOf(short) === 0 +} + +var appendChildMayThrowError = { + '#text': 1, + '#comment': 1, + script: 1, + style: 1, + noscript: 1 +} diff --git a/src/seed/browser.js b/src/seed/browser.js new file mode 100644 index 0000000..1de9f56 --- /dev/null +++ b/src/seed/browser.js @@ -0,0 +1,32 @@ +export let win = + typeof window === 'object' ? window : typeof global === 'object' ? global : {} + +export let inBrowser = !!win.location && win.navigator +/* istanbul ignore if */ + +export let document = inBrowser + ? win.document + : { + createElement: Object, + createElementNS: Object, + documentElement: 'xx', + contains: Boolean + } +export var root = inBrowser + ? document.documentElement + : { + outerHTML: 'x' + } + +let versions = { + objectobject: 7, //IE7-8 + objectundefined: 6, //IE6 + undefinedfunction: NaN, // other modern browsers + undefinedobject: NaN //Mobile Safari 8.0.0 (iOS 8.4.0) + //objectfunction chrome 47 +} +/* istanbul ignore next */ +export var msie = + document.documentMode || versions[typeof document.all + typeof XMLHttpRequest] + +export var modern = /NaN|undefined/.test(msie) || msie > 8 diff --git a/src/seed/cache.js b/src/seed/cache.js new file mode 100644 index 0000000..6e1045e --- /dev/null +++ b/src/seed/cache.js @@ -0,0 +1,111 @@ +/* + https://github.com/rsms/js-lru + entry entry entry entry + ______ ______ ______ ______ + | head |.newer => | |.newer => | |.newer => | tail | + | A | | B | | C | | D | + |______| <= older.|______| <= older.|______| <= older.|______| + + removed <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- added + */ +export function Cache(maxLength) { + // 标识当前缓存数组的大小 + this.size = 0 + // 标识缓存数组能达到的最大长度 + this.limit = maxLength + // head(最不常用的项),tail(最常用的项)全部初始化为undefined + + this.head = this.tail = void 0 + this._keymap = {} +} + +Cache.prototype = { + + put(key, value) { + var entry = { + key: key, + value: value + } + this._keymap[key] = entry + if (this.tail) { + // 如果存在tail(缓存数组的长度不为0),将tail指向新的 entry + this.tail.newer = entry + entry.older = this.tail + } else { + // 如果缓存数组的长度为0,将head指向新的entry + this.head = entry + } + this.tail = entry + // 如果缓存数组达到上限,则先删除 head 指向的缓存对象 + /* istanbul ignore if */ + if (this.size === this.limit) { + this.shift() + } else { + this.size++ + } + return value + }, + + shift() { + /* istanbul ignore next */ + var entry = this.head + /* istanbul ignore if */ + if (entry) { + // 删除 head ,并改变指向 + this.head = this.head.newer + // 同步更新 _keymap 里面的属性值 + this.head.older = + entry.newer = + entry.older = + this._keymap[entry.key] = + void 0 + delete this._keymap[entry.key] //#1029 + // 同步更新 缓存数组的长度 + this.size-- + } + }, + + get(key) { + var entry = this._keymap[key] + // 如果查找不到含有`key`这个属性的缓存对象 + if (entry === void 0) + return + // 如果查找到的缓存对象已经是 tail (最近使用过的) + /* istanbul ignore if */ + if (entry === this.tail) { + return entry.value + } + // HEAD--------------TAIL + // <.older .newer> + // <--- add direction -- + // A B C E + if (entry.newer) { + // 处理 newer 指向 + if (entry === this.head) { + // 如果查找到的缓存对象是 head (最近最少使用过的) + // 则将 head 指向原 head 的 newer 所指向的缓存对象 + this.head = entry.newer + } + // 将所查找的缓存对象的下一级的 older 指向所查找的缓存对象的older所指向的值 + // 例如:A B C D E + // 如果查找到的是D,那么将E指向C,不再指向D + entry.newer.older = entry.older // C <-- E. + } + if (entry.older) { + // 处理 older 指向 + // 如果查找到的是D,那么C指向E,不再指向D + entry.older.newer = entry.newer // C. --> E + } + // 处理所查找到的对象的 newer 以及 older 指向 + entry.newer = void 0 // D --x + // older指向之前使用过的变量,即D指向E + entry.older = this.tail // D. --> E + if (this.tail) { + // 将E的newer指向D + this.tail.newer = entry // E. <-- D + } + // 改变 tail 为D + this.tail = entry + return entry.value + } +} \ No newline at end of file diff --git a/src/seed/core.js b/src/seed/core.js new file mode 100644 index 0000000..32a0000 --- /dev/null +++ b/src/seed/core.js @@ -0,0 +1,293 @@ +import { win, document, msie, inBrowser, root, modern } from './browser' +import { Cache } from './cache' +import { directive, directives, delayCompileNodes } from './directive' + +export var window = win +export function Anot(el) { + return new Anot.init(el) +} + +Anot.init = function(el) { + this[0] = this.element = el +} + +Anot.fn = Anot.prototype = Anot.init.prototype + +export function shadowCopy(destination, source) { + for (var property in source) { + destination[property] = source[property] + } + return destination +} +export var rword = /[^, ]+/g +export var rnowhite = /\S+/g //存在非空字符 +export var platform = {} //用于放置平台差异的方法与属性 +export var isArray = function(target) { + return Anot.type(target) === 'array' +} + +export 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 +} + +var op = Object.prototype +export function quote(str) { + return Anot._quote(str) +} +export var inspect = op.toString +export var ohasOwn = op.hasOwnProperty +export var ap = Array.prototype + +var hasConsole = typeof console === 'object' +Anot.config = { debug: true } +export function log() { + if (hasConsole && Anot.config.debug) { + Function.apply.call(console.log, console, arguments) + } +} +export { + Cache, + directive, + directives, + delayCompileNodes, + document, + root, + msie, + modern, + inBrowser +} +export function warn() { + if (hasConsole && Anot.config.debug) { + var method = console.warn || console.log + // http://qiang106.iteye.com/blog/1721425 + Function.apply.call(method, console, arguments) + } +} +export function error(str, e) { + throw (e || Error)(str) +} +export function noop() {} +export function isObject(a) { + return a !== null && typeof a === 'object' +} + +export function range(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 +} + +var rhyphen = /([a-z\d])([A-Z]+)/g +export function hyphen(target) { + //转换为连字符线风格 + return target.replace(rhyphen, '$1-$2').toLowerCase() +} + +var rcamelize = /[-_][^-_]/g +export function camelize(target) { + //提前判断,提高getStyle等的效率 + if (!target || (target.indexOf('-') < 0 && target.indexOf('_') < 0)) { + return target + } + //转换为驼峰风格 + return target.replace(rcamelize, function(match) { + return match.charAt(1).toUpperCase() + }) +} + +export var _slice = ap.slice +export function slice(nodes, start, end) { + return _slice.call(nodes, start, end) +} + +var rhashcode = /\d\.\d{4}/ +//生成UUID http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript +export function makeHashCode(prefix) { + /* istanbul ignore next*/ + prefix = prefix || 'Anot' + /* istanbul ignore next*/ + return String(Math.random() + Math.random()).replace(rhashcode, prefix) +} +//生成事件回调的UUID(用户通过ms-on指令) +export function getLongID(fn) { + /* istanbul ignore next */ + return fn.uuid || (fn.uuid = makeHashCode('e')) +} +var UUID = 1 +//生成事件回调的UUID(用户通过Anot.bind) +export function getShortID(fn) { + /* istanbul ignore next */ + return fn.uuid || (fn.uuid = '_' + ++UUID) +} + +var rescape = /[-.*+?^${}()|[\]\/\\]/g +export function escapeRegExp(target) { + //http://stevenlevithan.com/regex/xregexp/ + //将字符串安全格式化为正则表达式的源码 + return (target + '').replace(rescape, '\\$&') +} + +export var eventHooks = {} +export var eventListeners = {} +export var validators = {} +export var cssHooks = {} + +window.Anot = Anot + +export function createFragment() { + /* istanbul ignore next */ + return document.createDocumentFragment() +} + +var rentities = /&[a-z0-9#]{2,10};/ +var temp = document.createElement('div') +shadowCopy(Anot, { + Array: { + merge: function(target, other) { + //合并两个数组 Anot2新增 + target.push.apply(target, other) + }, + 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 + } + }, + evaluatorPool: new Cache(888), + parsers: { + number: function(a) { + return a === '' ? '' : parseFloat(a) || 0 + }, + string: function(a) { + return a === null || a === void 0 ? '' : a + '' + }, + boolean: function(a) { + if (a === '') return a + return a === 'true' || a === '1' + } + }, + _decode: function _decode(str) { + if (rentities.test(str)) { + temp.innerHTML = str + return temp.innerText || temp.textContent + } + return str + } +}) + +//============== config ============ +export function config(settings) { + for (var p in settings) { + var val = settings[p] + if (typeof config.plugins[p] === 'function') { + config.plugins[p](val) + } else { + config[p] = val + } + } + return this +} + +var plugins = { + interpolate: function(array) { + var openTag = array[0] + var closeTag = array[1] + if (openTag === closeTag) { + throw new SyntaxError('interpolate openTag cannot equal to closeTag') + } + var str = openTag + 'test' + closeTag + + if (/[<>]/.test(str)) { + throw new SyntaxError('interpolate cannot contains "<" or ">"') + } + + config.openTag = openTag + config.closeTag = closeTag + var o = escapeRegExp(openTag) + var c = escapeRegExp(closeTag) + + config.rtext = new RegExp(o + '(.+?)' + c, 'g') + config.rexpr = new RegExp(o + '([\\s\\S]*)' + c) + } +} +export function createAnchor(nodeValue) { + return document.createComment(nodeValue) +} +config.plugins = plugins +config({ + interpolate: ['{{', '}}'], + debug: true +}) +//============ config ============ + +shadowCopy(Anot, { + shadowCopy, + + oneObject, + inspect, + ohasOwn, + rword, + version: 1, + vmodels: {}, + + directives, + directive, + + eventHooks, + eventListeners, + validators, + cssHooks, + + log, + noop, + warn, + error, + config, + + modern, + msie, + root, + document, + window, + inBrowser, + + isObject, + range, + slice, + hyphen, + camelize, + escapeRegExp, + quote, + + makeHashCode +}) diff --git a/src/seed/directive.js b/src/seed/directive.js new file mode 100644 index 0000000..9779d0e --- /dev/null +++ b/src/seed/directive.js @@ -0,0 +1,25 @@ +var delayCompile = {} + +export var directives = {} + +export function directive(name, opts) { + if (directives[name]) { + Anot.warn(name, 'directive have defined! ') + } + directives[name] = opts + if (!opts.update) { + opts.update = function() {} + } + if (opts.delay) { + delayCompile[name] = 1 + } + return opts +} + +export function delayCompileNodes(dirs) { + for (var i in delayCompile) { + if ('ms-' + i in dirs) { + return true + } + } +} diff --git a/src/seed/lang.modern.js b/src/seed/lang.modern.js new file mode 100644 index 0000000..74b62eb --- /dev/null +++ b/src/seed/lang.modern.js @@ -0,0 +1,142 @@ +//这里放置存在异议的方法 +import { Anot, ohasOwn, inspect } from './core' +export { Anot } +var rwindow = /^\[object (?:Window|DOMWindow|global)\]$/ +var rarraylike = /(Array|List|Collection|Map|Arguments|Set)\]$/ + +// Anot.type +var class2type = {} +'Boolean Number String Function Array Date RegExp Object Error'.replace( + Anot.rword, + function(name) { + class2type['[object ' + name + ']'] = name.toLowerCase() + } +) + +Anot.type = function(obj) { + //取得目标的类型 + if (obj == null) { + return String(obj) + } + // 早期的webkit内核浏览器实现了已废弃的ecma262v4标准,可以将正则字面量当作函数使用,因此typeof在判定正则时会返回function + return typeof obj === 'object' || typeof obj === 'function' + ? class2type[inspect.call(obj)] || 'object' + : typeof obj +} + +Anot._quote = JSON.stringify + +Anot.isFunction = function(fn) { + return typeof fn === 'function' +} + +Anot.isWindow = function(obj) { + return rwindow.test(inspect.call(obj)) +} + +/*判定是否是一个朴素的javascript对象(Object),不是DOM对象,不是BOM对象,不是自定义类的实例*/ +Anot.isPlainObject = function(obj) { + // 简单的 typeof obj === 'object'检测,会致使用isPlainObject(window)在opera下通不过 + return ( + inspect.call(obj) === '[object Object]' && + Object.getPrototypeOf(obj) === Object.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' && typeof 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 +} + +/*判定是否类数组,如节点集合,纯数组,arguments与拥有非负整数的length属性的纯JS对象*/ +export function isArrayLike(obj) { + /* istanbul ignore if*/ + if (obj && typeof obj === 'object') { + var n = obj.length, + str = inspect.call(obj) + if (rarraylike.test(str)) { + return true + } else if (str === '[object Object]' && n === n >>> 0) { + return true //由于ecma262v5能修改对象属性的enumerable,因此不能用propertyIsEnumerable来判定了 + } + } + return false +} + +Anot.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 + } + } + } + } +} diff --git a/src/vdom/VComment.js b/src/vdom/VComment.js new file mode 100644 index 0000000..988a013 --- /dev/null +++ b/src/vdom/VComment.js @@ -0,0 +1,16 @@ +import { document } from '../seed/core' + +export function VComment(text) { + this.nodeName = '#comment' + this.nodeValue = text +} +VComment.prototype = { + constructor: VComment, + toDOM: function() { + if (this.dom) return this.dom + return (this.dom = document.createComment(this.nodeValue)) + }, + toHTML: function() { + return '' + } +} diff --git a/src/vdom/VElement.modern.js b/src/vdom/VElement.modern.js new file mode 100644 index 0000000..4d7822b --- /dev/null +++ b/src/vdom/VElement.modern.js @@ -0,0 +1,94 @@ +import { Anot, document } from '../seed/core' + +export function VElement(type, props, children, isVoidTag) { + this.nodeName = type + this.props = props + this.children = children + this.isVoidTag = isVoidTag +} +VElement.prototype = { + constructor: VElement, + toDOM() { + if (this.dom) return this.dom + var dom, + tagName = this.nodeName + if (Anot.modern && svgTags[tagName]) { + dom = createSVG(tagName) + } else { + dom = document.createElement(tagName) + } + var props = this.props || {} + + for (var i in props) { + var val = props[i] + if (skipFalseAndFunction(val)) { + dom.setAttribute(i, val + '') + } + } + var c = this.children || [] + var template = c[0] ? c[0].nodeValue : '' + switch (this.nodeName) { + case 'xmp': + case 'style': + case 'script': + case 'noscript': + dom.innerHTML = template + break + case 'template': + if (supportTemplate) { + dom.innerHTML = template + } else { + /* istanbul ignore next*/ + dom.textContent = template + } + break + default: + if (!this.isVoidTag && this.children) { + this.children.forEach( + el => el && dom.appendChild(Anot.vdom(el, 'toDOM')) + ) + } + break + } + return (this.dom = dom) + }, + toHTML() { + var arr = [] + var props = this.props || {} + for (var i in props) { + var val = props[i] + if (skipFalseAndFunction(val)) { + arr.push(i + '=' + Anot.quote(props[i] + '')) + } + } + arr = arr.length ? ' ' + arr.join(' ') : '' + var str = '<' + this.nodeName + arr + if (this.isVoidTag) { + return str + '/>' + } + str += '>' + if (this.children) { + str += this.children + .map(el => (el ? Anot.vdom(el, 'toHTML') : '')) + .join('') + } + return str + '' + } +} + +function skipFalseAndFunction(a) { + return a !== false && Object(a) !== a +} + +function createSVG(type) { + return document.createElementNS('http://www.w3.org/2000/svg', type) +} + +var svgTags = Anot.oneObject( + 'circle,defs,ellipse,image,line,' + + 'path,polygon,polyline,rect,symbol,text,use,g,svg' +) + +if (Anot.inBrowser) { + var supportTemplate = 'content' in document.createElement('template') +} diff --git a/src/vdom/VFragment.js b/src/vdom/VFragment.js new file mode 100644 index 0000000..815c91c --- /dev/null +++ b/src/vdom/VFragment.js @@ -0,0 +1,36 @@ +import { Anot, createFragment } from '../seed/core' + +export function VFragment(children, key, val, index) { + this.nodeName = '#document-fragment' + this.children = children + this.key = key + this.val = val + this.index = index + this.props = {} +} +VFragment.prototype = { + constructor: VFragment, + toDOM() { + if (this.dom) return this.dom + var f = this.toFragment() + //IE6-11 docment-fragment都没有children属性 + this.split = f.lastChild + return (this.dom = f) + }, + dispose() { + this.toFragment() + this.innerRender && this.innerRender.dispose() + for (var i in this) { + this[i] = null + } + }, + toFragment() { + var f = createFragment() + this.children.forEach(el => f.appendChild(Anot.vdom(el, 'toDOM'))) + return f + }, + toHTML() { + var c = this.children + return c.map(el => Anot.vdom(el, 'toHTML')).join('') + } +} diff --git a/src/vdom/VText.js b/src/vdom/VText.js new file mode 100644 index 0000000..19ab661 --- /dev/null +++ b/src/vdom/VText.js @@ -0,0 +1,19 @@ +import { Anot, document } from '../seed/core' + +export function VText(text) { + this.nodeName = '#text' + this.nodeValue = text +} + +VText.prototype = { + constructor: VText, + toDOM() { + /* istanbul ignore if*/ + if (this.dom) return this.dom + var v = Anot._decode(this.nodeValue) + return (this.dom = document.createTextNode(v)) + }, + toHTML() { + return this.nodeValue + } +} diff --git a/src/vdom/modern.js b/src/vdom/modern.js new file mode 100644 index 0000000..0c222c1 --- /dev/null +++ b/src/vdom/modern.js @@ -0,0 +1,40 @@ +/** + * 虚拟DOM的4大构造器 + */ +import { Anot, createFragment } from '../seed/core' +import { VText } from './VText' +import { VComment } from './VComment' +import { VElement } from './VElement.modern' +import { VFragment } from './VFragment' + +Anot.mix(Anot, { + VText, + VComment, + VElement, + VFragment +}) + +var constNameMap = { + '#text': 'VText', + '#document-fragment': 'VFragment', + '#comment': 'VComment' +} + +var vdom = (Anot.vdomAdaptor = Anot.vdom = function(obj, method) { + if (!obj) { + //obj在ms-for循环里面可能是null + return method === 'toHTML' ? '' : createFragment() + } + var nodeName = obj.nodeName + if (!nodeName) { + return new Anot.VFragment(obj)[method]() + } + var constName = constNameMap[nodeName] || 'VElement' + return Anot[constName].prototype[method].call(obj) +}) + +Anot.domize = function(a) { + return Anot.vdom(a, 'toDOM') +} + +export { vdom, Anot, VText, VComment, VElement, VFragment } diff --git a/src/vmodel/Action.js b/src/vmodel/Action.js new file mode 100644 index 0000000..2d0d2d8 --- /dev/null +++ b/src/vmodel/Action.js @@ -0,0 +1,176 @@ +import { Anot } from '../seed/core' + +import { runActions, collectDeps } from './transaction' + +import { createGetter, createSetter } from '../parser/index' + +export var actionUUID = 1 +//需要重构 +export function Action(vm, options, callback) { + for (var i in options) { + if (protectedMenbers[i] !== 1) { + this[i] = options[i] + } + } + + this.vm = vm + this.observers = [] + this.callback = callback + this.uuid = ++actionUUID + this.ids = '' + this.mapIDs = {} //这个用于去重 + this.isAction = true + var expr = this.expr + // 缓存取值函数 + if (typeof this.getter !== 'function') { + this.getter = createGetter(expr, this.type) + } + // 缓存设值函数(双向数据绑定) + if (this.type === 'duplex') { + this.setter = createSetter(expr, this.type) + } + // 缓存表达式旧值 + this.value = NaN + // 表达式初始值 & 提取依赖 + if (!this.node) { + this.value = this.get() + } +} + +Action.prototype = { + getValue() { + var scope = this.vm + try { + return this.getter.call(scope, scope) + } catch (e) { + Anot.log(this.getter + ' exec error') + } + }, + + setValue(value) { + var scope = this.vm + if (this.setter) { + this.setter.call(scope, scope, value) + } + }, + + // get --> getValue --> getter + get(fn) { + var name = 'action track ' + this.type + + if (this.deep) { + Anot.deepCollect = true + } + + var value = collectDeps(this, this.getValue) + if (this.deep && Anot.deepCollect) { + Anot.deepCollect = false + } + + return value + }, + + /** + * 在更新视图前保存原有的value + */ + beforeUpdate() { + return (this.oldValue = getPlainObject(this.value)) + }, + + update(args, uuid) { + var oldVal = this.beforeUpdate() + var newVal = (this.value = this.get()) + var callback = this.callback + if (callback && this.diff(newVal, oldVal, args)) { + callback.call(this.vm, this.value, oldVal, this.expr) + } + this._isScheduled = false + }, + schedule() { + if (!this._isScheduled) { + this._isScheduled = true + if (!Anot.uniqActions[this.uuid]) { + Anot.uniqActions[this.uuid] = 1 + Anot.pendingActions.push(this) + } + + runActions() //这里会还原_isScheduled + } + }, + removeDepends() { + var self = this + this.observers.forEach(function(depend) { + Anot.Array.remove(depend.observers, self) + }) + }, + + /** + * 比较两个计算值是否,一致,在for, class等能复杂数据类型的指令中,它们会重写diff复法 + */ + diff(a, b) { + return a !== b + }, + + /** + * 销毁指令 + */ + dispose() { + this.value = null + this.removeDepends() + if (this.beforeDispose) { + this.beforeDispose() + } + for (var i in this) { + delete this[i] + } + } +} + +function getPlainObject(v) { + if (v && typeof v === 'object') { + if (v && v.$events) { + return v.$model + } else if (Array.isArray(v)) { + let ret = [] + for (let i = 0, n = v.length; i < n; i++) { + ret.push(getPlainObject(v[i])) + } + return ret + } else { + let ret = {} + for (let i in v) { + ret[i] = getPlainObject(v[i]) + } + return ret + } + } else { + return v + } +} + +export var protectedMenbers = { + vm: 1, + callback: 1, + + observers: 1, + oldValue: 1, + value: 1, + getValue: 1, + setValue: 1, + get: 1, + + removeDepends: 1, + beforeUpdate: 1, + update: 1, + //diff + //getter + //setter + //expr + //vdom + //type: "for" + //name: "ms-for" + //attrName: ":for" + //param: "click" + //beforeDispose + dispose: 1 +} diff --git a/src/vmodel/Computed.js b/src/vmodel/Computed.js new file mode 100644 index 0000000..64594c7 --- /dev/null +++ b/src/vmodel/Computed.js @@ -0,0 +1,102 @@ +import { Anot } from '../seed/core' + +import { obid, Mutation } from './Mutation' +import { collectDeps } from './transaction' + +function getBody(fn) { + var entire = fn.toString() + return entire.substring(entire.indexOf('{}') + 1, entire.lastIndexOf('}')) +} +//如果不存在三目,if,方法 +let instability = /(\?|if\b|\(.+\))/ + +function __create(o) { + var __ = function() {} + __.prototype = o + return new __() +} + +function __extends(child, parent) { + if (typeof parent === 'function') { + var proto = (child.prototype = __create(parent.prototype)) + proto.constructor = child + } +} +export var Computed = (function(_super) { + __extends(Computed, _super) + + function Computed(name, options, vm) { + //构造函数 + _super.call(this, name, undefined, vm) + delete options.get + delete options.set + + Anot.mix(this, options) + this.deps = {} + this.type = 'computed' + this.depsVersion = {} + this.isComputed = true + this.trackAndCompute() + if (!('isStable' in this)) { + this.isStable = !instability.test(getBody(this.getter)) + } + } + var cp = Computed.prototype + cp.trackAndCompute = function() { + if (this.isStable && this.depsCount > 0) { + this.getValue() + } else { + collectDeps(this, this.getValue.bind(this)) + } + } + + cp.getValue = function() { + return (this.value = this.getter.call(this.vm)) + } + + cp.schedule = function() { + var observers = this.observers + var i = observers.length + while (i--) { + var d = observers[i] + if (d.schedule) { + d.schedule() + } + } + } + + cp.shouldCompute = function() { + if (this.isStable) { + //如果变动因子确定,那么只比较变动因子的版本 + var toComputed = false + for (var i in this.deps) { + if (this.deps[i].version !== this.depsVersion[i]) { + toComputed = true + this.depsVersion[i] = this.deps[i].version + } + } + return toComputed + } + return true + } + cp.set = function() { + if (this.setter) { + Anot.transaction(this.setter, this.vm, arguments) + } + } + cp.get = function() { + //当被设置了就不稳定,当它被访问了一次就是稳定 + this.collect() + + if (this.shouldCompute()) { + this.trackAndCompute() + // console.log('computed 2 分支') + this.updateVersion() + // this.reportChanged() + } + + //下面这一行好像没用 + return this.value + } + return Computed +})(Mutation) diff --git a/src/vmodel/Mutation.js b/src/vmodel/Mutation.js new file mode 100644 index 0000000..6605978 --- /dev/null +++ b/src/vmodel/Mutation.js @@ -0,0 +1,90 @@ +import { + transactionStart, + transactionEnd, + reportObserved, + propagateChanged +} from './transaction' +import { Anot, platform } from '../seed/core' +/** +* + 与Computed等共享UUID +*/ +export let obid = 1 +export function Mutation(expr, value, vm) { + //构造函数 + this.expr = expr + if (value) { + var childVm = platform.createProxy(value, this) + if (childVm) { + value = childVm + } + } + this.value = value + this.vm = vm + try { + vm.$mutations[expr] = this + } catch (ignoreIE) {} + this.uuid = ++obid + this.updateVersion() + this.mapIDs = {} + this.observers = [] +} + +Mutation.prototype = { + get() { + if (Anot.trackingAction) { + this.collect() //被收集 + var childOb = this.value + if (childOb && childOb.$events) { + if (Array.isArray(childOb)) { + childOb.forEach(function(item) { + if (item && item.$events) { + item.$events.__dep__.collect() + } + }) + } else if (Anot.deepCollect) { + for (var key in childOb) { + if (childOb.hasOwnProperty(key)) { + var collectIt = childOb[key] + } + } + } + } + } + return this.value + }, + + collect() { + Anot.track(name, '被收集') + reportObserved(this) + }, + + updateVersion() { + this.version = Math.random() + Math.random() + }, + + notify() { + transactionStart() + propagateChanged(this) + transactionEnd() + }, + + set(newValue) { + var oldValue = this.value + if (newValue !== oldValue) { + if (Anot.isObject(newValue)) { + var hash = oldValue && oldValue.$hashcode + var childVM = platform.createProxy(newValue, this) + if (childVM) { + if (hash) { + childVM.$hashcode = hash + } + newValue = childVM + } + } + this.value = newValue + this.updateVersion() + this.notify() + } + } +} diff --git a/src/vmodel/ProxyArray.js b/src/vmodel/ProxyArray.js new file mode 100644 index 0000000..2f1daff --- /dev/null +++ b/src/vmodel/ProxyArray.js @@ -0,0 +1,121 @@ +import { Anot, ap, platform, modern, isObject } from '../seed/core' +import { Mutation } from './Mutation' + +var _splice = ap.splice +var __array__ = { + set: function(index, val) { + if (index >>> 0 === index && this[index] !== val) { + if (index > this.length) { + throw Error(index + 'set方法的第一个参数不能大于原数组长度') + } + this.splice(index, 1, val) + } + }, + toJSON: function() { + //为了解决IE6-8的解决,通过此方法显式地求取数组的$model + return (this.$model = platform.toJson(this)) + }, + contains: function(el) { + //判定是否包含 + return this.indexOf(el) !== -1 + }, + ensure: function(el) { + if (!this.contains(el)) { + //只有不存在才push + this.push(el) + return true + } + return false + }, + pushArray: function(arr) { + return this.push.apply(this, arr) + }, + remove: function(el) { + //移除第一个等于给定值的元素 + return this.removeAt(this.indexOf(el)) + }, + removeAt: function(index) { + //移除指定索引上的元素 + if (index >>> 0 === index) { + return this.splice(index, 1) + } + return [] + }, + clear: function() { + this.removeAll() + return this + }, + removeAll: function(all) { + //移除N个元素 + var size = this.length + var eliminate = Array.isArray(all) + ? function(el) { + return all.indexOf(el) !== -1 + } + : typeof all === 'function' + ? all + : false + + if (eliminate) { + for (var i = this.length - 1; i >= 0; i--) { + if (eliminate(this[i], i)) { + _splice.call(this, i, 1) + } + } + } else { + _splice.call(this, 0, this.length) + } + this.toJSON() + this.$events.__dep__.notify() + } +} +export function hijackMethods(array) { + for (var i in __array__) { + platform.hideProperty(array, i, __array__[i]) + } +} +var __method__ = [ + 'push', + 'pop', + 'shift', + 'unshift', + 'splice', + 'sort', + 'reverse' +] + +__method__.forEach(function(method) { + var original = ap[method] + __array__[method] = function() { + // 继续尝试劫持数组元素的属性 + var core = this.$events + + var args = platform.listFactory(arguments, true, core.__dep__) + var result = original.apply(this, args) + + this.toJSON() + core.__dep__.notify(method) + return result + } +}) + +export function listFactory(array, stop, dd) { + if (!stop) { + hijackMethods(array) + if (modern) { + Object.defineProperty(array, '$model', platform.modelAccessor) + } + platform.hideProperty(array, '$hashcode', Anot.makeHashCode('$')) + platform.hideProperty(array, '$events', { __dep__: dd || new Mutation() }) + } + var _dd = array.$events && array.$events.__dep__ + for (var i = 0, n = array.length; i < n; i++) { + var item = array[i] + if (isObject(item)) { + array[i] = platform.createProxy(item, _dd) + } + } + return array +} + +platform.listFactory = listFactory diff --git a/src/vmodel/modern.js b/src/vmodel/modern.js new file mode 100644 index 0000000..1cc1544 --- /dev/null +++ b/src/vmodel/modern.js @@ -0,0 +1,85 @@ +import { Anot, platform, modern } from '../seed/core' +import { $$skipArray } from './reserved' +import { Action } from './Action' +import './share' +import './ProxyArray' +export { Anot, platform } + +export function hideProperty(host, name, value) { + Object.defineProperty(host, name, { + value: value, + writable: true, + enumerable: false, + configurable: true + }) +} + +function $fire(expr, a) { + var list = this.$events[expr] + if (Array.isArray(list)) { + for (var i = 0, w; (w = list[i++]); ) { + w.callback.call(w.vm, a, w.value, w.expr) + } + } +} + +function $watch(expr, callback, deep) { + var core = this.$events + var w = new Action( + this, + { + deep: deep, + type: 'user', + expr: expr + }, + callback + ) + if (!core[expr]) { + core[expr] = [w] + } else { + core[expr].push(w) + } + return function() { + w.dispose() + Anot.Array.remove(core[expr], w) + if (core[expr].length === 0) { + delete core[expr] + } + } +} +export function watchFactory(core) { + return $watch +} + +export function fireFactory(core) { + return $fire +} + +export function afterCreate(vm, core, keys, bindThis) { + var ac = vm.$accessors + //隐藏系统属性 + for (var key in $$skipArray) { + hideProperty(vm, key, vm[key]) + } + //为不可监听的属性或方法赋值 + for (let i = 0; i < keys.length; i++) { + let key = keys[i] + if (!(key in ac)) { + let val = core[key] + if (bindThis && typeof val === 'function') { + vm[key] = val.bind(vm) + vm[key]._orig = val + continue + } + vm[key] = val + } + } + vm.$track = keys.join('☥') + vm.$events.__proxy__ = vm +} + +platform.fireFactory = fireFactory +platform.watchFactory = watchFactory +platform.afterCreate = afterCreate +platform.hideProperty = hideProperty +platform.createViewModel = Object.defineProperties diff --git a/src/vmodel/proxy.js b/src/vmodel/proxy.js new file mode 100644 index 0000000..3091c35 --- /dev/null +++ b/src/vmodel/proxy.js @@ -0,0 +1,161 @@ +import { Anot, platform, isObject, modern } from '../seed/core' +import { $$skipArray } from './reserved' +import { Mutation } from './Mutation' +import { Computed } from './Computed' +import { IProxy, canHijack, createProxy } from './share' + +if (typeof Proxy === 'function') { + Anot.config.inProxyMode = true + + platform.modelFactory = function modelFactory(definition, dd) { + var clone = {} + for (let i in definition) { + clone[i] = definition[i] + delete definition[i] + } + + definition.$id = clone.$id + var proxy = new IProxy(definition, dd) + + var vm = toProxy(proxy) + //先添加普通属性与监控属性 + for (let i in clone) { + vm[i] = clone[i] + } + var $computed = clone.$computed + //再添加计算属性 + if ($computed) { + delete clone.$computed + for (let i in $computed) { + let val = $computed[i] + if (typeof val === 'function') { + let _val = val + val = { get: _val } + } + if (val && val.get) { + val.getter = val.get + //在set方法中的target是IProxy,需要重写成Proxy,才能依赖收集 + val.vm = vm + if (val.set) val.setter = val.set + $computed[i] = val + delete clone[i] //去掉重名的监控属性 + } else { + delete $computed[i] + } + } + for (let i in $computed) { + vm[i] = $computed[i] + } + } + + return vm + } + + //https://developer.mozilla.org/en-US/docs/Archive/Web/Old_Proxy_API + function toProxy(definition) { + return Proxy.create + ? Proxy.create(definition, traps) + : new Proxy(definition, traps) + } + + function wrapIt(str) { + return '☥' + str + '☥' + } + var traps = { + deleteProperty(target, name) { + if (target.hasOwnProperty(name)) { + //移除一个属性,分三昌: + //1. 移除监听器 + //2. 移除真实对象的对应属性 + //3. 移除$track中的键名 + delete target.$accessors[name] + delete target[name] + target.$track = wrapIt(target.$track) + .replace(wrapIt(name), '') + .slice(1, -1) + } + return true + }, + get(target, name) { + if (name === '$model') { + return platform.toJson(target) + } + //收集依赖 + var m = target.$accessors[name] + if (m && m.get) { + return m.get() + } + + return target[name] + }, + set(target, name, value) { + if (name === '$model' || name === '$track') { + return true + } + if (name in $$skipArray) { + target[name] = value + return true + } + + var ac = target.$accessors + var oldValue = ac[name] ? ac[name].value : target[name] + + if (oldValue !== value) { + if (!target.hasOwnProperty(name)) { + updateTrack(target, name) + } + if (canHijack(name, value, target.$proxyItemBackdoor)) { + var $computed = target.$computed || {} + //如果是新属性 + if (!ac[name]) { + target[name] = value //必须设置,用于hasOwnProperty + var isComputed = !!$computed[name] + var Observable = isComputed ? Computed : Mutation + ac[name] = new Observable(name, value, target) + return true + } + var mutation = ac[name] + //创建子对象 + mutation.set(value) + target[name] = mutation.value + } else { + target[name] = value + } + } + // set方法必须返回true, 告诉Proxy已经成功修改了这个值,否则会抛 + //'set' on proxy: trap returned falsish for property xxx 错误 + return true + } + //has 只能用于 in 操作符,没什么用删去 + } + + function updateTrack(target, name) { + var arr = target.$track.match(/[^☥]+/g) || [] + arr.push(name) + target.$track = arr.sort().join('☥') + } + + Anot.itemFactory = platform.itemFactory = function itemFactory( + before, + after + ) { + var definition = before.$model + definition.$proxyItemBackdoor = true + definition.$id = + before.$hashcode + String(after.hashcode || Math.random()).slice(6) + definition.$accessors = Anot.mix({}, before.$accessors) + var vm = platform.modelFactory(definition) + vm.$track = before.$track + for (var i in after.data) { + vm[i] = after.data[i] + } + return vm + } + + platform.fuseFactory = function fuseFactory(before, after) { + var definition = Anot.mix(before.$model, after.$model) + definition.$id = before.$hashcode + after.$hashcode + definition.$accessors = Anot.mix({}, before.$accessors, after.$accessors) + return platform.modelFactory(definition) + } +} diff --git a/src/vmodel/reserved.js b/src/vmodel/reserved.js new file mode 100644 index 0000000..fc22573 --- /dev/null +++ b/src/vmodel/reserved.js @@ -0,0 +1,24 @@ +/** +$$skipArray:是系统级通用的不可监听属性 +$skipArray: 是当前对象特有的不可监听属性 + + 不同点是 + $$skipArray被hasOwnProperty后返回false + $skipArray被hasOwnProperty后返回true + */ +var falsy +export var $$skipArray = { + $id: falsy, + $render: falsy, + $track: falsy, + $element: falsy, + $computed: falsy, + $watch: falsy, + $fire: falsy, + $events: falsy, + $accessors: falsy, + $hashcode: falsy, + $mutations: falsy, + $vbthis: falsy, + $vbsetter: falsy +} diff --git a/src/vmodel/share.js b/src/vmodel/share.js new file mode 100644 index 0000000..363d3b4 --- /dev/null +++ b/src/vmodel/share.js @@ -0,0 +1,226 @@ +import { Anot, platform, isObject, modern } from '../seed/core' +import { $$skipArray } from './reserved' +import { Mutation } from './Mutation' +import { Computed } from './Computed' + +/** + * 这里放置ViewModel模块的共用方法 + * Anot.define: 全框架最重要的方法,生成用户VM + * IProxy, 基本用户数据产生的一个数据对象,基于$model与vmodel之间的形态 + * modelFactory: 生成用户VM + * canHijack: 判定此属性是否该被劫持,加入数据监听与分发的的逻辑 + * createProxy: listFactory与modelFactory的封装 + * createAccessor: 实现数据监听与分发的重要对象 + * itemFactory: ms-for循环中产生的代理VM的生成工厂 + * fuseFactory: 两个ms-controller间产生的代理VM的生成工厂 + */ + +Anot.define = function(definition) { + var $id = definition.$id + if (!$id) { + Anot.error('vm.$id must be specified') + } + if (Anot.vmodels[$id]) { + Anot.warn('error:[' + $id + '] had defined!') + } + var vm = platform.modelFactory(definition) + return (Anot.vmodels[$id] = vm) +} + +/** + * 在未来的版本,Anot改用Proxy来创建VM,因此 + */ + +export function IProxy(definition, dd) { + Anot.mix(this, definition) + Anot.mix(this, $$skipArray) + this.$hashcode = Anot.makeHashCode('$') + this.$id = this.$id || this.$hashcode + this.$events = { + __dep__: dd || new Mutation(this.$id) + } + if (Anot.config.inProxyMode) { + delete this.$mutations + this.$accessors = {} + this.$computed = {} + this.$track = '' + } else { + this.$accessors = { + $model: modelAccessor + } + } + if (dd === void 0) { + this.$watch = platform.watchFactory(this.$events) + this.$fire = platform.fireFactory(this.$events) + } else { + delete this.$watch + delete this.$fire + } +} + +platform.modelFactory = function modelFactory(definition, dd) { + var $computed = definition.$computed || {} + delete definition.$computed + var core = new IProxy(definition, dd) + var $accessors = core.$accessors + var keys = [] + + platform.hideProperty(core, '$mutations', {}) + + for (let key in definition) { + if (key in $$skipArray) continue + var val = definition[key] + keys.push(key) + if (canHijack(key, val)) { + $accessors[key] = createAccessor(key, val) + } + } + for (let key in $computed) { + if (key in $$skipArray) continue + var val = $computed[key] + if (typeof val === 'function') { + val = { + get: val + } + } + if (val && val.get) { + val.getter = val.get + val.setter = val.set + Anot.Array.ensure(keys, key) + $accessors[key] = createAccessor(key, val, true) + } + } + //将系统API以unenumerable形式加入vm, + //添加用户的其他不可监听属性或方法 + //重写$track + //并在IE6-8中增添加不存在的hasOwnPropert方法 + var vm = platform.createViewModel(core, $accessors, core) + platform.afterCreate(vm, core, keys, !dd) + return vm +} +var $proxyItemBackdoorMap = {} + +export function canHijack(key, val, $proxyItemBackdoor) { + if (key in $$skipArray) return false + if (key.charAt(0) === '$') { + if ($proxyItemBackdoor) { + if (!$proxyItemBackdoorMap[key]) { + $proxyItemBackdoorMap[key] = 1 + Anot.warn(`ms-for中的变量${key}不再建议以$为前缀`) + } + return true + } + return false + } + if (val == null) { + Anot.warn('定义vmodel时' + key + '的属性值不能为null undefine') + return true + } + if (/error|date|function|regexp/.test(Anot.type(val))) { + return false + } + return !(val && val.nodeName && val.nodeType) +} + +export function createProxy(target, dd) { + if (target && target.$events) { + return target + } + var vm + if (Array.isArray(target)) { + vm = platform.listFactory(target, false, dd) + } else if (isObject(target)) { + vm = platform.modelFactory(target, dd) + } + return vm +} + +platform.createProxy = createProxy + +platform.itemFactory = function itemFactory(before, after) { + var keyMap = before.$model + var core = new IProxy(keyMap) + var state = Anot.shadowCopy(core.$accessors, before.$accessors) //防止互相污染 + var data = after.data + //core是包含系统属性的对象 + //keyMap是不包含系统属性的对象, keys + for (var key in data) { + var val = (keyMap[key] = core[key] = data[key]) + state[key] = createAccessor(key, val) + } + var keys = Object.keys(keyMap) + var vm = platform.createViewModel(core, state, core) + platform.afterCreate(vm, core, keys) + return vm +} + +function createAccessor(key, val, isComputed) { + var mutation = null + var Accessor = isComputed ? Computed : Mutation + return { + get: function Getter() { + if (!mutation) { + mutation = new Accessor(key, val, this) + } + return mutation.get() + }, + set: function Setter(newValue) { + if (!mutation) { + mutation = new Accessor(key, val, this) + } + mutation.set(newValue) + }, + enumerable: true, + configurable: true + } +} + +platform.fuseFactory = function fuseFactory(before, after) { + var keyMap = Anot.mix(before.$model, after.$model) + var core = new IProxy( + Anot.mix(keyMap, { + $id: before.$id + after.$id + }) + ) + var state = Anot.mix(core.$accessors, before.$accessors, after.$accessors) //防止互相污染 + + var keys = Object.keys(keyMap) + //将系统API以unenumerable形式加入vm,并在IE6-8中添加hasOwnPropert方法 + var vm = platform.createViewModel(core, state, core) + platform.afterCreate(vm, core, keys, false) + return vm +} + +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') { + if (typeof val.$track === 'string') { + var obj = {} + var arr = val.$track.match(/[^☥]+/g) || [] + arr.forEach(function(i) { + var value = val[i] + obj[i] = value && value.$events ? toJson(value) : value + }) + return obj + } + } + return val +} + +var modelAccessor = { + get: function() { + return toJson(this) + }, + set: Anot.noop, + enumerable: false, + configurable: true +} + +platform.toJson = toJson +platform.modelAccessor = modelAccessor diff --git a/src/vmodel/transaction.js b/src/vmodel/transaction.js new file mode 100644 index 0000000..ac7b59f --- /dev/null +++ b/src/vmodel/transaction.js @@ -0,0 +1,147 @@ +import { Anot, config } from '../seed/core' + +Anot.pendingActions = [] +Anot.uniqActions = {} +Anot.inTransaction = 0 +config.trackDeps = false +Anot.track = function() { + if (config.trackDeps) { + Anot.log.apply(Anot, arguments) + } +} + +/** + * Batch is a pseudotransaction, just for purposes of memoizing ComputedValues when nothing else does. + * During a batch `onBecomeUnobserved` will be called at most once per observable. + * Avoids unnecessary recalculations. + */ + +export function runActions() { + if (Anot.isRunningActions === true || Anot.inTransaction > 0) return + Anot.isRunningActions = true + var tasks = Anot.pendingActions.splice(0, Anot.pendingActions.length) + for (var i = 0, task; (task = tasks[i++]); ) { + task.update() + delete Anot.uniqActions[task.uuid] + } + Anot.isRunningActions = false +} + +export function propagateChanged(target) { + var list = target.observers + for (var i = 0, el; (el = list[i++]); ) { + el.schedule() //通知action, computed做它们该做的事 + } +} + +//将自己抛到市场上卖 +export function reportObserved(target) { + var action = Anot.trackingAction || null + if (action !== null) { + Anot.track('征收到', target.expr) + action.mapIDs[target.uuid] = target + } +} + +var targetStack = [] + +export function collectDeps(action, getter) { + if (!action.observers) return + var preAction = Anot.trackingAction + if (preAction) { + targetStack.push(preAction) + } + Anot.trackingAction = action + Anot.track('【action】', action.type, action.expr, '开始征收依赖项') + //多个observe持有同一个action + action.mapIDs = {} //重新收集依赖 + var hasError = true, + result + try { + result = getter.call(action) + hasError = false + } finally { + if (hasError) { + Anot.warn('collectDeps fail', getter + '') + action.mapIDs = {} + Anot.trackingAction = preAction + } else { + // 确保它总是为null + Anot.trackingAction = targetStack.pop() + try { + resetDeps(action) + } catch (e) { + Anot.warn(e) + } + } + return result + } +} + +function resetDeps(action) { + var prev = action.observers, + curr = [], + checked = {}, + ids = [] + for (let i in action.mapIDs) { + let dep = action.mapIDs[i] + if (!dep.isAction) { + if (!dep.observers) { + //如果它已经被销毁 + delete action.mapIDs[i] + continue + } + ids.push(dep.uuid) + curr.push(dep) + checked[dep.uuid] = 1 + if (dep.lastAccessedBy === action.uuid) { + continue + } + dep.lastAccessedBy = action.uuid + Anot.Array.ensure(dep.observers, action) + } + } + var ids = ids.sort().join(',') + if (ids === action.ids) { + return + } + action.ids = ids + if (!action.isComputed) { + action.observers = curr + } else { + action.depsCount = curr.length + action.deps = Anot.mix({}, action.mapIDs) + action.depsVersion = {} + for (let i in action.mapIDs) { + let dep = action.mapIDs[i] + action.depsVersion[dep.uuid] = dep.version + } + } + + for (let i = 0, dep; (dep = prev[i++]); ) { + if (!checked[dep.uuid]) { + Anot.Array.remove(dep.observers, action) + } + } +} + +function transaction(action, thisArg, args) { + args = args || [] + var name = 'transaction ' + (action.name || action.displayName || 'noop') + transactionStart(name) + var res = action.apply(thisArg, args) + transactionEnd(name) + return res +} +Anot.transaction = transaction + +export function transactionStart(name) { + Anot.inTransaction += 1 +} + +export function transactionEnd(name) { + if (--Anot.inTransaction === 0) { + Anot.isRunningActions = false + runActions() + } +} diff --git a/src/vtree/clearString.js b/src/vtree/clearString.js new file mode 100644 index 0000000..7841bf5 --- /dev/null +++ b/src/vtree/clearString.js @@ -0,0 +1,52 @@ +/* + * 将要检测的字符串的字符串替换成??123这样的格式 + */ +export var stringNum = 0 +export var stringPool = { + map: {} +} +export var rfill = /\?\?\d+/g +export function dig(a) { + var key = '??' + stringNum++ + stringPool.map[key] = a + return key + ' ' +} +export function fill(a) { + var val = stringPool.map[a] + return val +} +export function clearString(str) { + var array = readString(str) + for (var i = 0, n = array.length; i < n; i++) { + str = str.replace(array[i], dig) + } + return str +} +//https://github.com/RubyLouvre/Anot/issues/1944 +function readString(str, i, ret) { + var end = false, + s = 0, + i = i || 0 + ret = ret || [] + for (var n = str.length; i < n; i++) { + var c = str.charAt(i) + if (!end) { + if (c === "'") { + end = "'" + s = i + } else if (c === '"') { + end = '"' + s = i + } + } else { + if (c === end) { + ret.push(str.slice(s, i + 1)) + end = false + } + } + } + if (end !== false) { + return readString(str, s + 1, ret) + } + return ret +} diff --git a/src/vtree/fromDOM.js b/src/vtree/fromDOM.js new file mode 100644 index 0000000..d527681 --- /dev/null +++ b/src/vtree/fromDOM.js @@ -0,0 +1,78 @@ +import { orphanTag } from './orphanTag' +import { voidTag } from './voidTag' +import { makeOrphan } from './makeOrphan' + +export function fromDOM(dom) { + return [from(dom)] +} + +export function from(node) { + var type = node.nodeName.toLowerCase() + switch (type) { + case '#text': + case '#comment': + return { + nodeName: type, + dom: node, + nodeValue: node.nodeValue + } + default: + var props = markProps(node, node.attributes || []) + var vnode = { + nodeName: type, + dom: node, + isVoidTag: !!voidTag[type], + props: props + } + if(type === 'option'){ + //即便你设置了option.selected = true, + //option.attributes也找不到selected属性 + props.selected = node.selected + } + if (orphanTag[type] || type === 'option') { + makeOrphan(vnode, type, node.text || node.innerHTML) + if (node.childNodes.length === 1) { + vnode.children[0].dom = node.firstChild + } + } else if (!vnode.isVoidTag) { + vnode.children = [] + for (var i = 0, el; el = node.childNodes[i++];) { + var child = from(el) + if (/\S/.test(child.nodeValue)) { + vnode.children.push(child) + } + } + } + return vnode + } +} + +var rformElement = /input|textarea|select/i + +function markProps(node, attrs) { + var ret = {} + for (var i = 0, n = attrs.length; i < n; i++) { + var attr = attrs[i] + if (attr.specified) { + //IE6-9不会将属性名变小写,比如它会将用户的contenteditable变成contentEditable + ret[attr.name.toLowerCase()] = attr.value + } + } + if (rformElement.test(node.nodeName)) { + ret.type = node.type + var a = node.getAttributeNode('value') + if (a && /\S/.test(a.value)) { //IE6,7中无法取得checkbox,radio的value + ret.value = a.value + } + + } + var style = node.style.cssText + if (style) { + ret.style = style + } + //类名 = 去重(静态类名+动态类名+ hover类名? + active类名) + if (ret.type === 'select-one') { + ret.selectedIndex = node.selectedIndex + } + return ret +} \ No newline at end of file diff --git a/src/vtree/fromString.js b/src/vtree/fromString.js new file mode 100644 index 0000000..cab79ef --- /dev/null +++ b/src/vtree/fromString.js @@ -0,0 +1,422 @@ +/** + * ------------------------------------------------------------ + * Anot2.2.6的新式lexer + * 将字符串变成一个虚拟DOM树,方便以后进一步变成模板函数 + * 此阶段只会生成VElement,VText,VComment + * ------------------------------------------------------------ + */ +import { Anot, Cache, config } from '../seed/core' +import { voidTag } from './voidTag' + +import { validateDOMNesting } from './validateDOMNesting' + +var specalTag = { + xmp: 1, + style: 1, + script: 1, + noscript: 1, + textarea: 1, + '#comment': 1, + template: 1 +} +var hiddenTag = { style: 1, script: 1, noscript: 1, template: 1 } +var rcontent = /\S/ //判定里面有没有内容 +var rsp = /\s/ +export function fromString(str) { + return from(str) +} +Anot.lexer = fromString + +var strCache = new Cache(100) + +function from(str) { + var cacheKey = str + var cached = strCache.get(cacheKey) + if (cached) { + return Anot.mix(true, [], cached) + } + + var ret = parse(str, false) + strCache.put(cacheKey, Anot.mix(true, [], ret)) + return ret +} + +/** + * + * + * @param {any} string + * @param {any} getOne 只返回一个节点 + * @returns + */ +function parse(string, getOne) { + getOne = getOne === void 666 || getOne === true + var ret = lexer(string, getOne) + if (getOne) { + return typeof ret[0] === 'string' ? ret[1] : ret[0] + } + return ret +} + +function lexer(string, getOne) { + var tokens = [] + var breakIndex = 9990 + var stack = [] + var origString = string + var origLength = string.length + + stack.last = function() { + return stack[stack.length - 1] + } + var ret = [] + + function addNode(node) { + var p = stack.last() + if (p && p.children) { + p.children.push(node) + } else { + ret.push(node) + } + } + + var lastNode + do { + if (--breakIndex === 0) { + break + } + var arr = getCloseTag(string) + + if (arr) { + //处理关闭标签 + string = string.replace(arr[0], '') + const node = stack.pop() + if (!node) { + throw '是不是有属性值没有用引号括起' + } + //处理下面两种特殊情况: + //1. option会自动移除元素节点,将它们的nodeValue组成新的文本节点 + //2. table会将没有被thead, tbody, tfoot包起来的tr或文本节点,收集到一个新的tbody元素中 + + if (node.nodeName === 'option') { + node.children = [ + { + nodeName: '#text', + nodeValue: getText(node) + } + ] + } else if (node.nodeName === 'table') { + insertTbody(node.children) + } + lastNode = null + if (getOne && ret.length === 1 && !stack.length) { + return [origString.slice(0, origLength - string.length), ret[0]] + } + continue + } + + var arr = getOpenTag(string) + if (arr) { + string = string.replace(arr[0], '') + var node = arr[1] + addNode(node) + var selfClose = !!(node.isVoidTag || specalTag[node.nodeName]) + if (!selfClose) { + //放到这里可以添加孩子 + stack.push(node) + } + if (getOne && selfClose && !stack.length) { + return [origString.slice(0, origLength - string.length), node] + } + lastNode = node + continue + } + + var text = '' + do { + //处理
<<<<<
的情况 + const index = string.indexOf('<') + if (index === 0) { + text += string.slice(0, 1) + string = string.slice(1) + } else { + break + } + } while (string.length) + + //处理
{aaa}
,
xxx{aaa}xxx
,
xxx
{aaa}sss的情况 + const index = string.indexOf('<') //判定它后面是否存在标签 + if (index === -1) { + text = string + string = '' + } else { + const openIndex = string.indexOf(config.openTag) + + if (openIndex !== -1 && openIndex < index) { + if (openIndex !== 0) { + text += string.slice(0, openIndex) + } + var dirString = string.slice(openIndex) + var textDir = parseTextDir(dirString) + text += textDir + string = dirString.slice(textDir.length) + } else { + text += string.slice(0, index) + string = string.slice(index) + } + } + var mayNode = addText(lastNode, text, addNode) + if (mayNode) { + lastNode = mayNode + } + } while (string.length) + return ret +} + +function addText(lastNode, text, addNode) { + if (rcontent.test(text)) { + if (lastNode && lastNode.nodeName === '#text') { + lastNode.nodeValue += text + return lastNode + } else { + lastNode = { + nodeName: '#text', + nodeValue: text + } + addNode(lastNode) + return lastNode + } + } +} + +function parseTextDir(string) { + var closeTag = config.closeTag + var openTag = config.openTag + var closeTagFirst = closeTag.charAt(0) + var closeTagLength = closeTag.length + var state = 'code', + quote, + escape + for (var i = openTag.length, n = string.length; i < n; i++) { + var c = string.charAt(i) + switch (state) { + case 'code': + if (c === '"' || c === "'") { + state = 'string' + quote = c + } else if (c === closeTagFirst) { + //如果遇到} + if (string.substr(i, closeTagLength) === closeTag) { + return string.slice(0, i + closeTagLength) + } + } + break + case 'string': + if (c === '\\' && /"'/.test(string.charAt(i + 1))) { + escape = !escape + } + if (c === quote && !escape) { + state = 'code' + } + break + } + } + throw '找不到界定符' + closeTag +} + +var rtbody = /^(tbody|thead|tfoot)$/ + +function insertTbody(nodes) { + var tbody = false + for (var i = 0, n = nodes.length; i < n; i++) { + var node = nodes[i] + if (rtbody.test(node.nodeName)) { + tbody = false + continue + } + + if (node.nodeName === 'tr') { + if (tbody) { + nodes.splice(i, 1) + tbody.children.push(node) + n-- + i-- + } else { + tbody = { + nodeName: 'tbody', + props: {}, + children: [node] + } + nodes.splice(i, 1, tbody) + } + } else { + if (tbody) { + nodes.splice(i, 1) + tbody.children.push(node) + n-- + i-- + } + } + } +} + +//
{{
}}
+function getCloseTag(string) { + if (string.indexOf(']*)>/) + if (match) { + var tag = match[1] + string = string.slice(3 + tag.length) + return [ + match[0], + { + nodeName: tag + } + ] + } + } + return null +} +var ropenTag = /\<(\w[^\s\/\>]*)/ + +function getOpenTag(string) { + if (string.indexOf('<') === 0) { + var i = string.indexOf('') + if (l === -1) { + thow('注释节点没有闭合 ' + string.slice(0, 100)) + } + var node = { + nodeName: '#comment', + nodeValue: string.slice(4, l) + } + return [string.slice(0, l + 3), node] + } + var match = string.match(ropenTag) //处理元素节点 + if (match) { + var leftContent = match[0], + tag = match[1] + var node = { + nodeName: tag, + props: {}, + children: [] + } + + string = string.replace(leftContent, '') //去掉标签名(rightContent) + try { + var arr = getAttrs(string) //处理属性 + } catch (e) {} + if (arr) { + node.props = arr[1] + string = string.replace(arr[0], '') + leftContent += arr[0] + } + + if (string.charAt(0) === '>') { + //处理开标签的边界符 + leftContent += '>' + string = string.slice(1) + if (voidTag[node.nodeName]) { + node.isVoidTag = true + } + } else if (string.slice(0, 2) === '/>') { + //处理开标签的边界符 + leftContent += '/>' + string = string.slice(2) + node.isVoidTag = true + } + + if (!node.isVoidTag && specalTag[tag]) { + //如果是script, style, xmp等元素 + var closeTag = '' + var j = string.indexOf(closeTag) + var nodeValue = string.slice(0, j) + leftContent += nodeValue + closeTag + node.children.push({ + nodeName: '#text', + nodeValue: nodeValue + }) + if (tag === 'textarea') { + node.props.type = tag + node.props.value = nodeValue + } + } + return [leftContent, node] + } + } +} + +function getText(node) { + var ret = '' + node.children.forEach(function(el) { + if (el.nodeName === '#text') { + ret += el.nodeValue + } else if (el.children && !hiddenTag[el.nodeName]) { + ret += getText(el) + } + }) + return ret +} + +function getAttrs(string) { + var state = 'AttrName', + attrName = '', + attrValue = '', + quote, + escape, + props = {} + for (var i = 0, n = string.length; i < n; i++) { + var c = string.charAt(i) + switch (state) { + case 'AttrName': + if ((c === '/' && string.charAt(i + 1) === '>') || c === '>') { + if (attrName) props[attrName] = attrName + return [string.slice(0, i), props] + } + if (rsp.test(c)) { + if (attrName) { + state = 'AttrEqual' + } + } else if (c === '=') { + if (!attrName) { + throw '必须指定属性名' + } + state = 'AttrQuote' + } else { + attrName += c + } + break + case 'AttrEqual': + if (c === '=') { + state = 'AttrQuote' + } else if (rcontent.test(c)) { + props[attrName] = attrName + attrName = c + state = 'AttrName' + } + break + case 'AttrQuote': + if (c === '"' || c === "'") { + quote = c + state = 'AttrValue' + escape = false + } + break + case 'AttrValue': + if (c === '\\' && /"'/.test(string.charAt(i + 1))) { + escape = !escape + } + if (c === '\n') { + break + } + if (c !== quote) { + attrValue += c + } else if (c === quote && !escape) { + props[attrName] = attrValue + attrName = attrValue = '' + state = 'AttrName' + } + break + } + } + throw '必须关闭标签' +} diff --git a/src/vtree/makeOrphan.js b/src/vtree/makeOrphan.js new file mode 100644 index 0000000..5655780 --- /dev/null +++ b/src/vtree/makeOrphan.js @@ -0,0 +1,51 @@ +/* + * 此模块只用于文本转虚拟DOM, + * 因为在真实浏览器会对我们的HTML做更多处理, + * 如, 添加额外属性, 改变结构 + * 此模块就是用于模拟这些行为 + */ +export function makeOrphan(node, nodeName, innerHTML) { + switch (nodeName) { + case 'style': + case 'script': + case 'noscript': + case 'template': + case 'xmp': + node.children = [ + { + nodeName: '#text', + nodeValue: innerHTML + } + ] + break + case 'textarea': + var props = node.props + props.type = nodeName + props.value = innerHTML + node.children = [ + { + nodeName: '#text', + nodeValue: innerHTML + } + ] + break + case 'option': + node.children = [ + { + nodeName: '#text', + nodeValue: trimHTML(innerHTML) + } + ] + break + } +} + +//专门用于处理option标签里面的标签 +var rtrimHTML = /<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?>|<\/\w+>/gi +function trimHTML(v) { + return String(v) + .replace(rtrimHTML, '') + .trim() +} + +//widget rule duplex validate diff --git a/src/vtree/makeTbody.js b/src/vtree/makeTbody.js new file mode 100644 index 0000000..34f59d4 --- /dev/null +++ b/src/vtree/makeTbody.js @@ -0,0 +1,35 @@ +//只有遇到第一个直接放在table下的tr元素,才会插入新tbody,并收集接下来的其他非tbody, thead, tfoot元素 + +var rtbody = /^(tbody|thead|tfoot)$/ +export function makeTbody(nodes) { + var tbody = false + for (var i = 0, n = nodes.length; i < n; i++) { + var node = nodes[i] + if (rtbody.test(node.nodeName)) { + tbody = false + continue + } + if (node.nodeName === 'tr') { + if (tbody) { + nodes.splice(i, 1) + tbody.children.push(node) + n-- + i-- + } else { + tbody = { + nodeName: 'tbody', + props: {}, + children: [node] + } + nodes.splice(i, 1, tbody) + } + } else { + if (tbody) { + nodes.splice(i, 1) + tbody.children.push(node) + n-- + i-- + } + } + } +} diff --git a/src/vtree/orphanTag.js b/src/vtree/orphanTag.js new file mode 100644 index 0000000..6dc7138 --- /dev/null +++ b/src/vtree/orphanTag.js @@ -0,0 +1,8 @@ +export var orphanTag = { + script: 1, + style: 1, + textarea: 1, + xmp: 1, + noscript: 1, + template: 1 +} \ No newline at end of file diff --git a/src/vtree/validateDOMNesting.js b/src/vtree/validateDOMNesting.js new file mode 100644 index 0000000..296b840 --- /dev/null +++ b/src/vtree/validateDOMNesting.js @@ -0,0 +1,63 @@ +import { Anot, oneObject } from '../seed/core' + +export function validateDOMNesting(parent, child) { + var parentTag = parent.nodeName + var tag = child.nodeName + var parentChild = nestObject[parentTag] + if (parentChild) { + if (parentTag === 'p') { + if (pNestChild[tag]) { + Anot.warn( + 'P element can not add these childlren:\n' + Object.keys(pNestChild) + ) + return false + } + } else if (!parentChild[tag]) { + Anot.warn( + parentTag.toUpperCase() + + 'element only add these children:\n' + + Object.keys(parentChild) + + '\nbut you add ' + + tag.toUpperCase() + + ' !!' + ) + return false + } + } + return true +} + +function makeObject(str) { + return oneObject(str + ',template,#document-fragment,#comment') +} +var pNestChild = oneObject('div,ul,ol,dl,table,h1,h2,h3,h4,h5,h6,form,fieldset') +var tNestChild = makeObject('tr,style,script') +var nestObject = { + p: pNestChild, + // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect + select: makeObject('option,optgroup,#text'), + optgroup: makeObject('option,#text'), + option: makeObject('#text'), + // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intd + // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incaption + // No special behavior since these rules fall back to "in body" mode for + // all except special table nodes which cause bad parsing behavior anyway. + + // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intr + tr: makeObject('th,td,style,script'), + + // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intbody + tbody: tNestChild, + tfoot: tNestChild, + thead: tNestChild, + // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incolgroup + colgroup: makeObject('col'), + // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intable + // table: oneObject('caption,colgroup,tbody,thead,tfoot,style,script,template,#document-fragment'), + // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inhead + head: makeObject( + 'base,basefont,bgsound,link,style,script,meta,title,noscript,noframes' + ), + // https://html.spec.whatwg.org/multipage/semantics.html#the-html-element + html: oneObject('head,body') +} diff --git a/src/vtree/voidTag.js b/src/vtree/voidTag.js new file mode 100644 index 0000000..720b2dc --- /dev/null +++ b/src/vtree/voidTag.js @@ -0,0 +1,21 @@ +export var voidTag = { + area: 1, + base: 1, + basefont: 1, + bgsound: 1, + br: 1, + col: 1, + command: 1, + embed: 1, + frame: 1, + hr: 1, + img: 1, + input: 1, + keygen: 1, + link: 1, + meta: 1, + param: 1, + source: 1, + track: 1, + wbr: 1 +}