Skip to content

Commit

Permalink
Modernize
Browse files Browse the repository at this point in the history
  • Loading branch information
fregante committed May 16, 2023
1 parent f766be2 commit 699afaa
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 86 deletions.
3 changes: 0 additions & 3 deletions .travis.yml

This file was deleted.

138 changes: 77 additions & 61 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,83 +1,99 @@
type Input = string | HTMLImageElement;
type Output = HTMLImageElement;
type Attributes = Record<string, string>;

type ImagePromise = Promise<HTMLImageElement> & {
image: HTMLImageElement;
};
type Result<Image = HTMLImageElement> = Readonly<{
image: Image;
loaded: () => Promise<Image>;
metadata: () => Promise<Image>;
}> ;

function isArrayLike(input: any): input is ArrayLike<Input> {
return input.length !== undefined;
function setAttributes(image: HTMLImageElement, attributes: Attributes): void {
for (const [attribute, value] of Object.entries(attributes)) {
image.setAttribute(attribute, value);
}
}

function loadSingleImage(image: HTMLImageElement): ImagePromise {
const promise = new Promise<HTMLImageElement>((resolve, reject) => {
if (image.naturalWidth) {
// If the browser can determine the naturalWidth the image is already loaded successfully
resolve(image);
} else if (image.complete) {
// If the image is complete but the naturalWidth is 0px it is probably broken
reject(image);
} else {
image.addEventListener('load', fulfill);
image.addEventListener('error', fulfill);
function lazyPromise<Image = HTMLImageElement>(fn: () => Promise<Image>): () => Promise<Image> {
let promise: Promise<Image> | undefined;
return () => {
if (!promise) {
promise = fn();
}

function fulfill(): void {
if (image.naturalWidth) {
resolve(image);
} else {
reject(image);
return promise;
};
}

function load(image: HTMLImageElement): Result {
const result = {
image,
loaded: lazyPromise(async () => {
await image.decode();
return image;
}),
metadata: lazyPromise(async () => new Promise<HTMLImageElement>((resolve, reject) => {
const check = () => {
if (image.naturalWidth) {
resolve(image);
clearInterval(interval);
return true;
}

if (image.complete) {
// If the image is complete but the naturalWidth is 0px it's probably broken
reject(image);
clearInterval(interval);
return true;
}

return false;
};

if (check()) {
return;
}

image.removeEventListener('load', fulfill);
image.removeEventListener('error', fulfill);
}
});
const interval = setInterval(check, 100);

return Object.assign(promise, {image});
}
result.loaded().then(resolve, reject);
})),
};

function loadImages(input: Input, attributes?: Attributes): ImagePromise;
function loadImages(input: ArrayLike<Input>, attributes?: Attributes): Promise<Output[]>;
function loadImages(input: Input | ArrayLike<Input>, attributes: Attributes = {}): ImagePromise | Promise<Output[]> {
if (input instanceof HTMLImageElement) {
return loadSingleImage(input);
}
return Object.freeze(result);
}

export function loadImage(input: Input, attributes: Attributes = {}): Result {
if (typeof input === 'string') {
/* Create a <img> from a string */
const src = input;
// Create a <img> from a string
const image = new Image();
Object.keys(attributes).forEach(
name => image.setAttribute(name, attributes[name])
);
image.src = src;
return loadSingleImage(image);
// Set attributes before `src`
setAttributes(image, attributes);

image.src = input;
return load(image);
}

if (isArrayLike(input)) {
// Momentarily ignore errors
const reflect = (img: Input): Promise<HTMLImageElement | Error> => loadImages(img, attributes).catch((error: Error) => error);
const reflected = [].map.call(input, reflect) as Array<HTMLImageElement | Error>;
const tsFix = Promise.all(reflected).then((results: Array<HTMLImageElement | Error>) => {
const loaded = results.filter((x: any): x is HTMLImageElement => x.naturalWidth);
if (loaded.length === results.length) {
return loaded;
}
if (input instanceof HTMLImageElement) {
setAttributes(input, attributes);
return load(input);
}

return Promise.reject({
loaded,
errored: results.filter((x: any): x is Error => !x.naturalWidth)
});
});
throw new TypeError('Expected image or string, got ' + typeof input);
}

// Variables named `tsFix` are only here because TypeScript hates Promise-returning functions.
return tsFix;
export function loadImages(inputs: Input[], attributes: Attributes = {}): Readonly<{
results: Result[];
loaded: () => Promise<HTMLImageElement[]>;
metadata: () => Promise<HTMLImageElement[]>;
}> {
if (Array.isArray(inputs)) {
const results = inputs.map(input => loadImage(input, attributes));
return Object.freeze({
results,
loaded: lazyPromise(async () => Promise.all(results.map(result => result.loaded()))),
metadata: lazyPromise(async () => Promise.all(results.map(result => result.metadata()))),
});
}

const tsFix = Promise.reject(new TypeError('input is not an image, a URL string, or an array of them.'));
return tsFix;
throw new TypeError('Expected array; got ' + typeof inputs);
}

export default loadImages;
14 changes: 8 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
"vanilla"
],
"repository": "fregante/image-promise",
"funding": "https://github.com/sponsors/fregante",
"license": "MIT",
"author": "Federico Brigante <[email protected]> (bfred.it)",
"author": "Federico Brigante <[email protected]> (https://fregante.com)",
"type": "module",
"module": "index.js",
"exports": "./index.js",
"main": "./index.js",
"types": "./index.d.ts",
"files": [
"index.js",
Expand All @@ -43,9 +45,9 @@
}
},
"devDependencies": {
"@sindresorhus/tsconfig": "^0.7.0",
"type-fest": "^0.13.1",
"typescript": "^3.8.3",
"xo": "^0.30.0"
"@sindresorhus/tsconfig": "^3.0.1",
"type-fest": "^3.10.0",
"typescript": "^5.0.4",
"xo": "^0.54.2"
}
}
14 changes: 5 additions & 9 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,25 @@ It can be used in two ways:
- pass a URL: it will generate an `<img>` and wait for it to load:

```js
loadImage('img.jpg').then(/* It's loaded! */)
loadImage('img.jpg').loaded.then(/* It's loaded! */)
```

- pass an `<img>`: it will wait for it to load:

```js
const img = document.querySelector('img.my-image');
loadImage(img).then(/* It's loaded! */)
loadImage(img).loaded.then(/* It's loaded! */)
```

- pass an array of URLs and/or `<img>`s, wait for them to load:
- pass an array of URLs and/or `<img>` to `loadImages`, wait for them to load:

```js
const img = document.querySelector('img.my-image');
loadImage([img, 'loading.gif']).then(/* Both are loaded! */)
loadImages([img, 'loading.gif']).then(/* Both are loaded! */)
```

## Install

You can download the [standalone bundle](https://bundle.fregante.com/?pkg=image-promise&global=loadImage)

Or use `npm`:

```sh
npm install image-promise
```
Expand Down Expand Up @@ -111,4 +107,4 @@ None! But you need to polyfill `window.Promise` in IE11 and lower.

## License

MIT © [Federico Brigante](https://bfred.it)
MIT © [Federico Brigante](https://fregante.com)
8 changes: 1 addition & 7 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
{
"extends": "@sindresorhus/tsconfig",
"compilerOptions": {
"outDir": ".",
"target": "es5",
"module": "es2015",
"lib": [
"es2015",
"dom"
]
"outDir": "."
},
"files": [
"index.ts"
Expand Down

0 comments on commit 699afaa

Please sign in to comment.