diff --git a/.changeset/hip-bikes-perform.md b/.changeset/hip-bikes-perform.md new file mode 100644 index 000000000..828231528 --- /dev/null +++ b/.changeset/hip-bikes-perform.md @@ -0,0 +1,7 @@ +--- +'@finsweet/attributes-date': major +'@finsweet/attributes': patch +'@finsweet/attributes-utils': patch +--- + +feat: new fs-date Attribute diff --git a/packages/attributes/package.json b/packages/attributes/package.json index 0d30c4071..941ab7fd4 100644 --- a/packages/attributes/package.json +++ b/packages/attributes/package.json @@ -47,6 +47,7 @@ "@finsweet/attributes-consent": "workspace:*", "@finsweet/attributes-copyclip": "workspace:*", "@finsweet/attributes-countitems": "workspace:*", + "@finsweet/attributes-date": "workspace:*", "@finsweet/attributes-displayvalues": "workspace:*", "@finsweet/attributes-favcustom": "workspace:*", "@finsweet/attributes-formsubmit": "workspace:*", diff --git a/packages/attributes/src/load.ts b/packages/attributes/src/load.ts index 307d45301..e26e7d31f 100644 --- a/packages/attributes/src/load.ts +++ b/packages/attributes/src/load.ts @@ -50,6 +50,10 @@ export const loadAttribute = async (solution: FsAttributeKey) => { return import('@finsweet/attributes-countitems'); } + case 'date': { + return import('@finsweet/attributes-date'); + } + case 'displayvalues': { return import('@finsweet/attributes-displayvalues'); } diff --git a/packages/attributes/tests/date.spec.ts b/packages/attributes/tests/date.spec.ts new file mode 100644 index 000000000..e6cc7a8e8 --- /dev/null +++ b/packages/attributes/tests/date.spec.ts @@ -0,0 +1,21 @@ +import { expect, test } from '@playwright/test'; + +import { waitAttributeLoaded } from './utils'; + +test.beforeEach(async ({ page }) => { + await page.goto('http://fs-attributes.webflow.io/date'); +}); + +test.describe('fs-date', () => { + test('Sets the correct value to the elements', async ({ page }) => { + await waitAttributeLoaded(page, 'date'); + + const element1 = page.getByTestId('1'); + const element2 = page.getByTestId('2'); + const element3 = page.getByTestId('3'); + + expect(await element1.innerText()).toBe('viernes, 12 de abril de 2024'); + expect(await element2.innerText()).toBe('miƩrcoles, 24 de abril de 2024'); + expect(await element3.innerText()).toBe('miƩrcoles, 17 de abril de 2024'); + }); +}); diff --git a/packages/date/README.md b/packages/date/README.md new file mode 100644 index 000000000..8939d56eb --- /dev/null +++ b/packages/date/README.md @@ -0,0 +1,3 @@ +# `fs-date` Attribute + +Date formatting for Webflow elements. diff --git a/packages/date/package.json b/packages/date/package.json new file mode 100644 index 000000000..e8a916157 --- /dev/null +++ b/packages/date/package.json @@ -0,0 +1,23 @@ +{ + "name": "@finsweet/attributes-date", + "version": "0.0.0", + "description": "Date formatting for Webflow elements.", + "private": true, + "type": "module", + "types": "src/index.ts", + "scripts": { + "lint": "eslint ./src && prettier --check ./src", + "lint:fix": "eslint ./src --fix && prettier --write ./src", + "check": "tsc --noEmit" + }, + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts" + } + }, + "dependencies": { + "@finsweet/attributes-utils": "workspace:*", + "chrono-node": "^2.7.5" + } +} diff --git a/packages/date/src/factory.ts b/packages/date/src/factory.ts new file mode 100644 index 000000000..3767c1110 --- /dev/null +++ b/packages/date/src/factory.ts @@ -0,0 +1,41 @@ +import { getAttribute } from './utils/selectors'; + +/** + * Inits the date element. + * @param dateElement + */ +export const initDateElement = async (dateElement: HTMLElement) => { + const value = getAttribute(dateElement, 'value'); + const parse = getAttribute(dateElement, 'parse'); + + let date: Date | undefined | null; + + if (value) { + const chrono = await import('chrono-node'); + + date = chrono.parseDate(value); + } else if (parse) { + const chrono = await import('chrono-node'); + + const referenceDate = new Date(dateElement.innerText); + + const ref = isNaN(referenceDate.getTime()) ? new Date() : referenceDate; + + date = chrono.parseDate(parse, ref); + } + + if (!date) { + date = new Date(dateElement.innerText); + } + + // Get the date on the element + const locale = getAttribute(dateElement, 'locale'); + const year = getAttribute(dateElement, 'year', true); + const weekday = getAttribute(dateElement, 'weekday', true); + const month = getAttribute(dateElement, 'month', true); + const day = getAttribute(dateElement, 'day', true); + + const formatted = new Intl.DateTimeFormat(locale, { year, weekday, month, day }).format(date); + + dateElement.innerText = formatted; +}; diff --git a/packages/date/src/index.ts b/packages/date/src/index.ts new file mode 100644 index 000000000..76864f6e2 --- /dev/null +++ b/packages/date/src/index.ts @@ -0,0 +1,3 @@ +export { version } from '../package.json'; +export { init } from './init'; +export { ELEMENTS, SETTINGS } from './utils/constants'; diff --git a/packages/date/src/init.ts b/packages/date/src/init.ts new file mode 100644 index 000000000..1aa5c7903 --- /dev/null +++ b/packages/date/src/init.ts @@ -0,0 +1,18 @@ +import { type FsAttributeInit, waitWebflowReady } from '@finsweet/attributes-utils'; + +import { initDateElement } from './factory'; +import { queryAllElements } from './utils/selectors'; + +/** + * Inits the attribute. + */ +export const init: FsAttributeInit = async () => { + await waitWebflowReady(); + + // localize date elements + const dateElements = queryAllElements('date'); + + await Promise.all(dateElements.map(initDateElement)); + + return {}; +}; diff --git a/packages/date/src/utils/constants.ts b/packages/date/src/utils/constants.ts new file mode 100644 index 000000000..7537c9975 --- /dev/null +++ b/packages/date/src/utils/constants.ts @@ -0,0 +1,63 @@ +import { type AttributeElements, type AttributeSettings } from '@finsweet/attributes-utils'; + +export const ELEMENTS = [ + /** + * Defines a date element. + */ + 'date', +] as const satisfies AttributeElements; + +export const SETTINGS = { + /** + * The locale used to format. + */ + locale: { + key: 'locale', + }, + + /** + * The representation of the year. + */ + year: { + key: 'year', + values: { numeric: 'numeric', '2-digit': '2-digit' }, + }, + + /** + * The representation of the weekday. + */ + weekday: { + key: 'weekday', + values: { long: 'long', short: 'short', narrow: 'narrow' }, + }, + + /** + * The representation of the month. + */ + month: { + key: 'month', + values: { numeric: 'numeric', '2-digit': '2-digit', long: 'long', short: 'short', narrow: 'narrow' }, + }, + + /** + * The representation of the day. + */ + day: { + key: 'day', + values: { numeric: 'numeric', '2-digit': '2-digit' }, + }, + + /** + * Defines a specific date value. + */ + value: { + key: 'value', + }, + + /** + * Defines a specific date value. + */ + parse: { + key: 'parse', + }, +} as const satisfies AttributeSettings; diff --git a/packages/date/src/utils/selectors.ts b/packages/date/src/utils/selectors.ts new file mode 100644 index 000000000..c787554ce --- /dev/null +++ b/packages/date/src/utils/selectors.ts @@ -0,0 +1,15 @@ +import { DATE_ATTRIBUTE, generateSelectors } from '@finsweet/attributes-utils'; + +import { ELEMENTS, SETTINGS } from './constants'; + +export const { + getAttribute, + getClosestElement, + getElementSelector, + getInstance, + getSettingAttributeName, + getSettingSelector, + hasAttributeValue, + queryAllElements, + queryElement, +} = generateSelectors(DATE_ATTRIBUTE, ELEMENTS, SETTINGS); diff --git a/packages/date/tsconfig.json b/packages/date/tsconfig.json new file mode 100644 index 000000000..4082f16a5 --- /dev/null +++ b/packages/date/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/packages/template/api/examples.ts b/packages/template/api/examples.ts deleted file mode 100644 index ce6cd2742..000000000 --- a/packages/template/api/examples.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { AttributeExamples } from '@finsweet/attributes-utils'; - -const examples: AttributeExamples = []; - -export default examples; diff --git a/packages/template/api/schema.ts b/packages/template/api/schema.ts deleted file mode 100644 index 3b22a2860..000000000 --- a/packages/template/api/schema.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { AttributeSchema } from '@finsweet/attributes-utils'; - -const schema: AttributeSchema = { - elements: [], - settings: [], -}; - -export default schema; diff --git a/packages/utils/src/constants/attributes.ts b/packages/utils/src/constants/attributes.ts index c232caf44..fa419316e 100644 --- a/packages/utils/src/constants/attributes.ts +++ b/packages/utils/src/constants/attributes.ts @@ -46,6 +46,8 @@ export const COPY_CLIP_ATTRIBUTE = 'copyclip'; export const COUNT_ITEMS_ATTRIBUTE = 'countitems'; +export const DATE_ATTRIBUTE = 'date'; + export const DISPLAY_VALUES_ATTRIBUTE = 'displayvalues'; export const DOCS_ATTRIBUTE = 'docs'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4930d572..0e7d8deb8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,7 +21,7 @@ importers: version: 2.27.1 '@finsweet/eslint-config': specifier: ^2.0.5 - version: 2.0.5(@typescript-eslint/eslint-plugin@5.59.11)(@typescript-eslint/parser@5.59.11)(eslint-config-prettier@8.8.0)(eslint-plugin-prettier@4.2.1)(eslint-plugin-simple-import-sort@12.0.0)(eslint@8.42.0)(prettier@2.8.8)(typescript@5.1.3) + version: 2.0.5(@typescript-eslint/eslint-plugin@5.59.11)(@typescript-eslint/parser@5.59.11)(eslint-config-prettier@8.8.0)(eslint-plugin-prettier@4.2.1)(eslint-plugin-simple-import-sort@10.0.0)(eslint@8.42.0)(prettier@2.8.8)(typescript@5.1.3) '@finsweet/tsconfig': specifier: ^1.3.2 version: 1.3.2(typescript@5.1.3) @@ -116,6 +116,9 @@ importers: '@finsweet/attributes-countitems': specifier: workspace:* version: link:../countitems + '@finsweet/attributes-date': + specifier: workspace:* + version: link:../date '@finsweet/attributes-displayvalues': specifier: workspace:* version: link:../displayvalues @@ -305,6 +308,15 @@ importers: specifier: workspace:* version: link:../utils + packages/date: + dependencies: + '@finsweet/attributes-utils': + specifier: workspace:* + version: link:../utils + chrono-node: + specifier: ^2.7.5 + version: 2.7.5 + packages/displayvalues: dependencies: '@finsweet/attributes-utils': @@ -1209,7 +1221,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@finsweet/eslint-config@2.0.5(@typescript-eslint/eslint-plugin@5.59.11)(@typescript-eslint/parser@5.59.11)(eslint-config-prettier@8.8.0)(eslint-plugin-prettier@4.2.1)(eslint-plugin-simple-import-sort@12.0.0)(eslint@8.42.0)(prettier@2.8.8)(typescript@5.1.3): + /@finsweet/eslint-config@2.0.5(@typescript-eslint/eslint-plugin@5.59.11)(@typescript-eslint/parser@5.59.11)(eslint-config-prettier@8.8.0)(eslint-plugin-prettier@4.2.1)(eslint-plugin-simple-import-sort@10.0.0)(eslint@8.42.0)(prettier@2.8.8)(typescript@5.1.3): resolution: {integrity: sha512-twlHPSSS1CuIXOSCnIWETqOXD3OiLmouPR/Qc5Io0xDecMXqy1K8rB87X5SLkgDGGSxPv09R/oKAAtWl6Lbkqw==} peerDependencies: '@typescript-eslint/eslint-plugin': ^5.59.9 @@ -1226,7 +1238,7 @@ packages: eslint: 8.42.0 eslint-config-prettier: 8.8.0(eslint@8.42.0) eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.8.0)(eslint@8.42.0)(prettier@2.8.8) - eslint-plugin-simple-import-sort: 12.0.0(eslint@8.42.0) + eslint-plugin-simple-import-sort: 10.0.0(eslint@8.42.0) prettier: 2.8.8 typescript: 5.1.3 dev: true @@ -1728,6 +1740,13 @@ packages: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true + /chrono-node@2.7.5: + resolution: {integrity: sha512-VJWqFN5rWmXVvXAxOD4i0jX8Tb4cLswaslyaAFhxM45zNXPsZleygPbgiaYBD7ORb9fj07zBgJb0Q6eKL+0iJg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + dayjs: 1.11.10 + dev: false + /ci-info@3.8.0: resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} engines: {node: '>=8'} @@ -2101,8 +2120,8 @@ packages: prettier-linter-helpers: 1.0.0 dev: true - /eslint-plugin-simple-import-sort@12.0.0(eslint@8.42.0): - resolution: {integrity: sha512-8o0dVEdAkYap0Cn5kNeklaKcT1nUsa3LITWEuFk3nJifOoD+5JQGoyDUW2W/iPWwBsNBJpyJS9y4je/BgxLcyQ==} + /eslint-plugin-simple-import-sort@10.0.0(eslint@8.42.0): + resolution: {integrity: sha512-AeTvO9UCMSNzIHRkg8S6c3RPy5YEwKWSQPx3DYghLedo2ZQxowPFLGDN1AZ2evfg6r6mjBSZSLxLFsWSu3acsw==} peerDependencies: eslint: '>=5.0.0' dependencies: