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,11 +77,21 @@ 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,
selection: undefined,
hash,
error: undefined,
loading: true,
url,
}),
);
const data = hash
? await loadGedcom(hash, gedcom, images)
: await loadFromUrl(url!, handleCors);
// Set state with data. // Set state with data.
this.setState( this.setState(
Object.assign({}, this.state, { Object.assign({}, this.state, {
@@ -94,8 +104,7 @@ export class App extends React.Component<RouteComponentProps, {}> {
showSidePanel, showSidePanel,
}), }),
); );
}, } catch (error) {
(error) => {
// Set error state. // Set error state.
this.setState( this.setState(
Object.assign({}, this.state, { Object.assign({}, this.state, {
@@ -103,19 +112,7 @@ export class App extends React.Component<RouteComponentProps, {}> {
loading: false, loading: false,
}), }),
); );
}, }
);
// Set loading state.
this.setState(
Object.assign({}, this.state, {
data: undefined,
selection: undefined,
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,28 +241,26 @@ 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');
});
} }
drawOnCanvas(): Promise<HTMLCanvasElement> { private async drawOnCanvas(): Promise<HTMLCanvasElement> {
return this.getSvgContentsWithInlinedImages() const contents = await this.getSvgContentsWithInlinedImages();
.then((contents) => new Blob([contents], {type: 'image/svg+xml'})) const blob = new Blob([contents], {type: 'image/svg+xml'});
.then(loadImage) return await drawOnCanvas(await loadImage(blob));
.then(drawOnCanvas);
} }
downloadPng() { async downloadPng() {
this.drawOnCanvas() const canvas = await this.drawOnCanvas();
.then((canvas) => canvasToBlob(canvas, 'image/png')) const blob = await canvasToBlob(canvas, 'image/png');
.then((blob) => saveAs(blob, 'topola.png')); saveAs(blob, 'topola.png');
} }
downloadPdf() { async downloadPdf() {
this.drawOnCanvas().then((canvas) => { const canvas = await this.drawOnCanvas();
const doc = new jsPDF({ const doc = new jsPDF({
orientation: canvas.width > canvas.height ? 'l' : 'p', orientation: canvas.width > canvas.height ? 'l' : 'p',
unit: 'pt', unit: 'pt',
@@ -272,6 +268,5 @@ export class Chart extends React.PureComponent<ChartProps, {}> {
}); });
doc.addImage(canvas, 'PNG', 0, 0, canvas.width, canvas.height, 'NONE'); doc.addImage(canvas, 'PNG', 0, 0, canvas.width, canvas.height, 'NONE');
doc.save('topola.pdf'); 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)
.then((response) => {
if (response.status !== 200) { if (response.status !== 200) {
return Promise.reject(new Error(response.statusText)); throw new Error(response.statusText);
} }
return response.text(); const gedcom = await response.text();
})
.then((gedcom) => {
return prepareData(gedcom, url); 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,7 +78,8 @@ 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 data = await loadFileAsText(gedcomFile);
const imageFileNames = images const imageFileNames = images
.map((image) => image.name) .map((image) => image.name)
.sort() .sort()
@@ -98,8 +91,6 @@ export class TopBar extends React.Component<
search: queryString.stringify({file: hash}), search: queryString.stringify({file: hash}),
state: {data, images: imageMap}, 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. */