diff --git a/src/file/checkbox/checkbox-symbol.ts b/src/file/checkbox/checkbox-symbol.ts new file mode 100644 index 0000000000..7e172c3141 --- /dev/null +++ b/src/file/checkbox/checkbox-symbol.ts @@ -0,0 +1,29 @@ +// This represents element type CT_SdtCheckboxSymbol element +// +// +// +// + +import { XmlAttributeComponent, XmlComponent } from "@file/xml-components"; +import { shortHexNumber } from "@util/values"; + +class CheckboxSymbolAttributes extends XmlAttributeComponent<{ + readonly val?: string | number | boolean; + readonly symbolfont?: string; +}> { + protected readonly xmlKeys = { + val: "w14:val", + symbolfont: "w14:font", + }; +} + +export class CheckBoxSymbolElement extends XmlComponent { + public constructor(name: string, val: string, font?: string) { + super(name); + if (font) { + this.root.push(new CheckboxSymbolAttributes({ val: shortHexNumber(val), symbolfont: font })); + } else { + this.root.push(new CheckboxSymbolAttributes({ val })); + } + } +} diff --git a/src/file/checkbox/checkbox-util.spec.ts b/src/file/checkbox/checkbox-util.spec.ts new file mode 100644 index 0000000000..74fc74b820 --- /dev/null +++ b/src/file/checkbox/checkbox-util.spec.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from "vitest"; +import { Formatter } from "@export/formatter"; +import { CheckBoxUtil } from "."; + +describe("CheckBoxUtil", () => { + describe("#constructor()", () => { + it("should create a CheckBoxUtil with proper root and default values", () => { + const checkBoxUtil = new CheckBoxUtil(); + + const tree = new Formatter().format(checkBoxUtil); + + expect(tree).to.deep.equal({ + "w14:checkbox": [ + { + "w14:checked": { + _attr: { + "w14:val": "0", + }, + }, + }, + { + "w14:checkedState": { + _attr: { + "w14:font": "MS Gothic", + "w14:val": "2612", + }, + }, + }, + { + "w14:uncheckedState": { + _attr: { + "w14:font": "MS Gothic", + "w14:val": "2610", + }, + }, + }, + ], + }); + }); + + it("should create a CheckBoxUtil with proper structure and custom values", () => { + const checkBoxUtil = new CheckBoxUtil({ + checked: true, + checkedState: { + value: "2713", + font: "Segoe UI Symbol", + }, + uncheckedState: { + value: "2705", + font: "Segoe UI Symbol", + }, + }); + + const tree = new Formatter().format(checkBoxUtil); + + expect(tree).to.deep.equal({ + "w14:checkbox": [ + { + "w14:checked": { + _attr: { + "w14:val": "1", + }, + }, + }, + { + "w14:checkedState": { + _attr: { + "w14:font": "Segoe UI Symbol", + "w14:val": "2713", + }, + }, + }, + { + "w14:uncheckedState": { + _attr: { + "w14:font": "Segoe UI Symbol", + "w14:val": "2705", + }, + }, + }, + ], + }); + }); + }); +}); diff --git a/src/file/checkbox/checkbox-util.ts b/src/file/checkbox/checkbox-util.ts new file mode 100644 index 0000000000..a3732c282e --- /dev/null +++ b/src/file/checkbox/checkbox-util.ts @@ -0,0 +1,44 @@ +// +// +// +// +// +// +// +// + +import { XmlComponent } from "@file/xml-components"; +import { CheckBoxSymbolElement } from "@file/checkbox/checkbox-symbol"; + +export interface ICheckboxSymbolProperties { + readonly value?: string; + readonly font?: string; +} + +export interface ICheckboxSymbolOptions { + readonly checked?: boolean; + readonly checkedState?: ICheckboxSymbolProperties; + readonly uncheckedState?: ICheckboxSymbolProperties; +} + +export class CheckBoxUtil extends XmlComponent { + private readonly DEFAULT_UNCHECKED_SYMBOL: string = "2610"; + private readonly DEFAULT_CHECKED_SYMBOL: string = "2612"; + private readonly DEFAULT_FONT: string = "MS Gothic"; + public constructor(options?: ICheckboxSymbolOptions) { + super("w14:checkbox"); + + const value = options?.checked ? "1" : "0"; + let symbol: string; + let font: string; + this.root.push(new CheckBoxSymbolElement("w14:checked", value)); + + symbol = options?.checkedState?.value ? options?.checkedState?.value : this.DEFAULT_CHECKED_SYMBOL; + font = options?.checkedState?.font ? options?.checkedState?.font : this.DEFAULT_FONT; + this.root.push(new CheckBoxSymbolElement("w14:checkedState", symbol, font)); + + symbol = options?.uncheckedState?.value ? options?.uncheckedState?.value : this.DEFAULT_UNCHECKED_SYMBOL; + font = options?.uncheckedState?.font ? options?.uncheckedState?.font : this.DEFAULT_FONT; + this.root.push(new CheckBoxSymbolElement("w14:uncheckedState", symbol, font)); + } +} diff --git a/src/file/checkbox/checkbox.spec.ts b/src/file/checkbox/checkbox.spec.ts new file mode 100644 index 0000000000..8599e60f42 --- /dev/null +++ b/src/file/checkbox/checkbox.spec.ts @@ -0,0 +1,211 @@ +import { describe, expect, it } from "vitest"; +import { Formatter } from "@export/formatter"; +import { CheckBox } from "."; + +describe("CheckBox", () => { + describe("#constructor()", () => { + it("should create a CheckBox with proper root and default values (no alias, no custom state)", () => { + const checkBox = new CheckBox(); + + const tree = new Formatter().format(checkBox); + + expect(tree).to.deep.equal({ + "w:sdt": [ + { + "w:sdtPr": [ + { + "w14:checkbox": [ + { + "w14:checked": { + _attr: { + "w14:val": "0", + }, + }, + }, + { + "w14:checkedState": { + _attr: { + "w14:font": "MS Gothic", + "w14:val": "2612", + }, + }, + }, + { + "w14:uncheckedState": { + _attr: { + "w14:font": "MS Gothic", + "w14:val": "2610", + }, + }, + }, + ], + }, + ], + }, + { + "w:sdtContent": [ + { + "w:r": [ + { + "w:sym": { + _attr: { + "w:char": "2610", + "w:font": "MS Gothic", + }, + }, + }, + ], + }, + ], + }, + ], + }); + }); + + it.each([ + ["2713", "Segoe UI Symbol", "2713", "Segoe UI Symbol"], + [undefined, undefined, "2612", "MS Gothic"], + ])("should create a CheckBox with proper root and custom values", (inputChar, inputFont, actualChar, actualFont) => { + const checkBox = new CheckBox("Custom Checkbox", { + checked: true, + checkedState: { + value: inputChar, + font: inputFont, + }, + uncheckedState: { + value: "2705", + font: "Segoe UI Symbol", + }, + }); + + const tree = new Formatter().format(checkBox); + + expect(tree).to.deep.equal({ + "w:sdt": [ + { + "w:sdtPr": [ + { + "w:alias": { + _attr: { + "w:val": "Custom Checkbox", + }, + }, + }, + { + "w14:checkbox": [ + { + "w14:checked": { + _attr: { + "w14:val": "1", + }, + }, + }, + { + "w14:checkedState": { + _attr: { + "w14:font": actualFont, + "w14:val": actualChar, + }, + }, + }, + { + "w14:uncheckedState": { + _attr: { + "w14:font": "Segoe UI Symbol", + "w14:val": "2705", + }, + }, + }, + ], + }, + ], + }, + { + "w:sdtContent": [ + { + "w:r": [ + { + "w:sym": { + _attr: { + "w:char": actualChar, + "w:font": actualFont, + }, + }, + }, + ], + }, + ], + }, + ], + }); + }); + + it("should create a CheckBox with proper root, custom state, and no alias", () => { + const checkBox = new CheckBox(undefined, { + checked: false, + checkedState: { + value: "2713", + font: "Segoe UI Symbol", + }, + uncheckedState: { + value: "2705", + font: "Segoe UI Symbol", + }, + }); + + const tree = new Formatter().format(checkBox); + + expect(tree).to.deep.equal({ + "w:sdt": [ + { + "w:sdtPr": [ + { + "w14:checkbox": [ + { + "w14:checked": { + _attr: { + "w14:val": "0", + }, + }, + }, + { + "w14:checkedState": { + _attr: { + "w14:font": "Segoe UI Symbol", + "w14:val": "2713", + }, + }, + }, + { + "w14:uncheckedState": { + _attr: { + "w14:font": "Segoe UI Symbol", + "w14:val": "2705", + }, + }, + }, + ], + }, + ], + }, + { + "w:sdtContent": [ + { + "w:r": [ + { + "w:sym": { + _attr: { + "w:char": "2705", + "w:font": "Segoe UI Symbol", + }, + }, + }, + ], + }, + ], + }, + ], + }); + }); + }); +}); diff --git a/src/file/checkbox/checkbox.ts b/src/file/checkbox/checkbox.ts new file mode 100644 index 0000000000..c60690e3af --- /dev/null +++ b/src/file/checkbox/checkbox.ts @@ -0,0 +1,43 @@ +import { SymbolRun } from "@file/paragraph/run/symbol-run"; +import { StructuredDocumentTagProperties } from "@file/table-of-contents/sdt-properties"; +import { StructuredDocumentTagContent } from "@file/table-of-contents/sdt-content"; +import { XmlComponent } from "@file/xml-components"; +import { CheckBoxUtil, ICheckboxSymbolOptions } from "./checkbox-util"; + +export class CheckBox extends XmlComponent { + // default values per Microsoft + private readonly DEFAULT_UNCHECKED_SYMBOL: string = "2610"; + private readonly DEFAULT_CHECKED_SYMBOL: string = "2612"; + private readonly DEFAULT_FONT: string = "MS Gothic"; + public constructor(alias?: string, options?: ICheckboxSymbolOptions) { + super("w:sdt"); + + const properties = new StructuredDocumentTagProperties(alias); + properties.addChildElement(new CheckBoxUtil(options)); + this.root.push(properties); + + const content = new StructuredDocumentTagContent(); + const checkedFont: string | undefined = options?.checkedState?.font; + const checkedText: string | undefined = options?.checkedState?.value; + const uncheckedFont: string | undefined = options?.uncheckedState?.font; + const uncheckedText: string | undefined = options?.uncheckedState?.value; + let symbolFont: string; + let char: string; + + if (options?.checked) { + symbolFont = checkedFont ? checkedFont : this.DEFAULT_FONT; + char = checkedText ? checkedText : this.DEFAULT_CHECKED_SYMBOL; + } else { + symbolFont = uncheckedFont ? uncheckedFont : this.DEFAULT_FONT; + char = uncheckedText ? uncheckedText : this.DEFAULT_UNCHECKED_SYMBOL; + } + + const initialRenderedChar = new SymbolRun({ + char: char, + symbolfont: symbolFont, + }); + + content.addChildElement(initialRenderedChar); + this.root.push(content); + } +} diff --git a/src/file/checkbox/index.ts b/src/file/checkbox/index.ts new file mode 100644 index 0000000000..9af5c31e28 --- /dev/null +++ b/src/file/checkbox/index.ts @@ -0,0 +1,3 @@ +export * from "./checkbox-util"; +export * from "./checkbox-symbol"; +export * from "./checkbox"; diff --git a/src/file/index.ts b/src/file/index.ts index 4bdac87ef9..6cf75e4f24 100644 --- a/src/file/index.ts +++ b/src/file/index.ts @@ -17,3 +17,4 @@ export * from "./track-revision"; export * from "./shared"; export * from "./border"; export * from "./vertical-align"; +export * from "./checkbox"; diff --git a/src/file/paragraph/paragraph.ts b/src/file/paragraph/paragraph.ts index a12b88df6a..feafb1450f 100644 --- a/src/file/paragraph/paragraph.ts +++ b/src/file/paragraph/paragraph.ts @@ -6,6 +6,7 @@ import { FileChild } from "@file/file-child"; import { TargetModeType } from "../relationships/relationship/relationship"; import { DeletedTextRun, InsertedTextRun } from "../track-revision"; +import { CheckBox } from "../checkbox"; import { ColumnBreak, PageBreak } from "./formatting/break"; import { Bookmark, ConcreteHyperlink, ExternalHyperlink, InternalHyperlink } from "./links"; import { Math } from "./math"; @@ -33,7 +34,8 @@ export type ParagraphChild = | Comment | CommentRangeStart | CommentRangeEnd - | CommentReference; + | CommentReference + | CheckBox; export interface IParagraphOptions extends IParagraphPropertiesOptions { readonly text?: string; diff --git a/src/file/table-of-contents/sdt-properties.ts b/src/file/table-of-contents/sdt-properties.ts index f1f2c56304..4ee856eda3 100644 --- a/src/file/table-of-contents/sdt-properties.ts +++ b/src/file/table-of-contents/sdt-properties.ts @@ -2,8 +2,10 @@ import { StringValueElement, XmlComponent } from "@file/xml-components"; export class StructuredDocumentTagProperties extends XmlComponent { - public constructor(alias: string) { + public constructor(alias?: string) { super("w:sdtPr"); - this.root.push(new StringValueElement("w:alias", alias)); + if (typeof alias === "string") { + this.root.push(new StringValueElement("w:alias", alias)); + } } }