@gouvfr/dsfr-roller 1.0.68 → 1.0.71
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 +2 -2
- package/src/component/components/sidemenu.js +1 -17
- package/src/node/generic/code-node.js +1 -1
- package/src/page/body/badges-map.js +18 -0
- package/src/page/body/badges.js +1 -17
- package/src/page/page.js +1 -0
- package/src/script/main/elements/copy-link.js +79 -0
- package/src/script/main/index.js +2 -0
- package/src/script/search/elements/result-card.js +70 -42
- package/src/script/search/elements/search-page.js +26 -26
- package/src/style/main/utility/_dsfr-doc-anchor-heading.scss +2 -4
- package/src/template/template.js +13 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gouvfr/dsfr-roller",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.71",
|
|
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",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
],
|
|
57
57
|
"main": "./index.js",
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@gouvfr/dsfr-forge": "=1.0.
|
|
59
|
+
"@gouvfr/dsfr-forge": "=1.0.71",
|
|
60
60
|
"@gouvfr/dsfr-publisher": "npm:@gouvfr/dsfr@1.14.2",
|
|
61
61
|
"deepmerge": "^4.3.1",
|
|
62
62
|
"ejs": "^3.1.10",
|
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
import { Component } from '../component.js';
|
|
2
2
|
import { formatLink } from '../../core/format-link.js';
|
|
3
3
|
import { log } from '@gouvfr/dsfr-forge';
|
|
4
|
-
|
|
5
|
-
const Badges = {
|
|
6
|
-
BETA: {
|
|
7
|
-
id: 'beta',
|
|
8
|
-
type: 'info',
|
|
9
|
-
icon: false,
|
|
10
|
-
size: 'sm',
|
|
11
|
-
},
|
|
12
|
-
NEW: {
|
|
13
|
-
id: 'new',
|
|
14
|
-
type: 'new',
|
|
15
|
-
icon: true,
|
|
16
|
-
size: 'sm',
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const BadgesMap = new Map(Object.values(Badges).map(badge => [badge.id, badge]));
|
|
4
|
+
import { BadgesMap } from '../../page/body/badges-map.js';
|
|
21
5
|
|
|
22
6
|
class Sidemenu extends Component {
|
|
23
7
|
constructor (data, badgeLabels) {
|
|
@@ -26,7 +26,7 @@ class CodeNode extends Node {
|
|
|
26
26
|
return `
|
|
27
27
|
<div class="code-snippet fr-mb-6v">
|
|
28
28
|
<pre><code${this.renderAttributes()}>${convertHTMLEntities(this.value)}</code></pre>
|
|
29
|
-
<button type="button" class="code-snippet--copy fr-btn fr-btn--sm fr-btn--tertiary" data-label-copied="${this.data?.fragments?.button?.copied}">${this.data?.fragments?.button?.copy}</button>
|
|
29
|
+
<button type="button" class="code-snippet--copy fr-btn fr-btn--sm fr-btn--tertiary" data-label-copied="${this.data?.fragments?.button?.['text-copied']}">${this.data?.fragments?.button?.copy}</button>
|
|
30
30
|
</div>
|
|
31
31
|
`;
|
|
32
32
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const BadgeVariants = {
|
|
2
|
+
BETA: {
|
|
3
|
+
id: 'beta',
|
|
4
|
+
type: 'info',
|
|
5
|
+
icon: false,
|
|
6
|
+
size: 'sm',
|
|
7
|
+
},
|
|
8
|
+
NEW: {
|
|
9
|
+
id: 'new',
|
|
10
|
+
type: 'new',
|
|
11
|
+
icon: true,
|
|
12
|
+
size: 'sm',
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const BadgesMap = new Map(Object.values(BadgeVariants).map(badge => [badge.id, badge]));
|
|
17
|
+
|
|
18
|
+
export { BadgesMap };
|
package/src/page/body/badges.js
CHANGED
|
@@ -1,21 +1,5 @@
|
|
|
1
1
|
import { log } from '@gouvfr/dsfr-forge';
|
|
2
|
-
|
|
3
|
-
const BadgeVariants = {
|
|
4
|
-
BETA: {
|
|
5
|
-
id: 'beta',
|
|
6
|
-
type: 'info',
|
|
7
|
-
icon: false,
|
|
8
|
-
size: 'sm',
|
|
9
|
-
},
|
|
10
|
-
NEW: {
|
|
11
|
-
id: 'new',
|
|
12
|
-
type: 'new',
|
|
13
|
-
icon: true,
|
|
14
|
-
size: 'sm',
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const BadgesMap = new Map(Object.values(BadgeVariants).map(badge => [badge.id, badge]));
|
|
2
|
+
import { BadgesMap } from './badges-map.js';
|
|
19
3
|
|
|
20
4
|
class Badges {
|
|
21
5
|
constructor(data) {
|
package/src/page/page.js
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Element } from '../core/element.js'
|
|
2
|
+
|
|
3
|
+
class CopyLink extends Element {
|
|
4
|
+
init() {
|
|
5
|
+
this._copiedLabel = this.button.getAttribute('data-label-copied');
|
|
6
|
+
this._isCopied = false;
|
|
7
|
+
this._heading = this.button.parentElement;
|
|
8
|
+
this.listenClick(this.button);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get button () {
|
|
12
|
+
return this.element;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get anchor () {
|
|
16
|
+
return this._heading && this._heading.id ? `#${this._heading.id}` : '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get url () {
|
|
20
|
+
return window.location.origin + window.location.pathname + window.location.search;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get anchoredUrl () {
|
|
24
|
+
return this.url + this.anchor;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get isCopied () {
|
|
28
|
+
return this._isCopied;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
set isCopied (value) {
|
|
32
|
+
this._isCopied = value;
|
|
33
|
+
if (value && this._tooltip) {
|
|
34
|
+
this._tooltip.innerText = this._copiedLabel;
|
|
35
|
+
this.button.setAttribute('aria-label', this._copiedLabel);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
createTooltip() {
|
|
40
|
+
const tooltip = document.createElement("span");
|
|
41
|
+
tooltip.id = `${this.button.id}-tooltip`;
|
|
42
|
+
tooltip.className = 'fr-tooltip fr-placement';
|
|
43
|
+
tooltip.setAttribute('aria-hidden', 'true');
|
|
44
|
+
this.button.setAttribute('aria-describedby', tooltip.id);
|
|
45
|
+
this._heading.insertAdjacentElement('beforeend', tooltip);
|
|
46
|
+
return tooltip;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
removeTooltip() {
|
|
50
|
+
if (this._tooltip) {
|
|
51
|
+
this._tooltip.remove();
|
|
52
|
+
this._tooltip = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
handleClick () {
|
|
57
|
+
navigator.clipboard.writeText(this.anchoredUrl).then(() => {
|
|
58
|
+
if (this._tooltip) return;
|
|
59
|
+
this._tooltip = this.createTooltip();
|
|
60
|
+
this.isCopied = true;
|
|
61
|
+
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
if (typeof window.dsfr === 'function') {
|
|
64
|
+
window.dsfr(this._tooltip).tooltip.show();
|
|
65
|
+
window.dsfr(this._tooltip).tooltip.isEnabled = false;
|
|
66
|
+
setTimeout(this._handleTimeout.bind(this), 1500);
|
|
67
|
+
}
|
|
68
|
+
}, 10);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_handleTimeout () {
|
|
73
|
+
window.dsfr(this._tooltip).tooltip.hide();
|
|
74
|
+
this.isCopied = false;
|
|
75
|
+
this.removeTooltip();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default CopyLink;
|
package/src/script/main/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import './minisearch/index.js';
|
|
2
2
|
import { instantiateElements } from './core/element.js'
|
|
3
3
|
import CopySnippet from './elements/copy-snippet.js';
|
|
4
|
+
import CopyLink from './elements/copy-link.js';
|
|
4
5
|
import Storybook from './elements/storybook.js';
|
|
5
6
|
import { SearchBar } from './elements/search-bar/index.js';
|
|
6
7
|
import { ConsentManagementPlatform } from './cmp/index.js';
|
|
7
8
|
|
|
8
9
|
window.onload = async () => {
|
|
9
10
|
await instantiateElements('.code-snippet--copy', CopySnippet);
|
|
11
|
+
await instantiateElements('.dsfr-doc-anchor-heading__button', CopyLink);
|
|
10
12
|
await instantiateElements('.dsfr-doc-storybook-leaf iframe', Storybook);
|
|
11
13
|
await instantiateElements('#search', SearchBar);
|
|
12
14
|
};
|
|
@@ -1,46 +1,74 @@
|
|
|
1
|
+
import { BadgesMap } from '../../../page/body/badges-map.js';
|
|
2
|
+
|
|
1
3
|
class ResultCard {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
4
|
+
constructor (data) {
|
|
5
|
+
this._title = data.title;
|
|
6
|
+
this._url = data.url;
|
|
7
|
+
this._desc = data.excerpt ?? data.summary;
|
|
8
|
+
this._cover = data.cover || '/static/img/placeholder.16x9.png';
|
|
9
|
+
this._section = data.section;
|
|
10
|
+
this._badge = data.badge || [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
getTags () {
|
|
14
|
+
if (!this._section) return '';
|
|
15
|
+
return `<div class="fr-card__start">
|
|
16
|
+
<ul class="fr-tags-group">
|
|
17
|
+
<li>
|
|
18
|
+
<p class="fr-tag">${this._section}</p>
|
|
19
|
+
</li>
|
|
20
|
+
</ul>
|
|
21
|
+
</div>`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getBadges () {
|
|
25
|
+
if (!this._badge?.length) return '';
|
|
26
|
+
|
|
27
|
+
const badges = Array.isArray(this._badge) ? this._badge : this._badge.split(',').map(b => b.trim());
|
|
28
|
+
const badgeItems = badges
|
|
29
|
+
.map(badge => {
|
|
30
|
+
const badgeData = BadgesMap.get(badge);
|
|
31
|
+
if (!badgeData) return null;
|
|
32
|
+
|
|
33
|
+
const badgeLabel = window.resource?.badge?.[badge] || badge;
|
|
34
|
+
let iconClass = '';
|
|
35
|
+
if (badgeData.icon === false) {
|
|
36
|
+
iconClass = ' fr-badge--no-icon';
|
|
37
|
+
} else {
|
|
38
|
+
iconClass = ` fr-badge--icon-${badgeData.icon}`;
|
|
39
|
+
}
|
|
40
|
+
return `<li><span class="fr-badge fr-badge--${badgeData.type} fr-badge--${badgeData.size}${iconClass}">${badgeLabel}</span></li>`;
|
|
41
|
+
})
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
.join('');
|
|
44
|
+
|
|
45
|
+
return `<ul class="fr-badges-group fr-mb-2v">${badgeItems}</ul>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getCover () {
|
|
49
|
+
if (!this._cover) return '';
|
|
50
|
+
return `<div class="fr-card__header">
|
|
51
|
+
<div class="fr-card__img">
|
|
52
|
+
<img src="${this._cover}" alt="" class="fr-responsive-img">
|
|
53
|
+
</div>
|
|
54
|
+
${this.getBadges()}
|
|
55
|
+
</div>`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
render () {
|
|
59
|
+
return `<div class="fr-card fr-enlarge-link fr-card--horizontal fr-mb-8v">
|
|
60
|
+
<div class="fr-card__body">
|
|
61
|
+
<div class="fr-card__content">
|
|
62
|
+
<h3 class="fr-card__title">
|
|
63
|
+
<a href="${this._url}">${this._title}</a>
|
|
64
|
+
</h3>
|
|
65
|
+
${this._desc ? `<p class="fr-card__desc">${this._desc}</p>` : ''}
|
|
66
|
+
${this.getTags()}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
${this.getCover()}
|
|
70
|
+
</div>`;
|
|
71
|
+
}
|
|
44
72
|
}
|
|
45
73
|
|
|
46
74
|
export { ResultCard };
|
|
@@ -7,39 +7,39 @@ import { Pagination } from '../../main/elements/pagination/index.js';
|
|
|
7
7
|
const RESULTS_PER_PAGES = 10;
|
|
8
8
|
|
|
9
9
|
class SearchPage extends Element {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
constructor(element) {
|
|
11
|
+
super(element, 'searchPage');
|
|
12
|
+
|
|
13
|
+
this._query = getQuery();
|
|
14
|
+
this._searchBarInput = document.querySelector('#search-input');
|
|
15
|
+
this._resultsCount = document.querySelector('#results-count');
|
|
16
|
+
this._resultsList = document.querySelector('#results-cards');
|
|
17
|
+
this._container = element.querySelector('#results-page--container');
|
|
18
|
+
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
async init() {
|
|
21
|
+
await window.searchEngine.init('searchPage');
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
if (this._query) {
|
|
24
|
+
this._searchBarInput.value = this._query;
|
|
25
|
+
const results = window.searchEngine.search(this._query);
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const count = new ResultsCount(results.length);
|
|
28
|
+
const resultsList = new ResultsList(results, RESULTS_PER_PAGES);
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
this._resultsCount.innerHTML = count.render();
|
|
31
|
+
document.title = `${document.title} - ${count.render()}`;
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
this._container.appendChild(resultsList.element);
|
|
34
|
+
resultsList.init();
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
36
|
+
if (results.length > RESULTS_PER_PAGES) {
|
|
37
|
+
const pagination = new Pagination(results, RESULTS_PER_PAGES);
|
|
38
|
+
this._container.appendChild(pagination.element);
|
|
39
|
+
pagination.init();
|
|
40
|
+
}
|
|
42
41
|
}
|
|
42
|
+
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export { SearchPage };
|
package/src/template/template.js
CHANGED
|
@@ -5,18 +5,23 @@ const PERMALINK_CONTENT_REGEX = /<!--\s*NO_PERMALINK_START\s*-->\s(?<content>[\s
|
|
|
5
5
|
const PREVENT_PERMALINKS_REGEX = /(?<permalinks><!--\s*NO_PERMALINK_START\s*-->\s(?:[\s\S]*)\s<!--\s*NO_PERMALINK_END\s*-->)/g;
|
|
6
6
|
const HEADING_REGEX = /<h(?<level>[2-6])(?<attrs>[^>]*)>(?<content>.*?)<\/h\1>/g;
|
|
7
7
|
const ATTRIBUTE_ID_REGEX = /\sid=["'](?<id>[^"']+)["']/;
|
|
8
|
-
const
|
|
8
|
+
const HEADING_BUTTON_CLASS = 'fr-btn--tooltip fr-btn fr-btn--sm fr-icon-link fr-btn--tertiary-no-outline dsfr-doc-anchor-heading__button';
|
|
9
9
|
|
|
10
|
-
const injectPermaLink = (match, level, attrs, content) => {
|
|
10
|
+
const injectPermaLink = (match, level, attrs, content, fragments) => {
|
|
11
11
|
const idMatch = attrs.match(ATTRIBUTE_ID_REGEX);
|
|
12
12
|
if (!idMatch || !idMatch.groups?.id) return match;
|
|
13
|
-
const
|
|
14
|
-
|
|
13
|
+
const copied = fragments?.button?.['link-copied'];
|
|
14
|
+
const copy = fragments?.button?.['copy-link'];
|
|
15
|
+
const buttonId = `${idMatch.groups.id}-anchor`;
|
|
16
|
+
const anchorButton = `<button type="button" id="${buttonId}" title="${copy}" data-label-copied="${copied}" class="${HEADING_BUTTON_CLASS}"></button>`;
|
|
17
|
+
return `<h${level}${attrs}>${content}${anchorButton}</h${level}>`;
|
|
18
|
+
|
|
15
19
|
};
|
|
16
20
|
|
|
17
21
|
class Template extends Renderable {
|
|
18
22
|
constructor (data, hasPermaLinks = false) {
|
|
19
23
|
super(data);
|
|
24
|
+
this._fragments = data.fragments;
|
|
20
25
|
nodeFactory.populate(data.fragments);
|
|
21
26
|
this._hasPermaLinks = hasPermaLinks;
|
|
22
27
|
this._content = nodeFactory.create({
|
|
@@ -45,7 +50,10 @@ class Template extends Renderable {
|
|
|
45
50
|
if (match && match.groups && match.groups.content) {
|
|
46
51
|
return match.groups.content;
|
|
47
52
|
}
|
|
48
|
-
|
|
53
|
+
|
|
54
|
+
return part.replace(HEADING_REGEX, (match, level, attrs, content) => {
|
|
55
|
+
return injectPermaLink(match, level, attrs, content, this._fragments);
|
|
56
|
+
});
|
|
49
57
|
}).join('');
|
|
50
58
|
}
|
|
51
59
|
|