Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
alexiglesias93 committed Apr 25, 2021
1 parent 397a5e8 commit 48bb7dc
Show file tree
Hide file tree
Showing 40 changed files with 572 additions and 97 deletions.
7 changes: 7 additions & 0 deletions .npmignore
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
36 changes: 10 additions & 26 deletions README.md
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`
47 changes: 47 additions & 0 deletions animations/fade.ts
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);
})();
});
};
1 change: 1 addition & 0 deletions animations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { fadeIn, fadeOut } from './fade';
17 changes: 0 additions & 17 deletions bin/build.js

This file was deleted.

17 changes: 17 additions & 0 deletions components/Debug.ts
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);
}
}
66 changes: 66 additions & 0 deletions components/DisplayController.ts
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;
}
}
79 changes: 79 additions & 0 deletions components/Interaction.ts
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;
}
3 changes: 3 additions & 0 deletions components/index.ts
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';
2 changes: 0 additions & 2 deletions dist/index.js

This file was deleted.

7 changes: 7 additions & 0 deletions helpers/cloneNode.ts
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;
32 changes: 32 additions & 0 deletions helpers/extractCommaSeparatedValues.ts
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;
17 changes: 17 additions & 0 deletions helpers/findTextNode.ts
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;
13 changes: 13 additions & 0 deletions helpers/getDistanceFromTop.ts
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;
23 changes: 23 additions & 0 deletions helpers/getFormFieldValue.ts
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;
10 changes: 10 additions & 0 deletions helpers/getObjectEntries.ts
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;
Loading

0 comments on commit 48bb7dc

Please sign in to comment.