diff --git a/demo/75-tab-stops.ts b/demo/75-tab-stops.ts new file mode 100644 index 0000000000..a2257419d8 --- /dev/null +++ b/demo/75-tab-stops.ts @@ -0,0 +1,80 @@ +// Exporting the document as a stream +// Import from 'docx' rather than '../build' if you install from npm +import * as fs from "fs"; +import { Document, HeadingLevel, Packer, Paragraph, TabStopPosition, TabStopType, TextRun } from "../build"; + +const columnWidth = TabStopPosition.MAX / 4; +const receiptTabStops = [ + // no need to define first left tab column + // the right aligned tab column position should point to the end of column + // i.e. in this case + // (end position of 1st) + (end position of current) + // columnWidth + columnWidth = columnWidth * 2 + + { type: TabStopType.RIGHT, position: columnWidth * 2 }, + { type: TabStopType.RIGHT, position: columnWidth * 3 }, + { type: TabStopType.RIGHT, position: TabStopPosition.MAX }, + ], + twoTabStops = [{ type: TabStopType.RIGHT, position: TabStopPosition.MAX }]; + +const doc = new Document({ + sections: [ + { + properties: {}, + children: [ + new Paragraph({ + heading: HeadingLevel.HEADING_1, + children: [new TextRun("Receipt 001")], + }), + new Paragraph({ + tabStops: twoTabStops, + children: [ + new TextRun({ + text: "To Bob.\tBy Alice.", + bold: true, + }), + ], + }), + new Paragraph({ + tabStops: twoTabStops, + children: [new TextRun("Foo Inc\tBar Inc")], + }), + new Paragraph({ text: "" }), + new Paragraph({ + tabStops: receiptTabStops, + + children: [ + new TextRun({ + text: "Item\tPrice\tQuantity\tSub-total", + bold: true, + }), + ], + }), + new Paragraph({ + tabStops: receiptTabStops, + text: "Item 3\t10\t5\t50", + }), + new Paragraph({ + tabStops: receiptTabStops, + text: "Item 3\t10\t5\t50", + }), + new Paragraph({ + tabStops: receiptTabStops, + text: "Item 3\t10\t5\t50", + }), + new Paragraph({ + tabStops: receiptTabStops, + children: [ + new TextRun({ + text: "\t\t\tTotal: 200", + bold: true, + }), + ], + }), + ], + }, + ], +}); + +const stream = Packer.toStream(doc); +stream.pipe(fs.createWriteStream("My Document.docx")); diff --git a/src/file/paragraph/formatting/tab-stop.spec.ts b/src/file/paragraph/formatting/tab-stop.spec.ts index 377e7bb060..40afd9965a 100644 --- a/src/file/paragraph/formatting/tab-stop.spec.ts +++ b/src/file/paragraph/formatting/tab-stop.spec.ts @@ -8,7 +8,7 @@ describe("LeftTabStop", () => { let tabStop: TabStop; beforeEach(() => { - tabStop = new TabStop(TabStopType.LEFT, 100); + tabStop = new TabStop([{ type: TabStopType.LEFT, position: 100 }]); }); describe("#constructor()", () => { @@ -32,7 +32,7 @@ describe("RightTabStop", () => { let tabStop: TabStop; beforeEach(() => { - tabStop = new TabStop(TabStopType.RIGHT, 100, LeaderType.DOT); + tabStop = new TabStop([{ type: TabStopType.RIGHT, position: 100, leader: LeaderType.DOT }]); }); describe("#constructor()", () => { diff --git a/src/file/paragraph/formatting/tab-stop.ts b/src/file/paragraph/formatting/tab-stop.ts index 4e8fe3436f..3d7b81474f 100644 --- a/src/file/paragraph/formatting/tab-stop.ts +++ b/src/file/paragraph/formatting/tab-stop.ts @@ -1,10 +1,19 @@ // http://officeopenxml.com/WPtab.php import { XmlAttributeComponent, XmlComponent } from "@file/xml-components"; +export interface TabStopDefinition { + readonly type: TabStopType; + readonly position: number | TabStopPosition; + readonly leader?: LeaderType; +} + export class TabStop extends XmlComponent { - public constructor(type: TabStopType, position: number, leader?: LeaderType) { + public constructor(tabDefinitions: readonly TabStopDefinition[]) { super("w:tabs"); - this.root.push(new TabStopItem(type, position, leader)); + + for (const tabDefinition of tabDefinitions) { + this.root.push(new TabStopItem(tabDefinition)); + } } } @@ -41,13 +50,13 @@ export class TabAttributes extends XmlAttributeComponent<{ } export class TabStopItem extends XmlComponent { - public constructor(value: TabStopType, position: string | number, leader?: LeaderType) { + public constructor({ type, position, leader }: TabStopDefinition) { super("w:tab"); this.root.push( new TabAttributes({ - val: value, + val: type, pos: position, - leader, + leader: leader, }), ); } diff --git a/src/file/paragraph/properties.ts b/src/file/paragraph/properties.ts index 6f0088ac98..03bdfc4fc1 100644 --- a/src/file/paragraph/properties.ts +++ b/src/file/paragraph/properties.ts @@ -9,7 +9,7 @@ import { PageBreakBefore } from "./formatting/break"; import { IIndentAttributesProperties, Indent } from "./formatting/indent"; import { ISpacingProperties, Spacing } from "./formatting/spacing"; import { HeadingLevel, Style } from "./formatting/style"; -import { LeaderType, TabStop, TabStopPosition, TabStopType } from "./formatting/tab-stop"; +import { TabStop, TabStopDefinition, TabStopType } from "./formatting/tab-stop"; import { NumberProperties } from "./formatting/unordered-list"; import { FrameProperties, IFrameOptions } from "./frame/frame-properties"; import { OutlineLevel } from "./links"; @@ -41,11 +41,7 @@ export interface IParagraphPropertiesOptions extends IParagraphStylePropertiesOp readonly heading?: HeadingLevel; readonly bidirectional?: boolean; readonly pageBreakBefore?: boolean; - readonly tabStops?: readonly { - readonly position: number | TabStopPosition; - readonly type: TabStopType; - readonly leader?: LeaderType; - }[]; + readonly tabStops?: readonly TabStopDefinition[]; readonly style?: string; readonly bullet?: { readonly level: number; @@ -132,19 +128,22 @@ export class ParagraphProperties extends IgnoreIfEmptyXmlComponent { this.push(new Shading(options.shading)); } - if (options.rightTabStop) { - this.push(new TabStop(TabStopType.RIGHT, options.rightTabStop)); - } + /** + * FIX: Multitab support for Libre Writer + * Ensure there is only one w:tabs tag with multiple w:tab + */ + const tabDefinitions: readonly TabStopDefinition[] = [ + ...(options.rightTabStop ? [{ type: TabStopType.RIGHT, position: options.rightTabStop }] : []), + ...(options.tabStops ? options.tabStops : []), + ...(options.leftTabStop ? [{ type: TabStopType.LEFT, position: options.leftTabStop }] : []), + ]; - if (options.tabStops) { - for (const tabStop of options.tabStops) { - this.push(new TabStop(tabStop.type, tabStop.position, tabStop.leader)); - } - } - - if (options.leftTabStop) { - this.push(new TabStop(TabStopType.LEFT, options.leftTabStop)); + if (tabDefinitions.length > 0) { + this.push(new TabStop(tabDefinitions)); } + /** + * FIX - END + */ if (options.bidirectional !== undefined) { this.push(new OnOffElement("w:bidi", options.bidirectional));