mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
Implement input/textarea/select element rendering
This commit is contained in:
parent
adb1f50f00
commit
56b3b6df27
@ -4,11 +4,12 @@
|
||||
import type {Border, BorderSide} from './parsing/border';
|
||||
import type {BorderRadius} from './parsing/borderRadius';
|
||||
import type {Padding} from './parsing/padding';
|
||||
import type Circle from './drawing/Circle';
|
||||
|
||||
import Vector from './Vector';
|
||||
import BezierCurve from './BezierCurve';
|
||||
import Vector from './drawing/Vector';
|
||||
import BezierCurve from './drawing/BezierCurve';
|
||||
|
||||
export type Path = Array<Vector | BezierCurve>;
|
||||
export type Path = Array<Vector | BezierCurve> | Circle;
|
||||
|
||||
const TOP = 0;
|
||||
const RIGHT = 1;
|
||||
|
@ -1,19 +1,30 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import type Color from './Color';
|
||||
import type Size from './Size';
|
||||
import type {Path, BoundCurves} from './Bounds';
|
||||
import type {TextBounds} from './TextBounds';
|
||||
import type Size from './drawing/Size';
|
||||
|
||||
import type {BackgroundImage} from './parsing/background';
|
||||
import type {Border, BorderSide} from './parsing/border';
|
||||
|
||||
import Vector from './Vector';
|
||||
import BezierCurve from './BezierCurve';
|
||||
|
||||
import type NodeContainer from './NodeContainer';
|
||||
import type TextContainer from './TextContainer';
|
||||
import type {Path, BoundCurves} from './Bounds';
|
||||
import type {ImageStore, ImageElement} from './ImageLoader';
|
||||
import type NodeContainer from './NodeContainer';
|
||||
import type StackingContext from './StackingContext';
|
||||
import type {TextBounds} from './TextBounds';
|
||||
|
||||
import BezierCurve from './drawing/BezierCurve';
|
||||
import Circle from './drawing/Circle';
|
||||
import Vector from './drawing/Vector';
|
||||
|
||||
import {
|
||||
parsePathForBorder,
|
||||
calculateContentBox,
|
||||
calculatePaddingBox,
|
||||
calculatePaddingBoxPath
|
||||
} from './Bounds';
|
||||
import {FontMetrics} from './Font';
|
||||
import TextContainer from './TextContainer';
|
||||
|
||||
import {
|
||||
BACKGROUND_ORIGIN,
|
||||
@ -24,14 +35,6 @@ import {
|
||||
} from './parsing/background';
|
||||
import {BORDER_STYLE} from './parsing/border';
|
||||
import {TEXT_DECORATION_LINE} from './parsing/textDecoration';
|
||||
import {
|
||||
parsePathForBorder,
|
||||
calculateContentBox,
|
||||
calculatePaddingBox,
|
||||
calculatePaddingBoxPath
|
||||
} from './Bounds';
|
||||
|
||||
import {FontMetrics} from './Font';
|
||||
|
||||
export type RenderOptions = {
|
||||
scale: number,
|
||||
@ -68,7 +71,7 @@ export default class CanvasRenderer {
|
||||
});
|
||||
}
|
||||
|
||||
if (container.textNodes.length) {
|
||||
if (container.childNodes.length) {
|
||||
this.ctx.fillStyle = container.style.color.toString();
|
||||
this.ctx.font = [
|
||||
container.style.font.fontStyle,
|
||||
@ -79,7 +82,14 @@ export default class CanvasRenderer {
|
||||
]
|
||||
.join(' ')
|
||||
.split(',')[0];
|
||||
container.textNodes.forEach(this.renderTextNode, this);
|
||||
container.childNodes.forEach(child => {
|
||||
if (child instanceof TextContainer) {
|
||||
this.renderTextNode(child);
|
||||
} else {
|
||||
this.path(child);
|
||||
this.ctx.fill();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (container.image) {
|
||||
@ -267,25 +277,37 @@ export default class CanvasRenderer {
|
||||
|
||||
path(path: Path) {
|
||||
this.ctx.beginPath();
|
||||
path.forEach((point, index) => {
|
||||
const start = point instanceof Vector ? point : point.start;
|
||||
if (index === 0) {
|
||||
this.ctx.moveTo(start.x, start.y);
|
||||
} else {
|
||||
this.ctx.lineTo(start.x, start.y);
|
||||
}
|
||||
if (path instanceof Circle) {
|
||||
this.ctx.arc(
|
||||
path.x + path.radius,
|
||||
path.y + path.radius,
|
||||
path.radius,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
path.forEach((point, index) => {
|
||||
const start = point instanceof Vector ? point : point.start;
|
||||
if (index === 0) {
|
||||
this.ctx.moveTo(start.x, start.y);
|
||||
} else {
|
||||
this.ctx.lineTo(start.x, start.y);
|
||||
}
|
||||
|
||||
if (point instanceof BezierCurve) {
|
||||
this.ctx.bezierCurveTo(
|
||||
point.startControl.x,
|
||||
point.startControl.y,
|
||||
point.endControl.x,
|
||||
point.endControl.y,
|
||||
point.end.x,
|
||||
point.end.y
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (point instanceof BezierCurve) {
|
||||
this.ctx.bezierCurveTo(
|
||||
point.startControl.x,
|
||||
point.startControl.y,
|
||||
point.endControl.x,
|
||||
point.endControl.y,
|
||||
point.end.x,
|
||||
point.end.y
|
||||
);
|
||||
}
|
||||
});
|
||||
this.ctx.closePath();
|
||||
}
|
||||
|
||||
|
128
src/Input.js
Normal file
128
src/Input.js
Normal file
@ -0,0 +1,128 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
import type NodeContainer from './NodeContainer';
|
||||
import TextContainer from './TextContainer';
|
||||
|
||||
import {BACKGROUND_CLIP, BACKGROUND_ORIGIN} from './parsing/background';
|
||||
import {BORDER_STYLE} from './parsing/border';
|
||||
|
||||
import Circle from './drawing/Circle';
|
||||
import Color from './Color';
|
||||
import Length from './Length';
|
||||
import {Bounds} from './Bounds';
|
||||
import {TextBounds} from './TextBounds';
|
||||
|
||||
export const INPUT_COLOR = new Color([42, 42, 42]);
|
||||
const INPUT_BORDER_COLOR = new Color([165, 165, 165]);
|
||||
const INPUT_BACKGROUND_COLOR = new Color([222, 222, 222]);
|
||||
const INPUT_BORDER = {
|
||||
borderWidth: 1,
|
||||
borderColor: INPUT_BORDER_COLOR,
|
||||
borderStyle: BORDER_STYLE.SOLID
|
||||
};
|
||||
export const INPUT_BORDERS = [INPUT_BORDER, INPUT_BORDER, INPUT_BORDER, INPUT_BORDER];
|
||||
export const INPUT_BACKGROUND = {
|
||||
backgroundColor: INPUT_BACKGROUND_COLOR,
|
||||
backgroundImage: [],
|
||||
backgroundClip: BACKGROUND_CLIP.PADDING_BOX,
|
||||
backgroundOrigin: BACKGROUND_ORIGIN.PADDING_BOX
|
||||
};
|
||||
|
||||
const RADIO_BORDER_RADIUS = new Length('50%');
|
||||
const RADIO_BORDER_RADIUS_TUPLE = [RADIO_BORDER_RADIUS, RADIO_BORDER_RADIUS];
|
||||
const INPUT_RADIO_BORDER_RADIUS = [
|
||||
RADIO_BORDER_RADIUS_TUPLE,
|
||||
RADIO_BORDER_RADIUS_TUPLE,
|
||||
RADIO_BORDER_RADIUS_TUPLE,
|
||||
RADIO_BORDER_RADIUS_TUPLE
|
||||
];
|
||||
|
||||
const CHECKBOX_BORDER_RADIUS = new Length('3px');
|
||||
const CHECKBOX_BORDER_RADIUS_TUPLE = [CHECKBOX_BORDER_RADIUS, CHECKBOX_BORDER_RADIUS];
|
||||
const INPUT_CHECKBOX_BORDER_RADIUS = [
|
||||
CHECKBOX_BORDER_RADIUS_TUPLE,
|
||||
CHECKBOX_BORDER_RADIUS_TUPLE,
|
||||
CHECKBOX_BORDER_RADIUS_TUPLE,
|
||||
CHECKBOX_BORDER_RADIUS_TUPLE
|
||||
];
|
||||
|
||||
export const getInputBorderRadius = (node: HTMLInputElement) => {
|
||||
return node.type === 'radio' ? INPUT_RADIO_BORDER_RADIUS : INPUT_CHECKBOX_BORDER_RADIUS;
|
||||
};
|
||||
|
||||
export const inlineInputElement = (node: HTMLInputElement, container: NodeContainer): void => {
|
||||
if (node.type === 'radio' || node.type === 'checkbox') {
|
||||
if (node.checked) {
|
||||
const size = Math.min(container.bounds.width, container.bounds.height);
|
||||
// TODO draw checkmark with Path Array<Vector>
|
||||
container.style.font.fontSize = `${size - 3}px`;
|
||||
container.childNodes.push(
|
||||
node.type === 'checkbox'
|
||||
? new TextContainer('\u2714', container, [
|
||||
new TextBounds(
|
||||
'\u2714',
|
||||
new Bounds(
|
||||
container.bounds.left + size / 6,
|
||||
container.bounds.top + size - 1,
|
||||
0,
|
||||
0
|
||||
)
|
||||
)
|
||||
])
|
||||
: new Circle(
|
||||
container.bounds.left + size / 4,
|
||||
container.bounds.top + size / 4,
|
||||
size / 4
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
inlineFormElement(getInputValue(node), node, container);
|
||||
}
|
||||
};
|
||||
|
||||
export const inlineTextAreaElement = (
|
||||
node: HTMLTextAreaElement,
|
||||
container: NodeContainer
|
||||
): void => {
|
||||
inlineFormElement(node.value, node, container);
|
||||
};
|
||||
|
||||
export const inlineSelectElement = (node: HTMLSelectElement, container: NodeContainer): void => {
|
||||
const option = node.options[node.selectedIndex || 0];
|
||||
inlineFormElement(option ? option.text || '' : '', node, container);
|
||||
};
|
||||
|
||||
export const reformatInputBounds = (bounds: Bounds): Bounds => {
|
||||
if (bounds.width > bounds.height) {
|
||||
bounds.left += (bounds.width - bounds.height) / 2;
|
||||
bounds.width = bounds.height;
|
||||
} else if (bounds.width < bounds.height) {
|
||||
bounds.top += (bounds.height - bounds.width) / 2;
|
||||
bounds.height = bounds.width;
|
||||
}
|
||||
return bounds;
|
||||
};
|
||||
|
||||
const inlineFormElement = (value: string, node: HTMLElement, container: NodeContainer): void => {
|
||||
const body = node.ownerDocument.body;
|
||||
if (value.length > 0 && body) {
|
||||
const wrapper = node.ownerDocument.createElement('html2canvaswrapper');
|
||||
wrapper.style = node.ownerDocument.defaultView.getComputedStyle(node, null).cssText;
|
||||
wrapper.style.position = 'fixed';
|
||||
wrapper.style.left = `${container.bounds.left}px`;
|
||||
wrapper.style.top = `${container.bounds.top}px`;
|
||||
const text = node.ownerDocument.createTextNode(value);
|
||||
wrapper.appendChild(text);
|
||||
body.appendChild(wrapper);
|
||||
container.childNodes.push(TextContainer.fromTextNode(text, container));
|
||||
body.removeChild(wrapper);
|
||||
}
|
||||
};
|
||||
|
||||
const getInputValue = (node: HTMLInputElement): string => {
|
||||
const value =
|
||||
node.type === 'password' ? new Array(node.value.length + 1).join('\u2022') : node.value;
|
||||
|
||||
return value.length === 0 ? node.placeholder || '' : value;
|
||||
};
|
@ -41,6 +41,13 @@ import {parseVisibility, VISIBILITY} from './parsing/visibility';
|
||||
import {parseZIndex} from './parsing/zIndex';
|
||||
|
||||
import {parseBounds, parseBoundCurves, calculatePaddingBoxPath} from './Bounds';
|
||||
import {
|
||||
INPUT_BACKGROUND,
|
||||
INPUT_BORDERS,
|
||||
INPUT_COLOR,
|
||||
getInputBorderRadius,
|
||||
reformatInputBounds
|
||||
} from './Input';
|
||||
|
||||
type StyleDeclaration = {
|
||||
background: Background,
|
||||
@ -66,22 +73,30 @@ export default class NodeContainer {
|
||||
name: ?string;
|
||||
parent: ?NodeContainer;
|
||||
style: StyleDeclaration;
|
||||
textNodes: Array<TextContainer>;
|
||||
childNodes: Array<TextContainer | Path>;
|
||||
bounds: Bounds;
|
||||
curvedBounds: BoundCurves;
|
||||
image: ?string;
|
||||
|
||||
constructor(node: HTMLElement, parent: ?NodeContainer, imageLoader: ImageLoader) {
|
||||
this.parent = parent;
|
||||
this.textNodes = [];
|
||||
const style = node.ownerDocument.defaultView.getComputedStyle(node, null);
|
||||
this.childNodes = [];
|
||||
const defaultView = node.ownerDocument.defaultView;
|
||||
const style = defaultView.getComputedStyle(node, null);
|
||||
const display = parseDisplay(style.display);
|
||||
|
||||
const IS_INPUT = node.type === 'radio' || node.type === 'checkbox';
|
||||
|
||||
this.style = {
|
||||
background: parseBackground(style, imageLoader),
|
||||
border: parseBorder(style),
|
||||
borderRadius: parseBorderRadius(style),
|
||||
color: new Color(style.color),
|
||||
background: IS_INPUT ? INPUT_BACKGROUND : parseBackground(style, imageLoader),
|
||||
border: IS_INPUT ? INPUT_BORDERS : parseBorder(style),
|
||||
borderRadius:
|
||||
(node instanceof defaultView.HTMLInputElement ||
|
||||
node instanceof HTMLInputElement) &&
|
||||
IS_INPUT
|
||||
? getInputBorderRadius(node)
|
||||
: parseBorderRadius(style),
|
||||
color: IS_INPUT ? INPUT_COLOR : new Color(style.color),
|
||||
display: display,
|
||||
float: parseCSSFloat(style.float),
|
||||
font: parseFont(style),
|
||||
@ -103,7 +118,7 @@ export default class NodeContainer {
|
||||
}
|
||||
|
||||
this.image = getImage(node, imageLoader);
|
||||
this.bounds = parseBounds(node);
|
||||
this.bounds = IS_INPUT ? reformatInputBounds(parseBounds(node)) : parseBounds(node);
|
||||
this.curvedBounds = parseBoundCurves(
|
||||
this.bounds,
|
||||
this.style.border,
|
||||
|
@ -5,6 +5,7 @@ import type Logger from './Logger';
|
||||
import StackingContext from './StackingContext';
|
||||
import NodeContainer from './NodeContainer';
|
||||
import TextContainer from './TextContainer';
|
||||
import {inlineInputElement, inlineTextAreaElement, inlineSelectElement} from './Input';
|
||||
|
||||
export const NodeParser = (
|
||||
node: HTMLElement,
|
||||
@ -45,17 +46,29 @@ const parseNodeTree = (
|
||||
for (let childNode = node.firstChild, nextNode; childNode; childNode = nextNode) {
|
||||
nextNode = childNode.nextSibling;
|
||||
const defaultView = childNode.ownerDocument.defaultView;
|
||||
if (childNode instanceof defaultView.Text) {
|
||||
if (childNode instanceof defaultView.Text || childNode instanceof Text) {
|
||||
if (childNode.data.trim().length > 0) {
|
||||
parent.textNodes.push(new TextContainer(childNode, parent));
|
||||
parent.childNodes.push(TextContainer.fromTextNode(childNode, parent));
|
||||
}
|
||||
} else if (childNode instanceof defaultView.HTMLElement) {
|
||||
} else if (
|
||||
childNode instanceof defaultView.HTMLElement ||
|
||||
childNode instanceof HTMLElement
|
||||
) {
|
||||
if (IGNORED_NODE_NAMES.indexOf(childNode.nodeName) === -1) {
|
||||
inlinePseudoElement(childNode, PSEUDO_BEFORE);
|
||||
inlinePseudoElement(childNode, PSEUDO_AFTER);
|
||||
const container = new NodeContainer(childNode, parent, imageLoader);
|
||||
|
||||
if (container.isVisible()) {
|
||||
if (childNode.tagName === 'INPUT') {
|
||||
// $FlowFixMe
|
||||
inlineInputElement(childNode, container);
|
||||
} else if (childNode.tagName === 'TEXTAREA') {
|
||||
// $FlowFixMe
|
||||
inlineTextAreaElement(childNode, container);
|
||||
} else if (childNode.tagName === 'SELECT') {
|
||||
// $FlowFixMe
|
||||
inlineSelectElement(childNode, container);
|
||||
}
|
||||
const treatAsRealStackingContext = createsRealStackingContext(
|
||||
container,
|
||||
childNode
|
||||
@ -105,7 +118,7 @@ const inlinePseudoElement = (node: HTMLElement, pseudoElt: ':before' | ':after')
|
||||
// $FlowFixMe
|
||||
anonymousReplacedElement.src = stripQuotes(image[1]);
|
||||
} else {
|
||||
anonymousReplacedElement.appendChild(node.ownerDocument.createTextNode(content));
|
||||
anonymousReplacedElement.textContent = content;
|
||||
}
|
||||
|
||||
anonymousReplacedElement.style = style.cssText;
|
||||
|
@ -2,7 +2,7 @@
|
||||
'use strict';
|
||||
|
||||
import {ucs2} from 'punycode';
|
||||
import type TextContainer from './TextContainer';
|
||||
import type NodeContainer from './NodeContainer';
|
||||
import {Bounds, parseBounds} from './Bounds';
|
||||
import {TEXT_DECORATION} from './parsing/textDecoration';
|
||||
|
||||
@ -24,20 +24,20 @@ export class TextBounds {
|
||||
}
|
||||
}
|
||||
|
||||
export const parseTextBounds = (textContainer: TextContainer, node: Text): Array<TextBounds> => {
|
||||
const codePoints = ucs2.decode(textContainer.text);
|
||||
const letterRendering =
|
||||
textContainer.parent.style.letterSpacing !== 0 || hasUnicodeCharacters(textContainer.text);
|
||||
export const parseTextBounds = (
|
||||
value: string,
|
||||
parent: NodeContainer,
|
||||
node: Text
|
||||
): Array<TextBounds> => {
|
||||
const codePoints = ucs2.decode(value);
|
||||
const letterRendering = parent.style.letterSpacing !== 0 || hasUnicodeCharacters(value);
|
||||
const textList = letterRendering ? codePoints.map(encodeCodePoint) : splitWords(codePoints);
|
||||
const length = textList.length;
|
||||
const textBounds = [];
|
||||
let offset = 0;
|
||||
for (let i = 0; i < length; i++) {
|
||||
let text = textList[i];
|
||||
if (
|
||||
textContainer.parent.style.textDecoration !== TEXT_DECORATION.NONE ||
|
||||
text.trim().length > 0
|
||||
) {
|
||||
if (parent.style.textDecoration !== TEXT_DECORATION.NONE || text.trim().length > 0) {
|
||||
if (FEATURES.SUPPORT_RANGE_BOUNDS) {
|
||||
textBounds.push(new TextBounds(text, getRangeBounds(node, offset, text.length)));
|
||||
} else {
|
||||
|
@ -12,10 +12,15 @@ export default class TextContainer {
|
||||
parent: NodeContainer;
|
||||
bounds: Array<TextBounds>;
|
||||
|
||||
constructor(node: Text, parent: NodeContainer) {
|
||||
this.text = transform(node.data, parent.style.textTransform);
|
||||
constructor(text: string, parent: NodeContainer, bounds: Array<TextBounds>) {
|
||||
this.text = text;
|
||||
this.parent = parent;
|
||||
this.bounds = parseTextBounds(this, node);
|
||||
this.bounds = bounds;
|
||||
}
|
||||
|
||||
static fromTextNode(node: Text, parent: NodeContainer) {
|
||||
const text = transform(node.data, parent.style.textTransform);
|
||||
return new TextContainer(text, parent, parseTextBounds(text, parent, node));
|
||||
}
|
||||
}
|
||||
|
||||
|
25
src/drawing/Circle.js
Normal file
25
src/drawing/Circle.js
Normal file
@ -0,0 +1,25 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
export default class Circle {
|
||||
x: number;
|
||||
y: number;
|
||||
radius: number;
|
||||
|
||||
constructor(x: number, y: number, radius: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.radius = radius;
|
||||
if (__DEV__) {
|
||||
if (isNaN(x)) {
|
||||
console.error(`Invalid x value given for Circle`);
|
||||
}
|
||||
if (isNaN(y)) {
|
||||
console.error(`Invalid y value given for Circle`);
|
||||
}
|
||||
if (isNaN(radius)) {
|
||||
console.error(`Invalid radius value given for Circle`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,8 +6,8 @@ import type ImageLoader, {ImageElement} from '../ImageLoader';
|
||||
|
||||
import Color from '../Color';
|
||||
import Length from '../Length';
|
||||
import Size from '../Size';
|
||||
import Vector from '../Vector';
|
||||
import Size from '../drawing/Size';
|
||||
import Vector from '../drawing/Vector';
|
||||
import {calculateBorderBoxPath, calculatePaddingBoxPath} from '../Bounds';
|
||||
|
||||
export type Background = {
|
||||
|
Loading…
Reference in New Issue
Block a user