Use async/await for async functions.

This commit is contained in:
Przemek Wiech
2019-03-16 23:53:20 +01:00
parent 750cb394e7
commit 25c5438a04
4 changed files with 109 additions and 144 deletions

View File

@@ -54,7 +54,7 @@ export class App extends React.Component<RouteComponentProps, {}> {
this.componentDidUpdate(); this.componentDidUpdate();
} }
componentDidUpdate() { async componentDidUpdate() {
if (this.props.location.pathname !== '/view') { if (this.props.location.pathname !== '/view') {
return; return;
} }
@@ -77,45 +77,42 @@ export class App extends React.Component<RouteComponentProps, {}> {
if (!url && !hash) { if (!url && !hash) {
this.props.history.replace({pathname: '/'}); this.props.history.replace({pathname: '/'});
} else if (this.isNewData(hash, url)) { } else if (this.isNewData(hash, url)) {
const loadedData = hash try {
? loadGedcom(hash, gedcom, images) // Set loading state.
: loadFromUrl(url!, handleCors); this.setState(
loadedData.then( Object.assign({}, this.state, {
(data) => { data: undefined,
// Set state with data. selection: undefined,
this.setState( hash,
Object.assign({}, this.state, { error: undefined,
data, loading: true,
hash, url,
selection: getSelection(data.chartData, indi, generation), }),
error: undefined, );
loading: false, const data = hash
url, ? await loadGedcom(hash, gedcom, images)
showSidePanel, : await loadFromUrl(url!, handleCors);
}), // Set state with data.
); this.setState(
}, Object.assign({}, this.state, {
(error) => { data,
// Set error state. hash,
this.setState( selection: getSelection(data.chartData, indi, generation),
Object.assign({}, this.state, { error: undefined,
error: error.message, loading: false,
loading: false, url,
}), showSidePanel,
); }),
}, );
); } catch (error) {
// Set loading state. // Set error state.
this.setState( this.setState(
Object.assign({}, this.state, { Object.assign({}, this.state, {
data: undefined, error: error.message,
selection: undefined, loading: false,
hash, }),
error: undefined, );
loading: true, }
url,
}),
);
} else if (this.state.data && this.state.selection) { } else if (this.state.data && this.state.selection) {
// Update selection if it has changed in the URL. // Update selection if it has changed in the URL.
const selection = getSelection( const selection = getSelection(

View File

@@ -38,28 +38,29 @@ function loadAsDataUrl(blob: Blob): Promise<string> {
}); });
} }
async function inlineImage(image: SVGImageElement) {
const href = image.href.baseVal;
if (!href) {
return;
}
try {
const response = await fetch(href);
const blob = await response.blob();
const dataUrl = await loadAsDataUrl(blob);
image.href.baseVal = dataUrl;
} catch (e) {
console.warn('Failed to load image:', e);
}
}
/** /**
* Fetches all images in the SVG and replaces them with inlined images as data * Fetches all images in the SVG and replaces them with inlined images as data
* URLs. Images are replaced in place. The replacement is done, the returned * URLs. Images are replaced in place. The replacement is done, the returned
* promise is resolved. * promise is resolved.
*/ */
function inlineImages(svg: Element): Promise<void[]> { async function inlineImages(svg: Element): Promise<void> {
const images = Array.from(svg.getElementsByTagName('image')); const images = Array.from(svg.getElementsByTagName('image'));
const promises = images.map((image) => { await Promise.all(images.map(inlineImage));
const href = image.href && image.href.baseVal;
if (!href) {
return Promise.resolve();
}
return fetch(href)
.then((response) => response.blob())
.then(loadAsDataUrl)
.then((dataUrl) => {
image.href.baseVal = dataUrl;
})
// Log and ignore errors.
.catch((e) => console.warn('Failed to load image:', e));
});
return Promise.all(promises);
} }
/** Loads a blob into an image object. */ /** Loads a blob into an image object. */
@@ -67,9 +68,7 @@ function loadImage(blob: Blob): Promise<HTMLImageElement> {
const image = new Image(); const image = new Image();
image.src = URL.createObjectURL(blob); image.src = URL.createObjectURL(blob);
return new Promise<HTMLImageElement>((resolve, reject) => { return new Promise<HTMLImageElement>((resolve, reject) => {
image.addEventListener('load', () => { image.addEventListener('load', () => resolve(image));
resolve(image);
});
}); });
} }
@@ -215,12 +214,11 @@ export class Chart extends React.PureComponent<ChartProps, {}> {
return new XMLSerializer().serializeToString(svg); return new XMLSerializer().serializeToString(svg);
} }
private getSvgContentsWithInlinedImages() { private async getSvgContentsWithInlinedImages() {
const svg = document.getElementById('chart')!.cloneNode(true) as Element; const svg = document.getElementById('chart')!.cloneNode(true) as Element;
svg.removeAttribute('transform'); svg.removeAttribute('transform');
return inlineImages(svg).then(() => await inlineImages(svg);
new XMLSerializer().serializeToString(svg), return new XMLSerializer().serializeToString(svg);
);
} }
/** Shows the print dialog to print the currently displayed chart. */ /** Shows the print dialog to print the currently displayed chart. */
@@ -243,35 +241,32 @@ export class Chart extends React.PureComponent<ChartProps, {}> {
document.body.appendChild(printWindow); document.body.appendChild(printWindow);
} }
downloadSvg() { async downloadSvg() {
this.getSvgContentsWithInlinedImages().then((contents) => { const contents = await this.getSvgContentsWithInlinedImages();
const blob = new Blob([contents], {type: 'image/svg+xml'}); const blob = new Blob([contents], {type: 'image/svg+xml'});
saveAs(blob, 'topola.svg'); saveAs(blob, 'topola.svg');
}); }
}
private async drawOnCanvas(): Promise<HTMLCanvasElement> {
drawOnCanvas(): Promise<HTMLCanvasElement> { const contents = await this.getSvgContentsWithInlinedImages();
return this.getSvgContentsWithInlinedImages() const blob = new Blob([contents], {type: 'image/svg+xml'});
.then((contents) => new Blob([contents], {type: 'image/svg+xml'})) return await drawOnCanvas(await loadImage(blob));
.then(loadImage) }
.then(drawOnCanvas);
} async downloadPng() {
const canvas = await this.drawOnCanvas();
downloadPng() { const blob = await canvasToBlob(canvas, 'image/png');
this.drawOnCanvas() saveAs(blob, 'topola.png');
.then((canvas) => canvasToBlob(canvas, 'image/png')) }
.then((blob) => saveAs(blob, 'topola.png'));
} async downloadPdf() {
const canvas = await this.drawOnCanvas();
downloadPdf() { const doc = new jsPDF({
this.drawOnCanvas().then((canvas) => { orientation: canvas.width > canvas.height ? 'l' : 'p',
const doc = new jsPDF({ unit: 'pt',
orientation: canvas.width > canvas.height ? 'l' : 'p', format: [canvas.width, canvas.height],
unit: 'pt',
format: [canvas.width, canvas.height],
});
doc.addImage(canvas, 'PNG', 0, 0, canvas.width, canvas.height, 'NONE');
doc.save('topola.pdf');
}); });
doc.addImage(canvas, 'PNG', 0, 0, canvas.width, canvas.height, 'NONE');
doc.save('topola.pdf');
} }
} }

View File

@@ -32,37 +32,32 @@ function prepareData(
} }
/** Fetches data from the given URL. Uses cors-anywhere if handleCors is true. */ /** Fetches data from the given URL. Uses cors-anywhere if handleCors is true. */
export function loadFromUrl( export async function loadFromUrl(
url: string, url: string,
handleCors: boolean, handleCors: boolean,
): Promise<TopolaData> { ): Promise<TopolaData> {
const cachedData = sessionStorage.getItem(url); const cachedData = sessionStorage.getItem(url);
if (cachedData) { if (cachedData) {
return Promise.resolve(JSON.parse(cachedData)); return JSON.parse(cachedData);
} }
const urlToFetch = handleCors const urlToFetch = handleCors
? 'https://cors-anywhere.herokuapp.com/' + url ? 'https://cors-anywhere.herokuapp.com/' + url
: url; : url;
return window const response = await window.fetch(urlToFetch);
.fetch(urlToFetch) if (response.status !== 200) {
.then((response) => { throw new Error(response.statusText);
if (response.status !== 200) { }
return Promise.reject(new Error(response.statusText)); const gedcom = await response.text();
} return prepareData(gedcom, url);
return response.text();
})
.then((gedcom) => {
return prepareData(gedcom, url);
});
} }
/** Loads data from the given GEDCOM file contents. */ /** Loads data from the given GEDCOM file contents. */
function loadGedcomSync( export async function loadGedcom(
hash: string, hash: string,
gedcom?: string, gedcom?: string,
images?: Map<string, string>, images?: Map<string, string>,
) { ): Promise<TopolaData> {
const cachedData = sessionStorage.getItem(hash); const cachedData = sessionStorage.getItem(hash);
if (cachedData) { if (cachedData) {
return JSON.parse(cachedData); return JSON.parse(cachedData);
@@ -72,16 +67,3 @@ function loadGedcomSync(
} }
return prepareData(gedcom, hash, images); return prepareData(gedcom, hash, images);
} }
/** Loads data from the given GEDCOM file contents. */
export function loadGedcom(
hash: string,
gedcom?: string,
images?: Map<string, string>,
): Promise<TopolaData> {
try {
return Promise.resolve(loadGedcomSync(hash, gedcom, images));
} catch (e) {
return Promise.reject(new Error('Failed to read GEDCOM file'));
}
}

View File

@@ -39,16 +39,6 @@ function loadFileAsText(file: File): Promise<string> {
}); });
} }
function loadFileAsDataUrl(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (evt: ProgressEvent) => {
resolve((evt.target as FileReader).result as string);
};
reader.readAsDataURL(file);
});
}
function isImageFileName(fileName: string) { function isImageFileName(fileName: string) {
const lower = fileName.toLowerCase(); const lower = fileName.toLowerCase();
return lower.endsWith('.jpg') || lower.endsWith('.png'); return lower.endsWith('.jpg') || lower.endsWith('.png');
@@ -62,12 +52,14 @@ export class TopBar extends React.Component<
inputRef?: Input; inputRef?: Input;
/** Handles the "Upload file" button. */ /** Handles the "Upload file" button. */
handleUpload(event: React.SyntheticEvent<HTMLInputElement>) { async handleUpload(event: React.SyntheticEvent<HTMLInputElement>) {
const files = (event.target as HTMLInputElement).files; const files = (event.target as HTMLInputElement).files;
if (!files || !files.length) { if (!files || !files.length) {
return; return;
} }
const filesArray = Array.from(files); const filesArray = Array.from(files);
(event.target as HTMLInputElement).value = ''; // Reset the file input.
const gedcomFile = const gedcomFile =
files.length === 1 files.length === 1
? files[0] ? files[0]
@@ -86,20 +78,19 @@ export class TopBar extends React.Component<
const imageMap = new Map( const imageMap = new Map(
images.map((entry) => [entry.name, entry.url] as [string, string]), images.map((entry) => [entry.name, entry.url] as [string, string]),
); );
loadFileAsText(gedcomFile).then((data) => {
const imageFileNames = images const data = await loadFileAsText(gedcomFile);
.map((image) => image.name) const imageFileNames = images
.sort() .map((image) => image.name)
.join('|'); .sort()
// Hash GEDCOM contents with uploaded image file names. .join('|');
const hash = md5(md5(data) + imageFileNames); // Hash GEDCOM contents with uploaded image file names.
this.props.history.push({ const hash = md5(md5(data) + imageFileNames);
pathname: '/view', this.props.history.push({
search: queryString.stringify({file: hash}), pathname: '/view',
state: {data, images: imageMap}, search: queryString.stringify({file: hash}),
}); state: {data, images: imageMap},
}); });
(event.target as HTMLInputElement).value = ''; // Reset the file input.
} }
/** Opens the "Load from URL" dialog. */ /** Opens the "Load from URL" dialog. */