From cfa727474e1e1f0e2dd51d8bc5d9915818b4e598 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 2 Jun 2021 19:14:41 +0200 Subject: [PATCH] XFA - Fix layout issues (again) - some elements weren't displayed because their rotation angle was not taken into account; - fix box model (XFA concept): - remove use of outline; - position correctly border which isn't part of box dimensions; - fix margins issues (see issue #13474). - move border on button instead of having it on wrapping div; --- src/core/xfa/html_utils.js | 224 ++++++++++------------- src/core/xfa/layout.js | 34 ++-- src/core/xfa/template.js | 345 ++++++++++++++++++++--------------- src/core/xfa/xhtml.js | 12 +- src/display/xfa_layer.js | 6 +- test/unit/xfa_tohtml_spec.js | 21 ++- web/xfa_layer_builder.css | 118 ++++++++---- 7 files changed, 415 insertions(+), 345 deletions(-) diff --git a/src/core/xfa/html_utils.js b/src/core/xfa/html_utils.js index d20ca1477..ba72649aa 100644 --- a/src/core/xfa/html_utils.js +++ b/src/core/xfa/html_utils.js @@ -172,68 +172,22 @@ const converters = { } } else { switch (node.hAlign) { - case "right": + case "left": + style.alignSelf = "start"; + break; case "center": - style.justifyContent = node.hAlign; + style.alignSelf = "center"; + break; + case "right": + style.alignSelf = "end"; break; } } }, - borderMarginPadding(node, style) { - // Get border width in order to compute margin and padding. - const borderWidths = [0, 0, 0, 0]; - const borderInsets = [0, 0, 0, 0]; - const marginNode = node.margin - ? [ - node.margin.topInset, - node.margin.rightInset, - node.margin.bottomInset, - node.margin.leftInset, - ] - : [0, 0, 0, 0]; - - let borderMargin; - if (node.border) { - Object.assign(style, node.border[$toStyle](borderWidths, borderInsets)); - borderMargin = style.margin; - delete style.margin; - } - - if (borderWidths.every(x => x === 0)) { - if (marginNode.every(x => x === 0)) { - return; - } - - // No border: margin & padding are padding - Object.assign(style, node.margin[$toStyle]()); - style.padding = style.margin; - delete style.margin; - delete style.outline; - delete style.outlineOffset; - return; - } - + margin(node, style) { if (node.margin) { - Object.assign(style, node.margin[$toStyle]()); - style.padding = style.margin; - delete style.margin; + style.margin = node.margin[$toStyle]().margin; } - - if (!style.borderWidth) { - // We've an outline so no need to fake one. - return; - } - - style.borderData = { - borderWidth: style.borderWidth, - borderColor: style.borderColor, - borderStyle: style.borderStyle, - margin: borderMargin, - }; - - delete style.borderWidth; - delete style.borderColor; - delete style.borderStyle; }, }; @@ -441,92 +395,99 @@ function toStyle(node, ...names) { return style; } -function addExtraDivForBorder(html) { - const style = html.attributes.style; - const data = style.borderData; - const children = []; +function createWrapper(node, html) { + const { attributes } = html; + const { style } = attributes; - const attributes = { - class: "xfaWrapper", - style: Object.create(null), + const wrapper = { + name: "div", + attributes: { + class: ["xfaWrapper"], + style: Object.create(null), + }, + children: [html], }; - for (const key of ["top", "left"]) { + attributes.class.push("xfaWrapped"); + + if (node.border) { + const { widths, insets } = node.border[$extra]; + let shiftH = 0; + let shiftW = 0; + switch (node.border.hand) { + case "even": + shiftW = widths[0] / 2; + shiftH = widths[3] / 2; + break; + case "left": + shiftW = widths[0]; + shiftH = widths[3]; + break; + } + const insetsW = insets[1] + insets[3]; + const insetsH = insets[0] + insets[2]; + const border = { + name: "div", + attributes: { + class: ["xfaBorder"], + style: { + top: `${insets[0] - widths[0] + shiftW}px`, + left: `${insets[3] - widths[3] + shiftH}px`, + width: insetsW ? `calc(100% - ${insetsW}px)` : "100%", + height: insetsH ? `calc(100% - ${insetsH}px)` : "100%", + }, + }, + children: [], + }; + + for (const key of [ + "border", + "borderWidth", + "borderColor", + "borderRadius", + "borderStyle", + ]) { + if (style[key] !== undefined) { + border.attributes.style[key] = style[key]; + delete style[key]; + } + } + wrapper.children.push(border); + } + + for (const key of [ + "background", + "backgroundClip", + "top", + "left", + "width", + "height", + "minWidth", + "minHeight", + "maxWidth", + "maxHeight", + "transform", + "transformOrigin", + ]) { if (style[key] !== undefined) { - attributes.style[key] = style[key]; + wrapper.attributes.style[key] = style[key]; + delete style[key]; } } - delete style.top; - delete style.left; - if (style.position === "absolute") { - attributes.style.position = "absolute"; + wrapper.attributes.style.position = "absolute"; } else { - attributes.style.position = "relative"; + wrapper.attributes.style.position = "relative"; } delete style.position; - if (style.justifyContent) { - attributes.style.justifyContent = style.justifyContent; - delete style.justifyContent; + if (style.alignSelf) { + wrapper.attributes.style.alignSelf = style.alignSelf; + delete style.alignSelf; } - if (data) { - delete style.borderData; - - let insets; - if (data.margin) { - insets = data.margin.split(" "); - delete data.margin; - } else { - insets = ["0px", "0px", "0px", "0px"]; - } - - let width = "100%"; - let height = width; - - if (insets[1] !== "0px" || insets[3] !== "0px") { - width = `calc(100% - ${parseInt(insets[1]) + parseInt(insets[3])}px`; - } - - if (insets[0] !== "0px" || insets[2] !== "0px") { - height = `calc(100% - ${parseInt(insets[0]) + parseInt(insets[2])}px`; - } - - const borderStyle = { - top: insets[0], - left: insets[3], - width, - height, - }; - - for (const [k, v] of Object.entries(data)) { - borderStyle[k] = v; - } - - if (style.transform) { - borderStyle.transform = style.transform; - } - - const borderDiv = { - name: "div", - attributes: { - class: "xfaBorderDiv", - style: borderStyle, - }, - }; - - children.push(borderDiv); - } - - children.push(html); - - return { - name: "div", - attributes, - children, - }; + return wrapper; } function fixTextIndent(styles) { @@ -535,11 +496,12 @@ function fixTextIndent(styles) { return; } + // If indent is negative then it's a hanging indent. const align = styles.textAlign || "left"; if (align === "left" || align === "right") { - const name = "margin" + (align === "left" ? "Left" : "Right"); - const margin = getMeasurement(styles[name], "0px"); - styles[name] = `${margin - indent}pt`; + const name = "padding" + (align === "left" ? "Left" : "Right"); + const padding = getMeasurement(styles[name], "0px"); + styles[name] = `${padding - indent}px`; } } @@ -573,8 +535,8 @@ function getFonts(family) { } export { - addExtraDivForBorder, computeBbox, + createWrapper, fixDimensions, fixTextIndent, getFonts, diff --git a/src/core/xfa/layout.js b/src/core/xfa/layout.js index b7eee56d8..d56b0d774 100644 --- a/src/core/xfa/layout.js +++ b/src/core/xfa/layout.js @@ -46,6 +46,9 @@ import { measureToString } from "./html_utils.js"; */ function flushHTML(node) { + if (!node[$extra]) { + return null; + } const attributes = node[$extra].attributes; const html = { name: "div", @@ -88,7 +91,7 @@ function addHTML(node, html, bbox) { extra.line = { name: "div", attributes: { - class: node.layout === "lr-tb" ? "xfaLr" : "xfaRl", + class: [node.layout === "lr-tb" ? "xfaLr" : "xfaRl"], }, children: [], }; @@ -120,12 +123,7 @@ function addHTML(node, html, bbox) { extra.height = Math.max(extra.height, h); const height = measureToString(extra.height); for (const child of extra.children) { - if (child.attributes.class === "xfaWrapper") { - child.children[child.children.length - 1].attributes.style.height = - height; - } else { - child.attributes.style.height = height; - } + child.attributes.style.height = height; } break; } @@ -148,6 +146,12 @@ function addHTML(node, html, bbox) { function getAvailableSpace(node) { const availableSpace = node[$extra].availableSpace; + const [marginW, marginH] = node.margin + ? [ + node.margin.leftInset + node.margin.rightInset, + node.margin.topInset + node.margin.leftInset, + ] + : [0, 0]; switch (node.layout) { case "lr-tb": @@ -155,18 +159,18 @@ function getAvailableSpace(node) { switch (node[$extra].attempt) { case 0: return { - width: availableSpace.width - node[$extra].currentWidth, - height: availableSpace.height - node[$extra].prevHeight, + width: availableSpace.width - marginW - node[$extra].currentWidth, + height: availableSpace.height - marginH - node[$extra].prevHeight, }; case 1: return { - width: availableSpace.width, - height: availableSpace.height - node[$extra].height, + width: availableSpace.width - marginW, + height: availableSpace.height - marginH - node[$extra].height, }; default: return { width: Infinity, - height: availableSpace.height - node[$extra].prevHeight, + height: availableSpace.height - marginH - node[$extra].prevHeight, }; } case "rl-row": @@ -174,12 +178,12 @@ function getAvailableSpace(node) { const width = node[$extra].columnWidths .slice(node[$extra].currentColumn) .reduce((a, x) => a + x); - return { width, height: availableSpace.height }; + return { width, height: availableSpace.height - marginH }; case "table": case "tb": return { - width: availableSpace.width, - height: availableSpace.height - node[$extra].height, + width: availableSpace.width - marginW, + height: availableSpace.height - marginH - node[$extra].height, }; case "position": default: diff --git a/src/core/xfa/template.js b/src/core/xfa/template.js index 794c684a7..a9eebffc3 100644 --- a/src/core/xfa/template.js +++ b/src/core/xfa/template.js @@ -14,6 +14,7 @@ */ import { + $acceptWhitespace, $addHTML, $appendChild, $break, @@ -34,6 +35,7 @@ import { $namespaceId, $nodeName, $onChild, + $onText, $removeChild, $searchNode, $setSetAttributes, @@ -50,9 +52,10 @@ import { XFAObjectArray, } from "./xfa_object.js"; import { $buildXFAObject, NamespaceIds } from "./namespaces.js"; +import { addHTML, flushHTML, getAvailableSpace } from "./layout.js"; import { - addExtraDivForBorder, computeBbox, + createWrapper, fixDimensions, fixTextIndent, getFonts, @@ -61,7 +64,6 @@ import { measureToString, toStyle, } from "./html_utils.js"; -import { addHTML, flushHTML, getAvailableSpace } from "./layout.js"; import { getBBox, getColor, @@ -103,26 +105,90 @@ function getRoot(node) { return parent; } +function getTransformedBBox(node) { + // Take into account rotation and anchor the get the + // real bounding box. + let w = node.w === "" ? NaN : node.w; + let h = node.h === "" ? NaN : node.h; + let [centerX, centerY] = [0, 0]; + switch (node.anchorType || "") { + case "bottomCenter": + [centerX, centerY] = [w / 2, h]; + break; + case "bottomLeft": + [centerX, centerY] = [0, h]; + break; + case "bottomRight": + [centerX, centerY] = [w, h]; + break; + case "middleCenter": + [centerX, centerY] = [w / 2, h / 2]; + break; + case "middleLeft": + [centerX, centerY] = [0, h / 2]; + break; + case "middleRight": + [centerX, centerY] = [w, h / 2]; + break; + case "topCenter": + [centerX, centerY] = [w / 2, 0]; + break; + case "topRight": + [centerX, centerY] = [w, 0]; + break; + } + + let x; + let y; + switch (node.rotate || 0) { + case 0: + [x, y] = [-centerX, -centerY]; + break; + case 90: + [x, y] = [-centerY, centerX]; + [w, h] = [h, -w]; + break; + case 180: + [x, y] = [centerX, centerY]; + [w, h] = [-w, -h]; + break; + case 270: + [x, y] = [centerY, -centerX]; + [w, h] = [-h, w]; + break; + } + + return [ + node.x + x + Math.min(0, w), + node.y + y + Math.min(0, h), + Math.abs(w), + Math.abs(h), + ]; +} + const NOTHING = 0; const NOSPACE = 1; const VALID = 2; function checkDimensions(node, space) { - if (node.w !== "" && Math.round(node.w + node.x - space.width) > 1) { + const [x, y, w, h] = getTransformedBBox(node); + if (node.w !== "" && Math.round(x + w - space.width) > 1) { const area = getRoot(node)[$extra].currentContentArea; - if (node.w + node.x > area.w) { - return NOTHING; - } - return NOSPACE; - } - - if (node.h !== "" && Math.round(node.h + node.y - space.height) > 1) { - const area = getRoot(node)[$extra].currentContentArea; - if (node.h + node.y > area.h) { + if (x + w > area.w) { return NOTHING; } return NOSPACE; } + + if (node.h !== "" && Math.round(y + h - space.height) > 1) { + const area = getRoot(node)[$extra].currentContentArea; + if (y + h > area.h) { + return NOTHING; + } + + return NOSPACE; + } + return VALID; } @@ -211,7 +277,7 @@ class Area extends XFAObject { const attributes = { style, id: this[$uid], - class: "xfaArea", + class: ["xfaArea"], }; if (this.name) { @@ -503,7 +569,7 @@ class Border extends XFAObject { this.margin = null; } - [$toStyle](widths, margins) { + [$toStyle]() { // TODO: incomplete. const edges = this.edge.children.slice(); if (edges.length < 4) { @@ -513,41 +579,32 @@ class Border extends XFAObject { } } - widths = widths || [0, 0, 0, 0]; - for (let i = 0; i < 4; i++) { - widths[i] = edges[i].thickness; - } - - margins = margins || [0, 0, 0, 0]; - const edgeStyles = edges.map(node => { const style = node[$toStyle](); style.color = style.color || "#000000"; return style; }); - let style; - if (this.margin) { - style = this.margin[$toStyle](); - margins[0] = this.margin.topInset; - margins[1] = this.margin.rightInset; - margins[2] = this.margin.bottomInset; - margins[3] = this.margin.leftInset; - } else { - style = Object.create(null); - } - let isForUi = false; - const parent = this[$getParent](); - const grandParent = parent ? parent[$getParent]() : null; - if (grandParent instanceof Ui) { - isForUi = true; + const widths = edges.map(edge => edge.thickness); + const insets = [0, 0, 0, 0]; + if (this.margin) { + insets[0] = this.margin.topInset; + insets[1] = this.margin.rightInset; + insets[2] = this.margin.bottomInset; + insets[3] = this.margin.leftInset; + } + this[$extra] = { widths, insets }; + // TODO: hand. + + const style = Object.create(null); + if (this.margin) { + Object.assign(style, this.margin[$toStyle]()); } if (this.fill) { Object.assign(style, this.fill[$toStyle]()); } - let hasRadius = false; if (this.corner.children.some(node => node.radius !== 0)) { const cornerStyles = this.corner.children.map(node => node[$toStyle]()); if (cornerStyles.length === 2 || cornerStyles.length === 3) { @@ -558,62 +615,24 @@ class Border extends XFAObject { } style.borderRadius = cornerStyles.map(s => s.radius).join(" "); - hasRadius = true; } - const firstEdge = edgeStyles[0]; - if ( - !hasRadius && - (this.edge.children.length <= 1 || - (edgeStyles.every( - x => - x.style === firstEdge.style && - x.width === firstEdge.width && - x.color === firstEdge.color - ) && - margins.every(x => x === margins[0]))) - ) { - // Rectangular border and same values for each edge then we've an outline - // so no need to emulate it. - - let borderStyle; - switch (this.presence) { - case "invisible": - case "hidden": - borderStyle = ""; - break; - case "inactive": - borderStyle = "none"; - break; - default: - borderStyle = firstEdge.style; - break; - } - - style.outline = `${firstEdge.width} ${firstEdge.color} ${borderStyle}`; - const offset = edges[0].thickness + margins[0]; - style.outlineOffset = `-${measureToString(offset)}`; - if (isForUi) { - style.padding = `${measureToString(offset + 1)}`; - } - } else { - switch (this.presence) { - case "invisible": - case "hidden": - style.borderStyle = ""; - break; - case "inactive": - style.borderStyle = "none"; - break; - default: - style.borderStyle = edgeStyles.map(s => s.style).join(" "); - break; - } - - style.borderWidth = edgeStyles.map(s => s.width).join(" "); - style.borderColor = edgeStyles.map(s => s.color).join(" "); + switch (this.presence) { + case "invisible": + case "hidden": + style.borderStyle = ""; + break; + case "inactive": + style.borderStyle = "none"; + break; + default: + style.borderStyle = edgeStyles.map(s => s.style).join(" "); + break; } + style.borderWidth = edgeStyles.map(s => s.width).join(" "); + style.borderColor = edgeStyles.map(s => s.color).join(" "); + return style; } } @@ -729,7 +748,8 @@ class Button extends XFAObject { return HTMLResult.success({ name: "button", attributes: { - class: "xfaButton", + id: this[$uid], + class: ["xfaButton"], style: {}, }, children: [], @@ -830,7 +850,7 @@ class Caption extends XFAObject { name: "div", attributes: { style, - class: "xfaCaption", + class: ["xfaCaption"], }, children, }); @@ -917,7 +937,7 @@ class CheckButton extends XFAObject { const input = { name: "input", attributes: { - class: className, + class: [className], style, fieldId, type, @@ -935,7 +955,7 @@ class CheckButton extends XFAObject { return HTMLResult.success({ name: "label", attributes: { - class: "xfaLabel", + class: ["xfaLabel"], }, children: [input], }); @@ -990,7 +1010,7 @@ class ChoiceList extends XFAObject { } const selectAttributes = { - class: "xfaSelect", + class: ["xfaSelect"], fieldId: this[$getParent]()[$getParent]()[$uid], style, }; @@ -1002,7 +1022,7 @@ class ChoiceList extends XFAObject { return HTMLResult.success({ name: "label", attributes: { - class: "xfaLabel", + class: ["xfaLabel"], }, children: [ { @@ -1101,7 +1121,7 @@ class ContentArea extends XFAObject { children: [], attributes: { style, - class: "xfaContentarea", + class: ["xfaContentarea"], id: this[$uid], }, }); @@ -1219,7 +1239,7 @@ class DateTimeEdit extends XFAObject { attributes: { type: "text", fieldId: this[$getParent]()[$getParent]()[$uid], - class: "xfaTextfield", + class: ["xfaTextfield"], style, }, }; @@ -1227,7 +1247,7 @@ class DateTimeEdit extends XFAObject { return HTMLResult.success({ name: "label", attributes: { - class: "xfaLabel", + class: ["xfaLabel"], }, children: [html], }); @@ -1432,7 +1452,8 @@ class Draw extends XFAObject { "presence", "rotate", "anchorType", - "borderMarginPadding" + "border", + "margin" ); const classNames = ["xfaDraw"]; @@ -1443,7 +1464,7 @@ class Draw extends XFAObject { const attributes = { style, id: this[$uid], - class: classNames.join(" "), + class: classNames, }; if (this.name) { @@ -1456,16 +1477,15 @@ class Draw extends XFAObject { children: [], }; - const extra = addExtraDivForBorder(html); const bbox = computeBbox(this, html, availableSpace); const value = this.value ? this.value[$toHTML](availableSpace).html : null; if (value === null) { - return HTMLResult.success(extra, bbox); + return HTMLResult.success(createWrapper(this, html), bbox); } html.children.push(value); - if (value.attributes.class === "xfaRich") { + if (value.attributes.class.includes("xfaRich")) { if (this.h === "") { style.height = "auto"; } @@ -1502,7 +1522,7 @@ class Draw extends XFAObject { } } - return HTMLResult.success(extra, bbox); + return HTMLResult.success(createWrapper(this, html), bbox); } } @@ -1528,11 +1548,7 @@ class Edge extends XFAObject { "lowered", "raised", ]); - // Cheat the thickness to have something nice at the end - this.thickness = Math.max( - 1, - Math.round(getMeasurement(attributes.thickness, "0.5pt")) - ); + this.thickness = getMeasurement(attributes.thickness, "0.5pt"); this.use = attributes.use || ""; this.usehref = attributes.usehref || ""; this.color = null; @@ -1544,7 +1560,7 @@ class Edge extends XFAObject { const style = toStyle(this, "visibility"); Object.assign(style, { linecap: this.cap, - width: measureToString(Math.max(1, Math.round(this.thickness))), + width: measureToString(this.thickness), color: this.color ? this.color[$toStyle]() : "#000000", style: "", }); @@ -1983,7 +1999,8 @@ class ExclGroup extends XFAObject { "dimensions", "position", "presence", - "borderMarginPadding", + "border", + "margin", "hAlign" ); const classNames = ["xfaExclgroup"]; @@ -1993,7 +2010,7 @@ class ExclGroup extends XFAObject { } attributes.style = style; - attributes.class = classNames.join(" "); + attributes.class = classNames; if (this.name) { attributes.xfaName = this.name; @@ -2025,6 +2042,9 @@ class ExclGroup extends XFAObject { } if (failure) { + if (this.layout === "position") { + delete this[$extra]; + } return HTMLResult.FAILURE; } @@ -2042,13 +2062,12 @@ class ExclGroup extends XFAObject { style.height = measureToString(this[$extra].height + marginV); } - let html = { + const html = { name: "div", attributes, children, }; - html = addExtraDivForBorder(html); let bbox; if (this.w !== "" && this.h !== "") { bbox = [this.x, this.y, this.w, this.h]; @@ -2061,7 +2080,7 @@ class ExclGroup extends XFAObject { delete this[$extra]; - return HTMLResult.success(html, bbox); + return HTMLResult.success(createWrapper(this, html), bbox); } } @@ -2227,7 +2246,7 @@ class Field extends XFAObject { "rotate", "anchorType", "presence", - "borderMarginPadding", + "margin", "hAlign" ); @@ -2240,7 +2259,7 @@ class Field extends XFAObject { const attributes = { style, id: this[$uid], - class: classNames.join(" "), + class: classNames, }; if (this.name) { @@ -2248,29 +2267,37 @@ class Field extends XFAObject { } const children = []; - let html = { + const html = { name: "div", attributes, children, }; - const bbox = computeBbox(this, html, availableSpace); - html = addExtraDivForBorder(html); + const borderStyle = this.border ? this.border[$toStyle]() : null; + const bbox = computeBbox(this, html, availableSpace); const ui = this.ui ? this.ui[$toHTML]().html : null; if (!ui) { - return HTMLResult.success(html, bbox); + Object.assign(style, borderStyle); + return HTMLResult.success(createWrapper(this, html), bbox); } if (!ui.attributes.style) { ui.attributes.style = Object.create(null); } + + if (this.ui.button) { + Object.assign(ui.attributes.style, borderStyle); + } else { + Object.assign(style, borderStyle); + } + children.push(ui); if (this.value) { if (this.ui.imageEdit) { ui.children.push(this.value[$toHTML]().html); - } else if (ui.name !== "button") { + } else if (!this.ui.button) { const value = this.value[$toHTML]().html; if (value) { if (ui.children[0].name === "textarea") { @@ -2284,43 +2311,42 @@ class Field extends XFAObject { const caption = this.caption ? this.caption[$toHTML]().html : null; if (!caption) { - return HTMLResult.success(html, bbox); + return HTMLResult.success(createWrapper(this, html), bbox); } - if (ui.name === "button") { - ui.attributes.style.background = style.background; - delete style.background; + if (this.ui.button) { if (caption.name === "div") { caption.name = "span"; } ui.children.push(caption); - return HTMLResult.success(html, bbox); } + if (!ui.attributes.class) { + ui.attributes.class = []; + } + ui.children.splice(0, 0, caption); switch (this.caption.placement) { case "left": - ui.attributes.style.flexDirection = "row"; + ui.attributes.class.push("xfaLeft"); break; case "right": - ui.attributes.style.flexDirection = "row-reverse"; + ui.attributes.class.push("xfaRight"); break; case "top": - ui.attributes.style.alignItems = "start"; - ui.attributes.style.flexDirection = "column"; + ui.attributes.class.push("xfaTop"); break; case "bottom": - ui.attributes.style.alignItems = "start"; - ui.attributes.style.flexDirection = "column-reverse"; + ui.attributes.class.push("xfaBottom"); break; case "inline": - delete ui.attributes.class; - caption.attributes.style.float = "left"; + // TODO; + ui.attributes.class.push("xfaInline"); break; } - return HTMLResult.success(html, bbox); + return HTMLResult.success(createWrapper(this, html), bbox); } } @@ -2660,7 +2686,7 @@ class Image extends StringObject { return HTMLResult.success({ name: "img", attributes: { - class: "xfaImage", + class: ["xfaImage"], style: {}, src: URL.createObjectURL(blob), }, @@ -2987,7 +3013,7 @@ class NumericEdit extends XFAObject { attributes: { type: "text", fieldId: this[$getParent]()[$getParent]()[$uid], - class: "xfaTextfield", + class: ["xfaTextfield"], style, }, }; @@ -2995,7 +3021,7 @@ class NumericEdit extends XFAObject { return HTMLResult.success({ name: "label", attributes: { - class: "xfaLabel", + class: ["xfaLabel"], }, children: [html], }); @@ -3955,7 +3981,8 @@ class Subform extends XFAObject { "dimensions", "position", "presence", - "borderMarginPadding", + "border", + "margin", "hAlign" ); const classNames = ["xfaSubform"]; @@ -3965,7 +3992,7 @@ class Subform extends XFAObject { } attributes.style = style; - attributes.class = classNames.join(" "); + attributes.class = classNames; if (this.name) { attributes.xfaName = this.name; @@ -3997,6 +4024,9 @@ class Subform extends XFAObject { } if (failure) { + if (this.layout === "position") { + delete this[$extra]; + } return HTMLResult.FAILURE; } @@ -4014,13 +4044,12 @@ class Subform extends XFAObject { style.height = measureToString(this[$extra].height + marginV); } - let html = { + const html = { name: "div", attributes, children, }; - html = addExtraDivForBorder(html); let bbox; if (this.w !== "" && this.h !== "") { bbox = [this.x, this.y, this.w, this.h]; @@ -4034,13 +4063,16 @@ class Subform extends XFAObject { if (this.breakAfter.children.length >= 1) { const breakAfter = this.breakAfter.children[0]; getRoot(this)[$break](breakAfter); - this[$extra].afterBreakAfter = HTMLResult.success(html, bbox); + this[$extra].afterBreakAfter = HTMLResult.success( + createWrapper(this, html), + bbox + ); return HTMLResult.FAILURE; } delete this[$extra]; - return HTMLResult.success(html, bbox); + return HTMLResult.success(createWrapper(this, html), bbox); } } @@ -4296,8 +4328,8 @@ class Template extends XFAObject { } const contentAreas = pageArea.contentArea.children; - const htmlContentAreas = page.children.filter( - node => node.attributes.class === "xfaContentarea" + const htmlContentAreas = page.children.filter(node => + node.attributes.class.includes("xfaContentarea") ); for (let i = 0, ii = contentAreas.length; i < ii; i++) { const contentArea = (this[$extra].currentContentArea = contentAreas[i]); @@ -4404,6 +4436,10 @@ class Text extends ContentObject { this.usehref = attributes.usehref || ""; } + [$acceptWhitespace]() { + return true; + } + [$onChild](child) { if (child[$namespaceId] === NamespaceIds.xhtml.id) { this[$content] = child; @@ -4413,6 +4449,13 @@ class Text extends ContentObject { return false; } + [$onText](str) { + if (this[$content] instanceof XFAObject) { + return; + } + super[$onText](str); + } + [$toHTML](availableSpace) { if (typeof this[$content] === "string") { // \u2028 is a line separator. @@ -4420,7 +4463,7 @@ class Text extends ContentObject { const html = { name: "span", attributes: { - class: "xfaRich", + class: ["xfaRich"], style: {}, }, value: this[$content], @@ -4521,7 +4564,7 @@ class TextEdit extends XFAObject { name: "textarea", attributes: { fieldId: this[$getParent]()[$getParent]()[$uid], - class: "xfaTextfield", + class: ["xfaTextfield"], style, }, }; @@ -4531,7 +4574,7 @@ class TextEdit extends XFAObject { attributes: { type: "text", fieldId: this[$getParent]()[$getParent]()[$uid], - class: "xfaTextfield", + class: ["xfaTextfield"], style, }, }; @@ -4540,7 +4583,7 @@ class TextEdit extends XFAObject { return HTMLResult.success({ name: "label", attributes: { - class: "xfaLabel", + class: ["xfaLabel"], }, children: [html], }); diff --git a/src/core/xfa/xhtml.js b/src/core/xfa/xhtml.js index efa1b8ecd..5b12bcdfe 100644 --- a/src/core/xfa/xhtml.js +++ b/src/core/xfa/xhtml.js @@ -80,9 +80,9 @@ const StyleMapping = new Map([ ], ["xfa-spacerun", ""], ["xfa-tab-stops", ""], - ["font-size", value => measureToString(1 * getMeasurement(value))], + ["font-size", value => measureToString(getMeasurement(value))], ["letter-spacing", value => measureToString(getMeasurement(value))], - ["line-height", value => measureToString(0.99 * getMeasurement(value))], + ["line-height", value => measureToString(getMeasurement(value))], ["margin", value => measureToString(getMeasurement(value))], ["margin-bottom", value => measureToString(getMeasurement(value))], ["margin-left", value => measureToString(getMeasurement(value))], @@ -216,7 +216,7 @@ class Body extends XhtmlObject { return HTMLResult.EMPTY; } html.name = "div"; - html.attributes.class = "xfaRich"; + html.attributes.class = ["xfaRich"]; return res; } } @@ -253,7 +253,7 @@ class Html extends XhtmlObject { return HTMLResult.success({ name: "div", attributes: { - class: "xfaRich", + class: ["xfaRich"], style: {}, }, value: this[$content] || "", @@ -262,7 +262,7 @@ class Html extends XhtmlObject { if (children.length === 1) { const child = children[0]; - if (child.attributes && child.attributes.class === "xfaRich") { + if (child.attributes && child.attributes.class.includes("xfaRich")) { return HTMLResult.success(child); } } @@ -270,7 +270,7 @@ class Html extends XhtmlObject { return HTMLResult.success({ name: "div", attributes: { - class: "xfaRich", + class: ["xfaRich"], style: {}, }, children, diff --git a/src/display/xfa_layer.js b/src/display/xfa_layer.js index 9de3dbf7d..e129ca049 100644 --- a/src/display/xfa_layer.js +++ b/src/display/xfa_layer.js @@ -20,7 +20,9 @@ class XfaLayer { const storedData = storage.getValue(fieldId, { value: null }); switch (element.name) { case "textarea": - html.textContent = storedData.value !== null ? storedData.value : ""; + if (storedData.value !== null) { + html.textContent = storedData.value; + } if (intent === "print") { break; } @@ -103,6 +105,8 @@ class XfaLayer { if (key !== "style") { if (key === "textContent") { html.textContent = value; + } else if (key === "class") { + html.setAttribute(key, value.join(" ")); } else { html.setAttribute(key, value); } diff --git a/test/unit/xfa_tohtml_spec.js b/test/unit/xfa_tohtml_spec.js index c116c4440..142c9615b 100644 --- a/test/unit/xfa_tohtml_spec.js +++ b/test/unit/xfa_tohtml_spec.js @@ -66,7 +66,7 @@ describe("XFAFactory", function () { expect(page1.children.length).toEqual(2); const container = page1.children[0]; - expect(container.attributes.class).toEqual("xfaContentarea"); + expect(container.attributes.class).toEqual(["xfaContentarea"]); expect(container.attributes.style).toEqual({ height: "789px", width: "456px", @@ -78,24 +78,29 @@ describe("XFAFactory", function () { const wrapper = page1.children[1]; const draw = wrapper.children[0]; - expect(wrapper.attributes.class).toEqual("xfaWrapper"); + expect(wrapper.attributes.class).toEqual(["xfaWrapper"]); expect(wrapper.attributes.style).toEqual({ + alignSelf: "start", + height: "22px", left: "2px", position: "absolute", top: "1px", + transform: "rotate(-90deg)", + transformOrigin: "top left", + width: "11px", }); - expect(draw.attributes.class).toEqual("xfaDraw xfaFont"); + expect(draw.attributes.class).toEqual([ + "xfaDraw", + "xfaFont", + "xfaWrapped", + ]); expect(draw.attributes.style).toEqual({ color: "#0c1722", fontFamily: "FooBar", fontSize: "6.93px", - height: "22px", - padding: "1px 4px 2px 3px", - transform: "rotate(-90deg)", - transformOrigin: "top left", + margin: "1px 4px 2px 3px", verticalAlign: "2px", - width: "11px", }); // draw element must be on each page. diff --git a/web/xfa_layer_builder.css b/web/xfa_layer_builder.css index ab18b9342..46a3cbd47 100644 --- a/web/xfa_layer_builder.css +++ b/web/xfa_layer_builder.css @@ -35,6 +35,11 @@ background: transparent; padding: 0; margin: 0; + pointer-events: auto; +} + +.xfaLayer div { + pointer-events: none; } .xfaLayer a { @@ -45,6 +50,10 @@ margin-left: 3em; } +.xfaLayer p { + margin-bottom: -1px; +} + .xfaFont { color: black; font-weight: normal; @@ -70,38 +79,79 @@ .xfaRich { z-index: 300; - line-height: 1.2; } .xfaSubform { z-index: 200; } +.xfaCaption { + overflow: hidden; + flex: 0 1 auto; +} + .xfaLabel { + height: 100%; + width: 100%; +} + +.xfaLeft { display: flex; flex-direction: row; align-items: center; - width: 100%; - height: 100%; } -.xfaCaption { - flex: 1 1 auto; +.xfaLeft > .xfaCaption { + max-height: 100%; } -.xfaBorderDiv { +.xfaRight { + display: flex; + flex-direction: row-reverse; + align-items: center; +} + +.xfaRight > .xfaCaption { + max-height: 100%; +} + +.xfaTop { + display: flex; + flex-direction: column; + align-items: start; +} + +.xfaTop > .xfaCaption { + max-width: 100%; +} + +.xfaBottom { + display: flex; + flex-direction: column-reverse; + align-items: start; +} + +.xfaBottom > .xfaCaption { + max-width: 100%; +} + +.xfaInline { + float: inline; +} + +.xfaBorder { background: transparent; position: absolute; pointer-events: none; } .xfaWrapper { - position: relative; display: flex; - align-items: center; - justify-content: center; - width: auto; - height: auto; + align-items: stretch; +} + +.xfaWrapped { + flex: 1 1 auto; } .xfaContentArea { @@ -122,7 +172,7 @@ .xfaSelect { width: 100%; height: 100%; - flex: 100 1 0; + flex: 1 1 0; border: none; resize: none; } @@ -160,10 +210,6 @@ height: auto; } -.xfaPosition { - display: block; -} - .xfaLrTb, .xfaRlTb, .xfaTb { @@ -178,18 +224,28 @@ flex: 1 1 auto; } -.xfaTb > div { - justify-content: left; +.xfaLr { + display: flex; + flex-direction: row; + align-items: stretch; } .xfaLr > div { - display: inline; - float: left; + flex: 1 1 auto; +} + +.xfaRl { + display: flex; + flex-direction: row-reverse; + align-items: stretch; } .xfaRl > div { - display: inline; - float: right; + flex: 1 1 auto; +} + +.xfaTb > div { + justify-content: left; } .xfaPosition { @@ -205,25 +261,20 @@ align-items: center; } -.xfaLrTb > div { - display: inline; - float: left; -} - -.xfaRlTb > div { - display: inline; - float: right; -} - .xfaTable { display: flex; flex-direction: column; + align-items: stretch; +} + +.xfaTable > div { + flex: 1 1 auto; } .xfaTable .xfaRow { display: flex; flex-direction: row; - flex: 1 1 auto; + align-items: stretch; } .xfaTable .xfaRow > div { @@ -233,6 +284,7 @@ .xfaTable .xfaRlRow { display: flex; flex-direction: row-reverse; + align-items: stretch; flex: 1; }