Loading app/Resources/js/admin.ts +2 −0 Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ import "./modules/video-clip-previewer"; import VideoClipBuilder from "./modules/VideoClipBuilder"; import "./modules/xml-editor"; import "@patternfly/elements/pf-tabs/pf-tabs.js"; import FieldArray from "./modules/FieldArray"; Dropdown(); Tooltip(); Loading @@ -39,3 +40,4 @@ PublishMessageWarning(); HotKeys(); ValidateFileSize(); VideoClipBuilder(); FieldArray(); app/Resources/js/modules/FieldArray.ts 0 → 100644 +159 −0 Original line number Diff line number Diff line import Tooltip from "./Tooltip"; const FieldArray = (): void => { const fieldArrays: NodeListOf<HTMLElement> = document.querySelectorAll("[data-field-array]"); for (let i = 0; i < fieldArrays.length; i++) { const fieldArray = fieldArrays[i]; const fieldArrayContainer = fieldArray.querySelector( "[data-field-array-container]" ); const items: NodeListOf<HTMLElement> = fieldArray.querySelectorAll( "[data-field-array-item]" ); const addButton = fieldArray.querySelector( "button[data-field-array-add]" ) as HTMLButtonElement; const deleteButtons: NodeListOf<HTMLButtonElement> = fieldArray.querySelectorAll("[data-field-array-delete]"); deleteButtons.forEach((deleteBtn) => { deleteBtn.addEventListener("click", (e) => { e.preventDefault(); deleteBtn.blur(); fieldArrayContainer ?.querySelector( `[data-field-array-item="${deleteBtn.dataset.fieldArrayDelete}"]` ) ?.remove(); }); }); // create base element to clone const baseItem = items[0].cloneNode(true) as HTMLElement; const elements: NodeListOf<HTMLFormElement> = baseItem.querySelectorAll( "input, select, textarea" ); elements.forEach((element) => { element.value = ""; }); if (fieldArrayContainer && addButton) { addButton.addEventListener("click", (event) => { event.preventDefault(); const newItem = baseItem.cloneNode(true) as HTMLElement; const deleteBtn: HTMLButtonElement | null = newItem.querySelector( "button[data-field-array-delete]" ); if (deleteBtn) { deleteBtn.addEventListener("click", () => { deleteBtn.blur(); newItem.remove(); }); fieldArrayContainer.appendChild(newItem); newItem.scrollIntoView({ behavior: "auto", block: "center", inline: "center", }); // reload tooltip module for showing remove button label Tooltip(); // focus to first form element if mouse click if (event.screenX !== 0 && event.screenY !== 0) { const elements: NodeListOf<HTMLFormElement> = newItem.querySelectorAll("input, select, textarea"); if (elements.length > 0) { elements[0].focus(); } } } }); const updateIndexes = () => { // get last child item to set item count const items: NodeListOf<HTMLElement> = fieldArrayContainer.querySelectorAll("[data-field-array-item]"); let itemIndex = 0; items.forEach((item) => { const itemNumber: HTMLElement | null = item.querySelector( "[data-field-array-number]" ); if (itemNumber) { itemNumber.innerHTML = "#"; const indexNum = itemIndex + 1; if (item.dataset.fieldArrayItem !== itemIndex.toString()) { item.classList.add("motion-safe:animate-single-pulse"); setTimeout(() => { item.classList.remove("motion-safe:animate-single-pulse"); itemNumber.innerHTML = indexNum.toString(); }, 300); } else { itemNumber.innerHTML = indexNum.toString(); } } item.dataset.fieldArrayItem = itemIndex.toString(); const deleteBtn = item.querySelector( "button[data-field-array-delete]" ) as HTMLButtonElement | null; if (deleteBtn) { deleteBtn.dataset.fieldArrayDelete = itemIndex.toString(); } const itemElements: NodeListOf<HTMLFormElement> = item.querySelectorAll("input, select, textarea"); itemElements.forEach((element) => { const label: HTMLLabelElement | null = item.querySelector( `label[for="${element.id}"]` ); const elementID = element.name.replace( /(.*\[)\d+?(\].*)/g, `$1${itemIndex}$2` ); if (label) { label.htmlFor = elementID; } element.id = elementID; element.name = elementID; }); itemIndex++; }); }; // add mutation observer to run index updates when field array // items are added or removed const callback = function (mutationList: MutationRecord[]) { for (const mutation of mutationList) { if (mutation.type === "childList") { updateIndexes(); } } }; const observer = new MutationObserver(callback); observer.observe(fieldArrayContainer, { childList: true }); } } }; export default FieldArray; app/Resources/styles/custom.css +19 −0 Original line number Diff line number Diff line @layer base { html { scroll-behavior: smooth; } .form-helper { @apply text-skin-muted; } } @layer components { .post-content { & a { Loading Loading @@ -78,4 +88,13 @@ #facc15 20px ); } .divide-fieldset-y > :not([hidden], legend) ~ :not([hidden], legend) { @apply pt-4; --tw-divide-y-reverse: 0; border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); border-bottom-width: calc(1px * var(--tw-divide-y-reverse)); } } app/Resources/styles/radioBtn.css +16 −6 Original line number Diff line number Diff line @layer components { .form-radio-btn { @apply absolute mt-3 ml-3 border-contrast border-3 text-accent-base; @apply absolute right-4 top-4 border-contrast border-3 text-accent-base; &:focus { @apply ring-accent; } &:checked { @apply ring-2 ring-contrast; & + label { @apply text-accent-contrast bg-accent-base; @apply text-accent-hover bg-base border-accent-base shadow-none; } & + label .form-radio-btn-description { @apply text-accent-base; } } & + label { @apply inline-flex items-center py-2 pl-8 pr-2 text-sm font-semibold rounded-lg cursor-pointer border-contrast bg-elevated border-3; @apply h-full w-full inline-flex flex-col items-start py-3 px-4 text-sm font-bold rounded-lg cursor-pointer border-contrast bg-elevated border-3 transition-all; box-shadow: 2px 2px 0 hsl(var(--color-border-contrast)); } & + label span { @apply pr-8; } color: hsl(var(--color-text-muted)); & + label .form-radio-btn-description { @apply font-normal text-xs text-skin-muted text-balance; } } } app/Views/Components/Forms/Checkbox.php +20 −2 Original line number Diff line number Diff line Loading @@ -17,6 +17,8 @@ class Checkbox extends FormComponent protected string $hint = ''; protected string $helper = ''; protected bool $isChecked = false; #[Override] Loading @@ -37,10 +39,26 @@ class Checkbox extends FormComponent 'slot' => $this->hint, ]))->render(); $this->mergeClass('inline-flex items-center'); $this->mergeClass('inline-flex items-start gap-x-2'); $helperText = ''; if ($this->helper !== '') { $helperId = $this->name . 'Help'; $helperText = (new Helper([ 'id' => $helperId, 'slot' => $this->helper, 'class' => '-mt-1', ]))->render(); $this->attributes['aria-describedby'] = $helperId; } return <<<HTML <label {$this->getStringifiedAttributes()}>{$checkboxInput}<span class="ml-2">{$this->slot}{$hint}</span></label> <label {$this->getStringifiedAttributes()}>{$checkboxInput} <div class="flex flex-col"> <span>{$this->slot}{$hint}</span> {$helperText} </div> </label> HTML; } } Loading
app/Resources/js/admin.ts +2 −0 Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ import "./modules/video-clip-previewer"; import VideoClipBuilder from "./modules/VideoClipBuilder"; import "./modules/xml-editor"; import "@patternfly/elements/pf-tabs/pf-tabs.js"; import FieldArray from "./modules/FieldArray"; Dropdown(); Tooltip(); Loading @@ -39,3 +40,4 @@ PublishMessageWarning(); HotKeys(); ValidateFileSize(); VideoClipBuilder(); FieldArray();
app/Resources/js/modules/FieldArray.ts 0 → 100644 +159 −0 Original line number Diff line number Diff line import Tooltip from "./Tooltip"; const FieldArray = (): void => { const fieldArrays: NodeListOf<HTMLElement> = document.querySelectorAll("[data-field-array]"); for (let i = 0; i < fieldArrays.length; i++) { const fieldArray = fieldArrays[i]; const fieldArrayContainer = fieldArray.querySelector( "[data-field-array-container]" ); const items: NodeListOf<HTMLElement> = fieldArray.querySelectorAll( "[data-field-array-item]" ); const addButton = fieldArray.querySelector( "button[data-field-array-add]" ) as HTMLButtonElement; const deleteButtons: NodeListOf<HTMLButtonElement> = fieldArray.querySelectorAll("[data-field-array-delete]"); deleteButtons.forEach((deleteBtn) => { deleteBtn.addEventListener("click", (e) => { e.preventDefault(); deleteBtn.blur(); fieldArrayContainer ?.querySelector( `[data-field-array-item="${deleteBtn.dataset.fieldArrayDelete}"]` ) ?.remove(); }); }); // create base element to clone const baseItem = items[0].cloneNode(true) as HTMLElement; const elements: NodeListOf<HTMLFormElement> = baseItem.querySelectorAll( "input, select, textarea" ); elements.forEach((element) => { element.value = ""; }); if (fieldArrayContainer && addButton) { addButton.addEventListener("click", (event) => { event.preventDefault(); const newItem = baseItem.cloneNode(true) as HTMLElement; const deleteBtn: HTMLButtonElement | null = newItem.querySelector( "button[data-field-array-delete]" ); if (deleteBtn) { deleteBtn.addEventListener("click", () => { deleteBtn.blur(); newItem.remove(); }); fieldArrayContainer.appendChild(newItem); newItem.scrollIntoView({ behavior: "auto", block: "center", inline: "center", }); // reload tooltip module for showing remove button label Tooltip(); // focus to first form element if mouse click if (event.screenX !== 0 && event.screenY !== 0) { const elements: NodeListOf<HTMLFormElement> = newItem.querySelectorAll("input, select, textarea"); if (elements.length > 0) { elements[0].focus(); } } } }); const updateIndexes = () => { // get last child item to set item count const items: NodeListOf<HTMLElement> = fieldArrayContainer.querySelectorAll("[data-field-array-item]"); let itemIndex = 0; items.forEach((item) => { const itemNumber: HTMLElement | null = item.querySelector( "[data-field-array-number]" ); if (itemNumber) { itemNumber.innerHTML = "#"; const indexNum = itemIndex + 1; if (item.dataset.fieldArrayItem !== itemIndex.toString()) { item.classList.add("motion-safe:animate-single-pulse"); setTimeout(() => { item.classList.remove("motion-safe:animate-single-pulse"); itemNumber.innerHTML = indexNum.toString(); }, 300); } else { itemNumber.innerHTML = indexNum.toString(); } } item.dataset.fieldArrayItem = itemIndex.toString(); const deleteBtn = item.querySelector( "button[data-field-array-delete]" ) as HTMLButtonElement | null; if (deleteBtn) { deleteBtn.dataset.fieldArrayDelete = itemIndex.toString(); } const itemElements: NodeListOf<HTMLFormElement> = item.querySelectorAll("input, select, textarea"); itemElements.forEach((element) => { const label: HTMLLabelElement | null = item.querySelector( `label[for="${element.id}"]` ); const elementID = element.name.replace( /(.*\[)\d+?(\].*)/g, `$1${itemIndex}$2` ); if (label) { label.htmlFor = elementID; } element.id = elementID; element.name = elementID; }); itemIndex++; }); }; // add mutation observer to run index updates when field array // items are added or removed const callback = function (mutationList: MutationRecord[]) { for (const mutation of mutationList) { if (mutation.type === "childList") { updateIndexes(); } } }; const observer = new MutationObserver(callback); observer.observe(fieldArrayContainer, { childList: true }); } } }; export default FieldArray;
app/Resources/styles/custom.css +19 −0 Original line number Diff line number Diff line @layer base { html { scroll-behavior: smooth; } .form-helper { @apply text-skin-muted; } } @layer components { .post-content { & a { Loading Loading @@ -78,4 +88,13 @@ #facc15 20px ); } .divide-fieldset-y > :not([hidden], legend) ~ :not([hidden], legend) { @apply pt-4; --tw-divide-y-reverse: 0; border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); border-bottom-width: calc(1px * var(--tw-divide-y-reverse)); } }
app/Resources/styles/radioBtn.css +16 −6 Original line number Diff line number Diff line @layer components { .form-radio-btn { @apply absolute mt-3 ml-3 border-contrast border-3 text-accent-base; @apply absolute right-4 top-4 border-contrast border-3 text-accent-base; &:focus { @apply ring-accent; } &:checked { @apply ring-2 ring-contrast; & + label { @apply text-accent-contrast bg-accent-base; @apply text-accent-hover bg-base border-accent-base shadow-none; } & + label .form-radio-btn-description { @apply text-accent-base; } } & + label { @apply inline-flex items-center py-2 pl-8 pr-2 text-sm font-semibold rounded-lg cursor-pointer border-contrast bg-elevated border-3; @apply h-full w-full inline-flex flex-col items-start py-3 px-4 text-sm font-bold rounded-lg cursor-pointer border-contrast bg-elevated border-3 transition-all; box-shadow: 2px 2px 0 hsl(var(--color-border-contrast)); } & + label span { @apply pr-8; } color: hsl(var(--color-text-muted)); & + label .form-radio-btn-description { @apply font-normal text-xs text-skin-muted text-balance; } } }
app/Views/Components/Forms/Checkbox.php +20 −2 Original line number Diff line number Diff line Loading @@ -17,6 +17,8 @@ class Checkbox extends FormComponent protected string $hint = ''; protected string $helper = ''; protected bool $isChecked = false; #[Override] Loading @@ -37,10 +39,26 @@ class Checkbox extends FormComponent 'slot' => $this->hint, ]))->render(); $this->mergeClass('inline-flex items-center'); $this->mergeClass('inline-flex items-start gap-x-2'); $helperText = ''; if ($this->helper !== '') { $helperId = $this->name . 'Help'; $helperText = (new Helper([ 'id' => $helperId, 'slot' => $this->helper, 'class' => '-mt-1', ]))->render(); $this->attributes['aria-describedby'] = $helperId; } return <<<HTML <label {$this->getStringifiedAttributes()}>{$checkboxInput}<span class="ml-2">{$this->slot}{$hint}</span></label> <label {$this->getStringifiedAttributes()}>{$checkboxInput} <div class="flex flex-col"> <span>{$this->slot}{$hint}</span> {$helperText} </div> </label> HTML; } }