@gouvfr/dsfr-roller 1.0.81 → 1.0.83

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gouvfr/dsfr-roller",
3
- "version": "1.0.81",
3
+ "version": "1.0.83",
4
4
  "description": "Le module `dsfr-roller` permet de publier le site de documentation du Système de Design de l’État - DSFR",
5
5
  "keywords": [
6
6
  "Système de Design de l'État",
@@ -60,8 +60,8 @@
60
60
  ],
61
61
  "main": "./index.js",
62
62
  "dependencies": {
63
- "@gouvfr/dsfr-forge": "=1.0.81",
64
- "@gouvfr/dsfr-kit": "=1.0.81",
63
+ "@gouvfr/dsfr-forge": "=1.0.83",
64
+ "@gouvfr/dsfr-kit": "=1.0.83",
65
65
  "@gouvfr/dsfr-publisher": "npm:@gouvfr/dsfr@1.14.4",
66
66
  "deepmerge": "^4.3.1",
67
67
  "ejs": "^3.1.10",
@@ -0,0 +1,24 @@
1
+ import { Component } from '../component.js';
2
+
3
+ class Search extends Component {
4
+ constructor (data) {
5
+ super(data, 'search');
6
+ }
7
+
8
+ get ejsPath () {
9
+ return 'src/dsfr/component/search/template/ejs/search.ejs';
10
+ }
11
+
12
+ async format () {
13
+ const title = this.data.title || this.data.label;
14
+ const search = {
15
+ id: this.data.id,
16
+ input: { id: this.data.inputId, placeholder: this.data.label, classes: this.data.inputClasses, label: this.data.label },
17
+ button: { id: this.data.buttonId, label: this.data.label, classes: this.data.buttonClasses, title: title, markup: 'button' }
18
+ };
19
+
20
+ return search;
21
+ }
22
+ }
23
+
24
+ export { Search };
@@ -0,0 +1,18 @@
1
+ import { Node } from '../../../node.js';
2
+ import { Search } from '../../../../component/components/search.js';
3
+
4
+ class SearchLeafDirective extends Node {
5
+ constructor (data) {
6
+ super(data);
7
+ this.search = new Search({...this.data.properties, ...this.data.attributes});
8
+ }
9
+
10
+ async render () {
11
+ const data = { label: await this.renderChildren() };
12
+ return this.search.render(data);
13
+ }
14
+ }
15
+
16
+ SearchLeafDirective.NAME = 'fr-search';
17
+
18
+ export { SearchLeafDirective };
@@ -0,0 +1,82 @@
1
+ import { Node } from '../../node.js'
2
+
3
+ class FilterLeafDirective extends Node {
4
+ constructor (data) {
5
+ super(data);
6
+ }
7
+
8
+ structure (data) {
9
+ return {
10
+ type: 'htmlContainer',
11
+ tagName: 'div',
12
+ classes: ['dsfr-doc-filter'],
13
+ attributes: {
14
+ 'data-fr-filter': data.properties.id,
15
+ 'data-fr-filter-subitem': data.properties.subItem || '',
16
+ },
17
+ children: [
18
+ {
19
+ type: 'leafDirective',
20
+ name: 'fr-search',
21
+ properties: {
22
+ id: `filter-${data.properties.id}`,
23
+ inputId: `filter-${data.properties.id}-input`,
24
+ inputClasses: ['dsfr-doc-filter-input'],
25
+ buttonId: `filter-${data.properties.id}-button`,
26
+ buttonClasses: ['dsfr-doc-filter-button'],
27
+ label: data.properties.label,
28
+ title: data.properties.title,
29
+ }
30
+ },
31
+ ...(data.properties.select ? [
32
+ {
33
+ type: 'htmlContainer',
34
+ tagName: 'div',
35
+ classes: ['dsfr-doc-filter-select', 'fr-select-group'],
36
+ children: [
37
+ {
38
+ type: 'htmlContainer',
39
+ tagName: 'label',
40
+ classes: ['fr-label'],
41
+ attributes: {
42
+ for: `filter-${data.properties.select}-select`
43
+ },
44
+ children: [
45
+ {
46
+ type: 'text',
47
+ value: data.properties.selectLabel
48
+ }
49
+ ]
50
+ },
51
+ {
52
+ type: 'htmlContainer',
53
+ tagName: 'select',
54
+ classes: ['fr-select'],
55
+ attributes: {
56
+ id: `filter-${data.properties.select}-select`,
57
+ name: `filter-${data.properties.select}-select`,
58
+ 'data-fr-filter-select': data.properties.select
59
+ },
60
+ children: data.properties.selectOptions.map(option => ({
61
+ type: 'htmlContainer',
62
+ tagName: 'option',
63
+ attributes: {
64
+ value: option.value
65
+ },
66
+ children: [{
67
+ type: 'text',
68
+ value: option.label
69
+ }]
70
+ }))
71
+ }
72
+ ]
73
+ }
74
+ ] : [])
75
+ ],
76
+ };
77
+ }
78
+ }
79
+
80
+ FilterLeafDirective.NAME = 'dsfr-doc-filter';
81
+
82
+ export { FilterLeafDirective };
@@ -61,7 +61,8 @@ import { VideoLeafDirective } from './directive/doc/video-leaf-directive.js';
61
61
  import { ImageTextDirective } from './directive/doc/image-text-directive.js'
62
62
  import { ChangelogLeafDirective } from './directive/doc/changelog-leaf-directive.js'
63
63
  import { PreventPermalinksContainerDirective } from './directive/doc/prevent-permalinks-container-directive.js';
64
-
64
+ import { FilterLeafDirective } from './directive/doc/filter-leaf-directive.js';
65
+ import { SearchLeafDirective } from './directive/components/search/search-leaf-directive.js';
65
66
 
66
67
  const NODES = [
67
68
  NodeRoot,
@@ -131,7 +132,9 @@ const DIRECTIVE_LEAFS = [
131
132
  ChangelogLeafDirective,
132
133
  PinLeafDirective,
133
134
  VideoLeafDirective,
134
- PageItemListLeafDirective
135
+ PageItemListLeafDirective,
136
+ SearchLeafDirective,
137
+ FilterLeafDirective
135
138
  ];
136
139
  const DIRECTIVE_TEXTS = [
137
140
  ImageTextDirective
@@ -7,7 +7,8 @@ class Resource extends Renderable {
7
7
  search: this._data.resource.search,
8
8
  pagination: this._data.resource.pagination,
9
9
  consent: this._data.resource.consent,
10
- badge: this._data.resource.badge
10
+ badge: this._data.resource.badge,
11
+ filter: this._data.resource.filter
11
12
  };
12
13
  return `<script>
13
14
  window.resource = ${JSON.stringify(resource)};
@@ -2,12 +2,22 @@ const paramsString = window.location.search;
2
2
  const searchParams = new URLSearchParams(paramsString);
3
3
  import { normalizeTerm } from '@gouvfr/dsfr-kit';
4
4
 
5
- const getQuery = () => {
6
- return normalizeTerm(searchParams.get('query'));
5
+ const getQuery = (query = 'query') => {
6
+ return searchParams.get(query) ? normalizeTerm(searchParams.get(query)) : '';
7
+ };
8
+
9
+ const setQuery = (query, value) => {
10
+ if (value) {
11
+ searchParams.set(query, value);
12
+ } else {
13
+ searchParams.delete(query);
14
+ }
15
+ const queryString = searchParams.toString();
16
+ window.history.pushState({}, '', queryString ? `?${queryString}` : window.location.pathname);
7
17
  };
8
18
 
9
19
  const getCurrentPagination = () => {
10
20
  return parseInt(searchParams.get('page'));
11
21
  };
12
22
 
13
- export { getQuery, getCurrentPagination };
23
+ export { getQuery, setQuery, getCurrentPagination };
@@ -4,13 +4,22 @@ class CopySnippet extends Element {
4
4
  get button () {
5
5
  return this.element;
6
6
  }
7
+
7
8
  init() {
8
9
  this._copyLabel = this.button.innerText;
9
10
  this._copiedLabel = this.button.getAttribute('data-label-copied');
10
- this._code = this.button.previousElementSibling.innerText;
11
+ this._code = this.getCodeToCopy();
11
12
  this.listenClick(this.button);
12
13
  }
13
14
 
15
+ getCodeToCopy () {
16
+ const card = this.button.closest('.fr-card');
17
+ const pictogramPreview = card?.querySelector('.pictogram-preview');
18
+ if (pictogramPreview) return pictogramPreview.innerHTML.trim();
19
+
20
+ return this.button.getAttribute('data-copy-value') || this.button.previousElementSibling?.innerText || '';
21
+ }
22
+
14
23
  get isCopied () {
15
24
  return this._isCopied;
16
25
  }
@@ -22,6 +31,7 @@ class CopySnippet extends Element {
22
31
  }
23
32
 
24
33
  handleClick () {
34
+ if (!this._code) return;
25
35
  navigator.clipboard.writeText(this._code);
26
36
  this.isCopied = true;
27
37
  setTimeout(this._handleTimeout.bind(this), 1500);
@@ -0,0 +1,95 @@
1
+ import { Element } from '../../core/element.js';
2
+ import { ResultsFilterList } from './results/results-filter-list.js';
3
+ import { getQuery, setQuery } from '../../core/get-query.js';
4
+ import { normalizeTerm } from '@gouvfr/dsfr-kit';
5
+
6
+ class FilterBar extends Element {
7
+ constructor (element) {
8
+ super(element, 'filterBar');
9
+ this._filterType = this.element.getAttribute('data-fr-filter');
10
+ this._filterInput = this.element.querySelector('.dsfr-doc-filter-input');
11
+ this._filterButton = this.element.querySelector('.dsfr-doc-filter-button');
12
+ this._select = this.element.querySelector('select');
13
+ this._query = normalizeTerm(this._filterInput.value);
14
+ }
15
+
16
+ async init () {
17
+ this._resultsList = new ResultsFilterList(this._filterType, this._select && this._select.value !== '' ? { [this._select.getAttribute('data-fr-filter-select')]: this._select.value } : {});
18
+ this.element.after(this._resultsList.element);
19
+ await this._resultsList.init();
20
+
21
+ const queryFromUrl = getQuery();
22
+ const filterFromUrl = getQuery('filter');
23
+
24
+ if (this._select && filterFromUrl) {
25
+ this._select.value = filterFromUrl;
26
+ }
27
+
28
+ const hasQueryFromUrl = !!queryFromUrl;
29
+ const hasFilterFromUrl = this._select && !!filterFromUrl && this._select.value === filterFromUrl;
30
+
31
+ if (hasQueryFromUrl) {
32
+ this._filterInput.value = queryFromUrl;
33
+ }
34
+
35
+ if (hasQueryFromUrl || hasFilterFromUrl) {
36
+ this.update(hasQueryFromUrl ? queryFromUrl : this._query, this._select && this._select.value !== '' ? { [this._select.getAttribute('data-fr-filter-select')]: this._select.value } : {});
37
+ }
38
+
39
+ if (this._select) {
40
+ this._select.addEventListener('change', this.handleChange.bind(this));
41
+ }
42
+
43
+ this._filterInput.addEventListener(
44
+ 'keyup',
45
+ this.handleKeyup.bind(this)
46
+ );
47
+
48
+ // Handle filter input clear
49
+ this._filterInput.addEventListener(
50
+ 'search', () => {
51
+ setQuery('query', '');
52
+ this.update('');
53
+ }
54
+ );
55
+
56
+ this._filterButton.addEventListener(
57
+ 'click',
58
+ this.handleFilterSubmit.bind(this)
59
+ );
60
+ }
61
+
62
+ update (query, filters = {}) {
63
+ this._query = query;
64
+ this._resultsList.update(query, filters);
65
+ }
66
+
67
+ handleFilterSubmit () {
68
+ setQuery('query', normalizeTerm(this._filterInput.value));
69
+ if (this._select && this._select.value !== '') {
70
+ setQuery('filter', normalizeTerm(this._select.value));
71
+ } else {
72
+ setQuery('filter', null);
73
+ }
74
+ this.update(normalizeTerm(this._filterInput.value), this._select && this._select.value !== '' ? { [this._select.getAttribute('data-fr-filter-select')]: this._select.value } : {});
75
+ }
76
+
77
+ handleKeyup (event) {
78
+ if (event.key === 'Enter') {
79
+ this.handleFilterSubmit(event);
80
+ } else {
81
+ if (this._select && this._select.value !== '') {
82
+ this.update(normalizeTerm(event.target.value), { [this._select.getAttribute('data-fr-filter-select')]: this._select.value });
83
+ } else {
84
+ this.update(normalizeTerm(event.target.value));
85
+ }
86
+ }
87
+ }
88
+
89
+ handleChange (event) {
90
+ const value = event.target.value;
91
+ this._resultsList.update(this._query, value !== '' ? { [this._select.getAttribute('data-fr-filter-select')]: value } : {});
92
+ }
93
+ }
94
+
95
+ export { FilterBar };
@@ -0,0 +1,80 @@
1
+ class ResultColorCard {
2
+ constructor (data) {
3
+ this._colorClass = data.colorClass;
4
+ this._tint = data.tint;
5
+ this._usage = data.usage;
6
+ this._context = data.context;
7
+ this._family = data.family;
8
+ this._hover = data.hover;
9
+ }
10
+
11
+ getHeader () {
12
+ if (!this._colorClass) return '';
13
+
14
+ let content = '';
15
+ switch (this._context) {
16
+ case 'text':
17
+ content = `<div class="color-preview" ${this._usage === 'inverted' ? 'style="background-color: var(--background-flat-' + this._tint + ')"' : ''}><p class="fr-h1 ${this._colorClass}">Aa</p></div>`;
18
+ break;
19
+
20
+ case 'artwork':
21
+ content = `<div class="color-preview ${this._colorClass}" style="background-color: var(--artwork-${this._usage}-${this._tint})"></div>`;
22
+ break;
23
+
24
+ default:
25
+ content = `<div class="color-preview ${this._colorClass} ${this._context === 'border' ? 'fr-border-width-1v' : ''}"></div>`;
26
+ }
27
+
28
+ return `<div class="fr-card__header">
29
+ <div class="fr-card__img">
30
+ ${content}
31
+ </div>
32
+ </div>`;
33
+ }
34
+
35
+ getFooter () {
36
+ return `<div class="fr-card__footer">
37
+ <ul class="fr-btns-group fr-btns-group--sm">
38
+ <li>
39
+ <button data-fr-analytics-action="reduce" data-copy-value="${this._colorClass}" data-label-copied="${window.resource?.filter?.copy?.classCopied}" class="fr-btn fr-btn--secondary dsfr-doc-copy-snippet">${window.resource?.filter?.copy?.class}</button>
40
+ </li>
41
+ </ul>
42
+ </div>`;
43
+ }
44
+
45
+ getTags () {
46
+ if (!this._context) return '';
47
+ return `<ul class="fr-tags-group fr-tags-group--sm">
48
+ <li>
49
+ <p class="fr-tag">${this._context}</p>
50
+ </li>
51
+ <li>
52
+ <p class="fr-tag">${this._usage}</p>
53
+ </li>
54
+ </ul>`;
55
+ }
56
+
57
+ getBody () {
58
+ return `<div class="fr-card__body">
59
+ <div class="fr-card__content">
60
+ <h3 class="fr-card__title">${this._tint}</h3>
61
+ <div class="fr-card__start">
62
+ ${this.getTags()}
63
+ </div>
64
+ <div class="fr-card__end">
65
+ <p class="fr-card__detail">${this._colorClass}</p>
66
+ </div>
67
+ </div>
68
+ ${this.getFooter()}
69
+ </div>`;
70
+ }
71
+
72
+ render () {
73
+ return `<div class="fr-card fr-card--shadow">
74
+ ${this.getBody()}
75
+ ${this.getHeader()}
76
+ </div>`;
77
+ }
78
+ }
79
+
80
+ export { ResultColorCard };
@@ -0,0 +1,69 @@
1
+ class ResultIconCard {
2
+ constructor (data) {
3
+ this._data = data;
4
+ this._iconName = data.name;
5
+ this._category = data.category;
6
+ this._family = data.family;
7
+ this._path = data.path;
8
+ this._class = 'fr-icon-' + this._iconName;
9
+ }
10
+
11
+ getHeader () {
12
+ if (!this._iconName) return '';
13
+ return `<div class="fr-card__header">
14
+ <div class="fr-card__img">
15
+ <div class="icon-preview">
16
+ <span class="fr-icon fr-icon--lg ${this._class}" aria-hidden="true"></span>
17
+ </div>
18
+ </div>
19
+ </div>`;
20
+ }
21
+
22
+ getFooter () {
23
+ return `<div class="fr-card__footer">
24
+ <ul class="fr-btns-group fr-btns-group--sm">
25
+ <li>
26
+ <button data-fr-analytics-action="reduce" data-copy-value="${this._class}" data-label-copied="${window.resource?.filter?.copy?.classCopied}" class="fr-btn fr-btn--secondary dsfr-doc-copy-snippet">${window.resource?.filter?.copy?.class}</button>
27
+ </li>
28
+ </ul>
29
+ </div>`;
30
+ }
31
+
32
+ getTags () {
33
+ if (!this._category || !this._family) return '';
34
+ return `<div class="fr-card__start">
35
+ <ul class="fr-tags-group fr-tags-group--sm">
36
+ <li>
37
+ <p class="fr-tag">${this._category}</p>
38
+ </li>
39
+ <li>
40
+ <p class="fr-tag">${this._family}</p>
41
+ </li>
42
+ </ul>
43
+ </div>`;
44
+ }
45
+
46
+ getBody () {
47
+ return `<div class="fr-card__body">
48
+ <div class="fr-card__content">
49
+ <h3 class="fr-card__title">${this._iconName}</h3>
50
+ <div class="fr-card__start">
51
+ ${this.getTags()}
52
+ </div>
53
+ <div class="fr-card__end">
54
+ <p class="fr-card__detail">${this._class}</p>
55
+ </div>
56
+ </div>
57
+ ${this.getFooter()}
58
+ </div>`;
59
+ }
60
+
61
+ render () {
62
+ return `<div class="fr-card fr-card--shadow">
63
+ ${this.getBody()}
64
+ ${this.getHeader()}
65
+ </div>`;
66
+ }
67
+ }
68
+
69
+ export { ResultIconCard };
@@ -0,0 +1,65 @@
1
+ class ResultPictogramCard {
2
+ constructor (data) {
3
+ this._data = data;
4
+ this._name = data.name;
5
+ this._category = data.category;
6
+ this._path = data.path;
7
+ this._code = `<svg aria-hidden="true" class="fr-artwork" viewBox="0 0 80 80" width="80px" height="80px">
8
+ <use class="fr-artwork-decorative" href="/dist/${this._path}#artwork-decorative"></use>
9
+ <use class="fr-artwork-minor" href="/dist/${this._path}#artwork-minor"></use>
10
+ <use class="fr-artwork-major" href="/dist/${this._path}#artwork-major"></use>
11
+ </svg>`;
12
+ }
13
+
14
+ getHeader () {
15
+ return `<div class="fr-card__header">
16
+ <div class="fr-card__img">
17
+ <div class="pictogram-preview">
18
+ ${this._code}
19
+ </div>
20
+ </div>
21
+ </div>`;
22
+ }
23
+
24
+ getFooter () {
25
+ return `<div class="fr-card__footer">
26
+ <ul class="fr-btns-group fr-btns-group--sm">
27
+ <li>
28
+ <button data-fr-analytics-action="reduce" data-label-copied="${window.resource?.filter?.copy?.codeCopied}" class="fr-btn fr-btn--secondary dsfr-doc-copy-snippet">${window.resource?.filter?.copy?.code}</button>
29
+ </li>
30
+ </ul>
31
+ </div>`;
32
+ }
33
+
34
+ getTags () {
35
+ if (!this._category) return '';
36
+ return `<div class="fr-card__start">
37
+ <ul class="fr-tags-group fr-tags-group--sm">
38
+ <li>
39
+ <p class="fr-tag">${this._category}</p>
40
+ </li>
41
+ </ul>
42
+ </div>`;
43
+ }
44
+
45
+ getBody () {
46
+ return `<div class="fr-card__body">
47
+ <div class="fr-card__content">
48
+ <h3 class="fr-card__title">${this._name}</h3>
49
+ <div class="fr-card__start">
50
+ ${this.getTags()}
51
+ </div>
52
+ </div>
53
+ ${this.getFooter()}
54
+ </div>`;
55
+ }
56
+
57
+ render () {
58
+ return `<div class="fr-card fr-card--shadow">
59
+ ${this.getBody()}
60
+ ${this.getHeader()}
61
+ </div>`;
62
+ }
63
+ }
64
+
65
+ export { ResultPictogramCard };
@@ -0,0 +1,27 @@
1
+ import { ResultIconCard } from './cards/result-icon-card.js';
2
+ import { ResultColorCard } from './cards/result-color-card.js';
3
+ import { ResultPictogramCard } from './cards/result-pictogram-card.js';
4
+
5
+ class ResultFilterItem {
6
+ constructor (type, data) {
7
+ this._data = data;
8
+ this._type = type;
9
+ }
10
+
11
+ render () {
12
+ switch (this._type) {
13
+ case 'icon':
14
+ return new ResultIconCard(this._data).render();
15
+
16
+ case 'colors':
17
+ // console.log(this._data);
18
+ // if (this._data.context === 'artwork' && this._data.usage !== 'minor') return;
19
+ return new ResultColorCard(this._data).render();
20
+
21
+ case 'pictogram':
22
+ return new ResultPictogramCard(this._data).render();
23
+ }
24
+ }
25
+ }
26
+
27
+ export { ResultFilterItem };
@@ -0,0 +1,21 @@
1
+ import { replaceFragment } from '../../../core/replace-fragments.js';
2
+
3
+ class ResultsFilterEmpty {
4
+ constructor (query) {
5
+ this._query = query;
6
+ }
7
+ render () {
8
+ const noResults = window.resource?.search?.noresults;
9
+ return `
10
+ <p class="fr-mb-2v">
11
+ <strong>${replaceFragment(noResults?.title, this._query)}</strong>
12
+ </p>
13
+ <p class="fr-mb-2v">${noResults?.subtitle}</p>
14
+ <ul class="dsfr-doc-filter-results-list--no-results">
15
+ <li>${noResults?.suggestions1}</li>
16
+ <li>${noResults?.suggestions2}</li>
17
+ </ul>`;
18
+ }
19
+ }
20
+
21
+ export { ResultsFilterEmpty };
@@ -0,0 +1,129 @@
1
+ import { ResultFilterItem } from './result-filter-item.js';
2
+ import { ResultsFilterEmpty } from './results-filter-empty.js';
3
+ import { Element } from '../../../core/element.js';
4
+ import { capitalize } from '@gouvfr/dsfr-kit';
5
+ import CopySnippet from '../../copy-snippet.js';
6
+
7
+ class ResultsFilterList extends Element {
8
+ constructor (type, filters = {}) {
9
+ const element = document.createElement('div');
10
+ element.classList.add('dsfr-doc-filter-results');
11
+ element.setAttribute('aria-live', 'polite');
12
+ super(element);
13
+ this._type = type;
14
+ this._filters = filters;
15
+ }
16
+
17
+ async init() {
18
+ await window.filterEngine.init(this._type);
19
+ this.update('');
20
+ }
21
+
22
+ search (query, filters = {}) {
23
+ this._results = window.filterEngine.search(query, { ...this._filters, ...filters });
24
+ this._resultsArray = Array.isArray(this._results) ? this._results : Object.values(this._results || {});
25
+ return this._resultsArray;
26
+ }
27
+
28
+ update (query, filters = {}) {
29
+ this._results = this.search(query, filters);
30
+
31
+ switch (true) {
32
+ case !this._results:
33
+ case this._results.length === 0:
34
+ const empty = new ResultsFilterEmpty(query);
35
+ this._content = empty.render();
36
+ break;
37
+
38
+ default:
39
+ this._items = this._results.map(result => new ResultFilterItem(this._type, result));
40
+ this._content = this.render();
41
+ }
42
+
43
+ this.element.innerHTML = this._content;
44
+ this.initCopyButtons();
45
+ }
46
+
47
+ initCopyButtons () {
48
+ const buttons = this.element.querySelectorAll('.dsfr-doc-copy-snippet');
49
+ buttons.forEach(button => {
50
+ const copySnippet = new CopySnippet(button);
51
+ copySnippet.init();
52
+ });
53
+ }
54
+
55
+ groupByCategory(categoryField = 'category', subcategoryField = null) {
56
+ const grouped = {};
57
+ this._items.forEach(item => {
58
+ const category = item._data[categoryField] || 'Non catégorisé';
59
+
60
+ if (!grouped[category]) {
61
+ grouped[category] = subcategoryField ? {} : [];
62
+ }
63
+
64
+ if (subcategoryField) {
65
+ const subcategory = item._data[subcategoryField] || 'Autre';
66
+ if (!grouped[category][subcategory]) {
67
+ grouped[category][subcategory] = [];
68
+ }
69
+ grouped[category][subcategory].push(item);
70
+ } else {
71
+ grouped[category].push(item);
72
+ }
73
+ });
74
+ return grouped;
75
+ }
76
+
77
+ sortByCategory(grouped) {
78
+ const sorted = {};
79
+ Object.keys(grouped).sort().forEach(category => {
80
+ sorted[category] = grouped[category];
81
+ });
82
+ return sorted;
83
+ }
84
+
85
+ render() {
86
+ switch (this._type) {
87
+ case 'colors':
88
+ this._grouped = this.groupByCategory('context', 'usage');
89
+ break;
90
+ default:
91
+ this._grouped = this.sortByCategory(this.groupByCategory());
92
+ }
93
+
94
+ return Object.entries(this._grouped).map(([category, itemsOrSubcategories]) => {
95
+ // Si c'est un tableau, on a une structure simple sans sous-catégories
96
+ if (Array.isArray(itemsOrSubcategories)) {
97
+ return `<div class="fr-grid-row fr-grid-row--gutters fr-mb-6v">
98
+ <div class="fr-col-12">
99
+ <h3 class="fr-mb-0">${capitalize(category)}</h3>
100
+ </div>
101
+ ${itemsOrSubcategories.map(item => `<div class="fr-col-12 fr-col-sm-6 fr-col-lg-4">
102
+ ${item.render()}
103
+ </div>`).join('')}
104
+ </div>`;
105
+ }
106
+
107
+ // Si c'est un objet, on a des sous-catégories
108
+ return `<div class="fr-mb-8v">
109
+ <div class="fr-col-12">
110
+ <h3>${capitalize(category)}</h3>
111
+ ${category === 'artwork' ? `<p class="fr-text--sm fr-mt-1v">${window.resource?.filter?.artworkDescription}</p>` : ''}
112
+ </div>
113
+ ${Object.entries(itemsOrSubcategories).map(([subcategory, items]) => {
114
+ return `<div class="fr-grid-row fr-grid-row--gutters fr-mb-6v">
115
+ <div class="fr-col-12">
116
+ <h4 class="fr-mb-0">${subcategory}</h4>
117
+ </div>
118
+ ${items.map(item => `<div class="fr-col-12 fr-col-sm-6 fr-col-lg-4">
119
+ ${item.render()}
120
+ </div>`).join('')}
121
+ </div>`;
122
+ }).join('')}
123
+ </div>`;
124
+ }).join('');
125
+ }
126
+
127
+ }
128
+
129
+ export { ResultsFilterList };
@@ -5,6 +5,7 @@ import CopyLink from './elements/copy-link.js';
5
5
  import ConsentCGU from './elements/consent-cgu.js';
6
6
  import Storybook from './elements/storybook.js';
7
7
  import { SearchBar } from './elements/search-bar/index.js';
8
+ import { FilterBar } from './elements/filter-bar/index.js';
8
9
  import { ConsentManagementPlatform } from './cmp/index.js';
9
10
 
10
11
  window.onload = async () => {
@@ -13,6 +14,7 @@ window.onload = async () => {
13
14
  await instantiateElements('.dsfr-doc-anchor-heading__button', CopyLink);
14
15
  await instantiateElements('.dsfr-doc-storybook-leaf iframe', Storybook);
15
16
  await instantiateElements('#search', SearchBar);
17
+ await instantiateElements('.dsfr-doc-filter', FilterBar);
16
18
  };
17
19
 
18
20
  const consentResource = window.resource?.consent || {};
@@ -0,0 +1,53 @@
1
+ import MiniSearch from 'minisearch';
2
+ import { normalizeTerm } from '@gouvfr/dsfr-kit';
3
+
4
+ const ROOT_REGEX = /^(?<root>\/(?<version>v\d+\.\d+|[\w-]+)\/(?<locale>[a-zA-Z-]+))/;
5
+
6
+ class FilterEngine {
7
+ async init (type) {
8
+ const response = await fetch(this.getFilterIndex(type));
9
+ this._documentJSON = await response.json();
10
+ this._document = JSON.stringify(this._documentJSON);
11
+ this._miniSearch = MiniSearch.loadJSON(this._document, {fields: this.getFields(), storeFields: this.getFields()});
12
+ }
13
+
14
+ getFilterIndex = (type) => {
15
+ const { groups: { root, version, locale } } = window.location.pathname.match(ROOT_REGEX);
16
+ return `${root}/${type}.json`;
17
+ }
18
+
19
+ getFields = () => {
20
+ const fields = Object.keys(this._documentJSON.fieldIds || {});
21
+ return fields;
22
+ }
23
+
24
+ getOptions = (filter = null) => {
25
+ const options = {
26
+ prefix: true,
27
+ fuzzy: 0.1,
28
+ processTerm: (term) => normalizeTerm(term.toLowerCase()),
29
+ tokenize: (string) => string.split(/\s+/)
30
+ };
31
+
32
+ if (filter) {
33
+ options.filter = (result) => {
34
+ return Object.entries(filter).every(([key, value]) => result[key] === value);
35
+ };
36
+ }
37
+
38
+ return options;
39
+ }
40
+
41
+ search (query, filter) {
42
+ if (query === '' || query == null || query.trim().length === 0) {
43
+ return Object.values(this._documentJSON.storedFields).filter(result => {
44
+ return Object.entries(filter).every(([key, value]) => result[key] === value);
45
+ });
46
+ } else {
47
+ const results = this._miniSearch.search(query, this.getOptions(filter));
48
+ return results;
49
+ }
50
+ }
51
+ }
52
+
53
+ export { FilterEngine };
@@ -1,53 +1,5 @@
1
- import MiniSearch from 'minisearch';
2
- import { normalizeTerm } from '@gouvfr/dsfr-kit';
3
-
4
- const LOAD_OPTIONS = {
5
- fields: ['title', 'keywords', 'summary', 'excerpt', 'text'],
6
- storeFields: ['title', 'url', 'boost', 'summary', 'excerpt', 'cover', 'section'],
7
- };
8
-
9
- const SEARCH_OPTIONS = {
10
- boost: { title: 2.5, keywords: 2, summary: 1.5, text: 1 },
11
- boostDocument: (documentId, query, storedFields) =>
12
- storedFields?.boost ?? 1,
13
- }
14
-
15
- const getOptions = (type) => {
16
- const options = {
17
- prefix: true,
18
- fuzzy: 0.2,
19
- processTerm: (term) => normalizeTerm(term.toLowerCase())
20
- };
21
-
22
- switch (type) {
23
- case 'search':
24
- return { ...options, ...SEARCH_OPTIONS };
25
-
26
- default:
27
- return { ...options, ...LOAD_OPTIONS };
28
- }
29
- }
30
-
31
- const ROOT_REGEX = /^(?<root>\/(?<version>v\d+\.\d+|[\w-]+)\/(?<locale>[a-zA-Z-]+))/;
32
- const CURRENT_VERSION_REGEX = /^(?<root>\/(?<version>v\d+\.\d+|[\w-]+)\/(?<locale>\w+)\/(?<search>v\d+\.\d+|[\w-]+))/
33
-
34
- const getSearchIndex = (type) => {
35
- const searchIndexRegex = type === 'searchPage' ? CURRENT_VERSION_REGEX : ROOT_REGEX;
36
- const { groups: { root, version, locale } } = window.location.pathname.match(searchIndexRegex);
37
- return `${root}/index.json`;
38
- }
39
-
40
- class SearchEngine {
41
- async init (type) {
42
- const response = await fetch(getSearchIndex(type));
43
- const json = JSON.stringify(await response.json());
44
- const options = getOptions('load');
45
- this._miniSearch = MiniSearch.loadJSON(json, options);
46
- }
47
-
48
- search (query) {
49
- return this._miniSearch.search(query, getOptions('search'));
50
- }
51
- }
1
+ import { SearchEngine } from './search-engine.js';
2
+ import { FilterEngine } from './filter-engine.js';
52
3
 
53
4
  window.searchEngine = new SearchEngine();
5
+ window.filterEngine = new FilterEngine();
@@ -0,0 +1,53 @@
1
+ import MiniSearch from 'minisearch';
2
+ import { normalizeTerm } from '@gouvfr/dsfr-kit';
3
+
4
+ const LOAD_OPTIONS = {
5
+ fields: ['title', 'keywords', 'summary', 'excerpt', 'text'],
6
+ storeFields: ['title', 'url', 'boost', 'summary', 'excerpt', 'cover', 'section'],
7
+ };
8
+
9
+ const SEARCH_OPTIONS = {
10
+ boost: { title: 2.5, keywords: 2, summary: 1.5, text: 1 },
11
+ boostDocument: (documentId, query, storedFields) =>
12
+ storedFields?.boost ?? 1,
13
+ }
14
+
15
+ const ROOT_REGEX = /^(?<root>\/(?<version>v\d+\.\d+|[\w-]+)\/(?<locale>[a-zA-Z-]+))/;
16
+ const CURRENT_VERSION_REGEX = /^(?<root>\/(?<version>v\d+\.\d+|[\w-]+)\/(?<locale>\w+)\/(?<search>v\d+\.\d+|[\w-]+))/
17
+
18
+ class SearchEngine {
19
+ async init (type) {
20
+ const response = await fetch(this.getIndex(type));
21
+ const json = JSON.stringify(await response.json());
22
+ const options = this.getOptions('load');
23
+ this._miniSearch = MiniSearch.loadJSON(json, options);
24
+ }
25
+
26
+ getOptions = (type) => {
27
+ const options = {
28
+ prefix: true,
29
+ fuzzy: 0.2,
30
+ processTerm: (term) => normalizeTerm(term.toLowerCase())
31
+ };
32
+
33
+ switch (type) {
34
+ case 'search':
35
+ return { ...options, ...SEARCH_OPTIONS };
36
+
37
+ default:
38
+ return { ...options, ...LOAD_OPTIONS };
39
+ }
40
+ }
41
+
42
+ getIndex = (type) => {
43
+ const searchIndexRegex = type === 'searchPage' ? CURRENT_VERSION_REGEX : ROOT_REGEX;
44
+ const { groups: { root, version, locale } } = window.location.pathname.match(searchIndexRegex);
45
+ return `${root}/index.json`;
46
+ }
47
+
48
+ search (query) {
49
+ return this._miniSearch.search(query, getOptions('search'));
50
+ }
51
+ }
52
+
53
+ export { SearchEngine };
@@ -0,0 +1,53 @@
1
+ .dsfr-doc-filter-results {
2
+ margin-top: 2rem;
3
+ margin-bottom: 2rem;
4
+
5
+ p + ul.dsfr-doc-filter-results-list--no-results {
6
+ margin-top: 0;
7
+ }
8
+
9
+ .fr-card__content {
10
+ padding: 1rem;
11
+
12
+ .fr-card__end {
13
+ margin-top: 0;
14
+ }
15
+
16
+ .fr-card__start {
17
+ .fr-tags-group>li {
18
+ line-height: 1.25rem;
19
+ }
20
+ }
21
+ }
22
+
23
+ .fr-card__footer {
24
+ padding: 0 1rem 1rem;
25
+ }
26
+
27
+ .fr-card__img {
28
+ .icon-preview, .pictogram-preview, .color-preview {
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ width: 100%;
33
+ height: 120px;
34
+ border-bottom: 1px solid var(--background-alt-grey);
35
+
36
+ & > * {
37
+ margin: 0;
38
+ }
39
+ }
40
+
41
+ .icon-preview {
42
+ height: 80px;
43
+ }
44
+ }
45
+ }
46
+
47
+ .dsfr-doc-filter-select {
48
+ margin-top: 1rem;
49
+
50
+ .fr-select {
51
+ max-width: 200px;
52
+ }
53
+ }
@@ -9,4 +9,5 @@
9
9
  @use 'dsfr-doc-edit';
10
10
  @use 'dsfr-doc-storybook-leaf';
11
11
  @use 'dsfr-doc-modal-cgu';
12
+ @use 'dsfr-doc-filter';
12
13
  @use 'fr-tile';
File without changes
@@ -1 +0,0 @@
1
- @use 'search-page';