Merge pull request #821 from dolanmiu/feat/text-frame

#269 Text Frame (Text Box)
This commit is contained in:
Dolan
2021-03-14 18:17:44 +00:00
committed by GitHub
21 changed files with 444 additions and 31 deletions

8
.nycrc
View File

@ -1,9 +1,9 @@
{ {
"check-coverage": true, "check-coverage": true,
"lines": 98.66, "lines": 98.67,
"functions": 97.06, "functions": 97.08,
"branches": 95.51, "branches": 95.6,
"statements": 98.67, "statements": 98.68,
"include": [ "include": [
"src/**/*.ts" "src/**/*.ts"
], ],

71
demo/61-text-frame.ts Normal file
View File

@ -0,0 +1,71 @@
// Text Frame (Text Box) example
// Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs";
import { Document, FrameAnchorType, HorizontalPositionAlign, Packer, Paragraph, TextRun, VerticalPositionAlign } from "../build";
const doc = new Document();
doc.addSection({
properties: {},
children: [
new Paragraph({
frame: {
position: {
x: 1000,
y: 3000,
},
width: 4000,
height: 1000,
anchor: {
horizontal: FrameAnchorType.MARGIN,
vertical: FrameAnchorType.MARGIN,
},
alignment: {
x: HorizontalPositionAlign.CENTER,
y: VerticalPositionAlign.TOP,
},
},
border: {
top: {
color: "auto",
space: 1,
value: "single",
size: 6,
},
bottom: {
color: "auto",
space: 1,
value: "single",
size: 6,
},
left: {
color: "auto",
space: 1,
value: "single",
size: 6,
},
right: {
color: "auto",
space: 1,
value: "single",
size: 6,
},
},
children: [
new TextRun("Hello World"),
new TextRun({
text: "Foo Bar",
bold: true,
}),
new TextRun({
text: "\tGithub is the best",
bold: true,
}),
],
}),
],
});
Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer);
});

View File

@ -3,8 +3,8 @@
> Easily generate .docx files with JS/TS. Works for Node and on the Browser. :100: > Easily generate .docx files with JS/TS. Works for Node and on the Browser. :100:
- Simple, declarative API - Simple, declarative API
- 50+ usage examples - 60+ usage examples
- Battle tested, mature, 97%+ coverage - Battle tested, mature, 98%+ coverage
[GitHub](https://github.com/dolanmiu/docx) [GitHub](https://github.com/dolanmiu/docx)
[Get Started](#Welcome) [Get Started](#Welcome)

View File

@ -22,6 +22,7 @@
* [Page Numbers](usage/page-numbers.md) * [Page Numbers](usage/page-numbers.md)
* [Change Tracking](usage/change-tracking.md) * [Change Tracking](usage/change-tracking.md)
* [Math](usage/math.md) * [Math](usage/math.md)
* [Text Frames](usage/text-frames.md)
* Styling * Styling
* [Styling with JS](usage/styling-with-js.md) * [Styling with JS](usage/styling-with-js.md)
* [Styling with XML](usage/styling-with-xml.md) * [Styling with XML](usage/styling-with-xml.md)

View File

@ -1,5 +1,6 @@
# Bullet Points # Bullet Points
!> Bullet Points requires an understanding of [Paragraphs](usage/paragraph.md).
## Example ## Example
To make a bullet point, simply make a paragraph into a bullet point: To make a bullet point, simply make a paragraph into a bullet point:

View File

@ -32,7 +32,7 @@ const paragraph = new Paragraph({
}); });
``` ```
After you create the paragraph, you must add the paragraph into the `document's section`. Learn more about `sections` here: After you create the paragraph, you must add the paragraph into a `section`:
```ts ```ts
doc.addSection({ doc.addSection({
@ -74,9 +74,11 @@ This is the list of options for a paragraph. A detailed explanation is below:
| keepNext | `boolean` | Optional | | | keepNext | `boolean` | Optional | |
| children | `(TextRun or PictureRun or Hyperlink)[]` | Optional | | | children | `(TextRun or PictureRun or Hyperlink)[]` | Optional | |
| style | `string` | Optional | | | style | `string` | Optional | |
| tabStop | `{ left?: ITabStopOptions; right?: ITabStopOptions; maxRight?: { leader: LeaderType; }; center?: ITabStopOptions }` | Optional | | | [tabStop](usage/tab-stops) | `{ left?: ITabStopOptions; right?: ITabStopOptions; maxRight?: { leader: LeaderType; }; center?: ITabStopOptions }` | Optional | |
| bullet | `{ level: number }` | Optional | | | [bullet](usage/bullet-points) | `{ level: number }` | Optional | |
| numbering | `{ num: ConcreteNumbering; level: number; custom?: boolean }` | Optional | | | [numbering](usage/numbering) | `{ num: ConcreteNumbering; level: number; custom?: boolean }` | Optional | |
| [widowControl](#widow-control) | `boolean` | Optional | |
| [frame](usage/text-frames.md) | `IFrameOptions` | Optional | |
## Text ## Text
@ -157,6 +159,17 @@ const paragraph = new Paragraph({
}); });
``` ```
## Widow Control
Allow First/Last Line to Display on a Separate Page
```ts
const paragraph = new Paragraph({
text: "shading",
widowControl: true,
});
```
## Spacing ## Spacing
Adding spacing between paragraphs Adding spacing between paragraphs
@ -195,7 +208,7 @@ const paragraph = new Paragraph({
## Styles ## Styles
To create styles, please refer to the styling Wiki: https://github.com/dolanmiu/docx/wiki/Styling To create styles, please refer to the [styling documentation](usage/styling-with-js)
![Word 2013 Styles menu](http://content.gcflearnfree.org/topics/233/style_apply_choose.png "Word 2013 Styles menu") ![Word 2013 Styles menu](http://content.gcflearnfree.org/topics/233/style_apply_choose.png "Word 2013 Styles menu")

69
docs/usage/text-frames.md Normal file
View File

@ -0,0 +1,69 @@
# Text Frames
Also known as `Text Boxes`
!> Text Frames requires an understanding of [Paragraphs](usage/paragraph.md).
> Text frames are paragraphs of text in a document which are positioned in a separate region or frame in the document, and can be positioned with a specific size and position relative to non-frame paragraphs in the current document.
## Intro
To make a `Text Frame`, simply add the `frame` property on a paragraph. `Borders` can be applied to frame simply by adding the `border` attribute.
```ts
new Paragraph({
frame: {
position: {
x: 1000,
y: 3000,
},
width: 4000,
height: 1000,
anchor: {
horizontal: FrameAnchorType.MARGIN,
vertical: FrameAnchorType.MARGIN,
},
alignment: {
x: HorizontalPositionAlign.CENTER,
y: VerticalPositionAlign.TOP,
},
},
border: {
top: {
color: "auto",
space: 1,
value: "single",
size: 6,
},
bottom: {
color: "auto",
space: 1,
value: "single",
size: 6,
},
left: {
color: "auto",
space: 1,
value: "single",
size: 6,
},
right: {
color: "auto",
space: 1,
value: "single",
size: 6,
},
},
children: [
new TextRun("Hello World"),
new TextRun({
text: "Foo Bar",
bold: true,
}),
new TextRun({
text: "\tGithub is the best",
bold: true,
}),
],
});
```

View File

@ -1,9 +1,9 @@
import { expect } from "chai"; import { expect } from "chai";
import { Formatter } from "export/formatter"; import { Formatter } from "export/formatter";
import { VerticalPositionAlign } from "file/shared/alignment";
import { Align } from "./align"; import { Align } from "./align";
import { VerticalPositionAlign } from "./floating-position";
describe("Align", () => { describe("Align", () => {
describe("#constructor()", () => { describe("#constructor()", () => {

View File

@ -1,6 +1,6 @@
// http://officeopenxml.com/drwPicFloating-position.php // http://officeopenxml.com/drwPicFloating-position.php
import { HorizontalPositionAlign, VerticalPositionAlign } from "file/shared/alignment";
import { XmlComponent } from "file/xml-components"; import { XmlComponent } from "file/xml-components";
import { HorizontalPositionAlign, VerticalPositionAlign } from "./floating-position";
export class Align extends XmlComponent { export class Align extends XmlComponent {
constructor(value: HorizontalPositionAlign | VerticalPositionAlign) { constructor(value: HorizontalPositionAlign | VerticalPositionAlign) {

View File

@ -1,5 +1,7 @@
// http://officeopenxml.com/drwPicFloating-position.php // http://officeopenxml.com/drwPicFloating-position.php
// http://officeopenxml.com/drwPicFloating.php // http://officeopenxml.com/drwPicFloating.php
import { HorizontalPositionAlign, VerticalPositionAlign } from "file/shared/alignment";
import { ITextWrapping } from "../text-wrap"; import { ITextWrapping } from "../text-wrap";
export enum HorizontalPositionRelativeFrom { export enum HorizontalPositionRelativeFrom {
@ -24,22 +26,6 @@ export enum VerticalPositionRelativeFrom {
TOP_MARGIN = "topMargin", TOP_MARGIN = "topMargin",
} }
export enum HorizontalPositionAlign {
CENTER = "center",
INSIDE = "inside",
LEFT = "left",
OUTSIDE = "outside",
RIGHT = "right",
}
export enum VerticalPositionAlign {
BOTTOM = "bottom",
CENTER = "center",
INSIDE = "inside",
OUTSIDE = "outside",
TOP = "top",
}
export interface IHorizontalPositionOptions { export interface IHorizontalPositionOptions {
readonly relative?: HorizontalPositionRelativeFrom; readonly relative?: HorizontalPositionRelativeFrom;
readonly align?: HorizontalPositionAlign; readonly align?: HorizontalPositionAlign;

View File

@ -1,8 +1,9 @@
import { expect } from "chai"; import { expect } from "chai";
import { Formatter } from "export/formatter"; import { Formatter } from "export/formatter";
import { HorizontalPositionAlign } from "file/shared/alignment";
import { HorizontalPositionAlign, HorizontalPositionRelativeFrom } from "./floating-position"; import { HorizontalPositionRelativeFrom } from "./floating-position";
import { HorizontalPosition } from "./horizontal-position"; import { HorizontalPosition } from "./horizontal-position";
describe("HorizontalPosition", () => { describe("HorizontalPosition", () => {

View File

@ -1,8 +1,9 @@
import { expect } from "chai"; import { expect } from "chai";
import { Formatter } from "export/formatter"; import { Formatter } from "export/formatter";
import { VerticalPositionAlign } from "file/shared/alignment";
import { VerticalPositionAlign, VerticalPositionRelativeFrom } from "./floating-position"; import { VerticalPositionRelativeFrom } from "./floating-position";
import { VerticalPosition } from "./vertical-position"; import { VerticalPosition } from "./vertical-position";
describe("VerticalPosition", () => { describe("VerticalPosition", () => {

View File

@ -14,3 +14,4 @@ export * from "./footer-wrapper";
export * from "./header"; export * from "./header";
export * from "./footnotes"; export * from "./footnotes";
export * from "./track-revision"; export * from "./track-revision";
export * from "./shared";

View File

@ -0,0 +1,86 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { HorizontalPositionAlign, VerticalPositionAlign } from "file/shared";
import { FrameAnchorType, FrameProperties } from "./frame-properties";
describe("FrameProperties", () => {
describe("#constructor()", () => {
it("should create", () => {
const currentFrameProperties = new FrameProperties({
position: {
x: 1000,
y: 3000,
},
width: 4000,
height: 1000,
anchor: {
horizontal: FrameAnchorType.MARGIN,
vertical: FrameAnchorType.MARGIN,
},
alignment: {
x: HorizontalPositionAlign.CENTER,
y: VerticalPositionAlign.TOP,
},
});
const tree = new Formatter().format(currentFrameProperties);
expect(tree).to.deep.equal({
"w:framePr": {
_attr: {
"w:h": 1000,
"w:hAnchor": "margin",
"w:vAnchor": "margin",
"w:w": 4000,
"w:x": 1000,
"w:xAlign": "center",
"w:y": 3000,
"w:yAlign": "top",
},
},
});
});
it("should create with the space attribute", () => {
const currentFrameProperties = new FrameProperties({
position: {
x: 1000,
y: 3000,
},
width: 4000,
height: 1000,
anchor: {
horizontal: FrameAnchorType.MARGIN,
vertical: FrameAnchorType.MARGIN,
},
alignment: {
x: HorizontalPositionAlign.CENTER,
y: VerticalPositionAlign.TOP,
},
space: {
horizontal: 100,
vertical: 200,
},
});
const tree = new Formatter().format(currentFrameProperties);
expect(tree).to.deep.equal({
"w:framePr": {
_attr: {
"w:h": 1000,
"w:hAnchor": "margin",
"w:vAnchor": "margin",
"w:w": 4000,
"w:x": 1000,
"w:xAlign": "center",
"w:y": 3000,
"w:yAlign": "top",
"w:hSpace": 100,
"w:vSpace": 200,
},
},
});
});
});
});

View File

@ -0,0 +1,111 @@
// http://officeopenxml.com/WPparagraph-textFrames.php
import { HorizontalPositionAlign, VerticalPositionAlign } from "file/shared/alignment";
import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
export enum DropCapType {
NONE = "none",
DROP = "drop",
MARGIN = "margin",
}
export enum FrameAnchorType {
MARGIN = "margin",
PAGE = "page",
TEXT = "text",
}
export enum FrameWrap {
AROUND = "around",
AUTO = "auto",
NONE = "none",
NOT_BESIDE = "notBeside",
THROUGH = "through",
TIGHT = "tight",
}
export interface IFrameOptions {
readonly anchorLock?: boolean;
readonly dropCap?: DropCapType;
readonly width: number;
readonly height: number;
readonly position: {
readonly x: number;
readonly y: number;
};
readonly wrap?: FrameWrap;
readonly lines?: number;
readonly anchor: {
readonly horizontal: FrameAnchorType;
readonly vertical: FrameAnchorType;
};
readonly space?: {
readonly horizontal: number;
readonly vertical: number;
};
readonly rule?: number;
readonly alignment: {
readonly x: HorizontalPositionAlign;
readonly y: VerticalPositionAlign;
};
}
export class FramePropertiesAttributes extends XmlAttributeComponent<{
readonly anchorLock?: boolean;
readonly dropCap?: DropCapType;
readonly width: number;
readonly height: number;
readonly x: number;
readonly y: number;
readonly wrap?: FrameWrap;
readonly lines?: number;
readonly anchorHorizontal?: FrameAnchorType;
readonly anchorVertical?: FrameAnchorType;
readonly spaceHorizontal?: number;
readonly spaceVertical?: number;
readonly rule?: number;
readonly alignmentX?: HorizontalPositionAlign;
readonly alignmentY?: VerticalPositionAlign;
}> {
protected readonly xmlKeys = {
anchorLock: "w:anchorLock",
dropCap: "w:dropCap",
width: "w:w",
height: "w:h",
x: "w:x",
y: "w:y",
anchorHorizontal: "w:hAnchor",
anchorVertical: "w:vAnchor",
spaceHorizontal: "w:hSpace",
spaceVertical: "w:vSpace",
rule: "w:hRule",
alignmentX: "w:xAlign",
alignmentY: "w:yAlign",
lines: "w:lines",
wrap: "w:wrap",
};
}
export class FrameProperties extends XmlComponent {
constructor(options: IFrameOptions) {
super("w:framePr");
this.root.push(
new FramePropertiesAttributes({
anchorLock: options.anchorLock,
dropCap: options.dropCap,
width: options.width,
height: options.height,
x: options.position.x,
y: options.position.y,
anchorHorizontal: options.anchor.horizontal,
anchorVertical: options.anchor.vertical,
spaceHorizontal: options.space?.horizontal,
spaceVertical: options.space?.vertical,
rule: options.rule,
alignmentX: options.alignment.x,
alignmentY: options.alignment.y,
lines: options.lines,
wrap: options.wrap,
}),
);
}
}

View File

@ -0,0 +1 @@
export * from "./frame-properties";

View File

@ -4,3 +4,4 @@ export * from "./properties";
export * from "./run"; export * from "./run";
export * from "./links"; export * from "./links";
export * from "./math"; export * from "./math";
export * from "./frame";

View File

@ -7,8 +7,10 @@ import { EMPTY_OBJECT } from "file/xml-components";
import { IViewWrapper } from "../document-wrapper"; import { IViewWrapper } from "../document-wrapper";
import { File } from "../file"; import { File } from "../file";
import { HorizontalPositionAlign, VerticalPositionAlign } from "../shared";
import { ShadingType } from "../table/shading"; import { ShadingType } from "../table/shading";
import { AlignmentType, HeadingLevel, LeaderType, PageBreak, TabStopPosition, TabStopType } from "./formatting"; import { AlignmentType, HeadingLevel, LeaderType, PageBreak, TabStopPosition, TabStopType } from "./formatting";
import { FrameAnchorType } from "./frame";
import { Bookmark, ExternalHyperlink } from "./links"; import { Bookmark, ExternalHyperlink } from "./links";
import { Paragraph } from "./paragraph"; import { Paragraph } from "./paragraph";
import { TextRun } from "./run"; import { TextRun } from "./run";
@ -859,6 +861,52 @@ describe("Paragraph", () => {
}); });
}); });
describe("#frame", () => {
it("should set frame attribute", () => {
const paragraph = new Paragraph({
frame: {
position: {
x: 1000,
y: 3000,
},
width: 4000,
height: 1000,
anchor: {
horizontal: FrameAnchorType.MARGIN,
vertical: FrameAnchorType.MARGIN,
},
alignment: {
x: HorizontalPositionAlign.CENTER,
y: VerticalPositionAlign.TOP,
},
},
});
const tree = new Formatter().format(paragraph);
expect(tree).to.deep.equal({
"w:p": [
{
"w:pPr": [
{
"w:framePr": {
_attr: {
"w:h": 1000,
"w:hAnchor": "margin",
"w:vAnchor": "margin",
"w:w": 4000,
"w:x": 1000,
"w:xAlign": "center",
"w:y": 3000,
"w:yAlign": "top",
},
},
},
],
},
],
});
});
});
describe("#prepForXml", () => { describe("#prepForXml", () => {
it("should set Internal Hyperlink", () => { it("should set Internal Hyperlink", () => {
const paragraph = new Paragraph({ const paragraph = new Paragraph({

View File

@ -13,6 +13,7 @@ import { HeadingLevel, Style } from "./formatting/style";
import { LeaderType, TabStop, TabStopPosition, TabStopType } from "./formatting/tab-stop"; import { LeaderType, TabStop, TabStopPosition, TabStopType } from "./formatting/tab-stop";
import { NumberProperties } from "./formatting/unordered-list"; import { NumberProperties } from "./formatting/unordered-list";
import { WidowControl } from "./formatting/widow-control"; import { WidowControl } from "./formatting/widow-control";
import { FrameProperties, IFrameOptions } from "./frame/frame-properties";
import { OutlineLevel } from "./links"; import { OutlineLevel } from "./links";
import { Shading } from "./run/formatting"; import { Shading } from "./run/formatting";
@ -55,6 +56,7 @@ export interface IParagraphPropertiesOptions extends IParagraphStylePropertiesOp
readonly color: string; readonly color: string;
}; };
readonly widowControl?: boolean; readonly widowControl?: boolean;
readonly frame?: IFrameOptions;
} }
export class ParagraphProperties extends IgnoreIfEmptyXmlComponent { export class ParagraphProperties extends IgnoreIfEmptyXmlComponent {
@ -159,6 +161,10 @@ export class ParagraphProperties extends IgnoreIfEmptyXmlComponent {
if (options.widowControl) { if (options.widowControl) {
this.push(new WidowControl(options.widowControl)); this.push(new WidowControl(options.widowControl));
} }
if (options.frame) {
this.push(new FrameProperties(options.frame));
}
} }
public push(item: XmlComponent): void { public push(item: XmlComponent): void {

View File

@ -0,0 +1,15 @@
export enum HorizontalPositionAlign {
CENTER = "center",
INSIDE = "inside",
LEFT = "left",
OUTSIDE = "outside",
RIGHT = "right",
}
export enum VerticalPositionAlign {
BOTTOM = "bottom",
CENTER = "center",
INSIDE = "inside",
OUTSIDE = "outside",
TOP = "top",
}

1
src/file/shared/index.ts Normal file
View File

@ -0,0 +1 @@
export * from "./alignment";