vendor/assets/javascripts/bower/vue/src/directives/repeat.js in fluentd-ui-0.3.8 vs vendor/assets/javascripts/bower/vue/src/directives/repeat.js in fluentd-ui-0.3.9

- old
+ new

@@ -1,246 +1,497 @@ -var utils = require('../utils'), - config = require('../config') +var _ = require('../util') +var isObject = _.isObject +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 -/** - * Binding that manages VMs based on an Array - */ module.exports = { - bind: function () { + /** + * Setup. + */ - this.identifier = '$r' + this.id + bind: function () { + // uid as a cache identifier + this.id = '__v_repeat_' + (++uid) + // we need to insert the objToArray converter + // as the first read filter. + 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 + // cache for primitive value instances + this.cache = Object.create(null) + }, - // a hash to cache the same expressions on repeated instances - // so they don't have to be compiled for every single instance - this.expCache = utils.hash() + /** + * Warn against v-if usage. + */ - var el = this.el, - ctn = this.container = el.parentNode + 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.' + ) + } + }, - // extract child Id, if any - this.childId = this.compiler.eval(utils.attr(el, 'ref')) + /** + * Check if v-ref/ v-el is also present. + */ - // create a comment node as a reference node for DOM insertions - this.ref = document.createComment(config.prefix + '-repeat-' + this.key) - ctn.insertBefore(this.ref, el) - ctn.removeChild(el) + checkRef: function () { + var childId = _.attr(this.el, 'ref') + this.childId = childId + ? this.vm.$interpolate(childId) + : null + var elId = _.attr(this.el, 'el') + this.elId = elId + ? this.vm.$interpolate(elId) + : null + }, - this.collection = null - this.vms = 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. + */ - }, - - update: function (collection) { - - if (!Array.isArray(collection)) { - if (utils.isObject(collection)) { - collection = utils.objectToArray(collection) - } else { - utils.warn('v-repeat only accepts Array or Object values.') - } + checkComponent: function () { + var id = _.attr(this.el, 'component') + var options = this.vm.$options + if (!id) { + this.Ctor = _.Vue // default constructor + this.inherit = true // inline repeats should inherit + // important: transclude with no options, just + // to ensure block start and block end + this.template = transclude(this.template) + this._linker = compile(this.template, options) + } else { + var tokens = textParser.parse(id) + if (!tokens) { // static component + var Ctor = this.Ctor = options.components[id] + _.assertAsset(Ctor, 'component', id) + if (Ctor) { + // merge an empty object with owner vm as parent + // so child vms can access parent assets. + var merged = mergeOptions( + Ctor.options, + {}, + { $parent: this.vm } + ) + this.template = transclude(this.template, merged) + this._linker = compile(this.template, merged) } + } else { + // to be resolved later + var ctorExp = textParser.tokensToExp(tokens) + this.ctorGetter = expParser.parse(ctorExp).get + } + } + }, - // keep reference of old data and VMs - // so we can reuse them if possible - this.oldVMs = this.vms - this.oldCollection = this.collection - collection = this.collection = collection || [] + /** + * Update. + * This is called whenever the Array mutates. + * + * @param {Array} data + */ - var isObject = collection[0] && utils.isObject(collection[0]) - this.vms = this.oldCollection - ? this.diff(collection, isObject) - : this.init(collection, isObject) + update: function (data) { + if (typeof data === 'number') { + data = range(data) + } + this.vms = this.diff(data || [], this.vms) + // update v-ref + if (this.childId) { + this.vm.$[this.childId] = this.vms + } + if (this.elId) { + this.vm.$$[this.elId] = this.vms.map(function (vm) { + return vm.$el + }) + } + }, - if (this.childId) { - this.vm.$[this.childId] = this.vms - } + /** + * 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} + */ - }, - - init: function (collection, isObject) { - var vm, vms = [] - for (var i = 0, l = collection.length; i < l; i++) { - vm = this.build(collection[i], i, isObject) - vms.push(vm) - if (this.compiler.init) { - this.container.insertBefore(vm.$el, this.ref) - } else { - vm.$before(this.ref) - } + 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 } - return vms - }, + if (idKey) { // swap track by id data + if (alias) { + vm[alias] = raw + } else { + vm._setData(raw) + } + } + } else { // new instance + vm = this.build(obj, i) + vm._new = true + } + 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 { + 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(targetNext.$el, null, false) + } + } else { + // new instance, insert to existing next + vm.$before(targetNext.$el) + } + } + vm._new = false + vm._reused = false + } + return vms + }, - /** - * Diff the new array with the old - * and determine the minimum amount of DOM manipulations. - */ - diff: function (newCollection, isObject) { + /** + * Build a new instance and cache it. + * + * @param {Object} data + * @param {Number} index + */ - var i, l, item, vm, - oldIndex, - targetNext, - currentNext, - nextEl, - ctn = this.container, - oldVMs = this.oldVMs, - vms = [] + build: function (data, index) { + var original = data + var meta = { $index: index } + if (this.converted) { + meta.$key = original.key + } + var raw = this.converted ? data.value : data + var alias = this.arg + var hasAlias = !isObject(raw) || alias + // wrap the raw data with alias + data = hasAlias ? {} : raw + if (alias) { + data[alias] = raw + } else if (hasAlias) { + meta.$value = raw + } + // resolve constructor + var Ctor = this.Ctor || this.resolveCtor(data, meta) + var vm = this.vm.$addChild({ + el: templateParser.clone(this.template), + _linker: this._linker, + _meta: meta, + data: data, + inherit: this.inherit + }, Ctor) + // cache instance + this.cacheVm(raw, vm) + return vm + }, - vms.length = newCollection.length + /** + * 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} + */ - // first pass, collect new reused and new created - for (i = 0, l = newCollection.length; i < l; i++) { - item = newCollection[i] - if (isObject) { - item.$index = i - if (item.__emitter__ && item.__emitter__[this.identifier]) { - // this piece of data is being reused. - // record its final position in reused vms - item.$reused = true - } else { - vms[i] = this.build(item, i, isObject) - } - } else { - // we can't attach an identifier to primitive values - // so have to do an indexOf... - oldIndex = indexOf(oldVMs, item) - if (oldIndex > -1) { - // record the position on the existing vm - oldVMs[oldIndex].$reused = true - oldVMs[oldIndex].$data.$index = i - } else { - vms[i] = this.build(item, i, isObject) - } - } - } + 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 + }, - // second pass, collect old reused and destroy unused - for (i = 0, l = oldVMs.length; i < l; i++) { - vm = oldVMs[i] - item = this.arg - ? vm.$data[this.arg] - : vm.$data - if (item.$reused) { - vm.$reused = true - delete item.$reused - } - if (vm.$reused) { - // update the index to latest - vm.$index = item.$index - // the item could have had a new key - if (item.$key && item.$key !== vm.$key) { - vm.$key = item.$key - } - vms[vm.$index] = vm - } else { - // this one can be destroyed. - if (item.__emitter__) { - delete item.__emitter__[this.identifier] - } - vm.$destroy() - } - } + /** + * Unbind, teardown everything + */ - // final pass, move/insert DOM elements - i = vms.length - while (i--) { - vm = vms[i] - item = vm.$data - targetNext = vms[i + 1] - if (vm.$reused) { - nextEl = vm.$el.nextSibling - // destroyed VMs' element might still be in the DOM - // due to transitions - while (!nextEl.vue_vm && nextEl !== this.ref) { - nextEl = nextEl.nextSibling - } - currentNext = nextEl.vue_vm - if (currentNext !== targetNext) { - if (!targetNext) { - ctn.insertBefore(vm.$el, this.ref) - } else { - nextEl = targetNext.$el - // new VMs' element might not be in the DOM yet - // due to transitions - while (!nextEl.parentNode) { - targetNext = vms[nextEl.vue_vm.$index + 1] - nextEl = targetNext - ? targetNext.$el - : this.ref - } - ctn.insertBefore(vm.$el, nextEl) - } - } - delete vm.$reused - delete item.$index - delete item.$key - } else { // a new vm - vm.$before(targetNext ? targetNext.$el : this.ref) - } - } + unbind: function () { + if (this.childId) { + delete this.vm.$[this.childId] + } + if (this.vms) { + var i = this.vms.length + var vm + while (i--) { + vm = this.vms[i] + this.uncacheVm(vm) + vm.$destroy() + } + } + }, - return vms - }, + /** + * 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 + */ - build: function (data, index, isObject) { - - // wrap non-object values - var raw, alias, - wrap = !isObject || this.arg - if (wrap) { - raw = data - alias = this.arg || '$value' - data = {} - data[alias] = raw + 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 ID 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.' + ) } - data.$index = index + } else { + _.define(data, this.id, vm) + } + } else { + if (!cache[data]) { + cache[data] = [vm] + } else { + cache[data].push(vm) + } + } + vm._raw = data + }, - var el = this.el.cloneNode(true), - Ctor = this.compiler.resolveComponent(el, data), - vm = new Ctor({ - el: el, - data: data, - parent: this.vm, - compilerOptions: { - repeat: true, - expCache: this.expCache - } - }) + /** + * Try to get a cached instance from a piece of data. + * + * @param {Object} data + * @return {Vue|undefined} + */ - if (isObject) { - // attach an ienumerable identifier to the raw data - (raw || data).__emitter__[this.identifier] = true + 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 + */ - unbind: function () { - if (this.childId) { - delete this.vm.$[this.childId] - } - if (this.vms) { - var i = this.vms.length - while (i--) { - this.vms[i].$destroy() - } - } + 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() } + } + } -// Helpers -------------------------------------------------------------------- +/** + * 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__ +} + /** - * Find an object or a wrapped data object - * from an Array + * 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 indexOf (vms, obj) { - for (var vm, i = 0, l = vms.length; i < l; i++) { - vm = vms[i] - if (!vm.$reused && vm.$value === obj) { - return i - } + +function objToArray (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] } - return -1 + } + // `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 } \ No newline at end of file