Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep start property and marker (if supported) when copying list from WordOnline #1200

Merged
merged 7 commits into from
Aug 25, 2022
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ const WORD_ONLINE_IDENTIFYING_SELECTOR =
const LIST_CONTAINER_ELEMENT_CLASS_NAME = 'ListContainerWrapper';
const IMAGE_CONTAINER_ELEMENT_CLASS_NAME = 'WACImageContainer';

//When the list style is a symbol and the value is not in the clipboard, WordOnline
const VALID_LIST_STYLE_CHAR_CODES = [
'111', //'o'
'9643', //'▫'
'9830', //'♦'
];

/**
* @internal
*/
Expand Down Expand Up @@ -102,7 +109,7 @@ export default function convertPastedContentFromWordOnline(fragment: DocumentFra
let listType: 'OL' | 'UL' = getContainerListType(listItemContainer); // list type that is contained by iterator.
// Initialize processed element with proper listType if this is the first element
if (!convertedListElement) {
convertedListElement = doc.createElement(listType);
convertedListElement = createNewList(listItemContainer, doc, listType);
}

// Get all list items(<li>) in the current iterator element.
Expand All @@ -117,7 +124,7 @@ export default function convertPastedContentFromWordOnline(fragment: DocumentFra
// and keep the processing going.
if (getTagOfNode(convertedListElement) != listType && itemLevel == 1) {
insertConvertedListToDoc(convertedListElement, fragment, itemBlock);
convertedListElement = doc.createElement(listType);
convertedListElement = createNewList(listItemContainer, doc, listType);
}
insertListItem(convertedListElement, item, listType, doc);
});
Expand Down Expand Up @@ -157,6 +164,15 @@ export default function convertPastedContentFromWordOnline(fragment: DocumentFra
});
}

function createNewList(listItemContainer: Element, doc: Document, tag: 'OL' | 'UL') {
const newList = doc.createElement(tag);
const startAttribute = listItemContainer.firstElementChild?.getAttribute('start');
if (startAttribute) {
newList.setAttribute('start', startAttribute);
}
return newList;
}

/**
* The node processing is based on the premise of only ol/ul is in ListContainerWrapper class
* However the html might be malformed, this function is to split all the other elements out of ListContainerWrapper
Expand Down Expand Up @@ -254,14 +270,25 @@ function getContainerListType(listItemContainer: Element): 'OL' | 'UL' | null {
function insertListItem(
listRootElement: Element,
itemToInsert: HTMLElement,
listType: string,
listType: 'UL' | 'OL',
doc: HTMLDocument
): void {
if (!listType) {
return;
}
// Get item level from 'data-aria-level' attribute
let itemLevel = parseInt(itemToInsert.getAttribute('data-aria-level'));
let itemLevel = parseInt(itemToInsert.getAttribute('data-aria-level') ?? '');

// Try to reuse the List Marker
let style = itemToInsert.getAttribute('data-leveltext');
if (
listType == 'UL' &&
style &&
VALID_LIST_STYLE_CHAR_CODES.indexOf(style.charCodeAt(0).toString()) > -1
) {
itemToInsert.style.listStyleType = `"${style} "`;
}

let curListLevel = listRootElement; // Level iterator to find the correct place for the current element.
// if the itemLevel is 1 it means the level iterator is at the correct place.
while (itemLevel > 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,7 @@ import convertPastedContentFromWordOnline from '../../../lib/plugins/Paste/offic

describe('wordOnlineHandler', () => {
function runTest(html: string, expectedInnerHtml: string) {
const doc = new DOMParser().parseFromString(html, 'text/html');
const fragment = doc.createDocumentFragment();
while (doc.body.firstChild) {
fragment.appendChild(doc.body.firstChild);
}

convertPastedContentFromWordOnline(fragment);

while (fragment.firstChild) {
doc.body.appendChild(fragment.firstChild);
}
const doc = sanitizeContent(html);

expect(doc.body.innerHTML).toBe(expectedInnerHtml);
}
Expand Down Expand Up @@ -384,4 +374,56 @@ describe('wordOnlineHandler', () => {
);
});
});

it('Keep the start property on lists and try to reuse the Word provided marker style', () => {
const doc = sanitizeContent(
'<html><body><div><p><span><span>Test</span></span><span>&nbsp;</span></p></div><div class="ListContainerWrapper"><ol start="1"><li data-aria-level="1" data-aria-posinset="1" aria-setsize="-1" data-listid="1" data-leveltext="%1."><p><span><span>Test</span></span><span>&nbsp;</span></p></li></ol></div><div class="ListContainerWrapper"><ul><li data-aria-level="2" data-aria-posinset="1" aria-setsize="-1" data-listid="1" data-leveltext="▫"><p><span><span>Test</span></span><span>&nbsp;</span></p></li></ul></div><div><p><span><span>Test</span></span><span>&nbsp;</span></p></div><div class="ListContainerWrapper"><ol start="5"><li data-aria-level="1" data-aria-posinset="2" aria-setsize="-1" data-listid="1" data-leveltext="%1."><p><span><span>Test</span></span><span>&nbsp;</span></p></li></ol></div><div class="ListContainerWrapper"><ul><li data-aria-level="2" data-aria-posinset="2" aria-setsize="-1" data-listid="1" data-leveltext="▫"><p><span><span>Test</span></span><span>&nbsp;</span></p></li></ul></div></body></html>'
);

doc.querySelectorAll('ul li').forEach(el => {
const dataLevelText = el.getAttribute('data-leveltext');
if (dataLevelText) {
expect((el as HTMLElement).style.listStyleType).toContain(dataLevelText);
}
});

const orderedLists = doc.querySelectorAll('ol');
expect(orderedLists.length).toBe(2);
expect(orderedLists[0].start).toBe(1);
expect(orderedLists[1].start).toBe(5);
});

it('Keep the start property on lists and remove marker style that is not reusable', () => {
const notUsableMarker = String.fromCharCode(10);
const doc = sanitizeContent(
`<html><body><div><p><span><span>Test</span></span><span>&nbsp;</span></p></div><div class="ListContainerWrapper"><ol start="1"><li data-aria-level="1" data-aria-posinset="1" aria-setsize="-1" data-listid="1" data-leveltext="%1."><p><span><span>Test</span></span><span>&nbsp;</span></p></li></ol></div><div class="ListContainerWrapper"><ul><li data-aria-level="2" data-aria-posinset="1" aria-setsize="-1" data-listid="1" data-leveltext="${notUsableMarker}"><p><span><span>Test</span></span><span>&nbsp;</span></p></li></ul></div><div><p><span><span>Test</span></span><span>&nbsp;</span></p></div><div class="ListContainerWrapper"><ol start="2"><li data-aria-level="1" data-aria-posinset="2" aria-setsize="-1" data-listid="1" data-leveltext="%1."><p><span><span>Test</span></span><span>&nbsp;</span></p></li></ol></div><div class="ListContainerWrapper"><ul><li data-aria-level="2" data-aria-posinset="2" aria-setsize="-1" data-listid="1" data-leveltext="${notUsableMarker}"><p><span><span>Test</span></span><span>&nbsp;</span></p></li></ul></div></body></html>`
);

doc.querySelectorAll('ul li').forEach(el => {
const dataLevelText = el.getAttribute('data-leveltext');
if (dataLevelText) {
expect((el as HTMLElement).style.listStyleType).not.toContain(dataLevelText);
}
});

const orderedLists = doc.querySelectorAll('ol');
expect(orderedLists.length).toBe(2);
expect(orderedLists[0].start).toBe(1);
expect(orderedLists[1].start).toBe(2);
});
});

function sanitizeContent(html: string) {
const doc = new DOMParser().parseFromString(html, 'text/html');
const fragment = doc.createDocumentFragment();
while (doc.body.firstChild) {
fragment.appendChild(doc.body.firstChild);
}

convertPastedContentFromWordOnline(fragment);

while (fragment.firstChild) {
doc.body.appendChild(fragment.firstChild);
}
return doc;
}