321 lines
8.8 KiB
JavaScript
321 lines
8.8 KiB
JavaScript
//双工绑定
|
|
var rduplexType = /^(?:checkbox|radio)$/
|
|
var rduplexParam = /^(?:radio|checked)$/
|
|
var rnoduplexInput = /^(file|button|reset|submit|checkbox|radio|range)$/
|
|
var duplexBinding = Anot.directive('duplex', {
|
|
priority: 2000,
|
|
init: function(binding, hasCast) {
|
|
var elem = binding.element
|
|
var vmodels = binding.vmodels
|
|
binding.changed = getBindingCallback(elem, 'data-changed', vmodels) || noop
|
|
var params = []
|
|
var casting = oneObject('string,number,boolean,checked')
|
|
if (elem.type === 'radio' && binding.param === '') {
|
|
binding.param = 'checked'
|
|
}
|
|
|
|
binding.param.replace(rw20g, function(name) {
|
|
if (rduplexType.test(elem.type) && rduplexParam.test(name)) {
|
|
name = 'checked'
|
|
binding.isChecked = true
|
|
binding.xtype = 'radio'
|
|
}
|
|
|
|
if (casting[name]) {
|
|
hasCast = true
|
|
}
|
|
Anot.Array.ensure(params, name)
|
|
})
|
|
if (!hasCast) {
|
|
params.push('string')
|
|
}
|
|
binding.param = params.join('-')
|
|
if (!binding.xtype) {
|
|
binding.xtype =
|
|
elem.tagName === 'SELECT'
|
|
? 'select'
|
|
: elem.type === 'checkbox'
|
|
? 'checkbox'
|
|
: elem.type === 'radio'
|
|
? 'radio'
|
|
: /^change/.test(elem.getAttribute('data-event'))
|
|
? 'change'
|
|
: 'input'
|
|
}
|
|
elem.expr = binding.expr
|
|
//===================绑定事件======================
|
|
var bound = (binding.bound = function(type, callback) {
|
|
elem.addEventListener(type, callback, false)
|
|
var old = binding.rollback
|
|
binding.rollback = function() {
|
|
elem.anotSetter = null
|
|
Anot.unbind(elem, type, callback)
|
|
old && old()
|
|
}
|
|
})
|
|
function callback(value) {
|
|
binding.changed.call(this, value)
|
|
}
|
|
var composing = false
|
|
function compositionStart() {
|
|
composing = true
|
|
}
|
|
function compositionEnd() {
|
|
composing = false
|
|
setTimeout(updateVModel)
|
|
}
|
|
var updateVModel = function(e) {
|
|
var val = elem.value
|
|
//防止递归调用形成死循环
|
|
//处理中文输入法在minlengh下引发的BUG
|
|
if (composing || val === binding.oldValue || binding.pipe === null) {
|
|
return
|
|
}
|
|
|
|
var lastValue = binding.pipe(val, binding, 'get')
|
|
binding.oldValue = val
|
|
binding.setter(lastValue)
|
|
|
|
callback.call(elem, lastValue)
|
|
Anot.fireDom(elem, 'change')
|
|
}
|
|
switch (binding.xtype) {
|
|
case 'radio':
|
|
bound('click', function() {
|
|
var lastValue = binding.pipe(elem.value, binding, 'get')
|
|
binding.setter(lastValue)
|
|
callback.call(elem, lastValue)
|
|
})
|
|
break
|
|
case 'checkbox':
|
|
bound('change', function() {
|
|
var method = elem.checked ? 'ensure' : 'remove'
|
|
var array = binding.getter.apply(0, binding.vmodels)
|
|
if (!Array.isArray(array)) {
|
|
log(':duplex应用于checkbox上要对应一个数组')
|
|
array = [array]
|
|
}
|
|
var val = binding.pipe(elem.value, binding, 'get')
|
|
Anot.Array[method](array, val)
|
|
callback.call(elem, array)
|
|
})
|
|
break
|
|
case 'change':
|
|
bound('change', updateVModel)
|
|
break
|
|
case 'input':
|
|
bound('input', updateVModel)
|
|
bound('keyup', updateVModel)
|
|
if (!IEVersion) {
|
|
bound('compositionstart', compositionStart)
|
|
bound('compositionend', compositionEnd)
|
|
bound('DOMAutoComplete', updateVModel)
|
|
}
|
|
break
|
|
case 'select':
|
|
bound('change', function() {
|
|
var val = Anot(elem).val() //字符串或字符串数组
|
|
if (Array.isArray(val)) {
|
|
val = val.map(function(v) {
|
|
return binding.pipe(v, binding, 'get')
|
|
})
|
|
} else {
|
|
val = binding.pipe(val, binding, 'get')
|
|
}
|
|
if (val + '' !== binding.oldValue) {
|
|
try {
|
|
binding.setter(val)
|
|
} catch (ex) {
|
|
log(ex)
|
|
}
|
|
}
|
|
})
|
|
bound('datasetchanged', function(e) {
|
|
if (e.bubble === 'selectDuplex') {
|
|
var value = binding._value
|
|
var curValue = Array.isArray(value) ? value.map(String) : value + ''
|
|
Anot(elem).val(curValue)
|
|
elem.oldValue = curValue + ''
|
|
callback.call(elem, curValue)
|
|
}
|
|
})
|
|
break
|
|
}
|
|
if (binding.xtype === 'input' && !rnoduplexInput.test(elem.type)) {
|
|
if (elem.type !== 'hidden') {
|
|
bound('focus', function() {
|
|
elem.msFocus = true
|
|
})
|
|
bound('blur', function() {
|
|
elem.msFocus = false
|
|
})
|
|
}
|
|
elem.anotSetter = updateVModel //#765
|
|
watchValueInTimer(function() {
|
|
if (root.contains(elem)) {
|
|
if (!elem.msFocus) {
|
|
updateVModel()
|
|
}
|
|
} else if (!elem.msRetain) {
|
|
return false
|
|
}
|
|
})
|
|
}
|
|
},
|
|
update: function(value) {
|
|
var elem = this.element,
|
|
binding = this,
|
|
curValue
|
|
if (!this.init) {
|
|
var cpipe = binding.pipe || (binding.pipe = pipe)
|
|
cpipe(null, binding, 'init')
|
|
this.init = 1
|
|
}
|
|
switch (this.xtype) {
|
|
case 'input':
|
|
elem.value = value
|
|
break
|
|
case 'change':
|
|
curValue = this.pipe(value, this, 'set') //fix #673
|
|
if (curValue !== this.oldValue) {
|
|
var fixCaret = false
|
|
if (elem.msFocus) {
|
|
try {
|
|
var start = elem.selectionStart
|
|
var end = elem.selectionEnd
|
|
if (start === end) {
|
|
var pos = start
|
|
fixCaret = true
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
elem.value = this.oldValue = curValue
|
|
if (fixCaret && !elem.readOnly) {
|
|
elem.selectionStart = elem.selectionEnd = pos
|
|
}
|
|
}
|
|
break
|
|
case 'radio':
|
|
curValue = binding.isChecked ? !!value : value + '' === elem.value
|
|
elem.checked = curValue
|
|
break
|
|
case 'checkbox':
|
|
var array = [].concat(value) //强制转换为数组
|
|
curValue = this.pipe(elem.value, this, 'get')
|
|
elem.checked = array.indexOf(curValue) > -1
|
|
break
|
|
case 'select':
|
|
//必须变成字符串后才能比较
|
|
binding._value = value
|
|
if (!elem.msHasEvent) {
|
|
elem.msHasEvent = 'selectDuplex'
|
|
//必须等到其孩子准备好才触发
|
|
} else {
|
|
Anot.fireDom(elem, 'datasetchanged', {
|
|
bubble: elem.msHasEvent
|
|
})
|
|
}
|
|
break
|
|
}
|
|
}
|
|
})
|
|
|
|
function fixNull(val) {
|
|
return val == null ? '' : val
|
|
}
|
|
Anot.duplexHooks = {
|
|
checked: {
|
|
get: function(val, binding) {
|
|
return !binding.oldValue
|
|
}
|
|
},
|
|
string: {
|
|
get: function(val) {
|
|
//同步到VM
|
|
return val
|
|
},
|
|
set: fixNull
|
|
},
|
|
boolean: {
|
|
get: function(val) {
|
|
return val === 'true'
|
|
},
|
|
set: fixNull
|
|
},
|
|
number: {
|
|
get: function(val, binding) {
|
|
var number = +val
|
|
if (+val === number) {
|
|
return number
|
|
}
|
|
return 0
|
|
},
|
|
set: fixNull
|
|
}
|
|
}
|
|
|
|
function pipe(val, binding, action, e) {
|
|
binding.param.replace(rw20g, function(name) {
|
|
var hook = Anot.duplexHooks[name]
|
|
if (hook && typeof hook[action] === 'function') {
|
|
val = hook[action](val, binding)
|
|
}
|
|
})
|
|
return val
|
|
}
|
|
|
|
var TimerID,
|
|
ribbon = []
|
|
|
|
Anot.tick = function(fn) {
|
|
if (ribbon.push(fn) === 1) {
|
|
TimerID = setInterval(ticker, 60)
|
|
}
|
|
}
|
|
|
|
function ticker() {
|
|
for (var n = ribbon.length - 1; n >= 0; n--) {
|
|
var el = ribbon[n]
|
|
if (el() === false) {
|
|
ribbon.splice(n, 1)
|
|
}
|
|
}
|
|
if (!ribbon.length) {
|
|
clearInterval(TimerID)
|
|
}
|
|
}
|
|
|
|
var watchValueInTimer = noop
|
|
new function() {
|
|
// jshint ignore:line
|
|
try {
|
|
//#272 IE9-IE11, firefox
|
|
var setters = {}
|
|
var aproto = HTMLInputElement.prototype
|
|
var bproto = HTMLTextAreaElement.prototype
|
|
function newSetter(value) {
|
|
// jshint ignore:line
|
|
setters[this.tagName].call(this, value)
|
|
if (!this.msFocus && this.anotSetter) {
|
|
this.anotSetter()
|
|
}
|
|
}
|
|
var inputProto = HTMLInputElement.prototype
|
|
Object.getOwnPropertyNames(inputProto) //故意引发IE6-8等浏览器报错
|
|
setters['INPUT'] = Object.getOwnPropertyDescriptor(aproto, 'value').set
|
|
|
|
Object.defineProperty(aproto, 'value', {
|
|
set: newSetter
|
|
})
|
|
setters['TEXTAREA'] = Object.getOwnPropertyDescriptor(bproto, 'value').set
|
|
Object.defineProperty(bproto, 'value', {
|
|
set: newSetter
|
|
})
|
|
} catch (e) {
|
|
//在chrome 43中 :duplex终于不需要使用定时器实现双向绑定了
|
|
// http://updates.html5rocks.com/2015/04/DOM-attributes-now-on-the-prototype
|
|
// https://docs.google.com/document/d/1jwA8mtClwxI-QJuHT7872Z0pxpZz8PBkf2bGAbsUtqs/edit?pli=1
|
|
watchValueInTimer = Anot.tick
|
|
}
|
|
}() // jshint ignore:line
|