-
Notifications
You must be signed in to change notification settings - Fork 161
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
de7e9db
commit a7c9886
Showing
6 changed files
with
259 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
146 changes: 146 additions & 0 deletions
146
packages/roosterjs-editor-api/lib/format/clearBlockFormat.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
99 changes: 99 additions & 0 deletions
99
packages/roosterjs-editor-api/lib/test/format/clearBlockFormatTest.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>' | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters