Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions flottform/forms/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
.vercel
.output
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
/test-results
/docs
6 changes: 5 additions & 1 deletion flottform/forms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,19 @@
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"test": "pnpm run test:unit",
"test:unit": "vitest"
"test:unit": "vitest",
"docs": "typedoc --options typedoc.json"
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.12.0",
"@fontsource/roboto": "^5.1.0",
"@types/qrcode": "^1.5.5",
"@vitest/browser": "^2.1.2",
"globals": "^15.10.0",
"qrcode": "^1.5.4",
"typedoc": "^0.26.10",
"typedoc-material-theme": "^1.1.0",
"vite": "^5.4.8",
"vite-plugin-dts": "^4.2.3",
"vitest": "^2.1.2",
Expand Down
111 changes: 105 additions & 6 deletions flottform/forms/src/default-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,54 @@ import { FlottformTextInputHost } from './flottform-text-input-host';
import { BaseInputHost, BaseListeners } from './internal';
import { FlottformCreateFileParams, FlottformCreateTextParams } from './types';

/**
* The result object returned when creating a Flottform component.
* Contains the root element and methods to interact with the component.
*/
export type DefaultFlottformComponentResult = {
/** The root HTML element of the Flottform component */
flottformRoot: HTMLElement;
/**
* Retrieves all existing Flottform items (both file and text entries) in the UI.
*
* @returns {NodeListOf<Element> | null} - A NodeList of Flottform input items (file and text entries), or `null` if no items are found.
*/
getAllFlottformItems: () => NodeListOf<Element> | null;
/**
* Creates a UI entry for receiving a file via WebRTC.
*
* @param {Object} params - Configuration options for the file input entry.
* @param {string} params.flottformApi - URL of the WebRTC signaling server.
* @param {Function} params.createClientUrl - A function that returns the URL where the second peer can upload the file.
* @param {HTMLInputElement} params.inputField - The file input field in the main form where the received file will be displayed or processed.
* @param {string} [params.id] - Optional ID for the file input entry.
* @param {string} [params.additionalItemClasses] - Optional additional CSS classes for styling the file input entry.
* @param {string} [params.label] - Optional label text for the file input entry.
* @param {string} [params.buttonLabel] - Optional text for the button that triggers the file reception.
* @param {string | Function} [params.onErrorText] - Optional error message displayed if the file transfer fails.
* @param {string} [params.onSuccessText] - Optional success message displayed after the file is successfully received.
*
* @returns {void}
*/
createFileItem: (params: FlottformCreateFileParams) => void;
/**
* Creates a UI entry for receiving text via WebRTC.
*
* @param {Object} params - Configuration options for the text input entry.
* @param {string} params.flottformApi - URL of the WebRTC signaling server.
* @param {Function} params.createClientUrl - A function that returns the URL where the second peer can send the text.
* @param {string} [params.id] - Optional ID for the text input entry.
* @param {string} [params.additionalItemClasses] - Optional additional CSS classes for styling the text input entry.
* @param {string} [params.label] - Optional label text for the text input entry.
* @param {string} [params.buttonLabel] - Optional text for the button that triggers the text reception.
* @param {string | Function} [params.onErrorText] - Optional error message displayed if the text transfer fails.
* @param {string} [params.onSuccessText] - Optional success message displayed after the text is successfully received.
*
* @returns {void}
*/
createTextItem: (params: FlottformCreateTextParams) => void;
};

const openInputsList = () => {
const flottformElementsContainerWrapper: HTMLDivElement = document.querySelector(
'.flottform-elements-container-wrapper'
Expand Down Expand Up @@ -40,6 +88,62 @@ const createLinkAndQrCode = (qrCode: string, link: string) => {
createChannelLinkWithOffer
};
};

/**
* Creates and attaches a default Flottform UI component to the specified anchor element. This UI acts as an intermediary to facilitate WebRTC-based peer-to-peer connections between two devices, allowing one peer to send data (files or text) to the other.
*
* Developers can customize aspects of the UI, such as the button text, descriptions, and CSS classes, while using the default behavior provided by the function to quickly set up the peer-to-peer data transfer mechanism.
*
* The generated UI component will be attached as a child to `flottformAnchorElement`. The UI includes a dialog with a QR code or link that the second peer can use to connect and upload files/text to the main form.
*
* The dialog also shows the progress of receiving the file or text from the other device or any errors that can happen.
*
* The returned object allows for further interactions, such as adding the UI necessary to handle receiving a file or text, and retrieving all existing Flottform items within the UI.
*
* @param {Object} params - Configuration options for setting up the Flottform component.
* @param {HTMLElement} params.flottformAnchorElement - The HTML element to which the Flottform component will be attached. It determines where the UI will be built on the page.
* @param {HTMLElement} [params.flottformRootElement] - An optional root element to use. If not provided, a new default root element (a `div` with the class `flottform-root`) will be created.
* @param {string} [params.additionalComponentClass] - Optional additional class to add for custom styling of the component.
* @param {string} [params.flottformRootTitle] - Optional title to set for the Flottform root element This is the text displayed on the button that opens the dialog (with the class `flottform-root-opener-button`).
* @param {string} [params.flottformRootDescription] - Optional description text shown inside the dialog when it is opened. It provides context for the user about what the Flottform component does (e.g., "Receive files from other devices").
*
* @returns {Object} - Returns an object with methods to interact with the Flottform component.
* @returns {HTMLElement} returns.flottformRoot - The root element of the Flottform UI.
* @returns {Function} returns.createFileItem - Function to create an entry in the UI for receiving files from another peer. The entry will show the QR code/link, progress of the file transfer, and handle any errors.
* @returns {Function} returns.createTextItem - Function to create an entry in the UI for receiving text from another peer. Similar to `createFileItem`, it handles text input, progress tracking, and error handling.
* @returns {Function} returns.getAllFlottformItems - Function to retrieve all current Flottform items (file and text entries) in the dialog.
*
* @example
*
*
* const flottformComponent = createDefaultFlottformComponent({
* flottformAnchorElement: document.getElementById('form-anchor'),
* flottformRootTitle: 'Share Data via Flottform',
* flottformRootDescription: 'This form is powered by Flottform. Upload files or send text from another device using the provided QR code.'
*});
*
* // Create an entry to receive files
* flottformComponent.createFileItem({
* flottformApi, // URL of the WebRTC signaling server
* createClientUrl: ({ endpointId }) => `/upload/${endpointId}`, // URL of the client page for file uploads
* inputField: document.querySelector('#fileInput'), // The file input field in the main form
* label: 'Upload Resume', // Label for the file input
* buttonLabel:'Submit File', // Button text for the file input
* onSuccessText: 'File received successfully!' // Success message displayed after the file is received
* });
*
* // Create an entry to receive text
* flottformComponent.createTextItem({
* flottformApi, // URL of the WebRTC signaling server
* createClientUrl: ({ endpointId }) => `/text/${endpointId}`, // URL of the client page for sending text
* label: 'Enter your message', // Label for the text input
* buttonLabel: 'Send Message', // Button text for the text input
* onErrorText: (error) => `Failed to receive text: ${error.message}` // Error message if text transfer fails
* });
*
* // Retrieve all Flottform items in the UI (file and text entries)
* const allItems = flottformComponent.getAllFlottformItems();
*/
export const createDefaultFlottformComponent = ({
flottformAnchorElement,
flottformRootElement,
Expand All @@ -52,12 +156,7 @@ export const createDefaultFlottformComponent = ({
additionalComponentClass?: string;
flottformRootTitle?: string;
flottformRootDescription?: string;
}): {
flottformRoot: HTMLElement;
createFileItem: (params: FlottformCreateFileParams) => void;
createTextItem: (params: FlottformCreateTextParams) => void;
getAllFlottformItems: () => NodeListOf<Element> | null;
} => {
}): DefaultFlottformComponentResult => {
const flottformRoot: HTMLElement =
flottformRootElement ??
document.querySelector('.flottform-root') ??
Expand Down
56 changes: 54 additions & 2 deletions flottform/forms/src/flottform-channel-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,26 @@ type Listeners = {
error: [e: string];
bufferedamountlow: [];
};

/**
* A class used to represent one peer (called client) to establish a WebRTC connection with another peer (called host).
* It handles ICE candidate gathering and sending/receiving data.
* The connection is initiated only when `start` method is called.
*
* This class emits various events during the connection lifecycle, such as `connected`, `disconnected`, and `error`, allowing you to respond to changes in the connection state.
*
* @fires init - Emitted when the client is initialized.
* @fires retrieving-info-from-endpoint - Emitted when information is being retrieved from the endpoint.
* @fires sending-client-info - Emitted when client information is being sent to the host.
* @fires connecting-to-host - Emitted when attempting to connect to the host.
* @fires connected - Emitted when the connection is successfully established.
* @fires connection-impossible - Emitted if the connection to the host cannot be established.
* @fires done - Emitted when the all of the data is received.
* @fires disconnected - Emitted when the connection is closed.
* @fires error - Emitted when there is an error during the connection.
* @fires bufferedamountlow - Emitted when the buffered amount for data channels is low.
*
* @extends EventEmitter
*/
export class FlottformChannelClient extends EventEmitter<Listeners> {
private flottformApi: string | URL;
private endpointId: string;
Expand All @@ -34,7 +53,16 @@ export class FlottformChannelClient extends EventEmitter<Listeners> {
private dataChannel: RTCDataChannel | null = null;
private pollForIceTimer: NodeJS.Timeout | number | null = null;
private BUFFER_THRESHOLD = 128 * 1024; // 128KB buffer threshold (maximum of 4 chunks in the buffer waiting to be sent over the network)

/**
* Creates an instance of FlottformChannelClient
*
* @param {Object} config - The configuration for setting up the channel for the host.
*
* @param {endpointId} - The unique identifier of the endpoint to connect to.
* @param {flottformApi} - The API endpoint for retrieving connection information.
* @param {pollTimeForIceInMs} - Optional time in milliseconds for polling ICE candidates.
* @param {logger} - Optional logger for logging connection events (default: `console`).
*/
constructor({
endpointId,
flottformApi,
Expand All @@ -61,6 +89,9 @@ export class FlottformChannelClient extends EventEmitter<Listeners> {
this.logger.info(`**Client State changed to: ${newState}`, details == undefined ? '' : details);
};

/**
* Starts the WebRTC connection process. The connection is not established until this method is called.
*/
start = async () => {
if (this.openPeerConnection) {
this.close();
Expand Down Expand Up @@ -114,6 +145,11 @@ export class FlottformChannelClient extends EventEmitter<Listeners> {
this.startPollingForIceCandidates(getEndpointInfoUrl);
};

/**
* Closes the WebRTC connection if it is currently established.
*
* @fires disconnected - Emitted when the connection is successfully closed.
*/
close = () => {
if (this.openPeerConnection) {
this.openPeerConnection.close();
Expand All @@ -122,6 +158,12 @@ export class FlottformChannelClient extends EventEmitter<Listeners> {
this.changeState('disconnected');
};

/**
* Sends data to the connected peer via the WebRTC data channel.
*
* @param data - The data to send to the peer.
* @fires error - Emits the state error if the connection is not established.
*/
// sendData = (data: string | Blob | ArrayBuffer | ArrayBufferView) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sendData = (data: any) => {
Expand All @@ -135,6 +177,11 @@ export class FlottformChannelClient extends EventEmitter<Listeners> {
this.dataChannel.send(data);
};

/**
* Determines if more data can be sent based on the WebRTC data channel's buffered amount. This is useful when dealing with large amounts of data.
*
* @returns `true` if more data can be sent, otherwise `false`.
*/
canSendMoreData = () => {
return (
this.dataChannel &&
Expand Down Expand Up @@ -175,6 +222,7 @@ export class FlottformChannelClient extends EventEmitter<Listeners> {
this.logger.error(`onicecandidateerror - ${this.openPeerConnection!.connectionState}`, e);
};
};

private setUpConnectionStateGathering = (getEndpointInfoUrl: string) => {
if (this.openPeerConnection === null) {
this.changeState(
Expand Down Expand Up @@ -212,12 +260,14 @@ export class FlottformChannelClient extends EventEmitter<Listeners> {
}
};
};

private stopPollingForIceCandidates = async () => {
if (this.pollForIceTimer) {
clearTimeout(this.pollForIceTimer);
}
this.pollForIceTimer = null;
};

private startPollingForIceCandidates = async (getEndpointInfoUrl: string) => {
if (this.pollForIceTimer) {
clearTimeout(this.pollForIceTimer);
Expand All @@ -227,6 +277,7 @@ export class FlottformChannelClient extends EventEmitter<Listeners> {

this.pollForIceTimer = setTimeout(this.startPollingForIceCandidates, this.pollTimeForIceInMs);
};

private pollForConnection = async (getEndpointInfoUrl: string) => {
if (this.openPeerConnection === null) {
this.changeState('error', "openPeerConnection is null. Unable to retrieve Host's details");
Expand All @@ -239,6 +290,7 @@ export class FlottformChannelClient extends EventEmitter<Listeners> {
await this.openPeerConnection.addIceCandidate(iceCandidate);
}
};

private putClientInfo = async (
putClientInfoUrl: string,
clientKey: string,
Expand Down
38 changes: 37 additions & 1 deletion flottform/forms/src/flottform-channel-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,26 @@ import {
DEFAULT_WEBRTC_CONFIG,
setIncludes
} from './internal';

/**
* A class used to represent one peer (called client) to establish a WebRTC connection with another peer (called host).
* It handles ICE candidate gathering and sending/receiving data.
* The connection is initiated only when `start` method is called.
*
* This class emits various events throughout the connection lifecycle such as `connected`, `disconnected`, and `error`, allowing you to respond to the connection state changes.
*
* @fires new - Emitted when the host is created and ready to accept clients.
* @fires waiting-for-client - Emitted when waiting for a client to connect.
* @fires waiting-for-data - Emitted when the host is ready to receive data.
* @fires waiting-for-ice - Emitted when ICE candidates are being gathered.
* @fires receiving-data - Emitted when the host is receiving data from the client.
* @fires file-received - Emitted when a complete file has been received.
* @fires done - Emitted when the transfer is complete.
* @fires error - Emitted when an error occurs during connection or data transfer.
* @fires connected - Emitted when the host successfully connects to a client.
* @fires disconnected - Emitted when the connection is closed.
*
* @extends EventEmitter
*/
export class FlottformChannelHost extends EventEmitter<FlottformEventMap> {
private flottformApi: string | URL;
private createClientUrl: (params: { endpointId: string }) => Promise<string>;
Expand All @@ -22,6 +41,15 @@ export class FlottformChannelHost extends EventEmitter<FlottformEventMap> {
private dataChannel: RTCDataChannel | null = null;
private pollForIceTimer: NodeJS.Timeout | number | null = null;

/**
* Creates an instance of FlottformChannelHost
*
* @param {Object} config - The configuration for setting up the channel for the host.
* @param {flottformApi} - The API endpoint for retrieving connection information.
* @param {createClientUrl} - A function that generates the client URL given an endpoint ID.
* @param {pollTimeForIceInMs} - The time interval (in ms) for polling ICE candidates.
* @param {logger} - Optional logger for logging connection events.
*/
constructor({
flottformApi,
createClientUrl,
Expand Down Expand Up @@ -51,6 +79,9 @@ export class FlottformChannelHost extends EventEmitter<FlottformEventMap> {
this.logger.info(`State changed to: ${newState}`, details == undefined ? '' : details);
};

/**
* Starts the WebRTC connection process for the host. The connection is not established until this method is called.
*/
start = async () => {
if (this.openPeerConnection) {
this.close();
Expand Down Expand Up @@ -97,6 +128,11 @@ export class FlottformChannelHost extends EventEmitter<FlottformEventMap> {
this.setupDataChannelListener();
};

/**
* Closes the WebRTC connection if it is currently established.
*
* @fires disconnected - Emitted when the connection is successfully closed.
*/
close = () => {
if (this.openPeerConnection) {
this.openPeerConnection.close();
Expand Down
Loading