eleventy-plugin-filter-page 0.1.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/LICENSE +7 -0
- package/README.md +161 -0
- package/dist/ecf.filter.js +197 -0
- package/dist/ecf.filter.min.js +1 -0
- package/dist/ecf.plugin.js +92 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2026 Daniel Maiovskyi
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# eleventy-plugin-filter-page
|
|
2
|
+
This Eleventy plugin repository contains two key pieces for "Eleventy Collection Filter" functionality:
|
|
3
|
+
- ecf.plugin.js
|
|
4
|
+
- ecf.filter.js
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
### 1. Install the plugin
|
|
8
|
+
```sh
|
|
9
|
+
npm install eleventy-plugin-filter-page
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### 2. Add this plugin to your config
|
|
13
|
+
```js
|
|
14
|
+
eleventyConfig.addPlugin(ecfPlugin);
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### 3. Configure the filters
|
|
18
|
+
There are multiple ways to provide configuration to the plugin, but the configuration itself follows the same schema.
|
|
19
|
+
|
|
20
|
+
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
|
+
|
|
22
|
+
#### Configuration schema
|
|
23
|
+
```ts
|
|
24
|
+
interface PluginConfig {
|
|
25
|
+
collection: string; // Name of the Eleventy collection this config applies to
|
|
26
|
+
filters: Array< // List of filters available for this collection
|
|
27
|
+
{
|
|
28
|
+
type: "radio";
|
|
29
|
+
label: string; // Human-readable label shown in the UI
|
|
30
|
+
field: string; // Front-matter field this filter targets
|
|
31
|
+
} | {
|
|
32
|
+
type: "checkbox";
|
|
33
|
+
label: string;
|
|
34
|
+
field: string;
|
|
35
|
+
} | {
|
|
36
|
+
type: "text";
|
|
37
|
+
label: string;
|
|
38
|
+
field: string;
|
|
39
|
+
/** Optional external <form> ID for wiring into existing markup. See TODO: Step 5 */
|
|
40
|
+
form?: string;
|
|
41
|
+
options?: {
|
|
42
|
+
placeholder?: string; // Placeholder text shown in the input
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
>;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
#### 3.1 Config as plugin opts
|
|
50
|
+
```js
|
|
51
|
+
eleventyConfig.addPlugin(ecfPlugin, {
|
|
52
|
+
config: {
|
|
53
|
+
collection: "posts",
|
|
54
|
+
filters: [ /* ... */ ]
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
#### 3.2 Separate file
|
|
59
|
+
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
|
+
```js
|
|
61
|
+
eleventyConfig.addPlugin(ecfPlugin, {
|
|
62
|
+
config: require('./src/_data/ecfConfig.json')
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
A sample of DecapCMS `config.yml` for this purpose can be seen here: [config.yml](./example/src/admin/config.yml)
|
|
67
|
+
|
|
68
|
+
#### 3.3 _data/ecfConfig.json
|
|
69
|
+
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:
|
|
70
|
+
```js
|
|
71
|
+
eleventyConfig.addPlugin(ecfPlugin, {
|
|
72
|
+
configDataField: 'ecfConfig'
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
or rely on ecfConfig default and import it without settings:
|
|
77
|
+
```js
|
|
78
|
+
eleventyConfig.addPlugin(ecfPlugin);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 4. Create your search page template
|
|
82
|
+
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
|
+
|
|
84
|
+
To get the basic version going, just copy the [example template file](./example/src/filter.njk) into your source directory and change the variables `targetCollection` and `identifyingField`, 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
|
+
|
|
86
|
+
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
|
+
|
|
88
|
+
## DecapCMS
|
|
89
|
+
In case you want to expose the configuration file to your CMS, here is the DecapCMS's configuration schema that matches the plugin's config [schema](#configuration-schema).
|
|
90
|
+
|
|
91
|
+
```yml
|
|
92
|
+
# DecapCMS configuration excerpt.
|
|
93
|
+
collections:
|
|
94
|
+
- label: "Configurations"
|
|
95
|
+
name: "configurations"
|
|
96
|
+
files:
|
|
97
|
+
- label: "Filter Configuration"
|
|
98
|
+
name: "ecfConfig"
|
|
99
|
+
file: "src/_data/ecfConfig.json"
|
|
100
|
+
preview_path: /search
|
|
101
|
+
description: "Filter Configuration"
|
|
102
|
+
extension: json
|
|
103
|
+
format: json
|
|
104
|
+
fields:
|
|
105
|
+
- label: "Configuration Items"
|
|
106
|
+
name: "items"
|
|
107
|
+
widget: list
|
|
108
|
+
summary: "Search Page config for Collection: \"{{ fields.collection }}\""
|
|
109
|
+
allow_add: false
|
|
110
|
+
allow_remove: false
|
|
111
|
+
allow_reorder: false
|
|
112
|
+
fields:
|
|
113
|
+
- label: "Collection ID (string)"
|
|
114
|
+
name: "collection"
|
|
115
|
+
hint: "Codename for the collection that will be filtered"
|
|
116
|
+
widget: string
|
|
117
|
+
required: true
|
|
118
|
+
- label: "Filters"
|
|
119
|
+
name: filters
|
|
120
|
+
widget: "list"
|
|
121
|
+
label_singular: "filter"
|
|
122
|
+
summary: "{{fields.label }} - {{fields.type }}"
|
|
123
|
+
fields:
|
|
124
|
+
- label: "Type"
|
|
125
|
+
name: "type"
|
|
126
|
+
hint: "Type of input for the filter"
|
|
127
|
+
widget: select
|
|
128
|
+
default: "checkbox"
|
|
129
|
+
options: ['radio', 'checkbox', 'text']
|
|
130
|
+
- label: "Label"
|
|
131
|
+
hint: "Display text for a filter"
|
|
132
|
+
name: "label"
|
|
133
|
+
widget: string
|
|
134
|
+
default: ""
|
|
135
|
+
- label: "Field"
|
|
136
|
+
name: "field"
|
|
137
|
+
hint: "Field that the collection will be filtered by (usually lowercase)"
|
|
138
|
+
widget: string
|
|
139
|
+
default: ""
|
|
140
|
+
- label: "External Form ID"
|
|
141
|
+
name: "form"
|
|
142
|
+
widget: string
|
|
143
|
+
hint: "If you are using a custom <form> template, you need to set this string for it to be found by the script"
|
|
144
|
+
default: ""
|
|
145
|
+
required: false
|
|
146
|
+
- label: "Options"
|
|
147
|
+
name: "options"
|
|
148
|
+
widget: object
|
|
149
|
+
default: {}
|
|
150
|
+
required: false
|
|
151
|
+
fields:
|
|
152
|
+
- label: "Placeholder"
|
|
153
|
+
name: "placeholder"
|
|
154
|
+
widget: string
|
|
155
|
+
hint: "Placeholder for text input"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## TODO
|
|
159
|
+
- NPM publish .gitlab-ci script
|
|
160
|
+
- example website .gitlab-ci script
|
|
161
|
+
- more filter types (number range)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/* The following constants must be defined before this script loads: */
|
|
2
|
+
/* global ecfData:readonly -- the data to filter */
|
|
3
|
+
/* global filterConfig:readonly -- configuration for the filter */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object<string, string | string[]>} StringMap
|
|
7
|
+
* @typedef {StringMap & { _id: string }} Entry
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Defined in the template file as JSON input for the script.
|
|
12
|
+
* @type {Entry[]} */
|
|
13
|
+
globalThis.ecfData;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Defined in the template file. Direct forward of the filter's JSON config file into a JS Object variable.
|
|
17
|
+
* @type {Object[]} */
|
|
18
|
+
globalThis.filterConfig;
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if (typeof ecfData === "undefined") {
|
|
22
|
+
alert('"ecfData" variable must be defined');
|
|
23
|
+
throw new Error("Global `ecfData` must be defined before this script runs.");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (typeof filterConfig === "undefined") {
|
|
27
|
+
alert('"filterConfig" variable must be defined');
|
|
28
|
+
throw new Error("Global `filterConfig` must be defined before this script runs.");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* BEGIN Helper Functions */
|
|
32
|
+
/**
|
|
33
|
+
* Merges `source` into `target`.
|
|
34
|
+
* @param {FormData} target
|
|
35
|
+
* @param {FormData} source
|
|
36
|
+
* @returns {FormData} `target` reference */
|
|
37
|
+
function mergeFormData(target, source) {
|
|
38
|
+
for (const [key, value] of source.entries()) {
|
|
39
|
+
target.append(key, value);
|
|
40
|
+
}
|
|
41
|
+
return target;
|
|
42
|
+
}
|
|
43
|
+
/** @returns {Element|Element[]} element */
|
|
44
|
+
function $(query) {
|
|
45
|
+
let q;
|
|
46
|
+
return (q = document.querySelectorAll(query)).length == 1 ? q[0] : q;
|
|
47
|
+
}
|
|
48
|
+
/* END Helper Functions */
|
|
49
|
+
|
|
50
|
+
/* BEGIN Script */
|
|
51
|
+
const defaultFormId = "ecf-filters";
|
|
52
|
+
|
|
53
|
+
const ecfForms = filterConfig
|
|
54
|
+
.filter(filter => filter.form)
|
|
55
|
+
.map(f => f.form)
|
|
56
|
+
.concat([defaultFormId])
|
|
57
|
+
.map(idString => {
|
|
58
|
+
return document.getElementById(idString);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Minor template Validation
|
|
62
|
+
const formTemplateValidation = ecfForms.every(el => {
|
|
63
|
+
return (el != null && el != undefined && el.tagName.toLowerCase() === 'form');
|
|
64
|
+
});
|
|
65
|
+
if (!formTemplateValidation) {
|
|
66
|
+
alert("Improperly configured template. All <form> elements with IDs from the filter configuration file must exist in the layout.");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// mapping each _id to an Element
|
|
70
|
+
const idToElementMap = new Map();
|
|
71
|
+
$("[data-search-id]").forEach(el => {
|
|
72
|
+
idToElementMap.set(el.getAttribute("data-search-id"), el);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Maps field name to current filter value from all forms
|
|
78
|
+
* @returns {Promise<Readonly<StringMap>>}
|
|
79
|
+
* */
|
|
80
|
+
async function preProcessFilters() {
|
|
81
|
+
// merge all form data into one
|
|
82
|
+
const formData = ecfForms.reduce((prev, curr) => {
|
|
83
|
+
return mergeFormData(prev, new FormData(curr));
|
|
84
|
+
}, new FormData());
|
|
85
|
+
const filterValues = {};
|
|
86
|
+
for (const [key, value] of formData.entries()) {
|
|
87
|
+
// checkbox
|
|
88
|
+
if (key.slice(0, 5) == "ecf-c") {
|
|
89
|
+
let l = key.split(" ");
|
|
90
|
+
if (!filterValues[l[1]]) {
|
|
91
|
+
filterValues[l[1]] = [];
|
|
92
|
+
}
|
|
93
|
+
if (value == "on") {
|
|
94
|
+
filterValues[l[1]].push(l[2]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// text input
|
|
98
|
+
else if (key.slice(0, 5) == "ecf-t") {
|
|
99
|
+
let l = key.split(" ");
|
|
100
|
+
if (value.length > 0) {
|
|
101
|
+
filterValues[l[1]] = value;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// radio
|
|
105
|
+
else {
|
|
106
|
+
filterValues[key] = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return filterValues;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Applies `filterValues` to a global `ecfData` array.
|
|
114
|
+
* @param {Promise<StringMap>} filterValues
|
|
115
|
+
* @returns {Promise<Entry[]>} - filtered subset of the `ecfData` array.
|
|
116
|
+
* */
|
|
117
|
+
async function applyFilterValues(filterValues) {
|
|
118
|
+
return ecfData.filter(function(entry) {
|
|
119
|
+
return filterConfig.map((filter) => {
|
|
120
|
+
const filterValue = filterValues[filter.field] || false;
|
|
121
|
+
if (!filterValue) return true;
|
|
122
|
+
switch (filter.type) {
|
|
123
|
+
case 'radio': {
|
|
124
|
+
return entry[filter.field] === filterValue;
|
|
125
|
+
}
|
|
126
|
+
case 'checkbox': {
|
|
127
|
+
return filterValue.every(attr => entry[filter.field].includes(attr));
|
|
128
|
+
}
|
|
129
|
+
case 'text': {
|
|
130
|
+
return entry[filter.field].toLowerCase().indexOf(filterValue) !== -1;
|
|
131
|
+
}
|
|
132
|
+
default: {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}).every(el => (el));
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Hides all the elements of entries that are NOT in the `filteredData` array
|
|
142
|
+
* - uses global `idToElementMap`
|
|
143
|
+
* @param {Entry[]} filteredData - array of filtered ecfData entries
|
|
144
|
+
* */
|
|
145
|
+
async function hideFilteredData(filteredData) {
|
|
146
|
+
// hide all elements
|
|
147
|
+
idToElementMap.values().forEach(element => element.style.display = "none");
|
|
148
|
+
// then unhide elements that were filtered out
|
|
149
|
+
filteredData.forEach(entry => {
|
|
150
|
+
idToElementMap.get(entry._id).style.display = '';
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Run on each filter update */
|
|
155
|
+
function handleFilterInput() {
|
|
156
|
+
preProcessFilters().then(applyFilterValues).then(hideFilteredData);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
globalThis.selectAllFilterGroup = function selectAllFilterGroup(fieldName) {
|
|
160
|
+
const filter = filterConfig.find(filter => filter.field == fieldName);
|
|
161
|
+
if (filter.type != 'checkbox') {
|
|
162
|
+
alert("Misconfiguration of Filters. Select All button must only be present on checkbox filters.")
|
|
163
|
+
}
|
|
164
|
+
ecfForms.forEach(form => {
|
|
165
|
+
form
|
|
166
|
+
.querySelectorAll('input[type="checkbox"][name^="ecf-c ' + fieldName + '"]')
|
|
167
|
+
.forEach(checkbox => {
|
|
168
|
+
checkbox.checked = true;
|
|
169
|
+
});
|
|
170
|
+
})
|
|
171
|
+
handleFilterInput();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
globalThis.clearFilterGroup = function clearFilterGroup(fieldName) {
|
|
175
|
+
const filter = filterConfig.find(filter => filter.field == fieldName);
|
|
176
|
+
if (filter.type == 'radio') {
|
|
177
|
+
ecfForms.forEach(form => {
|
|
178
|
+
form
|
|
179
|
+
.querySelectorAll('input[type="radio"][name="' + fieldName + '"]')
|
|
180
|
+
.forEach(radio => { radio.checked = false; });
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
if (filter.type == 'checkbox') {
|
|
184
|
+
ecfForms.forEach(form => {
|
|
185
|
+
form
|
|
186
|
+
.querySelectorAll('input[type="checkbox"][name^="ecf-c ' + fieldName + '"]')
|
|
187
|
+
.forEach(checkbox => { checkbox.checked = false; });
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
handleFilterInput();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
ecfForms.forEach(form => {
|
|
194
|
+
form.addEventListener("input", handleFilterInput);
|
|
195
|
+
form.addEventListener("onchange", handleFilterInput);
|
|
196
|
+
form.addEventListener("submit", e => { e.preventDefault() });
|
|
197
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
if(typeof ecfData==='undefined'){alert('"ecfData" variable must be defined');throw Error('Global `ecfData` must be defined before this script runs.')}if(typeof filterConfig==='undefined'){alert('"filterConfig" variable must be defined');throw Error('Global `filterConfig` must be defined before this script runs.')}function a(b,B){for(const[C,_b] of B.entries())b.append(C,_b);return b}function $(_a){let q;return (q=document.querySelectorAll(_a)).length==1?q[0]:q}var c='ecf-filters',d=filterConfig.filter(D=>D.form).map(f=>f.form).concat([c]).map(_A=>document.getElementById(_A)),A=d.every(E=>(E!=null&&E!=void 0&&E.tagName.toLowerCase()==='form'));!A&&alert('Improperly configured template. All <form> elements with IDs from the filter configuration file must exist in the layout.');var _=new Map;for(const aA of $('[data-search-id]'))_.set(aA.getAttribute('data-search-id'),aA);function g(){var aB=d.reduce((aC,aD)=>a(aC,new FormData(aD)),new FormData),_B={};for(const[aE,aF] of aB.entries())if(aE.slice(0,5)=='ecf-c'){let l=aE.split(' ');!_B[l[1]]&&(_B[l[1]]=[]);aF=='on'&&_B[l[1]].push(l[2])}else if(aE.slice(0,5)=='ecf-t'){let l=aE.split(' ');aF.length>0&&(_B[l[1]]=aF)}else _B[aE]=aF;return _B}function h(aG){return ecfData.filter(aH=>filterConfig.map(aI=>{var aJ=aG[aI.field]||!1;if(!aJ)return!0;switch(aI.type) {case 'radio':return aH[aI.field]===aJ;case 'checkbox':return aJ.every(aK=>aH[aI.field].includes(aK));case 'text':return aH[aI.field].toLowerCase().indexOf(aJ)!==-1;default:return!1}}).every(el=>(el)))}function i(aL){for(const aM of _.values())aM.style.display='none';for(const aN of aL)_.get(aN._id).style.display=''}function j(){g().then(h).then(i)}globalThis.selectAllFilterGroup=function aP(aO){var _c=filterConfig.find(aQ=>aQ.field==aO);_c.type!='checkbox'&&alert('Misconfiguration of Filters. Select All button must only be present on checkbox filters.');for(const aR of d)for(const aS of aR.querySelectorAll(`input[type="checkbox"][name^="ecf-c ${aO}"]`))aS.checked=!0;j()};globalThis.clearFilterGroup=function aU(aT){var _C=filterConfig.find(aV=>aV.field==aT);_C.type=='radio';_C.type=='checkbox';j()};for(const aW of d){aW.addEventListener('input',j);aW.addEventListener('onchange',j);aW.addEventListener('submit',e=>e.preventDefault())}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {{type: string, label: string, field: string}} FilterGroupConfig
|
|
3
|
+
* @typedef {{collection: string, filters: Array<FilterGroupConfig> }} FilterCollectionConfig
|
|
4
|
+
* @typedef {{items: Array<FilterCollectionConfig>}} EcfConfig
|
|
5
|
+
* @typedef {{configDataField: string} | {config: EcfConfig}} PluginConfig
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const defaultConfigFieldName = "ecfConfig";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {Object} [eleventyConfig] - exposed to a plugin by Eleventy by default
|
|
12
|
+
* @param {PluginConfig} [pluginConfig=null] - configuration of the plugin
|
|
13
|
+
*/
|
|
14
|
+
function ecfPlugin(eleventyConfig, pluginConfig = null) {
|
|
15
|
+
// config validation
|
|
16
|
+
let configFromMetadata = false;
|
|
17
|
+
let importedConfig = {};
|
|
18
|
+
if (pluginConfig != null) {
|
|
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
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Processes all entries in all configured collections for unique field values,
|
|
34
|
+
* then duplicates the config into the 'ecfFilters' collection with the 'values' field added. */
|
|
35
|
+
eleventyConfig.addCollection("ecfFilters", function(api) {
|
|
36
|
+
if (configFromMetadata) {
|
|
37
|
+
const configFieldName = pluginConfig.configDataField || defaultConfigFieldName;
|
|
38
|
+
const collection = api.getAll()[0].data[configFieldName];
|
|
39
|
+
if (collection === undefined || collection === null) {
|
|
40
|
+
throw new Error(`ECF configuration collection "${configFieldName}" not detected`)
|
|
41
|
+
}
|
|
42
|
+
importedConfig = collection;
|
|
43
|
+
}
|
|
44
|
+
if (!importedConfig || Object.keys(importedConfig).length == 0) {
|
|
45
|
+
throw new Error("Invalid ECF configuration")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (Object.prototype.hasOwnProperty.call(importedConfig, "items"))
|
|
49
|
+
importedConfig = importedConfig.items;
|
|
50
|
+
|
|
51
|
+
const ecfFiltersCollection = {};
|
|
52
|
+
importedConfig.forEach(function(collectionConfig) {
|
|
53
|
+
let { collection, filters } = collectionConfig;
|
|
54
|
+
// making sure we're not making config collection edits in-place.
|
|
55
|
+
let filterConfig = configFromMetadata ? JSON.parse(JSON.stringify(filters)) : filters;
|
|
56
|
+
|
|
57
|
+
// convert to Set to accept only unique values
|
|
58
|
+
for (const filter of filterConfig) {
|
|
59
|
+
filter.values = new Set();
|
|
60
|
+
}
|
|
61
|
+
const entries = api.getFilteredByTag(collection);
|
|
62
|
+
entries.forEach(entry => {
|
|
63
|
+
for (const filter of filterConfig) {
|
|
64
|
+
switch (filter.type) {
|
|
65
|
+
case "radio": {
|
|
66
|
+
let value = entry.data[filter.field];
|
|
67
|
+
let strValue = typeof value !== 'string' ? value.toString() : value;
|
|
68
|
+
filter.values.add(strValue);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case "checkbox": {
|
|
72
|
+
entry.data[filter.field]?.forEach(value => {
|
|
73
|
+
let strValue = typeof value !== 'string' ? value.toString() : value;
|
|
74
|
+
filter.values?.add(strValue)
|
|
75
|
+
});
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// converting from a Set into Array so templates can read it
|
|
83
|
+
for (const filter of filterConfig) {
|
|
84
|
+
filter.values = [...filter.values].filter(val => val != undefined && val != null);
|
|
85
|
+
}
|
|
86
|
+
ecfFiltersCollection[collection] = filterConfig;
|
|
87
|
+
});
|
|
88
|
+
return ecfFiltersCollection;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = ecfPlugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eleventy-plugin-filter-page",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "11ty plugin to create collection filter pages",
|
|
5
|
+
"main": "dist/ecf.plugin.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist/",
|
|
8
|
+
"README.md",
|
|
9
|
+
"LICENSE"
|
|
10
|
+
],
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://gitlab.com/excitedfellas/eleventy-plugin-filter-page.git"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://gitlab.com/excitedfellas/eleventy-plugin-filter-page/-/issues"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"lint": "eslint",
|
|
20
|
+
"build": "rm -rf dist && mkdir -p dist && cp -R src/. dist/ && minify dist/ecf.filter.js > dist/ecf.filter.min.js",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://gitlab.com/excitedfellas/eleventy-plugin-filter-page",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"11ty",
|
|
26
|
+
"eleventy",
|
|
27
|
+
"javascript",
|
|
28
|
+
"plugin"
|
|
29
|
+
],
|
|
30
|
+
"author": "Daniel Mayovskiy",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@eslint/js": "^9.39.2",
|
|
34
|
+
"eslint": "^9.39.2",
|
|
35
|
+
"globals": "^17.0.0",
|
|
36
|
+
"minify": "^11.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|