summaryrefslogtreecommitdiff
path: root/node_modules/@11ty/eleventy/src/Plugins/Pagination.js
diff options
context:
space:
mode:
authorShipwreckt <me@shipwreckt.co.uk>2025-10-31 20:02:14 +0000
committerShipwreckt <me@shipwreckt.co.uk>2025-10-31 20:02:14 +0000
commit7a52ddeba2a68388b544f529d2d92104420f77b0 (patch)
tree15ddd47457a2cb4a96060747437d36474e4f6b4e /node_modules/@11ty/eleventy/src/Plugins/Pagination.js
parent53d6ae2b5568437afa5e4995580a3fb679b7b91b (diff)
Changed from static to 11ty!
Diffstat (limited to 'node_modules/@11ty/eleventy/src/Plugins/Pagination.js')
-rwxr-xr-xnode_modules/@11ty/eleventy/src/Plugins/Pagination.js379
1 files changed, 379 insertions, 0 deletions
diff --git a/node_modules/@11ty/eleventy/src/Plugins/Pagination.js b/node_modules/@11ty/eleventy/src/Plugins/Pagination.js
new file mode 100755
index 0000000..8e5b1de
--- /dev/null
+++ b/node_modules/@11ty/eleventy/src/Plugins/Pagination.js
@@ -0,0 +1,379 @@
+import { isPlainObject } from "@11ty/eleventy-utils";
+import lodash from "@11ty/lodash-custom";
+import { DeepCopy } from "@11ty/eleventy-utils";
+
+import EleventyBaseError from "../Errors/EleventyBaseError.js";
+import { ProxyWrap } from "../Util/Objects/ProxyWrap.js";
+// import { DeepFreeze } from "../Util/Objects/DeepFreeze.js";
+import TemplateData from "../Data/TemplateData.js";
+
+const { set: lodashSet, get: lodashGet, chunk: lodashChunk } = lodash;
+
+class PaginationConfigError extends EleventyBaseError {}
+class PaginationError extends EleventyBaseError {}
+
+class Pagination {
+ constructor(tmpl, data, config) {
+ if (!config) {
+ throw new PaginationConfigError("Expected `config` argument to Pagination class.");
+ }
+
+ this.config = config;
+
+ this.setTemplate(tmpl);
+ this.setData(data);
+ }
+
+ get inputPathForErrorMessages() {
+ if (this.template) {
+ return ` (${this.template.inputPath})`;
+ }
+ return "";
+ }
+
+ static hasPagination(data) {
+ return "pagination" in data;
+ }
+
+ hasPagination() {
+ if (!this.data) {
+ throw new Error(
+ `Missing \`setData\` call for Pagination object${this.inputPathForErrorMessages}`,
+ );
+ }
+ return Pagination.hasPagination(this.data);
+ }
+
+ circularReferenceCheck(data) {
+ let key = data.pagination.data;
+ let includedTags = TemplateData.getIncludedTagNames(data);
+
+ for (let tag of includedTags) {
+ if (`collections.${tag}` === key) {
+ throw new PaginationError(
+ `Pagination circular reference${this.inputPathForErrorMessages}, data:\`${key}\` iterates over both the \`${tag}\` collection and also supplies pages to that collection.`,
+ );
+ }
+ }
+ }
+
+ setData(data) {
+ this.data = data || {};
+ this.target = [];
+
+ if (!this.hasPagination()) {
+ return;
+ }
+
+ if (!data.pagination) {
+ throw new Error(
+ `Misconfigured pagination data in template front matter${this.inputPathForErrorMessages} (YAML front matter precaution: did you use tabs and not spaces for indentation?).`,
+ );
+ } else if (!("size" in data.pagination)) {
+ throw new Error(
+ `Missing pagination size in front matter data${this.inputPathForErrorMessages}`,
+ );
+ }
+ this.circularReferenceCheck(data);
+
+ this.size = data.pagination.size;
+ this.alias = data.pagination.alias;
+ this.fullDataSet = this._get(this.data, this._getDataKey());
+ // this returns an array
+ this.target = this._resolveItems();
+ this.chunkedItems = this.pagedItems;
+ }
+
+ setTemplate(tmpl) {
+ this.template = tmpl;
+ }
+
+ _getDataKey() {
+ return this.data.pagination.data;
+ }
+
+ shouldResolveDataToObjectValues() {
+ if ("resolve" in this.data.pagination) {
+ return this.data.pagination.resolve === "values";
+ }
+ return false;
+ }
+
+ isFiltered(value) {
+ if ("filter" in this.data.pagination) {
+ let filtered = this.data.pagination.filter;
+ if (Array.isArray(filtered)) {
+ return filtered.indexOf(value) > -1;
+ }
+
+ return filtered === value;
+ }
+
+ return false;
+ }
+
+ _has(target, key) {
+ let notFoundValue = "__NOT_FOUND_ERROR__";
+ let data = lodashGet(target, key, notFoundValue);
+ return data !== notFoundValue;
+ }
+
+ _get(target, key) {
+ let notFoundValue = "__NOT_FOUND_ERROR__";
+ let data = lodashGet(target, key, notFoundValue);
+ if (data === notFoundValue) {
+ throw new Error(
+ `Could not find pagination data${this.inputPathForErrorMessages}, went looking for: ${key}`,
+ );
+ }
+ return data;
+ }
+
+ _resolveItems() {
+ let keys;
+ if (Array.isArray(this.fullDataSet)) {
+ keys = this.fullDataSet;
+ this.paginationTargetType = "array";
+ } else if (isPlainObject(this.fullDataSet)) {
+ this.paginationTargetType = "object";
+ if (this.shouldResolveDataToObjectValues()) {
+ keys = Object.values(this.fullDataSet);
+ } else {
+ keys = Object.keys(this.fullDataSet);
+ }
+ } else {
+ throw new Error(
+ `Unexpected data found in pagination target${this.inputPathForErrorMessages}: expected an Array or an Object.`,
+ );
+ }
+
+ // keys must be an array
+ let result = keys.slice();
+
+ if (this.data.pagination.before && typeof this.data.pagination.before === "function") {
+ // we don’t need to make a copy of this because we .slice() above to create a new copy
+ let fns = {};
+ if (this.config) {
+ fns = this.config.javascriptFunctions;
+ }
+ result = this.data.pagination.before.call(fns, result, this.data);
+ }
+
+ if (this.data.pagination.reverse === true) {
+ result = result.reverse();
+ }
+
+ if (this.data.pagination.filter) {
+ result = result.filter((value) => !this.isFiltered(value));
+ }
+
+ return result;
+ }
+
+ get pagedItems() {
+ if (!this.data) {
+ throw new Error(
+ `Missing \`setData\` call for Pagination object${this.inputPathForErrorMessages}`,
+ );
+ }
+
+ const chunks = lodashChunk(this.target, this.size);
+ if (this.data.pagination?.generatePageOnEmptyData) {
+ return chunks.length ? chunks : [[]];
+ } else {
+ return chunks;
+ }
+ }
+
+ getPageCount() {
+ if (!this.hasPagination()) {
+ return 0;
+ }
+
+ return this.chunkedItems.length;
+ }
+
+ getNormalizedItems(pageItems) {
+ return this.size === 1 ? pageItems[0] : pageItems;
+ }
+
+ getOverrideDataPages(items, pageNumber) {
+ return {
+ // See Issue #345 for more examples
+ page: {
+ previous: pageNumber > 0 ? this.getNormalizedItems(items[pageNumber - 1]) : null,
+ next: pageNumber < items.length - 1 ? this.getNormalizedItems(items[pageNumber + 1]) : null,
+ first: items.length ? this.getNormalizedItems(items[0]) : null,
+ last: items.length ? this.getNormalizedItems(items[items.length - 1]) : null,
+ },
+
+ pageNumber,
+ };
+ }
+
+ getOverrideDataLinks(pageNumber, templateCount, links) {
+ let obj = {};
+
+ // links are okay but hrefs are better
+ obj.previousPageLink = pageNumber > 0 ? links[pageNumber - 1] : null;
+ obj.previous = obj.previousPageLink;
+
+ obj.nextPageLink = pageNumber < templateCount - 1 ? links[pageNumber + 1] : null;
+ obj.next = obj.nextPageLink;
+
+ obj.firstPageLink = links.length > 0 ? links[0] : null;
+ obj.lastPageLink = links.length > 0 ? links[links.length - 1] : null;
+
+ obj.links = links;
+ // todo deprecated, consistency with collections and use links instead
+ obj.pageLinks = links;
+ return obj;
+ }
+
+ getOverrideDataHrefs(pageNumber, templateCount, hrefs) {
+ let obj = {};
+
+ // hrefs are better than links
+ obj.previousPageHref = pageNumber > 0 ? hrefs[pageNumber - 1] : null;
+ obj.nextPageHref = pageNumber < templateCount - 1 ? hrefs[pageNumber + 1] : null;
+
+ obj.firstPageHref = hrefs.length > 0 ? hrefs[0] : null;
+ obj.lastPageHref = hrefs.length > 0 ? hrefs[hrefs.length - 1] : null;
+
+ obj.hrefs = hrefs;
+
+ // better names
+ obj.href = {
+ previous: obj.previousPageHref,
+ next: obj.nextPageHref,
+ first: obj.firstPageHref,
+ last: obj.lastPageHref,
+ };
+
+ return obj;
+ }
+
+ async getPageTemplates() {
+ if (!this.data) {
+ throw new Error(
+ `Missing \`setData\` call for Pagination object${this.inputPathForErrorMessages}`,
+ );
+ }
+
+ if (!this.hasPagination()) {
+ return [];
+ }
+
+ let entries = [];
+ let items = this.chunkedItems;
+ let pages = this.size === 1 ? items.map((entry) => entry[0]) : items;
+
+ let links = [];
+ let hrefs = [];
+
+ let hasPermalinkField =
+ Boolean(this.data[this.config.keys.permalink]) ||
+ Boolean(this.data.eleventyComputed?.[this.config.keys.permalink]);
+
+ // Do *not* pass collections through DeepCopy, we’ll re-add them back in later.
+ let collections = this.data.collections;
+ if (collections) {
+ delete this.data.collections;
+ }
+
+ let parentData = DeepCopy(
+ {
+ pagination: {
+ data: this.data.pagination.data,
+ size: this.data.pagination.size,
+ alias: this.alias,
+ pages,
+ },
+ },
+ this.data,
+ );
+
+ // Restore skipped collections
+ if (collections) {
+ this.data.collections = collections;
+ // Keep the original reference to the collections, no deep copy!!
+ parentData.collections = collections;
+ }
+
+ // TODO this does work fine but let’s wait on enabling it.
+ // DeepFreeze(parentData, ["collections"]);
+
+ // TODO future improvement dea: use a light Template wrapper for paged template clones (PagedTemplate?)
+ // so that we don’t have the memory cost of the full template (and can reuse the parent
+ // template for some things)
+
+ let indices = new Set();
+ for (let j = 0; j <= items.length - 1; j++) {
+ indices.add(j);
+ }
+
+ for (let pageNumber of indices) {
+ let cloned = await this.template.clone();
+
+ if (pageNumber > 0 && !hasPermalinkField) {
+ cloned.setExtraOutputSubdirectory(pageNumber);
+ }
+
+ let paginationData = {
+ pagination: {
+ items: items[pageNumber],
+ },
+ page: {},
+ };
+ Object.assign(paginationData.pagination, this.getOverrideDataPages(items, pageNumber));
+
+ if (this.alias) {
+ lodashSet(paginationData, this.alias, this.getNormalizedItems(items[pageNumber]));
+ }
+
+ // Do *not* deep merge pagination data! See https://github.com/11ty/eleventy/issues/147#issuecomment-440802454
+ let clonedData = ProxyWrap(paginationData, parentData);
+
+ // Previous method:
+ // let clonedData = DeepCopy(paginationData, parentData);
+
+ let { /*linkInstance,*/ rawPath, path, href } = await cloned.getOutputLocations(clonedData);
+ // TODO subdirectory to links if the site doesn’t live at /
+ if (rawPath) {
+ links.push("/" + rawPath);
+ }
+
+ hrefs.push(href);
+
+ // page.url and page.outputPath are used to avoid another getOutputLocations call later, see Template->addComputedData
+ clonedData.page.url = href;
+ clonedData.page.outputPath = path;
+
+ entries.push({
+ pageNumber,
+
+ // This is used by i18n Plugin to allow subgroups of nested pagination to be separate
+ groupNumber: items[pageNumber]?.[0]?.eleventyPaginationGroupNumber,
+
+ template: cloned,
+ data: clonedData,
+ });
+ }
+
+ // we loop twice to pass in the appropriate prev/next links (already full generated now)
+ let index = 0;
+ for (let pageEntry of entries) {
+ let linksObj = this.getOverrideDataLinks(index, items.length, links);
+
+ Object.assign(pageEntry.data.pagination, linksObj);
+
+ let hrefsObj = this.getOverrideDataHrefs(index, items.length, hrefs);
+ Object.assign(pageEntry.data.pagination, hrefsObj);
+ index++;
+ }
+
+ return entries;
+ }
+}
+
+export default Pagination;