Skip to content

Commit

Permalink
Merge branch 'master' into u/juliaroldi/shadow-dom-image-wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
juliaroldi authored Jan 23, 2023
2 parents dcd1e6b + 49944bf commit 6795fc4
Show file tree
Hide file tree
Showing 27 changed files with 1,781 additions and 422 deletions.
4 changes: 4 additions & 0 deletions demo/scripts/controls/MainPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ContentModelRibbon from './ribbonButtons/contentModel/ContentModelRibbon'
import EditorOptionsPlugin from './sidePane/editorOptions/EditorOptionsPlugin';
import EventViewPlugin from './sidePane/eventViewer/EventViewPlugin';
import ExperimentalContentModelEditor from './editor/ExperimentalContentModelEditor';
import FormatPainterPlugin from './contentModel/plugins/FormatPainterPlugin';
import FormatStatePlugin from './sidePane/formatState/FormatStatePlugin';
import getToggleablePlugins from './getToggleablePlugins';
import MainPaneBase from './MainPaneBase';
Expand Down Expand Up @@ -128,6 +129,7 @@ class MainPane extends MainPaneBase {
private updateContentPlugin: UpdateContentPlugin;
private toggleablePlugins: EditorPlugin[] | null = null;
private contentModelPlugin: ContentModelPlugin;
private formatPainterPlugin: FormatPainterPlugin;
private mainWindowButtons: RibbonButton<RibbonStringKeys>[];
private popoutWindowButtons: RibbonButton<RibbonStringKeys>[];

Expand All @@ -150,6 +152,7 @@ class MainPane extends MainPaneBase {
this.emojiPlugin = createEmojiPlugin();
this.updateContentPlugin = createUpdateContentPlugin(UpdateMode.OnDispose, this.onUpdate);
this.contentModelPlugin = new ContentModelPlugin();
this.formatPainterPlugin = new FormatPainterPlugin();
this.mainWindowButtons = getButtons([
...AllButtonKeys,
darkMode,
Expand Down Expand Up @@ -437,6 +440,7 @@ class MainPane extends MainPaneBase {
this.pasteOptionPlugin,
this.emojiPlugin,
this.contentModelPlugin,
this.formatPainterPlugin,
];

if (this.state.showSidePane || this.state.popoutWindow) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { FormatView } from '../format/FormatView';
import { MetadataView } from '../format/MetadataView';
import { PaddingFormatRenderer } from '../format/formatPart/PaddingFormatRenderer';
import { TableCellMetadataFormatRender } from '../format/formatPart/TableCellMetadataFormatRender';
import { TextColorFormatRenderer } from '../format/formatPart/TextColorFormatRenderer';
import { updateTableCellMetadata } from 'roosterjs-content-model/lib/modelApi/metadata/updateTableCellMetadata';
import { useProperty } from '../../hooks/useProperty';
import { VerticalAlignFormatRenderer } from '../format/formatPart/VerticalAlignFormatRenderer';
Expand All @@ -30,6 +31,7 @@ const TableCellFormatRenderers: FormatRenderer<ContentModelTableCellFormat>[] =
PaddingFormatRenderer,
VerticalAlignFormatRenderer,
WordBreakFormatRenderer,
TextColorFormatRenderer,
];

export function ContentModelTableCellView(props: { cell: ContentModelTableCell }) {
Expand Down
71 changes: 71 additions & 0 deletions demo/scripts/controls/contentModel/plugins/FormatPainterPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { EditorPlugin, IEditor, PluginEvent, PluginEventType } from 'roosterjs-editor-types';
import {
applySegmentFormat,
ContentModelSegmentFormat,
getSegmentFormat,
IExperimentalContentModelEditor,
} from 'roosterjs-content-model';

const FORMATPAINTERCURSOR_SVG = require('./formatpaintercursor.svg');
const FORMATPAINTERCURSOR_STYLE = `;cursor: url("${FORMATPAINTERCURSOR_SVG}") 8.5 16, auto`;
const CURSOR_REGEX = /;?\s*cursor:\s*url\(\".*?\"\)[^;]*/gi;

interface FormatPainterFormatHolder {
format: ContentModelSegmentFormat | null;
}

export default class FormatPainterPlugin implements EditorPlugin {
private editor: IExperimentalContentModelEditor | null = null;

getName() {
return 'FormatPainter';
}

initialize(editor: IEditor) {
this.editor = editor as IExperimentalContentModelEditor;
}

dispose() {
this.editor = null;
}

onPluginEvent(event: PluginEvent) {
if (this.editor && event.eventType == PluginEventType.MouseUp) {
const formatHolder = getFormatHolder(this.editor);

if (formatHolder.format) {
applySegmentFormat(this.editor, formatHolder.format);
formatHolder.format = null;

setFormatPainterCursor(this.editor, false /*isOn*/);
}
}
}

static startFormatPainter(editor: IExperimentalContentModelEditor) {
const formatHolder = getFormatHolder(editor);
const format = getSegmentFormat(editor);

if (format) {
formatHolder.format = { ...format };
setFormatPainterCursor(editor, true /*isOn*/);
}
}
}

function getFormatHolder(editor: IEditor): FormatPainterFormatHolder {
return editor.getCustomData('__FormatPainterFormat', () => {
return {} as FormatPainterFormatHolder;
});
}

function setFormatPainterCursor(editor: IEditor, isOn: boolean) {
let styles = editor.getEditorDomAttribute('style') || '';
styles = styles.replace(CURSOR_REGEX, '');

if (isOn) {
styles += FORMATPAINTERCURSOR_STYLE;
}

editor.setEditorDomAttribute('style', styles);
}
22 changes: 22 additions & 0 deletions demo/scripts/controls/contentModel/plugins/formatpaintercursor.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { decreaseFontSizeButton } from './decreaseFontSizeButton';
import { decreaseIndentButton } from './decreaseIndentButton';
import { fontButton } from './fontButton';
import { fontSizeButton } from './fontSizeButton';
import { formatPainterButton } from './formatPainterButton';
import { formatTableButton } from './formatTableButton';
import { increaseFontSizeButton } from './increaseFontSizeButton';
import { increaseIndentButton } from './increaseIndentButton';
Expand Down Expand Up @@ -44,6 +45,7 @@ import {
} from './tableEditButtons';

const buttons = [
formatPainterButton,
boldButton,
italicButton,
underlineButton,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import FormatPainterPlugin from '../../contentModel/plugins/FormatPainterPlugin';
import isContentModelEditor from '../../editor/isContentModelEditor';
import { RibbonButton } from 'roosterjs-react';

/**
* @internal
* "Format Painter" button on the format ribbon
*/
export const formatPainterButton: RibbonButton<'formatPainter'> = {
key: 'formatPainter',
unlocalizedText: 'Format painter',
iconName: 'Brush',
onClick: editor => {
if (isContentModelEditor(editor)) {
FormatPainterPlugin.startFormatPainter(editor);
}
return true;
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { contains } from 'roosterjs-editor-dom';
import { ContentModelBlockGroup } from '../../publicTypes/group/ContentModelBlockGroup';
import { DomToModelContext } from '../../publicTypes/context/DomToModelContext';
import { getRegularSelectionOffsets } from '../utils/getRegularSelectionOffsets';
import { handleRegularSelection, processChildNode } from './childProcessor';

interface FormatStateContext extends DomToModelContext {
/**
* An optional stack of parent elements to process. When provided, the child nodes of current parent element will be ignored,
* but use the top element in this stack instead in childProcessor.
*/
nodeStack?: Node[];
}

/**
* @internal
* In order to get format, we can still use the regular child processor. However, to improve performance, we don't need to create
* content model for the whole doc, instead we only need to traverse the tree path that can arrive current selected node.
* This "reduced" child processor will first create a node stack that stores DOM node from root to current common ancestor node of selection,
* then use this stack as a faked DOM tree to create a reduced content model which we can use to retrieve format state
*/
export function reducedModelChildProcessor(
group: ContentModelBlockGroup,
parent: ParentNode,
context: FormatStateContext
) {
if (context.selectionRootNode) {
if (!context.nodeStack) {
context.nodeStack = createNodeStack(parent, context.selectionRootNode);
}

const stackChild = context.nodeStack.pop();

if (stackChild) {
const [nodeStartOffset, nodeEndOffset] = getRegularSelectionOffsets(context, parent);

// If selection is not on this node, skip getting node index to save some time since we don't need it here
const index =
nodeStartOffset >= 0 || nodeEndOffset >= 0 ? getChildIndex(parent, stackChild) : -1;

if (index >= 0) {
handleRegularSelection(index, context, group, nodeStartOffset, nodeEndOffset);
}

processChildNode(group, stackChild, context);

if (index >= 0) {
handleRegularSelection(index + 1, context, group, nodeStartOffset, nodeEndOffset);
}
} else {
// No child node from node stack, that means we have reached the deepest node of selection.
// Now we can use default child processor to perform full sub tree scanning for content model,
// So that all selected node will be included.
context.defaultElementProcessors.child(group, parent, context);
}
}
}

function createNodeStack(root: Node, startNode: Node): Node[] {
const result: Node[] = [];
let node: Node | null = startNode;

while (node && contains(root, node)) {
result.push(node);
node = node.parentNode;
}

return result;
}

function getChildIndex(parent: ParentNode, stackChild: Node) {
let index = 0;
let child = parent.firstChild;

while (child && child != stackChild) {
index++;
child = child.nextSibling;
}
return index;
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export const tableProcessor: ElementProcessor<HTMLTableElement> = (
);
parseFormat(
td,
context.formatParsers.segmentOnBlock,
context.formatParsers.segmentOnTableCell,
context.segmentFormat,
context
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ const defaultFormatKeysPerCategory: {
'backgroundColor',
],
segmentOnBlock: ['fontFamily', 'fontSize', 'underline', 'italic', 'bold', 'textColor'],
segmentOnTableCell: ['fontFamily', 'fontSize', 'underline', 'italic', 'bold'],
tableCell: [
'border',
'borderBox',
Expand All @@ -114,6 +115,7 @@ const defaultFormatKeysPerCategory: {
'direction',
'verticalAlign',
'wordBreak',
'textColor',
],
table: [
'id',
Expand Down
2 changes: 2 additions & 0 deletions packages/roosterjs-content-model/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export { default as setFontName } from './publicApi/segment/setFontName';
export { default as setFontSize } from './publicApi/segment/setFontSize';
export { default as setTextColor } from './publicApi/segment/setTextColor';
export { default as changeFontSize } from './publicApi/segment/changeFontSize';
export { default as applySegmentFormat } from './publicApi/segment/applySegmentFormat';
export { default as changeCapitalization } from './publicApi/segment/changeCapitalization';
export { default as insertImage } from './publicApi/insert/insertImage';
export { default as setListStyle } from './publicApi/list/setListStyle';
Expand All @@ -30,6 +31,7 @@ export { default as setDirection } from './publicApi/block/setDirection';
export { default as setHeaderLevel } from './publicApi/block/setHeaderLevel';
export { default as toggleBlockQuote } from './publicApi/block/toggleBlockQuote';
export { default as getFormatState } from './publicApi/format/getFormatState';
export { default as getSegmentFormat } from './publicApi/format/getSegmentFormat';
export { default as clearFormat } from './publicApi/format/clearFormat';
export { default as insertLink } from './publicApi/link/insertLink';
export { default as removeLink } from './publicApi/link/removeLink';
Expand Down
25 changes: 23 additions & 2 deletions packages/roosterjs-content-model/lib/modelApi/common/mergeModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,25 @@ import { normalizeTable } from '../table/normalizeTable';

/**
* @internal
* Options to specify how to merge models
*/
export function mergeModel(target: ContentModelDocument, source: ContentModelDocument) {
export interface MergeModelOption {
/**
* When there is only a table to merge, whether merge this table into current table (if any), or just directly insert (nested table).
* This is usually used when paste table inside a table
* @default false
*/
mergeTable?: boolean;
}

/**
* @internal
*/
export function mergeModel(
target: ContentModelDocument,
source: ContentModelDocument,
options?: MergeModelOption
) {
const insertPosition = deleteSelection(target);

if (insertPosition) {
Expand All @@ -35,7 +52,11 @@ export function mergeModel(target: ContentModelDocument, source: ContentModelDoc
break;

case 'Table':
mergeTable(insertPosition, block, source);
if (source.blocks.length == 1 && options?.mergeTable) {
mergeTable(insertPosition, block, source);
} else {
insertBlock(insertPosition, block);
}
break;

case 'BlockGroup':
Expand Down
Loading

0 comments on commit 6795fc4

Please sign in to comment.