UNPKG

10.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(opts) {
54 debug('initalizing');
55
56 // add constructors to the instance
57 this.define('Item', opts.Item || List.Item);
58 this.define('View', opts.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 // if an instance of `List` of `Views` is passed, load it now
79 if (Array.isArray(opts) || opts.isList) {
80 this.options = opts.options || {};
81 this.addList(opts.items || opts);
82
83 } else if (opts.isCollection) {
84 this.options = opts.options;
85 this.addItems(opts.views);
86
87 } else {
88 this.options = opts;
89 }
90};
91
92/**
93 * Add an item to `list.items`. This is identical to [setItem](#setItem)
94 * except `addItem` returns the `item`, add `setItem` returns the instance
95 * of `List`.
96 *
97 * ```js
98 * collection.addItem('foo', {content: 'bar'});
99 * ```
100 *
101 * @param {String|Object} `key` Item key or object
102 * @param {Object} `value` If key is a string, value is the item object.
103 * @developer The `item` method is decorated onto the collection using the `item` plugin
104 * @return {Object} returns the `item` instance.
105 * @api public
106 */
107
108List.prototype.addItem = function(key, value) {
109 var item = this.item(key, value);
110 utils.setInstanceNames(item, 'Item');
111
112 debug('adding item "%s"', item.key);
113
114 if (this.options.pager === true) {
115 List.addPager(item, this.items);
116 }
117
118 this.keys.push(item.key);
119 if (typeof item.use === 'function') {
120 this.run(item);
121 }
122
123 this.extendItem(item);
124 this.emit('load', item, this);
125 this.emit('item', item, this);
126
127 this.items.push(item);
128 return item;
129};
130
131/**
132 * Add an item to `list.items`. This is identical to [addItem](#addItem)
133 * except `addItem` returns the `item`, add `setItem` returns the instance
134 * of `List`.
135 *
136 * ```js
137 * var items = new Items(...);
138 * items.setItem('a.html', {path: 'a.html', contents: '...'});
139 * ```
140 * @param {String} `key`
141 * @param {Object} `value`
142 * @return {Object} Returns the instance of `List` to support chaining.
143 * @api public
144 */
145
146List.prototype.setItem = function(/*key, value*/) {
147 this.addItem.apply(this, arguments);
148 return this;
149};
150
151/**
152 * Load multiple items onto the collection.
153 *
154 * ```js
155 * collection.addItems({
156 * 'a.html': {content: '...'},
157 * 'b.html': {content: '...'},
158 * 'c.html': {content: '...'}
159 * });
160 * ```
161 * @param {Object|Array} `items`
162 * @return {Object} returns the instance for chaining
163 * @api public
164 */
165
166List.prototype.addItems = function(items) {
167 if (Array.isArray(items)) {
168 return this.addList.apply(this, arguments);
169 }
170
171 this.emit('addItems', items);
172 if (this.loaded) {
173 this.loaded = false;
174 return this;
175 }
176
177 this.visit('addItem', items);
178 return this;
179};
180
181/**
182 * Load an array of items or the items from another instance of `List`.
183 *
184 * ```js
185 * var foo = new List(...);
186 * var bar = new List(...);
187 * bar.addList(foo);
188 * ```
189 * @param {Array} `items` or an instance of `List`
190 * @param {Function} `fn` Optional sync callback function that is called on each item.
191 * @return {Object} returns the List instance for chaining
192 * @api public
193 */
194
195List.prototype.addList = function(list, fn) {
196 this.emit.call(this, 'addList', list);
197 if (this.loaded) {
198 this.loaded = false;
199 return this;
200 }
201
202 if (!Array.isArray(list)) {
203 throw new TypeError('expected list to be an array.');
204 }
205
206 if (typeof fn !== 'function') {
207 fn = utils.identity;
208 }
209
210 var len = list.length, i = -1;
211 while (++i < len) {
212 var item = list[i];
213 fn(item);
214 this.addItem(item.path, item);
215 }
216 return this;
217};
218
219/**
220 * Return true if the list has the given item (name).
221 *
222 * ```js
223 * list.addItem('foo.html', {content: '...'});
224 * list.hasItem('foo.html');
225 * //=> true
226 * ```
227 * @param {String} `key`
228 * @return {Object}
229 * @api public
230 */
231
232List.prototype.hasItem = function(key) {
233 return this.getIndex(key) !== -1;
234};
235
236/**
237 * Get a the index of a specific item from the list by `key`.
238 *
239 * ```js
240 * list.getIndex('foo.html');
241 * //=> 1
242 * ```
243 * @param {String} `key`
244 * @return {Object}
245 * @api public
246 */
247
248List.prototype.getIndex = function(key) {
249 if (typeof key === 'undefined') {
250 throw new Error('expected a string or instance of Item');
251 }
252
253 if (List.isItem(key)) {
254 key = key.key;
255 }
256
257 var idx = this.keys.indexOf(key);
258 if (idx !== -1) {
259 return idx;
260 }
261
262 idx = this.keys.indexOf(this.renameKey(key));
263 if (idx !== -1) {
264 return idx;
265 }
266
267 var items = this.items;
268 var len = items.length;
269
270 while (len--) {
271 var item = items[len];
272 var prop = this.renameKey(key, item);
273 if (utils.matchFile(prop, item)) {
274 return len;
275 }
276 }
277 return -1;
278};
279
280/**
281 * Get a specific item from the list by `key`.
282 *
283 * ```js
284 * list.getItem('foo.html');
285 * //=> '<Item <foo.html>>'
286 * ```
287 * @param {String} `key` The item name/key.
288 * @return {Object}
289 * @api public
290 */
291
292List.prototype.getItem = function(key) {
293 var idx = this.getIndex(key);
294 if (idx !== -1) {
295 return this.items[idx];
296 }
297};
298
299/**
300 * Proxy for `getItem`
301 *
302 * ```js
303 * list.getItem('foo.html');
304 * //=> '<Item "foo.html" <buffer e2 e2 e2>>'
305 * ```
306 * @param {String} `key` Pass the key of the `item` to get.
307 * @return {Object}
308 * @api public
309 */
310
311List.prototype.getView = function() {
312 return this.getItem.apply(this, arguments);
313};
314
315/**
316 * Remove an item from the list.
317 *
318 * ```js
319 * list.deleteItem('a.html');
320 * ```
321 * @param {Object|String} `key` Pass an `item` instance (object) or `item.key` (string).
322 * @api public
323 */
324
325List.prototype.deleteItem = function(item) {
326 var idx = this.getIndex(item);
327 if (idx !== -1) {
328 this.items.splice(idx, 1);
329 this.keys.splice(idx, 1);
330 }
331 return this;
332};
333
334/**
335 * Decorate each item on the list with additional methods
336 * and properties. This provides a way of easily overriding
337 * defaults.
338 *
339 * @param {Object} `item`
340 * @return {Object} Instance of item for chaining
341 * @api public
342 */
343
344List.prototype.extendItem = function(item) {
345 plugin.view(this, item);
346 return this;
347};
348
349/**
350 * Group all list `items` using the given property,
351 * properties or compare functions. See [group-array][]
352 * for the full range of available features and options.
353 *
354 * ```js
355 * var list = new List();
356 * list.addItems(...);
357 * var groups = list.groupBy('data.date', 'data.slug');
358 * ```
359 * @return {Object} Returns the grouped items.
360 * @api public
361 */
362
363List.prototype.groupBy = function() {
364 var args = [].slice.call(arguments);
365 var fn = utils.groupBy;
366
367 // Make `items` the first argument for group-array
368 args.unshift(this.items.slice());
369
370 // group the `items` and return the result
371 return new Group(fn.apply(fn, args));
372};
373
374/**
375 * Sort all list `items` using the given property,
376 * properties or compare functions. See [array-sort][]
377 * for the full range of available features and options.
378 *
379 * ```js
380 * var list = new List();
381 * list.addItems(...);
382 * var result = list.sortBy('data.date');
383 * //=> new sorted list
384 * ```
385 * @return {Object} Returns a new `List` instance with sorted items.
386 * @api public
387 */
388
389List.prototype.sortBy = function() {
390 var args = [].slice.call(arguments);
391 var last = args[args.length - 1];
392 var opts = this.options.sort || {};
393
394 // extend `list.options.sort` global options with local options
395 if (last && typeof last === 'object' && !Array.isArray(last)) {
396 opts = utils.extend({}, opts, args.pop());
397 }
398
399 // create the args to pass to array-sort
400 args.unshift(this.items.slice());
401 args.push(opts);
402
403 // sort the `items` array, then sort `keys`
404 var items = utils.sortBy.apply(utils.sortBy, args);
405 var list = new List(this.options);
406 list.addItems(items);
407 return list;
408};
409
410/**
411 * Resolve the layout to use for the given `item`
412 *
413 * @param {Object} `item`
414 * @return {String} Returns the name of the layout to use.
415 */
416
417List.prototype.resolveLayout = function(item) {
418 if (utils.isRenderable(item) && typeof item.layout === 'undefined') {
419 return this.option('layout');
420 }
421 return item.layout;
422};
423
424/**
425 * Paginate all `items` in the list with the given options,
426 * See [paginationator][] for the full range of available
427 * features and options.
428 *
429 * ```js
430 * var list = new List(items);
431 * var pages = list.paginate({limit: 5});
432 * ```
433 * @return {Object} Returns the paginated items.
434 * @api public
435 */
436
437List.prototype.paginate = function() {
438 var args = [].slice.call(arguments);
439 var fn = utils.paginationator;
440
441 // Make `items` the first argument for paginationator
442 args.unshift(this.items.slice());
443
444 // paginate the `items` and return the pages
445 var items = fn.apply(fn, args);
446 return items.pages;
447};
448
449/**
450 * Add paging (`prev`/`next`) information to the
451 * `data` object of an item.
452 *
453 * @param {Object} `item`
454 * @param {Array} `items` instance items.
455 */
456
457List.addPager = function addPager(item, items) {
458 item.data.pager = {};
459 item.data.pager.index = items.length;
460 utils.define(item.data.pager, 'current', item);
461
462 if (items.length) {
463 var prev = items[items.length - 1];
464 utils.define(item.data.pager, 'prev', prev);
465 utils.define(prev.data.pager, 'next', item);
466 }
467};
468
469/**
470 * Expose static properties
471 */
472
473utils.define(List, 'Item', require('vinyl-item'));
474utils.define(List, 'View', require('vinyl-view'));