Migrated all utilities to TypeScript

This commit is contained in:
Igor Zinken
2023-03-20 23:25:03 +01:00
parent 8adb358b3b
commit 92afec40f0
10 changed files with 105 additions and 65 deletions

View File

@@ -45,6 +45,25 @@ export type CanvasContextPairing = {
// data types handled internally by BitMappery as the canvas source content
export type CanvasDrawable = HTMLCanvasElement | HTMLImageElement;
export type CanvasDimensions = {
// the base dimensions describe the "best fit" scale to represent
// the currently active document at the current window size, this
// is basically the baseline used for the unzoomed document view
width : number;
height : number;
// the visible area of the canvas (as it is positioned inside a container
// that offers scrollable overflow)
visibleWidth : number;
visibleHeight : number;
// the maximum in- and out zoom level supported for the currently
// open document at the current available screen dimensions
maxInScale : number;
maxOutScale : number;
// whether the unzoomed images horizontal side is the dominant side (e.g. fills
// the full width of the visible canvas area)
horizontalDominant : boolean;
};
export type Brush = {
radius: number;
colors: string[];

View File

@@ -20,17 +20,17 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
let dropbox;
let drive;
let dropbox: any;
let drive: any;
export const getDropboxService = async () => {
export const getDropboxService = async (): Promise<any> => {
if ( !dropbox ) {
dropbox = await import( /* webpackChunkName: "dropbox-service" */ "@/services/dropbox-service" );
}
return dropbox;
};
export const getGoogleDriveService = async () => {
export const getGoogleDriveService = async (): Promise<any> => {
if ( !drive ) {
drive = await import( /* webpackChunkName: "google-drive-service" */ "@/services/google-drive-service" );
}

View File

@@ -20,7 +20,14 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
const rafCallbacks = [];
type RafCallback = () => void;
interface Cancelable {
cancel: () => void;
reset: () => void;
};
const rafCallbacks: RafCallback[] = [];
/**
* Returns a Promise that resolves when given timeToWait has expired.
@@ -33,7 +40,7 @@ const rafCallbacks = [];
*
* @param {Number=} timeToWait in milliseconds
*/
export const unblockedWait = ( timeToWait = 4 ) => {
export const unblockedWait = ( timeToWait = 4 ): Promise<void> => {
return new Promise( resolve => {
window.setTimeout( resolve, timeToWait );
});
@@ -47,15 +54,12 @@ export const unblockedWait = ( timeToWait = 4 ) => {
*
* @param {Function} callback fn to execute when delay has expired
* @param {Number=} timeToWait in milliseconds
* @returns {{
* cancel : Function,
* reset : Function
* }}
* @returns {Cancelable}
*/
export const cancelableCallback = ( callback, timeToWait = 250 ) => {
let timerId;
export const cancelableCallback = ( callback: () => void, timeToWait = 250 ): Cancelable => {
let timerId: number;
const startTimeout = () => {
timerId = window.setTimeout( callback, timeToWait );
timerId = window.setTimeout( callback, timeToWait ) as number;
};
const cancelTimeout = () => {
window.clearTimeout( timerId );
@@ -74,7 +78,7 @@ export const cancelableCallback = ( callback, timeToWait = 250 ) => {
* This will debounce multiple invocations of the same callback if it
* hasn't yet fired.
*/
export const rafCallback = callback => {
export const rafCallback = ( callback: RafCallback ): void => {
if ( rafCallbacks.includes( callback )) {
return;
}

View File

@@ -22,20 +22,23 @@
*/
import Bowser from "bowser";
let fsToggle, fsCallback, clientData;
let fsToggle: HTMLElement;
let fsCallback: ( isFullScreen: boolean ) => void;
let clientData: any;
export const isMobile = () => window.screen.availWidth <= 640; // see _variables.scss
export const isMobile = (): boolean => window.screen.availWidth <= 640; // see _variables.scss
// on non-mobile environments we allow focusing of the first form element within a component
// on mobile we don't do this as (depending on OS) this would force the keyboard to popup
export const focus = element => !isMobile() && element?.focus();
export const focus = ( element: HTMLElement ): void => !isMobile() && element?.focus();
export const supportsFullscreen = () => getClientData().os?.name !== "iOS";
export const supportsFullscreen = (): boolean => getClientData().os?.name !== "iOS";
export const isSafari = () => getClientData().browser?.name === "Safari";
export const isSafari = (): boolean => getClientData().browser?.name === "Safari";
export const setToggleButton = ( element, callback ) => {
export const setToggleButton = ( element: HTMLElement, callback: () => void ): void => {
const d = window.document;
fsToggle = element;
fsToggle.addEventListener( "click", toggleFullscreen );
@@ -47,13 +50,18 @@ export const setToggleButton = ( element, callback ) => {
/* internal methods */
export const toggleFullscreen = () => {
export const toggleFullscreen = (): void => {
const d = window.document;
let requestMethod, element;
let requestMethod: () => void;
let element: any;
// @ts-expect-error vendor specific prefixes
if ( d.fullscreenElement || d.webkitFullscreenElement ) {
// @ts-expect-error vendor specific prefixes
requestMethod = d.exitFullscreen || d.webkitExitFullscreen || d.mozCancelFullScreen || d.msExitFullscreen;
element = d;
} else {
// @ts-expect-error vendor specific prefixes
requestMethod = d.body.requestFullScreen || d.body.webkitRequestFullScreen || d.body.mozRequestFullScreen || d.body.msRequestFullscreen;
element = d.body;
}
@@ -62,12 +70,14 @@ export const toggleFullscreen = () => {
}
}
function handleFullscreenChange() {
function handleFullscreenChange(): void {
// @ts-expect-error vendor specific prefixes
fsCallback( document.webkitIsFullScreen || document.mozFullScreen || document.msFullscreenElement === true );
}
function getClientData() {
function getClientData(): any {
if ( !clientData && typeof window !== "undefined" ) {
// @ts-expect-error parsedResult not recognized
clientData = Bowser.getParser( window.navigator.userAgent ).parsedResult;
}
return clientData || {};

View File

@@ -25,13 +25,16 @@ import {
} from "@/definitions/file-types";
import { blobToResource, disposeResource } from "@/utils/resource-manager";
type FileDictionary = {
images: File[];
documents: File[];
thirdParty: File[];
};
/**
* Saves the binary data in given blob to a file of given fileName
*
* @param {Blob|File} blob
* @param {String} fileName
*/
export const saveBlobAsFile = ( blob, fileName ) => {
export const saveBlobAsFile = ( blob: Blob | File, fileName: string ): void => {
const blobURL = blobToResource( blob );
const anchor = document.createElement( "a" );
anchor.style.display = "none";
@@ -50,11 +53,8 @@ export const saveBlobAsFile = ( blob, fileName ) => {
/**
* Converts a base64 encoded String to a binary blob
*
* @param {String} base64string
* @return {Promise<Blob>}
*/
export const base64toBlob = async base64string => {
export const base64toBlob = async ( base64string: string ): Promise<Blob> => {
const base64 = await fetch( base64string );
const blob = await base64.blob();
return blob;
@@ -62,13 +62,10 @@ export const base64toBlob = async base64string => {
/**
* Retrieves the binary data pointed to by given blobUrl
*
* @param {String} blobUrl
* @return {Promise<Blob>}
*/
export const blobUriToBlob = async blobUrl => await fetch( blobUrl ).then( r => r.blob() );
export const blobUriToBlob = async ( blobUrl: string ): Promise<Blob> => await fetch( blobUrl ).then( r => r.blob() );
export const selectFile = ( acceptedTypes, multiple = false ) => {
export const selectFile = ( acceptedTypes: string, multiple = false ): Promise<FileList> => {
const fileBrowser = document.createElement( "input" );
fileBrowser.setAttribute( "type", "file" );
fileBrowser.setAttribute( "accept", acceptedTypes );
@@ -84,24 +81,25 @@ export const selectFile = ( acceptedTypes, multiple = false ) => {
);
fileBrowser.dispatchEvent( simulatedEvent );
return new Promise(( resolve, reject ) => {
fileBrowser.onchange = ({ target }) => resolve( target.files );
fileBrowser.onchange = ({ target }) => resolve(( target as HTMLInputElement ).files );
fileBrowser.onerror = reject;
});
};
export const readFile = ( file, optEncoding = "UTF-8" ) => {
export const readFile = ( file: File | Blob, optEncoding = "UTF-8" ): Promise<string> => {
const reader = new FileReader();
return new Promise(( resolve, reject ) => {
reader.onload = readerEvent => {
resolve( readerEvent.target.result );
reader.onload = ( readerEvent: ProgressEvent ) => {
resolve(( readerEvent.target as FileReader ).result as string );
};
reader.onerror = reject;
reader.readAsText( file, optEncoding );
});
};
export const readClipboardFiles = clipboardData => {
const items = [ ...( clipboardData?.items || []) ];
export const readClipboardFiles = ( clipboardData: DataTransfer ): FileDictionary => {
// @ts-expect-error Type 'DataTransferItemList' is not an array type (but it destructures just fine...)
const items = clipboardData ? [ ...clipboardData.items ] : [];
const images = items
.filter( item => item.kind === "file" && isImageFile( item ))
.map( item => item.getAsFile());
@@ -117,8 +115,9 @@ export const readClipboardFiles = clipboardData => {
return { images, documents, thirdParty };
};
export const readDroppedFiles = dataTransfer => {
const items = [ ...( dataTransfer?.files || []) ];
export const readDroppedFiles = ( dataTransfer: DataTransfer ): FileDictionary => {
// @ts-expect-error Type 'FileList' is not an array type (but it destructures just fine...)
const items = dataTransfer ? [ ...dataTransfer.files ] : [];
return {
images : items.filter( isImageFile ),
documents : items.filter( isProjectFile ),

View File

@@ -20,18 +20,21 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Store } from "vuex";
import type { Layer } from "@/definitions/document";
import { LayerTypes } from "@/definitions/layer-types";
import { enqueueState } from "@/factories/history-state-factory";
import type { BitMapperyState } from "@/store";
/**
* Replace the source / mask contents of given layer, updating its
* bounding box to be centered in relation to its previous size.
*
* @param {Object} layer
* @param {Layer} layer
* @param {HTMLCanvasElement} newSource
* @param {boolean=} isMask whether we're replacing the mask instead of source
*/
export const replaceLayerSource = ( layer, newSource, isMask = false ) => {
export const replaceLayerSource = ( layer: Layer, newSource: HTMLCanvasElement, isMask = false ): void => {
const xDelta = ( layer.width - newSource.width ) / 2;
const yDelta = ( layer.height - newSource.height ) / 2;
@@ -52,7 +55,7 @@ export const replaceLayerSource = ( layer, newSource, isMask = false ) => {
* Text layer addition can be initiated directly from the toolbox
* or keyboard shortcut. Here we define a resuable history enqueue
*/
export const addTextLayer = ({ getters, commit }) => {
export const addTextLayer = ({ getters, commit }: Store<BitMapperyState> ): void => {
const fn = () => commit( "addLayer", { type: LayerTypes.LAYER_TEXT });
fn();
const addedLayerIndex = getters.activeLayerIndex;

View File

@@ -20,10 +20,11 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Layer } from "@/definitions/document";
import { LayerTypes } from "@/definitions/layer-types";
import { resizeImage } from "@/utils/canvas-util";
export const renderCross = ( ctx, x, y, size ) => {
export const renderCross = ( ctx: CanvasRenderingContext2D, x: number, y: number, size: number ): void => {
ctx.save();
ctx.beginPath();
ctx.moveTo( x - size, y - size );
@@ -34,7 +35,7 @@ export const renderCross = ( ctx, x, y, size ) => {
ctx.restore();
};
export const resizeLayerContent = async ( layer, ratioX, ratioY ) => {
export const resizeLayerContent = async ( layer: Layer, ratioX: number, ratioY: number ): Promise<void> => {
const { source, mask } = layer;
layer.source = await resizeImage( source, source.width * ratioX, source.height * ratioY );
@@ -57,7 +58,7 @@ export const resizeLayerContent = async ( layer, ratioX, ratioY ) => {
}
};
export const cropLayerContent = async ( layer, left, top ) => {
export const cropLayerContent = async ( layer: Layer, left: number, top: number ): Promise<void> => {
/*
if ( layer.mask ) {
// no, mask coordinates are relative to layer

View File

@@ -21,13 +21,15 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
type SelectOption = {
label: string;
value: any;
};
/**
* Format select options for use with vue-search-select component
*
* @param {Array} items
* @return {Array<{value: *, text: string }>}
*/
export const mapSelectOptions = items => {
export const mapSelectOptions = ( items: any[] ): SelectOption[] => {
return items.map( value => {
if ( typeof value === "object" ) {
return value;
@@ -39,7 +41,7 @@ export const mapSelectOptions = items => {
/* internal methods */
const ucFirst = text => text.toLowerCase()
.split(" ")
.map(( s ) => s.charAt(0).toUpperCase() + s.substring(1))
.join(" ");
const ucFirst = ( text: string ): string => text.toLowerCase()
.split( " " )
.map(( s ) => s.charAt( 0 ).toUpperCase() + s.substring( 1 ))
.join( " " );

View File

@@ -20,7 +20,7 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export const truncate = ( string = "", maxLength = 100 ) =>
export const truncate = ( string = "", maxLength = 100 ): string =>
string.length > maxLength ? `${string.substr( 0, maxLength )}...` : string;
export const displayAsKb = size => `${( size / 1024 ).toFixed( 2 )} Kb`;
export const displayAsKb = ( size: number ): string => `${( size / 1024 ).toFixed( 2 )} Kb`;

View File

@@ -20,6 +20,8 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Document } from "@/definitions/document";
import type { CanvasDimensions } from "@/definitions/editor";
import { MIN_ZOOM, MAX_ZOOM } from "@/definitions/tool-types";
import { getZoomRange } from "@/math/image-math";
@@ -27,7 +29,7 @@ import { getZoomRange } from "@/math/image-math";
* Calculates the zoom level for given canvasDimensions to display the given
* document in its entierity
*/
export const fitInWindow = ( activeDocument, canvasDimensions ) => {
export const fitInWindow = ( activeDocument: Document, canvasDimensions: CanvasDimensions ): number => {
const { visibleWidth, visibleHeight, horizontalDominant } = canvasDimensions;
const {
zeroZoomWidth, zeroZoomHeight, pixelsPerZoomOutUnit,
@@ -44,14 +46,14 @@ export const fitInWindow = ( activeDocument, canvasDimensions ) => {
* Calculates the zoom level for given canvasDimensions to display the given
* document at its original size
*/
export const displayOriginalSize = ( activeDocument, canvasDimensions ) => {
export const displayOriginalSize = ( activeDocument: Document, canvasDimensions: CanvasDimensions ): number => {
const { width, height } = activeDocument;
const { horizontalDominant } = canvasDimensions;
const {
zeroZoomWidth, zeroZoomHeight, pixelsPerZoomOutUnit, pixelsPerZoomInUnit
} = getZoomRange( activeDocument, canvasDimensions );
let level;
let level: number;
if ( width === zeroZoomWidth ) {
level = 0; // equals precalculated best fit
} else if ( width < zeroZoomWidth ) {
@@ -72,4 +74,4 @@ export const displayOriginalSize = ( activeDocument, canvasDimensions ) => {
/* internal methods */
const clampLevel = level => Math.max( MIN_ZOOM, Math.min( MAX_ZOOM, level ));
const clampLevel = ( level: number ): number => Math.max( MIN_ZOOM, Math.min( MAX_ZOOM, level ));