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();
}
componentDidUpdate() {
async componentDidUpdate() {
if (this.props.location.pathname !== '/view') {
return;
}
@@ -77,45 +77,42 @@ export class App extends React.Component<RouteComponentProps, {}> {
if (!url && !hash) {
this.props.history.replace({pathname: '/'});
} else if (this.isNewData(hash, url)) {
const loadedData = hash
? loadGedcom(hash, gedcom, images)
: loadFromUrl(url!, handleCors);
loadedData.then(
(data) => {
// Set state with data.
this.setState(
Object.assign({}, this.state, {
data,
hash,
selection: getSelection(data.chartData, indi, generation),
error: undefined,
loading: false,
url,
showSidePanel,
}),
);
},
(error) => {
// Set error state.
this.setState(
Object.assign({}, this.state, {
error: error.message,
loading: false,
}),
);
},
);
// Set loading state.
this.setState(
Object.assign({}, this.state, {
data: undefined,
selection: undefined,
hash,
error: undefined,
loading: true,
url,
}),
);
try {
// Set loading state.
this.setState(
Object.assign({}, this.state, {
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.
this.setState(
Object.assign({}, this.state, {
data,
hash,
selection: getSelection(data.chartData, indi, generation),
error: undefined,
loading: false,
url,
showSidePanel,
}),
);
} catch (error) {
// Set error state.
this.setState(
Object.assign({}, this.state, {
error: error.message,
loading: false,
}),
);
}
} else if (this.state.data && this.state.selection) {
// Update selection if it has changed in the URL.
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
* URLs. Images are replaced in place. The replacement is done, the returned
* promise is resolved.
*/
function inlineImages(svg: Element): Promise<void[]> {
async function inlineImages(svg: Element): Promise<void> {
const images = Array.from(svg.getElementsByTagName('image'));
const promises = images.map((image) => {
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);
await Promise.all(images.map(inlineImage));
}
/** Loads a blob into an image object. */
@@ -67,9 +68,7 @@ function loadImage(blob: Blob): Promise<HTMLImageElement> {
const image = new Image();
image.src = URL.createObjectURL(blob);
return new Promise<HTMLImageElement>((resolve, reject) => {
image.addEventListener('load', () => {
resolve(image);
});
image.addEventListener('load', () => resolve(image));
});
}
@@ -215,12 +214,11 @@ export class Chart extends React.PureComponent<ChartProps, {}> {
return new XMLSerializer().serializeToString(svg);
}
private getSvgContentsWithInlinedImages() {
private async getSvgContentsWithInlinedImages() {
const svg = document.getElementById('chart')!.cloneNode(true) as Element;
svg.removeAttribute('transform');
return inlineImages(svg).then(() =>
new XMLSerializer().serializeToString(svg),
);
await inlineImages(svg);
return new XMLSerializer().serializeToString(svg);
}
/** 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);
}
downloadSvg() {
this.getSvgContentsWithInlinedImages().then((contents) => {
const blob = new Blob([contents], {type: 'image/svg+xml'});
saveAs(blob, 'topola.svg');
});
}
drawOnCanvas(): Promise<HTMLCanvasElement> {
return this.getSvgContentsWithInlinedImages()
.then((contents) => new Blob([contents], {type: 'image/svg+xml'}))
.then(loadImage)
.then(drawOnCanvas);
}
downloadPng() {
this.drawOnCanvas()
.then((canvas) => canvasToBlob(canvas, 'image/png'))
.then((blob) => saveAs(blob, 'topola.png'));
}
downloadPdf() {
this.drawOnCanvas().then((canvas) => {
const doc = new jsPDF({
orientation: canvas.width > canvas.height ? 'l' : 'p',
unit: 'pt',
format: [canvas.width, canvas.height],
});
doc.addImage(canvas, 'PNG', 0, 0, canvas.width, canvas.height, 'NONE');
doc.save('topola.pdf');
async downloadSvg() {
const contents = await this.getSvgContentsWithInlinedImages();
const blob = new Blob([contents], {type: 'image/svg+xml'});
saveAs(blob, 'topola.svg');
}
private async drawOnCanvas(): Promise<HTMLCanvasElement> {
const contents = await this.getSvgContentsWithInlinedImages();
const blob = new Blob([contents], {type: 'image/svg+xml'});
return await drawOnCanvas(await loadImage(blob));
}
async downloadPng() {
const canvas = await this.drawOnCanvas();
const blob = await canvasToBlob(canvas, 'image/png');
saveAs(blob, 'topola.png');
}
async downloadPdf() {
const canvas = await this.drawOnCanvas();
const doc = new jsPDF({
orientation: canvas.width > canvas.height ? 'l' : 'p',
unit: 'pt',
format: [canvas.width, canvas.height],
});
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. */
export function loadFromUrl(
export async function loadFromUrl(
url: string,
handleCors: boolean,
): Promise<TopolaData> {
const cachedData = sessionStorage.getItem(url);
if (cachedData) {
return Promise.resolve(JSON.parse(cachedData));
return JSON.parse(cachedData);
}
const urlToFetch = handleCors
? 'https://cors-anywhere.herokuapp.com/' + url
: url;
return window
.fetch(urlToFetch)
.then((response) => {
if (response.status !== 200) {
return Promise.reject(new Error(response.statusText));
}
return response.text();
})
.then((gedcom) => {
return prepareData(gedcom, url);
});
const response = await window.fetch(urlToFetch);
if (response.status !== 200) {
throw new Error(response.statusText);
}
const gedcom = await response.text();
return prepareData(gedcom, url);
}
/** Loads data from the given GEDCOM file contents. */
function loadGedcomSync(
export async function loadGedcom(
hash: string,
gedcom?: string,
images?: Map<string, string>,
) {
): Promise<TopolaData> {
const cachedData = sessionStorage.getItem(hash);
if (cachedData) {
return JSON.parse(cachedData);
@@ -72,16 +67,3 @@ function loadGedcomSync(
}
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) {
const lower = fileName.toLowerCase();
return lower.endsWith('.jpg') || lower.endsWith('.png');
@@ -62,12 +52,14 @@ export class TopBar extends React.Component<
inputRef?: Input;
/** Handles the "Upload file" button. */
handleUpload(event: React.SyntheticEvent<HTMLInputElement>) {
async handleUpload(event: React.SyntheticEvent<HTMLInputElement>) {
const files = (event.target as HTMLInputElement).files;
if (!files || !files.length) {
return;
}
const filesArray = Array.from(files);
(event.target as HTMLInputElement).value = ''; // Reset the file input.
const gedcomFile =
files.length === 1
? files[0]
@@ -86,20 +78,19 @@ export class TopBar extends React.Component<
const imageMap = new Map(
images.map((entry) => [entry.name, entry.url] as [string, string]),
);
loadFileAsText(gedcomFile).then((data) => {
const imageFileNames = images
.map((image) => image.name)
.sort()
.join('|');
// Hash GEDCOM contents with uploaded image file names.
const hash = md5(md5(data) + imageFileNames);
this.props.history.push({
pathname: '/view',
search: queryString.stringify({file: hash}),
state: {data, images: imageMap},
});
const data = await loadFileAsText(gedcomFile);
const imageFileNames = images
.map((image) => image.name)
.sort()
.join('|');
// Hash GEDCOM contents with uploaded image file names.
const hash = md5(md5(data) + imageFileNames);
this.props.history.push({
pathname: '/view',
search: queryString.stringify({file: hash}),
state: {data, images: imageMap},
});
(event.target as HTMLInputElement).value = ''; // Reset the file input.
}
/** Opens the "Load from URL" dialog. */