Skip to content

Commit

Permalink
New API: clearBlockFormat (#178)
Browse files Browse the repository at this point in the history
* clearFormat 2

* handle list

* update

* fix bug

* improve

* improve

* improve

* improve

* fix build

* add test case

* remove unnecessary change

* Fix comments

* 6.19.0 Support clearBlockFormat
  • Loading branch information
JiuqingSong authored Dec 12, 2018
1 parent de7e9db commit a7c9886
Show file tree
Hide file tree
Showing 6 changed files with 259 additions and 1 deletion.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "roosterjs",
"version": "6.18.1",
"version": "6.19.0",
"description": "Framework-independent javascript editor",
"repository": {
"type": "git",
Expand Down
146 changes: 146 additions & 0 deletions packages/roosterjs-editor-api/lib/format/clearBlockFormat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import collapseSelectedBlocks from '../utils/collapseSelectedBlocks';
import { Editor } from 'roosterjs-editor-core';
import {
getTagOfNode,
isBlockElement,
unwrap,
wrap,
splitBalancedNodeRange,
} from 'roosterjs-editor-dom';
import { ChangeSource, NodeType } from 'roosterjs-editor-types';

export const TAGS_TO_UNWRAP = 'B,I,U,STRONG,EM,SUB,SUP,STRIKE,FONT,CENTER,H1,H2,H3,H4,H5,H6,UL,OL,LI,SPAN,P,BLOCKQUOTE,CODE,S,PRE'.split(
','
);
export const TAGS_TO_STOP_UNWRAP = ['TD', 'TH', 'TR', 'TABLE', 'TBODY', 'THEAD'];
export const ATTRIBUTES_TO_PRESERVE = ['href'];

/**
* Clear all formats of selected blocks.
* When selection is collapsed, only clear format of current block.
* @param editor The editor instance
* @param tagsToUnwrap Optional. A string array contains HTML tags in upper case which we will unwrap when clear format
* @param tagsToStopUnwrap Optional. A string array contains HTML tags in upper case which we will stop unwrap if these tags are hit
*/
export default function clearBlockFormat(
editor: Editor,
tagsToUnwrap: string[] = TAGS_TO_UNWRAP,
tagsToStopUnwrap: string[] = TAGS_TO_STOP_UNWRAP,
attributesToPreserve: string[] = ATTRIBUTES_TO_PRESERVE
) {
editor.focus();
editor.addUndoSnapshot((start, end) => {
let groups: {
first?: HTMLElement;
last?: HTMLElement;
td?: HTMLElement;
}[] = [{}];
let stopUnwrapSelector = tagsToStopUnwrap.join(',');

// 1. Collapse the selected blocks and get first and last element
collapseSelectedBlocks(editor, element => {
let group = groups[groups.length - 1];
let td = editor.getElementAtCursor(stopUnwrapSelector, element);
if (td != group.td && group.first) {
groups.push((group = {}));
}

group.td = td;
group.first = group.first || element;
group.last = element;
});

groups.filter(group => group.first).forEach(group => {
// 2. Collapse with first and last element to make them under same parent
let nodes = editor.collapseNodes(group.first, group.last, true /*canSplitParent*/);

// 3. Continue collapse until we can't collapse any more (hit root node, or a table)
if (canCollapse(tagsToStopUnwrap, nodes[0])) {
while (
editor.contains(nodes[0].parentNode) &&
canCollapse(tagsToStopUnwrap, nodes[0].parentNode as HTMLElement)
) {
nodes = [splitBalancedNodeRange(nodes)];
}
}

// 4. Clear formats of the nodes
nodes.forEach(node =>
clearNodeFormat(
node as HTMLElement,
tagsToUnwrap,
tagsToStopUnwrap,
attributesToPreserve
)
);

// 5. Clear CSS of container TD if exist
if (group.td) {
let styles = group.td.getAttribute('style') || '';
let styleArray = styles.split(';');
styleArray = styleArray.filter(
style =>
style
.trim()
.toLowerCase()
.indexOf('border') == 0
);
styles = styleArray.join(';');
if (styles) {
group.td.setAttribute('style', styles);
} else {
group.td.removeAttribute('style');
}
}
});

editor.select(start, end);
}, ChangeSource.Format);
}

function clearNodeFormat(
node: Node,
tagsToUnwrap: string[],
tagsToStopUnwrap: string[],
attributesToPreserve: string[]
): boolean {
if (node.nodeType != NodeType.Element) {
return false;
}

// 1. Recursively clear format of all its child nodes
let allChildrenAreBlock = ([].slice.call(node.childNodes) as Node[])
.map(n => clearNodeFormat(n, tagsToUnwrap, tagsToStopUnwrap, attributesToPreserve))
.reduce((previousValue, value) => previousValue && value, true);

if (!canCollapse(tagsToStopUnwrap, node)) {
return false;
}

let returnBlockElement = isBlockElement(node);

// 2. If we should unwrap this tag, put it into an array and unwrap it later
if (tagsToUnwrap.indexOf(getTagOfNode(node)) >= 0 || allChildrenAreBlock) {
if (returnBlockElement && !allChildrenAreBlock) {
wrap(node);
}
unwrap(node);
} else {
// 3. Otherwise, remove all attributes
clearAttribute(node as HTMLElement, attributesToPreserve);
}

return returnBlockElement;
}

function clearAttribute(element: HTMLElement, attributesToPreserve: string[]) {
for (let attr of [].slice.call(element.attributes) as Attr[]) {
if (attributesToPreserve.indexOf(attr.name.toLowerCase()) < 0) {
element.removeAttribute(attr.name);
}
}
}

function canCollapse(tagsToStopUnwrap: string[], node: Node) {
return tagsToStopUnwrap.indexOf(getTagOfNode(node)) < 0;
}
6 changes: 6 additions & 0 deletions packages/roosterjs-editor-api/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export { default as changeFontSize, FONT_SIZES } from './format/changeFontSize';
export {
default as clearBlockFormat,
TAGS_TO_UNWRAP,
TAGS_TO_STOP_UNWRAP,
ATTRIBUTES_TO_PRESERVE,
} from './format/clearBlockFormat';
export { default as clearFormat } from './format/clearFormat';
export { default as createLink } from './format/createLink';
export { default as getFormatState } from './format/getFormatState';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import * as TestHelper from '../TestHelper';
import clearBlockFormat from '../../format/clearBlockFormat';
import { Editor } from 'roosterjs-editor-core';

describe('clearFormat()', () => {
let testID = 'clearFormat';
let editor: Editor;

beforeEach(() => {
editor = TestHelper.initEditor(testID);
});

afterEach(() => {
editor.dispose();
TestHelper.removeElement(testID);
});

function runTest(source: string, expected: string) {
editor.setContent(source);
clearBlockFormat(editor);
let result = editor.getContent();
expect(result).toBe(expected);
}

it('Empty', () => {
runTest('', '');
});

it('BIU', () => {
// The selection path like {"start":[0,1,1,0,0],"end":[0,2,0,5]} is generated from the "Take snapshot" functionality of sample site
runTest(
'<div><b>This <i>is</i></b><i> <u>a</u></i><u> test</u></div><!--{"start":[0,1,1,0,0],"end":[0,2,0,5]}-->',
'<div>This is a test</div>'
);
});

it('Hyperlink', () => {
runTest(
'<div>This is a <a href="http://contoso.com" title="http://contoso.com">test</a></div><!--{"start":[0,1,0,0],"end":[0,1,0,4]}-->',
'<div>This is a <a href="http://contoso.com">test</a></div>'
);
});

it('Fonts', () => {
runTest(
'<div><span style="font-size: 36pt;">Thi</span><span style="font-size: 36pt; color: rgb(117, 123, 128);">s </span><span style="color: rgb(117, 123, 128);">i</span><span style="color: rgb(117, 123, 128); background-color: rgb(0, 0, 255);">s</span><span style="background-color: rgb(0, 0, 255);"> a </span><span style="background-color: rgb(0, 0, 255); font-family: Arial;">t</span><span style="font-family: Arial;">est</span></div><!--{"start":[0,6,0,3],"end":[0,6,0,3]}-->',
'<div>This is a test</div>'
);
});

it('Super/Subscripts', () => {
runTest(
'<div><sup>This</sup> is <sub>a</sub> test</div><!--{"start":[0,3,5],"end":[0,3,5]}-->',
'<div>This is a test</div>'
);
});

it('Multi lines', () => {
runTest(
'<div>This</div><div>is<br>a</div><div>test</div><!--{"start":[0,0,0],"end":[2,0,4]}-->',
'<div>This</div><div>is</div><div>a</div><div>test</div>'
);
});

it('List - select all', () => {
runTest(
'<div>This</div><div><ul><li>is</li><ul><li>a</li><li>test</li></ul></ul></div><!--{"start":[0,0,0],"end":[1,0,1,1,0,4]}-->',
'<div>This</div><div>is</div><div>a</div><div>test</div>'
);
});

it('List - select partial', () => {
runTest(
'<div>This</div><div><ul><li>is</li><ul><li>a</li><li>test</li></ul></ul></div><!--{"start":[1,0,1,0,0,0],"end":[1,0,1,0,0,0]}-->',
'<div>This</div><div><ul><li>is</li></ul></div><div>a</div><div><ul><ul><li>test</li></ul></ul></div>'
);
});

it('List in table - select partial', () => {
runTest(
'<div><table><tr><td>This</td><td>is<br><ul><li>a</li><ul><li>test</li></ul></ul></td></tr></table><br></div><!--{"start":[0,0,0,1,2,1,0,0,2],"end":[0,0,0,1,2,1,0,0,2]}-->',
'<div><table><tbody><tr><td>This</td><td>is<br><ul><li>a</li></ul><div>test</div></td></tr></tbody></table><br></div>'
);
});

it('List in table - select cross cell', () => {
runTest(
'<div><table><tbody><tr><td>This</td><td>is<br><ul><li>a</li><ul><li>test</li></ul></ul></td></tr></tbody></table><br></div><!--{"start":[0,0,0,0,0,0,2],"end":[0,0,0,0,1,2,0,0,1]}-->',
'<div><table><tbody><tr><td>This</td><td><div>is</div><div>a</div><ul><ul><li>test</li></ul></ul></td></tr></tbody></table><br></div>'
);
});

it('Table has styles', () => {
runTest(
'<div><table><tr><td style="width: 120px;border-width: 1px;border-style: solid;border-color: rgb(171, 171, 171);font-size: 30px;">This is a test</td></tr></table><br></div><!--{"start":[0,0,0,0,0,14],"end":[0,0,0,0,0,14]}-->',
'<div><table><tbody><tr><td style="border-width: 1px;border-style: solid;border-color: rgb(171, 171, 171)">This is a test</td></tr></tbody></table><br></div>'
);
});
});
1 change: 1 addition & 0 deletions publish/samplesite/sample.htm
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@
</select>
&nbsp;
<button id="clearFormatButton">Clear Format</button>
<button id="clearBlockFormatButton">Clear Block Format</button>
&nbsp;
<button id="insertTable">Insert Table</button>
<select id="editTable" title="Edit table">
Expand Down
6 changes: 6 additions & 0 deletions publish/samplesite/scripts/initFormatBar.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
clearBlockFormat,
clearFormat,
createLink,
insertImage,
Expand Down Expand Up @@ -130,6 +131,11 @@ export default function initFormatBar() {
clearFormat(getCurrentEditor());
});

// ClearBlockFormat
document.getElementById('clearBlockFormatButton').addEventListener('click', function() {
clearBlockFormat(getCurrentEditor());
});

// Edit table
document.getElementById('editTable').addEventListener('change', function() {
let select = document.getElementById('editTable') as HTMLSelectElement;
Expand Down

0 comments on commit a7c9886

Please sign in to comment.