UNPKG

12.6 kBJavaScriptView Raw
1'use strict';
2
3var Base = require('base');
4var debug = require('debug')('base:templates:list');
5var plugin = require('./plugins');
6var Group = require('./group');
7var utils = require('./utils');
8
9/**
10 * Expose `List`
11 */
12
13module.exports = exports = List;
14
15/**
16 * Create an instance of `List` with the given `options`.
17 * Lists differ from collections in that items are stored
18 * as an array, allowing items to be paginated, sorted,
19 * and grouped.
20 *
21 * ```js
22 * var list = new List();
23 * list.addItem('foo', {content: 'bar'});
24 * ```
25 * @param {Object} `options`
26 * @api public
27 */
28
29function List(options) {
30 if (!(this instanceof List)) {
31 return new List(options);
32 }
33
34 Base.call(this);
35
36 this.is('list');
37 this.define('isCollection', true);
38 this.use(utils.option());
39 this.use(utils.plugin());
40 this.init(options || {});
41}
42
43/**
44 * Inherit `Base` and load static plugins
45 */
46
47plugin.static(Base, List, 'List');
48
49/**
50 * Initalize `List` defaults
51 */
52
53List.prototype.init = function(options) {
54 debug('initalizing');
55
56 // add constructors to the instance
57 this.define('Item', options.Item || List.Item);
58 this.define('View', options.View || List.View);
59
60 // decorate the instance
61 this.use(plugin.init);
62 this.use(plugin.renameKey());
63 this.use(plugin.context);
64 this.use(utils.engines());
65 this.use(utils.helpers());
66 this.use(utils.routes());
67 this.use(plugin.item('item', 'Item', {emit: false}));
68 this.use(plugin.item('view', 'View', {emit: false}));
69
70 // decorate `isItem` and `isView`, in case custom class is passed
71 plugin.is(this.Item);
72 plugin.is(this.View);
73
74 this.queue = [];
75 this.items = [];
76 this.keys = [];
77
78 // add some "list" methods to "list.items" to simplify usage in helpers
79 decorate(this, 'paginate');
80 decorate(this, 'filter', 'items');
81 decorate(this, 'groupBy', 'items');
82 decorate(this, 'sortBy', 'items');
83
84 // if an instance of `List` of `Views` is passed, load it now
85 if (Array.isArray(options)) {
86 this.options = {};
87 this.addList(options);
88
89 } else if (options.isList) {
90 this.options = options.options;
91 this.addList(options.items);
92
93 } else if (options.isCollection) {
94 this.options = options.options;
95 this.addItems(options.views);
96
97 } else {
98 this.options = options;
99 }
100};
101
102/**
103 * Add an item to `list.items`. This is identical to [setItem](#setItem)
104 * except `addItem` returns the `item`, add `setItem` returns the instance
105 * of `List`.
106 *
107 * ```js
108 * collection.addItem('foo', {content: 'bar'});
109 * ```
110 *
111 * @param {String|Object} `key` Item key or object
112 * @param {Object} `value` If key is a string, value is the item object.
113 * @developer The `item` method is decorated onto the collection using the `item` plugin
114 * @return {Object} returns the `item` instance.
115 * @api public
116 */
117
118List.prototype.addItem = function(key, value) {
119 var item = this.item(key, value);
120 utils.setInstanceNames(item, 'Item');
121
122 debug('adding item "%s"', item.key);
123
124 if (this.options.pager === true) {
125 List.addPager(item, this.items);
126 }
127
128 this.keys.push(item.key);
129 if (typeof item.use === 'function') {
130 this.run(item);
131 }
132
133 this.extendItem(item);
134 this.emit('load', item, this);
135 this.emit('item', item, this);
136
137 this.items.push(item);
138 return item;
139};
140
141/**
142 * Add an item to `list.items`. This is identical to [addItem](#addItem)
143 * except `addItem` returns the `item`, add `setItem` returns the instance
144 * of `List`.
145 *
146 * ```js
147 * var items = new Items(...);
148 * items.setItem('a.html', {path: 'a.html', contents: '...'});
149 * ```
150 * @param {String} `key`
151 * @param {Object} `value`
152 * @return {Object} Returns the instance of `List` to support chaining.
153 * @api public
154 */
155
156List.prototype.setItem = function(/*key, value*/) {
157 this.addItem.apply(this, arguments);
158 return this;
159};
160
161/**
162 * Load multiple items onto the collection.
163 *
164 * ```js
165 * collection.addItems({
166 * 'a.html': {content: '...'},
167 * 'b.html': {content: '...'},
168 * 'c.html': {content: '...'}
169 * });
170 * ```
171 * @param {Object|Array} `items`
172 * @return {Object} returns the instance for chaining
173 * @api public
174 */
175
176List.prototype.addItems = function(items) {
177 if (Array.isArray(items)) {
178 return this.addList.apply(this, arguments);
179 }
180
181 this.emit('addItems', items);
182 if (this.loaded) {
183 this.loaded = false;
184 return this;
185 }
186
187 this.visit('addItem', items);
188 return this;
189};
190
191/**
192 * Load an array of items or the items from another instance of `List`.
193 *
194 * ```js
195 * var foo = new List(...);
196 * var bar = new List(...);
197 * bar.addList(foo);
198 * ```
199 * @param {Array} `items` or an instance of `List`
200 * @param {Function} `fn` Optional sync callback function that is called on each item.
201 * @return {Object} returns the List instance for chaining
202 * @api public
203 */
204
205List.prototype.addList = function(list, fn) {
206 this.emit.call(this, 'addList', list);
207 if (this.loaded) {
208 this.loaded = false;
209 return this;
210 }
211
212 if (!Array.isArray(list)) {
213 throw new TypeError('expected list to be an array.');
214 }
215
216 var len = list.length, i = -1;
217 while (++i < len) {
218 var item = list[i];
219 if (typeof fn === 'function') {
220 item = fn(item) || item;
221 }
222 this.addItem(item.path, item);
223 }
224 return this;
225};
226
227/**
228 * Return true if the list has the given item (name).
229 *
230 * ```js
231 * list.addItem('foo.html', {content: '...'});
232 * list.hasItem('foo.html');
233 * //=> true
234 * ```
235 * @param {String} `key`
236 * @return {Object}
237 * @api public
238 */
239
240List.prototype.hasItem = function(key) {
241 return this.getIndex(key) !== -1;
242};
243
244/**
245 * Get a the index of a specific item from the list by `key`.
246 *
247 * ```js
248 * list.getIndex('foo.html');
249 * //=> 1
250 * ```
251 * @param {String} `key`
252 * @return {Object}
253 * @api public
254 */
255
256List.prototype.getIndex = function(key) {
257 if (typeof key === 'undefined') {
258 throw new Error('expected a string or instance of Item');
259 }
260
261 if (List.isItem(key)) {
262 key = key.key;
263 }
264
265 var idx = this.keys.indexOf(key);
266 if (idx !== -1) {
267 return idx;
268 }
269
270 idx = this.keys.indexOf(this.renameKey(key));
271 if (idx !== -1) {
272 return idx;
273 }
274
275 var items = this.items;
276 var len = items.length;
277
278 while (len--) {
279 var item = items[len];
280 var prop = this.renameKey(key, item);
281 if (utils.matchFile(prop, item)) {
282 return len;
283 }
284 }
285 return -1;
286};
287
288/**
289 * Get a specific item from the list by `key`.
290 *
291 * ```js
292 * list.getItem('foo.html');
293 * //=> '<Item <foo.html>>'
294 * ```
295 * @param {String} `key` The item name/key.
296 * @return {Object}
297 * @api public
298 */
299
300List.prototype.getItem = function(key) {
301 var idx = this.getIndex(key);
302 if (idx !== -1) {
303 return this.items[idx];
304 }
305};
306
307/**
308 * Proxy for `getItem`
309 *
310 * ```js
311 * list.getItem('foo.html');
312 * //=> '<Item "foo.html" <buffer e2 e2 e2>>'
313 * ```
314 * @param {String} `key` Pass the key of the `item` to get.
315 * @return {Object}
316 * @api public
317 */
318
319List.prototype.getView = function() {
320 return this.getItem.apply(this, arguments);
321};
322
323/**
324 * Remove an item from the list.
325 *
326 * ```js
327 * list.deleteItem('a.html');
328 * ```
329 * @param {Object|String} `key` Pass an `item` instance (object) or `item.key` (string).
330 * @api public
331 */
332
333List.prototype.deleteItem = function(item) {
334 var idx = this.getIndex(item);
335 if (idx !== -1) {
336 this.items.splice(idx, 1);
337 this.keys.splice(idx, 1);
338 }
339 return this;
340};
341
342/**
343 * Remove one or more items from the list.
344 *
345 * ```js
346 * list.deleteItems(['a.html', 'b.html']);
347 * ```
348 * @param {Object|String|Array} `items` List of items to remove.
349 * @api public
350 */
351
352List.prototype.deleteItems = function(items) {
353 if (!Array.isArray(items)) {
354 return this.deleteItem.apply(this, arguments);
355 }
356 for (var i = 0; i < items.length; i++) {
357 this.deleteItem(items[i]);
358 }
359 return this.items;
360};
361
362/**
363 * Decorate each item on the list with additional methods
364 * and properties. This provides a way of easily overriding
365 * defaults.
366 *
367 * @param {Object} `item`
368 * @return {Object} Instance of item for chaining
369 * @api public
370 */
371
372List.prototype.extendItem = function(item) {
373 plugin.view(this, item);
374 return this;
375};
376
377/**
378 * Filters list `items` using the given `fn` and returns
379 * a new array.
380 *
381 * ```js
382 * var items = list.filter(function(item) {
383 * return item.data.title.toLowerCase() !== 'home';
384 * });
385 * ```
386 * @return {Object} Returns a filtered array of items.
387 * @api public
388 */
389
390List.prototype.filter = function(fn) {
391 var list = new List(this.options);
392 list.addItems(this.items.slice().filter(fn));
393 return list;
394};
395
396/**
397 * Sort all list `items` using the given property,
398 * properties or compare functions. See [array-sort][]
399 * for the full range of available features and options.
400 *
401 * ```js
402 * var list = new List();
403 * list.addItems(...);
404 * var result = list.sortBy('data.date');
405 * //=> new sorted list
406 * ```
407 * @return {Object} Returns a new `List` instance with sorted items.
408 * @api public
409 */
410
411List.prototype.sortBy = function(items) {
412 var args = [].slice.call(arguments);
413 var last = args[args.length - 1];
414 var opts = this.options.sort || {};
415
416 // extend `list.options.sort` global options with local options
417 if (last && typeof last === 'object' && !Array.isArray(last)) {
418 opts = utils.extend({}, opts, args.pop());
419 }
420
421 if (!Array.isArray(items)) {
422 args.unshift(this.items.slice());
423 }
424
425 // create the args to pass to array-sort
426 args.push(opts);
427
428 // sort the `items` array, then sort `keys`
429 items = utils.sortBy.apply(utils.sortBy, args);
430 var list = new List(this.options);
431 list.addItems(items);
432 return list;
433};
434
435/**
436 * Group all list `items` using the given property, properties or
437 * compare functions. See [group-array][] for the full range of available
438 * features and options.
439 *
440 * ```js
441 * var list = new List();
442 * list.addItems(...);
443 * var groups = list.groupBy('data.date', 'data.slug');
444 * ```
445 * @return {Object} Returns the grouped items.
446 * @api public
447 */
448
449List.prototype.groupBy = function(items) {
450 var args = arguments;
451 var fn = utils.groupBy;
452
453 if (!Array.isArray(items)) {
454 // Make `items` the first argument for paginationator
455 args = [].slice.call(arguments);
456 args.unshift(this.items.slice());
457 }
458
459 // group the `items` and return the result
460 return new Group(fn.apply(fn, args));
461};
462
463/**
464 * Paginate all `items` in the list with the given options,
465 * See [paginationator][] for the full range of available
466 * features and options.
467 *
468 * ```js
469 * var list = new List(items);
470 * var pages = list.paginate({limit: 5});
471 * ```
472 * @return {Object} Returns the paginated items.
473 * @api public
474 */
475
476List.prototype.paginate = function(items, options) {
477 var fn = utils.paginationator;
478 var args = arguments;
479
480 if (!Array.isArray(items)) {
481 args = [].slice.call(arguments);
482 // Make `items` the first argument for paginationator
483 args.unshift(this.items.slice());
484 }
485
486 // paginate the `items` and return the pages
487 items = fn.apply(fn, args);
488 return items.pages;
489};
490
491/**
492 * Resolve the layout to use for the given `item`
493 *
494 * @param {Object} `item`
495 * @return {String} Returns the name of the layout to use.
496 */
497
498List.prototype.resolveLayout = function(item) {
499 if (utils.isRenderable(item) && typeof item.layout === 'undefined') {
500 return this.option('layout');
501 }
502 return item.layout;
503};
504
505/**
506 * Add paging (`prev`/`next`) information to the
507 * `data` object of an item.
508 *
509 * @param {Object} `item`
510 * @param {Array} `items` instance items.
511 */
512
513List.addPager = function addPager(item, items) {
514 var len = items.length;
515 var prev = items[len - 1] || null;
516
517 item.data.pager = {};
518 utils.define(item.data.pager, 'isPager', true);
519 utils.define(item.data.pager, 'current', item);
520
521 item.data.pager.index = len;
522 item.data.pager.isFirst = len === 0;
523 item.data.pager.first = items[0] || item;
524 item.data.pager.prev = prev;
525
526 if (prev) {
527 prev.data.pager.next = item;
528 }
529
530 Object.defineProperty(item.data.pager, 'isLast', {
531 configurable: true,
532 enumerable: true,
533 get: function() {
534 return this.index === items.length - 1;
535 }
536 });
537
538 Object.defineProperty(item.data.pager, 'last', {
539 configurable: true,
540 enumerable: true,
541 get: function() {
542 return items[items.length - 1];
543 }
544 });
545};
546
547/**
548 * Expose static properties
549 */
550
551utils.define(List, 'Item', require('vinyl-item'));
552utils.define(List, 'View', require('vinyl-view'));
553
554/**
555 * Helper function for decorating methods onto the `list.items` array
556 */
557
558function decorate(list, method, prop) {
559 utils.define(list.items, method, function() {
560 var res = list[method].apply(list, arguments);
561 return prop ? res[prop] : res;
562 });
563}