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;
This commit is contained in:
Calixte Denizet 2021-06-02 19:14:41 +02:00
parent e8fe0711ee
commit cfa727474e
7 changed files with 415 additions and 345 deletions

View File

@ -172,68 +172,22 @@ const converters = {
} }
} else { } else {
switch (node.hAlign) { switch (node.hAlign) {
case "right": case "left":
style.alignSelf = "start";
break;
case "center": case "center":
style.justifyContent = node.hAlign; style.alignSelf = "center";
break;
case "right":
style.alignSelf = "end";
break; break;
} }
} }
}, },
borderMarginPadding(node, style) { margin(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;
}
if (node.margin) { if (node.margin) {
Object.assign(style, node.margin[$toStyle]()); style.margin = node.margin[$toStyle]().margin;
style.padding = style.margin;
delete style.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; return style;
} }
function addExtraDivForBorder(html) { function createWrapper(node, html) {
const style = html.attributes.style; const { attributes } = html;
const data = style.borderData; const { style } = attributes;
const children = [];
const attributes = { const wrapper = {
class: "xfaWrapper", name: "div",
style: Object.create(null), 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) { 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") { if (style.position === "absolute") {
attributes.style.position = "absolute"; wrapper.attributes.style.position = "absolute";
} else { } else {
attributes.style.position = "relative"; wrapper.attributes.style.position = "relative";
} }
delete style.position; delete style.position;
if (style.justifyContent) { if (style.alignSelf) {
attributes.style.justifyContent = style.justifyContent; wrapper.attributes.style.alignSelf = style.alignSelf;
delete style.justifyContent; delete style.alignSelf;
} }
if (data) { return wrapper;
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,
};
} }
function fixTextIndent(styles) { function fixTextIndent(styles) {
@ -535,11 +496,12 @@ function fixTextIndent(styles) {
return; return;
} }
// If indent is negative then it's a hanging indent.
const align = styles.textAlign || "left"; const align = styles.textAlign || "left";
if (align === "left" || align === "right") { if (align === "left" || align === "right") {
const name = "margin" + (align === "left" ? "Left" : "Right"); const name = "padding" + (align === "left" ? "Left" : "Right");
const margin = getMeasurement(styles[name], "0px"); const padding = getMeasurement(styles[name], "0px");
styles[name] = `${margin - indent}pt`; styles[name] = `${padding - indent}px`;
} }
} }
@ -573,8 +535,8 @@ function getFonts(family) {
} }
export { export {
addExtraDivForBorder,
computeBbox, computeBbox,
createWrapper,
fixDimensions, fixDimensions,
fixTextIndent, fixTextIndent,
getFonts, getFonts,

View File

@ -46,6 +46,9 @@ import { measureToString } from "./html_utils.js";
*/ */
function flushHTML(node) { function flushHTML(node) {
if (!node[$extra]) {
return null;
}
const attributes = node[$extra].attributes; const attributes = node[$extra].attributes;
const html = { const html = {
name: "div", name: "div",
@ -88,7 +91,7 @@ function addHTML(node, html, bbox) {
extra.line = { extra.line = {
name: "div", name: "div",
attributes: { attributes: {
class: node.layout === "lr-tb" ? "xfaLr" : "xfaRl", class: [node.layout === "lr-tb" ? "xfaLr" : "xfaRl"],
}, },
children: [], children: [],
}; };
@ -120,12 +123,7 @@ function addHTML(node, html, bbox) {
extra.height = Math.max(extra.height, h); extra.height = Math.max(extra.height, h);
const height = measureToString(extra.height); const height = measureToString(extra.height);
for (const child of extra.children) { for (const child of extra.children) {
if (child.attributes.class === "xfaWrapper") { child.attributes.style.height = height;
child.children[child.children.length - 1].attributes.style.height =
height;
} else {
child.attributes.style.height = height;
}
} }
break; break;
} }
@ -148,6 +146,12 @@ function addHTML(node, html, bbox) {
function getAvailableSpace(node) { function getAvailableSpace(node) {
const availableSpace = node[$extra].availableSpace; 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) { switch (node.layout) {
case "lr-tb": case "lr-tb":
@ -155,18 +159,18 @@ function getAvailableSpace(node) {
switch (node[$extra].attempt) { switch (node[$extra].attempt) {
case 0: case 0:
return { return {
width: availableSpace.width - node[$extra].currentWidth, width: availableSpace.width - marginW - node[$extra].currentWidth,
height: availableSpace.height - node[$extra].prevHeight, height: availableSpace.height - marginH - node[$extra].prevHeight,
}; };
case 1: case 1:
return { return {
width: availableSpace.width, width: availableSpace.width - marginW,
height: availableSpace.height - node[$extra].height, height: availableSpace.height - marginH - node[$extra].height,
}; };
default: default:
return { return {
width: Infinity, width: Infinity,
height: availableSpace.height - node[$extra].prevHeight, height: availableSpace.height - marginH - node[$extra].prevHeight,
}; };
} }
case "rl-row": case "rl-row":
@ -174,12 +178,12 @@ function getAvailableSpace(node) {
const width = node[$extra].columnWidths const width = node[$extra].columnWidths
.slice(node[$extra].currentColumn) .slice(node[$extra].currentColumn)
.reduce((a, x) => a + x); .reduce((a, x) => a + x);
return { width, height: availableSpace.height }; return { width, height: availableSpace.height - marginH };
case "table": case "table":
case "tb": case "tb":
return { return {
width: availableSpace.width, width: availableSpace.width - marginW,
height: availableSpace.height - node[$extra].height, height: availableSpace.height - marginH - node[$extra].height,
}; };
case "position": case "position":
default: default:

View File

@ -14,6 +14,7 @@
*/ */
import { import {
$acceptWhitespace,
$addHTML, $addHTML,
$appendChild, $appendChild,
$break, $break,
@ -34,6 +35,7 @@ import {
$namespaceId, $namespaceId,
$nodeName, $nodeName,
$onChild, $onChild,
$onText,
$removeChild, $removeChild,
$searchNode, $searchNode,
$setSetAttributes, $setSetAttributes,
@ -50,9 +52,10 @@ import {
XFAObjectArray, XFAObjectArray,
} from "./xfa_object.js"; } from "./xfa_object.js";
import { $buildXFAObject, NamespaceIds } from "./namespaces.js"; import { $buildXFAObject, NamespaceIds } from "./namespaces.js";
import { addHTML, flushHTML, getAvailableSpace } from "./layout.js";
import { import {
addExtraDivForBorder,
computeBbox, computeBbox,
createWrapper,
fixDimensions, fixDimensions,
fixTextIndent, fixTextIndent,
getFonts, getFonts,
@ -61,7 +64,6 @@ import {
measureToString, measureToString,
toStyle, toStyle,
} from "./html_utils.js"; } from "./html_utils.js";
import { addHTML, flushHTML, getAvailableSpace } from "./layout.js";
import { import {
getBBox, getBBox,
getColor, getColor,
@ -103,26 +105,90 @@ function getRoot(node) {
return parent; 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 NOTHING = 0;
const NOSPACE = 1; const NOSPACE = 1;
const VALID = 2; const VALID = 2;
function checkDimensions(node, space) { 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; const area = getRoot(node)[$extra].currentContentArea;
if (node.w + node.x > area.w) { if (x + w > 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) {
return NOTHING; return NOTHING;
} }
return NOSPACE; 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; return VALID;
} }
@ -211,7 +277,7 @@ class Area extends XFAObject {
const attributes = { const attributes = {
style, style,
id: this[$uid], id: this[$uid],
class: "xfaArea", class: ["xfaArea"],
}; };
if (this.name) { if (this.name) {
@ -503,7 +569,7 @@ class Border extends XFAObject {
this.margin = null; this.margin = null;
} }
[$toStyle](widths, margins) { [$toStyle]() {
// TODO: incomplete. // TODO: incomplete.
const edges = this.edge.children.slice(); const edges = this.edge.children.slice();
if (edges.length < 4) { 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 edgeStyles = edges.map(node => {
const style = node[$toStyle](); const style = node[$toStyle]();
style.color = style.color || "#000000"; style.color = style.color || "#000000";
return style; 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 widths = edges.map(edge => edge.thickness);
const parent = this[$getParent](); const insets = [0, 0, 0, 0];
const grandParent = parent ? parent[$getParent]() : null; if (this.margin) {
if (grandParent instanceof Ui) { insets[0] = this.margin.topInset;
isForUi = true; 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) { if (this.fill) {
Object.assign(style, this.fill[$toStyle]()); Object.assign(style, this.fill[$toStyle]());
} }
let hasRadius = false;
if (this.corner.children.some(node => node.radius !== 0)) { if (this.corner.children.some(node => node.radius !== 0)) {
const cornerStyles = this.corner.children.map(node => node[$toStyle]()); const cornerStyles = this.corner.children.map(node => node[$toStyle]());
if (cornerStyles.length === 2 || cornerStyles.length === 3) { if (cornerStyles.length === 2 || cornerStyles.length === 3) {
@ -558,62 +615,24 @@ class Border extends XFAObject {
} }
style.borderRadius = cornerStyles.map(s => s.radius).join(" "); style.borderRadius = cornerStyles.map(s => s.radius).join(" ");
hasRadius = true;
} }
const firstEdge = edgeStyles[0]; switch (this.presence) {
if ( case "invisible":
!hasRadius && case "hidden":
(this.edge.children.length <= 1 || style.borderStyle = "";
(edgeStyles.every( break;
x => case "inactive":
x.style === firstEdge.style && style.borderStyle = "none";
x.width === firstEdge.width && break;
x.color === firstEdge.color default:
) && style.borderStyle = edgeStyles.map(s => s.style).join(" ");
margins.every(x => x === margins[0]))) break;
) {
// 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(" ");
} }
style.borderWidth = edgeStyles.map(s => s.width).join(" ");
style.borderColor = edgeStyles.map(s => s.color).join(" ");
return style; return style;
} }
} }
@ -729,7 +748,8 @@ class Button extends XFAObject {
return HTMLResult.success({ return HTMLResult.success({
name: "button", name: "button",
attributes: { attributes: {
class: "xfaButton", id: this[$uid],
class: ["xfaButton"],
style: {}, style: {},
}, },
children: [], children: [],
@ -830,7 +850,7 @@ class Caption extends XFAObject {
name: "div", name: "div",
attributes: { attributes: {
style, style,
class: "xfaCaption", class: ["xfaCaption"],
}, },
children, children,
}); });
@ -917,7 +937,7 @@ class CheckButton extends XFAObject {
const input = { const input = {
name: "input", name: "input",
attributes: { attributes: {
class: className, class: [className],
style, style,
fieldId, fieldId,
type, type,
@ -935,7 +955,7 @@ class CheckButton extends XFAObject {
return HTMLResult.success({ return HTMLResult.success({
name: "label", name: "label",
attributes: { attributes: {
class: "xfaLabel", class: ["xfaLabel"],
}, },
children: [input], children: [input],
}); });
@ -990,7 +1010,7 @@ class ChoiceList extends XFAObject {
} }
const selectAttributes = { const selectAttributes = {
class: "xfaSelect", class: ["xfaSelect"],
fieldId: this[$getParent]()[$getParent]()[$uid], fieldId: this[$getParent]()[$getParent]()[$uid],
style, style,
}; };
@ -1002,7 +1022,7 @@ class ChoiceList extends XFAObject {
return HTMLResult.success({ return HTMLResult.success({
name: "label", name: "label",
attributes: { attributes: {
class: "xfaLabel", class: ["xfaLabel"],
}, },
children: [ children: [
{ {
@ -1101,7 +1121,7 @@ class ContentArea extends XFAObject {
children: [], children: [],
attributes: { attributes: {
style, style,
class: "xfaContentarea", class: ["xfaContentarea"],
id: this[$uid], id: this[$uid],
}, },
}); });
@ -1219,7 +1239,7 @@ class DateTimeEdit extends XFAObject {
attributes: { attributes: {
type: "text", type: "text",
fieldId: this[$getParent]()[$getParent]()[$uid], fieldId: this[$getParent]()[$getParent]()[$uid],
class: "xfaTextfield", class: ["xfaTextfield"],
style, style,
}, },
}; };
@ -1227,7 +1247,7 @@ class DateTimeEdit extends XFAObject {
return HTMLResult.success({ return HTMLResult.success({
name: "label", name: "label",
attributes: { attributes: {
class: "xfaLabel", class: ["xfaLabel"],
}, },
children: [html], children: [html],
}); });
@ -1432,7 +1452,8 @@ class Draw extends XFAObject {
"presence", "presence",
"rotate", "rotate",
"anchorType", "anchorType",
"borderMarginPadding" "border",
"margin"
); );
const classNames = ["xfaDraw"]; const classNames = ["xfaDraw"];
@ -1443,7 +1464,7 @@ class Draw extends XFAObject {
const attributes = { const attributes = {
style, style,
id: this[$uid], id: this[$uid],
class: classNames.join(" "), class: classNames,
}; };
if (this.name) { if (this.name) {
@ -1456,16 +1477,15 @@ class Draw extends XFAObject {
children: [], children: [],
}; };
const extra = addExtraDivForBorder(html);
const bbox = computeBbox(this, html, availableSpace); const bbox = computeBbox(this, html, availableSpace);
const value = this.value ? this.value[$toHTML](availableSpace).html : null; const value = this.value ? this.value[$toHTML](availableSpace).html : null;
if (value === null) { if (value === null) {
return HTMLResult.success(extra, bbox); return HTMLResult.success(createWrapper(this, html), bbox);
} }
html.children.push(value); html.children.push(value);
if (value.attributes.class === "xfaRich") { if (value.attributes.class.includes("xfaRich")) {
if (this.h === "") { if (this.h === "") {
style.height = "auto"; 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", "lowered",
"raised", "raised",
]); ]);
// Cheat the thickness to have something nice at the end this.thickness = getMeasurement(attributes.thickness, "0.5pt");
this.thickness = Math.max(
1,
Math.round(getMeasurement(attributes.thickness, "0.5pt"))
);
this.use = attributes.use || ""; this.use = attributes.use || "";
this.usehref = attributes.usehref || ""; this.usehref = attributes.usehref || "";
this.color = null; this.color = null;
@ -1544,7 +1560,7 @@ class Edge extends XFAObject {
const style = toStyle(this, "visibility"); const style = toStyle(this, "visibility");
Object.assign(style, { Object.assign(style, {
linecap: this.cap, linecap: this.cap,
width: measureToString(Math.max(1, Math.round(this.thickness))), width: measureToString(this.thickness),
color: this.color ? this.color[$toStyle]() : "#000000", color: this.color ? this.color[$toStyle]() : "#000000",
style: "", style: "",
}); });
@ -1983,7 +1999,8 @@ class ExclGroup extends XFAObject {
"dimensions", "dimensions",
"position", "position",
"presence", "presence",
"borderMarginPadding", "border",
"margin",
"hAlign" "hAlign"
); );
const classNames = ["xfaExclgroup"]; const classNames = ["xfaExclgroup"];
@ -1993,7 +2010,7 @@ class ExclGroup extends XFAObject {
} }
attributes.style = style; attributes.style = style;
attributes.class = classNames.join(" "); attributes.class = classNames;
if (this.name) { if (this.name) {
attributes.xfaName = this.name; attributes.xfaName = this.name;
@ -2025,6 +2042,9 @@ class ExclGroup extends XFAObject {
} }
if (failure) { if (failure) {
if (this.layout === "position") {
delete this[$extra];
}
return HTMLResult.FAILURE; return HTMLResult.FAILURE;
} }
@ -2042,13 +2062,12 @@ class ExclGroup extends XFAObject {
style.height = measureToString(this[$extra].height + marginV); style.height = measureToString(this[$extra].height + marginV);
} }
let html = { const html = {
name: "div", name: "div",
attributes, attributes,
children, children,
}; };
html = addExtraDivForBorder(html);
let bbox; let bbox;
if (this.w !== "" && this.h !== "") { if (this.w !== "" && this.h !== "") {
bbox = [this.x, this.y, this.w, this.h]; bbox = [this.x, this.y, this.w, this.h];
@ -2061,7 +2080,7 @@ class ExclGroup extends XFAObject {
delete this[$extra]; delete this[$extra];
return HTMLResult.success(html, bbox); return HTMLResult.success(createWrapper(this, html), bbox);
} }
} }
@ -2227,7 +2246,7 @@ class Field extends XFAObject {
"rotate", "rotate",
"anchorType", "anchorType",
"presence", "presence",
"borderMarginPadding", "margin",
"hAlign" "hAlign"
); );
@ -2240,7 +2259,7 @@ class Field extends XFAObject {
const attributes = { const attributes = {
style, style,
id: this[$uid], id: this[$uid],
class: classNames.join(" "), class: classNames,
}; };
if (this.name) { if (this.name) {
@ -2248,29 +2267,37 @@ class Field extends XFAObject {
} }
const children = []; const children = [];
let html = { const html = {
name: "div", name: "div",
attributes, attributes,
children, children,
}; };
const bbox = computeBbox(this, html, availableSpace); const borderStyle = this.border ? this.border[$toStyle]() : null;
html = addExtraDivForBorder(html);
const bbox = computeBbox(this, html, availableSpace);
const ui = this.ui ? this.ui[$toHTML]().html : null; const ui = this.ui ? this.ui[$toHTML]().html : null;
if (!ui) { if (!ui) {
return HTMLResult.success(html, bbox); Object.assign(style, borderStyle);
return HTMLResult.success(createWrapper(this, html), bbox);
} }
if (!ui.attributes.style) { if (!ui.attributes.style) {
ui.attributes.style = Object.create(null); ui.attributes.style = Object.create(null);
} }
if (this.ui.button) {
Object.assign(ui.attributes.style, borderStyle);
} else {
Object.assign(style, borderStyle);
}
children.push(ui); children.push(ui);
if (this.value) { if (this.value) {
if (this.ui.imageEdit) { if (this.ui.imageEdit) {
ui.children.push(this.value[$toHTML]().html); ui.children.push(this.value[$toHTML]().html);
} else if (ui.name !== "button") { } else if (!this.ui.button) {
const value = this.value[$toHTML]().html; const value = this.value[$toHTML]().html;
if (value) { if (value) {
if (ui.children[0].name === "textarea") { if (ui.children[0].name === "textarea") {
@ -2284,43 +2311,42 @@ class Field extends XFAObject {
const caption = this.caption ? this.caption[$toHTML]().html : null; const caption = this.caption ? this.caption[$toHTML]().html : null;
if (!caption) { if (!caption) {
return HTMLResult.success(html, bbox); return HTMLResult.success(createWrapper(this, html), bbox);
} }
if (ui.name === "button") { if (this.ui.button) {
ui.attributes.style.background = style.background;
delete style.background;
if (caption.name === "div") { if (caption.name === "div") {
caption.name = "span"; caption.name = "span";
} }
ui.children.push(caption); ui.children.push(caption);
return HTMLResult.success(html, bbox); return HTMLResult.success(html, bbox);
} }
if (!ui.attributes.class) {
ui.attributes.class = [];
}
ui.children.splice(0, 0, caption); ui.children.splice(0, 0, caption);
switch (this.caption.placement) { switch (this.caption.placement) {
case "left": case "left":
ui.attributes.style.flexDirection = "row"; ui.attributes.class.push("xfaLeft");
break; break;
case "right": case "right":
ui.attributes.style.flexDirection = "row-reverse"; ui.attributes.class.push("xfaRight");
break; break;
case "top": case "top":
ui.attributes.style.alignItems = "start"; ui.attributes.class.push("xfaTop");
ui.attributes.style.flexDirection = "column";
break; break;
case "bottom": case "bottom":
ui.attributes.style.alignItems = "start"; ui.attributes.class.push("xfaBottom");
ui.attributes.style.flexDirection = "column-reverse";
break; break;
case "inline": case "inline":
delete ui.attributes.class; // TODO;
caption.attributes.style.float = "left"; ui.attributes.class.push("xfaInline");
break; break;
} }
return HTMLResult.success(html, bbox); return HTMLResult.success(createWrapper(this, html), bbox);
} }
} }
@ -2660,7 +2686,7 @@ class Image extends StringObject {
return HTMLResult.success({ return HTMLResult.success({
name: "img", name: "img",
attributes: { attributes: {
class: "xfaImage", class: ["xfaImage"],
style: {}, style: {},
src: URL.createObjectURL(blob), src: URL.createObjectURL(blob),
}, },
@ -2987,7 +3013,7 @@ class NumericEdit extends XFAObject {
attributes: { attributes: {
type: "text", type: "text",
fieldId: this[$getParent]()[$getParent]()[$uid], fieldId: this[$getParent]()[$getParent]()[$uid],
class: "xfaTextfield", class: ["xfaTextfield"],
style, style,
}, },
}; };
@ -2995,7 +3021,7 @@ class NumericEdit extends XFAObject {
return HTMLResult.success({ return HTMLResult.success({
name: "label", name: "label",
attributes: { attributes: {
class: "xfaLabel", class: ["xfaLabel"],
}, },
children: [html], children: [html],
}); });
@ -3955,7 +3981,8 @@ class Subform extends XFAObject {
"dimensions", "dimensions",
"position", "position",
"presence", "presence",
"borderMarginPadding", "border",
"margin",
"hAlign" "hAlign"
); );
const classNames = ["xfaSubform"]; const classNames = ["xfaSubform"];
@ -3965,7 +3992,7 @@ class Subform extends XFAObject {
} }
attributes.style = style; attributes.style = style;
attributes.class = classNames.join(" "); attributes.class = classNames;
if (this.name) { if (this.name) {
attributes.xfaName = this.name; attributes.xfaName = this.name;
@ -3997,6 +4024,9 @@ class Subform extends XFAObject {
} }
if (failure) { if (failure) {
if (this.layout === "position") {
delete this[$extra];
}
return HTMLResult.FAILURE; return HTMLResult.FAILURE;
} }
@ -4014,13 +4044,12 @@ class Subform extends XFAObject {
style.height = measureToString(this[$extra].height + marginV); style.height = measureToString(this[$extra].height + marginV);
} }
let html = { const html = {
name: "div", name: "div",
attributes, attributes,
children, children,
}; };
html = addExtraDivForBorder(html);
let bbox; let bbox;
if (this.w !== "" && this.h !== "") { if (this.w !== "" && this.h !== "") {
bbox = [this.x, this.y, 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) { if (this.breakAfter.children.length >= 1) {
const breakAfter = this.breakAfter.children[0]; const breakAfter = this.breakAfter.children[0];
getRoot(this)[$break](breakAfter); getRoot(this)[$break](breakAfter);
this[$extra].afterBreakAfter = HTMLResult.success(html, bbox); this[$extra].afterBreakAfter = HTMLResult.success(
createWrapper(this, html),
bbox
);
return HTMLResult.FAILURE; return HTMLResult.FAILURE;
} }
delete this[$extra]; 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 contentAreas = pageArea.contentArea.children;
const htmlContentAreas = page.children.filter( const htmlContentAreas = page.children.filter(node =>
node => node.attributes.class === "xfaContentarea" node.attributes.class.includes("xfaContentarea")
); );
for (let i = 0, ii = contentAreas.length; i < ii; i++) { for (let i = 0, ii = contentAreas.length; i < ii; i++) {
const contentArea = (this[$extra].currentContentArea = contentAreas[i]); const contentArea = (this[$extra].currentContentArea = contentAreas[i]);
@ -4404,6 +4436,10 @@ class Text extends ContentObject {
this.usehref = attributes.usehref || ""; this.usehref = attributes.usehref || "";
} }
[$acceptWhitespace]() {
return true;
}
[$onChild](child) { [$onChild](child) {
if (child[$namespaceId] === NamespaceIds.xhtml.id) { if (child[$namespaceId] === NamespaceIds.xhtml.id) {
this[$content] = child; this[$content] = child;
@ -4413,6 +4449,13 @@ class Text extends ContentObject {
return false; return false;
} }
[$onText](str) {
if (this[$content] instanceof XFAObject) {
return;
}
super[$onText](str);
}
[$toHTML](availableSpace) { [$toHTML](availableSpace) {
if (typeof this[$content] === "string") { if (typeof this[$content] === "string") {
// \u2028 is a line separator. // \u2028 is a line separator.
@ -4420,7 +4463,7 @@ class Text extends ContentObject {
const html = { const html = {
name: "span", name: "span",
attributes: { attributes: {
class: "xfaRich", class: ["xfaRich"],
style: {}, style: {},
}, },
value: this[$content], value: this[$content],
@ -4521,7 +4564,7 @@ class TextEdit extends XFAObject {
name: "textarea", name: "textarea",
attributes: { attributes: {
fieldId: this[$getParent]()[$getParent]()[$uid], fieldId: this[$getParent]()[$getParent]()[$uid],
class: "xfaTextfield", class: ["xfaTextfield"],
style, style,
}, },
}; };
@ -4531,7 +4574,7 @@ class TextEdit extends XFAObject {
attributes: { attributes: {
type: "text", type: "text",
fieldId: this[$getParent]()[$getParent]()[$uid], fieldId: this[$getParent]()[$getParent]()[$uid],
class: "xfaTextfield", class: ["xfaTextfield"],
style, style,
}, },
}; };
@ -4540,7 +4583,7 @@ class TextEdit extends XFAObject {
return HTMLResult.success({ return HTMLResult.success({
name: "label", name: "label",
attributes: { attributes: {
class: "xfaLabel", class: ["xfaLabel"],
}, },
children: [html], children: [html],
}); });

View File

@ -80,9 +80,9 @@ const StyleMapping = new Map([
], ],
["xfa-spacerun", ""], ["xfa-spacerun", ""],
["xfa-tab-stops", ""], ["xfa-tab-stops", ""],
["font-size", value => measureToString(1 * getMeasurement(value))], ["font-size", value => measureToString(getMeasurement(value))],
["letter-spacing", 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", value => measureToString(getMeasurement(value))],
["margin-bottom", value => measureToString(getMeasurement(value))], ["margin-bottom", value => measureToString(getMeasurement(value))],
["margin-left", value => measureToString(getMeasurement(value))], ["margin-left", value => measureToString(getMeasurement(value))],
@ -216,7 +216,7 @@ class Body extends XhtmlObject {
return HTMLResult.EMPTY; return HTMLResult.EMPTY;
} }
html.name = "div"; html.name = "div";
html.attributes.class = "xfaRich"; html.attributes.class = ["xfaRich"];
return res; return res;
} }
} }
@ -253,7 +253,7 @@ class Html extends XhtmlObject {
return HTMLResult.success({ return HTMLResult.success({
name: "div", name: "div",
attributes: { attributes: {
class: "xfaRich", class: ["xfaRich"],
style: {}, style: {},
}, },
value: this[$content] || "", value: this[$content] || "",
@ -262,7 +262,7 @@ class Html extends XhtmlObject {
if (children.length === 1) { if (children.length === 1) {
const child = children[0]; const child = children[0];
if (child.attributes && child.attributes.class === "xfaRich") { if (child.attributes && child.attributes.class.includes("xfaRich")) {
return HTMLResult.success(child); return HTMLResult.success(child);
} }
} }
@ -270,7 +270,7 @@ class Html extends XhtmlObject {
return HTMLResult.success({ return HTMLResult.success({
name: "div", name: "div",
attributes: { attributes: {
class: "xfaRich", class: ["xfaRich"],
style: {}, style: {},
}, },
children, children,

View File

@ -20,7 +20,9 @@ class XfaLayer {
const storedData = storage.getValue(fieldId, { value: null }); const storedData = storage.getValue(fieldId, { value: null });
switch (element.name) { switch (element.name) {
case "textarea": case "textarea":
html.textContent = storedData.value !== null ? storedData.value : ""; if (storedData.value !== null) {
html.textContent = storedData.value;
}
if (intent === "print") { if (intent === "print") {
break; break;
} }
@ -103,6 +105,8 @@ class XfaLayer {
if (key !== "style") { if (key !== "style") {
if (key === "textContent") { if (key === "textContent") {
html.textContent = value; html.textContent = value;
} else if (key === "class") {
html.setAttribute(key, value.join(" "));
} else { } else {
html.setAttribute(key, value); html.setAttribute(key, value);
} }

View File

@ -66,7 +66,7 @@ describe("XFAFactory", function () {
expect(page1.children.length).toEqual(2); expect(page1.children.length).toEqual(2);
const container = page1.children[0]; const container = page1.children[0];
expect(container.attributes.class).toEqual("xfaContentarea"); expect(container.attributes.class).toEqual(["xfaContentarea"]);
expect(container.attributes.style).toEqual({ expect(container.attributes.style).toEqual({
height: "789px", height: "789px",
width: "456px", width: "456px",
@ -78,24 +78,29 @@ describe("XFAFactory", function () {
const wrapper = page1.children[1]; const wrapper = page1.children[1];
const draw = wrapper.children[0]; const draw = wrapper.children[0];
expect(wrapper.attributes.class).toEqual("xfaWrapper"); expect(wrapper.attributes.class).toEqual(["xfaWrapper"]);
expect(wrapper.attributes.style).toEqual({ expect(wrapper.attributes.style).toEqual({
alignSelf: "start",
height: "22px",
left: "2px", left: "2px",
position: "absolute", position: "absolute",
top: "1px", 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({ expect(draw.attributes.style).toEqual({
color: "#0c1722", color: "#0c1722",
fontFamily: "FooBar", fontFamily: "FooBar",
fontSize: "6.93px", fontSize: "6.93px",
height: "22px", margin: "1px 4px 2px 3px",
padding: "1px 4px 2px 3px",
transform: "rotate(-90deg)",
transformOrigin: "top left",
verticalAlign: "2px", verticalAlign: "2px",
width: "11px",
}); });
// draw element must be on each page. // draw element must be on each page.

View File

@ -35,6 +35,11 @@
background: transparent; background: transparent;
padding: 0; padding: 0;
margin: 0; margin: 0;
pointer-events: auto;
}
.xfaLayer div {
pointer-events: none;
} }
.xfaLayer a { .xfaLayer a {
@ -45,6 +50,10 @@
margin-left: 3em; margin-left: 3em;
} }
.xfaLayer p {
margin-bottom: -1px;
}
.xfaFont { .xfaFont {
color: black; color: black;
font-weight: normal; font-weight: normal;
@ -70,38 +79,79 @@
.xfaRich { .xfaRich {
z-index: 300; z-index: 300;
line-height: 1.2;
} }
.xfaSubform { .xfaSubform {
z-index: 200; z-index: 200;
} }
.xfaCaption {
overflow: hidden;
flex: 0 1 auto;
}
.xfaLabel { .xfaLabel {
height: 100%;
width: 100%;
}
.xfaLeft {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
width: 100%;
height: 100%;
} }
.xfaCaption { .xfaLeft > .xfaCaption {
flex: 1 1 auto; 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; background: transparent;
position: absolute; position: absolute;
pointer-events: none; pointer-events: none;
} }
.xfaWrapper { .xfaWrapper {
position: relative;
display: flex; display: flex;
align-items: center; align-items: stretch;
justify-content: center; }
width: auto;
height: auto; .xfaWrapped {
flex: 1 1 auto;
} }
.xfaContentArea { .xfaContentArea {
@ -122,7 +172,7 @@
.xfaSelect { .xfaSelect {
width: 100%; width: 100%;
height: 100%; height: 100%;
flex: 100 1 0; flex: 1 1 0;
border: none; border: none;
resize: none; resize: none;
} }
@ -160,10 +210,6 @@
height: auto; height: auto;
} }
.xfaPosition {
display: block;
}
.xfaLrTb, .xfaLrTb,
.xfaRlTb, .xfaRlTb,
.xfaTb { .xfaTb {
@ -178,18 +224,28 @@
flex: 1 1 auto; flex: 1 1 auto;
} }
.xfaTb > div { .xfaLr {
justify-content: left; display: flex;
flex-direction: row;
align-items: stretch;
} }
.xfaLr > div { .xfaLr > div {
display: inline; flex: 1 1 auto;
float: left; }
.xfaRl {
display: flex;
flex-direction: row-reverse;
align-items: stretch;
} }
.xfaRl > div { .xfaRl > div {
display: inline; flex: 1 1 auto;
float: right; }
.xfaTb > div {
justify-content: left;
} }
.xfaPosition { .xfaPosition {
@ -205,25 +261,20 @@
align-items: center; align-items: center;
} }
.xfaLrTb > div {
display: inline;
float: left;
}
.xfaRlTb > div {
display: inline;
float: right;
}
.xfaTable { .xfaTable {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch;
}
.xfaTable > div {
flex: 1 1 auto;
} }
.xfaTable .xfaRow { .xfaTable .xfaRow {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex: 1 1 auto; align-items: stretch;
} }
.xfaTable .xfaRow > div { .xfaTable .xfaRow > div {
@ -233,6 +284,7 @@
.xfaTable .xfaRlRow { .xfaTable .xfaRlRow {
display: flex; display: flex;
flex-direction: row-reverse; flex-direction: row-reverse;
align-items: stretch;
flex: 1; flex: 1;
} }