DynamicModelCollection aka заход на бесконечные списки #185

Open
chestozo opened this Issue Nov 27, 2013 · 0 comments

Projects

None yet

1 participant

@chestozo
yandex-ui member

Написал тут нечто. Пока что, правда, это конечный список я знаю, сколько всего элементов, но, кажется, не сложно допилить для неизвестного числа элементов.

Тут хочу просто код привести, чтобы понять, надо это кому-то ещё, или нет )
Несложно перенести это в noscript, при желании.

Итак, код:

(function() {

var getNearestMultipleOf = function(num, divider) {
    var mod = num % divider;
    return num + (mod === 0 ? 0 : (divider - mod));
};

var generateSequence = function(from, to, reverse) {
    var result = [];
    for (var i = from; i <= to; i++) {
        result.push(i);
    }
    return reverse ? result.reverse() : result;
};


var DynamicModelCollection = function() {};

no.inherit(DynamicModelCollection, ns.ModelCollection);

DynamicModelCollection.prototype._init = function() {
    ns.ModelCollection.prototype._init.apply(this, arguments);

    var info = ns.Model.infoLite(this.id);

    if (!info.dynamic) {
        throw new Error('[dynamicModelCollection] info.dynamic was not specified');
    }

    var pagingModelInfo = ns.Model.infoLite(info.dynamic.pagingModel);
    this.dynamic = {};
    this.dynamic.pageSize = pagingModelInfo.params['page-size'];

    if (!this.dynamic.pageSize) {
        throw new Error('[dynamicModelCollection] info.dynamic.pagingModel has no set `page-size` parameter value');
    }

    // total will be set when first portion of data comes from server.
    // TODO support for infinite lists (we do not know how many items we have).
    this.dynamic.total = 0;
    this.dynamic.pages = {};

    this.listenToMutations();
};

DynamicModelCollection.prototype.sync = function(visible) {
    var pageMaxIndex = this.getPageMaxIndex(visible.pages.to);
    var models = this.models;

    // Add loading models.
    if (models.length < pageMaxIndex + 1) {
        this.insert(this.createFakeModels(models.length, pageMaxIndex));
        this.notifyNewItemsLoaded();
    }

    this.requestPages(generateSequence(visible.pages.from, visible.pages.to));
};

DynamicModelCollection.prototype.getItemIndex = function(params) {
    var models = this.models;
    for (var i = 0; i < models.length; i++) {
        var model = models[i];
        if (!this.isModelGood(model)) {
            continue;
        }

        if (this.compareParams(model.params, params)) {
            return i;
        }
    }
    return -1;
};

DynamicModelCollection.prototype.getItem = function(index) {
    var model = this.models[index];
    return this.isModelGood(model) ? model : null;
};

DynamicModelCollection.prototype.listenToMutations = function() {
    var that = this;
    this.on('ns-model-remove', function(evt, models) {
        // We are just inserting newly got items.
        if (that.isMutating) {
            return;
        }

        if (models && models.length) {
            that.dynamic.total -= models.length;
            // NOTE must go before notifyTotalChanged (so that we are ready for updates).
            that.syncPagesHash();
            that.notifyTotalChanged();
        }
    });
};

DynamicModelCollection.prototype.requestPages = function(pages) {
    for (var i = 0; i < pages.length; i++) {
        if (this.dynamic.pages[pages[i]]) {
            continue;
        }
        this.requestPage(pages[i]);
    }
};

DynamicModelCollection.prototype.requestPage = function(page) {
    var that = this;

    // NOTE We set flag to true but we'll check it later again (when we get real data).
    this.dynamic.pages[page] = true;

    this.requestPagingModel(page).done(function(pagingModel) {
        var newModels = pagingModel.models;
        var models = that.models;

        // Start items mutations.
        that.isMutating = true;

        // Check if total has changed.
        var totalChanged = false;
        var newTotal = that.extractTotal(pagingModel);
        if (newTotal !== that.dynamic.total) {
            that.dynamic.total = newTotal;
            that.removeRedundantItems();
            totalChanged = true;
        }

        var indexes = that.generateIndexesForPage(page);
        var firstIndex = indexes[0];
        var lastIndex = indexes[indexes.length - 1];

        var from = null;

        for (var i = 0; i < indexes.length; i++) {
            var index = indexes[i];
            var model = models[index];

            if (!that.isModelGood(model)) {
                if (from === null) {
                    from = index;
                }
            } else {
                that.replaceItems(from, index - 1, firstIndex, newModels);
                from = null;
            }
        }

        // Replace items to the end (if needed - check is done inside).
        that.replaceItems(from, lastIndex, firstIndex, newModels);
        from = null;

        // Mutations ended.
        that.isMutating = false;

        that.syncPagesHash();
        if (totalChanged) {
            that.notifyTotalChanged();
        }
        that.notifyNewItemsLoaded();
    });
};

DynamicModelCollection.prototype.replaceItems = function(from, to, offset, newItems) {
    if (from === null) {
        return;
    }
    this.removeItems(from, to);
    this.insert(newItems.slice(from - offset, to - offset + 1), from);
};

DynamicModelCollection.prototype.removeRedundantItems = function() {
    if (this.dynamic.total < this.models.length) {
        this.removeItems(this.dynamic.total, this.models.length - 1);
    }
};

DynamicModelCollection.prototype.removeItems = function(from, to) {
    var models = this.models;
    var indexes = generateSequence(from, to).filter(function(index) { return !!models[index]; });
    // NOTE reverse here because if not reversed current collection will remove items with wrong indexes.
    this.remove(indexes.reverse());
};

// Must be called only:
// - after some data changes (like after items removed)
// - after new data comes from server
DynamicModelCollection.prototype.syncPagesHash = function() {
    var models = this.models;

    // l - is the right most loaded item.
    // It is or the right index of the last loaded page or the right most index of all items.
    var l = Math.min(this.dynamic.total, getNearestMultipleOf(models.length, this.dynamic.pageSize));

    for (var i = 0; i < l; i++) {
        var page = Math.floor(i / this.dynamic.pageSize);
        var model = models[i];
        this.dynamic.pages[page] = this.isModelGood(model);
    }
};

DynamicModelCollection.prototype.createFakeModels = function(from, to) {
    var models = [];
    for (var i = from; i <= to; i++) {
        models.push(this.createFakeModel(i));
    }
    return models;
};

DynamicModelCollection.prototype.generateIndexesForPage = function(page) {
    var from = page * this.dynamic.pageSize;
    var to = this.getPageMaxIndex(page);
    return generateSequence(from, to);
};

DynamicModelCollection.prototype.getPageMaxIndex = function(page) {
    var last = (page + 1) * this.dynamic.pageSize - 1;
    return ((last + 1) > this.dynamic.total) ? (this.dynamic.total - 1) : last;
};

DynamicModelCollection.prototype.isModelGood = function(model) {
    return model && model.isValid() && !this.isFakeModel(model);
};

DynamicModelCollection.prototype.notifyTotalChanged = function() {
    this.trigger('total-changed', this.dynamic.total);
};

DynamicModelCollection.prototype.notifyNewItemsLoaded = function() {
    this.trigger('new-items-loaded');
};

DynamicModelCollection.prototype.createFakeModel = function() {
    throw new Error('[dynamicModelCollection] you need to implement createFakeModel() method');
};

DynamicModelCollection.prototype.isFakeModel = function(model) {
    throw new Error('[dynamicModelCollection] you need to implement isFakeModel() method');
};

DynamicModelCollection.prototype.extractTotal = function(pagingModel) {
    throw new Error('[dynamicModelCollection] you need to implement extractTotal() method');
};

DynamicModelCollection.prototype.compareParams = function(p1, p2) {
    throw new Error('[dynamicModelCollection] you need to implement compareParams() method');
};

DynamicModelCollection.prototype.requestPagingModel = function(page) {
    throw new Error('[dynamicModelCollection] you need to implement requestPagingModel ) method');
};

}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment