mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
Typescript conversion (#1828)
* initial typescript conversion * test: update overflow+transform ref test * fix: correctly render pseudo element content * fix: testrunner build * fix: karma test urls * test: update underline tests with <u> elements * test: update to es6-promise polyfill * test: remove watch from server * test: remove flow * format: update prettier for typescript * test: update eslint to use typescript parser * test: update linear gradient reftest * test: update test runner * test: update testrunner promise polyfill * fix: handle display: -webkit-flex correctly (fix #1817) * fix: correctly render gradients with clip & repeat (fix #1773) * fix: webkit-gradient function support * fix: implement radial gradients * fix: text-decoration rendering * fix: missing scroll positions for elements * ci: fix ios 11 tests * fix: ie logging * ci: improve device availability logging * fix: lint errors * ci: update to ios 12 * fix: check for console availability * ci: fix build dependency * test: update text reftests * fix: window reference for unit tests * feat: add hsl/hsla color support * fix: render options * fix: CSSKeyframesRule cssText Permission Denied on Internet Explorer 11 (#1830) * fix: option lint * fix: list type rendering * test: fix platform import * fix: ie css parsing for numbers * ci: add minified build * fix: form element rendering * fix: iframe rendering * fix: re-introduce experimental foreignobject renderer * fix: text-shadow rendering * feat: improve logging * fix: unit test logging * fix: cleanup resources * test: update overflow scrolling to work with ie * build: update build to include typings * fix: do not parse select element children * test: fix onclone test to work with older IEs * test: reduce reftest canvas sizes * test: remove dynamic setUp from list tests * test: update linear-gradient tests * build: remove old source files * build: update docs dependencies * build: fix typescript definition path * ci: include test.js on docs website
This commit is contained in:
committed by
GitHub
parent
20a797cbeb
commit
522a443055
225
src/core/__tests__/cache-storage.ts
Normal file
225
src/core/__tests__/cache-storage.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import {deepStrictEqual, fail} from 'assert';
|
||||
import {FEATURES} from '../features';
|
||||
import {createMockContext, proxy} from './mock-context';
|
||||
|
||||
const images: ImageMock[] = [];
|
||||
const xhr: XMLHttpRequestMock[] = [];
|
||||
const sleep = async (timeout: number) => await new Promise(resolve => setTimeout(resolve, timeout));
|
||||
|
||||
class ImageMock {
|
||||
src?: string;
|
||||
crossOrigin?: string;
|
||||
onload?: () => {};
|
||||
constructor() {
|
||||
images.push(this);
|
||||
}
|
||||
}
|
||||
|
||||
class XMLHttpRequestMock {
|
||||
sent: boolean;
|
||||
status: number;
|
||||
timeout: number;
|
||||
method?: string;
|
||||
url?: string;
|
||||
response?: string;
|
||||
onload?: () => {};
|
||||
ontimeout?: () => {};
|
||||
constructor() {
|
||||
this.sent = false;
|
||||
this.status = 500;
|
||||
this.timeout = 5000;
|
||||
xhr.push(this);
|
||||
}
|
||||
|
||||
async load(status: number, response: string) {
|
||||
this.response = response;
|
||||
this.status = status;
|
||||
if (this.onload) {
|
||||
this.onload();
|
||||
}
|
||||
await sleep(0);
|
||||
}
|
||||
|
||||
open(method: string, url: string) {
|
||||
this.method = method;
|
||||
this.url = url;
|
||||
}
|
||||
send() {
|
||||
this.sent = true;
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(global, 'Image', {value: ImageMock, writable: true});
|
||||
Object.defineProperty(global, 'XMLHttpRequest', {
|
||||
value: XMLHttpRequestMock,
|
||||
writable: true
|
||||
});
|
||||
|
||||
const setFeatures = (opts: {[key: string]: boolean} = {}) => {
|
||||
const defaults: {[key: string]: boolean} = {
|
||||
SUPPORT_SVG_DRAWING: true,
|
||||
SUPPORT_CORS_IMAGES: true,
|
||||
SUPPORT_CORS_XHR: true,
|
||||
SUPPORT_RESPONSE_TYPE: false
|
||||
};
|
||||
|
||||
Object.keys(defaults).forEach(key => {
|
||||
Object.defineProperty(FEATURES, key, {
|
||||
value: typeof opts[key] === 'boolean' ? opts[key] : defaults[key],
|
||||
writable: true
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describe('cache-storage', () => {
|
||||
beforeEach(() => setFeatures());
|
||||
afterEach(() => {
|
||||
xhr.splice(0, xhr.length);
|
||||
images.splice(0, images.length);
|
||||
});
|
||||
it('addImage adds images to cache', async () => {
|
||||
const cache = createMockContext('http://example.com', {proxy: null});
|
||||
await cache.addImage('http://example.com/test.jpg');
|
||||
await cache.addImage('http://example.com/test2.jpg');
|
||||
|
||||
deepStrictEqual(images.length, 2);
|
||||
deepStrictEqual(images[0].src, 'http://example.com/test.jpg');
|
||||
deepStrictEqual(images[1].src, 'http://example.com/test2.jpg');
|
||||
});
|
||||
|
||||
it('addImage should not add duplicate entries', async () => {
|
||||
const cache = createMockContext('http://example.com');
|
||||
await cache.addImage('http://example.com/test.jpg');
|
||||
await cache.addImage('http://example.com/test.jpg');
|
||||
|
||||
deepStrictEqual(images.length, 1);
|
||||
deepStrictEqual(images[0].src, 'http://example.com/test.jpg');
|
||||
});
|
||||
|
||||
describe('svg', () => {
|
||||
it('should add svg images correctly', async () => {
|
||||
const cache = createMockContext('http://example.com');
|
||||
await cache.addImage('http://example.com/test.svg');
|
||||
await cache.addImage('http://example.com/test2.svg');
|
||||
|
||||
deepStrictEqual(images.length, 2);
|
||||
deepStrictEqual(images[0].src, 'http://example.com/test.svg');
|
||||
deepStrictEqual(images[1].src, 'http://example.com/test2.svg');
|
||||
});
|
||||
|
||||
it('should omit svg images if not supported', async () => {
|
||||
setFeatures({SUPPORT_SVG_DRAWING: false});
|
||||
const cache = createMockContext('http://example.com');
|
||||
await cache.addImage('http://example.com/test.svg');
|
||||
await cache.addImage('http://example.com/test2.svg');
|
||||
|
||||
deepStrictEqual(images.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cross-origin', () => {
|
||||
it('addImage should not add images it cannot load/render', async () => {
|
||||
const cache = createMockContext('http://example.com', {
|
||||
proxy: undefined
|
||||
});
|
||||
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
|
||||
deepStrictEqual(images.length, 0);
|
||||
});
|
||||
|
||||
it('addImage should add images if tainting enabled', async () => {
|
||||
const cache = createMockContext('http://example.com', {
|
||||
allowTaint: true,
|
||||
proxy: undefined
|
||||
});
|
||||
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
|
||||
deepStrictEqual(images.length, 1);
|
||||
deepStrictEqual(images[0].src, 'http://html2canvas.hertzen.com/test.jpg');
|
||||
deepStrictEqual(images[0].crossOrigin, undefined);
|
||||
});
|
||||
|
||||
it('addImage should add images if cors enabled', async () => {
|
||||
const cache = createMockContext('http://example.com', {useCORS: true});
|
||||
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
|
||||
deepStrictEqual(images.length, 1);
|
||||
deepStrictEqual(images[0].src, 'http://html2canvas.hertzen.com/test.jpg');
|
||||
deepStrictEqual(images[0].crossOrigin, 'anonymous');
|
||||
});
|
||||
|
||||
it('addImage should not add images if cors enabled but not supported', async () => {
|
||||
setFeatures({SUPPORT_CORS_IMAGES: false});
|
||||
|
||||
const cache = createMockContext('http://example.com', {
|
||||
useCORS: true,
|
||||
proxy: undefined
|
||||
});
|
||||
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
|
||||
deepStrictEqual(images.length, 0);
|
||||
});
|
||||
|
||||
it('addImage should not add images to proxy if cors enabled', async () => {
|
||||
const cache = createMockContext('http://example.com', {useCORS: true});
|
||||
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
|
||||
deepStrictEqual(images.length, 1);
|
||||
deepStrictEqual(images[0].src, 'http://html2canvas.hertzen.com/test.jpg');
|
||||
deepStrictEqual(images[0].crossOrigin, 'anonymous');
|
||||
});
|
||||
|
||||
it('addImage should use proxy ', async () => {
|
||||
const cache = createMockContext('http://example.com');
|
||||
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
|
||||
deepStrictEqual(xhr.length, 1);
|
||||
deepStrictEqual(
|
||||
xhr[0].url,
|
||||
`${proxy}?url=${encodeURIComponent('http://html2canvas.hertzen.com/test.jpg')}&responseType=text`
|
||||
);
|
||||
await xhr[0].load(200, '<data response>');
|
||||
|
||||
deepStrictEqual(images.length, 1);
|
||||
deepStrictEqual(images[0].src, '<data response>');
|
||||
});
|
||||
|
||||
it('proxy should respect imageTimeout', async () => {
|
||||
const cache = createMockContext('http://example.com', {
|
||||
imageTimeout: 10
|
||||
});
|
||||
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
|
||||
|
||||
deepStrictEqual(xhr.length, 1);
|
||||
deepStrictEqual(
|
||||
xhr[0].url,
|
||||
`${proxy}?url=${encodeURIComponent('http://html2canvas.hertzen.com/test.jpg')}&responseType=text`
|
||||
);
|
||||
deepStrictEqual(xhr[0].timeout, 10);
|
||||
if (xhr[0].ontimeout) {
|
||||
xhr[0].ontimeout();
|
||||
}
|
||||
try {
|
||||
await cache.match('http://html2canvas.hertzen.com/test.jpg');
|
||||
fail('Expected result to timeout');
|
||||
} catch (e) {}
|
||||
});
|
||||
});
|
||||
|
||||
it('match should return cache entry', async () => {
|
||||
const cache = createMockContext('http://example.com');
|
||||
await cache.addImage('http://example.com/test.jpg');
|
||||
|
||||
if (images[0].onload) {
|
||||
images[0].onload();
|
||||
}
|
||||
|
||||
const response = await cache.match('http://example.com/test.jpg');
|
||||
|
||||
deepStrictEqual(response.src, 'http://example.com/test.jpg');
|
||||
});
|
||||
|
||||
it('image should respect imageTimeout', async () => {
|
||||
const cache = createMockContext('http://example.com', {imageTimeout: 10});
|
||||
cache.addImage('http://example.com/test.jpg');
|
||||
|
||||
try {
|
||||
await cache.match('http://example.com/test.jpg');
|
||||
fail('Expected result to timeout');
|
||||
} catch (e) {}
|
||||
});
|
||||
});
|
||||
45
src/core/__tests__/mock-context.ts
Normal file
45
src/core/__tests__/mock-context.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {CacheStorage} from '../cache-storage';
|
||||
import {URL} from 'url';
|
||||
import {Logger} from '../logger';
|
||||
|
||||
export const proxy = 'http://example.com/proxy';
|
||||
|
||||
export const createMockContext = (origin: string, opts = {}) => {
|
||||
const context = {
|
||||
location: {
|
||||
href: origin
|
||||
},
|
||||
document: {
|
||||
createElement(_name: string) {
|
||||
let _href = '';
|
||||
return {
|
||||
set href(value: string) {
|
||||
_href = value;
|
||||
},
|
||||
get href() {
|
||||
return _href;
|
||||
},
|
||||
get protocol() {
|
||||
return new URL(_href).protocol;
|
||||
},
|
||||
get hostname() {
|
||||
return new URL(_href).hostname;
|
||||
},
|
||||
get port() {
|
||||
return new URL(_href).port;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
CacheStorage.setContext(context as Window);
|
||||
Logger.create('test');
|
||||
return CacheStorage.create('test', {
|
||||
imageTimeout: 0,
|
||||
useCORS: false,
|
||||
allowTaint: false,
|
||||
proxy,
|
||||
...opts
|
||||
});
|
||||
};
|
||||
1
src/core/bitwise.ts
Normal file
1
src/core/bitwise.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const contains = (bit: number, value: number): boolean => (bit & value) !== 0;
|
||||
207
src/core/cache-storage.ts
Normal file
207
src/core/cache-storage.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import {FEATURES} from './features';
|
||||
import {Logger} from './logger';
|
||||
|
||||
export class CacheStorage {
|
||||
private static _caches: {[key: string]: Cache} = {};
|
||||
private static _link?: HTMLAnchorElement;
|
||||
private static _origin: string = 'about:blank';
|
||||
private static _current: Cache | null = null;
|
||||
|
||||
static create(name: string, options: ResourceOptions): Cache {
|
||||
return (CacheStorage._caches[name] = new Cache(name, options));
|
||||
}
|
||||
|
||||
static destroy(name: string): void {
|
||||
delete CacheStorage._caches[name];
|
||||
}
|
||||
|
||||
static open(name: string): Cache {
|
||||
const cache = CacheStorage._caches[name];
|
||||
if (typeof cache !== 'undefined') {
|
||||
return cache;
|
||||
}
|
||||
|
||||
throw new Error(`Cache with key "${name}" not found`);
|
||||
}
|
||||
|
||||
static getOrigin(url: string): string {
|
||||
const link = CacheStorage._link;
|
||||
if (!link) {
|
||||
return 'about:blank';
|
||||
}
|
||||
|
||||
link.href = url;
|
||||
link.href = link.href; // IE9, LOL! - http://jsfiddle.net/niklasvh/2e48b/
|
||||
return link.protocol + link.hostname + link.port;
|
||||
}
|
||||
|
||||
static isSameOrigin(src: string): boolean {
|
||||
return CacheStorage.getOrigin(src) === CacheStorage._origin;
|
||||
}
|
||||
|
||||
static setContext(window: Window) {
|
||||
CacheStorage._link = window.document.createElement('a');
|
||||
CacheStorage._origin = CacheStorage.getOrigin(window.location.href);
|
||||
}
|
||||
|
||||
static getInstance(): Cache {
|
||||
const current = CacheStorage._current;
|
||||
if (current === null) {
|
||||
throw new Error(`No cache instance attached`);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
static attachInstance(cache: Cache) {
|
||||
CacheStorage._current = cache;
|
||||
}
|
||||
|
||||
static detachInstance() {
|
||||
CacheStorage._current = null;
|
||||
}
|
||||
}
|
||||
|
||||
interface ResourceOptions {
|
||||
imageTimeout: number;
|
||||
useCORS: boolean;
|
||||
allowTaint: boolean;
|
||||
proxy?: string;
|
||||
}
|
||||
|
||||
export class Cache {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private readonly _cache: {[key: string]: Promise<any>};
|
||||
private readonly _options: ResourceOptions;
|
||||
private readonly id: string;
|
||||
|
||||
constructor(id: string, options: ResourceOptions) {
|
||||
this.id = id;
|
||||
this._options = options;
|
||||
this._cache = {};
|
||||
}
|
||||
|
||||
addImage(src: string): Promise<void> {
|
||||
const result = Promise.resolve();
|
||||
if (this.has(src)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (isBlobImage(src) || isRenderable(src)) {
|
||||
this._cache[src] = this.loadImage(src);
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
match(src: string): Promise<any> {
|
||||
return this._cache[src];
|
||||
}
|
||||
|
||||
private async loadImage(key: string) {
|
||||
const isSameOrigin = CacheStorage.isSameOrigin(key);
|
||||
const useCORS =
|
||||
!isInlineImage(key) && this._options.useCORS === true && FEATURES.SUPPORT_CORS_IMAGES && !isSameOrigin;
|
||||
const useProxy =
|
||||
!isInlineImage(key) &&
|
||||
!isSameOrigin &&
|
||||
typeof this._options.proxy === 'string' &&
|
||||
FEATURES.SUPPORT_CORS_XHR &&
|
||||
!useCORS;
|
||||
if (!isSameOrigin && this._options.allowTaint === false && !isInlineImage(key) && !useProxy && !useCORS) {
|
||||
return;
|
||||
}
|
||||
|
||||
let src = key;
|
||||
if (useProxy) {
|
||||
src = await this.proxy(src);
|
||||
}
|
||||
|
||||
Logger.getInstance(this.id).debug(`Added image ${key.substring(0, 256)}`);
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
//ios safari 10.3 taints canvas with data urls unless crossOrigin is set to anonymous
|
||||
if (isInlineBase64Image(src) || useCORS) {
|
||||
img.crossOrigin = 'anonymous';
|
||||
}
|
||||
img.src = src;
|
||||
if (img.complete === true) {
|
||||
// Inline XML images may fail to parse, throwing an Error later on
|
||||
setTimeout(() => resolve(img), 500);
|
||||
}
|
||||
if (this._options.imageTimeout > 0) {
|
||||
setTimeout(
|
||||
() => reject(`Timed out (${this._options.imageTimeout}ms) loading image`),
|
||||
this._options.imageTimeout
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private has(key: string): boolean {
|
||||
return typeof this._cache[key] !== 'undefined';
|
||||
}
|
||||
|
||||
keys(): Promise<string[]> {
|
||||
return Promise.resolve(Object.keys(this._cache));
|
||||
}
|
||||
|
||||
private proxy(src: string): Promise<string> {
|
||||
const proxy = this._options.proxy;
|
||||
|
||||
if (!proxy) {
|
||||
throw new Error('No proxy defined');
|
||||
}
|
||||
|
||||
const key = src.substring(0, 256);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const responseType = FEATURES.SUPPORT_RESPONSE_TYPE ? 'blob' : 'text';
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 200) {
|
||||
if (responseType === 'text') {
|
||||
resolve(xhr.response);
|
||||
} else {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => resolve(reader.result as string), false);
|
||||
reader.addEventListener('error', e => reject(e), false);
|
||||
reader.readAsDataURL(xhr.response);
|
||||
}
|
||||
} else {
|
||||
reject(`Failed to proxy resource ${key} with status code ${xhr.status}`);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = reject;
|
||||
xhr.open('GET', `${proxy}?url=${encodeURIComponent(src)}&responseType=${responseType}`);
|
||||
|
||||
if (responseType !== 'text' && xhr instanceof XMLHttpRequest) {
|
||||
xhr.responseType = responseType;
|
||||
}
|
||||
|
||||
if (this._options.imageTimeout) {
|
||||
const timeout = this._options.imageTimeout;
|
||||
xhr.timeout = timeout;
|
||||
xhr.ontimeout = () => reject(`Timed out (${timeout}ms) proxying ${key}`);
|
||||
}
|
||||
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const INLINE_SVG = /^data:image\/svg\+xml/i;
|
||||
const INLINE_BASE64 = /^data:image\/.*;base64,/i;
|
||||
const INLINE_IMG = /^data:image\/.*/i;
|
||||
|
||||
const isRenderable = (src: string): boolean => FEATURES.SUPPORT_SVG_DRAWING || !isSVG(src);
|
||||
const isInlineImage = (src: string): boolean => INLINE_IMG.test(src);
|
||||
const isInlineBase64Image = (src: string): boolean => INLINE_BASE64.test(src);
|
||||
const isBlobImage = (src: string): boolean => src.substr(0, 4) === 'blob';
|
||||
|
||||
const isSVG = (src: string): boolean => src.substr(-3).toLowerCase() === 'svg' || INLINE_SVG.test(src);
|
||||
162
src/core/features.ts
Normal file
162
src/core/features.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
const testRangeBounds = (document: Document) => {
|
||||
const TEST_HEIGHT = 123;
|
||||
|
||||
if (document.createRange) {
|
||||
const range = document.createRange();
|
||||
if (range.getBoundingClientRect) {
|
||||
const testElement = document.createElement('boundtest');
|
||||
testElement.style.height = `${TEST_HEIGHT}px`;
|
||||
testElement.style.display = 'block';
|
||||
document.body.appendChild(testElement);
|
||||
|
||||
range.selectNode(testElement);
|
||||
const rangeBounds = range.getBoundingClientRect();
|
||||
const rangeHeight = Math.round(rangeBounds.height);
|
||||
document.body.removeChild(testElement);
|
||||
if (rangeHeight === TEST_HEIGHT) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const testCORS = (): boolean => typeof new Image().crossOrigin !== 'undefined';
|
||||
|
||||
const testResponseType = (): boolean => typeof new XMLHttpRequest().responseType === 'string';
|
||||
|
||||
const testSVG = (document: Document): boolean => {
|
||||
const img = new Image();
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
return false;
|
||||
}
|
||||
|
||||
img.src = `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'></svg>`;
|
||||
|
||||
try {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
canvas.toDataURL();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const isGreenPixel = (data: Uint8ClampedArray): boolean =>
|
||||
data[0] === 0 && data[1] === 255 && data[2] === 0 && data[3] === 255;
|
||||
|
||||
const testForeignObject = (document: Document): Promise<boolean> => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const size = 100;
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
return Promise.reject(false);
|
||||
}
|
||||
ctx.fillStyle = 'rgb(0, 255, 0)';
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
const img = new Image();
|
||||
const greenImageSrc = canvas.toDataURL();
|
||||
img.src = greenImageSrc;
|
||||
const svg = createForeignObjectSVG(size, size, 0, 0, img);
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
return loadSerializedSVG(svg)
|
||||
.then((img: HTMLImageElement) => {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const data = ctx.getImageData(0, 0, size, size).data;
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
const node = document.createElement('div');
|
||||
node.style.backgroundImage = `url(${greenImageSrc})`;
|
||||
node.style.height = `${size}px`;
|
||||
// Firefox 55 does not render inline <img /> tags
|
||||
return isGreenPixel(data)
|
||||
? loadSerializedSVG(createForeignObjectSVG(size, size, 0, 0, node))
|
||||
: Promise.reject(false);
|
||||
})
|
||||
.then((img: HTMLImageElement) => {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
// Edge does not render background-images
|
||||
return isGreenPixel(ctx.getImageData(0, 0, size, size).data);
|
||||
})
|
||||
.catch(() => false);
|
||||
};
|
||||
|
||||
export const createForeignObjectSVG = (width: number, height: number, x: number, y: number, node: Node) => {
|
||||
const xmlns = 'http://www.w3.org/2000/svg';
|
||||
const svg = document.createElementNS(xmlns, 'svg');
|
||||
const foreignObject = document.createElementNS(xmlns, 'foreignObject');
|
||||
svg.setAttributeNS(null, 'width', width.toString());
|
||||
svg.setAttributeNS(null, 'height', height.toString());
|
||||
|
||||
foreignObject.setAttributeNS(null, 'width', '100%');
|
||||
foreignObject.setAttributeNS(null, 'height', '100%');
|
||||
foreignObject.setAttributeNS(null, 'x', x.toString());
|
||||
foreignObject.setAttributeNS(null, 'y', y.toString());
|
||||
foreignObject.setAttributeNS(null, 'externalResourcesRequired', 'true');
|
||||
svg.appendChild(foreignObject);
|
||||
|
||||
foreignObject.appendChild(node);
|
||||
|
||||
return svg;
|
||||
};
|
||||
|
||||
export const loadSerializedSVG = (svg: Node): Promise<HTMLImageElement> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
|
||||
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
|
||||
});
|
||||
};
|
||||
|
||||
export const FEATURES = {
|
||||
get SUPPORT_RANGE_BOUNDS() {
|
||||
'use strict';
|
||||
const value = testRangeBounds(document);
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_RANGE_BOUNDS', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_SVG_DRAWING() {
|
||||
'use strict';
|
||||
const value = testSVG(document);
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_SVG_DRAWING', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_FOREIGNOBJECT_DRAWING() {
|
||||
'use strict';
|
||||
const value =
|
||||
typeof Array.from === 'function' && typeof window.fetch === 'function'
|
||||
? testForeignObject(document)
|
||||
: Promise.resolve(false);
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_FOREIGNOBJECT_DRAWING', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_CORS_IMAGES() {
|
||||
'use strict';
|
||||
const value = testCORS();
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_CORS_IMAGES', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_RESPONSE_TYPE() {
|
||||
'use strict';
|
||||
const value = testResponseType();
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_RESPONSE_TYPE', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_CORS_XHR() {
|
||||
'use strict';
|
||||
const value = 'withCredentials' in new XMLHttpRequest();
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_CORS_XHR', {value});
|
||||
return value;
|
||||
}
|
||||
};
|
||||
62
src/core/logger.ts
Normal file
62
src/core/logger.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
export class Logger {
|
||||
static instances: {[key: string]: Logger} = {};
|
||||
|
||||
private readonly id: string;
|
||||
private readonly start: number;
|
||||
|
||||
constructor(id: string) {
|
||||
this.id = id;
|
||||
this.start = Date.now();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
debug(...args: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
if (typeof window !== 'undefined' && window.console && typeof console.debug === 'function') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(this.id, `${this.getTime()}ms`, ...args);
|
||||
} else {
|
||||
this.info(...args);
|
||||
}
|
||||
}
|
||||
|
||||
getTime(): number {
|
||||
return Date.now() - this.start;
|
||||
}
|
||||
|
||||
static create(id: string) {
|
||||
Logger.instances[id] = new Logger(id);
|
||||
}
|
||||
|
||||
static destroy(id: string) {
|
||||
delete Logger.instances[id];
|
||||
}
|
||||
|
||||
static getInstance(id: string): Logger {
|
||||
const instance = Logger.instances[id];
|
||||
if (typeof instance === 'undefined') {
|
||||
throw new Error(`No logger instance found with id ${id}`);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
info(...args: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
if (typeof window !== 'undefined' && window.console && typeof console.info === 'function') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info(this.id, `${this.getTime()}ms`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error(...args: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
if (typeof window !== 'undefined' && window.console && typeof console.error === 'function') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(this.id, `${this.getTime()}ms`, ...args);
|
||||
} else {
|
||||
this.info(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/core/util.ts
Normal file
1
src/core/util.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const SMALL_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||
Reference in New Issue
Block a user