From 48bb7dc9ddeb4a4a267f51979c90fdcc6a3681ac Mon Sep 17 00:00:00 2001 From: Alex Iglesias Date: Sun, 25 Apr 2021 19:05:06 +0200 Subject: [PATCH] First commit --- .npmignore | 7 +++ README.md | 36 ++++-------- animations/fade.ts | 47 +++++++++++++++ animations/index.ts | 1 + bin/build.js | 17 ------ components/Debug.ts | 17 ++++++ components/DisplayController.ts | 66 +++++++++++++++++++++ components/Interaction.ts | 79 ++++++++++++++++++++++++++ components/index.ts | 3 + dist/index.js | 2 - helpers/cloneNode.ts | 7 +++ helpers/extractCommaSeparatedValues.ts | 32 +++++++++++ helpers/findTextNode.ts | 17 ++++++ helpers/getDistanceFromTop.ts | 13 +++++ helpers/getFormFieldValue.ts | 23 ++++++++ helpers/getObjectEntries.ts | 10 ++++ helpers/getObjectKeys.ts | 8 +++ helpers/index.ts | 14 +++++ helpers/isScrollable.ts | 11 ++++ helpers/isVisible.ts | 8 +++ helpers/queryElement.ts | 19 +++++++ helpers/selectInputElement.ts | 26 +++++++++ helpers/throwError.ts | 10 ++++ helpers/wait.ts | 7 +++ helpers/writableStore.ts | 39 +++++++++++++ package-lock.json | 10 +--- package.json | 17 ++---- src/index.ts | 23 -------- tsconfig.json | 20 +++---- type-guards/index.ts | 2 + type-guards/isFormField.ts | 10 ++++ type-guards/isKeyOf.ts | 12 ++++ types/Entry.ts | 5 ++ types/FormField.ts | 3 + types/Instance.ts | 6 ++ types/Webflow.ts | 12 ++++ types/index.ts | 4 ++ webflow/getSiteId.ts | 6 ++ webflow/index.ts | 2 + webflow/restartWebflow.ts | 18 ++++++ 40 files changed, 572 insertions(+), 97 deletions(-) create mode 100644 .npmignore create mode 100644 animations/fade.ts create mode 100644 animations/index.ts delete mode 100644 bin/build.js create mode 100644 components/Debug.ts create mode 100644 components/DisplayController.ts create mode 100644 components/Interaction.ts create mode 100644 components/index.ts delete mode 100644 dist/index.js create mode 100644 helpers/cloneNode.ts create mode 100644 helpers/extractCommaSeparatedValues.ts create mode 100644 helpers/findTextNode.ts create mode 100644 helpers/getDistanceFromTop.ts create mode 100644 helpers/getFormFieldValue.ts create mode 100644 helpers/getObjectEntries.ts create mode 100644 helpers/getObjectKeys.ts create mode 100644 helpers/index.ts create mode 100644 helpers/isScrollable.ts create mode 100644 helpers/isVisible.ts create mode 100644 helpers/queryElement.ts create mode 100644 helpers/selectInputElement.ts create mode 100644 helpers/throwError.ts create mode 100644 helpers/wait.ts create mode 100644 helpers/writableStore.ts delete mode 100644 src/index.ts create mode 100644 type-guards/index.ts create mode 100644 type-guards/isFormField.ts create mode 100644 type-guards/isKeyOf.ts create mode 100644 types/Entry.ts create mode 100644 types/FormField.ts create mode 100644 types/Instance.ts create mode 100644 types/Webflow.ts create mode 100644 types/index.ts create mode 100644 webflow/getSiteId.ts create mode 100644 webflow/index.ts create mode 100644 webflow/restartWebflow.ts diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..e22816d --- /dev/null +++ b/.npmignore @@ -0,0 +1,7 @@ +node_modules/ +.eslintrc.js +.npmignore +.prettierignore +.prettierrc +package-lock.json +tsconfig.json \ No newline at end of file diff --git a/README.md b/README.md index e9510c2..64e649b 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,14 @@ -# Finsweet Developer Starter +# Typescript Utils -A starter template for both Client & Power projects. This project contains: +Typescript utils for custom Webflow projects. This project contains different categories of utils that can be used in any project. -- Preconfigured development tools: +All utils are fully tree shakeable and strongly typed. - - [Typescript](https://www.typescriptlang.org/): A superset of Javascript that adds an additional layer of Typings, bringing more security and efficiency to the written code. - - [Prettier](https://prettier.io/): Code formating that assures consistency across all Finsweet's projects. - - [ESLint](https://eslint.org/): Code linting that enforces industries' best practises. - - [ESBuild](https://esbuild.github.io/): Javascript bundler that compiles, bundles and minifies the original Typescript files. +Categories: -- Learning resources for new team members: - - - [Learning Typescript](#typescript): Everything you need to start confidently coding with Typescript. - - [Coding best practises](#best-practises): Learn how to write clean and semantic code that is easily understandable by your teammates. - - [Setting up your development environment](#dev-environment): Learn how to set up VSCode and to use the development tools included in this repository - - [Development workflows](#dev-workflows): See examples of workflows from your local environment to Webflow. - - [Git](#git): Learn how to collaborate with your teammates' code! - -## How to start - -The quickest way to start developing a new project is by [creating a new repository from this template](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-from-a-template#creating-a-repository-from-a-template). - -After that, open the new repository in your terminal and install the NPM packages by running: - -```bash -npm install -``` - -If this is your first time using this template, check out the [Resources](https://github.com/finsweet/developer-starter/tree/master/resources) section on this `README` and the boilerplate in the [`src/index.ts`](https://github.com/finsweet/developer-starter/blob/master/src/index.ts) file. Otherwise, feel free to remove them! +- Animations: `@finsweet/ts-utils/animations` +- Components: `@finsweet/ts-utils/components` +- Helpers: `@finsweet/ts-utils/helpers` +- Type Guards: `@finsweet/ts-utils/type-guards` +- Types: `@finsweet/ts-utils/types` +- Webflow: `@finsweet/ts-utils/webflow` diff --git a/animations/fade.ts b/animations/fade.ts new file mode 100644 index 0000000..fead00c --- /dev/null +++ b/animations/fade.ts @@ -0,0 +1,47 @@ +/** + * Fade in an element + * @param element + * @param display Display property, flex by default + * @returns An awaitable promise + */ +export const fadeIn = (element: HTMLElement, display = 'flex'): Promise => { + return new Promise((resolve) => { + element.style.opacity = '0'; + element.style.display = display; + + (function fade() { + const currentOpacity = parseFloat(element.style.opacity); + if (currentOpacity >= 1) { + resolve(); + return; + } + + const newOpacity = currentOpacity + 0.1; + element.style.opacity = newOpacity.toString(); + + requestAnimationFrame(fade); + })(); + }); +}; + +/** + * Fade out an element + * @param element + * @returns An awaitable promise + */ +export const fadeOut = (element: HTMLElement): Promise => { + return new Promise((resolve) => { + element.style.opacity = '1'; + + (function fade() { + const currentOpacity = parseFloat(element.style.opacity); + const newOpacity = currentOpacity - 0.1; + element.style.opacity = newOpacity.toString(); + + if (newOpacity <= 0) { + element.style.display = 'none'; + resolve(); + } else requestAnimationFrame(fade); + })(); + }); +}; diff --git a/animations/index.ts b/animations/index.ts new file mode 100644 index 0000000..0c83441 --- /dev/null +++ b/animations/index.ts @@ -0,0 +1 @@ +export { fadeIn, fadeOut } from './fade'; diff --git a/bin/build.js b/bin/build.js deleted file mode 100644 index 0468a3d..0000000 --- a/bin/build.js +++ /dev/null @@ -1,17 +0,0 @@ -// Import ESBuild -const { buildSync, BuildOptions } = require('esbuild'); // eslint-disable-line - -/** @type {BuildOptions} */ -const defaultSettings = { - bundle: true, - minify: true, - sourcemap: false, - outdir: 'dist', - target: 'es6', -}; - -// Files building -buildSync({ - ...defaultSettings, - entryPoints: ['src/index.ts'], -}); diff --git a/components/Debug.ts b/components/Debug.ts new file mode 100644 index 0000000..e03357e --- /dev/null +++ b/components/Debug.ts @@ -0,0 +1,17 @@ +// Constants +const alertTypes = ['info', 'error'] as const; + +export default class Debug { + private static alertsActivated = false; + + public static activateAlerts(): void { + this.alertsActivated = true; + } + + public static alert(text: string, type: 'info'): void; + public static alert(text: string, type: 'error'): T; + public static alert(text: string, type: typeof alertTypes[number]): T | void { + if (this.alertsActivated) window.alert(text); + if (type === 'error') throw new Error(text); + } +} diff --git a/components/DisplayController.ts b/components/DisplayController.ts new file mode 100644 index 0000000..a05be39 --- /dev/null +++ b/components/DisplayController.ts @@ -0,0 +1,66 @@ +import Interaction, { InteractionParams } from './Interaction'; +import { fadeIn, fadeOut } from '../animations/fade'; +import isVisible from '../helpers/isVisible'; + +// Types +export interface DisplayControllerParams { + element: HTMLElement; + interaction?: InteractionParams; + displayProperty?: 'block' | 'flex' | 'grid' | 'inline-block' | 'inline' | 'none'; + noTransition?: boolean; +} + +export default class DisplayController { + private readonly displayProperty; + private readonly interaction; + private readonly element; + private readonly noTransition; + private visible; + + constructor({ element, interaction, noTransition }: Omit); + constructor({ element, displayProperty, noTransition }: Omit); + constructor({ element, interaction, displayProperty, noTransition }: DisplayControllerParams) { + this.element = element; + this.noTransition = noTransition; + this.visible = isVisible(element); + this.displayProperty = displayProperty || 'block'; + + if (interaction) { + const { element, duration } = interaction; + this.interaction = new Interaction({ element, duration }); + } + } + + /** + * @returns If the element is visible + */ + public isVisible = (): boolean => this.visible; + + /** + * Displays the element + * @returns An awaitable promise + */ + public async display(): Promise { + if (this.visible) return; + + if (this.interaction) await this.interaction.trigger('first'); + else if (this.noTransition) this.element.style.display = this.displayProperty; + else await fadeIn(this.element, this.displayProperty); + + this.visible = true; + } + + /** + * Hides the element + * @returns An awaitable promise + */ + public async hide(): Promise { + if (!this.visible) return; + + if (this.interaction) await this.interaction.trigger('second'); + else if (this.noTransition) this.element.style.display = 'none'; + else await fadeOut(this.element); + + this.visible = false; + } +} diff --git a/components/Interaction.ts b/components/Interaction.ts new file mode 100644 index 0000000..dafc372 --- /dev/null +++ b/components/Interaction.ts @@ -0,0 +1,79 @@ +import { queryElement } from '../helpers'; +import wait from '../helpers/wait'; +import Debug from './Debug'; + +// Types +export interface InteractionParams { + element: HTMLElement | string; + duration?: + | number + | { + first?: number; + second?: number; + }; +} + +export default class Interaction { + private readonly element: HTMLElement; + private active = false; + private running = false; + private runningPromise?: Promise; + + public readonly duration: { + first: number; + second: number; + }; + + /** + * Acts as the controller for a Webflow Interaction. + * It accepts an element that will be clicked when required (firing a Mouse Click interaction). + * @param element Element that has the Mouse Click interaction. + * @param duration Optionally, the duration can be explicitly set so the trigger methods will return an awaitable Promise. + */ + constructor({ element, duration }: InteractionParams) { + this.element = + typeof element === 'string' + ? queryElement(element, HTMLElement) || Debug.alert('NoInteraction', 'error') + : element; + + this.duration = { + first: typeof duration === 'number' ? duration : duration?.first ?? 0, + second: typeof duration === 'number' ? duration : duration?.second ?? 0, + }; + } + + /** + * Trigger the interaction + * @param click Perform first or second click + * @returns True if the interaction was fired + */ + public async trigger(click?: 'first' | 'second'): Promise { + if ((click === 'first' && this.active) || (click === 'second' && !this.active)) return false; + if (!click) click = this.active ? 'second' : 'first'; + + this.element.click(); + + this.running = true; + this.runningPromise = wait(this.duration[click]); + await this.runningPromise; + this.running = false; + + this.active = click === 'first'; + return true; + } + + /** + * @returns If the interaction is active + */ + public isActive = (): boolean => this.active; + + /** + * @returns If the interaction is running + */ + public isRunning = (): boolean => this.running; + + /** + * @returns A promise that fulfills when the current running interaction has finished + */ + public untilFinished = (): Promise | undefined => this.runningPromise; +} diff --git a/components/index.ts b/components/index.ts new file mode 100644 index 0000000..380b5c6 --- /dev/null +++ b/components/index.ts @@ -0,0 +1,3 @@ +export { default as Debug } from './Debug'; +export { default as DisplayController, DisplayControllerParams } from './DisplayController'; +export { default as Interaction, InteractionParams } from './Interaction'; diff --git a/dist/index.js b/dist/index.js deleted file mode 100644 index 1f2f020..0000000 --- a/dist/index.js +++ /dev/null @@ -1,2 +0,0 @@ -(()=>{document.addEventListener("DOMContentLoaded",()=>{e("John Doe")});var e=n=>{console.log(`Hello ${n}!`);console.log('Is this a short code snippet? Try compiling this file with Typescript by running the "tsc" command on your terminal.'),console.log('Is this a big project? Try bundling and minifying the files with ESBuild by adding them in the "build.js" config file and running the "npm run build" command on your terminal.')};})(); -/*! This comment will be untouched when compiling */ diff --git a/helpers/cloneNode.ts b/helpers/cloneNode.ts new file mode 100644 index 0000000..491a350 --- /dev/null +++ b/helpers/cloneNode.ts @@ -0,0 +1,7 @@ +/** + * Clone a node that has the same type as the original one + * @param node + */ +const cloneNode = (node: T, deep = true): T => node.cloneNode(deep); + +export default cloneNode; diff --git a/helpers/extractCommaSeparatedValues.ts b/helpers/extractCommaSeparatedValues.ts new file mode 100644 index 0000000..2897a76 --- /dev/null +++ b/helpers/extractCommaSeparatedValues.ts @@ -0,0 +1,32 @@ +import isKeyOf from '../type-guards/isKeyOf'; + +/** + * Convert a string of comma separated values to an array of values + * @param string Comma separated string + * @param compareSource Acts as a type guard for making sure the extracted values match the compared source + * @param defaultValue Is set when there is no matching results after comparing with the source + */ +function extractCommaSeparatedValues(string: string | null | undefined): string[]; +function extractCommaSeparatedValues( + string: string | null | undefined, + compareSource: readonly T[], + defaultValue?: T +): T[]; +function extractCommaSeparatedValues( + string: string | null | undefined, + compareSource?: readonly T[], + defaultValue?: T +): string[] | T[] { + const emptyValue = defaultValue ? [defaultValue] : []; + if (!string) return emptyValue; + const items = string.split(/[ ,]+/); + + if (compareSource) { + const matches = items.filter((item) => isKeyOf(item, compareSource)) as T[]; + return matches.length ? matches : emptyValue; + } + + return items; +} + +export default extractCommaSeparatedValues; diff --git a/helpers/findTextNode.ts b/helpers/findTextNode.ts new file mode 100644 index 0000000..499b4f3 --- /dev/null +++ b/helpers/findTextNode.ts @@ -0,0 +1,17 @@ +/** + * Find the first child text node of an element + * @param element + */ +const findTextNode = (element: HTMLElement): ChildNode | undefined => { + let textNode: ChildNode | undefined; + + for (const node of element.childNodes) { + if (node instanceof HTMLElement && node.childNodes.length) textNode = findTextNode(node); + else if (node.nodeType === Node.TEXT_NODE) textNode = node; + if (textNode) break; + } + + return textNode; +}; + +export default findTextNode; diff --git a/helpers/getDistanceFromTop.ts b/helpers/getDistanceFromTop.ts new file mode 100644 index 0000000..ee1c9e5 --- /dev/null +++ b/helpers/getDistanceFromTop.ts @@ -0,0 +1,13 @@ +/** + * Get the distance between an element and the top of the window + * @param element + * @returns The distance in pixels + */ +const getDistanceFromTop = (element: Element): number => { + const rect = element.getBoundingClientRect(); + // prettier-ignore + const scrollTop = window.pageYOffset || (document.documentElement || document.body.parentNode || document.body).scrollTop; + return rect.top + scrollTop; +}; + +export default getDistanceFromTop; diff --git a/helpers/getFormFieldValue.ts b/helpers/getFormFieldValue.ts new file mode 100644 index 0000000..33b8b44 --- /dev/null +++ b/helpers/getFormFieldValue.ts @@ -0,0 +1,23 @@ +import FormField from '../types/FormField'; + +/** + * Gets the value of a given input element. + * @param {FormField} input + */ +const getFormFieldValue = (input: FormField): string => { + let { value } = input; + + // Perform actions depending on input type + if (input.type === 'checkbox') value = (input).checked.toString(); + if (input.type === 'radio') { + // Get the checked radio + const checkedOption = input.closest('form')?.querySelector(`input[name="${input.name}"]:checked`); + + // If exists, set its value + value = checkedOption instanceof HTMLInputElement ? checkedOption.value : ''; + } + + return value.toString(); +}; + +export default getFormFieldValue; diff --git a/helpers/getObjectEntries.ts b/helpers/getObjectEntries.ts new file mode 100644 index 0000000..c98587d --- /dev/null +++ b/helpers/getObjectEntries.ts @@ -0,0 +1,10 @@ +import Entry from '../types/Entry'; + +/** + * Get type safe Object.entries() + * @param object + */ +// prettier-ignore +const getObjectEntries = >>(object: T): Entry[] => Object.entries(object) as Entry[]; + +export default getObjectEntries; diff --git a/helpers/getObjectKeys.ts b/helpers/getObjectKeys.ts new file mode 100644 index 0000000..4c6e9c1 --- /dev/null +++ b/helpers/getObjectKeys.ts @@ -0,0 +1,8 @@ +/** + * Get the keys of an object with inferred typing + * @param object + * @returns + */ +const getObjectKeys = >(object: T): (keyof T)[] => Object.keys(object) as (keyof T)[]; + +export default getObjectKeys; diff --git a/helpers/index.ts b/helpers/index.ts new file mode 100644 index 0000000..9888c8e --- /dev/null +++ b/helpers/index.ts @@ -0,0 +1,14 @@ +export { default as cloneNode } from './cloneNode'; +export { default as extractCommaSeparatedValues } from './extractCommaSeparatedValues'; +export { default as findTextNode } from './findTextNode'; +export { default as writableStore, WritableStore } from './writableStore'; +export { default as wait } from './wait'; +export { default as throwError } from './throwError'; +export { default as selectInputElement } from './selectInputElement'; +export { default as queryElement } from './queryElement'; +export { default as isVisible } from './isVisible'; +export { default as isScrollable } from './isScrollable'; +export { default as getObjectKeys } from './getObjectKeys'; +export { default as getObjectEntries } from './getObjectEntries'; +export { default as getFormFieldValue } from './getFormFieldValue'; +export { default as getDistanceFromTop } from './getDistanceFromTop'; diff --git a/helpers/isScrollable.ts b/helpers/isScrollable.ts new file mode 100644 index 0000000..958019b --- /dev/null +++ b/helpers/isScrollable.ts @@ -0,0 +1,11 @@ +/** + * Check if an element is scrollable + * @param element + * @returns True or false + */ +const isScrollable = (element: Element): boolean => { + const { overflow } = getComputedStyle(element); + return overflow === 'auto' || overflow === 'scroll'; +}; + +export default isScrollable; diff --git a/helpers/isVisible.ts b/helpers/isVisible.ts new file mode 100644 index 0000000..edd36c3 --- /dev/null +++ b/helpers/isVisible.ts @@ -0,0 +1,8 @@ +/** + * Checks if an element is visible + * @param element + */ +// prettier-ignore +const isVisible = (element: HTMLElement): boolean => !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length); + +export default isVisible; diff --git a/helpers/queryElement.ts b/helpers/queryElement.ts new file mode 100644 index 0000000..58f0129 --- /dev/null +++ b/helpers/queryElement.ts @@ -0,0 +1,19 @@ +import Instance from '../types/Instance'; + +/** + * Query an element and make sure it's the correct type + * @param selector Selector string + * @param instance Instance target of the element type. Example: HTMLElement + * @param scope The scope context where to query. Defaults to document + * @returns The queried element or undefined + */ +const queryElement = ( + selector: string, + instance: Instance, + scope: ParentNode = document +): T | undefined => { + const element = scope.querySelector(selector); + if (element instanceof instance) return element; +}; + +export default queryElement; diff --git a/helpers/selectInputElement.ts b/helpers/selectInputElement.ts new file mode 100644 index 0000000..d61f644 --- /dev/null +++ b/helpers/selectInputElement.ts @@ -0,0 +1,26 @@ +/** + * Selects a custom radio or checkbox element + * @param element Element to select + * @param select - Defaults to true. If set to false, the input element will be unselected. + * @returns The value of the element + */ +const selectInputElement = (element: HTMLInputElement, select = true): string => { + if (element.type === 'checkbox' && select !== element.checked) element.click(); + + if (element.type === 'radio') { + // Add the checked class to the custom radio + const customInput = element.parentElement?.querySelector( + '.w-form-formradioinput--inputType-custom' + ); + if (customInput) customInput.classList[select ? 'add' : 'remove']('w--redirected-checked'); + + // Set the radio as checked + element.checked = select; + element.dispatchEvent(new Event('input', { bubbles: true })); + element.dispatchEvent(new Event('change', { bubbles: true })); + } + + return element.value; +}; + +export default selectInputElement; diff --git a/helpers/throwError.ts b/helpers/throwError.ts new file mode 100644 index 0000000..b8a3d6f --- /dev/null +++ b/helpers/throwError.ts @@ -0,0 +1,10 @@ +/** + * Display an alert and throw an exception + * @param message + * @returns The generic argument to assure type safety when querying DOM Elements. + */ +const throwError = (message: string): T => { + throw new Error(message); +}; + +export default throwError; diff --git a/helpers/wait.ts b/helpers/wait.ts new file mode 100644 index 0000000..160160b --- /dev/null +++ b/helpers/wait.ts @@ -0,0 +1,7 @@ +/** + * Awaitable promise for waiting X time + * @param time + */ +const wait = (time: number): Promise => new Promise((resolve) => setTimeout(resolve, time)); + +export default wait; diff --git a/helpers/writableStore.ts b/helpers/writableStore.ts new file mode 100644 index 0000000..82ea8ca --- /dev/null +++ b/helpers/writableStore.ts @@ -0,0 +1,39 @@ +export interface WritableStore { + get: () => T; + set: (newValue: T) => void; + update: (callback: (value: T) => T) => void; + subscribe: (callback: (value: T) => void) => () => void; +} + +/** + * Writable store + * @param value + * @returns + */ +const writableStore = (value: T): WritableStore => { + let subscribeFunctions: ((value: T) => void)[] = []; + + const get = (): T => value; + + const set = (newValue: T) => { + value = newValue; + subscribeFunctions.forEach((func) => func(newValue)); + }; + + const update = (callback: (value: T) => T) => { + set(callback(value)); + }; + + const subscribe = (callback: (value: T) => void) => { + subscribeFunctions.push(callback); + callback(value); + + return () => { + subscribeFunctions = subscribeFunctions.filter((func) => callback !== func); + }; + }; + + return { get, set, update, subscribe }; +}; + +export default writableStore; diff --git a/package-lock.json b/package-lock.json index 926d91a..3e0233c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { - "name": "@finsweet/developer-starter", - "version": "1.0.0", + "name": "@finsweet/ts-utils", + "version": "0.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -436,12 +436,6 @@ "ansi-colors": "^4.1.1" } }, - "esbuild": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.11.11.tgz", - "integrity": "sha512-iq5YdV63vY/nUAFIvY92BXVkYjMbOchnofLKoLKMPZIa4uuIJAJG9WRA+ZRjQBZbrsORUwvZcANeG2d3p46PJQ==", - "dev": true - }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", diff --git a/package.json b/package.json index ebff8b3..1c3276c 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,10 @@ { - "name": "@finsweet/developer-starter", - "version": "1.0.0", - "description": "Developer starter template for Finsweet projects.", - "main": "src/index.ts", - "scripts": { - "build": "node .\\bin\\build.js" - }, + "name": "@finsweet/ts-utils", + "version": "0.1.0", + "description": "Typescript utils for custom Webflow projects.", "repository": { "type": "git", - "url": "git+https://github.com/finsweet/developer-starter.git" + "url": "git+https://github.com/finsweet/ts-utils.git" }, "keywords": [ "webflow", @@ -21,14 +17,13 @@ }, "license": "ISC", "bugs": { - "url": "https://github.com/finsweet/developer-starter/issues" + "url": "https://github.com/finsweet/ts-utils/issues" }, - "homepage": "https://github.com/finsweet/developer-starter#readme", + "homepage": "https://github.com/finsweet/ts-utils#readme", "devDependencies": { "@finsweet/eslint-config": "^1.0.5", "@typescript-eslint/eslint-plugin": "^4.22.0", "@typescript-eslint/parser": "^4.22.0", - "esbuild": "^0.11.11", "eslint": "^7.24.0", "eslint-config-prettier": "^8.2.0", "eslint-plugin-prettier": "^3.4.0", diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 6f963bd..0000000 --- a/src/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Starter example. Check the comments! -document.addEventListener('DOMContentLoaded', () => { - const name = 'John Doe'; - helloStarter(name); -}); - -/** - * This JSDoc comment is used to describe the purpose of the function: Prints some welcome messages on the console. - * @param name - This is a description of the function parameter: The name that will be printed on the console. - */ -const helloStarter = (name: string) => { - console.log(`Hello ${name}!`); - - /*! This comment will be untouched when compiling */ - console.log( - 'Is this a short code snippet? Try compiling this file with Typescript by running the "tsc" command on your terminal.' - ); - - // This comment will be removed when compiling - console.log( - 'Is this a big project? Try bundling and minifying the files with ESBuild by adding them in the "build.js" config file and running the "npm run build" command on your terminal.' - ); -}; diff --git a/tsconfig.json b/tsconfig.json index 8863e1c..ac2f6f7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,8 +4,8 @@ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ @@ -14,18 +14,18 @@ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ - "outDir": "./dist", /* Redirect output structure to the directory. */ - "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "outDir": "./dist" /* Redirect output structure to the directory. */, + // "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, // "composite": true, /* Enable project compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ - "removeComments": true, /* Do not emit comments to output. */ + "removeComments": true /* Do not emit comments to output. */, // "noEmit": true, /* Do not emit outputs. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */ - "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + "downlevelIteration": true /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */, // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ /* Strict Type-Checking Options */ - "strict": true, /* Enable all strict type-checking options. */ + "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* Enable strict null checks. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */ @@ -49,7 +49,7 @@ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ @@ -64,7 +64,7 @@ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ /* Advanced Options */ - "skipLibCheck": true, /* Skip type checking of declaration files. */ - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + "skipLibCheck": true /* Skip type checking of declaration files. */, + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ } } diff --git a/type-guards/index.ts b/type-guards/index.ts new file mode 100644 index 0000000..41b3d96 --- /dev/null +++ b/type-guards/index.ts @@ -0,0 +1,2 @@ +export { default as isFormField } from './isFormField'; +export { default as isKeyOf } from './isKeyOf'; diff --git a/type-guards/isFormField.ts b/type-guards/isFormField.ts new file mode 100644 index 0000000..20da89a --- /dev/null +++ b/type-guards/isFormField.ts @@ -0,0 +1,10 @@ +import FormField from '../types/FormField'; + +/** + * Checks if an element is a form field element + * @param element + */ +// prettier-ignore +const isFormField = (element: Element | EventTarget | null): element is FormField => element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement; + +export default isFormField; diff --git a/type-guards/isKeyOf.ts b/type-guards/isKeyOf.ts new file mode 100644 index 0000000..cd66f8f --- /dev/null +++ b/type-guards/isKeyOf.ts @@ -0,0 +1,12 @@ +/** + * Check if a key is included in a readonly array + * @param key + * @param source readonly array of strings + * @returns True/false + */ +const isKeyOf = ( + key: string | null | undefined, + source: readonly T[] +): key is typeof source[number] => !!key && source.includes(key); + +export default isKeyOf; diff --git a/types/Entry.ts b/types/Entry.ts new file mode 100644 index 0000000..fda2b6f --- /dev/null +++ b/types/Entry.ts @@ -0,0 +1,5 @@ +/** + * Defines a typed object entry + */ +type Entry = { [K in keyof T]: [K, T[K]] }[keyof T]; +export default Entry; diff --git a/types/FormField.ts b/types/FormField.ts new file mode 100644 index 0000000..4542924 --- /dev/null +++ b/types/FormField.ts @@ -0,0 +1,3 @@ +type FormField = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; + +export default FormField; diff --git a/types/Instance.ts b/types/Instance.ts new file mode 100644 index 0000000..c777b8e --- /dev/null +++ b/types/Instance.ts @@ -0,0 +1,6 @@ +/** + * Declares the instance of an object + */ +type Instance = { new (): T; prototype: T }; + +export default Instance; diff --git a/types/Webflow.ts b/types/Webflow.ts new file mode 100644 index 0000000..487825e --- /dev/null +++ b/types/Webflow.ts @@ -0,0 +1,12 @@ +/** + * Includes methods of the Webflow.js object + */ +interface Webflow { + push: (callback: () => unknown) => void; + destroy: () => void; + ready: () => void; + require: (key: 'ix2') => { destroy: () => void; init: () => void }; + env: () => boolean; +} + +export default Webflow; diff --git a/types/index.ts b/types/index.ts new file mode 100644 index 0000000..fec403c --- /dev/null +++ b/types/index.ts @@ -0,0 +1,4 @@ +export { default as Entry } from './Entry'; +export { default as FormField } from './FormField'; +export { default as Instance } from './Instance'; +export { default as Webflow } from './Webflow'; diff --git a/webflow/getSiteId.ts b/webflow/getSiteId.ts new file mode 100644 index 0000000..23881e7 --- /dev/null +++ b/webflow/getSiteId.ts @@ -0,0 +1,6 @@ +/** + * @returns The Webflow Site ID of the website + */ +const getSiteId = (): string | null => document.documentElement.getAttribute('data-wf-site'); + +export default getSiteId; diff --git a/webflow/index.ts b/webflow/index.ts new file mode 100644 index 0000000..90ce91c --- /dev/null +++ b/webflow/index.ts @@ -0,0 +1,2 @@ +export { default as getSiteId } from './getSiteId'; +export { default as restartWebflow } from './restartWebflow'; diff --git a/webflow/restartWebflow.ts b/webflow/restartWebflow.ts new file mode 100644 index 0000000..0d662b9 --- /dev/null +++ b/webflow/restartWebflow.ts @@ -0,0 +1,18 @@ +import Webflow from '../types/Webflow'; + +declare global { + interface Window { + Webflow: Webflow; + } +} + +/** + * Restart Webflow JS library + */ +const restartWebflow = (): void => { + window.Webflow.destroy(); + window.Webflow.ready(); + window.Webflow.require('ix2')?.init(); +}; + +export default restartWebflow;