From 7335984ab7247d0b02bea95b7b037977261626de Mon Sep 17 00:00:00 2001 From: Matthias Christen Date: Fri, 15 Dec 2017 22:55:27 +0100 Subject: [PATCH] added support for rendering ordered lists and list-style --- package.json | 3 +- src/ListItem.js | 146 +++++++++++++++++++++++++++++ src/NodeParser.js | 4 + src/Util.js | 18 ++++ tests/reftests/list/liststyle.html | 75 +++++++++++++++ 5 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 src/ListItem.js create mode 100644 tests/reftests/list/liststyle.html diff --git a/package.json b/package.json index d6a464eef..1f7e9d7ec 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "homepage": "https://html2canvas.hertzen.com", "license": "MIT", "dependencies": { - "punycode": "2.1.0" + "punycode": "2.1.0", + "liststyletype-formatter": "latest" } } diff --git a/src/ListItem.js b/src/ListItem.js new file mode 100644 index 000000000..78acf1c7c --- /dev/null +++ b/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; + +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)); + } +}; diff --git a/src/NodeParser.js b/src/NodeParser.js index 3fb854d1e..825f1e167 100644 --- a/src/NodeParser.js +++ b/src/NodeParser.js @@ -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, @@ -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'; diff --git a/src/Util.js b/src/Util.js index 7d390e802..44a1a1382 100644 --- a/src/Util.js +++ b/src/Util.js @@ -17,5 +17,23 @@ export const copyCSSStyles = (style: CSSStyleDeclaration, target: HTMLElement): return target; }; +export const getParentOfType = (node: HTMLElement, parentTypes: Array): ?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'; diff --git a/tests/reftests/list/liststyle.html b/tests/reftests/list/liststyle.html new file mode 100644 index 000000000..768324208 --- /dev/null +++ b/tests/reftests/list/liststyle.html @@ -0,0 +1,75 @@ + + + + List tests + + + + + + +
    +
  • Alpha
  • +
  • Beta
  • +
  • Gamma
  • +
+
    +
  • Alpha
  • +
  • Beta
  • +
  • Gamma
  • +
+
    +
  • Alpha
  • +
  • Beta
  • +
  • Gamma
  • +
+
    +
  1. Alpha
  2. +
  3. Beta
  4. +
  5. Gamma
  6. +
+
    +
  1. Alpha
  2. +
  3. Beta
  4. +
  5. Gamma
  6. +
+
    +
  1. Alpha
  2. +
  3. Beta
  4. +
  5. Gamma
  6. +
+
    +
  1. Alpha
  2. +
  3. Beta
  4. +
  5. Gamma
  6. +
+ + \ No newline at end of file