diff --git a/dev/index.html b/dev/index.html index 297d7b7..67eb656 100644 --- a/dev/index.html +++ b/dev/index.html @@ -1,8 +1,83 @@ + - + + + + + + + diff --git a/package.json b/package.json index 0dc3564..68940d6 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "homepage": "https://github.com/urdenko/print-wanted#readme", "main": "dist/main.js", "devDependencies": { + "@types/css-font-loading-module": "0.0.4", "@typescript-eslint/eslint-plugin": "^2.27.0", "@typescript-eslint/parser": "^2.27.0", "eslint": "^6.8.0", @@ -29,6 +30,8 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.1", + "html-loader": "^1.1.0", + "ts-loader": "^7.0.1", "typescript": "^3.8.3", "webpack": "^4.42.1", "webpack-cli": "^3.3.11", diff --git a/src/font-loader.ts b/src/font-loader.ts new file mode 100644 index 0000000..bce8af1 --- /dev/null +++ b/src/font-loader.ts @@ -0,0 +1,38 @@ +export class FontLoader { + public static async loadAllFonts(fonts: string[], text: string[]): Promise { + const fontRequest = fonts.map((font) => `family=${font}`); + const normalizeFontText = this.normalizeFontText(text); + + const fontResponse = await fetch(`https://fonts.googleapis.com/css2?${fontRequest.join('&')}&display=swap&text=${normalizeFontText}`); + if (!fontResponse.ok) { + return; + } + + const rawText = (await fontResponse.text()).replace(/\n/g, ''); + const fontSections = rawText.match(/@font-face \{.+?\}/g); + + if (!fontSections) { + return; + } + + await Promise.all(fontSections.map(this.loadFont)); + } + + private static normalizeFontText(strings: string[]): string { + const uniqueChars = new Set(strings.join('').split('')); + return Array.from(uniqueChars).join('').replace(/\s+/, ''); + } + + private static async loadFont(fontSection: string) { + const fontUrl = fontSection.match(/url\(.+?\)/)?.[0]; + const fontName = fontSection.match(/font-family:\s*'(.+?)';/)?.[1]; + + if (!fontUrl || !fontName) { + return; + } + + const font = new FontFace(fontName, fontUrl); + await font.load(); + document.fonts.add(font); + } +} diff --git a/src/html.d.ts b/src/html.d.ts new file mode 100644 index 0000000..1198a9a --- /dev/null +++ b/src/html.d.ts @@ -0,0 +1,4 @@ +declare module '*.html' { + const value: string; + export default value; +} diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..c32f1ed --- /dev/null +++ b/src/index.html @@ -0,0 +1,93 @@ +
+
+
+ WANTED! +
+ +
+ For the following atrocity crimes: +
+
+ + + +
+ 😈 +
+ 😈 +
+ +
PREFERABLE ALIVE WITH ALL OF LIMBS
+ +
+ Reward from friends: +
+
+
+ + diff --git a/src/index.ts b/src/index.ts index 3f48a42..f11de27 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,92 @@ +import html from './index.html'; +import { FontLoader } from './font-loader'; + export class WantedPrintForm extends HTMLElement { + public readyEventName = 'form-ready'; + public errorEventName = 'form-error'; + + public wantedName!: string; + public image!: File; + public atrocityCrimes!: string; + public reward!: string; + + private componentShadowRoot!: ShadowRoot; + constructor() { super(); - const componentShadowRoot = this.attachShadow({ mode: 'open' }); + this.componentShadowRoot = this.attachShadow({ mode: 'open' }); const template = document.createElement('template'); - template.innerHTML = 'Wanted serious offender!'; - componentShadowRoot.appendChild(template.content.cloneNode(true)); + template.innerHTML = html; + this.componentShadowRoot.appendChild(template.content.cloneNode(true)); + } + + public async connectedCallback(): Promise { + try { + if (!this.isValidInputs()) { + throw new Error('Invalid input values!'); + } + + const atrocityCrimes = this.componentShadowRoot.getElementById('atrocity-crimes'); + atrocityCrimes && (atrocityCrimes.innerText = this.atrocityCrimes); + + const imageSrc = URL.createObjectURL(this.image); + const face = this.componentShadowRoot.getElementById('face') as HTMLImageElement | null; + + face && (await this.loadImage(imageSrc, face)); + + const wantedName = this.componentShadowRoot.getElementById('wanted-name'); + wantedName && (wantedName.innerText = this.wantedName); + + const reward = this.componentShadowRoot.getElementById('reward'); + reward && (reward.innerText = this.reward); + + const allText: string[] = this.getAllText(); + await FontLoader.loadAllFonts(['Patua One', 'Kalam'], allText); + + if (!this.isValidPaperSize()) { + throw new Error('Too match paper size!'); + } + } catch (error) { + const errorEvent = new CustomEvent(this.errorEventName, { detail: error }); + this.dispatchEvent(errorEvent); + return; + } + + const readyEvent = new CustomEvent(this.readyEventName); + this.dispatchEvent(readyEvent); + } + + private loadImage(src: string, img: HTMLImageElement): Promise { + return new Promise((resolve, reject) => { + img.addEventListener('load', () => resolve(img)); + img.addEventListener('error', (err) => reject(err)); + img.src = src; + }); + } + + private isValidInputs(): boolean { + return Boolean(this.atrocityCrimes && this.wantedName && this.image && this.reward); + } + + private getAllText(): string[] { + return [ + 'WANTED!', + 'For the following atrocity crimes:', + 'Reward from friends:', + this.wantedName, + this.atrocityCrimes, + this.reward, + '😈', + '⍟', + 'PREFERABLE ALIVE WITH ALL OF LIMBS', + ]; + } + + private isValidPaperSize(): boolean { + const page = this.componentShadowRoot.getElementById('page'); + return Boolean(page && page.offsetHeight === page.scrollHeight); } } diff --git a/tsconfig.json b/tsconfig.json index b91a04f..26b388b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "strict": true, "esModuleInterop": true, "outDir": "./dist/", - "noImplicitAny": true + "noImplicitAny": true, + "types": ["css-font-loading-module"] } } diff --git a/webpack.config.js b/webpack.config.js index 96f25d6..1d64b07 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -2,6 +2,19 @@ const path = require('path'); const prodConfig = { entry: './src/index.ts', + module: { + rules: [ + { + test: /\.ts$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + { + test: /\.html$/i, + loader: 'html-loader', + }, + ], + }, resolve: { extensions: ['.ts'], }, @@ -15,8 +28,21 @@ const prodConfig = { const devConfig = { entry: './src/index.ts', + module: { + rules: [ + { + test: /\.ts$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + { + test: /\.html$/i, + loader: 'html-loader', + }, + ], + }, resolve: { - extensions: ['.ts', '.js'], + extensions: ['.ts', '.js', '.html'], modules: ['src', 'node_modules'], }, output: {