- {% for page in collections[tag] | sort %}
- {% if not page.data.parent -%}
+ {% for page in children %}
{% include 'sidebar-link.njk' %}
- {%- endif %}
{% endfor %}
diff --git a/docs/_includes/sidebar-link.njk b/docs/_includes/sidebar-link.njk
index dab3e32a0..261a272ec 100644
--- a/docs/_includes/sidebar-link.njk
+++ b/docs/_includes/sidebar-link.njk
@@ -1,4 +1,4 @@
-{% if not (isAlpha and page.data.noAlpha) and page.fileSlug != tag and not page.data.unlisted -%}
+{% if not (isAlpha and page.data.noAlpha) and not page.data.unlisted -%}
{{ page.data.title }}
{% if page.data.status == 'experimental' %}{% endif %}
diff --git a/docs/_layouts/overview.njk b/docs/_layouts/overview.njk
index fbc99b73d..48645d379 100644
--- a/docs/_layouts/overview.njk
+++ b/docs/_layouts/overview.njk
@@ -1,6 +1,5 @@
---
layout: page-outline
-tags: ["overview"]
---
{% set forTag = forTag or (page.url | split('/') | last) %}
{% if description %}
@@ -13,8 +12,10 @@ tags: ["overview"]
-{% set allPages = collections[forTag] %}
+{% set allPages = allPages or collections[forTag] %}
+{% if allPages and allPages.length > 0 %}
{% include "grouped-pages.njk" %}
+{% endif %}
diff --git a/docs/_layouts/theme.njk b/docs/_layouts/theme.njk
index 97c737f41..63693354c 100644
--- a/docs/_layouts/theme.njk
+++ b/docs/_layouts/theme.njk
@@ -68,7 +68,7 @@ wa_data.palettes = {
- {% include "svgs/" + (palette.data.icon or "thumbnail-placeholder") + ".njk" %}
+ {% include "svgs/" + (palette.data.icon or "thumbnail-placeholder") + ".njk" ignore missing %}
{{ palette.data.title }}
diff --git a/docs/_utils/filters.js b/docs/_utils/filters.js
index 912b5dbb1..f81e5b909 100644
--- a/docs/_utils/filters.js
+++ b/docs/_utils/filters.js
@@ -29,6 +29,9 @@ function getCollection(name) {
}
export function getCollectionItemFromUrl(url, collection) {
+ if (!url) {
+ return null;
+ }
collection ??= getCollection.call(this, 'all') || [];
return collection.find(item => item.url === url);
}
@@ -42,35 +45,33 @@ export function split(text, separator) {
return (text + '').split(separator).filter(Boolean);
}
-export function breadcrumbs(url, { withCurrent = false } = {}) {
- const parts = split(url, '/');
- const ret = [];
+export function ancestors(url, { withCurrent = false, withRoot = false } = {}) {
+ let ret = [];
+ let currentUrl = url;
+ let currentItem = getCollectionItemFromUrl.call(this, url);
- while (parts.length) {
- let partialUrl = '/' + parts.join('/') + '/';
- let item = getCollectionItemFromUrl.call(this, partialUrl);
-
- if (item && (partialUrl !== url || withCurrent)) {
- let title = item.data.title;
- if (title) {
- ret.unshift({ url: partialUrl, title });
- }
- }
-
- parts.pop();
-
- if (item?.data.parent) {
- let parentURL = item.data.parent;
- if (!item.data.parent.startsWith('/')) {
- // Parent is in the same directory
- parts.push(item.data.parent);
- parentURL = '/' + parts.join('/') + '/';
- }
-
- let parentBreadcrumbs = breadcrumbs.call(this, parentURL, { withCurrent: true });
- return [...parentBreadcrumbs, ...ret];
+ if (!currentItem) {
+ // Might have eleventyExcludeFromCollections, jump to parent
+ let parentUrl = this.ctx.parentUrl;
+ if (parentUrl) {
+ url = parentUrl;
}
}
+
+ for (let item; (item = getCollectionItemFromUrl.call(this, url)); url = item.data.parentUrl) {
+ ret.unshift(item);
+ }
+
+ if (!withRoot && ret[0]?.page.url === '/') {
+ // Remove root
+ ret.shift();
+ }
+
+ if (!withCurrent && ret.at(-1)?.page.url === currentUrl) {
+ // Remove current page
+ ret.pop();
+ }
+
return ret;
}
@@ -180,69 +181,178 @@ export function sort(arr, by = { 'data.order': 1, 'data.title': '' }) {
/**
* Group an 11ty collection (or any array of objects with a `data.tags` property) by certain tags.
* @param {object[]} collection
- * @param { Object | (string | Object)[]} [tags] The tags to group by. If not provided/empty, defaults to grouping by all tags.
- * @returns { Object. } An object with keys for each tag, and an array of items for each tag.
+ * @param { Object | string[]} [options] Options object or array of tags to group by.
+ * @param {string[] | true} [options.tags] Tags to group by. If true, groups by all tags.
+ * If not provided/empty, defaults to grouping by page hierarchy, with any pages with more than 1 children becoming groups.
+ * @param {string[]} [options.groups] The groups to use if only a subset or a specific order is desired. Defaults to `options.tags`.
+ * @param {string[]} [options.titles] Any title overrides for groups.
+ * @param {string | false} [options.other="Other"] The title to use for the "Other" group. If `false`, the "Other" group is removed..
+ * @returns { Object. } An object of group ids to arrays of page objects.
*/
-export function groupByTags(collection, tags) {
+export function groupPages(collection, options = {}, page) {
if (!collection) {
- console.error(`Empty collection passed to groupByTags() to group by ${JSON.stringify(tags)}`);
- }
- if (!tags) {
- // Default to grouping by union of all tags
- tags = Array.from(new Set(collection.flatMap(item => item.data.tags)));
- } else if (Array.isArray(tags)) {
- // May contain objects of one-off tag -> label mappings
- tags = tags.map(tag => (typeof tag === 'object' ? Object.keys(tag)[0] : tag));
- } else if (typeof tags === 'object') {
- // tags is an object of tags to labels, so we just want the keys
- tags = Object.keys(tags);
+ console.error(`Empty collection passed to groupPages() to group by ${JSON.stringify(options)}`);
}
- let ret = Object.fromEntries(tags.map(tag => [tag, []]));
- ret.other = [];
+ if (Array.isArray(options)) {
+ options = { tags: options };
+ }
+
+ let { tags, groups, titles = {}, other = 'Other' } = options;
+
+ if (groups === undefined && Array.isArray(tags)) {
+ groups = tags;
+ }
+
+ let grouping;
+
+ if (tags) {
+ grouping = {
+ isGroup: item => undefined,
+ getCandidateGroups: item => item.data.tags,
+ getGroupMeta: group => ({}),
+ };
+ } else {
+ grouping = {
+ isGroup: item => (item.data.children.length >= 2 ? item.page.url : undefined),
+ getCandidateGroups: item => {
+ let parentUrl = item.data.parentUrl;
+ if (page?.url === parentUrl) {
+ return [];
+ }
+ return [parentUrl];
+ },
+ getGroupMeta: group => {
+ let item = byUrl[group] || getCollectionItemFromUrl.call(this, group);
+ return {
+ title: item?.data.title,
+ url: group,
+ item,
+ };
+ },
+ sortGroups: groups => sort(groups.map(url => byUrl[url]).filter(Boolean)).map(item => item.page.url),
+ };
+ }
+
+ let byUrl = {};
+ let byParentUrl = {};
for (let item of collection) {
- let categorized = false;
+ let url = item.page.url;
+ let parentUrl = item.data.parentUrl;
- for (let tag of tags) {
- if (item.data.tags.includes(tag)) {
- ret[tag].push(item);
- categorized = true;
- }
- }
+ byUrl[url] = item;
- if (!categorized) {
- ret.other.push(item);
+ if (parentUrl) {
+ byParentUrl[parentUrl] ??= [];
+ byParentUrl[parentUrl].push(item);
}
}
- // Remove empty categories
- for (let category in ret) {
- if (ret[category].length === 0) {
- delete ret[category];
+ let urlToGroups = {};
+
+ for (let item of collection) {
+ let url = item.page.url;
+ let parentUrl = item.data.parentUrl;
+
+ if (grouping.isGroup(item)) {
+ continue;
+ }
+
+ let parentItem = byUrl[parentUrl];
+ if (parentItem && !grouping.isGroup(parentItem)) {
+ // Their parent is also here and is not a group
+ continue;
+ }
+
+ let candidateGroups = grouping.getCandidateGroups(item);
+
+ if (groups) {
+ candidateGroups = candidateGroups.filter(group => groups.includes(group));
+ }
+
+ urlToGroups[url] ??= [];
+
+ for (let group of candidateGroups) {
+ urlToGroups[url].push(group);
+ }
+ }
+
+ let ret = {};
+
+ for (let url in urlToGroups) {
+ let groups = urlToGroups[url];
+ let item = byUrl[url];
+
+ if (groups.length === 0) {
+ // Not filtered out but also not categorized
+ groups = ['other'];
+ }
+
+ for (let group of groups) {
+ ret[group] ??= [];
+ ret[group].push(item);
+
+ if (!ret[group].meta) {
+ if (group === 'other') {
+ ret[group].meta = { title: other };
+ } else {
+ ret[group].meta = grouping.getGroupMeta(group);
+ ret[group].meta.title = titles[group] ?? ret[group].meta.title ?? capitalize(group);
+ }
+ }
+ }
+ }
+
+ if (other === false) {
+ delete ret.other;
+ }
+
+ // Sort
+ let sortedGroups = groups ?? grouping.sortGroups?.(Object.keys(ret));
+
+ if (sortedGroups) {
+ ret = sortObject(ret, sortedGroups);
+ }
+
+ Object.defineProperty(ret, 'meta', {
+ value: {
+ groupCount: Object.keys(ret).length,
+ },
+ enumerable: false,
+ });
+
+ return ret;
+}
+
+/**
+ * Sort an object by its keys
+ * @param {*} obj
+ * @param {function | string[]} order
+ */
+function sortObject(obj, order) {
+ let ret = {};
+ let sortedKeys = Array.isArray(order) ? order : Object.keys(obj).sort(order);
+
+ for (let key of sortedKeys) {
+ if (key in obj) {
+ ret[key] = obj[key];
+ }
+ }
+
+ // Add any keys that weren't in the order
+ for (let key in obj) {
+ if (!(key in ret)) {
+ ret[key] = obj[key];
}
}
return ret;
}
-export function getCategoryTitle(category, categories) {
- let title;
- if (Array.isArray(categories)) {
- // Find relevant entry
- // [{id: "Title"}, id2, ...]
- title = categories.find(entry => typeof entry === 'object' && entry?.[category])?.[category];
- } else if (typeof categories === 'object') {
- // {id: "Title", id2: "Title 2", ...}
- title = categories[category];
- }
-
- if (title) {
- return title;
- }
-
- // Capitalized
- return category.charAt(0).toUpperCase() + category.slice(1);
+function capitalize(str) {
+ str += '';
+ return str.charAt(0).toUpperCase() + str.slice(1);
}
const IDENTITY = x => x;
diff --git a/docs/docs/components/index.njk b/docs/docs/components/index.njk
index f5d19614d..64ddf6e40 100644
--- a/docs/docs/components/index.njk
+++ b/docs/docs/components/index.njk
@@ -2,13 +2,10 @@
title: Components
description: Components are the essential building blocks to create intuitive, cohesive experiences. Browse the library of customizable, framework-friendly web components included in Web Awesome.
layout: overview
-categories:
- - actions
- - feedback: 'Feedback & Status'
- - imagery
- - inputs
- - navigation
- - organization
- - helpers: 'Utilities'
override:tags: []
+categories:
+ tags: [actions, feedback, imagery, inputs, navigation, organization, helpers]
+ titles:
+ feedback: 'Feedback & Status'
+ helpers: 'Utilities'
---
diff --git a/docs/docs/docs.11tydata.js b/docs/docs/docs.11tydata.js
index 7ed1011e1..2232f7e53 100644
--- a/docs/docs/docs.11tydata.js
+++ b/docs/docs/docs.11tydata.js
@@ -1,10 +1,80 @@
+/**
+ * Global data for all pages
+ */
+import { sort } from '../_utils/filters.js';
+
export default {
eleventyComputed: {
- children(data) {
- let mainTag = data.tags?.[0];
- let collection = data.collections[mainTag] ?? [];
+ // Default parent. Can be overridden by explicitly setting parent in the data.
+ // parent can refer to either an ancestor page in the URL or another page in the same directory
+ parent(data) {
+ let { parent, page } = data;
- return collection.filter(item => item.data.parent === data.page.fileSlug);
+ if (parent) {
+ return parent;
+ }
+
+ return page.url.split('/').filter(Boolean).at(-2);
+ },
+
+ parentUrl(data) {
+ let { parent, page } = data;
+ return getParentUrl(page.url, parent);
+ },
+
+ parentItem(data) {
+ let { parentUrl } = data;
+ return data.collections.all.find(item => item.url === parentUrl);
+ },
+
+ children(data) {
+ let { collections, page, parentOf } = data;
+
+ if (parentOf) {
+ return collections[parentOf];
+ }
+
+ let collection = collections.all ?? [];
+ let url = page.url;
+
+ let ret = collection.filter(item => {
+ return item.data.parentUrl === url;
+ });
+
+ sort(ret);
+
+ return ret;
},
},
};
+
+function getParentUrl(url, parent) {
+ let parts = url.split('/').filter(Boolean);
+ let ancestorIndex = parts.findLastIndex(part => part === parent);
+ let retParts = parts.slice();
+
+ if (ancestorIndex > -1) {
+ // parent is an ancestor
+ retParts.splice(ancestorIndex + 1);
+ } else {
+ // parent is a sibling in the same directory
+ retParts.splice(-1, 1, parent);
+ }
+
+ let ret = retParts.join('/');
+
+ if (url.startsWith('/')) {
+ ret = '/' + ret;
+ }
+
+ if (!retParts.at(-1).includes('.') && !ret.endsWith('/')) {
+ // If no extension, make sure to end with a slash
+ ret += '/';
+ }
+
+ if (ret === '/docs/') {
+ ret = '/';
+ }
+
+ return ret;
+}
diff --git a/docs/docs/layout.njk b/docs/docs/layout.njk
index 67d8f8778..57af8bc94 100644
--- a/docs/docs/layout.njk
+++ b/docs/docs/layout.njk
@@ -2,6 +2,7 @@
title: Layout
description: Layout components and utility classes help you organize content that can adapt to any device or screen size. See the [installation instructions](#installation) to use Web Awesome's layout tools in your project.
layout: overview
+parentOf: layout
categories: ["components", "utilities"]
override:tags: []
---
@@ -22,4 +23,4 @@ Or, you can choose to import _only_ the utilities:
```html
```
-{% endmarkdown %}
\ No newline at end of file
+{% endmarkdown %}
diff --git a/docs/docs/palettes/index.njk b/docs/docs/palettes/index.njk
index 9f3eb51f4..36a41a6c3 100644
--- a/docs/docs/palettes/index.njk
+++ b/docs/docs/palettes/index.njk
@@ -5,6 +5,6 @@ layout: overview
override:tags: []
forTag: palette
categories:
+ tags: [other, pro]
other: Free
- pro: Pro
---
diff --git a/docs/docs/patterns/index.njk b/docs/docs/patterns/index.njk
index fee915547..e51a1f6ce 100644
--- a/docs/docs/patterns/index.njk
+++ b/docs/docs/patterns/index.njk
@@ -2,7 +2,5 @@
title: Patterns
description: Patterns are reusable solutions to common design problems.
layout: overview
-categories: ["e-commerce"]
-listChildren: true
override:tags: []
---
diff --git a/docs/docs/themes/index.njk b/docs/docs/themes/index.njk
index fa89792a5..71331c483 100644
--- a/docs/docs/themes/index.njk
+++ b/docs/docs/themes/index.njk
@@ -6,8 +6,8 @@ layout: overview
override:tags: []
forTag: theme
categories:
+ tags: [other, pro]
other: Free
- pro: Pro
---
diff --git a/docs/docs/tokens/index.njk b/docs/docs/tokens/index.njk
index 74337da3f..578b6524f 100644
--- a/docs/docs/tokens/index.njk
+++ b/docs/docs/tokens/index.njk
@@ -3,4 +3,5 @@ title: Design Tokens
description: A theme is a collection of predefined CSS custom properties that control global styles from color to shadows. These custom properties thread through all Web Awesome components for a consistent look and feel.
layout: overview
override:tags: []
+categories: {tags: true}
---