generated from finsweet/developer-starter
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
397a5e8
commit 48bb7dc
Showing
40 changed files
with
572 additions
and
97 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
node_modules/ | ||
.eslintrc.js | ||
.npmignore | ||
.prettierignore | ||
.prettierrc | ||
package-lock.json | ||
tsconfig.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<void> => { | ||
return new Promise<void>((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<void> => { | ||
return new Promise<void>((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); | ||
})(); | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { fadeIn, fadeOut } from './fade'; |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<T>(text: string, type: 'error'): T; | ||
public static alert<T>(text: string, type: typeof alertTypes[number]): T | void { | ||
if (this.alertsActivated) window.alert(text); | ||
if (type === 'error') throw new Error(text); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<DisplayControllerParams, 'displayProperty'>); | ||
constructor({ element, displayProperty, noTransition }: Omit<DisplayControllerParams, 'interaction'>); | ||
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<void> { | ||
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<void> { | ||
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<unknown>; | ||
|
||
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<boolean> { | ||
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<unknown> | undefined => this.runningPromise; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export { default as Debug } from './Debug'; | ||
export { default as DisplayController, DisplayControllerParams } from './DisplayController'; | ||
export { default as Interaction, InteractionParams } from './Interaction'; |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/** | ||
* Clone a node that has the same type as the original one | ||
* @param node | ||
*/ | ||
const cloneNode = <T extends Node>(node: T, deep = true): T => <T>node.cloneNode(deep); | ||
|
||
export default cloneNode; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<T extends string>( | ||
string: string | null | undefined, | ||
compareSource: readonly T[], | ||
defaultValue?: T | ||
): T[]; | ||
function extractCommaSeparatedValues<T extends string>( | ||
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = (<HTMLInputElement>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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import Entry from '../types/Entry'; | ||
|
||
/** | ||
* Get type safe Object.entries() | ||
* @param object | ||
*/ | ||
// prettier-ignore | ||
const getObjectEntries = <T extends Readonly<Record<string, unknown>>>(object: T): Entry<T>[] => Object.entries(object) as Entry<T>[]; | ||
|
||
export default getObjectEntries; |
Oops, something went wrong.