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 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
- #### Configuration schema
31
+ ##### Configuration schema
23
32
  ```ts
24
33
  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
- >;
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
- eleventyConfig.addPlugin(ecfPlugin, {
52
- config: {
53
- collection: "posts",
54
- filters: [ /* ... */ ]
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
- eleventyConfig.addPlugin(ecfPlugin, {
62
- config: require('./src/_data/ecfConfig.json')
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
- eleventyConfig.addPlugin(ecfPlugin, {
72
- configDataField: 'ecfConfig'
73
- });
83
+ eleventyConfig.addPlugin(ecfPlugin, {
84
+ configDataField: 'ecfConfig'
85
+ });
74
86
  ```
75
87
 
76
- or rely on ecfConfig default and import it without settings:
88
+ or rely on `ecfConfig` default and import the plugin with no further `.eleventy.js` configuration:
89
+
77
90
  ```js
78
- eleventyConfig.addPlugin(ecfPlugin);
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 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.
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"
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @typedef {Object<string, string | string[]>} StringMap
7
- * @typedef {StringMap & { _id: string }} Entry
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 _id to an Element
69
+ // mapping each ecf_id to an Element
70
70
  const idToElementMap = new Map();
71
- $("[data-search-id]").forEach(el => {
72
- idToElementMap.set(el.getAttribute("data-search-id"), el);
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._id).style.display = '';
150
+ idToElementMap.get(entry.ecf_id).style.display = '';
151
151
  });
152
152
  }
153
153
 
@@ -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._id).style.display=""})}function handleFilterInput(){preProcessFilters().then(applyFilterValues).then(hideFilteredData)}$("[data-search-id]").forEach(e=>{idToElementMap.set(e.getAttribute("data-search-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()})});
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()})});
@@ -1,11 +1,33 @@
1
1
  /**
2
2
  * @typedef {{type: string, label: string, field: string}} FilterGroupConfig
3
- * @typedef {{collection: string, filters: Array<FilterGroupConfig> }} FilterCollectionConfig
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 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
- }
39
+ let finalPluginConfig = {};
40
+ let finalPluginConfigComputed = false;
32
41
 
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`)
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
- importedConfig = collection;
43
- }
44
- if (!importedConfig || Object.keys(importedConfig).length == 0) {
45
- throw new Error("Invalid ECF configuration")
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
- if (Object.prototype.hasOwnProperty.call(importedConfig, "items"))
49
- importedConfig = importedConfig.items;
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 ecfFiltersCollection = {};
52
- importedConfig.forEach(function(collectionConfig) {
53
- let { collection, filters } = collectionConfig;
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(collection);
62
- entries.forEach(entry => {
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
- let strValue = typeof value !== 'string' ? value.toString() : value;
68
- filter.values.add(strValue);
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
- let strValue = typeof value !== 'string' ? value.toString() : value;
74
- filter.values?.add(strValue)
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
- ecfFiltersCollection[collection] = filterConfig;
135
+ out[collectionName].filters = filterConfig;
136
+ out[collectionName].identifyingField = filterCollectionConfig.identifyingField || defaultIdentifyingField;
87
137
  });
88
- return ecfFiltersCollection;
138
+ return out;
89
139
  });
140
+
141
+ eleventyConfig.addFilter('ecfResolveId', ecfResolveId);
90
142
  }
91
143
 
92
144
  module.exports = ecfPlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eleventy-plugin-filter-page",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "11ty plugin to create collection filter pages",
5
5
  "main": "dist/ecf.plugin.js",
6
6
  "files": [