fix: element cropping & scrolling (#2625)

This commit is contained in:
Niklas von Hertzen
2021-08-04 20:58:17 +08:00
committed by GitHub
parent 1338c7b203
commit 878e37a242
90 changed files with 750 additions and 552 deletions

View File

@@ -2,7 +2,14 @@ export class DocumentCloner {
clonedReferenceElement?: HTMLElement;
constructor() {
this.clonedReferenceElement = {} as HTMLElement;
this.clonedReferenceElement = {
ownerDocument: {
defaultView: {
pageXOffset: 12,
pageYOffset: 34
}
}
} as HTMLElement;
}
toIFrame(): Promise<HTMLIFrameElement> {

View File

@@ -13,20 +13,26 @@ import {
isTextareaElement,
isTextNode
} from './node-parser';
import {Logger} from '../core/logger';
import {isIdentToken, nonFunctionArgSeparator} from '../css/syntax/parser';
import {TokenType} from '../css/syntax/tokenizer';
import {CounterState, createCounterText} from '../css/types/functions/counter';
import {LIST_STYLE_TYPE, listStyleType} from '../css/property-descriptors/list-style-type';
import {CSSParsedCounterDeclaration, CSSParsedPseudoDeclaration} from '../css/index';
import {getQuote} from '../css/property-descriptors/quotes';
import {Context} from '../core/context';
export interface CloneOptions {
id: string;
ignoreElements?: (element: Element) => boolean;
onclone?: (document: Document, element: HTMLElement) => void;
}
export interface WindowOptions {
scrollX: number;
scrollY: number;
windowWidth: number;
windowHeight: number;
}
export type CloneConfigurations = CloneOptions & {
inlineImages: boolean;
copyStyles: boolean;
@@ -36,15 +42,17 @@ const IGNORE_ATTRIBUTE = 'data-html2canvas-ignore';
export class DocumentCloner {
private readonly scrolledElements: [Element, number, number][];
private readonly options: CloneConfigurations;
private readonly referenceElement: HTMLElement;
clonedReferenceElement?: HTMLElement;
private readonly documentElement: HTMLElement;
private readonly counters: CounterState;
private quoteDepth: number;
constructor(element: HTMLElement, options: CloneConfigurations) {
this.options = options;
constructor(
private readonly context: Context,
element: HTMLElement,
private readonly options: CloneConfigurations
) {
this.scrolledElements = [];
this.referenceElement = element;
this.counters = new CounterState();
@@ -81,9 +89,13 @@ export class DocumentCloner {
/(iPad|iPhone|iPod)/g.test(navigator.userAgent) &&
(cloneWindow.scrollY !== windowSize.top || cloneWindow.scrollX !== windowSize.left)
) {
documentClone.documentElement.style.top = -windowSize.top + 'px';
documentClone.documentElement.style.left = -windowSize.left + 'px';
documentClone.documentElement.style.position = 'absolute';
this.context.logger.warn('Unable to restore scroll position for cloned document');
this.context.windowBounds = this.context.windowBounds.add(
cloneWindow.scrollX - windowSize.left,
cloneWindow.scrollY - windowSize.top,
0,
0
);
}
}
@@ -162,7 +174,7 @@ export class DocumentCloner {
}
} catch (e) {
// accessing node.sheet.cssRules throws a DOMException
Logger.getInstance(this.options.id).error('Unable to access cssRules property', e);
this.context.logger.error('Unable to access cssRules property', e);
if (e.name !== 'SecurityError') {
throw e;
}
@@ -177,7 +189,7 @@ export class DocumentCloner {
img.src = canvas.toDataURL();
return img;
} catch (e) {
Logger.getInstance(this.options.id).info(`Unable to clone canvas contents, canvas is tainted`);
this.context.logger.info(`Unable to clone canvas contents, canvas is tainted`);
}
}
@@ -226,7 +238,7 @@ export class DocumentCloner {
createPseudoHideStyles(clone);
}
const counters = this.counters.parse(new CSSParsedCounterDeclaration(style));
const counters = this.counters.parse(new CSSParsedCounterDeclaration(this.context, style));
const before = this.resolvePseudoContent(node, clone, styleBefore, PseudoElementType.BEFORE);
for (let child = node.firstChild; child; child = child.nextSibling) {
@@ -290,8 +302,8 @@ export class DocumentCloner {
return;
}
this.counters.parse(new CSSParsedCounterDeclaration(style));
const declaration = new CSSParsedPseudoDeclaration(style);
this.counters.parse(new CSSParsedCounterDeclaration(this.context, style));
const declaration = new CSSParsedPseudoDeclaration(this.context, style);
const anonymousReplacedElement = document.createElement('html2canvaspseudoelement');
copyCSSStyles(style, anonymousReplacedElement);
@@ -318,7 +330,7 @@ export class DocumentCloner {
const counterState = this.counters.getCounterValue(counter.value);
const counterType =
counterStyle && isIdentToken(counterStyle)
? listStyleType.parse(counterStyle.value)
? listStyleType.parse(this.context, counterStyle.value)
: LIST_STYLE_TYPE.DECIMAL;
anonymousReplacedElement.appendChild(
@@ -331,7 +343,7 @@ export class DocumentCloner {
const counterStates = this.counters.getCounterValues(counter.value);
const counterType =
counterStyle && isIdentToken(counterStyle)
? listStyleType.parse(counterStyle.value)
? listStyleType.parse(this.context, counterStyle.value)
: LIST_STYLE_TYPE.DECIMAL;
const separator = delim && delim.type === TokenType.STRING_TOKEN ? delim.value : '';
const text = counterStates

View File

@@ -2,6 +2,7 @@ import {CSSParsedDeclaration} from '../css/index';
import {TextContainer} from './text-container';
import {Bounds, parseBounds} from '../css/layout/bounds';
import {isHTMLElementNode} from './node-parser';
import {Context} from '../core/context';
export const enum FLAGS {
CREATES_STACKING_CONTEXT = 1 << 1,
@@ -16,15 +17,15 @@ export class ElementContainer {
bounds: Bounds;
flags: number;
constructor(element: Element) {
this.styles = new CSSParsedDeclaration(window.getComputedStyle(element, null));
constructor(protected readonly context: Context, element: Element) {
this.styles = new CSSParsedDeclaration(context, window.getComputedStyle(element, null));
this.textNodes = [];
this.elements = [];
if (this.styles.transform !== null && isHTMLElementNode(element)) {
// getBoundingClientRect takes transforms into account
element.style.transform = 'none';
}
this.bounds = parseBounds(element);
this.bounds = parseBounds(this.context, element);
this.flags = 0;
}
}

View File

@@ -1,9 +1,10 @@
import {ElementContainer} from '../element-container';
import {Context} from '../../core/context';
export class LIElementContainer extends ElementContainer {
readonly value: number;
constructor(element: HTMLLIElement) {
super(element);
constructor(context: Context, element: HTMLLIElement) {
super(context, element);
this.value = element.value;
}
}

View File

@@ -1,10 +1,11 @@
import {ElementContainer} from '../element-container';
import {Context} from '../../core/context';
export class OLElementContainer extends ElementContainer {
readonly start: number;
readonly reversed: boolean;
constructor(element: HTMLOListElement) {
super(element);
constructor(context: Context, element: HTMLOListElement) {
super(context, element);
this.start = element.start;
this.reversed = typeof element.reversed === 'boolean' && element.reversed === true;
}

View File

@@ -1,8 +1,9 @@
import {ElementContainer} from '../element-container';
import {Context} from '../../core/context';
export class SelectElementContainer extends ElementContainer {
readonly value: string;
constructor(element: HTMLSelectElement) {
super(element);
constructor(context: Context, element: HTMLSelectElement) {
super(context, element);
const option = element.options[element.selectedIndex || 0];
this.value = option ? option.text || '' : '';
}

View File

@@ -1,8 +1,9 @@
import {ElementContainer} from '../element-container';
import {Context} from '../../core/context';
export class TextareaElementContainer extends ElementContainer {
readonly value: string;
constructor(element: HTMLTextAreaElement) {
super(element);
constructor(context: Context, element: HTMLTextAreaElement) {
super(context, element);
this.value = element.value;
}
}

View File

@@ -1,4 +1,4 @@
import {CSSParsedDeclaration} from '../css/index';
import {CSSParsedDeclaration} from '../css';
import {ElementContainer, FLAGS} from './element-container';
import {TextContainer} from './text-container';
import {ImageElementContainer} from './replaced-elements/image-element-container';
@@ -10,20 +10,21 @@ import {InputElementContainer} from './replaced-elements/input-element-container
import {SelectElementContainer} from './elements/select-element-container';
import {TextareaElementContainer} from './elements/textarea-element-container';
import {IFrameElementContainer} from './replaced-elements/iframe-element-container';
import {Context} from '../core/context';
const LIST_OWNERS = ['OL', 'UL', 'MENU'];
const parseNodeTree = (node: Node, parent: ElementContainer, root: ElementContainer) => {
const parseNodeTree = (context: Context, node: Node, parent: ElementContainer, root: ElementContainer) => {
for (let childNode = node.firstChild, nextNode; childNode; childNode = nextNode) {
nextNode = childNode.nextSibling;
if (isTextNode(childNode) && childNode.data.trim().length > 0) {
parent.textNodes.push(new TextContainer(childNode, parent.styles));
parent.textNodes.push(new TextContainer(context, childNode, parent.styles));
} else if (isElementNode(childNode)) {
if (isSlotElement(childNode) && childNode.assignedNodes) {
childNode.assignedNodes().forEach((childNode) => parseNodeTree(childNode, parent, root));
childNode.assignedNodes().forEach((childNode) => parseNodeTree(context, childNode, parent, root));
} else {
const container = createContainer(childNode);
const container = createContainer(context, childNode);
if (container.styles.isVisible()) {
if (createsRealStackingContext(childNode, container, root)) {
container.flags |= FLAGS.CREATES_REAL_STACKING_CONTEXT;
@@ -38,13 +39,13 @@ const parseNodeTree = (node: Node, parent: ElementContainer, root: ElementContai
parent.elements.push(container);
childNode.slot;
if (childNode.shadowRoot) {
parseNodeTree(childNode.shadowRoot, container, root);
parseNodeTree(context, childNode.shadowRoot, container, root);
} else if (
!isTextareaElement(childNode) &&
!isSVGElement(childNode) &&
!isSelectElement(childNode)
) {
parseNodeTree(childNode, container, root);
parseNodeTree(context, childNode, container, root);
}
}
}
@@ -52,50 +53,50 @@ const parseNodeTree = (node: Node, parent: ElementContainer, root: ElementContai
}
};
const createContainer = (element: Element): ElementContainer => {
const createContainer = (context: Context, element: Element): ElementContainer => {
if (isImageElement(element)) {
return new ImageElementContainer(element);
return new ImageElementContainer(context, element);
}
if (isCanvasElement(element)) {
return new CanvasElementContainer(element);
return new CanvasElementContainer(context, element);
}
if (isSVGElement(element)) {
return new SVGElementContainer(element);
return new SVGElementContainer(context, element);
}
if (isLIElement(element)) {
return new LIElementContainer(element);
return new LIElementContainer(context, element);
}
if (isOLElement(element)) {
return new OLElementContainer(element);
return new OLElementContainer(context, element);
}
if (isInputElement(element)) {
return new InputElementContainer(element);
return new InputElementContainer(context, element);
}
if (isSelectElement(element)) {
return new SelectElementContainer(element);
return new SelectElementContainer(context, element);
}
if (isTextareaElement(element)) {
return new TextareaElementContainer(element);
return new TextareaElementContainer(context, element);
}
if (isIFrameElement(element)) {
return new IFrameElementContainer(element);
return new IFrameElementContainer(context, element);
}
return new ElementContainer(element);
return new ElementContainer(context, element);
};
export const parseTree = (element: HTMLElement): ElementContainer => {
const container = createContainer(element);
export const parseTree = (context: Context, element: HTMLElement): ElementContainer => {
const container = createContainer(context, element);
container.flags |= FLAGS.CREATES_REAL_STACKING_CONTEXT;
parseNodeTree(element, container, container);
parseNodeTree(context, element, container, container);
return container;
};

View File

@@ -1,12 +1,13 @@
import {ElementContainer} from '../element-container';
import {Context} from '../../core/context';
export class CanvasElementContainer extends ElementContainer {
canvas: HTMLCanvasElement;
intrinsicWidth: number;
intrinsicHeight: number;
constructor(canvas: HTMLCanvasElement) {
super(canvas);
constructor(context: Context, canvas: HTMLCanvasElement) {
super(context, canvas);
this.canvas = canvas;
this.intrinsicWidth = canvas.width;
this.intrinsicHeight = canvas.height;

View File

@@ -1,9 +1,7 @@
import {ElementContainer} from '../element-container';
import {parseTree} from '../node-parser';
import {Color, color, COLORS, isTransparent} from '../../css/types/color';
import {Parser} from '../../css/syntax/parser';
const parseColor = (value: string): Color => color.parse(Parser.create(value).parseComponentValue());
import {Color, parseColor, COLORS, isTransparent} from '../../css/types/color';
import {Context} from '../../core/context';
export class IFrameElementContainer extends ElementContainer {
src: string;
@@ -12,8 +10,8 @@ export class IFrameElementContainer extends ElementContainer {
tree?: ElementContainer;
backgroundColor: Color;
constructor(iframe: HTMLIFrameElement) {
super(iframe);
constructor(context: Context, iframe: HTMLIFrameElement) {
super(context, iframe);
this.src = iframe.src;
this.width = parseInt(iframe.width, 10) || 0;
this.height = parseInt(iframe.height, 10) || 0;
@@ -24,16 +22,20 @@ export class IFrameElementContainer extends ElementContainer {
iframe.contentWindow.document &&
iframe.contentWindow.document.documentElement
) {
this.tree = parseTree(iframe.contentWindow.document.documentElement);
this.tree = parseTree(context, iframe.contentWindow.document.documentElement);
// http://www.w3.org/TR/css3-background/#special-backgrounds
const documentBackgroundColor = iframe.contentWindow.document.documentElement
? parseColor(
context,
getComputedStyle(iframe.contentWindow.document.documentElement).backgroundColor as string
)
: COLORS.TRANSPARENT;
const bodyBackgroundColor = iframe.contentWindow.document.body
? parseColor(getComputedStyle(iframe.contentWindow.document.body).backgroundColor as string)
? parseColor(
context,
getComputedStyle(iframe.contentWindow.document.body).backgroundColor as string
)
: COLORS.TRANSPARENT;
this.backgroundColor = isTransparent(documentBackgroundColor)

View File

@@ -1,16 +1,16 @@
import {ElementContainer} from '../element-container';
import {CacheStorage} from '../../core/cache-storage';
import {Context} from '../../core/context';
export class ImageElementContainer extends ElementContainer {
src: string;
intrinsicWidth: number;
intrinsicHeight: number;
constructor(img: HTMLImageElement) {
super(img);
constructor(context: Context, img: HTMLImageElement) {
super(context, img);
this.src = img.currentSrc || img.src;
this.intrinsicWidth = img.naturalWidth;
this.intrinsicHeight = img.naturalHeight;
CacheStorage.getInstance().addImage(this.src);
this.context.cache.addImage(this.src);
}
}

View File

@@ -5,6 +5,7 @@ import {BACKGROUND_ORIGIN} from '../../css/property-descriptors/background-origi
import {TokenType} from '../../css/syntax/tokenizer';
import {LengthPercentageTuple} from '../../css/types/length-percentage';
import {Bounds} from '../../css/layout/bounds';
import {Context} from '../../core/context';
const CHECKBOX_BORDER_RADIUS: LengthPercentageTuple = [
{
@@ -48,8 +49,8 @@ export class InputElementContainer extends ElementContainer {
readonly checked: boolean;
readonly value: string;
constructor(input: HTMLInputElement) {
super(input);
constructor(context: Context, input: HTMLInputElement) {
super(context, input);
this.type = input.type.toLowerCase();
this.checked = input.checked;
this.value = getInputValue(input);

View File

@@ -1,16 +1,16 @@
import {ElementContainer} from '../element-container';
import {CacheStorage} from '../../core/cache-storage';
import {parseBounds} from '../../css/layout/bounds';
import {Context} from '../../core/context';
export class SVGElementContainer extends ElementContainer {
svg: string;
intrinsicWidth: number;
intrinsicHeight: number;
constructor(img: SVGSVGElement) {
super(img);
constructor(context: Context, img: SVGSVGElement) {
super(context, img);
const s = new XMLSerializer();
const bounds = parseBounds(img);
const bounds = parseBounds(context, img);
img.setAttribute('width', `${bounds.width}px`);
img.setAttribute('height', `${bounds.height}px`);
@@ -18,6 +18,6 @@ export class SVGElementContainer extends ElementContainer {
this.intrinsicWidth = img.width.baseVal.value;
this.intrinsicHeight = img.height.baseVal.value;
CacheStorage.getInstance().addImage(this.svg);
this.context.cache.addImage(this.svg);
}
}

View File

@@ -1,14 +1,15 @@
import {CSSParsedDeclaration} from '../css/index';
import {TEXT_TRANSFORM} from '../css/property-descriptors/text-transform';
import {parseTextBounds, TextBounds} from '../css/layout/text';
import {Context} from '../core/context';
export class TextContainer {
text: string;
textBounds: TextBounds[];
constructor(node: Text, styles: CSSParsedDeclaration) {
constructor(context: Context, node: Text, styles: CSSParsedDeclaration) {
this.text = transform(node.data, styles.textTransform);
this.textBounds = parseTextBounds(this.text, styles, node);
this.textBounds = parseTextBounds(context, this.text, styles, node);
}
}