Implement input/textarea/select element rendering

This commit is contained in:
Niklas von Hertzen 2017-08-05 00:00:17 +08:00
parent adb1f50f00
commit 56b3b6df27
12 changed files with 275 additions and 66 deletions

View File

@ -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;

View File

@ -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
View 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;
};

View File

@ -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,

View File

@ -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;

View File

@ -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 {

View File

@ -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
View 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`);
}
}
}
}

View File

@ -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 = {