Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for rendering enumerations in ordered lists and for list-style #1320

Merged
merged 1 commit into from Dec 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -77,6 +77,7 @@
"homepage": "https://html2canvas.hertzen.com",
"license": "MIT",
"dependencies": {
"punycode": "2.1.0"
"punycode": "2.1.0",
"liststyletype-formatter": "latest"
}
}
146 changes: 146 additions & 0 deletions src/ListItem.js
@@ -0,0 +1,146 @@
/* @flow */
'use strict';

import type {BackgroundSource} from './parsing/background';
import type ResourceLoader from './ResourceLoader';

import {parseBackgroundImage} from './parsing/background';
import {copyCSSStyles, getParentOfType} from './Util';
import NodeContainer from './NodeContainer';
import TextContainer from './TextContainer';
import ListStyleTypeFormatter from 'liststyletype-formatter';

// Margin between the enumeration and the list item content
const MARGIN_RIGHT = 7;

export const LIST_STYLE_POSITION = {
INSIDE: 0,
OUTSIDE: 1
};

export type ListStylePosition = $Values<typeof LIST_STYLE_POSITION>;

export type ListStyle = {
listStyleType: string,
listStyleImage: BackgroundSource,
listStylePosition: ListStylePosition
};

export const parseListStyle = (style: CSSStyleDeclaration): ListStyle => {
const listStyleImage = parseBackgroundImage(style.getPropertyValue('list-style-image'));
return {
listStyleType: style.getPropertyValue('list-style-type'),
listStyleImage: listStyleImage && listStyleImage[0],
listStylePosition: parseListStylePosition(style.getPropertyValue('list-style-position'))
};
};

export const parseListStylePosition = (position: string): ListStylePosition => {
switch (position) {
case 'inside':
return LIST_STYLE_POSITION.INSIDE;
case 'outside':
return LIST_STYLE_POSITION.OUTSIDE;
}
return LIST_STYLE_POSITION.OUTSIDE;
};

const getListItemValue = (node: HTMLLIElement): number => {
if (node.value) {
return node.value;
}

const listContainer = getParentOfType(node, ['OL', 'UL']);
if (!listContainer || listContainer.tagName === 'UL') {
// The actual value isn't needed for unordered lists, just return an arbitrary value
return 1;
}

// $FlowFixMe
let value = listContainer.start !== undefined ? listContainer.start - 1 : 0;
const listItems = listContainer.querySelectorAll('li');
const lenListItems = listItems.length;

for (let i = 0; i < lenListItems; i++) {
// $FlowFixMe
const listItem: HTMLLIElement = listItems[i];
if (getParentOfType(listItem, ['OL']) === listContainer) {
value = listItem.hasAttribute('value') ? listItem.value : value + 1;
}
if (listItem === node) {
break;
}
}

return value;
};

export const inlineListItemElement = (
node: HTMLLIElement,
container: NodeContainer,
resourceLoader: ResourceLoader
): void => {
const style = node.ownerDocument.defaultView.getComputedStyle(node, null);
const listStyle = parseListStyle(style);

if (listStyle.listStyleType === 'none') {
return;
}

const wrapper = node.ownerDocument.createElement('html2canvaswrapper');
copyCSSStyles(style, wrapper);

wrapper.style.position = 'fixed';
wrapper.style.bottom = 'auto';

switch (listStyle.listStylePosition) {
case LIST_STYLE_POSITION.OUTSIDE:
wrapper.style.left = 'auto';
wrapper.style.right = `${node.ownerDocument.defaultView.innerWidth -
container.bounds.left +
MARGIN_RIGHT}px`;
wrapper.style.textAlign = 'right';
break;
case LIST_STYLE_POSITION.INSIDE:
wrapper.style.left = `${container.bounds.left}px`;
wrapper.style.right = 'auto';
wrapper.style.textAlign = 'left';
break;
}

let text;
if (listStyle.listStyleImage && listStyle.listStyleImage !== 'none') {
if (listStyle.listStyleImage.method === 'url') {
const image = node.ownerDocument.createElement('img');
image.src = listStyle.listStyleImage.args[0];
wrapper.style.top = `${container.bounds.top}px`;
wrapper.style.width = 'auto';
wrapper.style.height = 'auto';
wrapper.appendChild(image);
} else {
const size = parseFloat(container.style.font.fontSize) * 0.5;
wrapper.style.top = `${container.bounds.top + container.bounds.height - 1.5 * size}px`;
wrapper.style.width = `${size}px`;
wrapper.style.height = `${size}px`;
wrapper.style.backgroundImage = style.listStyleImage;
}
} else {
text = node.ownerDocument.createTextNode(
ListStyleTypeFormatter.format(getListItemValue(node), style.listStyleType)
);
wrapper.appendChild(text);
wrapper.style.top = `${container.bounds.top}px`;
}

// $FlowFixMe
const body: HTMLBodyElement = node.ownerDocument.body;
body.appendChild(wrapper);

if (text) {
container.childNodes.push(TextContainer.fromTextNode(text, container));
body.removeChild(wrapper);
} else {
// $FlowFixMe
container.childNodes.push(new NodeContainer(wrapper, container, resourceLoader, 0));
}
};
4 changes: 4 additions & 0 deletions src/NodeParser.js
Expand Up @@ -6,6 +6,7 @@ import StackingContext from './StackingContext';
import NodeContainer from './NodeContainer';
import TextContainer from './TextContainer';
import {inlineInputElement, inlineTextAreaElement, inlineSelectElement} from './Input';
import {inlineListItemElement} from './ListItem';

export const NodeParser = (
node: HTMLElement,
Expand Down Expand Up @@ -71,6 +72,9 @@ const parseNodeTree = (
} else if (childNode.tagName === 'SELECT') {
// $FlowFixMe
inlineSelectElement(childNode, container);
} else if (childNode.tagName === 'LI') {
// $FlowFixMe
inlineListItemElement(childNode, container, resourceLoader);
}

const SHOULD_TRAVERSE_CHILDREN = childNode.tagName !== 'TEXTAREA';
Expand Down
18 changes: 18 additions & 0 deletions src/Util.js
Expand Up @@ -17,5 +17,23 @@ export const copyCSSStyles = (style: CSSStyleDeclaration, target: HTMLElement):
return target;
};

export const getParentOfType = (node: HTMLElement, parentTypes: Array<string>): ?HTMLElement => {
let parent = node.parentNode;
if (!parent) {
return null;
}

// $FlowFixMe
while (parentTypes.indexOf(parent.tagName) < 0) {
parent = parent.parentNode;
if (!parent) {
return null;
}
}

// $FlowFixMe
return parent;
};

export const SMALL_IMAGE =
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
75 changes: 75 additions & 0 deletions tests/reftests/list/liststyle.html
@@ -0,0 +1,75 @@
<!doctype html>
<html>
<head>
<title>List tests</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../../test.js"></script>

<style>
.list1 {
list-style-type: circle;
}

.list2 {
list-style-image: url(../../assets/image.jpg);
}

.list3 {
list-style-image: linear-gradient(60deg, deeppink, aquamarine);
list-style-position: inside;
}

.list4 {
}

.list5 {
list-style-type: lower-roman;
}

.list6 {
list-style-type: hiragana;
}

.list7 {
list-style-type: simp-chinese-informal;
}
</style>
</head>
<body>
<ul class="list1">
<li>Alpha</li>
<li>Beta</li>
<li>Gamma</li>
</ul>
<ul class="list2">
<li>Alpha</li>
<li>Beta</li>
<li>Gamma</li>
</ul>
<ul class="list3">
<li>Alpha</li>
<li>Beta</li>
<li>Gamma</li>
</ul>
<ol class="list4" start="-1">
<li>Alpha</li>
<li>Beta</li>
<li>Gamma</li>
</ol>
<ol class="list5">
<li>Alpha</li>
<li value="5">Beta</li>
<li>Gamma</li>
</ol>
<ol class="list6">
<li>Alpha</li>
<li>Beta</li>
<li>Gamma</li>
</ol>
<ol class="list7">
<li>Alpha</li>
<li>Beta</li>
<li>Gamma</li>
</ol>
</body>
</html>