Adding clean-set (#124)

* Adding clean-set

* Moving to our own cleanSet and cleanMerge.

* Oops, this can be simpler

* Allow the path to be a number

* Better typing
This commit is contained in:
Jake Archibald
2018-08-07 12:19:06 +01:00
committed by GitHub
parent c90db020b0
commit 44f0700332
4 changed files with 82 additions and 37 deletions

6
package-lock.json generated
View File

@@ -7904,7 +7904,7 @@
}, },
"onetime": { "onetime": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
"dev": true "dev": true
}, },
@@ -8058,7 +8058,7 @@
}, },
"onetime": { "onetime": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
"dev": true "dev": true
}, },
@@ -9414,7 +9414,7 @@
}, },
"onetime": { "onetime": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
"dev": true "dev": true
}, },

View File

@@ -35,6 +35,7 @@ import {
} from '../../codecs/preprocessors'; } from '../../codecs/preprocessors';
import { decodeImage } from '../../codecs/decoders'; import { decodeImage } from '../../codecs/decoders';
import { cleanMerge } from '../../lib/clean-modify';
interface SourceImage { interface SourceImage {
file: File; file: File;
@@ -155,9 +156,6 @@ export default class App extends Component<Props, State> {
type: EncoderType, type: EncoderType,
options?: EncoderOptions, options?: EncoderOptions,
): void { ): void {
const images = this.state.images.slice() as [EncodedImage, EncodedImage];
const oldImage = images[index];
// Some type cheating here. // Some type cheating here.
// encoderMap[type].defaultOptions is always safe. // encoderMap[type].defaultOptions is always safe.
// options should always be correct for the type, but TypeScript isn't smart enough. // options should always be correct for the type, but TypeScript isn't smart enough.
@@ -166,12 +164,7 @@ export default class App extends Component<Props, State> {
options: options || encoderMap[type].defaultOptions, options: options || encoderMap[type].defaultOptions,
} as EncoderState; } as EncoderState;
images[index] = { const images = cleanMerge(this.state.images, index, { encoderState, preprocessorState });
...oldImage,
encoderState,
preprocessorState,
};
this.setState({ images }); this.setState({ images });
} }
@@ -250,19 +243,19 @@ export default class App extends Component<Props, State> {
async updateImage(index: number): Promise<void> { async updateImage(index: number): Promise<void> {
const { source } = this.state; const { source } = this.state;
if (!source) return; if (!source) return;
let images = this.state.images.slice() as [EncodedImage, EncodedImage];
// Each time we trigger an async encode, the counter changes. // Each time we trigger an async encode, the counter changes.
const loadingCounter = images[index].loadingCounter + 1; const loadingCounter = this.state.images[index].loadingCounter + 1;
const image = images[index] = { let images = cleanMerge(this.state.images, index, {
...images[index],
loadingCounter, loadingCounter,
loading: true, loading: true,
}; });
this.setState({ images }); this.setState({ images });
const image = images[index];
let file; let file;
try { try {
source.preprocessed = await preprocessImage(source, image.preprocessorState); source.preprocessed = await preprocessImage(source, image.preprocessorState);
@@ -286,16 +279,13 @@ export default class App extends Component<Props, State> {
throw err; throw err;
} }
images = this.state.images.slice() as [EncodedImage, EncodedImage]; images = cleanMerge(this.state.images, '' + index, {
images[index] = {
...images[index],
file, file,
bmp, bmp,
downloadUrl: URL.createObjectURL(file), downloadUrl: URL.createObjectURL(file),
loading: images[index].loadingCounter !== loadingCounter, loading: images[index].loadingCounter !== loadingCounter,
loadedCounter: loadingCounter, loadedCounter: loadingCounter,
}; });
this.setState({ images }); this.setState({ images });
} }

View File

@@ -1,6 +1,8 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import * as style from './style.scss'; import * as style from './style.scss';
import { bind } from '../../lib/util'; import { bind } from '../../lib/util';
import { cleanSet, cleanMerge } from '../../lib/clean-modify';
import MozJpegEncoderOptions from '../../codecs/mozjpeg/options'; import MozJpegEncoderOptions from '../../codecs/mozjpeg/options';
import BrowserJPEGEncoderOptions from '../../codecs/browser-jpeg/options'; import BrowserJPEGEncoderOptions from '../../codecs/browser-jpeg/options';
import WebPEncoderOptions from '../../codecs/webp/options'; import WebPEncoderOptions from '../../codecs/webp/options';
@@ -80,27 +82,18 @@ export default class Options extends Component<Props, State> {
@bind @bind
onPreprocessorEnabledChange(event: Event) { onPreprocessorEnabledChange(event: Event) {
const el = event.currentTarget as HTMLInputElement; const el = event.currentTarget as HTMLInputElement;
const preprocessor = el.name.split('.')[0] as keyof PreprocessorState; const preprocessor = el.name.split('.')[0] as keyof PreprocessorState;
this.props.onPreprocessorOptionsChange({ this.props.onPreprocessorOptionsChange(
...this.props.preprocessorState, cleanSet(this.props.preprocessorState, `${preprocessor}.enabled`, el.checked),
[preprocessor]: { );
...this.props.preprocessorState[preprocessor],
enabled: el.checked,
},
});
} }
@bind @bind
onQuantizerOptionsChange(opts: QuantizeOptions) { onQuantizerOptionsChange(opts: QuantizeOptions) {
this.props.onPreprocessorOptionsChange({ this.props.onPreprocessorOptionsChange(
...this.props.preprocessorState, cleanMerge(this.props.preprocessorState, 'quantizer', opts),
quantizer: { );
...opts,
enabled: this.props.preprocessorState.quantizer.enabled,
},
});
} }
render( render(

62
src/lib/clean-modify.ts Normal file
View File

@@ -0,0 +1,62 @@
function cleanSetOrMerge<A extends any[] | object>(
source: A,
keys: string | number | string[],
toSetOrMerge: any[] | object,
merge: boolean,
): A {
const splitKeys = Array.isArray(keys) ? keys : ('' + keys).split('.');
// Going off road in terms of types, otherwise TypeScript doesn't like the access-by-index.
// The assumptions in this code break if the object contains things which aren't arrays or
// plain objects.
let last = copy(source) as any;
const newObject = last;
const lastIndex = splitKeys.length - 1;
for (const [i, key] of splitKeys.entries()) {
if (i !== lastIndex) {
// Copy everything along the path.
last = last[key] = copy(last[key]);
} else {
// Merge or set.
last[key] = merge ?
Object.assign(copy(last[key]), toSetOrMerge) :
toSetOrMerge;
}
}
return newObject;
}
function copy<A extends any[] | object>(source: A): A {
// Some type cheating here, as TypeScript can't infer between generic types.
if (Array.isArray(source)) return [...source] as any;
return { ...(source as any) };
}
/**
* @param source Object to copy from.
* @param keys Path to modify, eg "foo.bar.baz".
* @param toMerge A value to merge into the value at the path.
*/
export function cleanMerge<A extends any[] | object>(
source: A,
keys: string | number | string[],
toMerge: any[] | object,
): A {
return cleanSetOrMerge(source, keys, toMerge, true);
}
/**
* @param source Object to copy from.
* @param keys Path to modify, eg "foo.bar.baz".
* @param newValue A value to set at the path.
*/
export function cleanSet<A extends any[] | object>(
source: A,
keys: string | number | string[],
newValue: any,
): A {
return cleanSetOrMerge(source, keys, newValue, false);
}