Skip to content

Commit

Permalink
Merge pull request #593 from microsoft/u/felipediaz/autodetect-clear-…
Browse files Browse the repository at this point in the history
…format

Add Autodetect strategy to clearFormat API
  • Loading branch information
Lufedi authored Jul 20, 2021
2 parents cfb8d65 + a5413bf commit c0f8d8e
Show file tree
Hide file tree
Showing 6 changed files with 369 additions and 114 deletions.
13 changes: 10 additions & 3 deletions demo/scripts/controls/ribbon/ribbonButtons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import MainPaneBase from '../MainPaneBase';
import renderInsertLinkDialog from './renderInsertLinkDialog';
import renderTableOptions from './renderTableOptions';
import RibbonButtonType from './RibbonButtonType';
import { Alignment, Direction, Indentation } from 'roosterjs-editor-types';
import { Alignment, ClearFormatMode, Direction, Indentation } from 'roosterjs-editor-types';
import { Browser } from 'roosterjs-editor-dom';
import { getDarkColor } from 'roosterjs-color-utils';
import {
Expand All @@ -23,7 +23,6 @@ import {
toggleSubscript,
toggleStrikethrough,
setDirection,
clearBlockFormat,
clearFormat,
toggleHeader,
toggleCodeBlock,
Expand Down Expand Up @@ -302,8 +301,16 @@ const buttons: { [key: string]: RibbonButtonType } = {
clearFormat: {
title: 'Remove formatting',
image: require('../svg/removeformat.svg'),
onClick: (editor, key) => (key == 'block' ? clearBlockFormat(editor) : clearFormat(editor)),
onClick: (editor, key) => {
const handlers: Record<string, ClearFormatMode> = {
autodetect: ClearFormatMode.AutoDetect,
block: ClearFormatMode.Block,
selection: ClearFormatMode.Inline,
};
clearFormat(editor, handlers[key]);
},
dropDownItems: {
autodetect: 'Remove format (Autodetect)',
selection: 'Remove formatting of selected text',
block: 'Remove formatting of selected paragraphs',
},
Expand Down
108 changes: 4 additions & 104 deletions packages/roosterjs-editor-api/lib/format/clearBlockFormat.ts
Original file line number Diff line number Diff line change
@@ -1,110 +1,10 @@
import blockFormat from '../utils/blockFormat';
import { IEditor } from 'roosterjs-editor-types';
import {
collapseNodesInRegion,
getSelectedBlockElementsInRegion,
getStyles,
getTagOfNode,
isBlockElement,
isNodeInRegion,
isVoidHtmlElement,
safeInstanceOf,
setStyles,
splitBalancedNodeRange,
toArray,
unwrap,
wrap,
} from 'roosterjs-editor-dom';

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(
','
);
const ATTRIBUTES_TO_PRESERVE = ['href', 'src'];
const TAGS_TO_STOP_UNWRAP = ['TD', 'TH', 'TR', 'TABLE', 'TBODY', 'THEAD'];
import clearFormat from './clearFormat';
import { ClearFormatMode, IEditor } from 'roosterjs-editor-types';

/**
* Clear all formats of selected blocks.
* When selection is collapsed, only clear format of current block.
* @deprecated Use clearFormat instead and pass the ClearFormatMode.Block as parameter
* @param editor The editor instance
*/
export default function clearBlockFormat(editor: IEditor) {
blockFormat(editor, region => {
const blocks = getSelectedBlockElementsInRegion(region);
let nodes = collapseNodesInRegion(region, blocks);

if (editor.contains(region.rootNode)) {
// If there are styles on table cell, wrap all its children and move down all non-border styles.
// So that we can preserve styles for unselected blocks as well as border styles for table
const nonborderStyles = removeNonBorderStyles(region.rootNode);
if (Object.keys(nonborderStyles).length > 0) {
const wrapper = wrap(toArray(region.rootNode.childNodes));
setStyles(wrapper, nonborderStyles);
}
}

while (nodes.length > 0 && isNodeInRegion(region, nodes[0].parentNode)) {
nodes = [splitBalancedNodeRange(nodes)];
}

nodes.forEach(clearNodeFormat);
});
}

function clearNodeFormat(node: Node): boolean {
// 1. Recursively clear format of all its child nodes
const areBlockElements = toArray(node.childNodes).map(clearNodeFormat);
let areAllChildrenBlock = areBlockElements.every(b => b);
let returnBlockElement = isBlockElement(node);

// 2. Unwrap the tag if necessary
const tag = getTagOfNode(node);
if (tag) {
if (
TAGS_TO_UNWRAP.indexOf(tag) >= 0 ||
(areAllChildrenBlock &&
!isVoidHtmlElement(node) &&
TAGS_TO_STOP_UNWRAP.indexOf(tag) < 0)
) {
if (returnBlockElement && !areAllChildrenBlock) {
wrap(node);
}
unwrap(node);
} else {
// 3. Otherwise, remove all attributes
clearAttribute(node as HTMLElement);
}
}

return returnBlockElement;
}

function clearAttribute(element: HTMLElement) {
const isTableCell = safeInstanceOf(element, 'HTMLTableCellElement');

for (let attr of toArray(element.attributes)) {
if (isTableCell && attr.name == 'style') {
removeNonBorderStyles(element);
} else if (
ATTRIBUTES_TO_PRESERVE.indexOf(attr.name.toLowerCase()) < 0 &&
attr.name.indexOf('data-') != 0
) {
element.removeAttribute(attr.name);
}
}
}

function removeNonBorderStyles(element: HTMLElement): Record<string, string> {
const styles = getStyles(element);
const result: Record<string, string> = {};

Object.keys(styles).forEach(name => {
if (name.indexOf('border') < 0) {
result[name] = styles[name];
delete styles[name];
}
});

setStyles(element, styles);

return result;
clearFormat(editor, ClearFormatMode.Block);
}
179 changes: 174 additions & 5 deletions packages/roosterjs-editor-api/lib/format/clearFormat.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,168 @@
import blockFormat from '../utils/blockFormat';
import execCommand from '../utils/execCommand';
import PartialInlineElement from 'roosterjs-editor-dom/lib/inlineElements/PartialInlineElement';
import setBackgroundColor from './setBackgroundColor';
import setFontName from './setFontName';
import setFontSize from './setFontSize';
import setTextColor from './setTextColor';
import toggleBold from './toggleBold';
import toggleItalic from './toggleItalic';
import toggleUnderline from './toggleUnderline';
import { ChangeSource, DocumentCommand, IEditor, QueryScope } from 'roosterjs-editor-types';
import {
ChangeSource,
ClearFormatMode,
DocumentCommand,
IEditor,
QueryScope,
} from 'roosterjs-editor-types';
import {
collapseNodesInRegion,
getSelectedBlockElementsInRegion,
getStyles,
getTagOfNode,
isBlockElement,
isNodeInRegion,
isVoidHtmlElement,
safeInstanceOf,
setStyles,
splitBalancedNodeRange,
toArray,
unwrap,
wrap,
} from 'roosterjs-editor-dom';

const STYLES_TO_REMOVE = ['font', 'text-decoration', 'color', 'background'];
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(
','
);
const ATTRIBUTES_TO_PRESERVE = ['href', 'src'];
const TAGS_TO_STOP_UNWRAP = ['TD', 'TH', 'TR', 'TABLE', 'TBODY', 'THEAD'];

/**
* Clear the format in current selection, after cleaning, the format will be
* changed to default format. The format that get cleaned include B/I/U/font name/
* font size/text color/background color/align left/align right/align center/superscript/subscript
* @param editor The editor instance
* @returns if the current selection is composed of two or more block elements
*/
function isMultiBlockSelection(editor: IEditor): boolean {
let transverser = editor.getSelectionTraverser();
let blockElement = transverser.currentBlockElement;
if (!blockElement) {
return false;
}

let nextBlockElement = transverser.getNextBlockElement();

//At least two blocks are selected
return !!nextBlockElement;
}

function clearNodeFormat(node: Node): boolean {
// 1. Recursively clear format of all its child nodes
const areBlockElements = toArray(node.childNodes).map(clearNodeFormat);
let areAllChildrenBlock = areBlockElements.every(b => b);
let returnBlockElement = isBlockElement(node);

// 2. Unwrap the tag if necessary
const tag = getTagOfNode(node);
if (tag) {
if (
TAGS_TO_UNWRAP.indexOf(tag) >= 0 ||
(areAllChildrenBlock &&
!isVoidHtmlElement(node) &&
TAGS_TO_STOP_UNWRAP.indexOf(tag) < 0)
) {
if (returnBlockElement && !areAllChildrenBlock) {
wrap(node);
}
unwrap(node);
} else {
// 3. Otherwise, remove all attributes
clearAttribute(node as HTMLElement);
}
}

return returnBlockElement;
}

function clearAttribute(element: HTMLElement) {
const isTableCell = safeInstanceOf(element, 'HTMLTableCellElement');

for (let attr of toArray(element.attributes)) {
if (isTableCell && attr.name == 'style') {
removeNonBorderStyles(element);
} else if (
ATTRIBUTES_TO_PRESERVE.indexOf(attr.name.toLowerCase()) < 0 &&
attr.name.indexOf('data-') != 0
) {
element.removeAttribute(attr.name);
}
}
}

function removeNonBorderStyles(element: HTMLElement): Record<string, string> {
const styles = getStyles(element);
const result: Record<string, string> = {};

Object.keys(styles).forEach(name => {
if (name.indexOf('border') < 0) {
result[name] = styles[name];
delete styles[name];
}
});

setStyles(element, styles);

return result;
}

/**
* Clear the format of the selected text or list of blocks
* If the current selection is compose of multiple block elements then remove the text and struture format for all the selected blocks
* If the current selection is compose of a partial inline element then only the text format is removed from the current selection
* @param editor The editor instance
*/
function clearAutoDetectFormat(editor: IEditor) {
const isMultiBlock = isMultiBlockSelection(editor);
if (!isMultiBlock) {
const transverser = editor.getSelectionTraverser();
const inlineElement = transverser.currentInlineElement;
const isPartial = inlineElement instanceof PartialInlineElement;
if (isPartial) {
clearFormat(editor);
return;
}
}
clearBlockFormat(editor);
}

/**
* Clear all formats of selected blocks.
* When selection is collapsed, only clear format of current block.
* @param editor The editor instance
*/
export default function clearFormat(editor: IEditor) {
function clearBlockFormat(editor: IEditor) {
blockFormat(editor, region => {
const blocks = getSelectedBlockElementsInRegion(region);
let nodes = collapseNodesInRegion(region, blocks);

if (editor.contains(region.rootNode)) {
// If there are styles on table cell, wrap all its children and move down all non-border styles.
// So that we can preserve styles for unselected blocks as well as border styles for table
const nonborderStyles = removeNonBorderStyles(region.rootNode);
if (Object.keys(nonborderStyles).length > 0) {
const wrapper = wrap(toArray(region.rootNode.childNodes));
setStyles(wrapper, nonborderStyles);
}
}

while (nodes.length > 0 && isNodeInRegion(region, nodes[0].parentNode)) {
nodes = [splitBalancedNodeRange(nodes)];
}

nodes.forEach(clearNodeFormat);
});
}

function clearInlineFormat(editor: IEditor) {
editor.focus();
editor.addUndoSnapshot(() => {
execCommand(editor, DocumentCommand.RemoveFormat);
Expand Down Expand Up @@ -70,3 +216,26 @@ export default function clearFormat(editor: IEditor) {
}
}, ChangeSource.Format);
}

/**
* Clear the format in current selection, after cleaning, the format will be
* changed to default format. The format that get cleaned include B/I/U/font name/
* font size/text color/background color/align left/align right/align center/superscript/subscript
* @param editor The editor instance
* @param formatType type of format to apply
*/
export default function clearFormat(
editor: IEditor,
formatType: ClearFormatMode = ClearFormatMode.Inline
) {
switch (formatType) {
case ClearFormatMode.Inline:
clearInlineFormat(editor);
break;
case ClearFormatMode.Block:
clearBlockFormat(editor);
break;
default:
clearAutoDetectFormat(editor);
}
}
Loading

0 comments on commit c0f8d8e

Please sign in to comment.