backbone-virtualized-listview
Version:
Virtualized list view for Backbone
1,427 lines (1,205 loc) • 46.5 kB
JavaScript
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory(require("underscore"), require("jquery"), require("backbone"), require("fast-binary-indexed-tree"));
else if(typeof define === 'function' && define.amd)
define(["underscore", "jquery", "backbone", "fast-binary-indexed-tree"], factory);
else if(typeof exports === 'object')
exports["backbone-virtualized-listview"] = factory(require("underscore"), require("jquery"), require("backbone"), require("fast-binary-indexed-tree"));
else
root["backbone-virtualized-listview"] = factory(root["underscore"], root["jquery"], root["backbone"], root["fast-binary-indexed-tree"]);
})(this, function(__WEBPACK_EXTERNAL_MODULE_1__, __WEBPACK_EXTERNAL_MODULE_2__, __WEBPACK_EXTERNAL_MODULE_3__, __WEBPACK_EXTERNAL_MODULE_4__) {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ exports: {},
/******/ id: moduleId,
/******/ loaded: false
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } };
var _underscore = __webpack_require__(1);
var _underscore2 = _interopRequireDefault(_underscore);
var _jquery = __webpack_require__(2);
var _jquery2 = _interopRequireDefault(_jquery);
var _backbone = __webpack_require__(3);
var _backbone2 = _interopRequireDefault(_backbone);
var _fastBinaryIndexedTree = __webpack_require__(4);
var _fastBinaryIndexedTree2 = _interopRequireDefault(_fastBinaryIndexedTree);
var _defaultList = __webpack_require__(5);
var _defaultList2 = _interopRequireDefault(_defaultList);
var _defaultItem = __webpack_require__(8);
var _defaultItem2 = _interopRequireDefault(_defaultItem);
var _viewport = __webpack_require__(9);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
// Helper function to created a scoped while loop
var whileTrue = function whileTrue(func) {
while (func()) {}
};
var INVALIDATION_NONE = 0;
var INVALIDATION_ITEMS = 0x1;
var INVALIDATION_EVENTS = 0x2;
var INVALIDATION_LIST = 0x4;
var INVALIDATION_ALL = 0x7;
var LIST_VIEW_EVENTS = ['willRedraw', 'didRedraw'];
/**
* The virtualized list view class.
*
* In addition to ordinary Backbone View options, the constructor also takes
*
* __virtualized__: whether or not the virtualization is enabled.
*
* __viewport__: the option locate the scrollable viewport. It can be
*
* * Omitted, auto detect the closest ancestor of the `$el` with 'overflowY'
* style being 'auto' or 'scroll'. Use the window viewport if found none.
* * A `string`, use it as a selector to select an __internal__ element as
* the viewport.
* * An `HTMLElement` or `jQuery`, use it as the viewport element.
* * The `window`, use the window viewport.
*
* @param {Object} options The constructor options.
* @param {boolean} [options.virtualized=true]
* @param {string | HTMLElement | jQuery | window} [options.viewport]
*
*/
var ListView = function (_Backbone$View) {
_inherits(ListView, _Backbone$View);
function ListView() {
_classCallCheck(this, ListView);
return _possibleConstructorReturn(this, (ListView.__proto__ || Object.getPrototypeOf(ListView)).apply(this, arguments));
}
_createClass(ListView, [{
key: 'initialize',
/**
* Backbone view initializer
* @see ListView
*/
value: function initialize() {
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
_ref$virtualized = _ref.virtualized,
virtualized = _ref$virtualized === undefined ? true : _ref$virtualized,
_ref$viewport = _ref.viewport,
viewport = _ref$viewport === undefined ? null : _ref$viewport;
this._props = { virtualized: virtualized, viewport: viewport };
this.options = {
model: {},
listTemplate: _defaultList2.default,
events: {},
items: [],
itemTemplate: _defaultItem2.default,
defaultItemHeight: 20
};
// States
this._state = {
indexFirst: 0,
indexLast: 0,
anchor: null,
invalidation: INVALIDATION_NONE,
removed: false,
eventsListView: {}
};
this._scheduleRedraw = _underscore2.default.noop;
}
}, {
key: '_initViewport',
value: function _initViewport() {
var viewport = this._props.viewport;
if (_underscore2.default.isString(viewport)) {
return new _viewport.ElementViewport(this.$(viewport));
} else if (viewport instanceof _jquery2.default) {
if (viewport.get(0) === window) {
return new _viewport.WindowViewport();
}
return new _viewport.ElementViewport(viewport);
} else if (viewport instanceof HTMLElement) {
return new _viewport.ElementViewport(viewport);
} else if (viewport === window) {
return new _viewport.WindowViewport();
}
var $el = this.$el;
while ($el.length > 0 && !$el.is(document)) {
if (_underscore2.default.contains(['auto', 'scroll'], $el.css('overflowY'))) {
return new _viewport.ElementViewport($el);
}
$el = $el.parent();
}
return new _viewport.WindowViewport();
}
}, {
key: '_hookUpViewport',
value: function _hookUpViewport() {
var _this2 = this;
this.viewport = this._initViewport();
if (this.virtualized) {
(function () {
var blockUntil = 0;
var onViewportChange = function onViewportChange() {
if (performance.now() > blockUntil) {
_this2._scheduleRedraw();
} else if (!_this2._state.removed) {
// If the scroll events are blocked, we shouldn't just swallow them.
// Wait for 0.1 second and give another try.
window.setTimeout(onViewportChange, 100);
}
};
_this2.viewport.on('change', onViewportChange);
//
// On keypress, we want to block the scroll events for 0.2 second to wait
// for the animation to complete. Otherwise, the scroll would change the
// geometry metrics and break the animation. The worst thing we may get is,
// for 'HOME' and 'END' keys, the view doesn't scroll to the right position.
//
_this2.viewport.on('keypress', function () {
blockUntil = performance.now() + 200;
});
})();
}
}
/**
* Whether or not the list view is virtualized
*/
}, {
key: 'remove',
/**
* Remove the view and unregister the event listeners.
*/
value: function remove() {
this._state.removed = true;
if (this.viewport) {
this.viewport.remove();
}
_get(ListView.prototype.__proto__ || Object.getPrototypeOf(ListView.prototype), 'remove', this).call(this);
}
}, {
key: '_applyPaddings',
value: function _applyPaddings(_ref2) {
var paddingTop = _ref2.paddingTop,
paddingBottom = _ref2.paddingBottom;
if (this.$topFiller && this.$bottomFiller) {
this.$topFiller.height(paddingTop);
this.$bottomFiller.height(paddingBottom);
}
}
}, {
key: '_processInvalidation',
value: function _processInvalidation() {
var _this3 = this;
var _options = this.options,
items = _options.items,
events = _options.events,
listTemplate = _options.listTemplate,
model = _options.model;
var invalidation = this._state.invalidation;
var eventsDOM = _underscore2.default.omit(events, LIST_VIEW_EVENTS);
var eventsListView = _underscore2.default.pick(events, LIST_VIEW_EVENTS);
if (invalidation & INVALIDATION_EVENTS) {
this.undelegateEvents();
_underscore2.default.each(this._state.eventsListView || {}, function (handler, event) {
_this3.off(event, handler);
});
}
if (invalidation & INVALIDATION_LIST) {
var isInternalViewport = _underscore2.default.isString(this._props.viewport);
if (isInternalViewport && this.viewport) {
this.viewport.remove();
this.viewport = null;
}
this.$el.html(listTemplate(model));
if (!this.viewport) {
this._hookUpViewport();
}
this.$topFiller = this.$('.top-filler');
this.$bottomFiller = this.$('.bottom-filler');
this._applyPaddings({
paddingTop: 0,
paddingBottom: this.itemHeights.read(items.length)
});
_underscore2.default.extend(this._state, { indexFirst: 0, indexLast: 0 });
}
if (invalidation & INVALIDATION_EVENTS) {
this.delegateEvents(eventsDOM);
_underscore2.default.each(eventsListView, function (handler, event) {
_this3.on(event, handler);
});
this._state.eventsListView = eventsListView;
}
var invalidateItems = invalidation & INVALIDATION_ITEMS;
_underscore2.default.extend(this._state, { invalidation: INVALIDATION_NONE });
return invalidateItems;
}
// Private API, redraw immediately
}, {
key: '_redraw',
value: function _redraw() {
var _this4 = this;
var invalidateItems = this._processInvalidation();
var _options2 = this.options,
items = _options2.items,
itemTemplate = _options2.itemTemplate;
var viewport = this.viewport,
itemHeights = this.itemHeights,
$topFiller = this.$topFiller,
$bottomFiller = this.$bottomFiller,
virtualized = this.virtualized;
var _state = this._state,
indexFirst = _state.indexFirst,
indexLast = _state.indexLast,
anchor = _state.anchor;
if (!invalidateItems && items.length === 0) {
return;
}
/**
* The event indicates the list will start redraw.
* @event ListView#willRedraw
*/
this.trigger('willRedraw');
whileTrue(function () {
var isCompleted = true;
var metricsViewport = viewport.getMetrics();
var visibleTop = metricsViewport.outer.top;
var visibleBot = metricsViewport.outer.bottom;
var listTopCur = _this4.$topFiller.get(0).getBoundingClientRect().top;
var scrollRatio = metricsViewport.scroll.ratioY;
var renderTop = false;
var renderBot = false;
whileTrue(function () {
var listTop = anchor ? anchor.top - itemHeights.read(anchor.index) : listTopCur;
var targetFirst = virtualized ? itemHeights.lowerBound(visibleTop - listTop) : 0;
var targetLast = virtualized ? Math.min(itemHeights.upperBound(visibleBot - listTop) + 1, items.length) : items.length;
var renderFirst = Math.max(targetFirst - 10, 0);
var renderLast = Math.min(targetLast + 10, items.length);
var renderMore = false;
// Clean up
if (targetFirst >= indexLast || targetLast <= indexFirst || invalidateItems) {
$topFiller.nextUntil($bottomFiller).remove();
indexFirst = indexLast = targetFirst;
if (targetFirst !== targetLast && items.length > 0) {
renderMore = true;
}
if (!anchor) {
var index = Math.round(targetFirst * (1 - scrollRatio) + targetLast * scrollRatio);
var top = listTopCur + itemHeights.read(index);
anchor = { index: index, top: top };
}
invalidateItems = false;
} else if (!anchor) {
var _index = Math.round(indexFirst * (1 - scrollRatio) + indexLast * scrollRatio);
var _top = listTopCur + itemHeights.read(_index);
anchor = { index: _index, top: _top };
}
// Render top
if (targetFirst < indexFirst) {
$topFiller.after(items.slice(renderFirst, indexFirst).map(itemTemplate));
$topFiller.nextUntil($bottomFiller).slice(0, indexFirst - renderFirst).each(function (offset, el) {
itemHeights.writeSingle(renderFirst + offset, el.getBoundingClientRect().height);
});
indexFirst = renderFirst;
renderMore = renderTop = true;
} else if (renderBot && !renderTop && renderFirst > indexFirst) {
(function () {
var removal = [];
$topFiller.nextUntil($bottomFiller).slice(0, renderFirst - indexFirst).each(function (offset, el) {
return removal.push(el);
});
(0, _jquery2.default)(removal).remove();
indexFirst = renderFirst;
renderMore = true;
})();
}
// Render bottom
if (targetLast > indexLast) {
$bottomFiller.before(items.slice(indexLast, renderLast).map(itemTemplate));
$topFiller.nextUntil($bottomFiller).slice(indexLast - indexFirst).each(function (offset, el) {
itemHeights.writeSingle(indexLast + offset, el.getBoundingClientRect().height);
});
indexLast = renderLast;
renderMore = renderBot = true;
} else if (renderTop && !renderBot && renderLast < indexLast) {
(function () {
var removal = [];
$topFiller.nextUntil($bottomFiller).slice(renderLast - indexFirst).each(function (offset, el) {
return removal.push(el);
});
(0, _jquery2.default)(removal).remove();
indexLast = renderLast;
renderMore = true;
})();
}
return renderMore;
});
// Update the padding
if (indexFirst !== _this4.indexFirst || indexLast !== _this4.indexLast) {
_this4._applyPaddings({
paddingTop: itemHeights.read(indexFirst),
paddingBottom: itemHeights.read(items.length) - itemHeights.read(indexLast)
});
}
// Adjust the scroll if it's changed significantly
var listTop = anchor.top - itemHeights.read(anchor.index);
var innerTop = listTop - (listTopCur - metricsViewport.inner.top);
var scrollTop = Math.round(visibleTop - innerTop);
var anchorNew = null;
// Do a second scroll for a middle anchor after the item is rendered
if (anchor.isMiddle) {
var index = anchor.index;
var itemTop = listTopCur + _this4.itemHeights.read(index);
var itemBot = listTopCur + _this4.itemHeights.read(index + 1);
anchorNew = {
index: index,
top: (visibleTop + visibleBot + itemTop - itemBot) / 2
};
isCompleted = false;
}
if (Math.abs(scrollTop - viewport.getMetrics().scroll.y) >= 1) {
_this4.viewport.scrollTo({ y: scrollTop });
isCompleted = false;
}
anchor = anchorNew;
return !isCompleted;
});
// Write back the render state
_underscore2.default.extend(this._state, { indexFirst: indexFirst, indexLast: indexLast, anchor: null });
/**
* The event indicates the list view have completed redraw.
* @event ListView#didRedraw
*/
this.trigger('didRedraw');
}
/**
* Get the item at certain index.
* @param {number} index The index of the item.
* @return {Object}
*/
}, {
key: 'itemAt',
value: function itemAt(index) {
return _underscore2.default.first(this.options.items.slice(index, index + 1));
}
/**
* Get the rendered DOM element at certain index.
* @param {number} index The index of the item.
* @return {HTMLElement}
*/
}, {
key: 'elementAt',
value: function elementAt(index) {
var _state2 = this._state,
indexFirst = _state2.indexFirst,
indexLast = _state2.indexLast;
if (index < indexFirst || index >= indexLast || !this.$topFiller || !this.$bottomFiller) {
return null;
}
return this.$topFiller.nextUntil(this.$bottomFiller).get(index - indexFirst);
}
/**
* The index of the first rendered item.
* @type {number}
*/
}, {
key: 'set',
/**
* Set the list view options. The following options can be set
*
* __model__: The model object to render the skeleton of the list view.
*
* __listTemplate__: The template to render the skeleton of the list view.
*
* * By default, it would render a single `UL`.
* * __Note__: It must contain the following elements with specified class
* names as the first and last siblings of the list items. All list items
* will be rendered in between.
* * `'top-filler'`: The filler block on top.
* * `'bottom-filler'`: The filler block at bottom.
*
* __events__: The events hash in form of `{ "event selector": callback }`.
*
* * Refer to {@link http://backbonejs.org/#View-events|Backbone.View~events}
* * In addition to the DOM events, it can also handle the `'willRedraw'` and
* `'didRedraw'` events of the list view.
* * __Note__: The callback __MUST__ be a function. Member function names are
* not supported.
*
* __items__: The model objects of the list items.
*
* __itemTemplate__: The template to render a list item.
*
* * By default, it would render a single `LI` filled with `item.text`.
* * __Note__: list items __MUST NOT__ have outer margins, otherwise the layout
* calculation will be inaccurate.
*
* __defaultItemHeight__: The estimated height of a single item.
*
* * It's not necessary to be accurate. But the accurater it is, the less the
* scroll bar is adjusted overtime.
*
* Refer to {@link ListView} for detail.
*
* @param {Object} options The new options.
* @param {Object} options.model
* @param {ListView~cbListTemplate} [options.listTemplate]
* @param {Object} options.events
* @param {Object[]} [options.items=[]]
* @param {ListView~cbItemTemplate} [options.itemTemplate]
* @param {number} [options.defaultItemHeight=20]
* @param {function} [callback] The callback to notify completion.
* @return {ListView} The list view itself.
*/
value: function set() {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var callback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _underscore2.default.noop;
var isSet = function isSet(key) {
return !_underscore2.default.isUndefined(options[key]);
};
var itemHeightsCur = this._itemHeights;
var invalidation = 0;
_underscore2.default.extend(this.options, options);
if (_underscore2.default.some(['model', 'listTemplate'], isSet)) {
invalidation |= INVALIDATION_ALL;
} else {
if (_underscore2.default.some(['items', 'itemTemplate', 'defaultItemHeight'], isSet)) {
if (isSet('defaultItemHeight') || this.itemHeights.maxVal !== this.length) {
this._itemHeights = null;
}
invalidation |= INVALIDATION_ITEMS;
}
if (isSet('events')) {
invalidation |= INVALIDATION_EVENTS;
}
}
if (invalidation) {
if (this.viewport && this.$topFiller && this.$topFiller.length > 0 && itemHeightsCur) {
var visibleTop = this.viewport.getMetrics().outer.top;
var listTopCur = this.$topFiller.get(0).getBoundingClientRect().top;
var visibleFirst = itemHeightsCur.lowerBound(visibleTop - listTopCur);
if (visibleFirst < this.length) {
var el = this.elementAt(visibleFirst);
if (el) {
var elTop = el.getBoundingClientRect().top;
this._state.anchor = {
index: visibleFirst,
top: elTop
};
}
}
}
this._invalidate(invalidation, callback);
} else {
callback();
}
return this;
}
}, {
key: '_invalidate',
value: function _invalidate(invalidation, callback) {
this._state.invalidation |= invalidation;
this._scheduleRedraw(true);
this.once('didRedraw', callback);
}
/**
* Invalidate the already rendered items and schedule another redraw.
* @param {function} [callback] The callback to notify completion.
*/
}, {
key: 'invalidate',
value: function invalidate() {
var callback = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _underscore2.default.noop;
this._invalidate(INVALIDATION_ITEMS, callback);
}
/**
* Scroll to a certain item.
* @param {number} index The index of the item.
* @param {string|number} [position='default'] The position of the item.
*
* The valid positions are
* * `'default'`, if the item is above the viewport top, scroll it to the
* top, if the item is below the viewport bottom, scroll it to the bottom,
* otherwise, keep the viewport unchanged.
* * `'top'`, scroll the item to top of the viewport.
* * `'middle'`, scroll the item to the vertical center of the viewport.
* * `'bottom'`, scroll the item to the bottom of the viewport.
* * `{number}`, scroll the item to the given offset from the viewport top.
*
* @param {function} [callback] The callback to notify completion.
*
*/
}, {
key: 'scrollToItem',
value: function scrollToItem() {
if (!this.$topFiller || !this.$bottomFiller) {
throw new Error('Cannot scroll before the view is rendered');
}
var index = 0;
var position = 'default';
var callback = _underscore2.default.noop;
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
if (args.length >= 3) {
index = args[0];
position = args[1];
callback = args[2];
} else if (args.length === 2) {
if (_underscore2.default.isFunction(args[1])) {
index = args[0];
callback = args[1];
} else {
index = args[0];
position = args[1];
}
} else if (args.length === 1) {
index = args[0];
}
this._scrollToItem(index, position, callback);
}
}, {
key: '_scrollToItem',
value: function _scrollToItem(index, position, callback) {
var metricsViewport = this.viewport.getMetrics();
var visibleTop = metricsViewport.outer.top;
var visibleBot = metricsViewport.outer.bottom;
var listTopCur = this.$topFiller.get(0).getBoundingClientRect().top;
var itemTop = listTopCur + this.itemHeights.read(index);
var itemBot = listTopCur + this.itemHeights.read(index + 1);
var pos = position;
if (pos === 'default') {
if (itemTop < visibleTop) {
pos = 'top';
} else if (itemBot > visibleBot) {
pos = 'bottom';
} else {
if (_underscore2.default.isFunction(callback)) {
callback();
}
return;
}
}
if (pos === 'top') {
this._state.anchor = {
index: index,
top: visibleTop
};
} else if (pos === 'bottom') {
this._state.anchor = {
index: index + 1,
top: visibleBot
};
} else if (pos === 'middle') {
this._state.anchor = {
index: index,
top: (visibleTop + visibleBot + itemTop - itemBot) / 2,
isMiddle: true
};
} else if (typeof pos === 'number') {
this._state.anchor = {
index: index,
top: visibleTop + pos
};
} else {
throw new Error('Invalid position');
}
this.once('didRedraw', callback);
this._scheduleRedraw();
}
/**
* Render the list view.
* @param {function} [callback] The callback to notify completion.
*/
}, {
key: 'render',
value: function render() {
var _this5 = this;
var callback = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _underscore2.default.noop;
var animationFrameId = null;
var timeoutId = null;
var redraw = function redraw() {
animationFrameId = null;
timeoutId = null;
if (!_this5._state.removed) {
_this5._redraw();
}
};
this._scheduleRedraw = function () {
var ignoreAnimationFrame = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
if (!timeoutId) {
if (ignoreAnimationFrame) {
timeoutId = window.setTimeout(redraw, 0);
if (animationFrameId) {
window.cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
} else if (!animationFrameId) {
animationFrameId = window.requestAnimationFrame(redraw);
}
}
};
// this._hookUpViewport();
this._invalidate(INVALIDATION_ALL, callback);
return this;
}
}, {
key: 'virtualized',
get: function get() {
return this._props.virtualized;
}
}, {
key: 'indexFirst',
get: function get() {
return this._state.indexFirst;
}
/**
* The index after the last rendered item.
* @type {number}
*/
}, {
key: 'indexLast',
get: function get() {
return this._state.indexLast;
}
/**
* The total count of the items.
* @type {number}
*/
}, {
key: 'length',
get: function get() {
return this.options.items.length;
}
/**
* The model object to render the skeleton of the list view.
* @type {Object}
*/
}, {
key: 'model',
get: function get() {
return this.options.model;
}
/**
* The template to render the skeleton of the list view.
* @callback ListView~cbListTemplate
* @param {Object} model The model object of the list view.
*/
/**
* The template to render the skeleton of the list view.
* @type {ListView~cbListTemplate}
*/
}, {
key: 'listTemplate',
get: function get() {
return this.options.listTemplate;
}
/**
* The template to render a list item.
* @callback ListView~cbItemTemplate
* @param {Object} item The model object of the item
*/
/**
* The template to render a list item.
* @type {ListView~cbItemTemplate}
*/
}, {
key: 'itemTemplate',
get: function get() {
return this.options.itemTemplate;
}
/**
* The default list item height.
* @type {number}
*/
}, {
key: 'defaultItemHeight',
get: function get() {
return this.options.defaultItemHeight;
}
/**
* @external BinaryIndexedTree
* @see {@link https://microsoft.github.io/fast-binary-indexed-tree-js/BinaryIndexedTree.html}
*/
/**
* The BinaryIndexedTree to get the heights and accumulated heights of items.
* @type {external:BinaryIndexedTree}
*/
}, {
key: 'itemHeights',
get: function get() {
if (!this._itemHeights) {
var _options3 = this.options,
defaultItemHeight = _options3.defaultItemHeight,
items = _options3.items;
this._itemHeights = new _fastBinaryIndexedTree2.default({
defaultFrequency: Math.max(defaultItemHeight, 1),
maxVal: items.length
});
}
return this._itemHeights;
}
}]);
return ListView;
}(_backbone2.default.View);
exports.default = ListView;
/***/ },
/* 1 */
/***/ function(module, exports) {
module.exports = __WEBPACK_EXTERNAL_MODULE_1__;
/***/ },
/* 2 */
/***/ function(module, exports) {
module.exports = __WEBPACK_EXTERNAL_MODULE_2__;
/***/ },
/* 3 */
/***/ function(module, exports) {
module.exports = __WEBPACK_EXTERNAL_MODULE_3__;
/***/ },
/* 4 */
/***/ function(module, exports) {
module.exports = __WEBPACK_EXTERNAL_MODULE_4__;
/***/ },
/* 5 */
/***/ function(module, exports, __webpack_require__) {
var jade = __webpack_require__(6);
module.exports = function template(locals) {
var buf = [];
var jade_mixins = {};
var jade_interp;
buf.push("<ul class=\"list-container\"><div class=\"top-filler\"></div><div class=\"bottom-filler\"></div></ul>");;return buf.join("");
}
/***/ },
/* 6 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
/**
* Merge two attribute objects giving precedence
* to values in object `b`. Classes are special-cased
* allowing for arrays and merging/joining appropriately
* resulting in a string.
*
* @param {Object} a
* @param {Object} b
* @return {Object} a
* @api private
*/
exports.merge = function merge(a, b) {
if (arguments.length === 1) {
var attrs = a[0];
for (var i = 1; i < a.length; i++) {
attrs = merge(attrs, a[i]);
}
return attrs;
}
var ac = a['class'];
var bc = b['class'];
if (ac || bc) {
ac = ac || [];
bc = bc || [];
if (!Array.isArray(ac)) ac = [ac];
if (!Array.isArray(bc)) bc = [bc];
a['class'] = ac.concat(bc).filter(nulls);
}
for (var key in b) {
if (key != 'class') {
a[key] = b[key];
}
}
return a;
};
/**
* Filter null `val`s.
*
* @param {*} val
* @return {Boolean}
* @api private
*/
function nulls(val) {
return val != null && val !== '';
}
/**
* join array as classes.
*
* @param {*} val
* @return {String}
*/
exports.joinClasses = joinClasses;
function joinClasses(val) {
return (Array.isArray(val) ? val.map(joinClasses) :
(val && typeof val === 'object') ? Object.keys(val).filter(function (key) { return val[key]; }) :
[val]).filter(nulls).join(' ');
}
/**
* Render the given classes.
*
* @param {Array} classes
* @param {Array.<Boolean>} escaped
* @return {String}
*/
exports.cls = function cls(classes, escaped) {
var buf = [];
for (var i = 0; i < classes.length; i++) {
if (escaped && escaped[i]) {
buf.push(exports.escape(joinClasses([classes[i]])));
} else {
buf.push(joinClasses(classes[i]));
}
}
var text = joinClasses(buf);
if (text.length) {
return ' class="' + text + '"';
} else {
return '';
}
};
exports.style = function (val) {
if (val && typeof val === 'object') {
return Object.keys(val).map(function (style) {
return style + ':' + val[style];
}).join(';');
} else {
return val;
}
};
/**
* Render the given attribute.
*
* @param {String} key
* @param {String} val
* @param {Boolean} escaped
* @param {Boolean} terse
* @return {String}
*/
exports.attr = function attr(key, val, escaped, terse) {
if (key === 'style') {
val = exports.style(val);
}
if ('boolean' == typeof val || null == val) {
if (val) {
return ' ' + (terse ? key : key + '="' + key + '"');
} else {
return '';
}
} else if (0 == key.indexOf('data') && 'string' != typeof val) {
if (JSON.stringify(val).indexOf('&') !== -1) {
console.warn('Since Jade 2.0.0, ampersands (`&`) in data attributes ' +
'will be escaped to `&`');
};
if (val && typeof val.toISOString === 'function') {
console.warn('Jade will eliminate the double quotes around dates in ' +
'ISO form after 2.0.0');
}
return ' ' + key + "='" + JSON.stringify(val).replace(/'/g, ''') + "'";
} else if (escaped) {
if (val && typeof val.toISOString === 'function') {
console.warn('Jade will stringify dates in ISO form after 2.0.0');
}
return ' ' + key + '="' + exports.escape(val) + '"';
} else {
if (val && typeof val.toISOString === 'function') {
console.warn('Jade will stringify dates in ISO form after 2.0.0');
}
return ' ' + key + '="' + val + '"';
}
};
/**
* Render the given attributes object.
*
* @param {Object} obj
* @param {Object} escaped
* @return {String}
*/
exports.attrs = function attrs(obj, terse){
var buf = [];
var keys = Object.keys(obj);
if (keys.length) {
for (var i = 0; i < keys.length; ++i) {
var key = keys[i]
, val = obj[key];
if ('class' == key) {
if (val = joinClasses(val)) {
buf.push(' ' + key + '="' + val + '"');
}
} else {
buf.push(exports.attr(key, val, false, terse));
}
}
}
return buf.join('');
};
/**
* Escape the given string of `html`.
*
* @param {String} html
* @return {String}
* @api private
*/
var jade_encode_html_rules = {
'&': '&',
'<': '<',
'>': '>',
'"': '"'
};
var jade_match_html = /[&<>"]/g;
function jade_encode_char(c) {
return jade_encode_html_rules[c] || c;
}
exports.escape = jade_escape;
function jade_escape(html){
var result = String(html).replace(jade_match_html, jade_encode_char);
if (result === '' + html) return html;
else return result;
};
/**
* Re-throw the given `err` in context to the
* the jade in `filename` at the given `lineno`.
*
* @param {Error} err
* @param {String} filename
* @param {String} lineno
* @api private
*/
exports.rethrow = function rethrow(err, filename, lineno, str){
if (!(err instanceof Error)) throw err;
if ((typeof window != 'undefined' || !filename) && !str) {
err.message += ' on line ' + lineno;
throw err;
}
try {
str = str || __webpack_require__(7).readFileSync(filename, 'utf8')
} catch (ex) {
rethrow(err, null, lineno)
}
var context = 3
, lines = str.split('\n')
, start = Math.max(lineno - context, 0)
, end = Math.min(lines.length, lineno + context);
// Error context
var context = lines.slice(start, end).map(function(line, i){
var curr = i + start + 1;
return (curr == lineno ? ' > ' : ' ')
+ curr
+ '| '
+ line;
}).join('\n');
// Alter exception message
err.path = filename;
err.message = (filename || 'Jade') + ':' + lineno
+ '\n' + context + '\n\n' + err.message;
throw err;
};
exports.DebugItem = function DebugItem(lineno, filename) {
this.lineno = lineno;
this.filename = filename;
}
/***/ },
/* 7 */
/***/ function(module, exports) {
/* (ignored) */
/***/ },
/* 8 */
/***/ function(module, exports, __webpack_require__) {
var jade = __webpack_require__(6);
module.exports = function template(locals) {
var buf = [];
var jade_mixins = {};
var jade_interp;
;var locals_for_with = (locals || {});(function (text) {
buf.push("<li>" + (jade.escape(null == (jade_interp = text) ? "" : jade_interp)) + "</li>");}.call(this,"text" in locals_for_with?locals_for_with.text:typeof text!=="undefined"?text:undefined));;return buf.join("");
}
/***/ },
/* 9 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ElementViewport = exports.WindowViewport = exports.Viewport = undefined;
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _backbone = __webpack_require__(3);
var _backbone2 = _interopRequireDefault(_backbone);
var _jquery = __webpack_require__(2);
var _jquery2 = _interopRequireDefault(_jquery);
var _underscore = __webpack_require__(1);
var _underscore2 = _interopRequireDefault(_underscore);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function getElementMetrics(el) {
return _underscore2.default.pick(el.getBoundingClientRect(), ['left', 'top', 'right', 'bottom', 'width', 'height']);
}
function calculateRatio(scroll, scrollMax) {
return scrollMax > 0 ? Math.min(Math.max(scroll / scrollMax, 0), 1) : 0;
}
var Viewport = exports.Viewport = function () {
function Viewport($el) {
var _this = this;
_classCallCheck(this, Viewport);
_underscore2.default.extend(this, _backbone2.default.Events);
this.$el = $el;
this.onScroll = function () {
_this.trigger('scroll');
_this.trigger('change');
};
this.onResize = function () {
_this.trigger('resize');
_this.trigger('change');
};
var keyCode = null;
var timestamp = performance.now();
this.onKeydown = function (event) {
// Consolidate the keydown events for the same key in 0.2 seconds
if (keyCode !== event.keyCode || performance.now() > timestamp + 200) {
keyCode = event.keyCode;
timestamp = performance.now();
_this.trigger('keypress', keyCode);
}
};
this.onKeyup = function () {
keyCode = null;
};
this.$el.on('resize', this.onResize);
this.$el.on('scroll', this.onScroll);
(0, _jquery2.default)(document).on('keydown', this.onKeydown);
(0, _jquery2.default)(document).on('keyup', this.onKeyup);
this.scrollTo = function (scrollNew) {
if (_underscore2.default.isNumber(scrollNew.x)) {
_this.$el.scrollLeft(scrollNew.x);
}
if (_underscore2.default.isNumber(scrollNew.y)) {
_this.$el.scrollTop(scrollNew.y);
}
};
}
_createClass(Viewport, [{
key: 'remove',
value: function remove() {
this.$el.off('resize', this.onResize);
this.$el.off('scroll', this.onScroll);
(0, _jquery2.default)(document).off('keydown', this.onKeydown);
(0, _jquery2.default)(document).off('keyup', this.onKeyup);
}
}, {
key: 'getMetrics',
value: function getMetrics() {
throw new Error('Not implemented');
}
}]);
return Viewport;
}();
var WindowViewport = exports.WindowViewport = function (_Viewport) {
_inherits(WindowViewport, _Viewport);
function WindowViewport() {
_classCallCheck(this, WindowViewport);
return _possibleConstructorReturn(this, (WindowViewport.__proto__ || Object.getPrototypeOf(WindowViewport)).call(this, (0, _jquery2.default)(window)));
}
_createClass(WindowViewport, [{
key: 'getMetrics',
value: function getMetrics() {
var inner = getElementMetrics(document.documentElement);
inner.width = document.documentElement.scrollWidth;
inner.height = document.documentElement.scrollHeight;
inner.right = inner.left + inner.width;
inner.bottom = inner.top + inner.height;
var outer = {
top: 0,
bottom: window.innerHeight,
left: 0,
right: window.innerWidth,
width: window.innerWidth,
height: window.innerHeight
};
var scroll = {
x: window.pageXOffset,
y: window.pageYOffset
};
scroll.ratioX = calculateRatio(scroll.x, inner.width - outer.width);
scroll.ratioY = calculateRatio(scroll.y, inner.height - outer.height);
return { inner: inner, outer: outer, scroll: scroll };
}
}]);
return WindowViewport;
}(Viewport);
var SCROLLABLE = ['auto', 'scroll'];
var ElementViewport = exports.ElementViewport = function (_Viewport2) {
_inherits(ElementViewport, _Viewport2);
function ElementViewport(el) {
_classCallCheck(this, ElementViewport);
var _this3 = _possibleConstructorReturn(this, (ElementViewport.__proto__ || Object.getPrototypeOf(ElementViewport)).call(this, (0, _jquery2.default)(el)));
_this3.el = _this3.$el.get(0);
_this3.$el.css('overflowX', function (s) {
return _underscore2.default.contains(SCROLLABLE, s) ? s : 'auto';
});
_this3.$el.css('overflowY', function (s) {
return _underscore2.default.contains(SCROLLABLE, s) ? s : 'auto';
});
return _this3;
}
_createClass(ElementViewport, [{
key: 'getMetrics',
value: function getMetrics() {
var outer = getElementMetrics(this.el);
var scroll = {
x: this.el.scrollLeft,
y: this.el.scrollTop
};
var inner = {
left: outer.left - scroll.x,
top: outer.top - scroll.y,
width: this.el.scrollWidth,
height: this.el.scrollHeight
};
inner.right = inner.left + inner.width;
inner.bottom = inner.top + inner.height;
scroll.ratioX = calculateRatio(scroll.x, inner.width - outer.width);
scroll.ratioY = calculateRatio(scroll.y, inner.height - outer.height);
return { outer: outer, inner: inner, scroll: scroll };
}
}]);
return ElementViewport;
}(Viewport);
/***/ }
/******/ ])
});
;
//# sourceMappingURL=backbone-virtualized-listview.js.map