This repository has been archived on 2023-08-30. You can view files and clone it, but cannot push or open issues/pull-requests.
bytedo
/
anot
Archived
1
0
Fork 0
master
宇天 2020-08-12 18:25:03 +08:00
commit 90408348bc
97 changed files with 8469 additions and 0 deletions

17
.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
.vscode
node_modules/
dist/
*.sublime-project
*.sublime-workspace
package-lock.json
._*
.Spotlight-V100
.Trashes
.DS_Store
.AppleDouble
.LSOverride

21
LICENSE Normal file
View File

@ -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.

2
README.md Normal file
View File

@ -0,0 +1,2 @@
## Anot.js 迷你mvvm框架
> 基于**司徒正美**的`avalon2.x`版精简修改而来。

25
package.json Normal file
View File

@ -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"
}
}

22
rollup.config.dev.js Normal file
View File

@ -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: []
}
]

22
rollup.config.prod.js Normal file
View File

@ -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()]
}
]

20
src/anot.js Normal file
View File

@ -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

22
src/anot.touch.js Normal file
View File

@ -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

View File

@ -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)
}
}
})

View File

@ -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)

View File

@ -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
})

126
src/directives/css.js Normal file
View File

@ -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
}

View File

@ -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)
}
})

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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'
//处理 <input ms-duplex='@aaa|limitBy(8)'/>{{@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
})
}
}

View File

@ -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
}

View File

@ -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 }

10
src/directives/expr.js Normal file
View File

@ -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
}
})

347
src/directives/for.js Normal file
View File

@ -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 = [
'<div>',
this.fragment,
'<!--',
this.signature,
'--></div>'
].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)
}
}

25
src/directives/html.js Normal file
View File

@ -0,0 +1,25 @@
import { Anot } from '../seed/core'
Anot.directive('html', {
update: function(vdom, value) {
this.beforeDispose()
this.innerRender = Anot.scan(
'<div class="ms-html-container">' + value + '</div>',
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
})

60
src/directives/if.js Normal file
View File

@ -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
}

View File

@ -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

20
src/directives/modern.js Normal file
View File

@ -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'

54
src/directives/on.js Normal file
View File

@ -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)
}
})

154
src/directives/rules.js Normal file
View File

@ -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
}
}
})

5
src/directives/skip.js Normal file
View File

@ -0,0 +1,5 @@
import { Anot } from '../seed/core'
Anot.directive('skip', {
delay: true
})

25
src/directives/text.js Normal file
View File

@ -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)
}
}
})

263
src/directives/validate.js Normal file
View File

@ -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 这是为了防止IE68添加子属性时添加的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,请下载并<script src=url>引入\nhttps://github.com/RubyLouvre/Anot/blob/master/test/promise.js'
)
}
/* istanbul ignore if */
if (elem.disabled) return
var rules = field.vdom.rules
var ngs = [],
isOk = true
if (!(rules.norequired && value === '')) {
for (var ruleName in rules) {
var ruleValue = rules[ruleName]
if (ruleValue === false) continue
var hook = Anot.validators[ruleName]
var resolve
promises.push(
new Promise(function(a, b) {
resolve = a
})
)
var next = function(a) {
var reason = {
element: elem,
data: field.data,
message:
elem.getAttribute('data-' + ruleName + '-message') ||
elem.getAttribute('data-message') ||
hook.message,
validateRule: ruleName,
getMessage: getMessage
}
if (a) {
resolve(true)
} else {
isOk = false
ngs.push(reason)
resolve(false)
}
}
field.data = {}
field.data[ruleName] = ruleValue
hook.get(value, field, next)
}
}
//如果promises不为空说明经过验证拦截器
return Promise.all(promises).then(function(array) {
if (!isValidateAll) {
var validator = field.validator
if (isOk) {
validator.onSuccess.call(
elem,
[
{
data: field.data,
element: elem
}
],
event
)
} else {
validator.onError.call(elem, ngs, event)
}
validator.onComplete.call(elem, ngs, event)
}
return ngs
})
}
})
//https://github.com/RubyLouvre/Anot/issues/1977
function getValidate(dom) {
while (dom.tagName !== 'FORM') {
dom = dom.parentNode
}
return dom._ms_validate_
}
function validateAllInSubmitFn(e) {
e.preventDefault()
var v = getValidate(e.target)
if (v && v.onManual) {
v.onManual()
}
}
function collectFeild(nodes, fields, validator) {
for (var i = 0, vdom; (vdom = nodes[i++]); ) {
var duplex = vdom.rules && vdom.duplex
if (duplex) {
fields.push(duplex)
bindValidateEvent(duplex, validator)
} else if (vdom.children) {
collectFeild(vdom.children, fields, validator)
} else if (Array.isArray(vdom)) {
collectFeild(vdom, fields, validator)
}
}
}
function findValidator(e) {
var dom = e.target
var duplex = dom._ms_duplex_
var vdom = (duplex || {}).vdom
if (duplex && vdom.rules && !duplex.validator) {
var msValidator = getValidate(dom)
if (msValidator && Anot.Array.ensure(msValidator.fields, duplex)) {
bindValidateEvent(duplex, msValidator)
}
}
}
function singleValidate(e) {
var dom = e.target
var duplex = dom._ms_duplex_
var msValidator = getValidate(e.target)
msValidator && msValidator.validate(duplex, 0, e)
}
function bindValidateEvent(field, validator) {
var node = field.dom
if (field.validator) {
return
}
field.validator = validator
/* istanbul ignore if */
if (validator.validateInKeyup && (!field.isChanged && !field.debounceTime)) {
Anot.bind(node, 'keyup', singleValidate)
}
/* istanbul ignore if */
if (validator.validateInBlur) {
Anot.bind(node, 'blur', singleValidate)
}
/* istanbul ignore if */
if (validator.resetInFocus) {
Anot.bind(node, 'focus', function(e) {
var dom = e.target
var field = dom._ms_duplex_
var validator = getValidate(e.target)
validator && validator.onReset.call(dom, e, field)
})
}
}
var rformat = /\\?{{([^{}]+)\}}/gm
function getMessage() {
var data = this.data || {}
return this.message.replace(rformat, function(_, name) {
return data[name] == null ? '' : data[name]
})
}
valiDir.defaults = {
validate: valiDir.validate,
onError: Anot.noop,
onSuccess: Anot.noop,
onComplete: Anot.noop,
onManual: Anot.noop,
onReset: Anot.noop,
onValidateAll: Anot.noop,
validateInBlur: true, //@config {Boolean} true在blur事件中进行验证,触发onSuccess, onError, onComplete回调
validateInKeyup: true, //@config {Boolean} true在keyup事件中进行验证,触发onSuccess, onError, onComplete回调
validateAllInSubmit: true, //@config {Boolean} true在submit事件中执行onValidateAll回调
resetInFocus: true, //@config {Boolean} true在focus事件中执行onReset回调,
deduplicateInValidateAll: false //@config {Boolean} false在validateAll回调中对reason数组根据元素节点进行去重
}

73
src/directives/visible.js Normal file
View File

@ -0,0 +1,73 @@
import { Anot } from '../seed/core'
import '../effect/index'
var none = 'none'
function parseDisplay(elem, val) {
//用于取得此类标签的默认display值
var doc = elem.ownerDocument
var nodeName = elem.nodeName
var key = '_' + nodeName
if (!parseDisplay[key]) {
var temp = doc.body.appendChild(doc.createElement(nodeName))
val = Anot.css(temp, 'display')
doc.body.removeChild(temp)
if (val === none) {
val = 'block'
}
parseDisplay[key] = val
}
return parseDisplay[key]
}
Anot.parseDisplay = parseDisplay
Anot.directive('visible', {
diff: function(newVal, oldVal) {
var n = !!newVal
if (oldVal === void 0 || n !== oldVal) {
this.value = n
return true
}
},
ready: true,
update: function(vdom, show) {
var dom = vdom.dom
if (dom && dom.nodeType === 1) {
var display = dom.style.display
var value
if (show) {
if (display === none) {
value = vdom.displayValue
if (!value) {
dom.style.display = ''
if (dom.style.cssText === '') {
dom.removeAttribute('style')
}
}
}
if (
dom.style.display === '' &&
Anot(dom).css('display') === none &&
// fix firefox BUG,必须挂到页面上
Anot.contains(dom.ownerDocument, dom)
) {
value = parseDisplay(dom)
}
} else {
if (display !== none) {
value = none
vdom.displayValue = display
}
}
var cb = function() {
if (value !== void 0) {
dom.style.display = value
}
}
Anot.applyEffect(dom, vdom, {
hook: show ? 'onEnterDone' : 'onLeaveDone',
cb: cb
})
}
}
})

45
src/dom/attr/modern.js Normal file
View File

@ -0,0 +1,45 @@
import { Anot, window, document } from '../../seed/core'
import { propMap } from './propMap'
var rsvg = /^\[object SVG\w*Element\]$/
export function updateAttrs(node, attrs) {
for (var attrName in attrs) {
var val = attrs[attrName]
/* istanbul ignore if*/
if (attrName.indexOf('data-') === 0 || rsvg.test(node)) {
node.setAttribute(attrName, val)
} else {
var propName = propMap[attrName] || attrName
if (typeof node[propName] === 'boolean') {
//布尔属性必须使用el.xxx = true|false方式设值
//如果为false, IE全系列下相当于setAttribute(xxx,''),
//会影响到样式,需要进一步处理
node[propName] = !!val
}
if (val === false) {
node.removeAttribute(attrName)
continue
}
//SVG只能使用setAttribute(xxx, yyy), VML只能使用node.xxx = yyy ,
//HTML的固有属性必须node.xxx = yyy
var isInnate = attrName in node.cloneNode(false)
if (isInnate) {
node[propName] = val + ''
} else {
node.setAttribute(attrName, val)
}
}
}
}
Anot.parseJSON = JSON.parse
Anot.fn.attr = function(name, value) {
if (arguments.length === 2) {
this[0].setAttribute(name, value)
return this
} else {
return this[0].getAttribute(name)
}
}

42
src/dom/attr/propMap.js Normal file
View File

@ -0,0 +1,42 @@
export var propMap = {} //不规则的属性名映射
//防止压缩时出错
'accept-charset,acceptCharset|char,ch|charoff,chOff|class,className|for,htmlFor|http-equiv,httpEquiv'.replace(
/[^\|]+/g,
function(a) {
var k = a.split(',')
propMap[k[0]] = k[1]
}
)
/*
contenteditable不是布尔属性
http://www.zhangxinxu.com/wordpress/2016/01/contenteditable-plaintext-only/
contenteditable=''
contenteditable='events'
contenteditable='caret'
contenteditable='plaintext-only'
contenteditable='true'
contenteditable='false'
*/
var bools = [
'autofocus,autoplay,async,allowTransparency,checked,controls',
'declare,disabled,defer,defaultChecked,defaultSelected,',
'isMap,loop,multiple,noHref,noResize,noShade',
'open,readOnly,selected'
].join(',')
bools.replace(/\w+/g, function(name) {
propMap[name.toLowerCase()] = name
})
var anomaly = [
'accessKey,bgColor,cellPadding,cellSpacing,codeBase,codeType,colSpan',
'dateTime,defaultValue,contentEditable,frameBorder,longDesc,maxLength,' +
'marginWidth,marginHeight,rowSpan,tabIndex,useMap,vSpace,valueType,vAlign'
].join(',')
anomaly.replace(/\w+/g, function(name) {
propMap[name.toLowerCase()] = name
})
//module.exports = propMap

32
src/dom/class/modern.js Normal file
View File

@ -0,0 +1,32 @@
import { Anot, rnowhite, rword } from '../../seed/core'
'add,remove'.replace(rword, function(method) {
Anot.fn[method + 'Class'] = function(cls) {
var el = this[0] || {}
//https://developer.mozilla.org/zh-CN/docs/Mozilla/Firefox/Releases/26
if (cls && typeof cls === 'string' && el.nodeType === 1) {
cls.replace(rnowhite, function(c) {
el.classList[method](c)
})
}
return this
}
})
Anot.shadowCopy(Anot.fn, {
hasClass: function(cls) {
var el = this[0] || {}
//IE10+, chrome8+, firefox3.6+, safari5.1+,opera11.5+支持classList,
//chrome24+,firefox26+支持classList2.0
return el.nodeType === 1 && el.classList.contains(cls)
},
toggleClass: function(value, stateVal) {
var isBool = typeof stateVal === 'boolean'
var me = this
String(value).replace(rnowhite, function(c) {
var state = isBool ? stateVal : !me.hasClass(c)
me[state ? 'addClass' : 'removeClass'](c)
})
return this
}
})

49
src/dom/css/modern.js Normal file
View File

@ -0,0 +1,49 @@
import { Anot } from '../../seed/core'
import { getWindow } from './share'
Anot.fn.offset = function() {
//取得距离页面左右角的坐标
var node = this[0]
try {
var rect = node.getBoundingClientRect()
// Make sure element is not hidden (display: none) or disconnected
// https://github.com/jquery/jquery/pull/2043/files#r23981494
if (rect.width || rect.height || node.getClientRects().length) {
var doc = node.ownerDocument
var root = doc.documentElement
var win = doc.defaultView
return {
top: rect.top + win.pageYOffset - root.clientTop,
left: rect.left + win.pageXOffset - root.clientLeft
}
}
} catch (e) {
return {
left: 0,
top: 0
}
}
}
Anot.each(
{
scrollLeft: 'pageXOffset',
scrollTop: 'pageYOffset'
},
function(method, prop) {
Anot.fn[method] = function(val) {
var node = this[0] || {}
var win = getWindow(node)
var top = method === 'scrollTop'
if (!arguments.length) {
return win ? win[prop] : node[method]
} else {
if (win) {
win.scrollTo(!top ? val : win[prop], top ? val : win[prop])
} else {
node[method] = val
}
}
}
}
)

274
src/dom/css/share.js Normal file
View File

@ -0,0 +1,274 @@
import { Anot, oneObject, cssHooks, window } from '../../seed/core'
var cssMap = oneObject('float', 'cssFloat')
export { cssMap, cssHooks }
Anot.cssNumber = oneObject(
'animationIterationCount,columnCount,order,flex,flexGrow,flexShrink,fillOpacity,fontWeight,lineHeight,opacity,orphans,widows,zIndex,zoom'
)
var prefixes = ['', '-webkit-', '-o-', '-moz-', '-ms-']
/* istanbul ignore next */
Anot.cssName = function(name, host, camelCase) {
if (cssMap[name]) {
return cssMap[name]
}
host = host || Anot.root.style || {}
for (var i = 0, n = prefixes.length; i < n; i++) {
camelCase = Anot.camelize(prefixes[i] + name)
if (camelCase in host) {
return (cssMap[name] = camelCase)
}
}
return null
}
/* istanbul ignore next */
Anot.css = function(node, name, value, fn) {
//读写删除元素节点的样式
if (node instanceof Anot) {
node = node[0]
}
if (node.nodeType !== 1) {
return
}
var prop = Anot.camelize(name)
name = Anot.cssName(prop) || /* istanbul ignore next*/ prop
if (value === void 0 || typeof value === 'boolean') {
//获取样式
fn = cssHooks[prop + ':get'] || cssHooks['@:get']
if (name === 'background') {
name = 'backgroundColor'
}
var val = fn(node, name)
return value === true ? parseFloat(val) || 0 : val
} else if (value === '') {
//请除样式
node.style[name] = ''
} else {
//设置样式
if (value == null || value !== value) {
return
}
if (isFinite(value) && !Anot.cssNumber[prop]) {
value += 'px'
}
fn = cssHooks[prop + ':set'] || cssHooks['@:set']
fn(node, name, value)
}
}
/* istanbul ignore next */
Anot.fn.css = function(name, value) {
if (Anot.isPlainObject(name)) {
for (var i in name) {
Anot.css(this, i, name[i])
}
} else {
var ret = Anot.css(this, name, value)
}
return ret !== void 0 ? ret : this
}
/* istanbul ignore next */
Anot.fn.position = function() {
var offsetParent,
offset,
elem = this[0],
parentOffset = {
top: 0,
left: 0
}
if (!elem) {
return parentOffset
}
if (this.css('position') === 'fixed') {
offset = elem.getBoundingClientRect()
} else {
offsetParent = this.offsetParent() //得到真正的offsetParent
offset = this.offset() // 得到正确的offsetParent
if (offsetParent[0].tagName !== 'HTML') {
parentOffset = offsetParent.offset()
}
parentOffset.top += Anot.css(offsetParent[0], 'borderTopWidth', true)
parentOffset.left += Anot.css(offsetParent[0], 'borderLeftWidth', true)
// Subtract offsetParent scroll positions
parentOffset.top -= offsetParent.scrollTop()
parentOffset.left -= offsetParent.scrollLeft()
}
return {
top: offset.top - parentOffset.top - Anot.css(elem, 'marginTop', true),
left: offset.left - parentOffset.left - Anot.css(elem, 'marginLeft', true)
}
}
/* istanbul ignore next */
Anot.fn.offsetParent = function() {
var offsetParent = this[0].offsetParent
while (offsetParent && Anot.css(offsetParent, 'position') === 'static') {
offsetParent = offsetParent.offsetParent
}
return Anot(offsetParent || Anot.root)
}
/* istanbul ignore next */
cssHooks['@:set'] = function(node, name, value) {
try {
//node.style.width = NaN;node.style.width = 'xxxxxxx';
//node.style.width = undefine 在旧式IE下会抛异常
node.style[name] = value
} catch (e) {}
}
/* istanbul ignore next */
cssHooks['@:get'] = function(node, name) {
if (!node || !node.style) {
throw new Error('getComputedStyle要求传入一个节点 ' + node)
}
var ret,
styles = window.getComputedStyle(node, null)
if (styles) {
ret = name === 'filter' ? styles.getPropertyValue(name) : styles[name]
if (ret === '') {
ret = node.style[name] //其他浏览器需要我们手动取内联样式
}
}
return ret
}
cssHooks['opacity:get'] = function(node) {
var ret = cssHooks['@:get'](node, 'opacity')
return ret === '' ? '1' : ret
}
'top,left'.replace(Anot.rword, function(name) {
cssHooks[name + ':get'] = function(node) {
var computed = cssHooks['@:get'](node, name)
return /px$/.test(computed) ? computed : Anot(node).position()[name] + 'px'
}
})
var cssShow = {
position: 'absolute',
visibility: 'hidden',
display: 'block'
}
var rdisplayswap = /^(none|table(?!-c[ea]).+)/
/* istanbul ignore next */
function showHidden(node, array) {
//http://www.cnblogs.com/rubylouvre/archive/2012/10/27/2742529.html
if (node.offsetWidth <= 0) {
//opera.offsetWidth可能小于0
if (rdisplayswap.test(cssHooks['@:get'](node, 'display'))) {
var obj = {
node: node
}
for (var name in cssShow) {
obj[name] = node.style[name]
node.style[name] = cssShow[name]
}
array.push(obj)
}
var parent = node.parentNode
if (parent && parent.nodeType === 1) {
showHidden(parent, array)
}
}
}
/* istanbul ignore next*/
Anot.each(
{
Width: 'width',
Height: 'height'
},
function(name, method) {
var clientProp = 'client' + name,
scrollProp = 'scroll' + name,
offsetProp = 'offset' + name
cssHooks[method + ':get'] = function(node, which, override) {
var boxSizing = -4
if (typeof override === 'number') {
boxSizing = override
}
which = name === 'Width' ? ['Left', 'Right'] : ['Top', 'Bottom']
var ret = node[offsetProp] // border-box 0
if (boxSizing === 2) {
// margin-box 2
return (
ret +
Anot.css(node, 'margin' + which[0], true) +
Anot.css(node, 'margin' + which[1], true)
)
}
if (boxSizing < 0) {
// padding-box -2
ret =
ret -
Anot.css(node, 'border' + which[0] + 'Width', true) -
Anot.css(node, 'border' + which[1] + 'Width', true)
}
if (boxSizing === -4) {
// content-box -4
ret =
ret -
Anot.css(node, 'padding' + which[0], true) -
Anot.css(node, 'padding' + which[1], true)
}
return ret
}
cssHooks[method + '&get'] = function(node) {
var hidden = []
showHidden(node, hidden)
var val = cssHooks[method + ':get'](node)
for (var i = 0, obj; (obj = hidden[i++]); ) {
node = obj.node
for (var n in obj) {
if (typeof obj[n] === 'string') {
node.style[n] = obj[n]
}
}
}
return val
}
Anot.fn[method] = function(value) {
//会忽视其display
var node = this[0]
if (arguments.length === 0) {
if (node.setTimeout) {
//取得窗口尺寸
return (
node['inner' + name] ||
node.document.documentElement[clientProp] ||
node.document.body[clientProp]
) //IE6下前两个分别为undefined,0
}
if (node.nodeType === 9) {
//取得页面尺寸
var doc = node.documentElement
//FF chrome html.scrollHeight< body.scrollHeight
//IE 标准模式 : html.scrollHeight> body.scrollHeight
//IE 怪异模式 : html.scrollHeight 最大等于可视窗口多一点?
return Math.max(
node.body[scrollProp],
doc[scrollProp],
node.body[offsetProp],
doc[offsetProp],
doc[clientProp]
)
}
return cssHooks[method + '&get'](node)
} else {
return this.css(method, value)
}
}
Anot.fn['inner' + name] = function() {
return cssHooks[method + ':get'](this[0], void 0, -2)
}
Anot.fn['outer' + name] = function(includeMargin) {
return cssHooks[method + ':get'](
this[0],
void 0,
includeMargin === true ? 2 : 0
)
}
}
)
export function getWindow(node) {
return node.window || node.defaultView || node.parentWindow || false
}

View File

@ -0,0 +1,37 @@
//http://www.feiesoft.com/html/events.html
//http://segmentfault.com/q/1010000000687977/a-1020000000688757
export var canBubbleUp = {
click: true,
dblclick: true,
keydown: true,
keypress: true,
keyup: true,
mousedown: true,
mousemove: true,
mouseup: true,
mouseover: true,
mouseout: true,
wheel: true,
mousewheel: true,
input: true,
change: true,
beforeinput: true,
compositionstart: true,
compositionupdate: true,
compositionend: true,
select: true,
//http://blog.csdn.net/lee_magnum/article/details/17761441
cut: true,
copy: true,
paste: true,
beforecut: true,
beforecopy: true,
beforepaste: true,
focusin: true,
focusout: true,
DOMFocusIn: true,
DOMFocusOut: true,
DOMActivate: true,
dragend: true,
datasetchanged: true
}

24
src/dom/event/modern.js Normal file
View File

@ -0,0 +1,24 @@
import { Anot, document } from '../../seed/core'
import { avEvent } from './share'
export { avEvent }
/* istanbul ignore next */
Anot._nativeBind = function(el, type, fn, capture) {
el.addEventListener(type, fn, !!capture)
}
/* istanbul ignore next */
Anot._nativeUnBind = function(el, type, fn, a) {
el.removeEventListener(type, fn, !!a)
}
/* istanbul ignore next */
Anot.fireDom = function(elem, type, opts) {
/* istanbul ignore else */
if (document.createEvent) {
var hackEvent = document.createEvent('Events')
hackEvent.initEvent(type, true, true, opts)
Anot.shadowCopy(hackEvent, opts)
elem.dispatchEvent(hackEvent)
}
}

313
src/dom/event/share.js Normal file
View File

@ -0,0 +1,313 @@
import {
Anot,
_slice,
eventHooks,
modern,
window,
document,
root,
getShortID
} from '../../seed/core'
import { canBubbleUp } from './canBubbleUp'
/* istanbul ignore if */
var hackSafari = Anot.modern && document.ontouchstart
//添加fn.bind, fn.unbind, bind, unbind
Anot.fn.bind = function(type, fn, phase) {
if (this[0]) {
//此方法不会链
return Anot.bind(this[0], type, fn, phase)
}
}
Anot.fn.unbind = function(type, fn, phase) {
if (this[0]) {
var args = _slice.call(arguments)
args.unshift(this[0])
Anot.unbind.apply(0, args)
}
return this
}
/*绑定事件*/
Anot.bind = function(elem, type, fn) {
if (elem.nodeType === 1) {
var value = elem.getAttribute('Anot-events') || ''
//如果是使用ms-on-*绑定的回调,其uuid格式为e12122324,
//如果是使用bind方法绑定的回调,其uuid格式为_12
var uuid = getShortID(fn)
var hook = eventHooks[type]
/* istanbul ignore if */
if (type === 'click' && hackSafari) {
elem.addEventListener('click', Anot.noop)
}
/* istanbul ignore if */
if (hook) {
type = hook.type || type
if (hook.fix) {
fn = hook.fix(elem, fn)
fn.uuid = uuid
}
}
var key = type + ':' + uuid
Anot.eventListeners[fn.uuid] = fn
/* istanbul ignore if */
if (value.indexOf(type + ':') === -1) {
//同一种事件只绑定一次
if (canBubbleUp[type] || (Anot.modern && focusBlur[type])) {
delegateEvent(type)
} else {
Anot._nativeBind(elem, type, dispatch)
}
}
var keys = value.split(',')
/* istanbul ignore if */
if (keys[0] === '') {
keys.shift()
}
if (keys.indexOf(key) === -1) {
keys.push(key)
setEventId(elem, keys.join(','))
//将令牌放进Anot-events属性中
}
return fn
} else {
/* istanbul ignore next */
function cb(e) {
fn.call(elem, new avEvent(e))
}
Anot._nativeBind(elem, type, cb)
return cb
}
}
function setEventId(node, value) {
node.setAttribute('Anot-events', value)
}
/* istanbul ignore next */
Anot.unbind = function(elem, type, fn) {
if (elem.nodeType === 1) {
var value = elem.getAttribute('Anot-events') || ''
switch (arguments.length) {
case 1:
Anot._nativeUnBind(elem, type, dispatch)
elem.removeAttribute('Anot-events')
break
case 2:
value = value
.split(',')
.filter(function(str) {
return str.indexOf(type + ':') === -1
})
.join(',')
setEventId(elem, value)
break
default:
var search = type + ':' + fn.uuid
value = value
.split(',')
.filter(function(str) {
return str !== search
})
.join(',')
setEventId(elem, value)
delete Anot.eventListeners[fn.uuid]
break
}
} else {
Anot._nativeUnBind(elem, type, fn)
}
}
var typeRegExp = {}
function collectHandlers(elem, type, handlers) {
var value = elem.getAttribute('Anot-events')
if (value && (elem.disabled !== true || type !== 'click')) {
var uuids = []
var reg =
typeRegExp[type] ||
(typeRegExp[type] = new RegExp('\\b' + type + '\\:([^,\\s]+)', 'g'))
value.replace(reg, function(a, b) {
uuids.push(b)
return a
})
if (uuids.length) {
handlers.push({
elem: elem,
uuids: uuids
})
}
}
elem = elem.parentNode
var g = Anot.gestureEvents || {}
if (elem && elem.getAttribute && (canBubbleUp[type] || g[type])) {
collectHandlers(elem, type, handlers)
}
}
var rhandleHasVm = /^e/
function dispatch(event) {
event = new avEvent(event)
var type = event.type
var elem = event.target
var handlers = []
collectHandlers(elem, type, handlers)
var i = 0,
j,
uuid,
handler
while ((handler = handlers[i++]) && !event.cancelBubble) {
var host = (event.currentTarget = handler.elem)
j = 0
while ((uuid = handler.uuids[j++])) {
if (event.stopImmediate) {
break
}
var fn = Anot.eventListeners[uuid]
if (fn) {
var vm = rhandleHasVm.test(uuid) ? handler.elem._ms_context_ : 0
if (vm && vm.$hashcode === false) {
return Anot.unbind(elem, type, fn)
}
var ret = fn.call(vm || elem, event)
if (ret === false) {
event.preventDefault()
event.stopPropagation()
}
}
}
}
}
var focusBlur = {
focus: true,
blur: true
}
function delegateEvent(type) {
var value = root.getAttribute('delegate-events') || ''
if (value.indexOf(type) === -1) {
//IE6-8会多次绑定同种类型的同一个函数,其他游览器不会
var arr = value.match(Anot.rword) || []
arr.push(type)
root.setAttribute('delegate-events', arr.join(','))
Anot._nativeBind(root, type, dispatch, !!focusBlur[type])
}
}
var eventProto = {
webkitMovementY: 1,
webkitMovementX: 1,
keyLocation: 1,
fixEvent: function() {},
preventDefault: function() {
var e = this.originalEvent || {}
e.returnValue = this.returnValue = false
if (modern && e.preventDefault) {
e.preventDefault()
}
},
stopPropagation: function() {
var e = this.originalEvent || {}
e.cancelBubble = this.cancelBubble = true
if (modern && e.stopPropagation) {
e.stopPropagation()
}
},
stopImmediatePropagation: function() {
this.stopPropagation()
this.stopImmediate = true
},
toString: function() {
return '[object Event]' //#1619
}
}
export function avEvent(event) {
if (event.originalEvent) {
return event
}
for (var i in event) {
if (!eventProto[i]) {
this[i] = event[i]
}
}
if (!this.target) {
this.target = event.srcElement
}
var target = this.target
this.fixEvent()
this.timeStamp = new Date() - 0
this.originalEvent = event
}
avEvent.prototype = eventProto
//针对firefox, chrome修正mouseenter, mouseleave
/* istanbul ignore if */
if (!('onmouseenter' in root)) {
Anot.each(
{
mouseenter: 'mouseover',
mouseleave: 'mouseout'
},
function(origType, fixType) {
eventHooks[origType] = {
type: fixType,
fix: function(elem, fn) {
return function(e) {
var t = e.relatedTarget
if (!t || (t !== elem && !(elem.compareDocumentPosition(t) & 16))) {
delete e.type
e.type = origType
return fn.apply(this, arguments)
}
}
}
}
}
)
}
//针对IE9+, w3c修正animationend
Anot.each(
{
AnimationEvent: 'animationend',
WebKitAnimationEvent: 'webkitAnimationEnd'
},
function(construct, fixType) {
if (window[construct] && !eventHooks.animationend) {
eventHooks.animationend = {
type: fixType
}
}
}
)
/* istanbul ignore if */
if (!('onmousewheel' in document)) {
/* IE6-11 chrome mousewheel wheelDetla -120 120
firefox DOMMouseScroll detail 下3 -3
firefox wheel detlaY 下3 -3
IE9-11 wheel deltaY 下40 -40
chrome wheel deltaY 下100 -100 */
var fixWheelType = document.onwheel !== void 0 ? 'wheel' : 'DOMMouseScroll'
var fixWheelDelta = fixWheelType === 'wheel' ? 'deltaY' : 'detail'
eventHooks.mousewheel = {
type: fixWheelType,
fix: function(elem, fn) {
return function(e) {
var delta = e[fixWheelDelta] > 0 ? -120 : 120
e.wheelDelta = ~~elem._ms_wheel_ + delta
elem._ms_wheel_ = e.wheelDeltaY = e.wheelDelta
e.wheelDeltaX = 0
if (Object.defineProperty) {
Object.defineProperty(e, 'type', {
value: 'mousewheel'
})
}
return fn.apply(this, arguments)
}
}
}
}

58
src/dom/html/index.js Normal file
View File

@ -0,0 +1,58 @@
import { Anot, Cache, document, createFragment } from '../../seed/core'
import { fromString } from '../../vtree/fromString'
export { Anot }
var rhtml = /<|&#?\w+;/
var htmlCache = new Cache(128)
var rxhtml = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi
Anot.parseHTML = function(html) {
var fragment = createFragment()
//处理非字符串
if (typeof html !== 'string') {
return fragment
}
//处理非HTML字符串
if (!rhtml.test(html)) {
return document.createTextNode(html)
}
html = html.replace(rxhtml, '<$1></$2>').trim()
var hasCache = htmlCache.get(html)
if (hasCache) {
return Anot.cloneNode(hasCache)
}
var vnodes = fromString(html)
for (var i = 0, el; (el = vnodes[i++]); ) {
var child = Anot.vdom(el, 'toDOM')
fragment.appendChild(child)
}
if (html.length < 1024) {
htmlCache.put(html, fragment)
}
return fragment
}
Anot.innerHTML = function(node, html) {
var parsed = Anot.parseHTML(html)
this.clearHTML(node)
node.appendChild(parsed)
}
//https://github.com/karloespiritu/escapehtmlent/blob/master/index.js
Anot.unescapeHTML = function(html) {
return String(html)
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
}
Anot.clearHTML = function(node) {
/* istanbul ignore next */
while (node.lastChild) {
node.removeChild(node.lastChild)
}
return node
}

13
src/dom/modern.js Normal file
View File

@ -0,0 +1,13 @@
/*******************************************************************
* DOM Api *
* shim,class,data,css,val,html,event,ready *
********************************************************************/
import './shim/modern'
import './class/modern'
import './attr/modern'
import './css/modern'
import './val/modern'
import './html/index'
import './event/modern'
import './ready/modern'

2
src/dom/rcheckedType.js Normal file
View File

@ -0,0 +1,2 @@
export var rcheckedType = /^(?:checkbox|radio)$/

38
src/dom/ready/modern.js Normal file
View File

@ -0,0 +1,38 @@
import { Anot, window, document, root, inBrowser } from '../../seed/core'
var readyList = []
export function fireReady(fn) {
Anot.isReady = true
while ((fn = readyList.shift())) {
fn(Anot)
}
}
Anot.ready = function(fn) {
readyList.push(fn)
if (Anot.isReady) {
fireReady()
}
}
Anot.ready(function() {
Anot.scan && Anot.scan(document.body)
})
/* istanbul ignore next */
function bootstrap() {
if (document.readyState === 'complete') {
setTimeout(fireReady) //如果在domReady之外加载
} else {
//必须传入三个参数否则在firefox4-26中报错
//caught exception: [Exception... "Not enough arguments" nsresult: "0x80570001 (NS_ERROR_XPC_NOT_ENOUGH_ARGS)"
document.addEventListener('DOMContentLoaded', fireReady, false)
}
Anot.bind(window, 'load', fireReady)
}
if (inBrowser) {
bootstrap()
}

View File

@ -0,0 +1,10 @@
/* istanbul ignore next */
export function fixContains(root, el) {
try {
//IE6-8,游离于DOM树外的文本节点访问parentNode有时会抛错
while ((el = el.parentNode)) {
if (el === root) return true
}
} catch (e) {}
return false
}

43
src/dom/shim/modern.js Normal file
View File

@ -0,0 +1,43 @@
//safari5+是把contains方法放在Element.prototype上而不是Node.prototype
import { Anot, document, window, root } from '../../seed/core'
import { fixContains } from './fixContains'
export { Anot }
Anot.contains = fixContains
Anot.cloneNode = function(a) {
return a.cloneNode(true)
}
if (Anot.modern) {
if (!document.contains) {
Node.prototype.contains = function(child) {
//IE6-8没有Node对象
return fixContains(this, child)
}
}
function fixFF(prop, cb) {
//firefox12 http://caniuse.com/#search=outerHTML
if (!(prop in root)) {
HTMLElement.prototype.__defineGetter__(prop, cb)
}
}
fixFF('outerHTML', function() {
//https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/children
var div = document.createElement('div')
div.appendChild(this)
return div.innerHTML
})
fixFF('children', function() {
var children = []
for (var i = 0, el; (el = this.childNodes[i++]); ) {
if (el.nodeType === 1) {
children.push(el)
}
}
return children
})
fixFF('innerText', function() {
return this.textContent
})
}

View File

@ -0,0 +1,8 @@
import { rcheckedType } from '../rcheckedType'
export function getDuplexType(elem) {
var ret = elem.tagName.toLowerCase()
if (ret === 'input') {
return rcheckedType.test(elem.type) ? 'checked' : elem.type
}
return ret
}

64
src/dom/val/modern.js Normal file
View File

@ -0,0 +1,64 @@
import { Anot } from '../../seed/core'
import { getDuplexType } from './getDuplexType'
export { getDuplexType }
var valHooks = {
'select:get': function self(node, ret, index, singleton) {
var nodes = node.children,
value,
index = ret ? index : node.selectedIndex
singleton = ret ? singleton : node.type === 'select-one' || index < 0
ret = ret || []
for (var i = 0, el; (el = nodes[i++]); ) {
if (!el.disabled) {
switch (el.nodeName.toLowerCase()) {
case 'option':
if (el.selected || el.index === index) {
value = el.value
if (singleton) {
return value
} else {
ret.push(value)
}
}
break
case 'optgroup':
value = self(el, ret, index, singleton)
if (typeof value === 'string') {
return value
}
break
}
}
}
return singleton ? null : ret
},
'select:set': function(node, values, optionSet) {
values = [].concat(values) //强制转换为数组
for (var i = 0, el; (el = node.options[i++]); ) {
if ((el.selected = values.indexOf(el.value) > -1)) {
optionSet = true
}
}
if (!optionSet) {
node.selectedIndex = -1
}
}
}
Anot.fn.val = function(value) {
var node = this[0]
if (node && node.nodeType === 1) {
var get = arguments.length === 0
var access = get ? ':get' : ':set'
var fn = valHooks[getDuplexType(node) + access]
if (fn) {
var val = fn(node, value)
} else if (get) {
return (node.value || '').replace(/\r/g, '')
} else {
node.value = value
}
}
return get ? val : this
}

57
src/effect/detect.js Normal file
View File

@ -0,0 +1,57 @@
/**
* ------------------------------------------------------------
* 检测浏览器对CSS动画的支持与API名
* ------------------------------------------------------------
*/
import { window } from '../seed/core'
let checker = {
TransitionEvent: 'transitionend',
WebKitTransitionEvent: 'webkitTransitionEnd',
OTransitionEvent: 'oTransitionEnd',
otransitionEvent: 'otransitionEnd'
}
let css3, tran, ani, name, animationEndEvent, transitionEndEvent
let transition = false
let animation = false
//有的浏览器同时支持私有实现与标准写法比如webkit支持前两种Opera支持1、3、4
for (name in checker) {
if (window[name]) {
tran = checker[name]
break
}
/* istanbul ignore next */
try {
let a = document.createEvent(name)
tran = checker[name]
break
} catch (e) {}
}
if (typeof tran === 'string') {
transition = css3 = true
transitionEndEvent = tran
}
//animationend有两个可用形态
//IE10+, Firefox 16+ & Opera 12.1+: animationend
//Chrome/Safari: webkitAnimationEnd
//http://blogs.msdn.com/b/davrous/archive/2011/12/06/introduction-to-css3-animat ions.aspx
//IE10也可以使用MSAnimationEnd监听但是回调里的事件 type依然为animationend
// el.addEventListener('MSAnimationEnd', function(e) {
// alert(e.type)// animationend
// })
checker = {
AnimationEvent: 'animationend',
WebKitAnimationEvent: 'webkitAnimationEnd'
}
for (name in checker) {
if (window[name]) {
ani = checker[name]
break
}
}
if (typeof ani === 'string') {
animation = css3 = true
animationEndEvent = ani
}
export { css3, animation, transition, animationEndEvent, transitionEndEvent }

327
src/effect/index.js Normal file
View File

@ -0,0 +1,327 @@
import { Anot, window, Cache } from '../seed/core'
import { cssDiff } from '../directives/css'
import {
css3,
animation,
transition,
animationEndEvent,
transitionEndEvent
} from './detect'
var effectDir = Anot.directive('effect', {
priority: 5,
diff: function(effect) {
var vdom = this.node
if (typeof effect === 'string') {
this.value = effect = {
is: effect
}
Anot.warn('ms-effect的指令值不再支持字符串,必须是一个对象')
}
this.value = vdom.effect = effect
var ok = cssDiff.call(this, effect, this.oldValue)
var me = this
if (ok) {
setTimeout(function() {
vdom.animating = true
effectDir.update.call(me, vdom, vdom.effect)
})
vdom.animating = false
return true
}
return false
},
update: function(vdom, change, opts) {
var dom = vdom.dom
if (dom && dom.nodeType === 1) {
//要求配置对象必须指定is属性action必须是布尔或enter,leave,move
var option = change || opts
var is = option.is
var globalOption = Anot.effects[is]
if (!globalOption) {
//如果没有定义特效
Anot.warn(is + ' effect is undefined')
return
}
var finalOption = {}
var action = actionMaps[option.action]
if (typeof Effect.prototype[action] !== 'function') {
Anot.warn('action is undefined')
return
}
//必须预定义特效
var effect = new Anot.Effect(dom)
Anot.mix(finalOption, globalOption, option, { action })
if (finalOption.queue) {
animationQueue.push(function() {
effect[action](finalOption)
})
callNextAnimation()
} else {
effect[action](finalOption)
}
return true
}
}
})
let move = 'move'
let leave = 'leave'
let enter = 'enter'
var actionMaps = {
true: enter,
false: leave,
enter,
leave,
move,
undefined: enter
}
var animationQueue = []
export function callNextAnimation() {
var fn = animationQueue[0]
if (fn) {
fn()
}
}
Anot.effects = {}
Anot.effect = function(name, opts) {
var definition = (Anot.effects[name] = opts || {})
if (css3 && definition.css !== false) {
patchObject(definition, 'enterClass', name + '-enter')
patchObject(
definition,
'enterActiveClass',
definition.enterClass + '-active'
)
patchObject(definition, 'leaveClass', name + '-leave')
patchObject(
definition,
'leaveActiveClass',
definition.leaveClass + '-active'
)
}
return definition
}
function patchObject(obj, name, value) {
if (!obj[name]) {
obj[name] = value
}
}
var Effect = function(dom) {
this.dom = dom
}
Anot.Effect = Effect
Effect.prototype = {
enter: createAction('Enter'),
leave: createAction('Leave'),
move: createAction('Move')
}
function execHooks(options, name, el) {
var fns = [].concat(options[name])
for (var i = 0, fn; (fn = fns[i++]); ) {
if (typeof fn === 'function') {
fn(el)
}
}
}
var staggerCache = new Cache(128)
function createAction(action) {
var lower = action.toLowerCase()
return function(option) {
var dom = this.dom
var elem = Anot(dom)
//处理与ms-for指令相关的stagger
//========BEGIN=====
var staggerTime = isFinite(option.stagger) ? option.stagger * 1000 : 0
if (staggerTime) {
if (option.staggerKey) {
var stagger =
staggerCache.get(option.staggerKey) ||
staggerCache.put(option.staggerKey, {
count: 0,
items: 0
})
stagger.count++
stagger.items++
}
}
var staggerIndex = (stagger && stagger.count) || 0
//=======END==========
var stopAnimationID
var animationDone = function(e) {
var isOk = e !== false
if (--dom.__ms_effect_ === 0) {
Anot.unbind(dom, transitionEndEvent)
Anot.unbind(dom, animationEndEvent)
}
clearTimeout(stopAnimationID)
var dirWord = isOk ? 'Done' : 'Abort'
execHooks(option, 'on' + action + dirWord, dom)
if (stagger) {
if (--stagger.items === 0) {
stagger.count = 0
}
}
if (option.queue) {
animationQueue.shift()
callNextAnimation()
}
}
//执行开始前的钩子
execHooks(option, 'onBefore' + action, dom)
if (option[lower]) {
//使用JS方式执行动画
option[lower](dom, function(ok) {
animationDone(ok !== false)
})
} else if (css3) {
//使用CSS3方式执行动画
elem.addClass(option[lower + 'Class'])
elem.removeClass(getNeedRemoved(option, lower))
if (!dom.__ms_effect_) {
//绑定动画结束事件
elem.bind(transitionEndEvent, animationDone)
elem.bind(animationEndEvent, animationDone)
dom.__ms_effect_ = 1
} else {
dom.__ms_effect_++
}
setTimeout(function() {
//用xxx-active代替xxx类名的方式 触发CSS3动画
var time = Anot.root.offsetWidth === NaN
elem.addClass(option[lower + 'ActiveClass'])
//计算动画时长
time = getAnimationTime(dom)
if (!time === 0) {
//立即结束动画
animationDone(false)
} else if (!staggerTime) {
//如果动画超出时长还没有调用结束事件,这可能是元素被移除了
//如果强制结束动画
stopAnimationID = setTimeout(function() {
animationDone(false)
}, time + 32)
}
}, 17 + staggerTime * staggerIndex) // = 1000/60
}
}
}
Anot.applyEffect = function(dom, vdom, opts) {
var cb = opts.cb
var curEffect = vdom.effect
if (curEffect && dom && dom.nodeType === 1) {
var hook = opts.hook
var old = curEffect[hook]
if (cb) {
if (Array.isArray(old)) {
old.push(cb)
} else if (old) {
curEffect[hook] = [old, cb]
} else {
curEffect[hook] = [cb]
}
}
getAction(opts)
Anot.directives.effect.update(vdom, curEffect, Anot.shadowCopy({}, opts))
} else if (cb) {
cb(dom)
}
}
/**
* 获取方向
*/
export function getAction(opts) {
if (!opts.action) {
return (opts.action = opts.hook
.replace(/^on/, '')
.replace(/Done$/, '')
.toLowerCase())
}
}
/**
* 需要移除的类名
*/
function getNeedRemoved(options, name) {
var name = name === 'leave' ? 'enter' : 'leave'
return Array(name + 'Class', name + 'ActiveClass')
.map(function(cls) {
return options[cls]
})
.join(' ')
}
/**
* 计算动画长度
*/
var transitionDuration = Anot.cssName('transition-duration')
var animationDuration = Anot.cssName('animation-duration')
var rsecond = /\d+s$/
export function toMillisecond(str) {
var ratio = rsecond.test(str) ? 1000 : 1
return parseFloat(str) * ratio
}
export function getAnimationTime(dom) {
var computedStyles = window.getComputedStyle(dom, null)
var tranDuration = computedStyles[transitionDuration]
var animDuration = computedStyles[animationDuration]
return toMillisecond(tranDuration) || toMillisecond(animDuration)
}
/**
*
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="dist/Anot.js"></script>
<script>
Anot.effect('animate')
var vm = Anot.define({
$id: 'ani',
a: true
})
</script>
<style>
.animate-enter, .animate-leave{
width:100px;
height:100px;
background: #29b6f6;
transition:all 2s;
-moz-transition: all 2s;
-webkit-transition: all 2s;
-o-transition:all 2s;
}
.animate-enter-active, .animate-leave{
width:300px;
height:300px;
}
.animate-leave-active{
width:100px;
height:100px;
}
</style>
</head>
<body>
<div :controller='ani' >
<p><input type='button' value='click' :click='@a =!@a'></p>
<div :effect="{is:'animate',action:@a}"></div>
</div>
</body>
</html>
*
*/

180
src/filters/array.js Normal file
View File

@ -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
}

248
src/filters/date.js Normal file
View File

@ -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

11
src/filters/escape.js Normal file
View File

@ -0,0 +1,11 @@
//https://github.com/teppeis/htmlspecialchars
export function escapeFilter(str) {
if (str == null) return ''
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}

33
src/filters/event.js Normal file
View File

@ -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 }

72
src/filters/index.js Normal file
View File

@ -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 }

34
src/filters/number.js Normal file
View File

@ -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)
}

31
src/filters/sanitize.js Normal file
View File

@ -0,0 +1,31 @@
var rscripts = /<script[^>]*>([\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
// <a href="javasc&NewLine;ript&colon;alert('XSS')">chrome</a>
// <a href="data:text/html;base64, PGltZyBzcmM9eCBvbmVycm9yPWFsZXJ0KDEpPg==">chrome</a>
// <a href="jav ascript:alert('XSS');">IE67chrome</a>
// <a href="jav&#x09;ascript:alert('XSS');">IE67chrome</a>
// <a href="jav&#x0A;ascript:alert('XSS');">IE67chrome</a>
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事件
})
}

44
src/gesture/drag.js Normal file
View File

@ -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)

114
src/gesture/pinch.js Normal file
View File

@ -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中事件分为三类
触摸事件通过触摸手势进行触发例如手指点击缩放
运动事件通过加速器进行触发例如手机晃动
远程控制事件通过其他远程设备触发例如耳机控制按钮
*/

60
src/gesture/press.js Normal file
View File

@ -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)

48
src/gesture/readme.md Normal file
View File

@ -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` 就行了
```
//你的业务代码
<div ms-on-drag="@drag">xxxx</div>
```

178
src/gesture/recognizer.js Normal file
View File

@ -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 }

86
src/gesture/rotate.js Normal file
View File

@ -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)

48
src/gesture/swipe.js Normal file
View File

@ -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)

297
src/gesture/tap.js Normal file
View File

@ -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)

71
src/parser/attributes.js Normal file
View File

@ -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
}

154
src/parser/index.js Normal file
View File

@ -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
}
}

44
src/parser/interpolate.js Normal file
View File

@ -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'
}
]
}

43
src/renders/Directive.js Normal file
View File

@ -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
}

407
src/renders/domRender.js Normal file
View File

@ -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<tuple>}
*/
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
}

View File

@ -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

96
src/renders/share.js Normal file
View File

@ -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
}

32
src/seed/browser.js Normal file
View File

@ -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

111
src/seed/cache.js Normal file
View File

@ -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 <D> 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
}
}

293
src/seed/core.js Normal file
View File

@ -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
})

25
src/seed/directive.js Normal file
View File

@ -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
}
}
}

142
src/seed/lang.modern.js Normal file
View File

@ -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
}
}
}
}
}

16
src/vdom/VComment.js Normal file
View File

@ -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 '<!--' + this.nodeValue + '-->'
}
}

View File

@ -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 + '</' + this.nodeName + '>'
}
}
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')
}

36
src/vdom/VFragment.js Normal file
View File

@ -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('')
}
}

19
src/vdom/VText.js Normal file
View File

@ -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
}
}

40
src/vdom/modern.js Normal file
View File

@ -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 }

176
src/vmodel/Action.js Normal file
View File

@ -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
}

102
src/vmodel/Computed.js Normal file
View File

@ -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)

90
src/vmodel/Mutation.js Normal file
View File

@ -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()
}
}
}

121
src/vmodel/ProxyArray.js Normal file
View File

@ -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

85
src/vmodel/modern.js Normal file
View File

@ -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

161
src/vmodel/proxy.js Normal file
View File

@ -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)
}
}

24
src/vmodel/reserved.js Normal file
View File

@ -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
}

226
src/vmodel/share.js Normal file
View File

@ -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

147
src/vmodel/transaction.js Normal file
View File

@ -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()
}
}

52
src/vtree/clearString.js Normal file
View File

@ -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
}

78
src/vtree/fromDOM.js Normal file
View File

@ -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
}

422
src/vtree/fromString.js Normal file
View File

@ -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 {
//处理<div><<<<<<div>的情况
const index = string.indexOf('<')
if (index === 0) {
text += string.slice(0, 1)
string = string.slice(1)
} else {
break
}
} while (string.length)
//处理<div>{aaa}</div>,<div>xxx{aaa}xxx</div>,<div>xxx</div>{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--
}
}
}
}
//<div>{{<div/>}}</div>
function getCloseTag(string) {
if (string.indexOf('</') === 0) {
var match = string.match(/\<\/(\w+[^\s\/\>]*)>/)
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 (i === 0) {
var l = 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 = '</' + tag + '>'
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 '必须关闭标签'
}

51
src/vtree/makeOrphan.js Normal file
View File

@ -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

35
src/vtree/makeTbody.js Normal file
View File

@ -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--
}
}
}
}

8
src/vtree/orphanTag.js Normal file
View File

@ -0,0 +1,8 @@
export var orphanTag = {
script: 1,
style: 1,
textarea: 1,
xmp: 1,
noscript: 1,
template: 1
}

View File

@ -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')
}

21
src/vtree/voidTag.js Normal file
View File

@ -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
}