8000 Fix the Choices.js quirks by isolating it in a shadow root by m-vo · Pull Request #8261 · contao/contao · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Fix the Choices.js quirks by isolating it in a shadow root #8261

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
88a718c
remove Choices contao-component and install it as a npm package
m-vo Apr 3, 2025
d7f298e
move contao--color-scheme controller to the html element and allow ha…
m-vo Apr 3, 2025
b076c53
rewrite contao--choices controller with shadow isolation and refactor…
m-vo Apr 3, 2025
efdb4d1
fix filter layout
m-vo Apr 3, 2025
b564d49
build assets
m-vo Apr 3, 2025
bd80975
simplify import
m-vo Apr 3, 2025
798f69a
allow defining a config value via a data attribute
m-vo Apr 3, 2025
f810e21
rename custom properties to a generic name
m-vo Apr 3, 2025
1f938ea
build assets
m-vo Apr 3, 2025
f907a1d
fix border, remove unnecessary properties, reorder
m-vo Apr 4, 2025
f367ac8
normalize line height
m-vo Apr 4, 2025
392cded
fix class name
m-vo Apr 4, 2025
9adfc31
fix specificity
m-vo Apr 4, 2025
087b59b
fix .is-open selector + simplify
m-vo Apr 4, 2025
d0fef2b
fix tl_search panel
m-vo Apr 4, 2025
0a317ca
make this friendly for upstream merging by applying FC CS
m-vo Apr 4, 2025
2b62096
build assets
m-vo Apr 4, 2025
8000 a6c9c0b
Merge branch '5.5' into bugfix/isolated-choices
zoglo Apr 23, 2025
4df465b
Add widths for the `shadow-root` instances for choices
zoglo Apr 23, 2025
4ff6c23
Adjust the font-size for .choices list items
zoglo Apr 23, 2025
d166609
Only add margin to initialized choices controllers that are not withi…
zoglo Apr 23, 2025
79205f6
Rebuild the assets
zoglo Apr 23, 2025
193a026
Merge branch '5.5' into bugfix/isolated-choices
leofeyer May 12, 2025
8f682e0
Rebuild the assets
leofeyer May 12, 2025
7736f81
CS
leofeyer May 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
"cmsig/seal-symfony-bundle": "^0.6",
"contao-components/ace": "^1.8",
"contao-components/altcha": "^1.0",
"contao-components/choices": "^11.0",
"contao-components/colorbox": "^1.6",
"contao-components/contao": "^9.1",
"contao-components/datepicker": "^3.0",
Expand Down
88 changes: 57 additions & 31 deletions core-bundle/assets/controllers/choices-controller.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
import { Controller } from '@hotwired/stimulus';
import Choices from 'choices.js';
import css from '!!css-loader!../styles/component/choices.pcss';

export default class ChoicesController extends Controller {
addMutationGuard = false;
removeMutationGuard = false;
static values = { config: Object };

connect() {
if (this.addMutationGuard) {
static styleSheet = null;

initialize() {
if (ChoicesController.styleSheet) {
return;
}

// Choices wraps the element multiple times during initialization, leading to
// multiple disconnects/reconnects of the controller that we need to ignore.
this.addMutationGuard = true;
ChoicesController.styleSheet = new CSSStyleSheet();
ChoicesController.styleSheet.replace(css);
}

const select = this.element;
connect() {
// Choices.js wraps/unwraps the underlying select element when the instance is created/destroyed and may create
// a lot of DOM nodes. To prevent interference with our Stimulus mutation observers, we therefore isolate it in
// a shadow root with its own cloned select element.
const [shadowRoot, select] = this._initializeShadowRoot();

this.choices = new Choices(select, {
const config = {
shadowRoot: shadowRoot,
shouldSort: false,
duplicateItemsAllowed: false,
allowHTML: false,
Expand All @@ -30,50 +38,68 @@ export default class ChoicesController extends Controller {
threshold: 0.4,
},
callbackOnInit: () => {
const choices = select.closest('.choices')?.querySelector('.choices__list--dropdown > .choices__list');
const choices = shadowRoot.querySelector('.choices__list--dropdown > .choices__list');

if (choices && select.dataset.placeholder) {
choices.dataset.placeholder = select.dataset.placeholder;
}

queueMicrotask(() => {
this.addMutationGuard = false;
});
},
itemSelectText: '', // suppress the "Press to select" text from taking half of the width
loadingText: Contao.lang.loading,
noResultsText: Contao.lang.noResults,
noChoicesText: Contao.lang.noOptions,
removeItemLabelText: function (value) {
return Contao.lang.removeItem.concat(' ').concat(value);
},
};

// Allow others to alter the config before we create the instance.
this.dispatch('create', {
detail: { select, config: Object.assign(config, this.configValue) },
});

this.choices = new Choices(select, config);
}

disconnect() {
if (this.addMutationGuard || this.removeMutationGuard) {
return;
}

this._removeChoices();
this._restoreInitialState();
}

beforeCache() {
// Let choices unwrap the element container before Turbo caches the
// page. It will be recreated, when the connect() call happens on the
// restored page.
this._removeChoices();
// Restore changes to the DOM. They will get recreated when the connect() call happens on the restored page.
this._restoreInitialState();
}

_removeChoices() {
// Safely unwrap the element by preventing disconnect/connect calls
// during the process.
this.removeMutationGuard = true;
_initializeShadowRoot() {
// Create a sibling host element and hide the initial element.
this._host = document.createElement('div');
< F438 /td> this._host.setAttribute('data-contao--color-scheme-target', 'outlet');

this.element.insertAdjacentElement('afterend', this._host);
this.element.classList.add('hidden');

this.choices?.destroy();
this.choices = null;
// Clone the initial element and link it with a "change" event listener.
const selectForChoices = this.element.cloneNode(true);

queueMicrotask(() => {
this.removeMutationGuard = false;
selectForChoices.addEventListener('change', () => {
this.element.value = selectForChoices.value;
});

// Create a shadow root where the cloned element and the Choices instance will live.
const shadowRoot = this._host.attachShadow({ mode: 'open' });

shadowRoot.appendChild(selectForChoices);
shadowRoot.adoptedStyleSheets.push(ChoicesController.styleSheet);

return [shadowRoot, selectForChoices];
}

_restoreInitialState() {
// Make sure any document-wide event listeners are removed, so we aren't leaking memory.
this.choices.destroy();

// Remove the host element and restore the initial styling.
this._host.remove();
this.element.classList.remove('hidden');
}
}
10 changes: 9 additions & 1 deletion core-bundle/assets/controllers/color-scheme-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ const prefersDark = () => {

const setColorScheme = () => {
document.documentElement.dataset.colorScheme = prefersDark() ? 'dark' : 'light';

document.querySelectorAll('*[data-contao--color-scheme-target="outlet"]').forEach((el) => {
el.dataset.colorScheme = prefersDark() ? 'dark' : 'light';
});
};

window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', setColorScheme);
setColorScheme();

export default class extends Controller {
static targets = ['label'];
static targets = ['label', 'outlet'];

static values = {
i18n: {
Expand All @@ -41,6 +45,10 @@ export default class extends Controller {
this.matchMedia.removeEventListener('change', this.setLabel);
}

outletTargetConnected() {
setColorScheme();
}

toggle (e) {
e.preventDefault();

Expand Down
200 changes: 200 additions & 0 deletions core-bundle/assets/styles/component/choices.pcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
@import "choices.js/public/assets/styles/choices.css";

.choices {
display: inline-block;
position: relative;
width: 100%;
height: 30px;
font-size: 0.875rem;
box-sizing: border-box;
border: 1px solid var(--form-border);
border-radius: 2px;
background: var(--form-bg)
url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNmE2YTZjIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMS41IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Im02IDkgNiA2IDYtNiIvPjwvc3ZnPgo=")
right -16px top 3px no-repeat;
text-align: left;

&:dir(rtl) {
text-align: right;
}

&[disabled] {
color: var(--form-text-disabled);
background-color: var(--form-bg-disabled);
border: 1px solid var(--form-border-disabled);
cursor: not-allowed;
}

&:focus {
outline: revert;
}

&.is-focused,
&.is-open {
z-index: 4; /* Higher z-index than tinymce and sticky-footer */
}

.choices__inner {
background-color: transparent;
padding: 0;
border: 0 none;
border-radius: revert;
min-height: unset;
}

.choices__input {
background: transparent;
padding: 3px 6px;
margin: 0;
width: auto;
line-height: 17px;
}

.choices__placeholder {
opacity: 1;
}

.choices__heading {
color: var(--text, #222);
cursor: default;
border: 0 none;
padding: 3px 0 3px 6px;
font-size: revert;
}

.choices__button {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjMjIyIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==");
padding-bottom: 2px;
border-left-color: var(--select-border) !important;
}

.choices__list--dropdown,
.choices__list[aria-expanded] {
background: var(--form-bg, #fff);
border: 1px solid var(--form-border, #aaa);
word-break: break-word;
left: -1px;

.choices__item,
.choices__list:before {
padding: 3px 6px;
font-size: 13px;
}

.choices__item--selectable,
.choices__list[aria-expanded] .choices__item--selectable {
&.is-highlighted {
background: var(--select-highlighted);
color: #fff;
}
}
}

.choices__list {
&:has(> .choices__group) > .choices__item {
padding-left: 20px;
}

&[data-placeholder]:before {
content: attr(data-placeholder);
display: block;
cursor: default;
opacity: 0.7;
padding: 3px 6px;
font-size: 13px;
}
}

/* Select one */
&[data-type="select-one"] {
&::after {
display: none;
}

.choices__item {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding: 7px 0;
}

.choices__inner {
padding: 0;

&::after {
content: "";
height: 12px;
width: 12px;
background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNmE2YTZjIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMS41IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Im02IDkgNiA2IDYtNiIvPjwvc3ZnPgo=")
center center no-repeat;
position: absolute;
right: 6px;
top: 8px;
transform: none;
pointer-events: none;
}

&:dir(rtl)::after {
left: 6px;
right: auto;
}

.choices__list {
box-sizing: border-box;
padding: 0 22px 0 6px;

&:dir(rtl) {
padding: 0 6px 0 22px;
}
}
}

.choices__input {
height: auto;
padding: 3px 6px;
border-bottom: 1px solid var(--form-border, #aaa);
background: transparent;
}

.choices__button {
display: none;
visibility: hidden;
}

&.is-open .choices__inner::after {
margin-top: -0.5px;
transform: rotate(180deg);
}
}

/* Select multiple */
&[data-type="select-multiple"] {
min-height: 30px;
height: unset;

.choices__inner .choices__item {
border-radius: 2px;
background-color: var(--select-bg);
color: var(--text, #222);
border: 1px solid var(--select-border);
line-height: 16px;
padding: 2px 6px 2px 6px;
margin: 3px 0 3px 3px;
position: relative;
top: 0;
word-break: revert;
vertical-align: baseline;
font-weight: inherit;
}
}
}

/* Revert the cross in light mode */
:host([data-color-scheme="light"]) .choices {
&[data-type="select-multiple"],
&[data-type="select-on F438 e"] {
.choices__button {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjMjIyIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==");
}
}
}
1 change: 0 additions & 1 deletion core-bundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
"cmsig/seal-symfony-bundle": "^0.6",
"contao-components/ace": "^1.8",
"contao-components/altcha": "^1.0",
"contao-components/choices": "^11.0",
"contao-components/colorbox": "^1.6",
"contao-components/contao": "^9.1",
"contao-components/datepicker": "^3.0",
Expand Down
4 changes: 1 addition & 3 deletions core-bundle/contao/templates/backend/be_main.html5
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="<?= $this->language ?>" data-controller="contao--scroll-offset" data-action="store-scroll-offset@window->contao--scroll-offset#store contao--tinymce:editor-loaded->contao--scroll-offset#scrollToWidgetError turbo:render@document->contao--scroll-offset#restore">
<html lang="<?= $this->language ?>" data-controller="contao--scroll-offset contao--color-scheme" data-action="store-scroll-offset@window->contao--scroll-offset#store contao--tinymce:editor-loaded->contao--scroll-offset#scrollToWidgetError turbo:render@document->contao--scroll-offset#restore">
<head>

<?php $this->block('head'); ?>
Expand All @@ -12,7 +12,6 @@
<meta name="referrer" content="origin">
<?php $this->endblock(); ?>

<link rel="stylesheet" href="<?= $this->asset('css/choices.min.css', 'contao-components/choices') ?>">
<link rel="stylesheet" href="<?= $this->asset('css/simplemodal.min.css', 'contao-components/simplemodal') ?>">
<link rel="stylesheet" href="<?= $this->asset('css/datepicker.min.css', 'contao-components/datepicker') ?>">
<link rel="stylesheet" href="<?= $this->asset('backend.css', 'system/themes/'.$this->theme) ?>">
Expand All @@ -21,7 +20,6 @@

<script><?= $this->getLocaleString() ?></script>
<script src="<?= $this->asset('js/mootools.min.js', 'contao-components/mootools') ?>"></script>
<script src="<?= $this->asset('js/choices.min.js', 'contao-components/choices') ?>"></script>
<script src="<?= $this->asset('js/simplemodal.min.js', 'contao-components/simplemodal') ?>"></script>
<script src="<?= $this->asset('js/datepicker.min.js', 'contao-components/datepicker') ?>"></script>
<script src="<?= $this->asset('js/tinymce.min.js', 'contao-components/tinymce4') ?>"></script>
Expand Down
Loading
0