silk-gui
Version:
GUI for developers and Node OS
537 lines (505 loc) • 13.9 kB
JavaScript
var _ = require('../util')
var isObject = _.isObject
var isPlainObject = _.isPlainObject
var textParser = require('../parsers/text')
var expParser = require('../parsers/expression')
var templateParser = require('../parsers/template')
var compile = require('../compiler/compile')
var transclude = require('../compiler/transclude')
var mergeOptions = require('../util/merge-option')
var uid = 0
module.exports = {
/**
* Setup.
*/
bind: function () {
// uid as a cache identifier
this.id = '__v_repeat_' + (++uid)
// we need to insert the objToArray converter
// as the first read filter, because it has to be invoked
// before any user filters. (can't do it in `update`)
if (!this.filters) {
this.filters = {}
}
// add the object -> array convert filter
var objectConverter = _.bind(objToArray, this)
if (!this.filters.read) {
this.filters.read = [objectConverter]
} else {
this.filters.read.unshift(objectConverter)
}
// setup ref node
this.ref = document.createComment('v-repeat')
_.replace(this.el, this.ref)
// check if this is a block repeat
this.template = this.el.tagName === 'TEMPLATE'
? templateParser.parse(this.el, true)
: this.el
// check other directives that need to be handled
// at v-repeat level
this.checkIf()
this.checkRef()
this.checkComponent()
// check for trackby param
this.idKey =
this._checkParam('track-by') ||
this._checkParam('trackby') // 0.11.0 compat
this.cache = Object.create(null)
},
/**
* Warn against v-if usage.
*/
checkIf: function () {
if (_.attr(this.el, 'if') !== null) {
_.warn(
'Don\'t use v-if with v-repeat. ' +
'Use v-show or the "filterBy" filter instead.'
)
}
},
/**
* Check if v-ref/ v-el is also present.
*/
checkRef: function () {
var refID = _.attr(this.el, 'ref')
this.refID = refID
? this.vm.$interpolate(refID)
: null
var elId = _.attr(this.el, 'el')
this.elId = elId
? this.vm.$interpolate(elId)
: null
},
/**
* Check the component constructor to use for repeated
* instances. If static we resolve it now, otherwise it
* needs to be resolved at build time with actual data.
*/
checkComponent: function () {
var id = _.attr(this.el, 'component')
var options = this.vm.$options
if (!id) {
// default constructor
this.Ctor = _.Vue
// inline repeats should inherit
this.inherit = true
// important: transclude with no options, just
// to ensure block start and block end
this.template = transclude(this.template)
this._linkFn = compile(this.template, options)
} else {
this.asComponent = true
// check inline-template
if (this._checkParam('inline-template') !== null) {
// extract inline template as a DocumentFragment
this.inlineTempalte = _.extractContent(this.el, true)
}
var tokens = textParser.parse(id)
if (!tokens) { // static component
var Ctor = this.Ctor = options.components[id]
_.assertAsset(Ctor, 'component', id)
var merged = mergeOptions(Ctor.options, {}, {
$parent: this.vm
})
merged.template = this.inlineTempalte || merged.template
merged._asComponent = true
merged._parent = this.vm
this.template = transclude(this.template, merged)
// Important: mark the template as a root node so that
// custom element components don't get compiled twice.
// fixes #822
this.template.__vue__ = true
this._linkFn = compile(this.template, merged)
} else {
// to be resolved later
var ctorExp = textParser.tokensToExp(tokens)
this.ctorGetter = expParser.parse(ctorExp).get
}
}
},
/**
* Update.
* This is called whenever the Array mutates.
*
* @param {Array|Number|String} data
*/
update: function (data) {
data = data || []
var type = typeof data
if (type === 'number') {
data = range(data)
} else if (type === 'string') {
data = _.toArray(data)
}
this.vms = this.diff(data, this.vms)
// update v-ref
if (this.refID) {
this.vm.$[this.refID] = this.vms
}
if (this.elId) {
this.vm.$$[this.elId] = this.vms.map(function (vm) {
return vm.$el
})
}
},
/**
* Diff, based on new data and old data, determine the
* minimum amount of DOM manipulations needed to make the
* DOM reflect the new data Array.
*
* The algorithm diffs the new data Array by storing a
* hidden reference to an owner vm instance on previously
* seen data. This allows us to achieve O(n) which is
* better than a levenshtein distance based algorithm,
* which is O(m * n).
*
* @param {Array} data
* @param {Array} oldVms
* @return {Array}
*/
diff: function (data, oldVms) {
var idKey = this.idKey
var converted = this.converted
var ref = this.ref
var alias = this.arg
var init = !oldVms
var vms = new Array(data.length)
var obj, raw, vm, i, l
// First pass, go through the new Array and fill up
// the new vms array. If a piece of data has a cached
// instance for it, we reuse it. Otherwise build a new
// instance.
for (i = 0, l = data.length; i < l; i++) {
obj = data[i]
raw = converted ? obj.$value : obj
vm = !init && this.getVm(raw)
if (vm) { // reusable instance
vm._reused = true
vm.$index = i // update $index
if (converted) {
vm.$key = obj.$key // update $key
}
if (idKey) { // swap track by id data
if (alias) {
vm[alias] = raw
} else {
vm._setData(raw)
}
}
} else { // new instance
vm = this.build(obj, i, true)
vm._new = true
vm._reused = false
}
vms[i] = vm
// insert if this is first run
if (init) {
vm.$before(ref)
}
}
// if this is the first run, we're done.
if (init) {
return vms
}
// Second pass, go through the old vm instances and
// destroy those who are not reused (and remove them
// from cache)
for (i = 0, l = oldVms.length; i < l; i++) {
vm = oldVms[i]
if (!vm._reused) {
this.uncacheVm(vm)
vm.$destroy(true)
}
}
// final pass, move/insert new instances into the
// right place. We're going in reverse here because
// insertBefore relies on the next sibling to be
// resolved.
var targetNext, currentNext
i = vms.length
while (i--) {
vm = vms[i]
// this is the vm that we should be in front of
targetNext = vms[i + 1]
if (!targetNext) {
// This is the last item. If it's reused then
// everything else will eventually be in the right
// place, so no need to touch it. Otherwise, insert
// it.
if (!vm._reused) {
vm.$before(ref)
}
} else {
var nextEl = targetNext.$el
if (vm._reused) {
// this is the vm we are actually in front of
currentNext = findNextVm(vm, ref)
// we only need to move if we are not in the right
// place already.
if (currentNext !== targetNext) {
vm.$before(nextEl, null, false)
}
} else {
// new instance, insert to existing next
vm.$before(nextEl)
}
}
vm._new = false
vm._reused = false
}
return vms
},
/**
* Build a new instance and cache it.
*
* @param {Object} data
* @param {Number} index
* @param {Boolean} needCache
*/
build: function (data, index, needCache) {
var meta = { $index: index }
if (this.converted) {
meta.$key = data.$key
}
var raw = this.converted ? data.$value : data
var alias = this.arg
if (alias) {
data = {}
data[alias] = raw
} else if (!isPlainObject(raw)) {
// non-object values
data = {}
meta.$value = raw
} else {
// default
data = raw
}
// resolve constructor
var Ctor = this.Ctor || this.resolveCtor(data, meta)
var vm = this.vm.$addChild({
el: templateParser.clone(this.template),
_asComponent: this.asComponent,
_host: this._host,
_linkFn: this._linkFn,
_meta: meta,
data: data,
inherit: this.inherit,
template: this.inlineTempalte
}, Ctor)
// flag this instance as a repeat instance
// so that we can skip it in vm._digest
vm._repeat = true
// cache instance
if (needCache) {
this.cacheVm(raw, vm)
}
// sync back changes for $value, particularly for
// two-way bindings of primitive values
var self = this
vm.$watch('$value', function (val) {
if (self.converted) {
self.rawValue[vm.$key] = val
} else {
self.rawValue.$set(vm.$index, val)
}
})
return vm
},
/**
* Resolve a contructor to use for an instance.
* The tricky part here is that there could be dynamic
* components depending on instance data.
*
* @param {Object} data
* @param {Object} meta
* @return {Function}
*/
resolveCtor: function (data, meta) {
// create a temporary context object and copy data
// and meta properties onto it.
// use _.define to avoid accidentally overwriting scope
// properties.
var context = Object.create(this.vm)
var key
for (key in data) {
_.define(context, key, data[key])
}
for (key in meta) {
_.define(context, key, meta[key])
}
var id = this.ctorGetter.call(context, context)
var Ctor = this.vm.$options.components[id]
_.assertAsset(Ctor, 'component', id)
return Ctor
},
/**
* Unbind, teardown everything
*/
unbind: function () {
if (this.refID) {
this.vm.$[this.refID] = null
}
if (this.vms) {
var i = this.vms.length
var vm
while (i--) {
vm = this.vms[i]
this.uncacheVm(vm)
vm.$destroy()
}
}
},
/**
* Cache a vm instance based on its data.
*
* If the data is an object, we save the vm's reference on
* the data object as a hidden property. Otherwise we
* cache them in an object and for each primitive value
* there is an array in case there are duplicates.
*
* @param {Object} data
* @param {Vue} vm
*/
cacheVm: function (data, vm) {
var idKey = this.idKey
var cache = this.cache
var id
if (idKey) {
id = data[idKey]
if (!cache[id]) {
cache[id] = vm
} else {
_.warn('Duplicate track-by key in v-repeat: ' + id)
}
} else if (isObject(data)) {
id = this.id
if (data.hasOwnProperty(id)) {
if (data[id] === null) {
data[id] = vm
} else {
_.warn(
'Duplicate objects are not supported in v-repeat ' +
'when using components or transitions.'
)
}
} else {
_.define(data, this.id, vm)
}
} else {
if (!cache[data]) {
cache[data] = [vm]
} else {
cache[data].push(vm)
}
}
vm._raw = data
},
/**
* Try to get a cached instance from a piece of data.
*
* @param {Object} data
* @return {Vue|undefined}
*/
getVm: function (data) {
if (this.idKey) {
return this.cache[data[this.idKey]]
} else if (isObject(data)) {
return data[this.id]
} else {
var cached = this.cache[data]
if (cached) {
var i = 0
var vm = cached[i]
// since duplicated vm instances might be a reused
// one OR a newly created one, we need to return the
// first instance that is neither of these.
while (vm && (vm._reused || vm._new)) {
vm = cached[++i]
}
return vm
}
}
},
/**
* Delete a cached vm instance.
*
* @param {Vue} vm
*/
uncacheVm: function (vm) {
var data = vm._raw
if (this.idKey) {
this.cache[data[this.idKey]] = null
} else if (isObject(data)) {
data[this.id] = null
vm._raw = null
} else {
this.cache[data].pop()
}
}
}
/**
* Helper to find the next element that is an instance
* root node. This is necessary because a destroyed vm's
* element could still be lingering in the DOM before its
* leaving transition finishes, but its __vue__ reference
* should have been removed so we can skip them.
*
* @param {Vue} vm
* @param {CommentNode} ref
* @return {Vue}
*/
function findNextVm (vm, ref) {
var el = (vm._blockEnd || vm.$el).nextSibling
while (!el.__vue__ && el !== ref) {
el = el.nextSibling
}
return el.__vue__
}
/**
* Attempt to convert non-Array objects to array.
* This is the default filter installed to every v-repeat
* directive.
*
* It will be called with **the directive** as `this`
* context so that we can mark the repeat array as converted
* from an object.
*
* @param {*} obj
* @return {Array}
* @private
*/
function objToArray (obj) {
// regardless of type, store the un-filtered raw value.
this.rawValue = obj
if (!isPlainObject(obj)) {
return obj
}
var keys = Object.keys(obj)
var i = keys.length
var res = new Array(i)
var key
while (i--) {
key = keys[i]
res[i] = {
$key: key,
$value: obj[key]
}
}
// `this` points to the repeat directive instance
this.converted = true
return res
}
/**
* Create a range array from given number.
*
* @param {Number} n
* @return {Array}
*/
function range (n) {
var i = -1
var ret = new Array(n)
while (++i < n) {
ret[i] = i
}
return ret
}