Skip to content
Merged
2 changes: 1 addition & 1 deletion demo/scripts/controlsV2/demoButtons/tableEditButtons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export const tableMergeButton: RibbonButton<
key: 'ribbonButtonTableMerge',
iconName: '',
unlocalizedText: 'Merge',
isDisabled: formatState => !formatState.isInTable,
isDisabled: formatState => !formatState.isInTable || !formatState.canMergeTableCell,
dropDownMenu: {
items: {
menuNameTableMergeAbove: 'Merge above',
Expand Down
4 changes: 4 additions & 0 deletions demo/scripts/controlsV2/mainPane/MainPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { getTabs, tabNames } from '../tabs/getTabs';
import { getTheme } from '../theme/themes';
import { MarkdownPanePlugin } from '../sidePane/MarkdownPane/MarkdownPanePlugin';
import { MarkdownPastePlugin } from 'roosterjs-content-model-markdown';
import { TextUtilitiesPanePlugin } from '../sidePane/TextUtilities/TextUtilitiesPanePlugin';
import { OptionState, UrlPlaceholder } from '../sidePane/editorOptions/OptionState';
import { popoutButton } from '../demoButtons/popoutButton';
import { PresetPlugin } from '../sidePane/presets/PresetPlugin';
Expand Down Expand Up @@ -116,6 +117,7 @@ export class MainPane extends React.Component<{}, MainPaneState> {
private samplePickerPlugin: SamplePickerPlugin;
private snapshots: Snapshots;
private markdownPanePlugin: MarkdownPanePlugin;
private textUtilitiesPanePlugin: TextUtilitiesPanePlugin;
private findReplacePlugin: FindReplacePlugin;
private findReplaceContext: FindReplaceContext;
private imageEditPlugin: ImageEditPlugin | undefined;
Expand Down Expand Up @@ -157,6 +159,7 @@ export class MainPane extends React.Component<{}, MainPaneState> {
this.formatPainterPlugin = new FormatPainterPlugin();
this.samplePickerPlugin = new SamplePickerPlugin();
this.markdownPanePlugin = new MarkdownPanePlugin();
this.textUtilitiesPanePlugin = new TextUtilitiesPanePlugin();

this.findReplaceContext = createFindReplaceContext();
this.findReplacePlugin = new FindReplacePlugin(this.findReplaceContext);
Expand Down Expand Up @@ -552,6 +555,7 @@ export class MainPane extends React.Component<{}, MainPaneState> {
this.contentModelPanePlugin,
this.presetPlugin,
this.markdownPanePlugin,
this.textUtilitiesPanePlugin,
];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.container {
flex-direction: column;
display: flex;
height: 100%;
}

.textArea {
flex: 1;
width: 100%;
box-sizing: border-box;
resize: none;
}

.errorMessage {
color: #a80000;
margin: 4px 0;
}

.buttons {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
157 changes: 157 additions & 0 deletions demo/scripts/controlsV2/sidePane/TextUtilities/TextUtilitiesPane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import * as React from 'react';
import {
createEmptyModel,
createFormatContainer,
createParagraph,
createText,
} from 'roosterjs-content-model-dom';
import type { TextUtilitiesPaneProps } from './TextUtilitiesPanePlugin';

const styles = require('./TextUtilitiesPane.scss');

interface TextUtilitiesPaneState {
errorMessage: string;
}

export default class TextUtilitiesPane extends React.Component<
TextUtilitiesPaneProps,
TextUtilitiesPaneState
> {
private text = React.createRef<HTMLTextAreaElement>();

constructor(props: TextUtilitiesPaneProps) {
super(props);
this.state = { errorMessage: '' };
}

private beautifyJson = () => {
this.applyTransformation(
value => JSON.stringify(JSON.parse(value), null, 2),
'Invalid JSON'
);
};

private encodeUriComponent = () => {
this.applyTransformation(
value => encodeURIComponent(value),
'Unable to encode URI component'
);
};

private decodeUriComponent = () => {
this.applyTransformation(value => decodeURIComponent(value), 'Invalid URI component');
};

private encodeBase64 = () => {
this.applyTransformation(value => encodeTextToBase64(value), 'Unable to encode Base64');
};

private decodeBase64 = () => {
this.applyTransformation(value => decodeBase64ToText(value), 'Invalid Base64');
};

private applyTransformation = (transform: (value: string) => string, fallbackError: string) => {
let result: string;

try {
result = transform(this.text.current.value);
} catch (error) {
this.setState({
errorMessage: error instanceof Error ? error.message : fallbackError,
});
return;
}

this.setEditorText(result);
};

private setEditorText = (text: string) => {
const container = createFormatContainer('pre');
const paragraph = createParagraph(true);

paragraph.segments.push(createText(text));
container.blocks.push(paragraph);
this.props.getEditor().formatContentModel(model => {
model.blocks = [container];
return true;
});
this.setState({ errorMessage: '' });
};

private clear = () => {
this.text.current.value = '';
this.setState({ errorMessage: '' });
};

private clearEditor = () => {
const editor = this.props.getEditor();
editor.formatContentModel(model => {
model.blocks = createEmptyModel().blocks;
return true;
});
};

render() {
return (
<div className={styles.container}>
<p>Enter text to transform and display in the editor.</p>
<textarea className={styles.textArea} title="Text input" ref={this.text} />
{this.state.errorMessage ? (
<div className={styles.errorMessage} role="alert">
{this.state.errorMessage}
</div>
) : null}
<div className={styles.buttons}>
<button type="button" onClick={this.beautifyJson}>
Beautify JSON
</button>
<button type="button" onClick={this.encodeUriComponent}>
Encode URI component
</button>
<button type="button" onClick={this.decodeUriComponent}>
Decode URI component
</button>
<button type="button" onClick={this.encodeBase64}>
Encode as Base64
</button>
<button type="button" onClick={this.decodeBase64}>
Decode from Base64
</button>
<button type="button" onClick={this.clear}>
Clear
</button>
<button type="button" onClick={this.clearEditor}>
Clear editor
</button>
</div>
</div>
);
}
}

function encodeTextToBase64(value: string): string {
const encodedValue = encodeURIComponent(value);
let binaryValue = '';

for (let i = 0; i < encodedValue.length; i++) {
if (encodedValue[i] == '%') {
binaryValue += String.fromCharCode(parseInt(encodedValue.substr(i + 1, 2), 16));
i += 2;
} else {
binaryValue += encodedValue[i];
}
}

return btoa(binaryValue);
}

function decodeBase64ToText(value: string): string {
const binaryValue = atob(value);
let encodedValue = '';

for (let i = 0; i < binaryValue.length; i++) {
encodedValue += '%' + ('0' + binaryValue.charCodeAt(i).toString(16)).slice(-2);
}

return decodeURIComponent(encodedValue);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import TextUtilitiesPane from './TextUtilitiesPane';
import { IEditor } from 'roosterjs-content-model-types';
import { SidePaneElementProps } from '../SidePaneElement';
import { SidePanePluginImpl } from '../SidePanePluginImpl';

export interface TextUtilitiesPaneProps extends SidePaneElementProps {
getEditor: () => IEditor;
}

export class TextUtilitiesPanePlugin extends SidePanePluginImpl<
TextUtilitiesPane,
TextUtilitiesPaneProps
> {
constructor() {
super(TextUtilitiesPane, 'textUtilities', 'Text Utilities');
}

getComponentProps(base: TextUtilitiesPaneProps) {
return {
...base,
getEditor: () => this.editor,
};
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { canMergeCells } from './canMergeCells';
import { getSelectedCells, mutateBlock } from 'roosterjs-content-model-dom';
import type { ShallowMutableContentModelTable } from 'roosterjs-content-model-types';
import { getSelectedCells, mutateBlock, canMergeCells } from 'roosterjs-content-model-dom';
import type {
BorderKey,
ContentModelTableCellFormat,
ShallowMutableContentModelTable,
} from 'roosterjs-content-model-types';

/**
* @internal
Expand All @@ -12,6 +15,17 @@ export function mergeTableCells(table: ShallowMutableContentModelTable) {
sel &&
canMergeCells(table.rows, sel.firstRow, sel.firstColumn, sel.lastRow, sel.lastColumn)
) {
const firstCell = mutateBlock(table.rows[sel.firstRow].cells[sel.firstColumn]);
const borderTop = getHorizontalBorder(table, sel.firstRow, sel, 'borderTop');
const borderRight = getVerticalBorder(table, sel.lastColumn, sel, 'borderRight');
const borderBottom = getHorizontalBorder(table, sel.lastRow, sel, 'borderBottom');
const borderLeft = getVerticalBorder(table, sel.firstColumn, sel, 'borderLeft');

setBorder(firstCell.format, 'borderTop', borderTop);
setBorder(firstCell.format, 'borderRight', borderRight);
setBorder(firstCell.format, 'borderBottom', borderBottom);
setBorder(firstCell.format, 'borderLeft', borderLeft);

for (let rowIndex = sel.firstRow; rowIndex <= sel.lastRow; rowIndex++) {
for (let colIndex = sel.firstColumn; colIndex <= sel.lastColumn; colIndex++) {
const cell = table.rows[rowIndex].cells[colIndex];
Expand All @@ -26,3 +40,47 @@ export function mergeTableCells(table: ShallowMutableContentModelTable) {
}
}
}

function getHorizontalBorder(
table: ShallowMutableContentModelTable,
rowIndex: number,
sel: { firstColumn: number; lastColumn: number },
borderKey: BorderKey
) {
const border = table.rows[rowIndex].cells[sel.firstColumn].format[borderKey];

return border &&
table.rows[rowIndex].cells
.slice(sel.firstColumn, sel.lastColumn + 1)
.every(cell => cell.format[borderKey] == border)
? border
: undefined;
}

function getVerticalBorder(
table: ShallowMutableContentModelTable,
columnIndex: number,
sel: { firstRow: number; lastRow: number },
borderKey: BorderKey
) {
const border = table.rows[sel.firstRow].cells[columnIndex].format[borderKey];

return border &&
table.rows
.slice(sel.firstRow, sel.lastRow + 1)
.every(row => row.cells[columnIndex].format[borderKey] == border)
? border
: undefined;
}

function setBorder(
format: ContentModelTableCellFormat,
borderKey: BorderKey,
border: string | undefined
) {
if (border) {
format[borderKey] = border;
} else {
delete format[borderKey];
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { canMergeCells } from './canMergeCells';
import { getSelectedCells, mutateBlock } from 'roosterjs-content-model-dom';
import { getSelectedCells, mutateBlock, canMergeCells } from 'roosterjs-content-model-dom';
import type {
ShallowMutableContentModelTable,
TableHorizontalMergeOperation,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { canMergeCells } from './canMergeCells';
import { getSelectedCells, mutateBlock } from 'roosterjs-content-model-dom';
import { getSelectedCells, mutateBlock, canMergeCells } from 'roosterjs-content-model-dom';
import type {
ShallowMutableContentModelTable,
TableVerticalMergeOperation,
Expand Down
Loading
Loading