diff --git a/package-lock.json b/package-lock.json index 5d0ae585..46976a36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "dayjs": "~1.11.7", "embla-carousel": "~7.0.9", "embla-carousel-autoplay": "~7.0.9", + "pikaday": "~1.8.2", "pinia": "~2.0.30", "the-new-css-reset": "~1.8.4", "vue": "~3.2.47", @@ -36,6 +37,7 @@ "@types/cookiebot-sdk": "~2.43.4", "@types/google.maps": "~3.51.0", "@types/node": "~18.13.0", + "@types/pikaday": "~1.7.6", "@types/resize-observer-browser": "~0.1.6", "@typescript-eslint/eslint-plugin": "~5.52.0", "@typescript-eslint/parser": "~5.52.0", @@ -8258,6 +8260,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/pikaday": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/@types/pikaday/-/pikaday-1.7.9.tgz", + "integrity": "sha512-DZ6oG5WoYX9Es2VTgvJpzxKMTU4WE0jq9noVSs0VTdmCREYv/AXSaybQj0Sm9Q9XO1pmFrdfBCpL4OVRX/QkAg==", + "dev": true, + "dependencies": { + "moment": ">=2.29.2" + } + }, "node_modules/@types/pretty-hrtime": { "version": "1.0.1", "dev": true, @@ -10959,7 +10970,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001429", + "version": "1.0.30001574", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001574.tgz", + "integrity": "sha512-BtYEK4r/iHt/txm81KBudCUcTy7t+s9emrIaHqjYurQ10x71zJ5VQ9x1dYPcz/b+pKSp4y/v1xSI67A+LzpNyg==", "dev": true, "funding": [ { @@ -10969,9 +10982,12 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/capture-exit": { "version": "2.0.0", @@ -19701,6 +19717,15 @@ "ufo": "^1.0.1" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/move-concurrently": { "version": "1.0.1", "dev": true, @@ -21032,6 +21057,11 @@ "node": ">=6" } }, + "node_modules/pikaday": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/pikaday/-/pikaday-1.8.2.tgz", + "integrity": "sha512-TNtsE+34BIax3WtkB/qqu5uepV1McKYEgvL3kWzU7aqPCpMEN6rBF3AOwu4WCwAealWlBGobXny/9kJb49C1ew==" + }, "node_modules/pinia": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.30.tgz", @@ -34287,6 +34317,15 @@ "version": "5.0.3", "dev": true }, + "@types/pikaday": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/@types/pikaday/-/pikaday-1.7.9.tgz", + "integrity": "sha512-DZ6oG5WoYX9Es2VTgvJpzxKMTU4WE0jq9noVSs0VTdmCREYv/AXSaybQj0Sm9Q9XO1pmFrdfBCpL4OVRX/QkAg==", + "dev": true, + "requires": { + "moment": ">=2.29.2" + } + }, "@types/pretty-hrtime": { "version": "1.0.1", "dev": true @@ -36259,7 +36298,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001429", + "version": "1.0.30001574", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001574.tgz", + "integrity": "sha512-BtYEK4r/iHt/txm81KBudCUcTy7t+s9emrIaHqjYurQ10x71zJ5VQ9x1dYPcz/b+pKSp4y/v1xSI67A+LzpNyg==", "dev": true }, "capture-exit": { @@ -42361,6 +42402,12 @@ "ufo": "^1.0.1" } }, + "moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "dev": true + }, "move-concurrently": { "version": "1.0.1", "dev": true, @@ -43294,6 +43341,11 @@ "version": "4.0.1", "dev": true }, + "pikaday": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/pikaday/-/pikaday-1.8.2.tgz", + "integrity": "sha512-TNtsE+34BIax3WtkB/qqu5uepV1McKYEgvL3kWzU7aqPCpMEN6rBF3AOwu4WCwAealWlBGobXny/9kJb49C1ew==" + }, "pinia": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.30.tgz", diff --git a/package.json b/package.json index 0d1af2d0..ff2cdef8 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "dayjs": "~1.11.7", "embla-carousel": "~7.0.9", "embla-carousel-autoplay": "~7.0.9", + "pikaday": "~1.8.2", "pinia": "~2.0.30", "the-new-css-reset": "~1.8.4", "vue": "~3.2.47", @@ -88,6 +89,7 @@ "@types/cookiebot-sdk": "~2.43.4", "@types/google.maps": "~3.51.0", "@types/node": "~18.13.0", + "@types/pikaday": "~1.7.6", "@types/resize-observer-browser": "~0.1.6", "@typescript-eslint/eslint-plugin": "~5.52.0", "@typescript-eslint/parser": "~5.52.0", diff --git a/src/assets/icons.svg b/src/assets/icons.svg index 95ed0f1f..4f5323ad 100644 --- a/src/assets/icons.svg +++ b/src/assets/icons.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/icons/i-arrow-2--left.svg b/src/assets/icons/i-arrow-2--left.svg new file mode 100644 index 00000000..f56853c7 --- /dev/null +++ b/src/assets/icons/i-arrow-2--left.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/i-arrow-2--right.svg b/src/assets/icons/i-arrow-2--right.svg new file mode 100644 index 00000000..f4f433b7 --- /dev/null +++ b/src/assets/icons/i-arrow-2--right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/i-calendar.svg b/src/assets/icons/i-calendar.svg new file mode 100644 index 00000000..50a1b5f3 --- /dev/null +++ b/src/assets/icons/i-calendar.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/components/c-date-picker.vue b/src/components/c-date-picker.vue new file mode 100644 index 00000000..ed630045 --- /dev/null +++ b/src/components/c-date-picker.vue @@ -0,0 +1,657 @@ + + + + + + + + + + + + + + + {{ $t(range ? 'c-date-picker.calendarRangeTitle' : 'c-date-picker.calendarTitle') }} + + + + + + + + + + + + + + + + + diff --git a/src/compositions/form-states.ts b/src/compositions/form-states.ts index 37d65cd7..bb27c292 100644 --- a/src/compositions/form-states.ts +++ b/src/compositions/form-states.ts @@ -6,7 +6,7 @@ import { ref, } from 'vue'; -export enum FieldStates { +export enum FieldState { Default = 'default', Success = 'success', Info = 'info', @@ -15,7 +15,7 @@ export enum FieldStates { } type StateModifiers = { - state: FieldStates; + state: FieldState; active: boolean; focus: boolean; hover: boolean; @@ -35,7 +35,7 @@ export const withProps = () => ({ // eslint-disable-line -- TODO: did not know h * Form states for class names (default, error, success, warning, info) */ state: { - type: String as PropType, + type: String as PropType, default: 'default', validator: (value: string): boolean => [ 'error', @@ -50,7 +50,7 @@ export const withProps = () => ({ // eslint-disable-line -- TODO: did not know h /** * Defines the reactive properties which can be used for form elements */ -const formStates = (inputState: Ref): FormStates => { +const formStates = (inputState: Ref): FormStates => { const active = ref(false); const focus = ref(false); const hover = ref(false); @@ -68,20 +68,20 @@ const formStates = (inputState: Ref): FormStates => { /** * Holds a boolean if the form element has default state. */ - const hasDefaultState: ComputedRef = computed(() => inputState.value === FieldStates.Default); + const hasDefaultState: ComputedRef = computed(() => inputState.value === FieldState.Default); /** * Holds a string containing the icon name matching the current form element state. */ const stateIcon: ComputedRef = computed(() => { switch (inputState.value) { - case FieldStates.Error: + case FieldState.Error: return 'i-error'; - case FieldStates.Success: + case FieldState.Success: return 'i-check'; - case FieldStates.Info: + case FieldState.Info: return 'i-info'; default: diff --git a/src/elements/e-date.vue b/src/elements/e-date.vue new file mode 100644 index 00000000..7438e793 --- /dev/null +++ b/src/elements/e-date.vue @@ -0,0 +1,172 @@ + + + + + + + + + + + diff --git a/src/elements/e-input.vue b/src/elements/e-input.vue index 1fe85518..ac2c2f80 100644 --- a/src/elements/e-input.vue +++ b/src/elements/e-input.vue @@ -51,6 +51,7 @@ type Setup = FormStates & { input: Ref; slot: Ref; + slotStart: Ref; } type Data = { @@ -147,16 +148,23 @@ }, }, - emits: ['update:modelValue', 'focus', 'blur', 'enter'], + emits: { + 'update:modelValue': (payload: string) => typeof payload === 'string', + 'focus': () => true, + 'blur': () => true, + 'enter': () => true, + }, setup(props): Setup { const input = ref(); const slot = ref(); + const slotStart = ref(); return { ...useFormStates(toRefs(props).state), input, slot, + slotStart, }; }, @@ -192,7 +200,16 @@ }; }, }, - // watch: {}, + watch: { + /** + * Updates internal value if model value got changed from parent. + */ + modelValue(value: string) { + if (value !== this.internalValue) { + this.internalValue = value; + } + }, + }, // beforeCreate() {}, // created() {}, @@ -290,6 +307,14 @@ this.input.style.paddingRight = `${slotWidth + 10}px`; } } + + if (this.slotStart) { + const slotWidth = this.slotStart.clientWidth; + + if (this.input) { + this.input.style.paddingLeft = `${slotWidth + 15}px`; + } + } }, /** @@ -419,6 +444,7 @@ right: variables.$spacing--5; display: flex; transform: translateY(-50%); + pointer-events: none; } &__slot { diff --git a/src/plugins/consent/c-consent-gatekeeper.vue b/src/plugins/consent/c-consent-gatekeeper.vue index bb7dedf5..320af0fb 100644 --- a/src/plugins/consent/c-consent-gatekeeper.vue +++ b/src/plugins/consent/c-consent-gatekeeper.vue @@ -13,12 +13,12 @@ import { ConsentGroup, showConsentDialog, consentState } from '@/plugins/consent/index'; import eButton from '@/elements/e-button.vue'; - interface Setup { + type Setup = { showConsentDialog: typeof showConsentDialog; consentState: typeof consentState; } - // interface Data {} + // type Data = {} /** * TODO: if you want to use this component make sure to register and enable cookiebot.com first. diff --git a/src/plugins/dayjs.ts b/src/plugins/dayjs.ts index 470b6bfc..2a335432 100644 --- a/src/plugins/dayjs.ts +++ b/src/plugins/dayjs.ts @@ -7,6 +7,16 @@ import isBetween from 'dayjs/plugin/isBetween'; import { Plugin } from 'vue'; import de from 'dayjs/locale/de'; +/** + * Holds the date format templates used with dayjs. + */ +export enum DateFormat { + DD_MM_YYYY = 'DD.MM.YYYY', + DD_MM_YYYY_HH_mm = 'DD.MM.YYYY HH:mm', + dddd_DD_MMMM_YYYY = 'dddd [/] DD. MMMM YYYY', + dddd_DD_MMMM_YYYY_HH_mm = 'dddd [/] DD. MMMM YYYY HH:mm' +} + const plugin: Plugin = { install(app) { dayjs.locale('de-ch', de); @@ -15,7 +25,6 @@ const plugin: Plugin = { dayjs.extend(isSameOrAfter); dayjs.extend(isBetween); - // eslint-disable-next-line no-param-reassign app.config.globalProperties.$dayjs = dayjs; }, }; diff --git a/src/setup/scss/variables/_font.scss b/src/setup/scss/variables/_font.scss index 17d0ea50..7c76580d 100644 --- a/src/setup/scss/variables/_font.scss +++ b/src/setup/scss/variables/_font.scss @@ -14,3 +14,9 @@ $font-size--16: 16px; $font-size--18: 18px; $font-size--24: 24px; $font-size--30: 30px; + +// Line height +$line-height--18: 18px; +$line-height--20: 20px; +$line-height--25: 25px; +$line-height--30: 30px; diff --git a/src/setup/scss/variables/_z-index.scss b/src/setup/scss/variables/_z-index.scss index 45b82001..7b3fe111 100644 --- a/src/setup/scss/variables/_z-index.scss +++ b/src/setup/scss/variables/_z-index.scss @@ -4,6 +4,7 @@ $z-index: ( navigation: 10, dropdown: 15, focusItem: 30, + datePicker: 700, globalNotification: 900, modal: 1000, ); diff --git a/src/styleguide/routes/components/r-consent.vue b/src/styleguide/routes/components/r-consent.vue index 5eb02ebf..f7ac4fea 100644 --- a/src/styleguide/routes/components/r-consent.vue +++ b/src/styleguide/routes/components/r-consent.vue @@ -13,11 +13,11 @@ import { defineComponent } from 'vue'; import { cConsentGatekeeper, ConsentGroup } from '@/plugins/consent'; - interface Setup { + type Setup = { ConsentGroup: typeof ConsentGroup; } - // interface Data {} + // type Data = {} /** * Showcase for the consent plugin. diff --git a/src/styleguide/routes/sandbox/r-forms.vue b/src/styleguide/routes/sandbox/r-forms.vue index 1f585a2d..442c4d2d 100644 --- a/src/styleguide/routes/sandbox/r-forms.vue +++ b/src/styleguide/routes/sandbox/r-forms.vue @@ -27,7 +27,7 @@ + + + + + + + + + + + + Submit @@ -176,7 +192,9 @@ import eCheckbox from '@/elements/e-checkbox.vue'; import eTextarea from '@/elements/e-textarea.vue'; import eButton from '@/elements/e-button.vue'; - import { FieldStates } from '@/compositions/form-states'; + import { FieldState } from '@/compositions/form-states'; + import eDate from '@/elements/e-date.vue'; + import cDatePicker from '@/components/c-date-picker.vue'; type SelectItem = { label: string; @@ -186,7 +204,7 @@ type Setup = { v$: Ref; formRef: Ref; - FieldStates: typeof FieldStates; + FieldState: typeof FieldState; } type Data = { @@ -200,6 +218,9 @@ topics: string[]; frequency: string; businessFields: string[]; + date: Date; + startDate: Date; + endDate: Date; }; mock: { businessFields: SelectItem[]; @@ -221,6 +242,8 @@ eCheckbox, eTextarea, eButton, + cDatePicker, + eDate, }, setup(): Setup { @@ -230,7 +253,7 @@ // eslint-disable-next-line id-length v$: useVuelidate(), formRef, - FieldStates, + FieldState, }; }, @@ -246,6 +269,9 @@ topics: ['technics'], frequency: 'twiceAMonth', businessFields: [], + date: new Date(), + startDate: new Date(), + endDate: new Date(), }, mock: { languages: [ diff --git a/src/translations/de.json b/src/translations/de.json index 013b0c9b..94830c64 100644 --- a/src/translations/de.json +++ b/src/translations/de.json @@ -49,6 +49,15 @@ "itemsEmpty": "Keine Ergebnisse.", "searchTitle": "Suche" }, + "c-date-picker": { + "calendarRangeTitle": "Datumsbereich auswählen", + "calendarTitle": "Datum auswählen", + "inputEndLabel": "Ende", + "inputLabel": "Datum", + "inputStartLabel": "Start", + "nextMonth": "Nächster Monat", + "previousMonth": "Vorheriger Monat" + }, "c-slider":{ "navigationPrevious": "Zurück", "navigationNext": "Vorwärts"