我有一个Google文档模板,可以在其中克隆并生成报告。我正在向这些报告中填充内容的表格添加表格。有时,所有表都可以放在一个页面中,但在某些情况下,它们会溢出到第二页中,从而使一个表在两个页面之间拆分。我想通过将此表推到第二页来避免该表在页面之间分裂。
GAS API不提供禁用此功能或提供信息以检索当前页面或页码的方法。是否有人有解决方法来避免上述情况?下面是我在做什么的示例代码。
function TestFunction() {
var oFolder = DriveApp.getFolderById(Z__FOLDER_ID);
var oReport = DriveApp.getFileById(Z_TEMPLATE_ID).makeCopy("TEST", oFolder);
var oDoc = DocumentApp.openById(oReport.getId());
var arrTables = oDoc.getBody().getTables();
var copiedTable = arrTables[2].copy();
arrTables[2].removeFromParent();
var iTableCount = 0;
for(var iHdrIdx = 0; iHdrIdx < 7; iHdrIdx++) {
var oCompTable = copiedTable.copy();
oCompTable.replaceText("<PLCHLDER_1>", "TEST_1");
oCompTable.replaceText("<PLCHLDER_2>", "TEST_2");
iTableCount = iTableCount + 1;
oDoc.getBody().insertTable(13 + iTableCount, oCompTable);
}
oDoc.saveAndClose();
}
[如果您仔细查看Google文档的structure,您会注意到它是树数据结构,而不是广义上的文档(尽管可能会认为章节/节/段落也是树)样的结构)。
[我怀疑以上是API缺乏页面相关方法的原因-尽管将来可能会添加它们]]
由于文档是树,因此可以将确定何时发生分页的问题简化为计算子高度之和
使页面高度溢出时的点。为了正确获得发生分裂的位置(并跟踪此类元素),我们需要解决几个子问题:
PageBreak
时,可以重置总计数器,因为下一个元素将位于顶部(由溢出偏移)。请注意,由于PageBreak
不是独立的(包装为Paragraph
或ListItem
,所以可以随时遇到。)>TableCell
中最高的TableRow
计入总高度。ContainerElement
,这意味着它们的高度等于/**
* @summary checks if element is a container
* @param {GoogleAppsScript.Document.Element} elem
* @param {GoogleAppsScript.Document.ElementType} type
* @returns {boolean}
*/
const isContainer = (elem, type) => {
const Types = DocumentApp.ElementType;
const containerTypes = [
Types.BODY_SECTION,
Types.EQUATION,
Types.EQUATION_FUNCTION,
Types.FOOTER_SECTION,
Types.HEADER_SECTION,
Types.LIST_ITEM,
Types.PARAGRAPH,
Types.TABLE,
Types.TABLE_CELL,
Types.TABLE_ROW,
Types.TABLE_OF_CONTENTS
];
return containerTypes.includes(type || elem.getType());
};
/**
* @summary gets aspect ratio of a font
* @param {string} fontFamily
* @returns {number}
* @default .52
*/
const getAspectRatio = (fontFamily) => {
const aspects = {
Arial: .52,
Calibri: .47,
Courier: .43,
Garamond: .38,
Georgia: .48,
Helvetica: .52,
Times: .45,
Verdana: .58
};
return aspects[fontFamily] || .618;
};
/**
* @summary checks if Element is direct child of Body
* @param {GoogleAppsScript.Document.Element} elem
* @returns {boolean}
*/
const isTopLevel = (elem) => {
const { ElementType } = DocumentApp;
return elem.getParent().getType() === ElementType.BODY_SECTION;
};
/**
* @summary copies non-object array values as is
* @param {any[]} arr
* @returns {any[]}
*/
const shallowCopy = (arr) => {
return arr.map(el => el);
};
状态跟踪由于我们还必须跟踪溢出状态,处理的元素等,因此我选择添加一个
Tracker
“类”来处理状态管理。最值得注意的是:
processResults
方法:
setDimensions
,setMargins
,resetDimensions
和resetMargins
和私有inits
方法允许我们操纵边界)。Body
的高度设置为0
(否则它将复制子高度)。TableRow
的高度设置为最高的TableCell
。handleOverflow
方法:totalHeight
设置器:每次重新计算都会查找高度溢出,并在需要时调用溢出处理程序。
/**
* @typedef {object} Tracker
* @property {Map.<GoogleAppsScript.Document.ElementType, function>} callbacks map of height processers
* @property {?GoogleAppsScript.Document.Element} currElement current elemenet processed
* @property {number[]} dimensions exposes dimensions of a page
* @property {function(): void} handleOverflow handles page height overflow
* @property {function(): boolean} isOverflow checks if height overflew page height
* @property {number[]} margins exposes margins of a page
* @property {number} overflow getter for overflow status
* @property {function(boolean, ...number): number} processResults process callback results
* @property {function(): Tracker} resetDimensions restores old dimensions
* @property {function(): Tracker} resetMargins restores old margins
* @property {function(): void} resetOverflow resets most resent overflow
* @property {function(): void} resetTotalHeight resets accumulated height
* @property {function(...number): void} setDimensions reinits containing dimensions
* @property {function(...number): void} setMargins reinits containing margins
* @property {function(string, ...any): void} setStore abstract property store setter
* @property {number} significantWidth exposes significant page width
* @property {number} significantHeight exposes significant page height
* @property {GoogleAppsScript.Document.Element[]} splits list of elements split over page
* @property {number} totalHeight total height
*
* @summary factory for element trackers
* @param {Tracker#callbacks} callbacks
* @param {Bounds} bounds
* @param {Tracker#splits} [splits]
* @returns {Tracker}
*/
function makeTracker(callbacks, bounds, splits = []) {
const inits = {
dimensions: shallowCopy(bounds.dimensions),
margins: shallowCopy(bounds.margins)
};
const privates = {
bounds,
current: null,
currentType: null,
currOverflow: 0,
needsReset: 0,
totalHeight: 0
};
const { ElementType } = DocumentApp;
const ResultProcessors = new Map()
.set(ElementType.BODY_SECTION, () => 0)
.set(ElementType.TABLE_ROW, (results) => {
return results.reduce((result, acc) => result > acc ? result : acc, 0);
})
.set("default", (results) => {
return results.reduce((result, acc) => result + acc, 0);
});
return ({
callbacks,
splits,
get currElement() {
return privates.current;
},
set currElement(element) {
privates.current = element;
privates.currentType = element.getType();
},
get dimensions() {
const { bounds } = privates;
return bounds.dimensions;
},
get margins() {
const { bounds } = privates;
return bounds.margins;
},
get overflow() {
const { bounds, totalHeight } = privates;
return totalHeight - bounds.significantHeight;
},
get significantHeight() {
const { bounds } = privates;
return bounds.significantHeight;
},
get significantWidth() {
const { bounds } = privates;
return bounds.significantWidth;
},
get totalHeight() {
return privates.totalHeight;
},
/**
* @summary total height setter
* @description intercepts & recalcs overflow
* @param {number} height
*/
set totalHeight(height) {
privates.totalHeight = height;
if (this.isOverflow()) {
privates.currOverflow = this.overflow;
this.handleOverflow();
}
},
isOverflow() {
return this.overflow > 0;
},
handleOverflow() {
const { currElement, splits } = this;
const type = privates.currentType;
const ignore = [
ElementType.TEXT,
ElementType.TABLE_ROW
];
if (!ignore.includes(type)) {
splits.push(currElement);
}
this.resetTotalHeight();
},
processResults(...results) {
this.resetMargins().resetDimensions();
const { currentType } = privates;
const processed = (
ResultProcessors.get(currentType) ||
ResultProcessors.get("default")
)(results);
return processed;
},
resetDimensions() {
const { bounds } = privates;
const { dimensions } = bounds;
dimensions.length = 0;
dimensions.push(...inits.dimensions);
return this;
},
resetMargins() {
const { bounds } = privates;
const { margins } = bounds;
margins.length = 0;
margins.push(...inits.margins);
return this;
},
resetOverflow() {
privates.currOverflow = 0;
},
resetTotalHeight() {
const { currOverflow } = privates;
this.totalHeight = currOverflow;
this.resetOverflow();
},
setDimensions(...newDimensions) {
return this.setStore("dimensions", ...newDimensions);
},
setMargins(...newMargins) {
return this.setStore("margins", ...newMargins);
},
setStore(property, ...values) {
const { bounds } = privates;
const initStore = inits[property];
const temp = values.map((val, idx) => {
return val === null ? initStore[idx] : val;
});
const store = bounds[property];
store.length = 0;
store.push(...temp);
}
});
};
I。获取页面边界第一个子问题很容易解决(示例可能很复杂,但是便于传递状态)。这里需要注意的是
significantWidth
和significantHeight
吸气剂,它们返回元素可以占用的宽度和高度(即,没有边距)。[如果您想知道,为什么将
54
添加到顶部和底部页边距,它是一个等于1.5
默认垂直页边距(36点)的“魔术数字”,以确保正确的页面溢出(我花了数小时来弄清楚为什么会有大约appx的空间。尽管HeaderSection
和FooterSection
默认为null
,但此大小会添加到首页和底页页边距中,但似乎没有)。
/**
* @typedef {object} Bounds
* @property {number} bottom bottom page margin
* @property {number[]} dimensions page constraints
* @property {number} left left page margin
* @property {number[]} margins page margins
* @property {number} right right page margin
* @property {number} top top page margin
* @property {number} xMargins horizontal page margins
* @property {number} yMargins vertical page margins
*
* @summary gets dimensions of pages in body
* @param {Body} body
* @returns {Bounds}
*/
function getDimensions(body) {
const margins = [
body.getMarginTop() + 54,
body.getMarginRight(),
body.getMarginBottom() + 54,
body.getMarginLeft()
];
const dimensions = [
body.getPageHeight(),
body.getPageWidth()
];
return ({
margins,
dimensions,
get top() {
return this.margins[0];
},
get right() {
return this.margins[1];
},
get bottom() {
return this.margins[2];
},
get left() {
return this.margins[3];
},
get xMargins() {
return this.left + this.right;
},
get yMargins() {
return this.top + this.bottom;
},
get height() {
return this.dimensions[0];
},
get width() {
return this.dimensions[1];
},
get significantWidth() {
return this.width - this.xMargins;
},
get significantHeight() {
return this.height - this.yMargins;
}
});
}
II。遍历元素我们需要从根(
Body
)开始逐步遍历所有子代,直到到达一片叶子(没有子代的元素),获取它们的外部高度和子代的高度(如果有的话),同时始终跟踪PageBreaks
和积累的高度。作为Element
直系子代的每个Body
都将被拆分。注意,
PageBreak
将重置总高度计数器:
/**
* @summary executes a callback for element and its children
* @param {GoogleAppsScript.Document.Element} root
* @param {Tracker} tracker
* @param {boolean} [inCell]
* @returns {number}
*/
function walkElements(root, tracker, inCell = false) {
const { ElementType } = DocumentApp;
const type = root.getType();
if (type === ElementType.PAGE_BREAK) {
tracker.resetTotalHeight();
return 0;
}
const { callbacks } = tracker;
const callback = callbacks.get(type);
const elemResult = callback(root, tracker);
const isCell = type === ElementType.TABLE_CELL;
const cellBound = inCell || isCell;
const childResults = [];
if (isCell || isContainer(root, type)) {
const numChildren = root.getNumChildren();
for (let i = 0; i < numChildren; i++) {
const child = root.getChild(i);
const result = walkElements(child, tracker, cellBound);
childResults.push(result);
}
}
tracker.currElement = root;
const processed = tracker.processResults(elemResult, ...childResults);
isTopLevel(root) && (tracker.totalHeight += processed);
return processed;
}
高度。此外,由于某些元素是容器,因此它们的基本高度等于其子代的总高度之和。因此,我们可以将第三个子问题细分为:
III。计算元素高度通常,
full
元素的高度是顶部,底部边距(或填充或边框)+ base
原始类型
文字高度
[Text
]元素由海图字符组成,因此要计算基本高度必须:1
/**
* @summary calculates Text element height
* @param {GoogleAppsScript.Document.Text} elem
* @param {Tracker} tracker
* @returns {number}
*/
function getTextHeight(elem, tracker) {
const { significantWidth } = tracker;
const fontFamily = elem.getFontFamily();
const charHeight = elem.getFontSize() || 11;
const charWidth = charHeight * getAspectRatio(fontFamily);
/** @type {GoogleAppsScript.Document.ListItem|GoogleAppsScript.Document.Paragraph} */
const parent = elem.getParent();
const lineSpacing = parent.getLineSpacing();
const startIndent = parent.getIndentStart();
const endIndent = parent.getIndentEnd();
const lineWidth = significantWidth - (startIndent + endIndent);
const text = elem.getText();
let adjustedWidth = 0, numLines = 1;
for (const char of text) {
adjustedWidth += charWidth;
const diff = adjustedWidth - lineWidth;
if (diff > 0) {
adjustedWidth = diff;
numLines++;
}
}
return numLines * charHeight * lineSpacing;
}
幸运的是,我们的walker递归处理子元素,因此我们只需要处理每种容器类型的细节(跟踪器的容器类型
processResults
方法将连接子高度)。段落
Paragraph
有两个属性集,这些属性增加了其全部高度:边距
(其中我们只需要顶部和底部-通过getAttributes()
可访问)和spacing:/**
* @summary calcs par height
* @param {GoogleAppsScript.Document.Paragraph} par
* @returns {number}
*/
function getParagraphHeight(par) {
const attrEnum = DocumentApp.Attribute;
const attributes = par.getAttributes();
const before = par.getSpacingBefore();
const after = par.getSpacingAfter();
const spacing = before + after;
const marginTop = attributes[attrEnum.MARGIN_TOP] || 0;
const marginBottom = attributes[attrEnum.MARGIN_BOTTOM] || 0;
let placeholderHeight = 0;
if (par.getNumChildren() === 0) {
const text = par.asText();
placeholderHeight = (text.getFontSize() || 11) * (par.getLineSpacing() || 1.15);
}
return marginTop + marginBottom + spacing + placeholderHeight;
}
注意placeholderHeight
部分-因为当您附加Table
时,必须插入一个空的Paragraph
(无Text
),它等于1行默认文本。
表格单元格
TableCell
元素是一个容器,用作其子元素的主体,因此,例如,计算单元格内部的Text
的高度,尺寸和边距(填充在此上下文中>是相同的边界的边界临时设置为该单元格的边界(高度可以保留不变):
/**
* @summary calcs TableCell height
* @param {GoogleAppsScript.Document.TableCell} elem
* @param {Tracker} tracker
* @returns {number}
*/
function getTableCellHeight(elem, tracker) {
const top = elem.getPaddingTop();
const bottom = elem.getPaddingBottom();
const left = elem.getPaddingLeft();
const right = elem.getPaddingRight();
const width = elem.getWidth();
tracker.setDimensions(null, width);
tracker.setMargins(top, right, bottom, left);
return top + bottom;
}
[表格行
TableRow
没有任何特定属性可计入全高度(并且我们的跟踪器处理TableCell
高度):]]/**
* @summary calcs TableRow height
* @param {GoogleAppsScript.Document.TableRow} row
* @returns {number}
*/
function getTableRowHeight(row) {
return 0;
}
表格
Table
仅包含行,仅将水平边框宽度添加到总数中(只有顶部[或底部]行具有2个边界而不会发生碰撞,因此仅行数+ 1
个边界计数):/**
* @summary calcs Table height
* @param {GoogleAppsScript.Document.Table} elem
* @returns {number}
*/
function getTableHeight(elem) {
const border = elem.getBorderWidth();
const rows = elem.getNumRows();
return border * (rows + 1);
}
IV。确定溢出
/**
* @summary finds elements spl it by pages
* @param {GoogleAppsScript.Document.Document} doc
* @returns {GoogleAppsScript.Document.Element[]}
*/
function findSplitElements(doc) {
const body = doc.getBody();
const bounds = getDimensions(body);
const TypeEnum = DocumentApp.ElementType;
const heightMap = new Map()
.set(TypeEnum.BODY_SECTION, () => 0)
.set(TypeEnum.PARAGRAPH, getParagraphHeight)
.set(TypeEnum.TABLE, getTableHeight)
.set(TypeEnum.TABLE_ROW, getTableRowHeight)
.set(TypeEnum.TABLE_CELL, getTableCellHeight)
.set(TypeEnum.TEXT, getTextHeight);
const tracker = makeTracker(heightMap, bounds);
walkElements(body, tracker);
return tracker.splits;
};
驱动程序功能为了测试整个解决方案是否有效,我使用了该驱动程序:
function doNTimes(n, callback, ...args) {
for (let i = 0; i < n; i++) {
callback(...args);
}
}
function prepareDoc() {
const doc = getTestDoc(); //gets Document somehow
const body = doc.getBody();
doNTimes(30, () => body.appendParagraph("Redrum Redrum Redrum Redrum".repeat(8)));
const cells = [
[1, 2, 0, "A", "test"],
[3, 4, 0, "B", "test"],
[5, 6, 0, "C", "test"],
[7, 8, 0, "D", "test"],
[9, 10, 0, "E", "test"],
[11, 12, 0, "F", "test"]
];
body.appendTable(cells);
doNTimes(8, (c) => body.appendTable(c), cells);
body.appendPageBreak();
doNTimes(5, (c) => body.appendTable(c), cells);
const splits = findSplitElements(doc);
for (const split of splits) {
split.setAttributes({
[DocumentApp.Attribute.BACKGROUND_COLOR]: "#fd9014"
});
}
return doc.getUrl();
}
驱动程序功能将使用背景颜色标记每个拆分元素(您可能希望在每个元素之前添加PageBreak
:
注意