From 453a96b57cc232ab2f64fb549c84ceb92b4d4d03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20N=C3=B6lke?= Date: Tue, 8 Oct 2019 11:39:50 +0200 Subject: [PATCH] fix(list): text content of a list item cannot be styled (#120) BREAKING CHANGE: The `addListItem` method on a `List` no longer accepts a string and it doesn't automagically create a "hidden" paragraph. Instead an empty `ListItem` is being created. On the `ListItem` instance it is now possible to add headings or paragraphs. Fixes: #67 --- CHANGELOG.md | 2 + README.md | 4 +- examples/homer-resume/index.ts | 13 ++--- src/api/office/TextBody.ts | 4 ++ src/api/text/List.spec.ts | 27 ++++++----- src/api/text/List.ts | 89 +++++++++++++++------------------- src/api/text/ListItem.spec.ts | 27 +++++++++++ src/api/text/ListItem.ts | 46 ++++++++++++++---- src/xml/DomVisitor.spec.ts | 15 ++++-- test/integration.spec.ts | 5 +- 10 files changed, 146 insertions(+), 86 deletions(-) create mode 100644 src/api/text/ListItem.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ee8981d..669a862d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] (2019-??-??) +### Changed +- **list:** text content of a list item cannot be styled, closes [#67](https://github.com/connium/simple-odf/issues/67) ## [0.10.1] (2019-07-11) ### Changed diff --git a/README.md b/README.md index 9000e134..2537d213 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,8 @@ body.addHeading('Credits', 2); body.addParagraph('This was quite easy. Do you want to know why?'); const list = body.addList(); -list.addItem('one-liner setup'); -list.addItem('just write like you would do in a full-blown editor'); +list.addItem().addParagraph('one-liner setup'); +list.addItem().addParagraph('just write like you would do in a full-blown editor'); document.saveFlat('/home/homer/My_first_document.fodf'); ``` diff --git a/examples/homer-resume/index.ts b/examples/homer-resume/index.ts index 44cee1e1..3681d0da 100644 --- a/examples/homer-resume/index.ts +++ b/examples/homer-resume/index.ts @@ -40,7 +40,7 @@ jobTitleStyle.setTypeface(Typeface.Bold); const companyNameStyle = new ParagraphStyle(); companyNameStyle.setFontName(fontName); -companyNameStyle.setFontSize(10) +companyNameStyle.setFontSize(10); companyNameStyle.setTypeface(Typeface.Italic); const defaultStyle = new ParagraphStyle(); @@ -93,8 +93,9 @@ body.addHeading('Nuclear Safety Inspector', 3) body.addParagraph('Springfield Nuclear Power Plant, Springfield, USA') .setStyle(companyNameStyle); const list1 = body.addList(); -list1.addItem('Strengthened safety procedures that resulted in 75% fewer accidents on days I was absent'); -list1.addItem('Pioneered workplace stress-reduction methods that worked for at least one employee'); +// tslint:disable-next-line:max-line-length +list1.addItem().addParagraph('Strengthened safety procedures that resulted in 75% fewer accidents on days I was absent'); +list1.addItem().addParagraph('Pioneered workplace stress-reduction methods that worked for at least one employee'); body.addParagraph(); @@ -105,8 +106,8 @@ body.addHeading('Owner and Chief Driver for Snow-Plowing Business', 3) body.addParagraph('Mr. Plow, Springfield, USA') .setStyle(companyNameStyle); const list2 = body.addList(); -list2.addItem('Boosted business 15% by executing late-night TV marketing campaign'); -list2.addItem('Received key to the city in recognition of the achievements'); +list2.addItem().addParagraph('Boosted business 15% by executing late-night TV marketing campaign'); +list2.addItem().addParagraph('Received key to the city in recognition of the achievements'); body.addParagraph(); body.addParagraph(); @@ -127,7 +128,7 @@ body.addParagraph('1969-10 - 1973-06') body.addHeading('Springfield High School', 3) .setStyle(jobTitleStyle); const list3 = body.addList(); -list3.addItem('Graduated 4-'); +list3.addItem().addParagraph('Graduated 4-'); body.addParagraph(); body.addParagraph(); diff --git a/src/api/office/TextBody.ts b/src/api/office/TextBody.ts index 59156dc5..98906c11 100644 --- a/src/api/office/TextBody.ts +++ b/src/api/office/TextBody.ts @@ -32,6 +32,10 @@ export class TextBody extends OdfElement { /** * Adds an empty list at the end of the document. * + * @example + * new TextBody() + * .addList(); + * * @returns {List} The newly added list * @since 0.7.0 */ diff --git a/src/api/text/List.spec.ts b/src/api/text/List.spec.ts index 8d78f796..947cb8a2 100644 --- a/src/api/text/List.spec.ts +++ b/src/api/text/List.spec.ts @@ -10,28 +10,31 @@ describe(List.name, () => { beforeEach(() => { list = new List(); - testItem1 = new ListItem('first'); - testItem2 = new ListItem('second'); - testItem3 = new ListItem('third'); + testItem1 = new ListItem(); + testItem2 = new ListItem(); + testItem3 = new ListItem(); }); describe('#addItem', () => { beforeEach(() => { - list.addItem('first'); + list.addItem(); }); it('create new item at the end of the list and return the added item', () => { - const addedItem = list.addItem('second'); + const addedItem = list.addItem(); - expect(addedItem).toEqual(testItem2); - expect(list.getItems()).toEqual([testItem1, testItem2]); + expect(addedItem).toBeInstanceOf(ListItem); + expect(list.getItems().length).toBe(2); }); it('add new item to the end of the list and return the added item', () => { - const addedItem = list.addItem(testItem2); + const testItem = new ListItem(); - expect(addedItem).toBe(testItem2); - expect(list.getItems()).toEqual([testItem1, testItem2]); + const addedItem = list.addItem(testItem); + + expect(addedItem).toBe(testItem); + expect(list.getItems().length).toBe(2); + expect(list.getItem(1)).toBe(testItem); }); }); @@ -43,11 +46,11 @@ describe(List.name, () => { list.addItem(testItem2); list.addItem(testItem3); - itemToAdd = new ListItem('new'); + itemToAdd = new ListItem(); }); it('insert item at the specified position and return the added item', () => { - const insertedItem = list.insertItem(2, 'new'); + const insertedItem = list.insertItem(2, itemToAdd); expect(insertedItem).toEqual(itemToAdd); expect(list.getItems()).toEqual([testItem1, testItem2, itemToAdd, testItem3]); diff --git a/src/api/text/List.ts b/src/api/text/List.ts index 70dffe99..95b66114 100644 --- a/src/api/text/List.ts +++ b/src/api/text/List.ts @@ -6,9 +6,9 @@ import { ListItem } from './ListItem'; * * @example * const list = document.getBody().addList(); - * list.addItem('First item'); - * list.addItem('Second item'); - * list.insertItem(1, 'After first item'); + * list.addItem(); + * list.addItem(); + * list.insertItem(1, new ListItem()); * list.removeItemAt(2); * * @since 0.2.0 @@ -27,58 +27,44 @@ export class List extends OdfElement { } /** - * The `addItem()` method adds a new list item with the specified text or adds the specified item to the list. + * The `addItem()` method adds a new list item or adds the specified item to the list. * * @example * const list = new List(); - * list.addItem('First item'); - * list.addItem(new ListItem('Second item')); + * list.addItem(); + * list.addItem(new ListItem()); * - * @param {string | ListItem} [item] The text content of the new item or the item to add + * @param {ListItem} [item] The item to add * @returns {ListItem} The added `ListItem` object * @since 0.2.0 */ - public addItem (item?: string | ListItem): ListItem { - if (item instanceof ListItem) { - this.append(item); - return item; - } - - const listItem = new ListItem(item); + public addItem (item?: ListItem): ListItem { + const listItem = item || new ListItem(); this.append(listItem); return listItem; } /** - * The `insertItem` method inserts a new list item with the specified text - * or inserts the specified item at the specified position. + * The `insertItem` method inserts the specified item at the specified position. * The item is inserted before the item at the specified position. * - * If the position is greater than the current number items, the new item is appended at the end of the list. + * If the position is greater than the current number of items, the new item is appended at the end of the list. * If the position is negative, the new item is inserted as first element. * * @example * const list = new List(); - * list.addItem('First item'); // 'First item' - * list.addItem('Second item'); // 'First item', 'Second item' - * list.insertItem(1, 'After first item'); // 'First item', 'After first item', 'Second item' + * list.addItem(); + * list.insertItem(0, new ListItem()); // insert before existing item * * @param {number} position The index at which to insert the list item (starting from 0). - * @param {string | ListItem} item The text content of the new item or the item to insert + * @param {ListItem} item The item to insert * @returns {ListItem} The inserted `ListItem` object * @since 0.2.0 */ - public insertItem (position: number, item: string | ListItem): ListItem { - if (item instanceof ListItem) { - this.insert(position, item); - return item; - } - - const listItem = new ListItem(item); - this.insert(position, listItem); - - return listItem; + public insertItem (position: number, item: ListItem): ListItem { + this.insert(position, item); + return item; } /** @@ -87,10 +73,10 @@ export class List extends OdfElement { * * @example * const list = new List(); - * list.addItem('First item'); - * list.addItem('Second item'); - * list.getItem(1); // 'Second item' - * list.getItem(2); // undefined + * list.addItem(); + * list.addItem(); + * list.getItem(1); // second item + * list.getItem(2); // undefined * * @param {number} position The index of the requested list item (starting from 0). * @returns {ListItem | undefined} The `ListItem` object at the specified position @@ -106,10 +92,10 @@ export class List extends OdfElement { * * @example * const list = new List(); - * list.getItems(); // [] - * list.addItem('First item'); - * list.addItem('Second item'); - * list.getItems(); // ['First item', 'Second item'] + * list.getItems(); // [] + * list.addItem(); + * list.addItem(); + * list.getItems(); // [first item, second item] * * @returns {ListItem[]} A copy of the list of `ListItem` objects * @since 0.2.0 @@ -123,11 +109,11 @@ export class List extends OdfElement { * * @example * const list = new List(); - * list.addItem('First item'); - * list.addItem('Second item'); - * list.removeItemAt(0); // 'First item' - * list.getItems(); // ['Second item'] - * list.removeItemAt(2); // undefined + * list.addItem(); + * list.addItem(); + * list.removeItemAt(0); // first item + * list.getItems(); // [second item] + * list.removeItemAt(2); // undefined * * @param {number} position The index of the list item to remove (starting from 0). * @returns {ListItem | undefined} The removed `ListItem` object @@ -143,9 +129,10 @@ export class List extends OdfElement { * * @example * const list = new List(); - * list.addItem('First item'); // 'First item' - * list.addItem('Second item'); // 'First item', 'Second item' - * list.clear(); // - + * list.addItem(); + * list.addItem(); + * list.clear(); + * list.getItems(); // [] * * @returns {List} The `List` object * @since 0.2.0 @@ -165,10 +152,10 @@ export class List extends OdfElement { * * @example * const list = new List(); - * list.size(); // 0 - * list.addItem('First item'); - * list.addItem('Second item'); - * list.size(); // 2 + * list.size(); // 0 + * list.addItem(); + * list.addItem(); + * list.size(); // 2 * * @returns {number} The number of items in this list * @since 0.2.0 diff --git a/src/api/text/ListItem.spec.ts b/src/api/text/ListItem.spec.ts new file mode 100644 index 00000000..02923c39 --- /dev/null +++ b/src/api/text/ListItem.spec.ts @@ -0,0 +1,27 @@ +import { Heading } from './Heading'; +import { ListItem } from './ListItem'; +import { Paragraph } from './Paragraph'; + +describe(ListItem.name, () => { + let listItem: ListItem; + + beforeEach(() => { + listItem = new ListItem(); + }); + + describe('#addHeading', () => { + it('return a heading', () => { + const heading = listItem.addHeading(); + + expect(heading).toBeInstanceOf(Heading); + }); + }); + + describe('#addParagraph', () => { + it('return a paragraph', () => { + const paragraph = listItem.addParagraph(); + + expect(paragraph).toBeInstanceOf(Paragraph); + }); + }); +}); diff --git a/src/api/text/ListItem.ts b/src/api/text/ListItem.ts index 05cb0ebb..b1f60af1 100644 --- a/src/api/text/ListItem.ts +++ b/src/api/text/ListItem.ts @@ -1,32 +1,58 @@ import { OdfElement } from '../OdfElement'; +import { Heading } from './Heading'; import { Paragraph } from './Paragraph'; /** * This class represents an item in a list. * * @example - * const list = document.getBody() - * .addList() - * .addItem('First item'); + * const listItem = new ListItem(); + * listItem.addHeading('headline'); + * listItem.addParagraph('paragraph'); * * @since 0.2.0 */ export class ListItem extends OdfElement { - private paragraph: Paragraph; - /** * Creates a `ListItem` instance that represents an item in a list. * * @example - * new ListItem('First item'); + * new ListItem(); * - * @param {string} [text=''] The text content of the list item; defaults to an empty string if omitted * @since 0.2.0 */ - public constructor (text?: string) { + public constructor () { super(); + } + + /** + * Adds a heading at the end of the list item. + * If a text is given, this will be set as text content of the heading. + * + * @param {string} [text] The text content of the heading + * @param {number} [level=1] The heading level; defaults to 1 if omitted + * @returns {Heading} The newly added heading + * @since 0.11.0 + */ + public addHeading (text?: string, level = 1): Heading { + const heading = new Heading(text, level); + this.append(heading); + + return heading; + } + + /** + * Adds a paragraph at the end of the list item. + * If a text is given, this will be set as text content of the paragraph. + * + * @param {string} [text] The text content of the paragraph + * @returns {Paragraph} The newly added paragraph + * @since 0.11.0 + */ + public addParagraph (text?: string): Paragraph { + const paragraph = new Paragraph(text); + this.append(paragraph); - this.paragraph = new Paragraph(text); - this.append(this.paragraph); + return paragraph; } } diff --git a/src/xml/DomVisitor.spec.ts b/src/xml/DomVisitor.spec.ts index 26d0cd1c..bfa79055 100644 --- a/src/xml/DomVisitor.spec.ts +++ b/src/xml/DomVisitor.spec.ts @@ -136,13 +136,22 @@ describe(DomVisitor.name, () => { expect(documentAsString).not.toMatch(/ { - list.addItem('first'); + it('insert a list with a list item and a nested heading', () => { + list.addItem().addHeading(testText, 2); domVisitor.visit(list, testDocument, testRoot); const documentAsString = new XMLSerializer().serializeToString(testDocument); - expect(documentAsString).toMatch(/first<\/text:p><\/text:list-item><\/text:list>/); + expect(documentAsString).toMatch(/some text<\/text:h><\/text:list-item><\/text:list>/); + }); + + it('insert a list with a list item and a nested paragraph', () => { + list.addItem().addParagraph(testText); + + domVisitor.visit(list, testDocument, testRoot); + + const documentAsString = new XMLSerializer().serializeToString(testDocument); + expect(documentAsString).toMatch(/some text<\/text:p><\/text:list-item><\/text:list>/); }); }); diff --git a/test/integration.spec.ts b/test/integration.spec.ts index fa820f25..ce3300c2 100644 --- a/test/integration.spec.ts +++ b/test/integration.spec.ts @@ -300,8 +300,9 @@ xdescribe('integration', () => { heading.setStyle(style); const list = body.addList(); - list.addItem('first item'); - list.addItem('second item'); + list.addItem().addHeading('list item heading', 3); + list.addItem().addParagraph('first item'); + list.addItem().addParagraph('second item'); }); it('save document', async (done) => {