From 92afec40f00d9e52fdffdaa390f54d382e61ae6f Mon Sep 17 00:00:00 2001 From: Igor Zinken Date: Mon, 20 Mar 2023 23:25:03 +0100 Subject: [PATCH] Migrated all utilities to TypeScript --- src/definitions/editor.ts | 19 +++++++++ ...vice-loader.js => cloud-service-loader.ts} | 8 ++-- .../{debounce-util.js => debounce-util.ts} | 24 ++++++----- ...nvironment-util.js => environment-util.ts} | 30 +++++++++----- src/utils/{file-util.js => file-util.ts} | 41 +++++++++---------- src/utils/{layer-util.js => layer-util.ts} | 9 ++-- src/utils/{render-util.js => render-util.ts} | 7 ++-- ...h-select-util.js => search-select-util.ts} | 18 ++++---- src/utils/{string-util.js => string-util.ts} | 4 +- src/utils/{zoom-util.js => zoom-util.ts} | 10 +++-- 10 files changed, 105 insertions(+), 65 deletions(-) rename src/utils/{cloud-service-loader.js => cloud-service-loader.ts} (90%) rename src/utils/{debounce-util.js => debounce-util.ts} (84%) rename src/utils/{environment-util.js => environment-util.ts} (72%) rename src/utils/{file-util.js => file-util.ts} (73%) rename src/utils/{layer-util.js => layer-util.ts} (87%) rename src/utils/{render-util.js => render-util.ts} (86%) rename src/utils/{search-select-util.js => search-select-util.ts} (83%) rename src/utils/{string-util.js => string-util.ts} (88%) rename src/utils/{zoom-util.js => zoom-util.ts} (86%) diff --git a/src/definitions/editor.ts b/src/definitions/editor.ts index 5d6907c..3417946 100644 --- a/src/definitions/editor.ts +++ b/src/definitions/editor.ts @@ -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[]; diff --git a/src/utils/cloud-service-loader.js b/src/utils/cloud-service-loader.ts similarity index 90% rename from src/utils/cloud-service-loader.js rename to src/utils/cloud-service-loader.ts index 2a75067..f1997b6 100644 --- a/src/utils/cloud-service-loader.js +++ b/src/utils/cloud-service-loader.ts @@ -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 => { if ( !dropbox ) { dropbox = await import( /* webpackChunkName: "dropbox-service" */ "@/services/dropbox-service" ); } return dropbox; }; -export const getGoogleDriveService = async () => { +export const getGoogleDriveService = async (): Promise => { if ( !drive ) { drive = await import( /* webpackChunkName: "google-drive-service" */ "@/services/google-drive-service" ); } diff --git a/src/utils/debounce-util.js b/src/utils/debounce-util.ts similarity index 84% rename from src/utils/debounce-util.js rename to src/utils/debounce-util.ts index 698fd80..c2e3c42 100644 --- a/src/utils/debounce-util.js +++ b/src/utils/debounce-util.ts @@ -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 => { 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; } diff --git a/src/utils/environment-util.js b/src/utils/environment-util.ts similarity index 72% rename from src/utils/environment-util.js rename to src/utils/environment-util.ts index 7ef445e..dfd22ae 100644 --- a/src/utils/environment-util.js +++ b/src/utils/environment-util.ts @@ -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 || {}; diff --git a/src/utils/file-util.js b/src/utils/file-util.ts similarity index 73% rename from src/utils/file-util.js rename to src/utils/file-util.ts index 732fc48..9a94421 100644 --- a/src/utils/file-util.js +++ b/src/utils/file-util.ts @@ -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} */ -export const base64toBlob = async base64string => { +export const base64toBlob = async ( base64string: string ): Promise => { 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} */ -export const blobUriToBlob = async blobUrl => await fetch( blobUrl ).then( r => r.blob() ); +export const blobUriToBlob = async ( blobUrl: string ): Promise => await fetch( blobUrl ).then( r => r.blob() ); -export const selectFile = ( acceptedTypes, multiple = false ) => { +export const selectFile = ( acceptedTypes: string, multiple = false ): Promise => { 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 => { 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 ), diff --git a/src/utils/layer-util.js b/src/utils/layer-util.ts similarity index 87% rename from src/utils/layer-util.js rename to src/utils/layer-util.ts index 57ff173..8bfa6b9 100644 --- a/src/utils/layer-util.js +++ b/src/utils/layer-util.ts @@ -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 ): void => { const fn = () => commit( "addLayer", { type: LayerTypes.LAYER_TEXT }); fn(); const addedLayerIndex = getters.activeLayerIndex; diff --git a/src/utils/render-util.js b/src/utils/render-util.ts similarity index 86% rename from src/utils/render-util.js rename to src/utils/render-util.ts index aba0a2d..f9005bd 100644 --- a/src/utils/render-util.js +++ b/src/utils/render-util.ts @@ -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 => { 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 => { /* if ( layer.mask ) { // no, mask coordinates are relative to layer diff --git a/src/utils/search-select-util.js b/src/utils/search-select-util.ts similarity index 83% rename from src/utils/search-select-util.js rename to src/utils/search-select-util.ts index c82938e..69c577c 100644 --- a/src/utils/search-select-util.js +++ b/src/utils/search-select-util.ts @@ -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( " " ); diff --git a/src/utils/string-util.js b/src/utils/string-util.ts similarity index 88% rename from src/utils/string-util.js rename to src/utils/string-util.ts index 13effdc..757c250 100644 --- a/src/utils/string-util.js +++ b/src/utils/string-util.ts @@ -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`; diff --git a/src/utils/zoom-util.js b/src/utils/zoom-util.ts similarity index 86% rename from src/utils/zoom-util.js rename to src/utils/zoom-util.ts index 1f3ad2e..8b5da32 100644 --- a/src/utils/zoom-util.js +++ b/src/utils/zoom-util.ts @@ -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 ));