Skip to content

Commit

Permalink
Port PickerPlugin (#2569)
Browse files Browse the repository at this point in the history
* Port PickerPlugin

* fix buid

* Improve

* fix build

* Improve

* Improve

* add test

* Improve
  • Loading branch information
JiuqingSong authored Apr 16, 2024
1 parent 70684a4 commit 74bc863
Show file tree
Hide file tree
Showing 22 changed files with 1,664 additions and 85 deletions.
4 changes: 4 additions & 0 deletions demo/scripts/controlsV2/mainPane/MainPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { PresetPlugin } from '../sidePane/presets/PresetPlugin';
import { redoButton } from '../roosterjsReact/ribbon/buttons/redoButton';
import { registerWindowForCss, unregisterWindowForCss } from '../../utils/cssMonitor';
import { Rooster } from '../roosterjsReact/rooster';
import { SamplePickerPlugin } from '../plugins/SamplePickerPlugin';
import { SidePane } from '../sidePane/SidePane';
import { SidePanePlugin } from '../sidePane/SidePanePlugin';
import { SnapshotPlugin } from '../sidePane/snapshot/SnapshotPlugin';
Expand Down Expand Up @@ -88,6 +89,7 @@ export class MainPane extends React.Component<{}, MainPaneState> {
private ribbonPlugin: RibbonPlugin;
private snapshotPlugin: SnapshotPlugin;
private formatPainterPlugin: FormatPainterPlugin;
private samplePickerPlugin: SamplePickerPlugin;
private snapshots: Snapshots;

protected sidePane = React.createRef<SidePane>();
Expand Down Expand Up @@ -125,6 +127,7 @@ export class MainPane extends React.Component<{}, MainPaneState> {
this.presetPlugin = new PresetPlugin();
this.ribbonPlugin = createRibbonPlugin();
this.formatPainterPlugin = new FormatPainterPlugin();
this.samplePickerPlugin = new SamplePickerPlugin();

this.state = {
showSidePane: window.location.hash != '',
Expand Down Expand Up @@ -327,6 +330,7 @@ export class MainPane extends React.Component<{}, MainPaneState> {
const plugins: EditorPlugin[] = [
this.ribbonPlugin,
this.formatPainterPlugin,
this.samplePickerPlugin,
...this.getToggleablePlugins(),
this.contentModelPanePlugin.getInnerRibbonPlugin(),
this.updateContentPlugin,
Expand Down
206 changes: 206 additions & 0 deletions demo/scripts/controlsV2/plugins/SamplePickerPlugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import * as React from 'react';
import { Callout } from '@fluentui/react/lib/Callout';
import { DOMInsertPoint } from 'roosterjs-content-model-types';
import { IContextualMenuItem } from '@fluentui/react/lib/ContextualMenu';
import { mergeStyles } from '@fluentui/react/lib/Styling';
import { ReactEditorPlugin, UIUtilities } from '../roosterjsReact/common';
import {
PickerDirection,
PickerHandler,
PickerHelper,
PickerPlugin,
PickerSelectionChangMode,
getDOMInsertPointRect,
} from 'roosterjs-content-model-plugins';
import {
createContentModelDocument,
createEntity,
createParagraph,
} from 'roosterjs-content-model-dom';

const itemStyle = mergeStyles({
height: '20px',
margin: '4px',
padding: '4px',
minWidth: '200px',
});

const selectedItemStyle = mergeStyles({
backgroundColor: 'blue',
color: 'white',
fontWeight: 'bold',
});

export class SamplePickerPlugin extends PickerPlugin implements ReactEditorPlugin {
private pickerHandler: SamplePickerHandler;

constructor() {
const pickerHandler = new SamplePickerHandler();
super('@', pickerHandler);

this.pickerHandler = pickerHandler;
}

setUIUtilities(uiUtilities: UIUtilities): void {
this.pickerHandler.setUIUtilities(uiUtilities);
}
}

class SamplePickerHandler implements PickerHandler {
private uiUtilities: UIUtilities;
private index = 0;
private ref: IPickerMenu | null = null;
private queryString: string;
private items: IContextualMenuItem[] = [];
private onClose: (() => void) | null = null;
private helper: PickerHelper | null = null;

onInitialize(helper: PickerHelper) {
this.helper = helper;
}

onDispose() {
this.helper = null;
}

setUIUtilities(uiUtilities: UIUtilities): void {
this.uiUtilities = uiUtilities;
}

onTrigger(queryString: string, pos: DOMInsertPoint): PickerDirection | null {
this.index = 0;
this.queryString = queryString;
this.items = buildItems(queryString, this.index);

const rect = getDOMInsertPointRect(this.helper.editor.getDocument(), pos);

if (rect) {
this.onClose = this.uiUtilities.renderComponent(
<PickerMenu
x={rect.left}
y={(rect.bottom + rect.top) / 2}
ref={ref => (this.ref = ref)}
items={this.items}
/>
);
return 'vertical';
} else {
return null;
}
}

onClosePicker() {
this.onClose?.();
this.onClose = null;
}

onSelectionChanged(mode: PickerSelectionChangMode): void {
switch (mode) {
case 'first':
case 'firstInRow':
case 'previousPage':
this.index = 0;
break;

case 'last':
case 'lastInRow':
case 'nextPage':
this.index = 4;
break;

case 'previous':
this.index = this.index - 1;

if (this.index < 0) {
this.index = 4;
}

break;

case 'next':
this.index = (this.index + 1) % 5;
break;
}

this.items = buildItems(this.queryString, this.index);
this.ref?.setMenuItems(this.items);
}

onSelect(): void {
const text = this.items[this.index]?.text;

if (text) {
const span = this.helper.editor.getDocument().createElement('span');
span.textContent = '@' + text;
span.style.textDecoration = 'underline';
span.style.color = 'blue';

const entity = createEntity(span, true /*isReadonly*/, {}, 'TEST_ENTITY');
const paragraph = createParagraph();
const doc = createContentModelDocument();

paragraph.segments.push(entity);
doc.blocks.push(paragraph);

this.helper.replaceQueryString(
doc,
{
changeSource: 'SamplePicker',
},
true /*canUndoByBackspace*/
);
}

this.onClose?.();
this.onClose = null;
this.ref = null;
this.helper.closePicker();
}

onQueryStringChanged(queryString: string): void {
this.queryString = queryString;

if (queryString.length > 100 || queryString.split(' ').length > 4) {
// Querystring is too long, so close picker
this.helper.closePicker();
} else {
this.items = buildItems(this.queryString, this.index);
this.ref?.setMenuItems(this.items);
}
}
}

function buildItems(queryString: string, index: number): IContextualMenuItem[] {
return [1, 2, 3, 4, 5].map((x, i) => ({
key: 'item' + i,
text: queryString.substring(1) + ' item ' + x,
checked: i == index,
}));
}

interface IPickerMenu {
setMenuItems: (items: IContextualMenuItem[]) => void;
}

const PickerMenu = React.forwardRef(
(
props: { x: number; y: number; items: IContextualMenuItem[] },
ref: React.Ref<IPickerMenu>
) => {
const [items, setItems] = React.useState<IContextualMenuItem[]>(props.items);

React.useImperativeHandle(ref, () => ({
setMenuItems: setItems,
}));

return (
<Callout target={{ left: props.x, top: props.y }} isBeakVisible={false} gapSpace={10}>
{items.map(item => (
<div className={itemStyle + (item.checked ? ' ' + selectedItemStyle : '')}>
{item.text}
</div>
))}
</Callout>
);
}
);
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import * as React from 'react';
import { ButtonKeys, Buttons } from '../utils/buttons';
import { Callout, DirectionalHint } from '@fluentui/react/lib/Callout';
import { getDOMInsertPointRect } from 'roosterjs-content-model-plugins';
import { getLocalizedString } from '../../common/index';
import { getObjectKeys } from 'roosterjs-content-model-dom';
import { getPositionRect } from '../utils/getPositionRect';
import { Icon } from '@fluentui/react/lib/Icon';
import { IconButton } from '@fluentui/react/lib/Button';
import { memoizeFunction } from '@fluentui/react/lib/Utilities';
import { mergeStyleSets } from '@fluentui/react/lib/Styling';
import { renderReactComponent } from '../../common/utils/renderReactComponent';
import { useTheme } from '@fluentui/react/lib/Theme';
import { useWindow } from '@fluentui/react/lib/WindowProvider';
import type { LocalizedStrings, UIUtilities } from '../../common/index';
import type { Theme } from '@fluentui/react/lib/Theme';
import type { PasteOptionButtonKeys, PasteOptionStringKeys } from '../type/PasteOptionStringKeys';
Expand Down Expand Up @@ -106,7 +107,7 @@ const PasteOptionComponent = React.forwardRef(function PasteOptionFunc(
const classNames = getPasteOptionClassNames(theme);
const [selectedKey, setSelectedKey] = React.useState<PasteOptionButtonKeys | null>(null);

const rect = getPositionRect(container, offset);
const rect = getDOMInsertPointRect(useWindow().document, { node: container, offset });
const target = rect && { x: props.isRtl ? rect.left : rect.right, y: rect.bottom };

React.useImperativeHandle(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@ export interface FormatStatePaneProps extends FormatStatePaneState, SidePaneElem
env?: EditorEnvironment;
}

export default class FormatStatePane extends React.Component<
FormatStatePaneProps,
FormatStatePaneState
> {
export class FormatStatePane extends React.Component<FormatStatePaneProps, FormatStatePaneState> {
constructor(props: FormatStatePaneProps) {
super(props);
this.state = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import FormatStatePane, { FormatStatePaneProps, FormatStatePaneState } from './FormatStatePane';
import { FormatStatePane, FormatStatePaneProps, FormatStatePaneState } from './FormatStatePane';
import { getDOMInsertPointRect } from 'roosterjs-content-model-plugins';
import { getFormatState } from 'roosterjs-content-model-api';
import { getPositionRect } from '../../roosterjsReact/pasteOptions/utils/getPositionRect';
import { PluginEvent } from 'roosterjs-content-model-types';
import { SidePaneElementProps } from '../SidePaneElement';
import { SidePanePluginImpl } from '../SidePanePluginImpl';
Expand Down Expand Up @@ -50,7 +50,7 @@ export class FormatStatePlugin extends SidePanePluginImpl<FormatStatePane, Forma
const offset = selection.isReverted
? selection.range.startOffset
: selection.range.endOffset;
const rect = getPositionRect(node, offset);
const rect = getDOMInsertPointRect(this.editor.getDocument(), { node, offset });

if (rect) {
x = rect.left;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import type {
ContentModelSegmentFormat,
ContentModelText,
FormatContentModelContext,
FormatContentModelOptions,
IEditor,
} from 'roosterjs-content-model-types';

/**
* Invoke a callback to format the text segment before the selection marker using Content Model
* @param editor The editor object
* @param callback The callback to format the text segment.
* @returns True if the segment before cursor is found and callback is called, otherwise false
*/
export function formatTextSegmentBeforeSelectionMarker(
editor: IEditor,
Expand All @@ -21,8 +23,11 @@ export function formatTextSegmentBeforeSelectionMarker(
paragraph: ContentModelParagraph,
markerFormat: ContentModelSegmentFormat,
context: FormatContentModelContext
) => boolean
) {
) => boolean,
options?: FormatContentModelOptions
): boolean {
let result = false;

editor.formatContentModel((model, context) => {
const selectedSegmentsAndParagraphs = getSelectedSegmentsAndParagraphs(
model,
Expand All @@ -36,10 +41,15 @@ export function formatTextSegmentBeforeSelectionMarker(
if (marker.segmentType === 'SelectionMarker' && markerIndex > 0) {
const previousSegment = paragraph.segments[markerIndex - 1];
if (previousSegment && previousSegment.segmentType === 'Text') {
result = true;

return callback(model, previousSegment, paragraph, marker.format, context);
}
}
}

return false;
});
}, options);

return result;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,9 @@ export const createEditorContext: CreateEditorContext = (core, saveIndex) => {
...getRootComputedStyleForContext(logicalRoot.ownerDocument),
};

checkRootRtl(logicalRoot, context);
if (core.domHelper.isRightToLeft()) {
context.isRootRtl = true;
}

return context;
};

function checkRootRtl(element: HTMLElement, context: EditorContext) {
const style = element?.ownerDocument.defaultView?.getComputedStyle(element);

if (style?.direction == 'rtl') {
context.isRootRtl = true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ class DOMHelperImpl implements DOMHelper {
const activeElement = this.contentDiv.ownerDocument.activeElement;
return !!(activeElement && this.contentDiv.contains(activeElement));
}

/**
* Check if the root element is in RTL mode
*/
isRightToLeft(): boolean {
const contentDiv = this.contentDiv;
const style = contentDiv.ownerDocument.defaultView?.getComputedStyle(contentDiv);

return style?.direction == 'rtl';
}
}

/**
Expand Down
Loading

0 comments on commit 74bc863

Please sign in to comment.