diff --git a/src/core/xfa/html_utils.js b/src/core/xfa/html_utils.js index 0753c6244..6e34f0cc5 100644 --- a/src/core/xfa/html_utils.js +++ b/src/core/xfa/html_utils.js @@ -13,10 +13,13 @@ * limitations under the License. */ -import { $toStyle, XFAObject } from "./xfa_object.js"; +import { $getParent, $toStyle, XFAObject } from "./xfa_object.js"; import { warn } from "../../shared/util.js"; function measureToString(m) { + if (typeof m === "string") { + return "0px"; + } return Number.isInteger(m) ? `${m}px` : `${m.toFixed(2)}px`; } @@ -56,31 +59,34 @@ const converters = { if (node.w) { style.width = measureToString(node.w); } else { - if (node.maxW && node.maxW.value > 0) { + style.width = "auto"; + if (node.maxW > 0) { style.maxWidth = measureToString(node.maxW); } - if (node.minW && node.minW.value > 0) { - style.minWidth = measureToString(node.minW); - } + style.minWidth = measureToString(node.minW); } if (node.h) { style.height = measureToString(node.h); } else { - if (node.maxH && node.maxH.value > 0) { + style.height = "auto"; + if (node.maxH > 0) { style.maxHeight = measureToString(node.maxH); } - if (node.minH && node.minH.value > 0) { - style.minHeight = measureToString(node.minH); - } + style.minHeight = measureToString(node.minH); } }, position(node, style) { - if (node.x !== "" || node.y !== "") { - style.position = "absolute"; - style.left = measureToString(node.x); - style.top = measureToString(node.y); + const parent = node[$getParent](); + if (parent && parent.layout && parent.layout !== "position") { + // IRL, we've some x/y in tb layout. + // Specs say x/y is only used in positioned layout. + return; } + + style.position = "absolute"; + style.left = measureToString(node.x); + style.top = measureToString(node.y); }, rotate(node, style) { if (node.rotate) { @@ -91,8 +97,40 @@ const converters = { style.transformOrigin = "top left"; } }, + presence(node, style) { + switch (node.presence) { + case "invisible": + style.visibility = "hidden"; + break; + case "hidden": + case "inactive": + style.display = "none"; + break; + } + }, }; +function layoutClass(node) { + switch (node.layout) { + case "position": + return "xfaPosition"; + case "lr-tb": + return "xfaLrTb"; + case "rl-row": + return "xfaRlRow"; + case "rl-tb": + return "xfaRlTb"; + case "row": + return "xfaRow"; + case "table": + return "xfaTable"; + case "tb": + return "xfaTb"; + default: + return "xfaPosition"; + } +} + function toStyle(node, ...names) { const style = Object.create(null); for (const name of names) { @@ -117,4 +155,4 @@ function toStyle(node, ...names) { return style; } -export { measureToString, toStyle }; +export { layoutClass, measureToString, toStyle }; diff --git a/src/core/xfa/template.js b/src/core/xfa/template.js index 98cbb6c63..65678819f 100644 --- a/src/core/xfa/template.js +++ b/src/core/xfa/template.js @@ -51,7 +51,7 @@ import { getRelevant, getStringOption, } from "./utils.js"; -import { measureToString, toStyle } from "./html_utils.js"; +import { layoutClass, measureToString, toStyle } from "./html_utils.js"; import { Util, warn } from "../../shared/util.js"; const TEMPLATE_NS_ID = NamespaceIds.template.id; @@ -115,8 +115,8 @@ class Area extends XFAObject { this.relevant = getRelevant(attributes.relevant); this.use = attributes.use || ""; this.usehref = attributes.usehref || ""; - this.x = getMeasurement(attributes.x); - this.y = getMeasurement(attributes.y); + this.x = getMeasurement(attributes.x, "0pt"); + this.y = getMeasurement(attributes.y, "0pt"); this.desc = null; this.extras = null; this.area = new XFAObjectArray(); @@ -346,6 +346,10 @@ class BooleanElement extends Option01 { this.use = attributes.use || ""; this.usehref = attributes.usehref || ""; } + + [$toHTML]() { + return this[$content] === 1; + } } class Border extends XFAObject { @@ -369,6 +373,83 @@ class Border extends XFAObject { this.fill = null; this.margin = null; } + + [$toStyle](widths, margins) { + // TODO: incomplete. + const edgeStyles = this.edge.children.map(node => node[$toStyle]()); + const cornerStyles = this.edge.children.map(node => node[$toStyle]()); + let style; + if (this.margin) { + style = this.margin[$toStyle](); + if (margins) { + margins.push( + this.margin.topInset, + this.margin.rightInset, + this.margin.bottomInset, + this.margin.leftInset + ); + } + } else { + style = Object.create(null); + if (margins) { + margins.push(0, 0, 0, 0); + } + } + + if (this.fill) { + Object.assign(style, this.fill[$toStyle]()); + } + + if (edgeStyles.length > 0) { + if (widths) { + this.edge.children.forEach(node => widths.push(node.thickness)); + if (widths.length < 4) { + const last = widths[widths.length - 1]; + for (let i = widths.length; i < 4; i++) { + widths.push(last); + } + } + } + + if (edgeStyles.length === 2 || edgeStyles.length === 3) { + const last = edgeStyles[edgeStyles.length - 1]; + for (let i = edgeStyles.length; i < 4; i++) { + edgeStyles.push(last); + } + } + + style.borderWidth = edgeStyles.map(s => s.width).join(" "); + style.borderColor = edgeStyles.map(s => s.color).join(" "); + style.borderStyle = edgeStyles.map(s => s.style).join(" "); + } else { + if (widths) { + widths.push(0, 0, 0, 0); + } + } + + if (cornerStyles.length > 0) { + if (cornerStyles.length === 2 || cornerStyles.length === 3) { + const last = cornerStyles[cornerStyles.length - 1]; + for (let i = cornerStyles.length; i < 4; i++) { + cornerStyles.push(last); + } + } + + style.borderRadius = cornerStyles.map(s => s.radius).join(" "); + } + + switch (this.presence) { + case "invisible": + case "hidden": + style.borderStyle = ""; + break; + case "inactive": + style.borderStyle = "none"; + break; + } + + return style; + } } class Break extends XFAObject { @@ -471,6 +552,17 @@ class Button extends XFAObject { this.usehref = attributes.usehref || ""; this.extras = null; } + + [$toHTML]() { + // TODO: highlight. + return { + name: "button", + attributes: { + class: "xfaButton", + style: {}, + }, + }; + } } class Calculate extends XFAObject { @@ -521,6 +613,47 @@ class Caption extends XFAObject { [$setValue](value) { _setValue(this, value); } + + [$toHTML]() { + // TODO: incomplete. + if (!this.value) { + return null; + } + + const value = this.value[$toHTML](); + if (!value) { + return null; + } + const children = []; + if (typeof value === "string") { + children.push({ + name: "#text", + value, + }); + } else { + children.push(value); + } + + const style = toStyle(this, "font", "margin", "para", "visibility"); + switch (this.placement) { + case "left": + case "right": + style.minWidth = measureToString(this.reserve); + break; + case "top": + case "bottom": + style.minHeight = measureToString(this.reserve); + break; + } + + return { + name: "div", + attributes: { + style, + }, + children, + }; + } } class Certificate extends StringObject { @@ -575,6 +708,83 @@ class CheckButton extends XFAObject { this.extras = null; this.margin = null; } + + [$toHTML]() { + // TODO: shape and mark == default. + const style = toStyle(this, "border", "margin"); + const size = measureToString(this.size); + + style.width = style.height = size; + + let mark, radius; + if (this.shape === "square") { + mark = "■"; + radius = "10%"; + } else { + mark = "●"; + radius = "50%"; + } + + if (!style.borderRadius) { + style.borderRadius = radius; + } + + if (this.mark !== "default") { + // TODO: To avoid some rendering issues we should use svg + // to draw marks. + switch (this.mark) { + case "check": + mark = "✓"; + break; + case "circle": + mark = "●"; + break; + case "cross": + mark = "✕"; + break; + case "diamond": + mark = "♦"; + break; + case "square": + mark = "■"; + break; + case "star": + mark = "★"; + break; + } + } + + if (size !== "10px") { + style.fontSize = size; + style.lineHeight = size; + style.width = size; + style.height = size; + } + + return { + name: "label", + attributes: { + class: "xfaLabel", + }, + children: [ + { + name: "input", + attributes: { + class: "xfaCheckbox", + type: "checkbox", + }, + }, + { + name: "span", + attributes: { + class: "xfaCheckboxMark", + mark, + style, + }, + }, + ], + }; + } } class ChoiceList extends XFAObject { @@ -599,6 +809,27 @@ class ChoiceList extends XFAObject { this.extras = null; this.margin = null; } + + [$toHTML]() { + // TODO: incomplete. + const style = toStyle(this, "border", "margin"); + return { + name: "label", + attributes: { + class: "xfaLabel", + }, + children: [ + { + name: "select", + attributes: { + class: "xfaSxelect", + multiple: this.open === "multiSelect", + style, + }, + }, + ], + }; + } } class Color extends XFAObject { @@ -662,8 +893,8 @@ class ContentArea extends XFAObject { this.use = attributes.use || ""; this.usehref = attributes.usehref || ""; this.w = getMeasurement(attributes.w); - this.x = getMeasurement(attributes.x); - this.y = getMeasurement(attributes.y); + this.x = getMeasurement(attributes.x, "0pt"); + this.y = getMeasurement(attributes.y, "0pt"); this.desc = null; this.extras = null; } @@ -727,12 +958,19 @@ class Corner extends XFAObject { this.extras = null; } - [$finalize]() { - this.color = this.color || getColor(null, [0, 0, 0]); + [$toStyle]() { + // In using CSS it's only possible to handle radius + // (at least with basic css). + // Is there a real use (interest ?) of all these properties ? + // Maybe it's possible to implement them using svg and border-image... + // TODO: implement all the missing properties. + const style = toStyle(this, "visibility"); + style.radius = measureToString(this.radius); + return style; } } -class Date extends ContentObject { +class DateElement extends ContentObject { constructor(attributes) { super(TEMPLATE_NS_ID, "date"); this.id = attributes.id || ""; @@ -744,6 +982,10 @@ class Date extends ContentObject { [$finalize]() { this[$content] = new Date(this[$content].trim()); } + + [$toHTML]() { + return this[$content].toString(); + } } class DateTime extends ContentObject { @@ -758,6 +1000,10 @@ class DateTime extends ContentObject { [$finalize]() { this[$content] = new Date(this[$content].trim()); } + + [$toHTML]() { + return this[$content].toString(); + } } class DateTimeEdit extends XFAObject { @@ -777,6 +1023,29 @@ class DateTimeEdit extends XFAObject { this.extras = null; this.margin = null; } + + [$toHTML]() { + // TODO: incomplete. + // When the picker is host we should use type=date for the input + // but we need to put the buttons outside the text-field. + const style = toStyle(this, "border", "font", "margin"); + const html = { + name: "input", + attributes: { + type: "text", + class: "xfaTextfield", + style, + }, + }; + + return { + name: "label", + attributes: { + class: "xfaLabel", + }, + children: [html], + }; + } } class Decimal extends ContentObject { @@ -802,6 +1071,10 @@ class Decimal extends ContentObject { const number = parseFloat(this[$content].trim()); this[$content] = isNaN(number) ? null : number; } + + [$toHTML]() { + return this[$content] !== null ? this[$content].toString() : ""; + } } class DefaultUi extends XFAObject { @@ -878,7 +1151,7 @@ class Draw extends XFAObject { defaultValue: 1, validate: x => x >= 1, }); - this.h = getMeasurement(attributes.h); + this.h = attributes.h ? getMeasurement(attributes.h) : ""; this.hAlign = getStringOption(attributes.hAlign, [ "left", "center", @@ -889,10 +1162,10 @@ class Draw extends XFAObject { ]); this.id = attributes.id || ""; this.locale = attributes.locale || ""; - this.maxH = getMeasurement(attributes.maxH); - this.maxW = getMeasurement(attributes.maxW); - this.minH = getMeasurement(attributes.minH); - this.minW = getMeasurement(attributes.minW); + this.maxH = getMeasurement(attributes.maxH, "0pt"); + this.maxW = getMeasurement(attributes.maxW, "0pt"); + this.minH = getMeasurement(attributes.minH, "0pt"); + this.minW = getMeasurement(attributes.minW, "0pt"); this.name = attributes.name || ""; this.presence = getStringOption(attributes.presence, [ "visible", @@ -908,9 +1181,9 @@ class Draw extends XFAObject { }); this.use = attributes.use || ""; this.usehref = attributes.usehref || ""; - this.w = getMeasurement(attributes.w); - this.x = getMeasurement(attributes.x); - this.y = getMeasurement(attributes.y); + this.w = attributes.w ? getMeasurement(attributes.w) : ""; + this.x = getMeasurement(attributes.x, "0pt"); + this.y = getMeasurement(attributes.y, "0pt"); this.assist = null; this.border = null; this.caption = null; @@ -940,6 +1213,7 @@ class Draw extends XFAObject { "font", "dimensions", "position", + "presence", "rotate", "anchorType" ); @@ -955,6 +1229,10 @@ class Draw extends XFAObject { class: clazz.join(" "), }; + if (this.name) { + attributes.xfaName = this.name; + } + return { name: "div", attributes, @@ -992,8 +1270,50 @@ class Edge extends XFAObject { this.extras = null; } - [$finalize]() { - this.color = this.color || getColor(null, [0, 0, 0]); + [$toStyle]() { + // TODO: dashDot & dashDotDot. + const style = toStyle(this, "visibility"); + Object.assign(style, { + linecap: this.cap, + width: measureToString(this.thickness), + color: this.color ? this.color[$toHTML]() : "#000000", + style: "", + }); + + if (this.presence !== "visible") { + style.style = "none"; + } else { + switch (this.stroke) { + case "solid": + style.style = "solid"; + break; + case "dashDot": + style.style = "dashed"; + break; + case "dashDotDot": + style.style = "dashed"; + break; + case "dashed": + style.style = "dashed"; + break; + case "dotted": + style.style = "dotted"; + break; + case "embossed": + style.style = "ridge"; + break; + case "etched": + style.style = "groove"; + break; + case "lowered": + style.style = "inset"; + break; + case "raised": + style.style = "outset"; + break; + } + } + return style; } } @@ -1228,7 +1548,7 @@ class ExclGroup extends XFAObject { defaultValue: 1, validate: x => x >= 1, }); - this.h = getMeasurement(attributes.h); + this.h = attributes.h ? getMeasurement(attributes.h) : ""; this.hAlign = getStringOption(attributes.hAlign, [ "left", "center", @@ -1247,10 +1567,10 @@ class ExclGroup extends XFAObject { "table", "tb", ]); - this.maxH = getMeasurement(attributes.maxH); - this.maxW = getMeasurement(attributes.maxW); - this.minH = getMeasurement(attributes.minH); - this.minW = getMeasurement(attributes.minW); + this.maxH = getMeasurement(attributes.maxH, "0pt"); + this.maxW = getMeasurement(attributes.maxW, "0pt"); + this.minH = getMeasurement(attributes.minH, "0pt"); + this.minW = getMeasurement(attributes.minW, "0pt"); this.name = attributes.name || ""; this.presence = getStringOption(attributes.presence, [ "visible", @@ -1261,9 +1581,9 @@ class ExclGroup extends XFAObject { this.relevant = getRelevant(attributes.relevant); this.use = attributes.use || ""; this.usehref = attributes.usehref || ""; - this.w = getMeasurement(attributes.w); - this.x = getMeasurement(attributes.x); - this.y = getMeasurement(attributes.y); + this.w = attributes.w ? getMeasurement(attributes.w) : ""; + this.x = getMeasurement(attributes.x, "0pt"); + this.y = getMeasurement(attributes.y, "0pt"); this.assist = null; this.bind = null; this.border = null; @@ -1399,7 +1719,7 @@ class Field extends XFAObject { defaultValue: 1, validate: x => x >= 1, }); - this.h = getMeasurement(attributes.h); + this.h = attributes.h ? getMeasurement(attributes.h) : ""; this.hAlign = getStringOption(attributes.hAlign, [ "left", "center", @@ -1410,10 +1730,10 @@ class Field extends XFAObject { ]); this.id = attributes.id || ""; this.locale = attributes.locale || ""; - this.maxH = getMeasurement(attributes.maxH); - this.maxW = getMeasurement(attributes.maxW); - this.minH = getMeasurement(attributes.minH); - this.minW = getMeasurement(attributes.minW); + this.maxH = getMeasurement(attributes.maxH, "0pt"); + this.maxW = getMeasurement(attributes.maxW, "0pt"); + this.minH = getMeasurement(attributes.minH, "0pt"); + this.minW = getMeasurement(attributes.minW, "0pt"); this.name = attributes.name || ""; this.presence = getStringOption(attributes.presence, [ "visible", @@ -1429,9 +1749,9 @@ class Field extends XFAObject { }); this.use = attributes.use || ""; this.usehref = attributes.usehref || ""; - this.w = getMeasurement(attributes.w); - this.x = getMeasurement(attributes.x); - this.y = getMeasurement(attributes.y); + this.w = attributes.w ? getMeasurement(attributes.w) : ""; + this.x = getMeasurement(attributes.x, "0pt"); + this.y = getMeasurement(attributes.y, "0pt"); this.assist = null; this.bind = null; this.border = null; @@ -1462,7 +1782,7 @@ class Field extends XFAObject { } [$toHTML]() { - if (!this.value) { + if (!this.ui) { return null; } @@ -1472,10 +1792,39 @@ class Field extends XFAObject { "dimensions", "position", "rotate", - "anchorType" + "anchorType", + "presence" ); + // Get border width in order to compute margin and padding. + const borderWidths = []; + const marginWidths = []; + if (this.border) { + Object.assign(style, this.border[$toStyle](borderWidths, marginWidths)); + } + + if (this.margin) { + style.paddingTop = measureToString( + this.margin.topInset - borderWidths[0] - marginWidths[0] + ); + style.paddingRight = measureToString( + this.margin.rightInset - borderWidths[1] - marginWidths[1] + ); + style.paddingBottom = measureToString( + this.margin.bottomInset - borderWidths[2] - marginWidths[2] + ); + style.paddingLeft = measureToString( + this.margin.leftInset - borderWidths[3] - marginWidths[3] + ); + } else { + style.paddingTop = measureToString(-borderWidths[0] - marginWidths[0]); + style.paddingRight = measureToString(-borderWidths[1] - marginWidths[1]); + style.paddingBottom = measureToString(-borderWidths[2] - marginWidths[2]); + style.paddingLeft = measureToString(-borderWidths[3] - marginWidths[3]); + } + const clazz = ["xfaField"]; + // If no font, font properties are inherited. if (this.font) { clazz.push("xfaFont"); } @@ -1486,11 +1835,68 @@ class Field extends XFAObject { class: clazz.join(" "), }; - return { + if (this.name) { + attributes.xfaName = this.name; + } + + const children = []; + const html = { name: "div", attributes, - children: [], + children, }; + + const ui = this.ui ? this.ui[$toHTML]() : null; + if (!ui) { + return html; + } + + if (!ui.attributes.style) { + ui.attributes.style = Object.create(null); + } + children.push(ui); + + if (this.value && ui.name !== "button") { + // TODO: should be ok with string but html ?? + ui.children[0].attributes.value = this.value[$toHTML](); + } + + const caption = this.caption ? this.caption[$toHTML]() : null; + if (!caption) { + return html; + } + + if (ui.name === "button") { + ui.attributes.style.background = style.color; + delete style.color; + if (caption.name === "div") { + caption.name = "span"; + } + ui.children = [caption]; + return html; + } + + ui.children.splice(0, 0, caption); + switch (this.caption.placement) { + case "left": + ui.attributes.style.flexDirection = "row"; + break; + case "right": + ui.attributes.style.flexDirection = "row-reverse"; + break; + case "top": + ui.attributes.style.flexDirection = "column"; + break; + case "bottom": + ui.attributes.style.flexDirection = "column-reverse"; + break; + case "inline": + delete ui.attributes.class; + caption.attributes.style.float = "left"; + break; + } + + return html; } } @@ -1509,7 +1915,7 @@ class Fill extends XFAObject { this.color = null; this.extras = null; - // One-of properties + // One-of properties or none this.linear = null; this.pattern = null; this.radial = null; @@ -1518,7 +1924,6 @@ class Fill extends XFAObject { } [$toStyle]() { - let fill = "#000000"; for (const name of Object.getOwnPropertyNames(this)) { if (name === "extras" || name === "color") { continue; @@ -1528,12 +1933,13 @@ class Fill extends XFAObject { continue; } - fill = obj[$toStyle](this.color); - break; + return { + color: obj[$toStyle](this.color), + }; } return { - color: fill, + color: this.color ? this.color[$toStyle]() : "#000000", }; } } @@ -1582,6 +1988,10 @@ class Float extends ContentObject { const number = parseFloat(this[$content].trim()); this[$content] = isNaN(number) ? null : number; } + + [$toHTML]() { + return this[$content] !== null ? this[$content].toString() : ""; + } } class Font extends XFAObject { @@ -1798,6 +2208,31 @@ class Image extends StringObject { this.use = attributes.use || ""; this.usehref = attributes.usehref || ""; } + + [$toHTML]() { + const html = { + name: "img", + attributes: { + style: {}, + }, + }; + + if (this.href) { + html.attributes.src = new URL(this.href); + return html; + } + + if (this.transferEncoding === "base64") { + const buffer = Uint8Array.from(atob(this[$content]), c => + c.charCodeAt(0) + ); + const blob = new Blob([buffer], { type: this.contentType }); + html.attributes.src = URL.createObjectURL(blob); + return html; + } + + return null; + } } class ImageEdit extends XFAObject { @@ -1826,6 +2261,10 @@ class Integer extends ContentObject { const number = parseInt(this[$content].trim(), 10); this[$content] = isNaN(number) ? null : number; } + + [$toHTML]() { + return this[$content] !== null ? this[$content].toString() : ""; + } } class Issuers extends XFAObject { @@ -1999,6 +2438,15 @@ class Margin extends XFAObject { this.usehref = attributes.usehref || ""; this.extras = null; } + + [$toStyle]() { + return { + marginLeft: measureToString(this.leftInset), + marginRight: measureToString(this.rightInset), + marginTop: measureToString(this.topInset), + marginBottom: measureToString(this.bottomInset), + }; + } } class Mdp extends XFAObject { @@ -2068,6 +2516,27 @@ class NumericEdit extends XFAObject { this.extras = null; this.margin = null; } + + [$toHTML]() { + // TODO: incomplete. + const style = toStyle(this, "border", "font", "margin"); + const html = { + name: "input", + attributes: { + type: "text", + class: "xfaTextfield", + style, + }, + }; + + return { + name: "label", + attributes: { + class: "xfaLabel", + }, + children: [html], + }; + } } class Occur extends XFAObject { @@ -2295,6 +2764,35 @@ class Para extends XFAObject { }); this.hyphenation = null; } + + [$toHTML]() { + const style = { + marginLeft: measureToString(this.marginLeft), + marginRight: measureToString(this.marginRight), + paddingTop: measureToString(this.spaceAbove), + paddingBottom: measureToString(this.spaceBelow), + textIndent: measureToString(this.textIndent), + verticalAlign: this.vAlign, + }; + + if (this.lineHeight.value >= 0) { + style.lineHeight = measureToString(this.lineHeight); + } + + if (this.tabDefault) { + style.tabSize = measureToString(this.tabDefault); + } + + if (this.tabStops.length > 0) { + // TODO + } + + if (this.hyphenatation) { + Object.assign(style, this.hyphenatation[$toHTML]()); + } + + return style; + } } class PasswordEdit extends XFAObject { @@ -2714,7 +3212,7 @@ class Subform extends XFAObject { .trim() .split(/\s+/) .map(x => (x === "-1" ? -1 : getMeasurement(x))); - this.h = getMeasurement(attributes.h); + this.h = attributes.h ? getMeasurement(attributes.h) : ""; this.hAlign = getStringOption(attributes.hAlign, [ "left", "center", @@ -2734,14 +3232,14 @@ class Subform extends XFAObject { "tb", ]); this.locale = attributes.locale || ""; - this.maxH = getMeasurement(attributes.maxH); - this.maxW = getMeasurement(attributes.maxW); + this.maxH = getMeasurement(attributes.maxH, "0pt"); + this.maxW = getMeasurement(attributes.maxW, "0pt"); this.mergeMode = getStringOption(attributes.mergeMode, [ "consumeData", "matchTemplate", ]); - this.minH = getMeasurement(attributes.minH); - this.minW = getMeasurement(attributes.minW); + this.minH = getMeasurement(attributes.minH, "0pt"); + this.minW = getMeasurement(attributes.minW, "0pt"); this.name = attributes.name || ""; this.presence = getStringOption(attributes.presence, [ "visible", @@ -2757,9 +3255,9 @@ class Subform extends XFAObject { this.scope = getStringOption(attributes.scope, ["name", "none"]); this.use = attributes.use || ""; this.usehref = attributes.usehref || ""; - this.w = getMeasurement(attributes.w); - this.x = getMeasurement(attributes.x); - this.y = getMeasurement(attributes.y); + this.w = attributes.w ? getMeasurement(attributes.w) : ""; + this.x = getMeasurement(attributes.x, "0pt"); + this.y = getMeasurement(attributes.y, "0pt"); this.assist = null; this.bind = null; this.bookend = null; @@ -2816,12 +3314,17 @@ class Subform extends XFAObject { page = pageAreas[pageNumber][$toHTML](); } - const style = toStyle(this, "dimensions", "position"); + const style = toStyle(this, "dimensions", "position", "presence"); + const clazz = ["xfaSubform"]; + const cl = layoutClass(this); + if (cl) { + clazz.push(cl); + } const attributes = { style, id: this[$uid], - class: "xfaSubform", + class: clazz.join(" "), }; if (this.name) { @@ -3014,6 +3517,13 @@ class Text extends ContentObject { warn(`XFA - Invalid content in Text: ${child[$nodeName]}.`); return false; } + + [$toHTML]() { + if (typeof this[$content] === "string") { + return this[$content]; + } + return this[$content][$toHTML](); + } } class TextEdit extends XFAObject { @@ -3047,6 +3557,37 @@ class TextEdit extends XFAObject { this.extras = null; this.margin = null; } + + [$toHTML]() { + // TODO: incomplete. + const style = toStyle(this, "border", "font", "margin"); + let html; + if (this.multiline === 1) { + html = { + name: "textarea", + attributes: { + style, + }, + }; + } else { + html = { + name: "input", + attributes: { + type: "text", + class: "xfaTextfield", + style, + }, + }; + } + + return { + name: "label", + attributes: { + class: "xfaLabel", + }, + children: [html], + }; + } } class Time extends StringObject { @@ -3062,6 +3603,10 @@ class Time extends StringObject { // TODO this[$content] = new Date(this[$content]); } + + [$toHTML]() { + return this[$content].toString(); + } } class TimeStamp extends XFAObject { @@ -3148,6 +3693,22 @@ class Ui extends XFAObject { this.signature = null; this.textEdit = null; } + + [$toHTML]() { + // TODO: picture. + for (const name of Object.getOwnPropertyNames(this)) { + if (name === "extras" || name === "picture") { + continue; + } + const obj = this[name]; + if (!(obj instanceof XFAObject)) { + continue; + } + + return obj[$toHTML](); + } + return null; + } } class Validate extends XFAObject { @@ -3226,6 +3787,19 @@ class Value extends XFAObject { this[value[$nodeName]] = value; this[$appendChild](value); } + + [$toHTML]() { + for (const name of Object.getOwnPropertyNames(this)) { + const obj = this[name]; + if (!(obj instanceof XFAObject)) { + continue; + } + + return obj[$toHTML](); + } + + return null; + } } class Variables extends XFAObject { @@ -3364,7 +3938,7 @@ class TemplateNamespace { } static date(attrs) { - return new Date(attrs); + return new DateElement(attrs); } static dateTime(attrs) { diff --git a/test/unit/xfa_parser_spec.js b/test/unit/xfa_parser_spec.js index 329a40fe7..ad07a3b58 100644 --- a/test/unit/xfa_parser_spec.js +++ b/test/unit/xfa_parser_spec.js @@ -117,7 +117,7 @@ describe("XFAParser", function () { anchorType: "topLeft", colSpan: 1, columnWidths: [0], - h: 0, + h: "", hAlign: "left", id: "", layout: "position", @@ -134,7 +134,7 @@ describe("XFAParser", function () { scope: "name", use: "", usehref: "", - w: 0, + w: "", x: 0, y: 0, proto: { diff --git a/web/xfa_layer_builder.css b/web/xfa_layer_builder.css index 3a36581ca..a691ac909 100644 --- a/web/xfa_layer_builder.css +++ b/web/xfa_layer_builder.css @@ -26,8 +26,10 @@ font: inherit; font-kerning: inherit; letter-spacing: inherit; + text-align: inherit; text-decoration: inherit; vertical-align: inherit; + box-sizing: border-box; } .xfaFont { @@ -43,20 +45,131 @@ .xfaDraw { z-index: 200; - background-color: #ff000080; } .xfaExclgroup { z-index: 300; - background-color: #0000ff80; } .xfaField { z-index: 300; - background-color: #00ff0080; } .xfaSubform { z-index: 100; - background-color: #ffff0080; +} + +.xfaLabel { + display: flex; + flex-direction: row; + align-items: center; + width: 100%; + height: 100%; +} + +.xfaCaption { + flex: 1 1 auto; +} + +.xfaTextfield, +.xfaSelect { + width: 100%; + height: 100%; + flex: 1 1 auto; + border: none; +} + +.xfaLabel > input[type="checkbox"] { + /* Use this trick to make the checkbox invisible but + but still focusable. */ + position: absolute; + left: -99999px; +} + +.xfaLabel > input[type="checkbox"]:focus + .xfaCheckboxMark { + box-shadow: 0 0 5px rgba(0, 0, 0, 0.7); +} + +.xfaCheckboxMark { + cursor: pointer; + flex: 0 0 auto; + border-style: solid; + border-width: 2px; + border-color: #8f8f9d; + font-size: 10px; + line-height: 10px; + width: 10px; + height: 10px; + text-align: center; + vertical-align: middle; + display: flex; + flex-direction: row; + align-items: center; +} + +.xfaCheckbox:checked + .xfaCheckboxMark::after { + content: attr(mark); +} + +.xfaButton { + cursor: pointer; + width: 100%; + height: 100%; + border: none; + text-align: center; +} + +.xfaButton:hover { + background: Highlight; +} + +.xfaLrTb, +.xfaRlTb, +.xfaTb, +.xfaPosition { + display: block; +} + +.xfaPosition { + position: relative; +} + +.xfaValignMiddle { + display: flex; + align-items: center; +} + +.xfaLrTb > div { + display: inline; + float: left; +} + +.xfaRlTb > div { + display: inline; + float: right; +} + +.xfaTable { + display: flex; + flex-direction: column; +} + +.xfaTable .xfaRow { + display: flex; + flex-direction: row; + flex: 1 1 auto; +} + +.xfaTable .xfaRow > div { + flex: 1 1 auto; +} + +.xfaTable .xfaRlRow { + display: flex; + flex-direction: row-reverse; + flex: 1; +} + +.xfaTable .xfaRlRow > div { + flex: 1; }