diff --git a/pom.xml b/pom.xml index f890a23..0a74c66 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.flowingcode.vaadin.addons day-of-week-selector-addon - 1.1.1-SNAPSHOT + 1.2.0-SNAPSHOT Day of Week Selector Add-on Day of Week Selector Add-on for Vaadin Flow https://www.flowingcode.com/en/open-source/ diff --git a/src/main/java/com/flowingcode/vaadin/addons/dayofweekselector/DayOfWeekSelector.java b/src/main/java/com/flowingcode/vaadin/addons/dayofweekselector/DayOfWeekSelector.java index 522180f..faa5284 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/dayofweekselector/DayOfWeekSelector.java +++ b/src/main/java/com/flowingcode/vaadin/addons/dayofweekselector/DayOfWeekSelector.java @@ -19,13 +19,19 @@ */ package com.flowingcode.vaadin.addons.dayofweekselector; +import com.vaadin.flow.component.ClientCallable; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Tag; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.customfield.CustomField; import com.vaadin.flow.component.datepicker.DatePicker.DatePickerI18n; import com.vaadin.flow.component.dependency.CssImport; -import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.dependency.JsModule; +import com.vaadin.flow.dom.Element; + import java.time.DayOfWeek; +import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.EnumSet; @@ -33,15 +39,23 @@ import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; -import java.util.stream.Stream; /** * Shows the days of the week so they can be selected. */ @SuppressWarnings("serial") @CssImport("./styles/fc-days-of-week-selector-styles.css") +@JsModule("./src/fc-days-of-week-selector.ts") +@Tag("fc-days-of-week-selector") public class DayOfWeekSelector extends CustomField> { + public static final class CssProperties { + public static final String OVERFLOW_ICON_SIZE = "--fc-days-of-week-selector-overflow-icon-size"; + + private CssProperties() { + } + } + private static class DayOfWeekButton extends Button { private static final String CLASS_NAME = "fc-days-of-week-selector-button"; @@ -59,6 +73,7 @@ public DayOfWeek getDayOfWeek() { return dayOfWeek; } + @ClientCallable private void toggleState() { setState(!state); } @@ -74,29 +89,13 @@ private void setState(boolean state) { } - private HorizontalLayout buttonsLayout; + private List dayButtons = new ArrayList<>(); /** * Creates a new instance of {@code DayOfWeekSelector}. */ public DayOfWeekSelector() { - getStyle().set("padding", "var(--lumo-space-m)"); - this.setWidthFull(); - - buttonsLayout = new HorizontalLayout(); - buttonsLayout.add(new DayOfWeekButton(DayOfWeek.SUNDAY, "S")); - buttonsLayout.add(new DayOfWeekButton(DayOfWeek.MONDAY, "M")); - buttonsLayout.add(new DayOfWeekButton(DayOfWeek.TUESDAY, "T")); - buttonsLayout.add(new DayOfWeekButton(DayOfWeek.WEDNESDAY, "W")); - buttonsLayout.add(new DayOfWeekButton(DayOfWeek.THURSDAY, "T")); - buttonsLayout.add(new DayOfWeekButton(DayOfWeek.FRIDAY, "F")); - buttonsLayout.add(new DayOfWeekButton(DayOfWeek.SATURDAY, "S")); - - buttonsLayout.setClassName("fc-days-of-week-selector-buttons-layout"); - - getButtons().forEach(button -> button.addClickListener(ev -> updateValue())); - add(buttonsLayout); - clear(); + initDefaultDayButtons(); } /** @@ -142,32 +141,35 @@ protected boolean valueEquals(Set value1, Set value2) { return value1 != value2 && Objects.equals(value1, value2); } - private Stream getButtons() { - return buttonsLayout.getChildren() - .filter(DayOfWeekButton.class::isInstance) - .map(DayOfWeekButton.class::cast); - } - @Override protected Set generateModelValue() { - return getButtons().filter(DayOfWeekButton::getState).map(DayOfWeekButton::getDayOfWeek) - .collect(Collectors.toCollection(this::getEmptyValue)); + return dayButtons.stream().filter(DayOfWeekButton::getState).map(DayOfWeekButton::getDayOfWeek) + .collect(Collectors.toCollection(this::getEmptyValue)); } @Override protected void setPresentationValue(Set newPresentationValue) { - getButtons() - .forEach(button -> button.setState(newPresentationValue.contains(button.getDayOfWeek()))); + dayButtons + .forEach(button -> button.setState(newPresentationValue.contains(button.getDayOfWeek()))); } @Override public void setReadOnly(boolean readOnly) { getElement().setProperty("readonly", readOnly); getElement().setAttribute("readonly", readOnly); - getButtons().forEach(button -> button.setEnabled(!readOnly)); + dayButtons.forEach(button -> { + button.setEnabled(!readOnly); + if (readOnly) { + button.addClassName("readOnly"); + } else { + button.removeClassName("readOnly"); + } + }); } - /** Sets the value of this object. */ + /** + * Sets the value of this object. + */ public void setValue(DayOfWeek first, DayOfWeek... rest) { setValue(EnumSet.of(first, rest)); } @@ -202,14 +204,14 @@ public void setWeekDaysShort(List weekdaysShort) { for (DayOfWeek day : DayOfWeek.values()) { int index = day.getValue() % 7; String text = weekdaysShort.get(index); - getButtons().filter(button -> button.getDayOfWeek() == day) - .forEach(button -> button.setText(text)); + dayButtons.stream().filter(button -> button.getDayOfWeek() == day) + .forEach(button -> button.setText(text)); } } /** * Sets the tooltips of the week days, starting from {@code sun} and ending on {@code sat}. - * + * * @param weekdaysTooltip the tooltips of the week days */ public void setWeekDaysTooltip(List weekdaysTooltip) { @@ -218,8 +220,8 @@ public void setWeekDaysTooltip(List weekdaysTooltip) { for (DayOfWeek day : DayOfWeek.values()) { int index = day.getValue() % 7; String text = weekdaysTooltip.get(index); - getButtons().filter(button -> button.getDayOfWeek() == day) - .forEach(button -> button.setTooltipText(text)); + dayButtons.stream().filter(button -> button.getDayOfWeek() == day) + .forEach(button -> button.setTooltipText(text)); } } @@ -233,14 +235,68 @@ public void setWeekDaysTooltip(List weekdaysTooltip) { * @throws IllegalArgumentException if firstDayOfWeek is invalid */ public void setFirstDayOfWeek(DayOfWeek first) { - DayOfWeekButton[] buttons = getButtons().toArray(DayOfWeekButton[]::new); + DayOfWeekButton[] buttons = dayButtons.toArray(DayOfWeekButton[]::new); if (buttons[0].dayOfWeek != first) { + var sortedButtons = new ArrayList(); Arrays.sort(buttons, Comparator.comparing(DayOfWeekButton::getDayOfWeek)); - buttonsLayout.removeAll(); - for (int i = 0; i < 7; i++) { - buttonsLayout.add(buttons[(i + first.getValue() - 1) % 7]); + for (int i = 0; i < buttons.length; i++) { + sortedButtons.add(buttons[(i + first.getValue() - 1) % buttons.length]); } + setDayButtons(sortedButtons); } } + /** + * Sets the icon for the overflow menu trigger. + * + * @param icon the icon component, not {@code null} + * @throws NullPointerException if {@code icon} is {@code null} + */ + public void setOverflowIcon(Component icon) { + Objects.requireNonNull(icon, "icon cannot be null"); + + // Remove any existing slotted icon before appending the new one + getElement().getChildren() + .filter(el -> "overflowIcon".equals(el.getAttribute("slot"))) + .findFirst() + .ifPresent(Element::removeFromParent); + + icon.setClassName("overflow-icon"); + // Assign icon to the slot + icon.getElement().setAttribute("slot", "overflowIcon"); + getElement().appendChild(icon.getElement()); + } + + private void initDefaultDayButtons() { + setDayButtons(List.of(new DayOfWeekButton(DayOfWeek.SUNDAY, "S"), + new DayOfWeekButton(DayOfWeek.MONDAY, "M"), + new DayOfWeekButton(DayOfWeek.TUESDAY, "T"), + new DayOfWeekButton(DayOfWeek.WEDNESDAY, "W"), + new DayOfWeekButton(DayOfWeek.THURSDAY, "T"), + new DayOfWeekButton(DayOfWeek.FRIDAY, "F"), + new DayOfWeekButton(DayOfWeek.SATURDAY, "S") + )); + } + + private void setDayButtons(List buttons) { + this.clearDayButtons(); + this.dayButtons = buttons; + buttons.forEach(button -> button.addClickListener(ev -> updateValue())); + this.addDayButtons(); + } + + private void addDayButtons() { + this.dayButtons.forEach(button -> { + button.getElement().setAttribute("slot", "daysOfWeek"); + this.getElement().appendChild(button.getElement()); + }); + } + + private void clearDayButtons() { + this.dayButtons.forEach(button -> { + button.getElement().removeAttribute("slot"); + button.removeFromParent(); + }); + } + } diff --git a/src/main/resources/META-INF/frontend/styles/fc-days-of-week-selector-styles.css b/src/main/resources/META-INF/frontend/styles/fc-days-of-week-selector-styles.css index 0979d3d..77f4e67 100644 --- a/src/main/resources/META-INF/frontend/styles/fc-days-of-week-selector-styles.css +++ b/src/main/resources/META-INF/frontend/styles/fc-days-of-week-selector-styles.css @@ -35,18 +35,26 @@ vaadin-button.fc-days-of-week-selector-button[theme~='primary'][disabled]::part( opacity: 1; } -vaadin-custom-field[readonly] vaadin-button.fc-days-of-week-selector-button[disabled] { +vaadin-button.fc-days-of-week-selector-button[disabled].readOnly{ color: var(--lumo-secondary-text-color); background: transparent; } -vaadin-custom-field[readonly] vaadin-button.fc-days-of-week-selector-button[theme~='primary'][disabled] { +vaadin-button.fc-days-of-week-selector-button[theme~='primary'][disabled].readOnly { border: var(--vaadin-input-field-readonly-border, 1px dashed var(--lumo-contrast-30pct)); } +vaadin-button.fc-days-of-week-selector-button[focus-ring]{ + box-shadow: none; +} + vaadin-horizontal-layout.fc-days-of-week-selector-buttons-layout { width: 100%; justify-content: center; flex-wrap: wrap; padding: 0 10px; } + +vaadin-context-menu-overlay vaadin-context-menu-list-box::part(items) { + padding: 0 var(--lumo-space-xs); +} diff --git a/src/main/resources/META-INF/resources/frontend/src/fc-days-of-week-selector.ts b/src/main/resources/META-INF/resources/frontend/src/fc-days-of-week-selector.ts new file mode 100644 index 0000000..239e247 --- /dev/null +++ b/src/main/resources/META-INF/resources/frontend/src/fc-days-of-week-selector.ts @@ -0,0 +1,235 @@ +/*- + * #%L + * Day of Week Selector Add-on + * %% + * Copyright (C) 2025 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +import {ResizeMixin} from '@vaadin/component-base/src/resize-mixin.js'; +import '@vaadin/context-menu'; +import type {ContextMenuItem} from '@vaadin/context-menu'; +import {ThemableMixin} from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js'; +import {css, html, LitElement} from 'lit'; +import {customElement, property, query, queryAssignedNodes, state} from 'lit/decorators.js'; +import {Button} from "@vaadin/button"; + +@customElement('fc-days-of-week-selector') +export class DaysOfWeekSelector extends ResizeMixin(ThemableMixin(LitElement)) { + + @query('[part~="overflow-badge"]') + _overflowBadge!: HTMLElement + + @queryAssignedNodes({slot: 'daysOfWeek', flatten: true}) + _daysOfWeek!: Array; + + @query('[part~="container"]') + _container!: HTMLElement; + + @property() + theme: string = ''; + + @property() + label: string = ''; + + @state() + private _overflowItems: ContextMenuItem[] = []; + + static styles = [ + css` + :host { + display: block; + width: 100%; + } + + [hidden] { + display: none; + } + + [part="container"] { + position: relative; + display: flex; + width: 100%; + flex-wrap: nowrap; + overflow: hidden; + align-items: center; + } + + [part="overflow-badge"] { + line-height: 0.5em; + } + + :host(:not([has-label])) [part='label'] { + display: none; + } + + [part="label"] { + align-self: flex-start; + color: var(--badge-list-label-color); + font-weight: var(--badge-list-label-font-weight); + font-size: var(--badge-list-label-font-size); + margin-left: var(--badge-list-label-margin-left); + transition: color 0.2s; + line-height: inherit; + padding-right: 1em; + padding-bottom: 0.5em; + padding-top: 0.25em; + margin-top: -0.25em; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + position: relative; + max-width: 100%; + box-sizing: border-box; + } + + ::slotted([slot="overflowIcon"]) { + --lumo-icon-size-m: var(--fc-days-of-week-selector-overflow-icon-size, 1em); + } + + /* Style the fallback inside the slot */ + vaadin-icon.overflow-icon { + --lumo-icon-size-m: var(--fc-days-of-week-selector-overflow-icon-size, 0.75em); + } + + ` + ]; + + static get is() { + return 'fc-days-of-week-selector'; + } + + _set_theme(theme: string) { + this.theme = theme; + } + + /** + * Override getter from `ResizeMixin` to observe parent. + * + * @protected + * @override + */ + get _observeParent() { + return true; + } + + /** + * Implement callback from `ResizeMixin` to update badges + * and detect whether to show or hide the overflow badge. + * + * @protected + * @override + */ + _onResize() { + this._updateOnOverflow(); + } + + /** + * Returns overflow button width. + * + * @private + */ + private _getOverflowButtonWidth() { + let overflowWidth = 0; + const badge = this._overflowBadge; + if (badge) { + const wasHidden = badge.hasAttribute('hidden'); + if (wasHidden) { + badge.removeAttribute('hidden'); + } + overflowWidth = badge.getBoundingClientRect().width || 0; + if (wasHidden) { + badge.setAttribute('hidden', ''); + } + } + return overflowWidth; + } + + private _updateOnOverflow() { + const containerWidth = this.getBoundingClientRect().width; + let usedWidth = 0; + const overflowButtons: Node[] = []; + const overflowButtonWidth = this._getOverflowButtonWidth(); + + for (const button of this._daysOfWeek) { + button.style.display = ''; + const width = button.getBoundingClientRect().width; + + if (usedWidth + width > containerWidth - overflowButtonWidth) { + const clonedButton = this._cloneButton(button); + overflowButtons.push(clonedButton); + + // hide button from slot + button.style.display = 'none'; + } else { + usedWidth += width; + } + } + + // Update context menu items from hidden buttons + this._overflowItems = overflowButtons.map(button => ({ + component: button, + })); + + // Update badges visibility + this._overflowBadge.toggleAttribute('hidden', this._overflowItems.length < 1); + } + + private _cloneButton(originalButton: Button) { + const clonedButton = originalButton.cloneNode(true); + clonedButton._originalButton = originalButton; + clonedButton.removeAttribute("slot"); + clonedButton.style.display = ''; + clonedButton.addEventListener('click', ev => { + // Update server side button component state + clonedButton._originalButton.$server.toggleState(); + if (clonedButton.getAttribute("theme")) { + clonedButton.removeAttribute("theme") + } else { + clonedButton.setAttribute("theme", "primary"); + } + // Notify value change to DayOfWeekSelector component + this.dispatchEvent(new CustomEvent('change', {bubbles: true})); + }); + return clonedButton; + } + + render() { + return html` +
+ +
+
+ + + + +
+ `; + } + + update(_changedProperties: Map) { + if (_changedProperties.has('label')) { + this.toggleAttribute('has-label', this.label != null); + } + super.update(_changedProperties); + } + +} \ No newline at end of file diff --git a/src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java b/src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java index 2f0554d..eb2d644 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java +++ b/src/test/java/com/flowingcode/vaadin/addons/DemoLayout.java @@ -19,10 +19,12 @@ */ package com.flowingcode.vaadin.addons; +import com.vaadin.flow.component.dependency.CssImport; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.router.RouterLayout; @SuppressWarnings("serial") +@CssImport("/styles/shared-styles.css") public class DemoLayout extends Div implements RouterLayout { public DemoLayout() { diff --git a/src/test/java/com/flowingcode/vaadin/addons/dayofweekselector/DayOfWeekSelectorDemo.java b/src/test/java/com/flowingcode/vaadin/addons/dayofweekselector/DayOfWeekSelectorDemo.java index 5b727fe..69939c2 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/dayofweekselector/DayOfWeekSelectorDemo.java +++ b/src/test/java/com/flowingcode/vaadin/addons/dayofweekselector/DayOfWeekSelectorDemo.java @@ -7,9 +7,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,29 +20,54 @@ package com.flowingcode.vaadin.addons.dayofweekselector; import com.flowingcode.vaadin.addons.demo.DemoSource; +import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; +import com.vaadin.flow.theme.lumo.LumoIcon; + import java.time.DayOfWeek; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; @DemoSource -@PageTitle("Demo") @SuppressWarnings("serial") +@PageTitle("Demo") @Route(value = "day-of-week-selector/demo", layout = DayOfWeekSelectorDemoView.class) -public class DayOfWeekSelectorDemo extends Div { +public class DayOfWeekSelectorDemo extends VerticalLayout { public DayOfWeekSelectorDemo() { - DayOfWeekSelector selector1 = - new DayOfWeekSelector("Enabled", DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY); - add(selector1); + addClassName("demo"); + + DayOfWeekSelector selector1 = new DayOfWeekSelector("Enabled", DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY); + selector1.setWidthFull(); + Button updateValueButton = new Button("Set value to SUN-MON-SAT"); + updateValueButton.addClickListener(e -> { + selector1.setValue(DayOfWeek.SUNDAY, DayOfWeek.MONDAY, DayOfWeek.FRIDAY, DayOfWeek.SATURDAY); + }); + VerticalLayout content=new VerticalLayout(selector1, updateValueButton); + content.setPadding(false); + add(content); + + DayOfWeekSelector selectorOverflow = new DayOfWeekSelector("Overflow", DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY); + selectorOverflow.setWidth("200px"); + selectorOverflow.setOverflowIcon(LumoIcon.ANGLE_RIGHT.create()); + selectorOverflow.getStyle().set(DayOfWeekSelector.CssProperties.OVERFLOW_ICON_SIZE, "1em"); + + Div valueContent = new Div(); + Consumer> valueRenderer = + value -> valueContent.setText("Value: " + value.stream().map(Enum::name).collect(Collectors.joining(" - "))); + add(new Div(selectorOverflow, valueContent)); + valueRenderer.accept(selectorOverflow.getValue()); + selectorOverflow.addValueChangeListener(e -> valueRenderer.accept(e.getValue())); - DayOfWeekSelector selector2 = - new DayOfWeekSelector("Read-only", DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY); + DayOfWeekSelector selector2 = new DayOfWeekSelector("Read-only", DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY); selector2.setReadOnly(true); add(selector2); - DayOfWeekSelector selector3 = - new DayOfWeekSelector("Disabled", DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY); + DayOfWeekSelector selector3 = new DayOfWeekSelector("Disabled", DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY); selector3.setEnabled(false); add(selector3); } diff --git a/src/test/resources/META-INF/frontend/styles/shared-styles.css b/src/test/resources/META-INF/frontend/styles/shared-styles.css index 8b8413b..9db9d63 100644 --- a/src/test/resources/META-INF/frontend/styles/shared-styles.css +++ b/src/test/resources/META-INF/frontend/styles/shared-styles.css @@ -18,3 +18,13 @@ * #L% */ /*Demo styles*/ +html { +} + +.demo { + gap: var(--lumo-space-m); +} + +fc-days-of-week-selector::part(label) { + font-weight: bold; +} \ No newline at end of file