eleventy-plugin-filter-page 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -37
- package/dist/ecf.filter.js +5 -5
- package/dist/ecf.filter.min.js +1 -1
- package/dist/ecf.plugin.js +93 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,15 @@ This Eleventy plugin repository contains two key pieces for "Eleventy Collection
|
|
|
3
3
|
- ecf.plugin.js
|
|
4
4
|
- ecf.filter.js
|
|
5
5
|
|
|
6
|
+
## Examples
|
|
7
|
+
|
|
8
|
+
- Campgames Website:
|
|
9
|
+
- live: [https://campgamesdb.com/](https://campgamesdb.com/)
|
|
10
|
+
- source: [gitlab](https://gitlab.com/excitedfellas/campgames-db)
|
|
11
|
+
- Example directory in this repository:
|
|
12
|
+
- live: [link](https://eleventy-plugin-filter-page-01919e.gitlab.io/filter/)
|
|
13
|
+
- source: [gitlab](https://gitlab.com/excitedfellas/eleventy-plugin-filter-page/-/tree/main/example)
|
|
14
|
+
|
|
6
15
|
## Usage
|
|
7
16
|
### 1. Install the plugin
|
|
8
17
|
```sh
|
|
@@ -19,69 +28,73 @@ There are multiple ways to provide configuration to the plugin, but the configur
|
|
|
19
28
|
|
|
20
29
|
Code for a *complete* 11ty project with an example config and resulting page can be found in the [example folder](./example/). You can preview the example website here: link eventually.
|
|
21
30
|
|
|
22
|
-
|
|
31
|
+
##### Configuration schema
|
|
23
32
|
```ts
|
|
24
33
|
interface PluginConfig {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
collection: string; // Name of the Eleventy collection this config applies to
|
|
35
|
+
identifyingField?: string; // unique field to each item in the collection. Falls back to 'url' when left empty
|
|
36
|
+
filters: Array< // List of filters available for this collection
|
|
37
|
+
{
|
|
38
|
+
type: "radio";
|
|
39
|
+
label: string; // Human-readable label shown in the UI
|
|
40
|
+
field: string; // Front-matter field this filter targets
|
|
41
|
+
} | {
|
|
42
|
+
type: "checkbox";
|
|
43
|
+
label: string;
|
|
44
|
+
field: string;
|
|
45
|
+
} | {
|
|
46
|
+
type: "text";
|
|
47
|
+
label: string;
|
|
48
|
+
field: string;
|
|
49
|
+
/** Optional external <form> ID for wiring into existing markup. See TODO: Step 5 */
|
|
50
|
+
form?: string;
|
|
51
|
+
options?: {
|
|
52
|
+
placeholder?: string; // Placeholder text shown in the input
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
>;
|
|
46
56
|
}
|
|
47
57
|
```
|
|
48
58
|
|
|
49
59
|
#### 3.1 Config as plugin opts
|
|
50
60
|
```js
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
eleventyConfig.addPlugin(ecfPlugin, {
|
|
62
|
+
config: {
|
|
63
|
+
collection: "posts",
|
|
64
|
+
identifyingField: 'url',
|
|
65
|
+
filters: [ /* ... */ ]
|
|
66
|
+
}
|
|
67
|
+
});
|
|
57
68
|
```
|
|
58
69
|
#### 3.2 Separate file
|
|
59
70
|
In case you want to expose your configuration file to a CMS (ex. DecapCMS), you may want to store the configuration in a separate `.json` file and import it as follows:
|
|
60
71
|
```js
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
eleventyConfig.addPlugin(ecfPlugin, {
|
|
73
|
+
config: require('./src/_data/ecfConfig.json')
|
|
74
|
+
});
|
|
64
75
|
```
|
|
65
76
|
|
|
66
77
|
A sample of DecapCMS `config.yml` for this purpose can be seen here: [config.yml](./example/src/admin/config.yml)
|
|
67
78
|
|
|
68
79
|
#### 3.3 _data/ecfConfig.json
|
|
69
80
|
Eleventy exposes files in the `_data` folder as front-matter metadata to the templates. It is also exposed to this plugin, so you can have a config stored in the `ecfConfig` metadata collection, or if you prefer to name it something else, you can import it as follows:
|
|
81
|
+
|
|
70
82
|
```js
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
eleventyConfig.addPlugin(ecfPlugin, {
|
|
84
|
+
configDataField: 'ecfConfig'
|
|
85
|
+
});
|
|
74
86
|
```
|
|
75
87
|
|
|
76
|
-
or rely on ecfConfig default and import
|
|
88
|
+
or rely on `ecfConfig` default and import the plugin with no further `.eleventy.js` configuration:
|
|
89
|
+
|
|
77
90
|
```js
|
|
78
|
-
|
|
91
|
+
eleventyConfig.addPlugin(ecfPlugin);
|
|
79
92
|
```
|
|
80
93
|
|
|
81
94
|
### 4. Create your search page template
|
|
82
95
|
Everyone's styling and HTML layout for their 11ty projects is going to be different. This plugin permits you to adjust the template to your liking, as long as the main building blocks are there.
|
|
83
96
|
|
|
84
|
-
To get the basic version going, just copy the [example template file](./example/src/filter.njk) into your source directory and change
|
|
97
|
+
To get the basic version going, just copy the [example template file](./example/src/filter.njk) into your source directory and change `targetCollection` variable, as well as adjust the `resultItem` macro to match your collection's fields, and you're good to go. For any further modification, rudimentary knowledge of Nunjucks and Eleventy should suffice.
|
|
85
98
|
|
|
86
99
|
If you have suggestions on how to go about improving this step (architecture or instructions), please submit a Pull Request or open an Issue in this repository.
|
|
87
100
|
|
|
@@ -115,6 +128,11 @@ collections:
|
|
|
115
128
|
hint: "Codename for the collection that will be filtered"
|
|
116
129
|
widget: string
|
|
117
130
|
required: true
|
|
131
|
+
- label: "Identifying Field"
|
|
132
|
+
name: "identifyingField"
|
|
133
|
+
hint: "A unique field to each item in the collection"
|
|
134
|
+
widget: string
|
|
135
|
+
default: 'url'
|
|
118
136
|
- label: "Filters"
|
|
119
137
|
name: filters
|
|
120
138
|
widget: "list"
|
package/dist/ecf.filter.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @typedef {Object<string, string | string[]>} StringMap
|
|
7
|
-
* @typedef {StringMap & {
|
|
7
|
+
* @typedef {StringMap & { ecf_id: string }} Entry
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -66,10 +66,10 @@ if (!formTemplateValidation) {
|
|
|
66
66
|
alert("Improperly configured template. All <form> elements with IDs from the filter configuration file must exist in the layout.");
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
// mapping each
|
|
69
|
+
// mapping each ecf_id to an Element
|
|
70
70
|
const idToElementMap = new Map();
|
|
71
|
-
$("[data-
|
|
72
|
-
idToElementMap.set(el.getAttribute("data-
|
|
71
|
+
$("[data-ecf-id]").forEach(el => {
|
|
72
|
+
idToElementMap.set(el.getAttribute("data-ecf-id"), el);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
|
|
@@ -147,7 +147,7 @@ async function hideFilteredData(filteredData) {
|
|
|
147
147
|
idToElementMap.values().forEach(element => element.style.display = "none");
|
|
148
148
|
// then unhide elements that were filtered out
|
|
149
149
|
filteredData.forEach(entry => {
|
|
150
|
-
idToElementMap.get(entry.
|
|
150
|
+
idToElementMap.get(entry.ecf_id).style.display = '';
|
|
151
151
|
});
|
|
152
152
|
}
|
|
153
153
|
|
package/dist/ecf.filter.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
if(globalThis.ecfData,"undefined"==typeof ecfData)throw alert('"ecfData" variable must be defined'),new Error("Global `ecfData` must be defined before this script runs.");if("undefined"==typeof filterConfig)throw alert('"filterConfig" variable must be defined'),new Error("Global `filterConfig` must be defined before this script runs.");function mergeFormData(e,t){for(var[r,n]of t.entries())e.append(r,n);return e}function $(e){return 1==(e=document.querySelectorAll(e)).length?e[0]:e}let defaultFormId="ecf-filters",ecfForms=filterConfig.filter(e=>e.form).map(e=>e.form).concat([defaultFormId]).map(e=>document.getElementById(e)),formTemplateValidation=ecfForms.every(e=>null!=e&&"form"===e.tagName.toLowerCase()),idToElementMap=(formTemplateValidation||alert("Improperly configured template. All <form> elements with IDs from the filter configuration file must exist in the layout."),new Map);async function preProcessFilters(){var e,t,r,n={};for([e,t]of ecfForms.reduce((e,t)=>mergeFormData(e,new FormData(t)),new FormData).entries())"ecf-c"==e.slice(0,5)?(n[(r=e.split(" "))[1]]||(n[r[1]]=[]),"on"==t&&n[r[1]].push(r[2])):"ecf-t"==e.slice(0,5)?(r=e.split(" "),0<t.length&&(n[r[1]]=t)):n[e]=t;return n}async function applyFilterValues(n){return ecfData.filter(function(r){return filterConfig.map(t=>{var e=n[t.field]||!1;if(!e)return!0;switch(t.type){case"radio":return r[t.field]===e;case"checkbox":return e.every(e=>r[t.field].includes(e));case"text":return-1!==r[t.field].toLowerCase().indexOf(e);default:return!1}}).every(e=>e)})}async function hideFilteredData(e){idToElementMap.values().forEach(e=>e.style.display="none"),e.forEach(e=>{idToElementMap.get(e.
|
|
1
|
+
if(globalThis.ecfData,"undefined"==typeof ecfData)throw alert('"ecfData" variable must be defined'),new Error("Global `ecfData` must be defined before this script runs.");if("undefined"==typeof filterConfig)throw alert('"filterConfig" variable must be defined'),new Error("Global `filterConfig` must be defined before this script runs.");function mergeFormData(e,t){for(var[r,n]of t.entries())e.append(r,n);return e}function $(e){return 1==(e=document.querySelectorAll(e)).length?e[0]:e}let defaultFormId="ecf-filters",ecfForms=filterConfig.filter(e=>e.form).map(e=>e.form).concat([defaultFormId]).map(e=>document.getElementById(e)),formTemplateValidation=ecfForms.every(e=>null!=e&&"form"===e.tagName.toLowerCase()),idToElementMap=(formTemplateValidation||alert("Improperly configured template. All <form> elements with IDs from the filter configuration file must exist in the layout."),new Map);async function preProcessFilters(){var e,t,r,n={};for([e,t]of ecfForms.reduce((e,t)=>mergeFormData(e,new FormData(t)),new FormData).entries())"ecf-c"==e.slice(0,5)?(n[(r=e.split(" "))[1]]||(n[r[1]]=[]),"on"==t&&n[r[1]].push(r[2])):"ecf-t"==e.slice(0,5)?(r=e.split(" "),0<t.length&&(n[r[1]]=t)):n[e]=t;return n}async function applyFilterValues(n){return ecfData.filter(function(r){return filterConfig.map(t=>{var e=n[t.field]||!1;if(!e)return!0;switch(t.type){case"radio":return r[t.field]===e;case"checkbox":return e.every(e=>r[t.field].includes(e));case"text":return-1!==r[t.field].toLowerCase().indexOf(e);default:return!1}}).every(e=>e)})}async function hideFilteredData(e){idToElementMap.values().forEach(e=>e.style.display="none"),e.forEach(e=>{idToElementMap.get(e.ecf_id).style.display=""})}function handleFilterInput(){preProcessFilters().then(applyFilterValues).then(hideFilteredData)}$("[data-ecf-id]").forEach(e=>{idToElementMap.set(e.getAttribute("data-ecf-id"),e)}),globalThis.selectAllFilterGroup=function(t){"checkbox"!=filterConfig.find(e=>e.field==t).type&&alert("Misconfiguration of Filters. Select All button must only be present on checkbox filters."),ecfForms.forEach(e=>{e.querySelectorAll('input[type="checkbox"][name^="ecf-c '+t+'"]').forEach(e=>{e.checked=!0})}),handleFilterInput()},globalThis.clearFilterGroup=function(t){var e=filterConfig.find(e=>e.field==t);"radio"==e.type&&ecfForms.forEach(e=>{e.querySelectorAll('input[type="radio"][name="'+t+'"]').forEach(e=>{e.checked=!1})}),"checkbox"==e.type&&ecfForms.forEach(e=>{e.querySelectorAll('input[type="checkbox"][name^="ecf-c '+t+'"]').forEach(e=>{e.checked=!1})}),handleFilterInput()},ecfForms.forEach(e=>{e.addEventListener("input",handleFilterInput),e.addEventListener("onchange",handleFilterInput),e.addEventListener("submit",e=>{e.preventDefault()})});
|
package/dist/ecf.plugin.js
CHANGED
|
@@ -1,11 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @typedef {{type: string, label: string, field: string}} FilterGroupConfig
|
|
3
|
-
* @typedef {{collection: string, filters: Array<FilterGroupConfig
|
|
3
|
+
* @typedef {{collection: string, filters: Array<FilterGroupConfig>, identifyingField?: string }} FilterCollectionConfig
|
|
4
4
|
* @typedef {{items: Array<FilterCollectionConfig>}} EcfConfig
|
|
5
5
|
* @typedef {{configDataField: string} | {config: EcfConfig}} PluginConfig
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const defaultConfigFieldName = "ecfConfig";
|
|
9
|
+
const defaultIdentifyingField = "url";
|
|
10
|
+
|
|
11
|
+
function ecfResolveId(item, key) {
|
|
12
|
+
if (!item || !key) return undefined;
|
|
13
|
+
|
|
14
|
+
// Template-backed items (normal Eleventy collection entries)
|
|
15
|
+
if (item.data && key in item.data) {
|
|
16
|
+
return item.data[key];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Direct property on the item (raw JSON collections)
|
|
20
|
+
if (key in item) {
|
|
21
|
+
return item[key];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Common Eleventy fallbacks
|
|
25
|
+
if (key === "url" && item.url) return item.url;
|
|
26
|
+
if (key === "fileSlug" && item.fileSlug) return item.fileSlug;
|
|
27
|
+
if (key === "inputPath" && item.inputPath) return item.inputPath;
|
|
28
|
+
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
9
31
|
|
|
10
32
|
/**
|
|
11
33
|
* @param {Object} [eleventyConfig] - exposed to a plugin by Eleventy by default
|
|
@@ -14,43 +36,60 @@ const defaultConfigFieldName = "ecfConfig";
|
|
|
14
36
|
function ecfPlugin(eleventyConfig, pluginConfig = null) {
|
|
15
37
|
// config validation
|
|
16
38
|
let configFromMetadata = false;
|
|
17
|
-
let
|
|
18
|
-
|
|
19
|
-
if (pluginConfig.configDataField) {
|
|
20
|
-
configFromMetadata = true;
|
|
21
|
-
}
|
|
22
|
-
else if (pluginConfig.config && typeof pluginConfig.config === 'object') {
|
|
23
|
-
importedConfig = pluginConfig.config;
|
|
24
|
-
}
|
|
25
|
-
else {
|
|
26
|
-
throw new Error("Invalid ECF configuration, missing 'config' or 'configDataField' or 'configFile' property")
|
|
27
|
-
}
|
|
28
|
-
} else {
|
|
29
|
-
// will use "ecfConfig" collection by default
|
|
30
|
-
configFromMetadata = true;
|
|
31
|
-
}
|
|
39
|
+
let finalPluginConfig = {};
|
|
40
|
+
let finalPluginConfigComputed = false;
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
42
|
+
/** @returns {Array<FilterCollectionConfig>} */
|
|
43
|
+
function getFinalConfig(eleventyCollectionApi) {
|
|
44
|
+
if (finalPluginConfigComputed) {
|
|
45
|
+
return finalPluginConfig;
|
|
46
|
+
} else {
|
|
47
|
+
// first checking where to get config from
|
|
48
|
+
if (pluginConfig != null) {
|
|
49
|
+
if (pluginConfig.configDataField) {
|
|
50
|
+
configFromMetadata = true;
|
|
51
|
+
}
|
|
52
|
+
else if (pluginConfig.config && typeof pluginConfig.config === 'object') {
|
|
53
|
+
finalPluginConfig = pluginConfig.config;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
throw new Error("Invalid ECF configuration, missing 'config' or 'configDataField'")
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
// fallback to collectionEntry.data["ecfConfig"] as config source
|
|
60
|
+
configFromMetadata = true;
|
|
41
61
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
62
|
+
|
|
63
|
+
// if from metadata, getting it by using Collection API provided by addCollection's function param
|
|
64
|
+
if (configFromMetadata) {
|
|
65
|
+
const configFieldName = pluginConfig.configDataField || defaultConfigFieldName;
|
|
66
|
+
const dataFieldConfig = eleventyCollectionApi.getAll()[0].data[configFieldName];
|
|
67
|
+
if (dataFieldConfig === undefined || dataFieldConfig === null) {
|
|
68
|
+
throw new Error(`ECF configuration collection "${configFieldName}" not detected`)
|
|
69
|
+
}
|
|
70
|
+
finalPluginConfig = dataFieldConfig;
|
|
71
|
+
}
|
|
72
|
+
if (!finalPluginConfig || Object.keys(finalPluginConfig).length == 0) {
|
|
73
|
+
throw new Error("Invalid ECF configuration")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (Object.prototype.hasOwnProperty.call(finalPluginConfig, "items"))
|
|
77
|
+
finalPluginConfig = finalPluginConfig.items;
|
|
78
|
+
|
|
79
|
+
finalPluginConfigComputed = true;
|
|
80
|
+
return finalPluginConfig;
|
|
46
81
|
}
|
|
47
82
|
|
|
48
|
-
|
|
49
|
-
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Processes all entries in all configured collections for unique field values,
|
|
86
|
+
* then puts the final filter config into the 'ecfFilters' collection */
|
|
87
|
+
eleventyConfig.addCollection("ecfData", function(api) {
|
|
88
|
+
let config = getFinalConfig(api);
|
|
50
89
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
let { collection, filters } =
|
|
90
|
+
const out = {};
|
|
91
|
+
config.forEach(function(filterCollectionConfig) {
|
|
92
|
+
let { collection: collectionName, filters } = filterCollectionConfig;
|
|
54
93
|
// making sure we're not making config collection edits in-place.
|
|
55
94
|
let filterConfig = configFromMetadata ? JSON.parse(JSON.stringify(filters)) : filters;
|
|
56
95
|
|
|
@@ -58,35 +97,48 @@ function ecfPlugin(eleventyConfig, pluginConfig = null) {
|
|
|
58
97
|
for (const filter of filterConfig) {
|
|
59
98
|
filter.values = new Set();
|
|
60
99
|
}
|
|
61
|
-
const entries = api.getFilteredByTag(
|
|
62
|
-
|
|
100
|
+
const entries = api.getFilteredByTag(collectionName);
|
|
101
|
+
out[collectionName] = {};
|
|
102
|
+
out[collectionName].items = entries.map(entry => {
|
|
103
|
+
let ecfDataEntry = {};
|
|
63
104
|
for (const filter of filterConfig) {
|
|
105
|
+
ecfDataEntry[filter.field] = entry.data[filter.field];
|
|
106
|
+
ecfDataEntry["ecf_id"] = ecfResolveId(entry, filterCollectionConfig.identifyingField || defaultIdentifyingField).toString();
|
|
107
|
+
|
|
64
108
|
switch (filter.type) {
|
|
65
109
|
case "radio": {
|
|
66
110
|
let value = entry.data[filter.field];
|
|
67
|
-
|
|
68
|
-
|
|
111
|
+
if (value) {
|
|
112
|
+
let strValue = typeof value !== 'string' ? value.toString() : value;
|
|
113
|
+
filter.values.add(strValue);
|
|
114
|
+
}
|
|
69
115
|
break;
|
|
70
116
|
}
|
|
71
117
|
case "checkbox": {
|
|
72
118
|
entry.data[filter.field]?.forEach(value => {
|
|
73
|
-
|
|
74
|
-
|
|
119
|
+
if (value) {
|
|
120
|
+
let strValue = typeof value !== 'string' ? value.toString() : value;
|
|
121
|
+
filter.values?.add(strValue)
|
|
122
|
+
}
|
|
75
123
|
});
|
|
76
124
|
break;
|
|
77
125
|
}
|
|
78
126
|
}
|
|
79
127
|
}
|
|
128
|
+
return ecfDataEntry;
|
|
80
129
|
});
|
|
81
130
|
|
|
82
131
|
// converting from a Set into Array so templates can read it
|
|
83
132
|
for (const filter of filterConfig) {
|
|
84
133
|
filter.values = [...filter.values].filter(val => val != undefined && val != null);
|
|
85
134
|
}
|
|
86
|
-
|
|
135
|
+
out[collectionName].filters = filterConfig;
|
|
136
|
+
out[collectionName].identifyingField = filterCollectionConfig.identifyingField || defaultIdentifyingField;
|
|
87
137
|
});
|
|
88
|
-
return
|
|
138
|
+
return out;
|
|
89
139
|
});
|
|
140
|
+
|
|
141
|
+
eleventyConfig.addFilter('ecfResolveId', ecfResolveId);
|
|
90
142
|
}
|
|
91
143
|
|
|
92
144
|
module.exports = ecfPlugin;
|