mirror of
https://github.com/igorski/bitmappery.git
synced 2026-06-17 11:45:04 +02:00
Migrated all utilities to TypeScript
This commit is contained in:
@@ -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[];
|
||||
|
||||
@@ -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" );
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 || {};
|
||||
@@ -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 ),
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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( " " );
|
||||
@@ -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`;
|
||||
@@ -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 ));
|
||||
Reference in New Issue
Block a user