From 44f07003323d231e7cb60047097490a589c24fd7 Mon Sep 17 00:00:00 2001 From: Jake Archibald Date: Tue, 7 Aug 2018 12:19:06 +0100 Subject: [PATCH] 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 --- package-lock.json | 6 ++-- src/components/App/index.tsx | 28 +++++---------- src/components/Options/index.tsx | 23 +++++------- src/lib/clean-modify.ts | 62 ++++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 37 deletions(-) create mode 100644 src/lib/clean-modify.ts diff --git a/package-lock.json b/package-lock.json index 5fa73ac2..d84fe23d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7904,7 +7904,7 @@ }, "onetime": { "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=", "dev": true }, @@ -8058,7 +8058,7 @@ }, "onetime": { "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=", "dev": true }, @@ -9414,7 +9414,7 @@ }, "onetime": { "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=", "dev": true }, diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index a1f58d5b..5c74700c 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -35,6 +35,7 @@ import { } from '../../codecs/preprocessors'; import { decodeImage } from '../../codecs/decoders'; +import { cleanMerge } from '../../lib/clean-modify'; interface SourceImage { file: File; @@ -155,9 +156,6 @@ export default class App extends Component { type: EncoderType, options?: EncoderOptions, ): void { - const images = this.state.images.slice() as [EncodedImage, EncodedImage]; - const oldImage = images[index]; - // Some type cheating here. // encoderMap[type].defaultOptions is always safe. // options should always be correct for the type, but TypeScript isn't smart enough. @@ -166,12 +164,7 @@ export default class App extends Component { options: options || encoderMap[type].defaultOptions, } as EncoderState; - images[index] = { - ...oldImage, - encoderState, - preprocessorState, - }; - + const images = cleanMerge(this.state.images, index, { encoderState, preprocessorState }); this.setState({ images }); } @@ -250,19 +243,19 @@ export default class App extends Component { async updateImage(index: number): Promise { const { source } = this.state; if (!source) return; - let images = this.state.images.slice() as [EncodedImage, EncodedImage]; // 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] = { - ...images[index], + let images = cleanMerge(this.state.images, index, { loadingCounter, loading: true, - }; + }); this.setState({ images }); + const image = images[index]; + let file; try { source.preprocessed = await preprocessImage(source, image.preprocessorState); @@ -286,16 +279,13 @@ export default class App extends Component { throw err; } - images = this.state.images.slice() as [EncodedImage, EncodedImage]; - - images[index] = { - ...images[index], + images = cleanMerge(this.state.images, '' + index, { file, bmp, downloadUrl: URL.createObjectURL(file), loading: images[index].loadingCounter !== loadingCounter, loadedCounter: loadingCounter, - }; + }); this.setState({ images }); } diff --git a/src/components/Options/index.tsx b/src/components/Options/index.tsx index 4fe0675b..955b7d0a 100644 --- a/src/components/Options/index.tsx +++ b/src/components/Options/index.tsx @@ -1,6 +1,8 @@ import { h, Component } from 'preact'; + import * as style from './style.scss'; import { bind } from '../../lib/util'; +import { cleanSet, cleanMerge } from '../../lib/clean-modify'; import MozJpegEncoderOptions from '../../codecs/mozjpeg/options'; import BrowserJPEGEncoderOptions from '../../codecs/browser-jpeg/options'; import WebPEncoderOptions from '../../codecs/webp/options'; @@ -80,27 +82,18 @@ export default class Options extends Component { @bind onPreprocessorEnabledChange(event: Event) { const el = event.currentTarget as HTMLInputElement; - const preprocessor = el.name.split('.')[0] as keyof PreprocessorState; - this.props.onPreprocessorOptionsChange({ - ...this.props.preprocessorState, - [preprocessor]: { - ...this.props.preprocessorState[preprocessor], - enabled: el.checked, - }, - }); + this.props.onPreprocessorOptionsChange( + cleanSet(this.props.preprocessorState, `${preprocessor}.enabled`, el.checked), + ); } @bind onQuantizerOptionsChange(opts: QuantizeOptions) { - this.props.onPreprocessorOptionsChange({ - ...this.props.preprocessorState, - quantizer: { - ...opts, - enabled: this.props.preprocessorState.quantizer.enabled, - }, - }); + this.props.onPreprocessorOptionsChange( + cleanMerge(this.props.preprocessorState, 'quantizer', opts), + ); } render( diff --git a/src/lib/clean-modify.ts b/src/lib/clean-modify.ts new file mode 100644 index 00000000..ed750ed6 --- /dev/null +++ b/src/lib/clean-modify.ts @@ -0,0 +1,62 @@ +function cleanSetOrMerge( + 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(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( + 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( + source: A, + keys: string | number | string[], + newValue: any, +): A { + return cleanSetOrMerge(source, keys, newValue, false); +}