diff --git a/components/accordion/accordion.component.yml b/components/accordion/accordion.component.yml index 5c1a7a2176959f1c7d6419d3b06443dceed23f49..9b054436089ff98d57c82bf717b55ed3b88a79c7 100644 --- a/components/accordion/accordion.component.yml +++ b/components/accordion/accordion.component.yml @@ -10,6 +10,9 @@ props: attributes: type: Drupal\Core\Template\Attribute title: Attributes + button_attributes: + type: Drupal\Core\Template\Attribute + title: Button attributes content: type: string title: Content diff --git a/components/accordion/accordion.twig b/components/accordion/accordion.twig index 1b4df84b2346a2b07ee71b663be4b8b47c5841f6..ec6060f682e2d5f19e338859e8ea2826c40722e5 100644 --- a/components/accordion/accordion.twig +++ b/components/accordion/accordion.twig @@ -1,13 +1,16 @@ {% set accordion_id = accordion_id|default('accordion-' ~ random()) %} {% set attributes = attributes|default(create_attribute()) %} +{% set button_attributes = button_attributes|default(create_attribute()) %} {% set title_tag = title_tag|default('h3') %} <section{{ attributes.addClass('fr-accordion') }}> <{{ title_tag }} class="fr-accordion__title"> - <button class="fr-accordion__btn" - aria-expanded="{{ expanded ? 'true' : 'false' }}" - aria-controls="{{ accordion_id }}" - type="button"> + <button{{ button_attributes + .addClass('fr-accordion__btn') + .setAttribute('type', button) + .setAttribute('aria-expanded', expanded ? 'true' : 'false') + .setAttribute('aria-controls', accordion_id) }} + > {{ title }} </button> </{{ title_tag }}> diff --git a/components/card/card.component.yml b/components/card/card.component.yml index 6b9c967617293e8dfbab22a9ac28c406a7d30511..cc6b0da53a45fcc843cf6e6b7b82fe0159457ddf 100644 --- a/components/card/card.component.yml +++ b/components/card/card.component.yml @@ -72,6 +72,9 @@ props: title: type: string title: Title + title_attributes: + type: Drupal\Core\Template\Attribute + title: Title attributes title_tag: type: string title: Title HTML tag diff --git a/components/card/card.twig b/components/card/card.twig index 1f95bf3b81169d78e02ffff4c684af9264dae92b..1b32a00aae381c35db197feaa431af63d0dba429 100644 --- a/components/card/card.twig +++ b/components/card/card.twig @@ -1,6 +1,7 @@ {% set attributes = attributes|default(create_attribute()) %} {% set title_tag = title_tag|default('h3') %} {% set link_attributes = link_attributes|default(create_attribute()) %} +{% set title_attributes = title_attributes|default(create_attribute()) %} {% if variant %} {% set attributes = attributes.addClass('fr-card--' ~ variant) %} @@ -32,7 +33,7 @@ <div{{ attributes.addClass('fr-card') }}> <div class="fr-card__body"> <div class="fr-card__content"> - <{{ title_tag }} class="fr-card__title"> + <{{ title_tag }}{{ title_attributes.addClass('fr-card__title') }}> {% if use_button %} <buton{{ link_attributes }}>{{ title }}</buton> {% elseif url %} diff --git a/components/modal/modal.component.yml b/components/modal/modal.component.yml index 183171f0f715836b03caa9fa2dd41c7465e8d994..e2dff3c07ea3dbe05d5d198df549e2b0da6f5452 100644 --- a/components/modal/modal.component.yml +++ b/components/modal/modal.component.yml @@ -15,16 +15,25 @@ props: type: string title: Button title attribute description: 'By default: "Close", or the translated string in another language.' + content: + type: string + title: Content + footer: + type: string + title: Footer html_id: type: string title: HTML identifier title: type: string title: Title + title_tag: + type: string + title: Title HTML tag + default: h1 slots: modal_content: title: Modal content - required: true modal_footer: title: Modal footer modal_title: diff --git a/components/modal/modal.twig b/components/modal/modal.twig index 6acc70c9fcb35c25aa077053ff14fcbe3e7fec39..652e233cd6a4831d27dcf59e7bbd78e455d23279 100644 --- a/components/modal/modal.twig +++ b/components/modal/modal.twig @@ -2,6 +2,7 @@ {% set button_label = button_label|default('Close'|t) %} {% set button_title = button_title|default('Close'|t) %} {% set html_id = html_id|default('modal-default') %} +{% set title_tag = title_tag|default('h1') %} <dialog id="{{ html_id }}" class="fr-modal" role="dialog" aria-labelledby="{{ html_id ~ '-title' }}"> <div class="fr-container fr-container--fluid fr-container-md"> @@ -14,19 +15,20 @@ </button> </div> <div class="fr-modal__content"> - <div id="{{ html_id ~ '-title' }}" class="fr-modal__title"> + <{{ title_tag }} id="{{ html_id ~ '-title' }}" class="fr-modal__title"> {% block modal_title %} {{ title }} {% endblock modal_title %} - </div> + </{{ title_tag }}> {% block modal_content %} - <p>{{ 'Default content.'|t }}</p> + {{ content }} {% endblock modal_content %} </div> - {% if modal_footer or block('modal_footer') is defined %} + + {% if footer or (block('modal_footer') is defined and block('modal_footer')|trim is not empty) %} <div class="fr-modal__footer"> {% block modal_footer %} - {{ modal_footer }} + {{ footer }} {% endblock modal_footer %} </div> {% endif %} diff --git a/components/transcription/transcription.component.yml b/components/transcription/transcription.component.yml index 3c89bd474967b386122e71d6e39e2c313f4afdc1..0c0f6d8870c4197792aff8502c4bb868c954f355 100644 --- a/components/transcription/transcription.component.yml +++ b/components/transcription/transcription.component.yml @@ -7,10 +7,20 @@ props: attributes: type: Drupal\Core\Template\Attribute title: Attributes + button_label: + type: string + title: Button label + button_title: + type: string + title: Button title attribute content: type: string title: Content description: Display into transcription modal. + expanded: + type: boolean + title: Is expanded? + default: false modal_id: type: string title: Modal identifier diff --git a/components/transcription/transcription.twig b/components/transcription/transcription.twig index e47af06b837a9f062d8b88183169e9ea0a0a1eed..150816ef6986609e949860002e844a1d0501283c 100644 --- a/components/transcription/transcription.twig +++ b/components/transcription/transcription.twig @@ -1,20 +1,23 @@ {% set attributes = attributes|default(create_attribute()) %} +{% set button_label = button_label|default('Enlarge'|t) %} +{% set button_title = button_title|default('Enlarge transcription'|t) %} {% set title = title|default('Transcription'|t) %} {% set transcription_id = transcription_id|default('transcription-' ~ random()) %} {% set collapse_id = 'fr-transcription-collapse-' ~ transcription_id %} {% set modal_id = 'fr-transcription-modal-' ~ transcription_id %} {% set modal_title = modal_title|default(title) %} +{% set expanded = expanded ?? false %} <div{{ attributes.addClass('fr-transcription') }}> - <button type="button" class="fr-transcription__btn" aria-expanded="false" aria-controls="{{ collapse_id }}"> + <button type="button" class="fr-transcription__btn" aria-expanded="{{ expanded ? 'true': 'false' }}" aria-controls="{{ collapse_id }}"> {{ title }} </button> <div class="fr-collapse" id="{{ collapse_id }}"> <div class="fr-transcription__footer"> <div class="fr-transcription__actions-group"> - <button type="button" class="fr-btn fr-btn--fullscreen" aria-controls="{{ modal_id }}" data-fr-opened="false" title="{{ 'Enlarge transcription'|t }}"> - {{ 'Enlarge'|t }} + <button type="button" class="fr-btn fr-btn--fullscreen" aria-controls="{{ modal_id }}" data-fr-opened="false" title="{{ button_title }}"> + {{ button_label }} </button> </div> </div> diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000000000000000000000000000000000000..c35d1e992b9ebd6eb2454446b98cb59f5ba55a97 --- /dev/null +++ b/composer.lock @@ -0,0 +1,18 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "e2b57849f956d4c67658848cdb027ed8", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/css/core.css b/css/base/core.css similarity index 100% rename from css/core.css rename to css/base/core.css diff --git a/css/ckeditor5.css b/css/ckeditor5.css index a1ca5daca89d9231f3d281f3b3c6975166fa00a6..94e00f699e6279fe692f75abd269ab331681f1b9 100644 --- a/css/ckeditor5.css +++ b/css/ckeditor5.css @@ -111,7 +111,7 @@ --underline-thickness: 0.0625em; --underline-img: linear-gradient(0deg, currentcolor, currentcolor); --xl-base: 1em; - --ol-content: counters(li-counter, ".") ".  "; + --ol-content: counters(li-counter, ".") ".  "; /* stylelint-disable-line no-irregular-whitespace */ color: var(--text-default-grey); font-family: Marianne,arial,sans-serif; @@ -252,78 +252,103 @@ .ck-content ol[start] { counter-reset: none; } + .ck-content ol[start="0"] { counter-reset: li-counter -1; } + .ck-content ol[start="2"] { counter-reset: li-counter 1; } + .ck-content ol[start="3"] { counter-reset: li-counter 2; } + .ck-content ol[start="4"] { counter-reset: li-counter 3; } + .ck-content ol[start="5"] { counter-reset: li-counter 4; } + .ck-content ol[start="6"] { counter-reset: li-counter 5; } + .ck-content ol[start="7"] { counter-reset: li-counter 6; } + .ck-content ol[start="8"] { counter-reset: li-counter 7; } + .ck-content ol[start="9"] { counter-reset: li-counter 8; } + .ck-content ol[start="10"] { counter-reset: li-counter 9; } + .ck-content ol[start="11"] { counter-reset: li-counter 10; } + .ck-content ol[start="12"] { counter-reset: li-counter 11; } + .ck-content ol[start="13"] { counter-reset: li-counter 12; } + .ck-content ol[start="14"] { counter-reset: li-counter 13; } + .ck-content ol[start="15"] { counter-reset: li-counter 14; } + .ck-content ol[start="16"] { counter-reset: li-counter 15; } + .ck-content ol[start="17"] { counter-reset: li-counter 16; } + .ck-content ol[start="18"] { counter-reset: li-counter 17; } + .ck-content ol[start="19"] { counter-reset: li-counter 18; } + .ck-content ol[start="20"] { counter-reset: li-counter 19; } + .ck-content ol[start="21"] { counter-reset: li-counter 20; } + .ck-content ol[start="22"] { counter-reset: li-counter 21; } + .ck-content ol[start="23"] { counter-reset: li-counter 22; } + .ck-content ol[start="24"] { counter-reset: li-counter 23; } + .ck-content ol[start="25"] { counter-reset: li-counter 24; } diff --git a/css/component/action-links.css b/css/component/action-links.css new file mode 100644 index 0000000000000000000000000000000000000000..3cb17c608ec7ae3231cf1715d65bd0e56b1d65cb --- /dev/null +++ b/css/component/action-links.css @@ -0,0 +1,17 @@ +/** + * @file + * Styles for local action links. +*/ + +.local-actions { + --local-action-margin: 1rem; + + list-style: none; + margin: 0 calc(var(--local-action-margin) * -1); + padding: 0; +} + +.local-actions li { + display: inline-block; + margin: 0 var(--local-action-margin); +} diff --git a/css/component/checkbox.css b/css/component/checkbox.css new file mode 100644 index 0000000000000000000000000000000000000000..462f7872729cdae398d5a4fe318de5eb91f549e7 --- /dev/null +++ b/css/component/checkbox.css @@ -0,0 +1,19 @@ +/** + * @file + * Manage styles for checkbox. + */ + +/* Fix checkbox rendering when label is visually hidden */ +.fr-checkbox-group input[type="checkbox"] + label.visually-hidden { + width: 1.5rem; + height: 1.5rem; + margin-left: 0; + margin-top: 0; + padding-left: 1.5rem; + clip: auto; + z-index: 1; +} + +.fr-checkbox-group input[type="checkbox"] + label.visually-hidden::before { + left: 0; +} diff --git a/css/component/dropbutton.css b/css/component/dropbutton.css new file mode 100644 index 0000000000000000000000000000000000000000..fbf48812eaa7181d7288b00b3dd2d5207bc6cbad --- /dev/null +++ b/css/component/dropbutton.css @@ -0,0 +1,39 @@ +/** + * @file + * Manage styles for dropbutton. + */ + +html.js .dropbutton-wrapper .dropbutton .secondary-action { + display: block; +} + +.dropbutton-wrapper:not(.open) .dropbutton__item:first-of-type ~ .dropbutton__item { + visibility: hidden; + /** + * By setting a height of 1px, the dropbutton items are hidden + * from view while still occupying minimal space, ensuring the layout remains intact. + */ + height: 1px; +} + +.dropbutton__items { + position: fixed; + padding: .25rem; + background-color: var(--background-default-grey); +} + +.dropbutton__items a { + margin-right: 2em; +} + +.dropbutton li { + padding-bottom: 0; +} + +.js td .dropbutton-multiple { + padding-right: 0; +} + +.js .dropbutton li, .js .dropbutton a { + display: inline-block; +} diff --git a/css/component/form.css b/css/component/form.css new file mode 100644 index 0000000000000000000000000000000000000000..e8c9651ada73ea184335c3c208f1009f62569348 --- /dev/null +++ b/css/component/form.css @@ -0,0 +1,119 @@ +/** + * @file + * Manage styles for form. + */ + +:root { + --form-spacing: 1.5rem; + --form-spacing-s: calc(var(--form-spacing) * .67); + --form-spacing-xs: calc(var(--form-spacing) / 3); + --form-item-spacing: var(--form-spacing); + + /* Label font-size * line-height */ + --form-label-line-height: calc(1rem * 1.5rem); + /* Spacing between label and input. */ + --form-label-input-spacing: .5rem; +} + +/* Provide a class to remove spacing. */ +.dsfr4drupal-form--no-spacing { + --form-spacing: 0; +} + +/* Unset form spacing for default DSFR forms. */ +.fr-follow__newsletter, +.fr-search-bar { + --form-spacing: 0; +} + +/* Provide a class to wrap form with borders. */ +.dsfr4drupal-form--bordered { + padding: var(--form-spacing-xs) var(--form-spacing-s) var(--form-spacing-s); + border: 1px solid var(--border-default-grey); +} + +.fr-checkbox-group + .fr-checkbox-group, +.fr-radio-group + .fr-radio-group { + margin-top: .25rem; +} + +.form-actions a + button, +.form-actions button + button, +.form-actions button + a { + margin-left: var(--form-item-spacing); +} + +.fr-label > .field-edit-link { + font-size: .75em; +} + +.fr-label .field-edit-link > button { + padding: 0; +} + +.fr-upload-group { + padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing); + border: 1px solid var(--border-default-grey); +} + +.fr-upload-group > label { + font-weight: 700; +} + +/* Remove specific spacing into fieldset - START */ +.fr-fieldset__content .fr-checkbox-group input[type="checkbox"] + label::before, +.fr-fieldset__content .fr-radio-group:not(.fr-radio-rich) input[type="radio"] + label::before { + top: auto; +} + +.fr-fieldset__content .fr-checkbox-group label, +.fr-fieldset__content .fr-radio-group label { + padding: 0; +} +/* Remove specific spacing into fieldset - END */ + +main[role="main"] > .fr-container > form, +main[role="main"] > .fr-container--fluid > form { + margin-block: var(--form-spacing); +} + +.form-wrapper:not(:last-child) { + margin-bottom: var(--form-item-spacing); +} + +.form-wrapper.js-filter-wrapper { + font-size: .75em; +} + +/* Disable table scrolling into form item - START */ +.form-item .fr-table__container { + overflow: initial; +} + +.form-item .fr-table > table caption { + max-width: calc(100vw - 2rem); +} + +.form-item .fr-table .fr-table__wrapper .fr-table__content table th, +.form-item .fr-table .fr-table__wrapper .fr-table__content table td { + white-space: normal; +} +/* Disable table scrolling into form - END */ + +.js-filter-wrapper { + margin-top: calc(var(--form-item-spacing) * -.75); +} + +.js input.form-autocomplete { + padding-right: 2.5rem; + background-position: calc(100% - 1rem) 50%; +} + +.paragraphs-tabs-wrapper { + padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing); + border: 1px solid var(--border-default-grey); +} + +.paragraphs-tabs-wrapper > .form-item { + margin-block: 0; +} diff --git a/css/drupal.node.preview.css b/css/component/node-preview.css similarity index 69% rename from css/drupal.node.preview.css rename to css/component/node-preview.css index bf08321352c675e92dae8727ee02f744723e5e13..860c39f0e8a3fbc89ab2131fc4612e9e1e53f5f1 100644 --- a/css/drupal.node.preview.css +++ b/css/component/node-preview.css @@ -1,6 +1,6 @@ /** * @file - * Manage node preview styling. + * Manage styles for node preview. */ .node-preview-container { @@ -8,7 +8,7 @@ background-position: 0 100%; background-repeat: no-repeat; background-size: 100% .25rem; - background-image: linear-gradient(0deg,var(--border-plain-grey),var(--border-plain-grey)); + background-image: linear-gradient(0deg,var(--border-default-grey),var(--border-default-grey)); padding: 2rem 2rem 2.25rem; /* Based on DSFR header z-index. */ z-index: calc(var(--ground) + 751); diff --git a/css/component/paragraphs.admin.css b/css/component/paragraphs.admin.css new file mode 100644 index 0000000000000000000000000000000000000000..bcf024f37a55d7d0be2e1750678a6212cdd3ec61 --- /dev/null +++ b/css/component/paragraphs.admin.css @@ -0,0 +1,29 @@ +/** + * @file + * Manage styles for paragraphs admin. + */ + +.paragraph-type-title { + font-size: 1.5em; + font-weight: 700; + margin-top: var(--form-spacing-xs); +} + +.js .field--widget-entity-reference-paragraphs td { + /* Force DSFR table styles */ + padding: .5rem 1rem; +} + +.js .field--widget-entity-reference-paragraphs .field-multiple-table > thead > tr > th:nth-child(2), +.js .field--widget-entity-reference-paragraphs .field-multiple-table > tbody > tr > td:nth-child(3) { + padding: 0; +} + +.js .field--widget-entity-reference-paragraphs .field-multiple-drag { + vertical-align: middle; + padding-right: 0; +} + +.paragraphs-tabs-wrapper .field-multiple-table > thead > tr .field-label .paragraphs-actions { + margin-inline: auto 0; +} diff --git a/css/component/paragraphs.widget.css b/css/component/paragraphs.widget.css new file mode 100644 index 0000000000000000000000000000000000000000..2c299990f740750c57a3607d65242532a2762c18 --- /dev/null +++ b/css/component/paragraphs.widget.css @@ -0,0 +1,45 @@ +/** + * @file + * Manage styles for paragraphs widget. + */ + +.paragraph-type-label { + font-size: 1.5em; + font-weight: 700; +} + +.js .field--widget-paragraphs td { + /* Force DSFR table styles */ + padding: .5rem 1rem; +} + +.js .field--widget-paragraphs .field-multiple-drag { + min-width: auto; +} + +.js .field--widget-paragraphs .draggable .tabledrag-handle { + padding: 0; +} + +.js .field--widget-paragraphs .draggable .field-multiple-drag { + padding-right: 0; +} + +.paragraphs-tabs-wrapper .field-multiple-table > thead > tr > th:nth-child(2), +.paragraphs-tabs-wrapper .field-multiple-table > tbody > tr > td:nth-child(3) { + padding: 0; +} + +.paragraphs-tabs-wrapper .field-multiple-table > thead > tr .field-label .label { + display: inline-block; +} + +.paragraphs-tabs-wrapper .field-multiple-table > thead > tr .field-label .paragraphs-actions { + margin-inline: auto 0; +} + +.is-horizontal .paragraphs-tabs:first-of-type { + /* Remove unwanted styles */ + position: static; + background-color: transparent; +} diff --git a/css/component/radio.css b/css/component/radio.css new file mode 100644 index 0000000000000000000000000000000000000000..ffc48409cd9fd8ca6aac015e5fdc9ee35d2e402e --- /dev/null +++ b/css/component/radio.css @@ -0,0 +1,12 @@ +/** + * @file + * Manage styles for radio. + */ + +.fr-fieldset .fr-fieldset__content .fr-radio-group:not(.fr-radio-rich) input[type="radio"] { + top: auto; +} + +.fr-fieldset .fr-fieldset__content .fr-radio-group:not(.fr-radio-rich) input[type="radio"] + label { + background-position: left center; +} diff --git a/css/component/table.css b/css/component/table.css new file mode 100644 index 0000000000000000000000000000000000000000..962213b1f10a2c38c08ecf48659c548dfdd3acf6 --- /dev/null +++ b/css/component/table.css @@ -0,0 +1,14 @@ +/** + * @file + * Manage styles for table. + */ + +.fr-table__content td .fr-checkbox-group, +.fr-table__content th .fr-checkbox-group { + vertical-align: top; +} + +.fr-table__content th.select-all input[type="checkbox"] { + width: 1.5rem; + height: 1.5rem; +} diff --git a/css/component/tabledrag.css b/css/component/tabledrag.css new file mode 100644 index 0000000000000000000000000000000000000000..76f5569ba94e1ebd8930d37d994caef2c39ef35c --- /dev/null +++ b/css/component/tabledrag.css @@ -0,0 +1,42 @@ +/** + * @file + * Manage styles for tabledrag. + */ + +a.tabledrag-handle[href] { + background-image: none; +} + +.draggable a.tabledrag-handle { + margin: 0; +} + +a.tabledrag-handle .handle { + margin: 0; + padding: 0; + min-width: 16px; + min-height: 16px; + height: 100%; + background-position: center center; +} + +.draggable.drag td, +.draggable.drag th { + background-color: var(--background-default-grey-active); +} + +.field-multiple-drag { + max-width: fit-content; +} + +.tabledrag-toggle-weight { + font-size: .75em; +} + +.tabledrag-toggle-weight-wrapper + .fr-table { + margin-top: 0; +} + +.tabledrag-changed-warning { + color: var(--warning-425-625); +} diff --git a/css/toolbar.css b/css/component/toolbar.css similarity index 86% rename from css/toolbar.css rename to css/component/toolbar.css index 1989351ca3c2578409252ead1ecce2777441b1c1..235df43c2aaf6e3c8b3ca82123ec289947385077 100644 --- a/css/toolbar.css +++ b/css/component/toolbar.css @@ -1,6 +1,6 @@ /** * @file - * Fix toolbar styling. + * Manage styles for toolbar. */ .toolbar a { diff --git a/css/component/views-exposed-form.css b/css/component/views-exposed-form.css new file mode 100644 index 0000000000000000000000000000000000000000..263b11ac228b1b805297d39631b662bd60ce5ab0 --- /dev/null +++ b/css/component/views-exposed-form.css @@ -0,0 +1,39 @@ +/** + * @file + * Manage styles for views exposed form. + */ + +/** + * Use flexbox and some margin resets to make the fields + actions go inline. + */ +.views-exposed-form--inline { + display: flex; + flex-wrap: wrap; + margin-block: var(--form-spacing-s); +} + +.views-exposed-form--preview.views-exposed-form--preview { + margin-top: 0; +} + +.views-exposed-form--inline .views-exposed-form__item { + max-width: 100%; + margin-block: var(--form-spacing-s) 0; + margin-inline: 0 var(--form-spacing-xs); +} + +.views-exposed-form--inline .form-item--no-label, +.views-exposed-form--inline .views-exposed-form__item.views-exposed-form__item.views-exposed-form__item--actions { + margin-block: var(--form-spacing-s) 0; + align-self: flex-end; +} + +.views-exposed-form--inline .form-item--no-label, +.views-exposed-form--inline .views-exposed-form__item.views-exposed-form__item--actions { + margin-top: calc(var(--form-label-line-height) + var(--form-label-input-spacing)); +} + +.views-exposed-form--inline .fr-input-group:not(:last-child), +.views-exposed-form--inline .fr-select-group:not(:last-child) { + margin-bottom: 0; +} diff --git a/css/card.css b/css/theme/card.css similarity index 59% rename from css/card.css rename to css/theme/card.css index 47925848e327b6bd01b2883630f9a5600f9d6e4b..32f5bf9a87ad13f53305b26647a30c690d6993e7 100644 --- a/css/card.css +++ b/css/theme/card.css @@ -1,6 +1,6 @@ /** * @file - * Fix card styling with "time" html tag. + * Fix styles for card with "time" html tag. */ .fr-card__detail time { diff --git a/css/display.button.css b/css/theme/display.button.css similarity index 100% rename from css/display.button.css rename to css/theme/display.button.css diff --git a/css/theme/horizontal-tabs.css b/css/theme/horizontal-tabs.css new file mode 100644 index 0000000000000000000000000000000000000000..6943d5d17e23fe9cd09c08c8a9f28e43917f8e4d --- /dev/null +++ b/css/theme/horizontal-tabs.css @@ -0,0 +1,62 @@ +/** + * @file + * Manage styles for horizontal tabs. + * Try as much as possible to reproduce the styles of the DSFR tabs. + */ + +.horizontal-tabs { + margin: 0 0 var(--form-spacing); + border: 0; +} + +.horizontal-tabs .horizontal-tabs-list { + /* Remove unwanted styles */ + background: none; + border: 0; + + /* Force DSFR tabs styles */ + display: flex; + margin: -4px 0; + padding: 4px .75rem; +} + +.horizontal-tabs ul.horizontal-tabs-list li a { + /* Force DSFR tabs styles */ + display: inline-flex; + padding: .5rem 1rem; + position: relative; +} + +.horizontal-tabs .horizontal-tab-button { + /* Remove unwanted styles */ + float: none; + background: none; + border: 0; + min-width: auto; +} + +.horizontal-tabs .horizontal-tab-button a:hover { + /* Force DSFR tabs styles */ + background-color: var(--hover-tint); +} + +.horizontal-tabs-panes { + border: 1px solid var(--border-default-grey); + border-top: 0; + padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing-s); +} + +.horizontal-tabs-panes .horizontal-tabs-pane { + padding: 0; +} + +.horizontal-tabs-list.fr-tabs__list + .horizontal-tabs-panes { + /* Fix order */ + order: 3; +} + +.horizontal-tabs-panes .fr-accordion__btn, +.horizontal-tabs-panes .fr-input, +.horizontal-tabs-panes .fr-select { + box-sizing: border-box; +} diff --git a/css/theme/layout-builder.css b/css/theme/layout-builder.css new file mode 100644 index 0000000000000000000000000000000000000000..1f1274fe6cf45092089bc92d6431251afe571f99 --- /dev/null +++ b/css/theme/layout-builder.css @@ -0,0 +1,43 @@ +/** + * @file + * Manage styles for layout builder. + */ + +.layout-builder__link--add { + color: var(--text-default-grey); + padding-left: 0; +} + +.layout-builder__link--add[href]:hover { + --underline-hover-width: 0; +} + +.layout-builder__link--add::before { + content: ''; + display: inline-block; + vertical-align: middle; + width: 16px; + height: 16px; + background-color: var(--text-default-grey); + mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16px' height='16px'%3e%3cpath d='M0.656,9.023c0,0.274,0.224,0.5,0.499,0.5l4.853,0.001c0.274-0.001,0.501,0.226,0.5,0.5l0.001,4.853 c-0.001,0.273,0.227,0.5,0.501,0.5l1.995-0.009c0.273-0.003,0.497-0.229,0.5-0.503l0.002-4.806c0-0.272,0.228-0.5,0.499-0.502 l4.831-0.021c0.271-0.005,0.497-0.23,0.501-0.502l0.008-1.998c0-0.276-0.225-0.5-0.499-0.5l-4.852,0c-0.275,0-0.502-0.228-0.501-0.5 L9.493,1.184c0-0.275-0.225-0.499-0.5-0.499L6.997,0.693C6.722,0.694,6.496,0.92,6.495,1.195L6.476,6.026 c-0.001,0.274-0.227,0.5-0.501,0.5L1.167,6.525C0.892,6.526,0.665,6.752,0.665,7.026L0.656,9.023z'/%3e%3c/svg%3e"); + mask-position: left center; + mask-repeat: no-repeat; + mask-size: 100%; + margin-right: .25rem; +} + +.layout-builder__link--add::before:hover { + background-color: var(--hover-tint); +} + +.layout-builder__link--remove { + /* Force layout builder styles. */ + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23bebebe' d='M3.51 13.925c.194.194.512.195.706.001l3.432-3.431c.194-.194.514-.194.708 0l3.432 3.431c.192.194.514.193.707-.001l1.405-1.417c.191-.195.189-.514-.002-.709l-3.397-3.4c-.192-.193-.192-.514-.002-.708l3.401-3.43c.189-.195.189-.515 0-.709l-1.407-1.418c-.195-.195-.513-.195-.707-.001l-3.43 3.431c-.195.194-.516.194-.708 0l-3.432-3.431c-.195-.195-.512-.194-.706.001l-1.407 1.417c-.194.195-.194.515 0 .71l3.403 3.429c.193.195.193.514-.001.708l-3.4 3.399c-.194.195-.195.516-.001.709l1.406 1.419z'/%3e%3c/svg%3e"); + background-position: center center; + background-repeat: no-repeat; + background-size: auto; +} + +.layout-builder__region { + margin-top: .25rem; +} diff --git a/css/theme/media-library.css b/css/theme/media-library.css new file mode 100644 index 0000000000000000000000000000000000000000..a31b7ac1932e5503b7d882cb9947a0d5711cbd3d --- /dev/null +++ b/css/theme/media-library.css @@ -0,0 +1,68 @@ +/** + * @file + * Manage styles for media library. + */ + +.media-library-content { + padding: 1em; +} + +/* Remove margin at the end of the form */ +.js-media-library-view .views-form { + margin-bottom: calc(var(--form-spacing) * -1); +} + +.js-media-library-view .views-row { + position: relative; + margin-block: var(--form-item-spacing) !important; /* stylelint-disable-line declaration-no-important */ +} + +.js-media-library-view .views-row .fr-checkbox-group { + position: absolute; + margin-top: .5rem; + margin-left: .5rem; +} + +.js-media-library-add-form-added-media { + --ul-type: none; +} + +.js-media-library-add-form-added-media li { + padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing); + border: 1px solid var(--border-default-grey); +} + +.js-media-library-add-form-added-media img { + display: block; + margin: 0 auto; + max-width: 100%; + height: auto; +} + +.js-media-library-add-form-added-media .form-item-name label { + display: inline-block; + font-weight: 700; +} + +.media-library-select-all input[type="checkbox"] { + display: inline-block; + vertical-align: bottom; + width: 1.5rem; + height: 1.5rem; + margin-right: .5rem; +} + +.media-library-item__edit, +.media-library-item__remove { + position: absolute; + z-index: 1; + top: 1.25rem; +} + +.media-library-item__edit { + right: 3.5rem; +} + +.media-library-item__remove { + right: 1.25rem; +} diff --git a/css/navigation.header.css b/css/theme/navigation.header.css similarity index 100% rename from css/navigation.header.css rename to css/theme/navigation.header.css diff --git a/css/pager.css b/css/theme/pager.css similarity index 84% rename from css/pager.css rename to css/theme/pager.css index a1aa43f03c3098f19e624e5de7c049f8254e94e1..ca2627ca87bc99e4e5d6ad8c58cc9c4d670f3848 100644 --- a/css/pager.css +++ b/css/theme/pager.css @@ -1,6 +1,6 @@ /** * @file - * Fix pagination to allow AJAX use. + * Fix styles for pagination to allow AJAX use. */ .fr-pagination__link[href=""], diff --git a/css/theme/select2.css b/css/theme/select2.css new file mode 100644 index 0000000000000000000000000000000000000000..755286317c8b74c000c5d90e4218bfa919a1ca0e --- /dev/null +++ b/css/theme/select2.css @@ -0,0 +1,165 @@ +/** + * @file + * Manage styles for Select2 widget. + */ + +/* Duplicate "select" component styles - START */ +.select2-container .selection { + display: block; + appearance: none; + width: 100%; + border-radius: 0.25rem 0.25rem 0 0; + font-size: 1rem; + line-height: 1.5rem; + padding: 0.5rem 2.5rem 0.5rem 1rem; + background-repeat: no-repeat; + background-position: calc(100% - 1rem) 50%; + background-size: 1rem 1rem; + color: var(--text-default-grey); + background-color: var(--background-contrast-grey); + + --idle: transparent; + --hover: var(--background-contrast-grey-hover); + --active: var(--background-contrast-grey-active); + + box-shadow: inset 0 -2px 0 0 var(--border-plain-grey); + + --data-uri-svg: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 24 24' ><path fill='%23161616' d='M12,13.1l5-4.9l1.4,1.4L12,15.9L5.6,9.5l1.4-1.4L12,13.1z'/></svg>"); + + background-image: var(--data-uri-svg); +} + +.fr-fieldset--error .select2-container, +.fr-select-group--error .select2-container { + box-shadow: inset 0 -2px 0 0 var(--border-plain-error); +} + +.select2-container:-webkit-autofill, +.select2-container:-webkit-autofill:hover, +.select2-container:-webkit-autofill:focus { + box-shadow: inset 0 -2px 0 0 var(--border-plain-grey), inset 0 0 0 1000px var(--background-contrast-blue-france); + -webkit-text-fill-color: var(--text-label-grey); +} + +.fr-select:disabled + .select2-container { + --data-uri-svg: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 24 24' ><path fill='%23929292' d='M12,13.1l5-4.9l1.4,1.4L12,15.9L5.6,9.5l1.4-1.4L12,13.1z'/></svg>"); + + color: var(--text-disabled-grey); + box-shadow: inset 0 -2px 0 0 var(--border-disabled-grey); + background-image: var(--data-uri-svg); +} + +:root[data-fr-theme="dark"] .select2-container { + --data-uri-svg: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 24 24' ><path fill='%23fff' d='M12,13.1l5-4.9l1.4,1.4L12,15.9L5.6,9.5l1.4-1.4L12,13.1z'/></svg>"); +} + +:root[data-fr-theme="dark"] .fr-select:disabled + .select2-container { + --data-uri-svg: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 24 24' ><path fill='%23666' d='M12,13.1l5-4.9l1.4,1.4L12,15.9L5.6,9.5l1.4-1.4L12,13.1z'/></svg>"); + + cursor: not-allowed; +} + +@media (-ms-high-contrast: active), (forced-colors: active) { + .select2-container { + background-image: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 24 24' fill='canvastext'><path d='M12,13.1l5-4.9l1.4,1.4L12,15.9L5.6,9.5l1.4-1.4L12,13.1z'/></svg>"); + } +} +/* Duplicate "select" component styles - END */ + +.select2-container .select2-selection--multiple { + background-color: transparent; + padding: 0; + display: block; + min-height: auto; +} + +.select2-container .select2-selection--multiple, +.select2-container.select2-container--focus .select2-selection--multiple { + border: 0; +} + +.select2-container .select2-search { + display: inline-block; +} + +.select2-container .select2-search--inline .select2-search__field { + margin: 0; + font-size: inherit; + font-family: Marianne, arial, sans-serif; + height: 24px; +} + +.select2-container .select2-search--inline .select2-search__field::placeholder { + color: var(--text-default-grey); +} + +.select2-container .select2-selection--single .select2-selection__placeholder { + color: var(--text-mention-grey); +} + +.select2-dropdown { + margin-top: 2px; + padding: .25rem; + background-color: var(--background-contrast-grey); +} + +.select2-container--open .select2-dropdown--below { + border: 1px solid var(--border-plain-grey); +} + +.select2-container--default .select2-results__option--highlighted.select2-results__option--selectable { + background-color: var(--background-contrast-grey-hover); + color: var(--text-default-grey); +} + +.select2-selection__rendered .select2-selection__choice { + margin-right: .5rem; +} + +.select2-container--default .select2-selection--multiple .select2-selection__choice { + position: relative; + display: inline-flex; + background-color: var(--background-action-high-blue-france); + color: var(--text-inverted-blue-france); + border-radius: 1rem; + font-size: .875rem; + padding: .25rem .75rem; + border: 0; +} + +.select2-container--default .select2-selection--multiple .select2-selection__choice:hover { + background-color: var(--background-action-high-blue-france-hover); +} + +.select2-container--default .select2-selection--multiple .select2-selection__choice__remove { + position: initial; + left: auto; + top: auto; + display: flex; + order: 2; + padding: 0 .25rem; + border: 0; + font-size: 1.5em; + line-height: 1; + color: var(--text-inverted-blue-france); +} + +.select2-container--default .select2-selection--multiple .select2-selection__choice__remove::before { + content: ""; + position: absolute; + display: block; + inset: 0; + width: 100%; + height: 100%; + z-index: 1; +} + +.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover, +.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:focus { + background-color: transparent; + color: inherit; +} + +.select2-container--default .select2-selection--multiple .select2-selection__choice__display { + display: flex; +} diff --git a/css/theme/system.admin.css b/css/theme/system.admin.css new file mode 100644 index 0000000000000000000000000000000000000000..510c76b10ea5b6dda4851aba28f127efc0d0c87e --- /dev/null +++ b/css/theme/system.admin.css @@ -0,0 +1,23 @@ +/** + * @file + * Manage styles for system admin. + */ + +.compact-link { + text-align: right; +} + +.panel { + padding: 1rem 1rem .5rem; + border: 1px solid var(--border-default-grey); +} + +.panel + .panel { + margin-top: 1.5rem; +} + +.list-group dd + dt { + margin-top: .25rem; + padding-top: .5rem; + border-top: 1px solid var(--border-default-grey); +} diff --git a/css/tile.css b/css/theme/tile.css similarity index 59% rename from css/tile.css rename to css/theme/tile.css index 494eca32ed3294b76d8b877683fe21f81f9ab636..13c9ba4caf92a9812499af164a0fcc6806c35842 100644 --- a/css/tile.css +++ b/css/theme/tile.css @@ -1,6 +1,6 @@ /** * @file - * Fix card styling with "time" html tag. + * Fix styles for tile with "time" html tag. */ .fr-tile__detail time { diff --git a/css/tooltip.css b/css/theme/tooltip.css similarity index 89% rename from css/tooltip.css rename to css/theme/tooltip.css index c7fac6daea5227412d92ed16bfe20c73ee6fa4f2..9d75cb1a6eb3847ca016e8e706ca06651697ea07 100644 --- a/css/tooltip.css +++ b/css/theme/tooltip.css @@ -1,6 +1,6 @@ /** * @file - * Fix tooltip styling. + * Fix styles for tooltip. */ .fr-btn--tooltip { diff --git a/css/theme/ui-dialog.css b/css/theme/ui-dialog.css new file mode 100644 index 0000000000000000000000000000000000000000..a682040fc621e172419abdecf02bd0ea20e367c1 --- /dev/null +++ b/css/theme/ui-dialog.css @@ -0,0 +1,95 @@ +/** + * @file + * Manage styles for UI dialog. + * Try as much as possible to reproduce the styles of the DSFR modal. + */ + +.ui-dialog { + padding: 0; + max-height: 80vh !important; /* stylelint-disable-line declaration-no-important */ + filter: drop-shadow(var(--lifted-shadow)); +} + +.ui-dialog .ui-dialog-titlebar { + margin: 0; + padding: 4rem 2rem 0; +} + +.ui-dialog .ui-dialog-title { + --title-spacing: 0 0 1rem 0; + + margin: var(--title-spacing); +} + +.ui-dialog .ui-dialog-content { + margin-bottom: 4rem; + padding: 0 2rem; +} + +.ui-widget-content { + background: var(--background-default-grey); + color: var(--text-default-grey); +} + +.ui-widget.ui-widget-content { + border: 0; +} + +.ui-widget-header { + background: none; + border: 0; + color: var(--text-title-grey); + font-size: 1.375rem; + font-weight: 700; +} + +@media (width >= 48em) { + .ui-widget-header { + font-size: 1.5rem; + line-height: 2rem; + } +} + +.ui-dialog .ui-dialog-titlebar-close { + top: 1rem; + right: 2rem; + margin: 0; + padding: .25rem .75rem; + width: auto; + height: auto; + min-height: 2rem; + background: none; + border: 0; + text-align: right; + text-indent: initial; + color: var(--text-action-high-blue-france); + font-size: .875rem; + font-weight: 500; + line-height: 1.5rem; + overflow: initial; +} + +.ui-dialog .ui-dialog-titlebar-close:hover { + background-color: var(--hover-tint); +} + +.ui-dialog .ui-dialog-titlebar-close::after { + --icon-size: 1rem; + + background-color: currentcolor; + content: ''; + display: inline-block; + flex: 0 0 auto; + height: var(--icon-size); + margin-left: .5rem; + margin-right: -.125rem; + mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0ibTEyIDEwLjYgNC45NS00Ljk2IDEuNCAxLjRMMTMuNDIgMTJsNC45NiA0Ljk1LTEuNCAxLjRMMTIgMTMuNDJsLTQuOTUgNC45Ni0xLjQtMS40TDEwLjU4IDEyIDUuNjMgNy4wNWwxLjQtMS40eiIvPjwvc3ZnPg=="); + mask-size: 100% 100%; + vertical-align: calc((.75em - var(--icon-size)) * .5); + width: var(--icon-size); +} + +.ui-dialog .ui-dialog-titlebar-close .ui-button-icon, +.ui-dialog .ui-dialog-titlebar-close .ui-button-icon-space { + display: none; +} diff --git a/css/theme/vertical-tabs.css b/css/theme/vertical-tabs.css new file mode 100644 index 0000000000000000000000000000000000000000..bf97ad7561d4e0de151e5a74dfec78b9aca49a6a --- /dev/null +++ b/css/theme/vertical-tabs.css @@ -0,0 +1,51 @@ +/** + * @file + * Manage styles for vertical tabs. + */ + +.vertical-tabs__menu [href] { + background-image: none; +} + +.vertical-tabs__menu li { + --li-bottom: 0; +} + +.vertical-tabs__menu-item a:hover { + outline: none; +} + +.vertical-tabs__menu-item.is-selected a { + background-color: var(--background-default-grey); + background-image: linear-gradient(0deg,var(--border-active-blue-france),var(--border-active-blue-france)),linear-gradient(0deg,var(--border-default-grey),var(--border-default-grey)),linear-gradient(0deg,var(--border-default-grey),var(--border-default-grey)),linear-gradient(0deg,var(--border-default-grey),var(--border-default-grey)); + background-size: 100% 2px,1px calc(100% - 1px),1px calc(100% - 1px),0 1px; + color: var(--text-active-blue-france); +} + +.vertical-tabs__menu-item:not(.is-selected) a { + background-color: var(--background-action-low-blue-france); +} + +.vertical-tabs__menu-item:not(.is-selected) a:hover { + background-color: var(--background-action-low-blue-france-hover); +} + +.vertical-tabs__menu-item.is-selected .vertical-tabs__menu-item-title { + color: inherit; +} + +.vertical-tabs__menu-item a:focus .vertical-tabs__menu-item-title, +.vertical-tabs__menu-item a:active .vertical-tabs__menu-item-title, +.vertical-tabs__menu-item a:hover .vertical-tabs__menu-item-title { + text-decoration: none; +} + +.vertical-tabs__panes { + padding: var(--form-spacing-xs) var(--form-spacing) var(--form-spacing-s); +} + +.vertical-tabs__pane .fr-accordion__btn, +.vertical-tabs__pane .fr-input, +.vertical-tabs__pane .fr-select { + box-sizing: border-box; +} diff --git a/dsfr4drupal.info.yml b/dsfr4drupal.info.yml index 17068cbc63c869ae29a26234185913350226d0f7..caf13fbad435a7b3d6a223205658ce630dd77ed0 100644 --- a/dsfr4drupal.info.yml +++ b/dsfr4drupal.info.yml @@ -33,21 +33,49 @@ ckeditor5-stylesheets: - css/ckeditor5.css libraries: + - dsfr4drupal/core - dsfr4drupal/utility libraries-extend: + core/drupal.dialog: + - dsfr4drupal/drupal.dialog + core/drupal.dropbutton: + - dsfr4drupal/drupal.dropbutton core/drupal.message: - dsfr4drupal/drupal.message + core/drupal.tabledrag: + - dsfr4drupal/drupal.tabledrag + core/drupal.vertical-tabs: + - dsfr4drupal/drupal.vertical-tabs + field_group/element.horizontal_tabs: + - dsfr4drupal/element.horizontal_tabs + layout_builder/drupal.layout_builder: + - dsfr4drupal/drupal.layout_builder + media_library/view: + - dsfr4drupal/media_library.theme + - dsfr4drupal/media_library.view + media_library/widget: + - dsfr4drupal/media_library.theme navigation/navigation.layout: - dsfr4drupal/navigation.layout node/drupal.node.preview: - dsfr4drupal/drupal.node.preview + paragraphs/drupal.paragraphs.admin: + - dsfr4drupal/drupal.paragraphs.admin + paragraphs/drupal.paragraphs.widget: + - dsfr4drupal/drupal.paragraphs.widget + select2/select2: + - dsfr4drupal/select2 + system/admin: + - dsfr4drupal/admin tarte_au_citron/tarte_au_citron_lib: - dsfr4drupal/tarteaucitron tacjs/tarteaucitron.js: - dsfr4drupal/tarteaucitron toolbar/toolbar: - dsfr4drupal/toolbar + views/views.module: + - dsfr4drupal/views libraries-override: tacjs/tarteaucitron.js: diff --git a/dsfr4drupal.libraries.yml b/dsfr4drupal.libraries.yml index 99244fcd9ffbe0874e8a0c089ae64e694b866a2b..57f6416718aa05c67f6e145dd4435cca72dd74f3 100644 --- a/dsfr4drupal.libraries.yml +++ b/dsfr4drupal.libraries.yml @@ -2,11 +2,19 @@ # These Javascript files are needed for old/outdated browsers. # @see: https://www.systeme-de-design.gouv.fr/utilisation-et-organisation/developpeurs/prise-en-main/ +admin: + css: + theme: + css/theme/system.admin.css: {} + component.accordion: css: component: /libraries/dsfr/dist/component/accordion/accordion.min.css: { minified: true } + js: + js/accordion.js: {} dependencies: + - core/once - dsfr4drupal/core component.alert: @@ -62,7 +70,7 @@ component.card: component: /libraries/dsfr/dist/component/card/card.min.css: { minified: true } theme: - css/card.css: {} + css/theme/card.css: {} dependencies: - dsfr4drupal/component.button - dsfr4drupal/core @@ -71,6 +79,7 @@ component.checkbox: css: component: /libraries/dsfr/dist/component/checkbox/checkbox.min.css: { minified: true } + css/component/checkbox.css: {} dependencies: - dsfr4drupal/component.form - dsfr4drupal/core @@ -111,7 +120,9 @@ component.display: component.display.button: css: theme: - css/display.button.css: {} + css/theme/display.button.css: {} + dependencies: + - dsfr4drupal/core component.connect: css: @@ -139,6 +150,7 @@ component.form: css: component: /libraries/dsfr/dist/component/form/form.min.css: { minified: true } + css/component/form.css: {} dependencies: - dsfr4drupal/core @@ -242,7 +254,7 @@ component.pagination: component: /libraries/dsfr/dist/component/pagination/pagination.min.css: { minified: true } theme: - css/pager.css: {} + css/theme/pager.css: {} dependencies: - dsfr4drupal/core @@ -301,6 +313,7 @@ component.radio: css: component: /libraries/dsfr/dist/component/radio/radio.min.css: { minified: true } + css/component/radio.css: {} dependencies: - dsfr4drupal/component.form - dsfr4drupal/core @@ -397,6 +410,7 @@ component.table: css: component: /libraries/dsfr/dist/component/table/table.min.css: { minified: true } + css/component/table.css: {} js: /libraries/dsfr/dist/component/table/table.module.min.js: minified: true @@ -425,7 +439,7 @@ component.tile: component: /libraries/dsfr/dist/component/tile/tile.min.css: { minified: true } theme: - css/tile.css: {} + css/theme/tile.css: {} dependencies: - dsfr4drupal/core @@ -452,7 +466,7 @@ component.tooltip: component: /libraries/dsfr/dist/component/tooltip/tooltip.min.css: { minified: true } theme: - css/tooltip.css: {} + css/theme/tooltip.css: {} js: /libraries/dsfr/dist/component/tooltip/tooltip.module.min.js: minified: true @@ -523,7 +537,7 @@ core: css: base: /libraries/dsfr/dist/core/core.min.css: { minified: true } - css/core.css: {} + css/base/core.css: {} js: js/core.js: {} /libraries/dsfr/dist/core/core.module.min.js: @@ -548,6 +562,27 @@ core: verbose: true level: info +drupal.dialog: + css: + theme: + # Need to fix weight to 99 to override "Gin toolbar" styles. + css/theme/ui-dialog.css: { weight: 100 } + +drupal.dropbutton: + css: + component: + css/component/dropbutton.css: {} + js: + js/dropbutton.js: {} + dependencies: + - core/drupal + - core/once + +drupal.layout_builder: + css: + theme: + css/theme/layout-builder.css: {} + drupal.message: js: js/messages.js: {} @@ -555,9 +590,55 @@ drupal.message: - dsfr4drupal/component.alert drupal.node.preview: + css: + component: + css/component/node-preview.css: {} + +drupal.paragraphs.admin: + css: + component: + css/component/paragraphs.admin.css: { } + +drupal.paragraphs.widget: + css: + component: + css/component/paragraphs.widget.css: { } + js: + js/paragraphs.widget.js: {} + dependencies: + - core/drupal + - core/once + - dsfr4drupal/component.tab + +drupal.tabledrag: + css: + component: + css/component/tabledrag.css: {} + js: + js/tabledrag.js: {} + dependencies: + - core/drupal + - core/jquery + +drupal.vertical-tabs: + css: + theme: + css/theme/vertical-tabs.css: {} + +element.horizontal_tabs: css: theme: - css/drupal.node.preview.css: {} + css/theme/horizontal-tabs.css: {} + js: + js/horizontal-tabs.js: {} + dependencies: + - core/once + - dsfr4drupal/component.tab + +local-actions: + css: + component: + css/component/action-links.css: {} #legacy: # js: @@ -568,10 +649,22 @@ drupal.node.preview: # # Move DSFR modules to first load to improve Javascript file aggregation. # weight: -50 +media_library.theme: + css: + theme: + css/theme/media-library.css: {} + +media_library.view: + js: + js/media-library.view.js: {} + dependencies: + - core/drupal + - core/once + navigation.layout: css: theme: - css/navigation.header.css: {} + css/theme/navigation.header.css: {} scheme: css: @@ -587,6 +680,11 @@ scheme: dependencies: - dsfr4drupal/core +select2: + css: + theme: + css/theme/select2.css: {} + tarteaucitron: css: theme: @@ -594,8 +692,8 @@ tarteaucitron: toolbar: css: - theme: - css/toolbar.css: {} + component: + css/component/toolbar.css: {} utility: css: @@ -608,3 +706,10 @@ utility.icons: css: base: /libraries/dsfr/dist/utility/icons/icons.min.css: { minified: true } + +views: + css: + component: + css/component/views-exposed-form.css: {} + dependencies: + - dsfr4drupal/drupal.form diff --git a/dsfr4drupal.theme b/dsfr4drupal.theme index 657df94570f61247cf4f375696e23030214f3fb4..0a9999418b3469a9250812a2fe79e9b620bf0906 100644 --- a/dsfr4drupal.theme +++ b/dsfr4drupal.theme @@ -10,6 +10,7 @@ declare(strict_types=1); use Drupal\Core\Asset\AttachedAssetsInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\dsfr4drupal\Dsfr4DrupalInterface; +use Drupal\dsfr4drupal\Dsfr4DrupalPreRender; // Include all files from the includes directory. $includes_path = dirname(__FILE__) . '/includes/*.theme'; @@ -17,6 +18,18 @@ foreach (glob($includes_path) as $file) { require_once dirname(__FILE__) . '/includes/' . basename($file); } +/** + * Implements hook_element_info_alter(). + */ +function dsfr4drupal_element_info_alter(array &$type): void { + if (isset($type['horizontal_tabs'])) { + $type['horizontal_tabs']['#pre_render'][] = [Dsfr4DrupalPreRender::class, 'horizontalTabs']; + } + if (isset($type['vertical_tabs'])) { + $type['vertical_tabs']['#pre_render'][] = [Dsfr4DrupalPreRender::class, 'verticalTabs']; + } +} + /** * Implements hook_preprocess(). */ diff --git a/includes/field.theme b/includes/field.theme index a9988cd234d4a814b95e78926d19c17d789e8ccb..f1dec96f1b43ea73f8f8bf913328c25881ece6b3 100644 --- a/includes/field.theme +++ b/includes/field.theme @@ -32,3 +32,15 @@ function dsfr4drupal_preprocess_field(array &$variables): void { } } } + +/** + * Implements hook_preprocess_HOOK() for "field__media__thumbnail". + */ +function dsfr4drupal_preprocess_field__media__thumbnail(array &$variables): void { + $items = &$variables['items']; + + // Add "fr-responsive-img" by default. + foreach ($items as &$item) { + $item['content']['#item_attributes']['class'][] = 'fr-responsive-img'; + } +} diff --git a/includes/form.theme b/includes/form.theme index 1e613965d5c01cd1a4471a3c017f87549cde09d8..7f73ee61b30edd20023494c2af8f429d772bf221 100644 --- a/includes/form.theme +++ b/includes/form.theme @@ -34,6 +34,29 @@ function dsfr4drupal_form_node_preview_form_select_alter(array &$form, FormState $form['#attached']['library'][] = 'dsfr4drupal/component.link'; } +/** + * Implements hook_form_FORM_ID_alter() for "views_exposed_form". + */ +function dsfr4drupal_form_views_exposed_form_alter(array &$form, FormStateInterface $form_state, string $form_id): void { + /** @var \Drupal\views\ViewExecutable $view */ + $view = $form_state->getStorage()['view']; + + $view_ids = [ + // Stylize media library widget modal. + 'media_library', + ]; + + if ( + // Stylize views exposed form on admin pages. + \Drupal::service('router.admin_context')->isAdminRoute() || + // Specific rule for some views. + in_array($view->id(), $view_ids) + ) { + $form['#attributes']['class'][] = 'views-exposed-form--inline'; + $form['#attributes']['class'][] = 'dsfr4drupal-form--bordered'; + } +} + /** * Implements hook_preprocess_HOOK() for "fieldset". */ @@ -52,6 +75,9 @@ function dsfr4drupal_preprocess_fieldset(array &$variables): void { $variables['attributes']['aria-labelledby'] .= ' ' . $variables['error_id']; } + // Show help text as tooltip. + $variables['description_tooltip'] = theme_get_setting('form_description_tooltip') ?? FALSE; + $variables['#attached']['library'][] = 'dsfr4drupal/component.form'; } @@ -70,6 +96,29 @@ function dsfr4drupal_preprocess_form(array &$variables): void { } } +/** + * Implements hook_preprocess_HOOK() for "views_exposed_form". + */ +function dsfr4drupal_preprocess_views_exposed_form(array &$variables): void { + $form = &$variables['form']; + + // Add BEM classes for items in the form. + // Sorted keys. + foreach (Element::children($form, TRUE) as $child_key) { + if (!empty($form[$child_key]['#type'])) { + if ($form[$child_key]['#type'] === 'actions') { + // We need the key of the element that precedes the actions' element. + $form[$child_key]['#attributes']['class'][] = 'views-exposed-form__item'; + $form[$child_key]['#attributes']['class'][] = 'views-exposed-form__item--actions'; + } + + if (!in_array($form[$child_key]['#type'], ['hidden', 'actions'])) { + $form[$child_key]['#wrapper_attributes']['class'][] = 'views-exposed-form__item'; + } + } + } +} + /** * Implements hook_preprocess_HOOK() for "webform". */ @@ -134,7 +183,7 @@ function dsfr4drupal_preprocess_form_element(array &$variables): void { */ function dsfr4drupal_preprocess_form_element_label(array &$variables): void { // We need to have description in form element label template. - $variables['description'] = $variables['element']['#description']; + $variables['description'] = $variables['element']['#description'] ?? ''; // Show help text as tooltip. $variables['description_tooltip'] = theme_get_setting('form_description_tooltip') ?? FALSE; @@ -265,6 +314,23 @@ function dsfr4drupal_preprocess_textarea(array &$variables): void { } } +/** + * Implements hook_theme_suggestions_HOOK_alter() for "form_element". + */ +function dsfr4drupal_theme_suggestions_form_element_alter(array &$suggestions, array $variables): void { + if (empty($suggestions)) { + $suggestions[] = 'form_element'; + } + + // Set suggestions by form element type. + $new_suggestions = []; + foreach ($suggestions as $suggestion) { + $new_suggestions[] = $suggestion; + $new_suggestions[] = $suggestion . '__' . $variables['element']['#type']; + } + $suggestions = $new_suggestions; +} + /** * Implements hook_theme_suggestions_HOOK_alter() for "input". */ diff --git a/includes/menu.theme b/includes/menu.theme index 36748b1581a706cf530b67b2aba063f1a9b22e0b..08f0483fc07225f361280956fe86d0c809db1232 100644 --- a/includes/menu.theme +++ b/includes/menu.theme @@ -26,3 +26,13 @@ function dsfr4drupal_preprocess_menu(array &$variables): void { // Define menu "aria-label". $variables['aria_label'] = $menu ? $menu->label() : ''; } + +/** + * Implements hook_preprocess_HOOK() for "menu". + */ +function dsfr4drupal_preprocess_menu_local_action(array &$variables): void { + $link = &$variables['link']; + + $link['#options']['attributes']['class'][] = 'fr-btn'; + $link['#attached']['library'][] = 'dsfr4drupal/component.button'; +} diff --git a/includes/system.theme b/includes/system.theme new file mode 100644 index 0000000000000000000000000000000000000000..7b3bf794ecc5f2011669470004c1530c42b666f4 --- /dev/null +++ b/includes/system.theme @@ -0,0 +1,37 @@ +<?php + +/** + * @file + * Functions to support system theming in the "DSFR for Drupal" theme. + */ + +declare(strict_types=1); + +/** + * Implements hook_theme_suggestions_HOOK_alter() for details. + */ +function dsfr4drupal_theme_suggestions_details_alter(array &$suggestions, array $variables): void { + /** + * The property "#horizontal_tab_item" is added during element prerender. + * @see: \Drupal\dsfr4drupal\Dsfr4DrupalPrerender::horizontalTabs() + */ + if (!empty($variables['element']['#horizontal_tab_item'])) { + $suggestions[] = 'details__horizontal_tabs'; + } + + /** + * The property "#vertical_tab_item" is added during element prerender. + * @see: \Drupal\dsfr4drupal\Dsfr4DrupalPrerender::verticalTabs() + */ + if (!empty($variables['element']['#vertical_tab_item'])) { + $suggestions[] = 'details__vertical_tabs'; + } + + /** + * Styles are reset with Drupal dialog off canvas. + * Use default details rendering instead of DSFR accordion component. + */ + if (\Drupal::request()->query->get('_wrapper_format') === 'drupal_dialog.off_canvas') { + $suggestions[] = 'details__dialog_off_canvas'; + } +} diff --git a/includes/views.theme b/includes/views.theme new file mode 100644 index 0000000000000000000000000000000000000000..cd4484da4defd2c6cc80dfe8553244994581b6a3 --- /dev/null +++ b/includes/views.theme @@ -0,0 +1,66 @@ +<?php + +/** + * @file + * Functions to support views theming in the "DSFR for Drupal" theme. + */ + +declare(strict_types=1); +use Drupal\views\ViewExecutable; + +/** + * Implements hook_preprocss_hook() for "views_view__media_library". + */ +function dsfr4drupal_preprocess_views_view__media_library(array &$variables): void { + if (empty($variables['header'])) { + return; + } + + foreach ($variables['header'] as &$header) { + $options = &$header['#options']; + + // Add tab component attributes. + $options['attributes']['aria-selected'] = 'false'; + $options['attributes']['class'][] = 'fr-tabs__tab'; + $options['attributes']['role'] = 'tab'; + + // Set tab active. + if ($options['view']->current_display === $options['target_display_id']) { + $options['attributes']['aria-selected'] = 'true'; + $options['attributes']['class'][] = 'fr-tabs__tab--selected'; + } + } + + $variables['#attached']['library'][] = 'dsfr4drupal/component.tab'; + $variables['#attached']['library'][] = 'dsfr4drupal/component.link'; +} + +/** + * Implements hook_views_pre_render(). + */ +function dsfr4drupal_views_pre_render(ViewExecutable $view) { + if ( + $view->id() === 'media_library' && + $view->current_display === 'page' + ) { + $add_classes = function (&$option, array $classes_to_add) { + $classes = preg_split('/\s+/', $option); + $classes = array_filter($classes); + $classes = array_merge($classes, $classes_to_add); + $option = implode(' ', array_unique($classes)); + }; + + if (array_key_exists('edit_media', $view->field)) { + $add_classes( + $view->field['edit_media']->options['alter']['link_class'], + ['media-library-item__edit', 'fr-btn', 'fr-btn--secondary', 'fr-btn--sm', 'fr-icon-ball-pen-fill'] + ); + } + if (array_key_exists('delete_media', $view->field)) { + $add_classes( + $view->field['delete_media']->options['alter']['link_class'], + ['media-library-item__remove', 'fr-btn', 'fr-btn--secondary', 'fr-btn--sm', 'fr-icon-close-line'] + ); + } + } +} diff --git a/js/accordion.js b/js/accordion.js new file mode 100644 index 0000000000000000000000000000000000000000..e2202d9d5fdd4eabfe4bf3d9bc4701313afa30e5 --- /dev/null +++ b/js/accordion.js @@ -0,0 +1,23 @@ +/** + * @file + * Manage accordion features with DSFR. + */ + +((Drupal, once) => { + "use strict"; + + Drupal.behaviors.dsfrAccordion = { + attach: (context) => { + + // Search all accordion in current context. + once("dsfr-accordion", ".fr-accordion", context).forEach((element) => { + // Manage accordion button click event. + element.querySelector(".fr-accordion__btn").addEventListener("click", event => { + // Disable form submission. + event.preventDefault(); + }); + }); + } + }; + +})(Drupal, once); diff --git a/js/dropbutton.js b/js/dropbutton.js new file mode 100644 index 0000000000000000000000000000000000000000..403cdddd9f66a56ef662a62bd41a05e53f10b17d --- /dev/null +++ b/js/dropbutton.js @@ -0,0 +1,76 @@ +/** + * @file + * Manage "dropbutton" behaviors. + */ + +((Drupal, once) => { + Drupal.behaviors.dsfr4drupalDropbutton = { + attach: function (context) { + once("dsfr4drupal-dropbutton", ".dropbutton-multiple:has(.dropbutton--dsfr4drupal)", context).forEach(element => { + element.querySelector(".dropbutton-toggle").addEventListener("click", () => { + this.updatePosition(element); + + window.addEventListener("scroll", () => Drupal.debounce(this.updatePositionIfOpen(element), 100)); + window.addEventListener("resize", () => Drupal.debounce(this.updatePositionIfOpen(element), 100)); + }); + }); + }, + + updatePosition: (element) => { + const preferredDir = document.documentElement.dir ?? "ltr"; + const secondaryAction = element.querySelector(".secondary-action"); + const dropMenu = secondaryAction.querySelector(".dropbutton__items"); + const toggleHeight = element.offsetHeight; + const dropMenuWidth = dropMenu.offsetWidth; + const dropMenuHeight = dropMenu.offsetHeight; + const boundingRect = secondaryAction.getBoundingClientRect(); + const spaceBelow = window.innerHeight - boundingRect.bottom; + const spaceLeft = boundingRect.left; + const spaceRight = window.innerWidth - boundingRect.right; + + // Calculate the menu position based on available space and the preferred + // reading direction. + const leftAlignStyles = { + left: `${boundingRect.left}px`, + right: "auto" + }; + const rightAlignStyles = { + left: "auto", + right: `${window.innerWidth - boundingRect.right}px` + }; + + if (preferredDir === "ltr") { + if (spaceRight >= dropMenuWidth) { + Object.assign(dropMenu.style, leftAlignStyles); + } + else { + Object.assign(dropMenu.style, rightAlignStyles); + } + } + else { + if (spaceLeft >= dropMenuWidth) { + Object.assign(dropMenu.style, rightAlignStyles); + } + else { + Object.assign(dropMenu.style, leftAlignStyles); + } + } + + if (spaceBelow >= dropMenuHeight) { + dropMenu.style.top = `${boundingRect.bottom}px`; + } + else { + dropMenu.style.top = `${boundingRect.top - toggleHeight - dropMenuHeight}px` + } + + }, + + updatePositionIfOpen: (element) => { + if (element.classList.contains("open")) { + this.updatePosition(element); + } + }, + + }; + +})(Drupal, once); diff --git a/js/horizontal-tabs.js b/js/horizontal-tabs.js new file mode 100644 index 0000000000000000000000000000000000000000..030a42db3099275c3f37b9f4d15492178965d8f8 --- /dev/null +++ b/js/horizontal-tabs.js @@ -0,0 +1,51 @@ +/** + * @file + * Manage horizontal tabs features with DSFR. + */ + +((Drupal, once) => { + "use strict"; + + const CLASS_LIST = "fr-tabs__list"; + const CLASS_TAB = "fr-tabs__tab"; + const CLASS_TAB_SELECTED = "fr-tabs__tab--selected"; + const CLASS_TABS = "fr-tabs"; + + Drupal.behaviors.dsfrHorizontalTabs = { + attach: (context) => { + + // Search all horizontal tabs in current context. + once("dsfr-horizontal-tabs", ".horizontal-tabs", context).forEach((element) => { + const wrapper = element.querySelector(`.${CLASS_TABS}`); + const list = element.querySelector(`.horizontal-tabs-list.${CLASS_LIST}`); + if (!wrapper || !list) { + return; + } + + wrapper.append(list); + + list.querySelectorAll("li").forEach((item) => { + const link = item.querySelector("a"); + link.classList.add(CLASS_TAB); + + if (item.classList.contains("selected")) { + link.classList.add(CLASS_TAB_SELECTED); + link.setAttribute("aria-selected", "true"); + } + + link.addEventListener("click", () => { + // Remove selected class on old active link. + const selected = list.querySelector(`.${CLASS_TAB_SELECTED}`); + selected.classList.remove(CLASS_TAB_SELECTED); + selected.removeAttribute("aria-selected"); + + // Add class to new element. + link.classList.add(CLASS_TAB_SELECTED); + link.setAttribute("aria-selected", "true"); + }) + }); + }); + } + }; + +})(Drupal, once); diff --git a/js/media-library.view.js b/js/media-library.view.js new file mode 100644 index 0000000000000000000000000000000000000000..e360d7d1cae0b06211e95f2937ea2bece26ae6bf --- /dev/null +++ b/js/media-library.view.js @@ -0,0 +1,24 @@ +/** + * @file + * Manage media library view behaviors. + */ + +((Drupal, once) => { + "use strict"; + + + Drupal.behaviors.dsfrMediaLibraryView = { + attach: (context) => { + const selectAll = once("dsfr-media-library-select-all", ".media-library-select-all", context); + if (!selectAll.length) { + return; + } + + selectAll.forEach(element => { + // Move "select all" element after header. + context.querySelector("#edit-header").after(element); + }); + }, + }; + +})(Drupal, once); diff --git a/js/paragraphs.widget.js b/js/paragraphs.widget.js new file mode 100644 index 0000000000000000000000000000000000000000..d9bb779a51db215d6f5be83f132b31a1902f0ed9 --- /dev/null +++ b/js/paragraphs.widget.js @@ -0,0 +1,60 @@ +/** + * @file + * Manage paragraphs widget rendering with DSFR styles. + */ + +(function (Drupal, once) { + "use strict"; + + const CLASS_LIST = "fr-tabs__list"; + const CLASS_TAB = "fr-tabs__tab"; + const CLASS_TAB_SELECTED = "fr-tabs__tab--selected"; + const CLASS_TABS = "fr-tabs"; + + Drupal.behaviors.dsfrParagraphsWidget = { + attach: (context) => { + const wrappers = once("dsfr-paragraphs-tabs-wrapper", ".paragraphs-tabs-wrapper", context); + if (!wrappers) { + return; + } + + wrappers.forEach(wrapper => { + const list = wrapper.querySelector(".paragraphs-tabs"); + if ( + !list || + list.classList.contains("paragraphs-tabs-hide") + ) { + return; + } + + const listWrapper = document.createElement("div"); + listWrapper.classList.add(CLASS_TABS); + wrapper.prepend(listWrapper); + + listWrapper.appendChild(list); + list.classList.add(CLASS_LIST); + + list.querySelectorAll("li > a").forEach(link => { + link.classList.add(CLASS_TAB); + + if (link.classList.contains("is-active")) { + link.classList.add(CLASS_TAB_SELECTED); + link.setAttribute("aria-selected", "true"); + } + + link.addEventListener("click", () => { + // Remove selected class on old active link. + const selected = list.querySelector(`.${CLASS_TAB_SELECTED}`); + selected.classList.remove(CLASS_TAB_SELECTED); + selected.removeAttribute("aria-selected"); + + // Add class to new element. + link.classList.add(CLASS_TAB_SELECTED); + link.setAttribute("aria-selected", "true"); + }) + }); + }); + }, + }; + +})(Drupal, once); diff --git a/js/tabledrag.js b/js/tabledrag.js new file mode 100644 index 0000000000000000000000000000000000000000..5b6ecea6e5dab2f073987e04bcd1b5874eef23cc --- /dev/null +++ b/js/tabledrag.js @@ -0,0 +1,80 @@ +/** + * @file + * Manage tabbledrag features with DSFR. + */ + +/** + * Triggers when weights columns are toggled. + * + * @event columnschange + */ + +(function ($, Drupal) { + "use strict"; + + const initColumnsOriginal = Drupal.tableDrag.prototype.initColumns; + const addChangedWarningOriginal = Drupal.tableDrag.prototype.row.prototype.addChangedWarning; + + /** + * {@inheritdoc} + */ + Drupal.tableDrag.prototype.row.prototype.addChangedWarning = function () { + const $table = $(this.table.parentNode); + + // Do not add the changed warning if one is already present. + if (!$table.find(".tabledrag-changed-warning").length) { + addChangedWarningOriginal.call(this); + + const $tableWrapper = $table.parents(".fr-table"); + + // Move changed warning message before DSFR table wrapper. + $tableWrapper.parent().prepend($table.find(".tabledrag-changed-warning")); + } + }; + + /** + * {@inheritdoc} + */ + Drupal.tableDrag.prototype.initColumns = function () { + const $tableWrapper = this.$table.parents(".fr-table"); + const $toggleWeightWrapper = this.$table.prev(); + + // Move toggle weight wrapper before DSFR table wrapper. + $tableWrapper.before($toggleWeightWrapper); + + // Call original "initColumns" method. + initColumnsOriginal.call(this); + }; + + $.extend( + Drupal.theme, + /** @lends Drupal.theme */ { + /** + * Constructs contents of the toggle weight button. + * + * @param {boolean} show + * If the table weights are currently displayed. + * + * @return {string} + * HTML markup for the weight toggle button content. + */ + toggleButtonContent: (show) => { + const classes = [ + "tabledrag-toggle-weight", + "fr-icon--sm", + ]; + let text = ""; + if (show) { + classes.push("fr-icon-eye-off-line"); + text = Drupal.t("Hide row weights"); + } + else { + classes.push("fr-icon-eye-line"); + text = Drupal.t("Show row weights"); + } + return `<span class="${classes.join(" ")}" aria-hidden="true"></span> ${text}`; + }, + }, + ); + +})(jQuery, Drupal); diff --git a/src/Dsfr4DrupalPreRender.php b/src/Dsfr4DrupalPreRender.php new file mode 100644 index 0000000000000000000000000000000000000000..cbea2666e7cc3969a8642cf19f0c864f72921fa5 --- /dev/null +++ b/src/Dsfr4DrupalPreRender.php @@ -0,0 +1,87 @@ +<?php + +namespace Drupal\dsfr4drupal; + +use Drupal\Core\Render\Element; +use Drupal\Core\Security\TrustedCallbackInterface; + +/** + * Implements trusted prerender callbacks for the "DSFR for Drupal" theme. + * + * @internal + */ +class Dsfr4DrupalPreRender implements TrustedCallbackInterface { + + /** + * Prerender callback for Horizontal Tabs element. + * + * @param array $element + * The render array element. + * + * @return array + * The new render array element. + */ + public static function horizontalTabs(array $element): array { + return self::tabs($element, 'horizontal'); + } + + /** + * Prerender callback for Vertical Tabs element. + * + * @param array $element + * The render array element. + * + * @return array + * The new render array element. + */ + public static function verticalTabs(array $element): array { + return self::tabs($element, 'vertical'); + } + + /** + * Prerender callback for tabs element. + * + * @param array $element + * The render array element. + * + * @return array + * The new render array element. + */ + private static function tabs(array $element, string $orientation): array { + $isDetails = isset($element['group']['#type']) && $element['group']['#type'] === 'details'; + $existingGroups = isset($element['group']['#groups']) && is_array($element['group']['#groups']); + + // If the horizontal/vertical tabs have a details group, add attributes to those + // details elements so they are styled as accordion items and have BEM classes. + if ($isDetails && $existingGroups) { + $groupKeys = Element::children($element['group']['#groups'], TRUE); + + $groupKey = implode('][', $element['#parents']); + // Only check siblings against groups because we are only looking for + // group elements. + if (in_array($groupKey, $groupKeys)) { + $childrenKeys = Element::children($element['group']['#groups'][$groupKey], TRUE); + + foreach ($childrenKeys as $childKey) { + $type = $element['group']['#groups'][$groupKey][$childKey]['#type'] ?? NULL; + if ($type === 'details') { + $element['group']['#groups'][$groupKey][$childKey]['#' . $orientation . '_tab_item'] = TRUE; + } + } + } + } + + return $element; + } + + /** + * {@inheritdoc} + */ + public static function trustedCallbacks(): array { + return [ + 'horizontalTabs', + 'verticalTabs', + ]; + } + +} diff --git a/templates/base/base-page-404.html.twig b/templates/base/base-page-404.html.twig index a0ab027b3f55dda57affc3ccd526deb9b2cde9b7..17535f67e9be4d545c6c96d5e7db3052f048ee9c 100644 --- a/templates/base/base-page-404.html.twig +++ b/templates/base/base-page-404.html.twig @@ -1,6 +1,6 @@ {% set links = links|default([ { - 'url': url('<front>')|render|render, + 'url': 'internal:/<front>', 'title': 'Homepage'|t, }, ]) %} diff --git a/templates/base/base-page-500.html.twig b/templates/base/base-page-500.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..4180e5b53d50339d19d106e837d36c4f7816e605 --- /dev/null +++ b/templates/base/base-page-500.html.twig @@ -0,0 +1,38 @@ +{% set links = links|default([ + { + 'url': 'internal:/contact', + 'title': 'Contact us'|t, + }, +]) %} +{% set subtitle = subtitle|default('Error 500'|t) %} +{% set title = title|default('Unexpected error'|t) %} +{% set text_lead = text_lead|default('Sorry, there is an issue with the service, we are working to resolve it as quickly as possible.'|t) %} +{% set text = text|default('Please try refreshing the page or try again later.'|t) %} + +<div class="fr-py-0 fr-col-12 fr-col-md-6"> + <h1>{{ title }}</h1> + <p class="fr-text--sm fr-mb-3w">{{ subtitle }}</p> + <p class="fr-text--lead fr-mb-3w">{{ text_lead }}</p> + <p class="fr-text--sm fr-mb-5w">{{ text }}</p> + {% if links %} + <ul class="fr-btns-group fr-btns-group--inline-md"> + {% for link in links %} + <li> + {{ link(link.title, link.url, {'class': 'fr-btn'}) }} + </li> + {% endfor %} + </ul> + {{ attach_library('dsfr4drupal/component.button') }} + {% endif %} +</div> +<div class="fr-col-12 fr-col-md-3 fr-col-offset-md-1 fr-px-6w fr-px-md-0 fr-py-0"> + <svg xmlns="http://www.w3.org/2000/svg" class="fr-responsive-img fr-artwork" aria-hidden="true" width="160" height="200" viewBox="0 0 160 200"> + <use class="fr-artwork-motif" href="{{ dsfr_path }}artwork/background/ovoid.svg#artwork-motif"></use> + <use class="fr-artwork-background" href="{{ dsfr_path }}artwork/background/ovoid.svg#artwork-background"></use> + <g transform="translate(40, 60)"> + <use class="fr-artwork-decorative" href="{{ dsfr_path }}artwork/pictograms/system/technical-error.svg#artwork-decorative"></use> + <use class="fr-artwork-minor" href="{{ dsfr_path }}artwork/pictograms/system/technical-error.svg#artwork-minor"></use> + <use class="fr-artwork-major" href="{{ dsfr_path }}artwork/pictograms/system/technical-error.svg#artwork-major"></use> + </g> + </svg> +</div> diff --git a/templates/block/block--local-actions-block.html.twig b/templates/block/block--local-actions-block.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..04c8bbe0719525e4888756fbfc024c2bc0d8dd47 --- /dev/null +++ b/templates/block/block--local-actions-block.html.twig @@ -0,0 +1,10 @@ +{% extends 'block.html.twig' %} + +{% block content %} + {% if content %} + <ul class="local-actions fr-mb-4v"> + {{ content }} + </ul> + {{ attach_library('dsfr4drupal/local-actions') }} + {% endif %} +{% endblock %} diff --git a/templates/block/block.html.twig b/templates/block/block.html.twig index 1ac5205a55bc73f94855306a5c9cef2f88a7d46a..221b492ba84074805c2dbddf4e950437736f20e9 100644 --- a/templates/block/block.html.twig +++ b/templates/block/block.html.twig @@ -1,16 +1,16 @@ {% if attributes is not empty %} -<div{{ attributes }}> + <div{{ attributes }}> {% endif %} - {{ title_prefix }} - {% if label %} - <h2{{ title_attributes }}>{{ label }}</h2> - {% endif %} - {{ title_suffix }} - {% block content %} - {{ content }} - {% endblock %} +{{ title_prefix }} +{% if label %} + <h2{{ title_attributes }}>{{ label }}</h2> +{% endif %} +{{ title_suffix }} +{% block content %} + {{ content }} +{% endblock %} {% if attributes is not empty %} -</div> + </div> {% endif %} diff --git a/templates/field/field--media--thumbnail.html.twig b/templates/field/field--media--thumbnail.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..df508d39da13111caff8d7ee0f6db67b82e2c573 --- /dev/null +++ b/templates/field/field--media--thumbnail.html.twig @@ -0,0 +1,2 @@ +{# Include default "field" template without alter it to benefits of preprocessing. #} +{% include 'field.html.twig' %} diff --git a/templates/field/field--node--created.html.twig b/templates/field/field--node--created.html.twig index 82077662f79056d5bb50588dd967fe4522941f86..2e5679c52b52c55a24eb9d1bf6e75669622635b2 100644 --- a/templates/field/field--node--created.html.twig +++ b/templates/field/field--node--created.html.twig @@ -1,5 +1,5 @@ {% if not is_inline %} - {% include "field.html.twig" %} + {% include 'field.html.twig' %} {% else %} {%- if attributes is not empty -%} <span{{ attributes }}> diff --git a/templates/field/field--node--title.html.twig b/templates/field/field--node--title.html.twig index 82077662f79056d5bb50588dd967fe4522941f86..2e5679c52b52c55a24eb9d1bf6e75669622635b2 100644 --- a/templates/field/field--node--title.html.twig +++ b/templates/field/field--node--title.html.twig @@ -1,5 +1,5 @@ {% if not is_inline %} - {% include "field.html.twig" %} + {% include 'field.html.twig' %} {% else %} {%- if attributes is not empty -%} <span{{ attributes }}> diff --git a/templates/form/details--horizontal-tabs.html.twig b/templates/form/details--horizontal-tabs.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..c5a902d161069050858a2c218f19f9d1dfdaff04 --- /dev/null +++ b/templates/form/details--horizontal-tabs.html.twig @@ -0,0 +1,2 @@ +{# Specific "details" templating for vertical tabs. Do not use DSFR accordion. #} +{% include '@system/templates/details.html.twig' %} diff --git a/templates/form/details--vertical-tabs.html.twig b/templates/form/details--vertical-tabs.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..c5a902d161069050858a2c218f19f9d1dfdaff04 --- /dev/null +++ b/templates/form/details--vertical-tabs.html.twig @@ -0,0 +1,2 @@ +{# Specific "details" templating for vertical tabs. Do not use DSFR accordion. #} +{% include '@system/templates/details.html.twig' %} diff --git a/templates/form/field-multiple-value-form.html.twig b/templates/form/field-multiple-value-form.html.twig index 46964d3879296e7426d6f1c3bb7da79fac8c9ca0..6654c13d0eced13be358d1cf4707bb81151541c7 100644 --- a/templates/form/field-multiple-value-form.html.twig +++ b/templates/form/field-multiple-value-form.html.twig @@ -1,7 +1,7 @@ {% if multiple %} {% set classes = [ 'js-form-item', - 'form-item' + 'form-item', ] %} <div{{ attributes.addClass(classes) }}> diff --git a/templates/form/fieldset.html.twig b/templates/form/fieldset.html.twig index 2143e1571708e75a85d6505cca00654ec8c6b863..1584727d3cf86ee36fd25367b09cd168fa8ef227 100644 --- a/templates/form/fieldset.html.twig +++ b/templates/form/fieldset.html.twig @@ -11,10 +11,17 @@ required ? 'form-required', ] %} <div class="fr-form-group"> - <fieldset{{ attributes.setAttribute('role', 'group').addClass(classes) }}> + <fieldset{{ attributes.addClass(classes) }}> <legend{{ legend.attributes.addClass(legend_classes) }}>{{ legend.title }} {% if description.content %} - <span class="fr-hint-text">{{ description.content }}</span> + {% if description_tooltip %} + {{ include('dsfr4drupal:tooltip', { + 'title': tooltip_title|default('Help text'|t), + 'tooltip': description.content, + }, with_context=false) }} + {% else %} + <span class="fr-hint-text">{{ description.content }}</span> + {% endif %} {% endif %} </legend> <div class="fr-fieldset__content"> diff --git a/templates/form/form-element--checkbox.html.twig b/templates/form/form-element--checkbox.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..ebc4eb61be8ed3b7462b9a97d51ecfcd13bbc1f4 --- /dev/null +++ b/templates/form/form-element--checkbox.html.twig @@ -0,0 +1,26 @@ +{% set classes = [ + 'js-form-item', + 'form-item', + 'js-form-type-' ~ type|clean_class, + 'form-item-' ~ name|clean_class, + 'js-form-item-' ~ name|clean_class, + title_display not in ['after', 'before'] ? 'form-item--no-label', + disabled == 'disabled' ? 'form-disabled', +] %} + +<div{{ attributes.addClass(classes) }}> + {% if prefix is not empty %} + <span class="field-prefix">{{ prefix }}</span> + {% endif %} + + {{ children }} + {% if suffix is not empty %} + <span class="field-suffix">{{ suffix }}</span> + {% endif %} + {# The label is always display after to display checkbox. #} + {{ label }} + {# In case of we need to display errors in form element. #} + {% if show_error %} + {% include '@dsfr4drupal/templates/form/form-element.error.html.twig' %} + {% endif %} +</div> diff --git a/templates/form/form-element--radio.html.twig b/templates/form/form-element--radio.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..74456c61ce3f71958afe3e7c2740f7ae4a06656e --- /dev/null +++ b/templates/form/form-element--radio.html.twig @@ -0,0 +1 @@ +{% include 'form-element--checkbox.html.twig' %} diff --git a/templates/form/form-element-label.html.twig b/templates/form/form-element-label.html.twig index a8138ad01bd13847008a74b8ce63a85fd2c860f9..0686a5cc5527bebd034b609e469680f7e7dd5254 100644 --- a/templates/form/form-element-label.html.twig +++ b/templates/form/form-element-label.html.twig @@ -1,4 +1,3 @@ -{% set tooltip_title = tooltip_title|default('Help text'|t) %} {% set classes = [ title_display == 'after' ? 'option', title_display == 'invisible' ? 'visually-hidden', @@ -11,7 +10,7 @@ {% if description.content %} {% if description_tooltip %} {{ include('dsfr4drupal:tooltip', { - 'title': tooltip_title, + 'title': tooltip_title|default('Help text'|t), 'tooltip': description.content, }, with_context=false) }} {% else %} diff --git a/templates/form/form-element.html.twig b/templates/form/form-element.html.twig index adee85c6d1edfe27c80a459f67a1eabb2b134cc3..c436fac980d2c123085b0762137682485ce3e50f 100644 --- a/templates/form/form-element.html.twig +++ b/templates/form/form-element.html.twig @@ -4,7 +4,7 @@ 'js-form-type-' ~ type|clean_class, 'form-item-' ~ name|clean_class, 'js-form-item-' ~ name|clean_class, - title_display not in ['after', 'before'] ? 'form-no-label', + title_display not in ['after', 'before'] ? 'form-item--no-label', disabled == 'disabled' ? 'form-disabled', ] %} diff --git a/templates/form/horizontal-tabs.html.twig b/templates/form/horizontal-tabs.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..7af90c489620d55ed0aad64e34953c14fba94147 --- /dev/null +++ b/templates/form/horizontal-tabs.html.twig @@ -0,0 +1,6 @@ +<div data-horizontal-tabs class="horizontal-tabs"> + <div class="fr-tabs"></div> + {# Cannot add list into "fr-tabs", otherwise it's not populated. #} + <ul data-horizontal-tabs-list class="horizontal-tabs-list fr-tabs__list"></ul> + <div data-horizontal-tabs-panes{{ attributes }}>{{ children }}</div> +</div> diff --git a/templates/form/input--password.html.twig b/templates/form/input--password.html.twig index 0fb51b568d25b48db30c3b2d7b46dac12dc53929..93718b48752ed651a6e827d501c439d72be24661 100644 --- a/templates/form/input--password.html.twig +++ b/templates/form/input--password.html.twig @@ -13,7 +13,6 @@ <label class="fr--password__checkbox fr-label" for="{{ show_password_id }}"> {{ show_password_label }} </label> - </div> </div> {% endif %} {{ attach_library('dsfr4drupal/component.password') }} diff --git a/templates/form/links--dropbutton.html.twig b/templates/form/links--dropbutton.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..0b94975dc92e13ac725182aeb71c6234b543caa6 --- /dev/null +++ b/templates/form/links--dropbutton.html.twig @@ -0,0 +1,63 @@ +{# +/** + * @file + * Theme override for a set of links. + * + * Available variables: + * - attributes: Attributes for the UL containing the list of links. + * - links: Links to be output. + * Each link will have the following elements: + * - link: (optional) A render array that returns a link. See + * template_preprocess_links() for details how it is generated. + * - text: The link text. + * - attributes: HTML attributes for the list item element. + * - text_attributes: (optional) HTML attributes for the span element if no + * 'url' was supplied. + * - heading: (optional) A heading to precede the links. + * - text: The heading text. + * - level: The heading level (e.g. 'h2', 'h3'). + * - attributes: (optional) A keyed list of attributes for the heading. + * If the heading is a string, it will be used as the text of the heading and + * the level will default to 'h2'. + * + * Headings should be used on navigation menus and any list of links that + * consistently appears on multiple pages. To make the heading invisible use + * the 'visually-hidden' CSS class. Do not use 'display:none', which + * removes it from screen readers and assistive technology. Headings allow + * screen reader and keyboard only users to navigate to or skip the links. + * See http://juicystudio.com/article/screen-readers-display-none.php and + * http://www.w3.org/TR/WCAG-TECHS/H42.html for more information. + * + * @see template_preprocess_links() + */ +#} +{% if links -%} + {%- if heading -%} + {%- if heading.level -%} + <{{ heading.level }}{{ heading.attributes }}>{{ heading.text }}</{{ heading.level }}> + {%- else -%} + <h2{{ heading.attributes }}>{{ heading.text }}</h2> + {%- endif -%} + {%- endif -%} + <ul{{ attributes.addClass('dropbutton--dsfr4drupal') }}> + {%- for item in links -%} + {% if loop.index == 2 %} + <li class="dropbutton__item"> + <ul class="dropbutton__items"> + {% endif %} + <li{{ item.attributes.addClass('dropbutton__item') }}> + {%- if item.link -%} + {{ item.link }} + {%- elseif item.text_attributes -%} + <span{{ item.text_attributes }}>{{ item.text }}</span> + {%- else -%} + {{ item.text }} + {%- endif -%} + </li> + {% if loop.length > 1 and loop.last %} + </ul> + </li> + {% endif %} + {%- endfor -%} + </ul> +{%- endif %} diff --git a/templates/layout/page.html.twig b/templates/layout/page.html.twig index 3d79ad9be714e8e07ec94b3a0d07c37dabca81e6..0433eeaae686bee59e7a63b02751268ddbab2345 100644 --- a/templates/layout/page.html.twig +++ b/templates/layout/page.html.twig @@ -11,7 +11,7 @@ <div class="{{ container_class|default('fr-container') }}"> {{ page.breadcrumb }} </div> -<main id="main" role="main" class="{{ container_class|default('fr-container') }}" tabindex="-1"> +<main id="main" role="main" tabindex="-1"> <div class="{{ container_class|default('fr-container') }}"> {{ page.content }} </div> diff --git a/templates/media/media--media-library.html.twig b/templates/media/media--media-library.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..e51ace768486f31a7e5c78f4761444994a0b9cf1 --- /dev/null +++ b/templates/media/media--media-library.html.twig @@ -0,0 +1,20 @@ +{% if attributes is not empty %} + <div{{ attributes }}> +{% endif %} + +<div{{ preview_attributes.addClass('js-media-library-item-preview') }}> + {{ include('dsfr4drupal:card', { + 'image': content.thumbnail ? content.thumbnail : '', + 'title': name, + 'title_attributes': metadata_attributes, + 'description': content|without('thumbnail'), + 'no_arrow': true, + 'detail': not status ? 'unpublished'|t : '', + 'detail_icon': 'fr-icon-warning-fill', + 'variant': 'sm', + }, with_context=false) }} +</div> + +{% if attributes is not empty %} + </div> +{% endif %} diff --git a/templates/media/media.html.twig b/templates/media/media.html.twig index 427b3f58ac1f7c17e932389ff82b1c512c9722ff..349904d1145183f70f9451d963037fa56f13345f 100644 --- a/templates/media/media.html.twig +++ b/templates/media/media.html.twig @@ -1,10 +1,10 @@ {% if attributes is not empty %} -<div{{ attributes }}> + <div{{ attributes }}> {% endif %} - {{ title_suffix.contextual_links }} - {{ content }} +{{ title_suffix.contextual_links }} +{{ content }} {% if attributes is not empty %} -</div> + </div> {% endif %} diff --git a/templates/menu/menu-local-tasks.html.twig b/templates/menu/menu-local-tasks.html.twig index 90c4ed730ba4e8992dfead1825da3c3d135558dd..22097ad7c26e67a2514b59d50ed8dfbe38550b77 100644 --- a/templates/menu/menu-local-tasks.html.twig +++ b/templates/menu/menu-local-tasks.html.twig @@ -1,3 +1,5 @@ +{% set attributes = attributes.addClass('fr-mb-4v') %} + {% if primary %} <h2 class="visually-hidden">{{ 'Primary tabs'|t }}</h2> {{ include('dsfr4drupal:tabs', {'tabs': primary}) }} diff --git a/templates/node/node.html.twig b/templates/node/node.html.twig index b23d6da932ae43e4fbe607095bd1463e367ce319..f46d52da5ba3f41998778792191a0c87e912e822 100644 --- a/templates/node/node.html.twig +++ b/templates/node/node.html.twig @@ -1,17 +1,17 @@ {% if attributes is not empty %} -<div{{ attributes }}> + <div{{ attributes }}> {% endif %} - {% if content_attributes is not empty %} +{% if content_attributes is not empty %} <div{{ content_attributes }}> - {% endif %} +{% endif %} - {{ content }} +{{ content }} - {% if content_attributes is not empty %} +{% if content_attributes is not empty %} </div> - {% endif %} +{% endif %} {% if attributes is not empty %} -</div> + </div> {% endif %} diff --git a/templates/region/region.html.twig b/templates/region/region.html.twig index ce195584fefa48b188831488049f9c2c11c9a816..80f6ad31dc0b7b6938928c45fa5fc5a75e3f278a 100644 --- a/templates/region/region.html.twig +++ b/templates/region/region.html.twig @@ -1,11 +1,11 @@ {% if content %} {% if attributes is not empty %} - <div{{ attributes }}> + <div{{ attributes }}> {% endif %} - {{ content }} + {{ content }} {% if attributes is not empty %} - </div> + </div> {% endif %} {% endif %} diff --git a/templates/system/admin-block-content.html.twig b/templates/system/admin-block-content.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..c3f443387d5b91b0dd0c89ea31f145c97cee9fcd --- /dev/null +++ b/templates/system/admin-block-content.html.twig @@ -0,0 +1,14 @@ +{% set classes = [ + 'list-group', + compact ? 'compact', +] %} +{% if content %} + <dl{{ attributes.addClass(classes) }}> + {% for item in content %} + <dt class="list-group__link fr-mt-2v">{{ item.link }}</dt> + {% if item.description %} + <dd class="list-group__description">{{ item.description }}</dd> + {% endif %} + {% endfor %} + </dl> +{% endif %} diff --git a/templates/system/admin-page.html.twig b/templates/system/admin-page.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..b82b265a438d7d3d711573acea4ee1f1e3f500fa --- /dev/null +++ b/templates/system/admin-page.html.twig @@ -0,0 +1,13 @@ +<div class="clearfix"> + {{ system_compact_link|add_class('fr-text--sm') }} + + <div class="fr-grid-row fr-grid-row--gutters"> + {% for container in containers %} + <div class="fr-col-12 fr-col-md-6"> + {% for block in container.blocks %} + {{ block }} + {% endfor %} + </div> + {% endfor %} + </div> +</div> diff --git a/templates/system/details--dialog-off-canvas.html.twig b/templates/system/details--dialog-off-canvas.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..9bbbd4924c7d922e2bd5e52ba9d1a48aee263d03 --- /dev/null +++ b/templates/system/details--dialog-off-canvas.html.twig @@ -0,0 +1,2 @@ +{# Styles are reset with Drupal dialog off canvas. Use default details rendering instead of DSFR accordion component. #} +{% include '@system/templates/details.html.twig' %} diff --git a/templates/system/details.html.twig b/templates/system/details.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..f9dce319b46a7591e2f13382d9201a3bedee0f0a --- /dev/null +++ b/templates/system/details.html.twig @@ -0,0 +1,17 @@ +{% set content %} + {% if errors %} + <div> + {{ errors }} + </div> + {% endif %} + + {{ description }} + {{ children }} + {{ value }} +{% endset %} + +{{ include('dsfr4drupal:accordion', { + 'button_attributes': create_attribute({'class': required ? ['js-form-required', 'form-required'] : ''}), + 'expanded': not element['#attributes']['open'] is empty, + 'title_tag': 'div', +}) }} diff --git a/templates/system/status-report-general-info.html.twig b/templates/system/status-report-general-info.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..c1554f7bf23ed601b2587c37d2d1cc2756a55a36 --- /dev/null +++ b/templates/system/status-report-general-info.html.twig @@ -0,0 +1,61 @@ +<h2>{{ 'General System Information'|t }}</h2> +<div class="fr-grid-row fr-grid-row--gutters fr-mb-4v"> + <div class="fr-col-12 fr-col-md-6"> + <div class="system-status-general-info__item"> + <h3 class="fr-h5 system-status-general-info__item-title">{{ 'Drupal Version'|t }}</h3> + {{ drupal.value }} + {% if drupal.description %} + {{ drupal.description }} + {% endif %} + </div> + </div> + <div class="fr-col-12 fr-col-md-6"> + <div class="system-status-general-info__item"> + <h3 class="fr-h5 system-status-general-info__item-title">{{ 'Web Server'|t }}</h3> + {{ webserver.value }} + {% if webserver.description %} + {{ webserver.description }} + {% endif %} + </div> + </div> + <div class="fr-col-12 fr-col-md-6"> + <div class="system-status-general-info__item"> + <h3 class="fr-h5 system-status-general-info__item-title">{{ 'PHP'|t }}</h3> + <h4 class="fr-h6">{{ 'Version'|t }}</h4> {{ php.value }} + {% if php.description %} + {{ php.description }} + {% endif %} + + <h4 class="fr-h6">{{ 'Memory limit'|t }}</h4>{{ php_memory_limit.value }} + {% if php_memory_limit.description %} + {{ php_memory_limit.description }} + {% endif %} + </div> + </div> + <div class="fr-col-12 fr-col-md-6"> + <div class="system-status-general-info__item"> + <h3 class="fr-h5 system-status-general-info__item-title">{{ 'Database'|t }}</h3> + <h4 class="fr-h6">{{ 'Version'|t }}</h4>{{ database_system_version.value }} + {% if database_system_version.description %} + {{ database_system_version.description }} + {% endif %} + + <h4 class="fr-h6">{{ 'System'|t }}</h4>{{ database_system.value }} + {% if database_system.description %} + {{ database_system.description }} + {% endif %} + </div> + </div> + <div class="fr-col-12"> + <div class="system-status-general-info__item"> + <h3 class="fr-h5 system-status-general-info__item-title">{{ 'Last Cron Run'|t }}</h3> + {{ cron.value }} + {% if cron.run_cron %} + {{ cron.run_cron }} + {% endif %} + {% if cron.description %} + {{ cron.description }} + {% endif %} + </div> + </div> +</div> diff --git a/templates/views/views-view--media-library.html.twig b/templates/views/views-view--media-library.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..d31d2c279da24761ce8b18a162170d9a801e38a7 --- /dev/null +++ b/templates/views/views-view--media-library.html.twig @@ -0,0 +1,43 @@ +{% set classes = [ + dom_id ? 'js-view-dom-id-' ~ dom_id, +] %} + +<div{{ attributes.addClass(classes) }}> + {{ title_prefix }} + {{ title }} + {{ title_suffix }} + + {% if header %} + <header class="fr-tabs"> + <ul class="fr-tabs__list" role="tablist"> + {% for link in header %} + <li role="presentation"> + {{ link }} + </li> + {% endfor %} + </ul> + </header> + {{ attach_library('dsfr4drupal/component.tab') }} + {% endif %} + + {{ exposed }} + {{ attachment_before }} + + {% if rows -%} + {{ rows }} + {% elseif empty -%} + {{ empty }} + {% endif %} + {{ pager }} + + {{ attachment_after }} + {{ more }} + + {% if footer %} + <footer> + {{ footer }} + </footer> + {% endif %} + + {{ feed_icons }} +</div> diff --git a/templates/views/views-view-unformatted--media-library.html.twig b/templates/views/views-view-unformatted--media-library.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..61fa26d4877eea14c9f5e9d2880ecc60fe1b6530 --- /dev/null +++ b/templates/views/views-view-unformatted--media-library.html.twig @@ -0,0 +1,15 @@ +{% set row_classes = [ + default_row_class ? 'views-row', +] %} + +{% if title %} + <h3>{{ title }}</h3> +{% endif %} + +<div class="fr-grid-row fr-grid-row--gutters"> + {% for row in rows %} + <div{{ row.attributes.addClass(row_classes).addClass(['fr-col-12', 'fr-col-sm-6', 'fr-col-md-4', 'fr-col-lg-3']) }}> + {{- row.content -}} + </div> + {% endfor %} +</div> diff --git a/translations/fr.po b/translations/fr.po index e3ef9201881f2d9e236e711f1bacc178591866c1..792f25db923b0b69a3a5617e5dff455d83ed400e 100644 --- a/translations/fr.po +++ b/translations/fr.po @@ -268,6 +268,9 @@ msgstr "@code - @label" msgid "Page not found" msgstr "Page non trouvée" +msgid "Unexpected error" +msgstr "Erreur inattendue" + msgid "Show" msgstr "Afficher" @@ -277,9 +280,15 @@ msgstr "Afficher le mot de passe" msgid "Error 404" msgstr "Erreur 404" +msgid "Error 500" +msgstr "Erreur 500" + msgid "The page you are looking for cannot be found. We apologize for the inconvenience caused." msgstr "La page que vous cherchez est introuvable. Excusez-nous pour la gène occasionnée." +msgid "Sorry, there is an issue with the service, we are working to resolve it as quickly as possible." +msgstr "Désolé, le service rencontre un probleÌ€me, nous travaillons pour le reÌsoudre le plus rapidement possible." + msgid "" "If you typed the web address into the browser, verify that it is correct. " "The page may no longer be available.<br />In this case, to continue your visit you can consult our home page, " @@ -289,6 +298,12 @@ msgstr "" "La page n’est peut-être plus disponible.<br />Dans ce cas, pour continuer votre visite vous pouvez consulter notre page d’accueil, " "ou effectuer une recherche avec notre moteur de recherche en haut de page.<br />Sinon contactez-nous pour que l’on puisse vous rediriger vers la bonne information." +msgid "Please try refreshing the page or try again later." +msgstr "Essayez de rafraîchir la page ou bien ressayez plus tard." + +msgid "Contact us" +msgstr "Contactez-nous" + msgid "Copy to clipboard" msgstr "Copier dans le presse papier" diff --git a/vendor/autoload.php b/vendor/autoload.php new file mode 100644 index 0000000000000000000000000000000000000000..8d66326355f80a560cb2e1bac318a69949cc0c73 --- /dev/null +++ b/vendor/autoload.php @@ -0,0 +1,25 @@ +<?php + +// autoload.php @generated by Composer + +if (PHP_VERSION_ID < 50600) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL; + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, $err); + } elseif (!headers_sent()) { + echo $err; + } + } + trigger_error( + $err, + E_USER_ERROR + ); +} + +require_once __DIR__ . '/composer/autoload_real.php'; + +return ComposerAutoloaderInite2b57849f956d4c67658848cdb027ed8::getLoader(); diff --git a/vendor/composer/ClassLoader.php b/vendor/composer/ClassLoader.php new file mode 100644 index 0000000000000000000000000000000000000000..7824d8f7eafe8db890975f0fa2dfab31435900da --- /dev/null +++ b/vendor/composer/ClassLoader.php @@ -0,0 +1,579 @@ +<?php + +/* + * This file is part of Composer. + * + * (c) Nils Adermann <naderman@naderman.de> + * Jordi Boggiano <j.boggiano@seld.be> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier <fabien@symfony.com> + * @author Jordi Boggiano <j.boggiano@seld.be> + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var \Closure(string):void */ + private static $includeFile; + + /** @var string|null */ + private $vendorDir; + + // PSR-4 + /** + * @var array<string, array<string, int>> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array<string, list<string>> + */ + private $prefixDirsPsr4 = array(); + /** + * @var list<string> + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * List of PSR-0 prefixes + * + * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) + * + * @var array<string, array<string, list<string>>> + */ + private $prefixesPsr0 = array(); + /** + * @var list<string> + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var array<string, string> + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var array<string, bool> + */ + private $missingClasses = array(); + + /** @var string|null */ + private $apcuPrefix; + + /** + * @var array<string, self> + */ + private static $registeredLoaders = array(); + + /** + * @param string|null $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); + } + + /** + * @return array<string, list<string>> + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array<string, list<string>> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return list<string> + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return list<string> + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return array<string, string> Array of classname => path + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array<string, string> $classMap Class to filename map + * + * @return void + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param list<string>|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list<string>|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param list<string>|string $paths The PSR-0 base directories + * + * @return void + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list<string>|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + * + * @return void + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + * + * @return void + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + * + * @return void + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + $includeFile = self::$includeFile; + $includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders keyed by their corresponding vendor directories. + * + * @return array<string, self> + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } + + /** + * @return void + */ + private static function initializeIncludeClosure() + { + if (self::$includeFile !== null) { + return; + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + */ + self::$includeFile = \Closure::bind(static function($file) { + include $file; + }, null, null); + } +} diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php new file mode 100644 index 0000000000000000000000000000000000000000..51e734a774b3ed9ca110a921cb40a74f8c7905c2 --- /dev/null +++ b/vendor/composer/InstalledVersions.php @@ -0,0 +1,359 @@ +<?php + +/* + * This file is part of Composer. + * + * (c) Nils Adermann <naderman@naderman.de> + * Jordi Boggiano <j.boggiano@seld.be> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null + */ + private static $installed; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list<string> + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list<string> + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints((string) $constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + } + + /** + * @return array[] + * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + + if (self::$canGetVendors) { + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ + $required = require $vendorDir.'/composer/installed.php'; + $installed[] = self::$installedByVendor[$vendorDir] = $required; + if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $installed[count($installed) - 1]; + } + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array()) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..f27399a042d95c4708af3a8c74d35d338763cf8f --- /dev/null +++ b/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000000000000000000000000000000000000..0fb0a2c194b8590999a5ed79e357d4a9c1e9d8b8 --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,10 @@ +<?php + +// autoload_classmap.php @generated by Composer + +$vendorDir = dirname(__DIR__); +$baseDir = dirname($vendorDir); + +return array( + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', +); diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000000000000000000000000000000000000..15a2ff3ad6d8d6ea2b6b1f9552c62d745ffc9bf4 --- /dev/null +++ b/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ +<?php + +// autoload_namespaces.php @generated by Composer + +$vendorDir = dirname(__DIR__); +$baseDir = dirname($vendorDir); + +return array( +); diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php new file mode 100644 index 0000000000000000000000000000000000000000..3890ddc2409b78e2720b8ec2db64d76b18811b3d --- /dev/null +++ b/vendor/composer/autoload_psr4.php @@ -0,0 +1,9 @@ +<?php + +// autoload_psr4.php @generated by Composer + +$vendorDir = dirname(__DIR__); +$baseDir = dirname($vendorDir); + +return array( +); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php new file mode 100644 index 0000000000000000000000000000000000000000..5c6bdc2aac2439eda41cec7dcde9b259b3e54eb1 --- /dev/null +++ b/vendor/composer/autoload_real.php @@ -0,0 +1,36 @@ +<?php + +// autoload_real.php @generated by Composer + +class ComposerAutoloaderInite2b57849f956d4c67658848cdb027ed8 +{ + private static $loader; + + public static function loadClassLoader($class) + { + if ('Composer\Autoload\ClassLoader' === $class) { + require __DIR__ . '/ClassLoader.php'; + } + } + + /** + * @return \Composer\Autoload\ClassLoader + */ + public static function getLoader() + { + if (null !== self::$loader) { + return self::$loader; + } + + spl_autoload_register(array('ComposerAutoloaderInite2b57849f956d4c67658848cdb027ed8', 'loadClassLoader'), true, true); + self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); + spl_autoload_unregister(array('ComposerAutoloaderInite2b57849f956d4c67658848cdb027ed8', 'loadClassLoader')); + + require __DIR__ . '/autoload_static.php'; + call_user_func(\Composer\Autoload\ComposerStaticInite2b57849f956d4c67658848cdb027ed8::getInitializer($loader)); + + $loader->register(true); + + return $loader; + } +} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php new file mode 100644 index 0000000000000000000000000000000000000000..3e4da4b5236ce7f8115d22974ee5d82678e7a4ed --- /dev/null +++ b/vendor/composer/autoload_static.php @@ -0,0 +1,20 @@ +<?php + +// autoload_static.php @generated by Composer + +namespace Composer\Autoload; + +class ComposerStaticInite2b57849f956d4c67658848cdb027ed8 +{ + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->classMap = ComposerStaticInite2b57849f956d4c67658848cdb027ed8::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json new file mode 100644 index 0000000000000000000000000000000000000000..87fda747e6ce957c1bf1b4d373bb5c60dbf96e7d --- /dev/null +++ b/vendor/composer/installed.json @@ -0,0 +1,5 @@ +{ + "packages": [], + "dev": true, + "dev-package-names": [] +} diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php new file mode 100644 index 0000000000000000000000000000000000000000..61d003e1314dc1510e74e71c8a68064a5d2f5a03 --- /dev/null +++ b/vendor/composer/installed.php @@ -0,0 +1,23 @@ +<?php return array( + 'root' => array( + 'name' => 'drupal/dsfr4drupal', + 'pretty_version' => '1.x-dev', + 'version' => '1.9999999.9999999.9999999-dev', + 'reference' => 'c48cfd677155caefea8b2a0c84ffe527588e0484', + 'type' => 'drupal-theme', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev' => true, + ), + 'versions' => array( + 'drupal/dsfr4drupal' => array( + 'pretty_version' => '1.x-dev', + 'version' => '1.9999999.9999999.9999999-dev', + 'reference' => 'c48cfd677155caefea8b2a0c84ffe527588e0484', + 'type' => 'drupal-theme', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + ), +);