Skip to content

Commit

Permalink
Port demo site step 2 (microsoft#2465)
Browse files Browse the repository at this point in the history
  • Loading branch information
JiuqingSong authored Mar 4, 2024
1 parent 2d9d5f0 commit 3f4928b
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toArray } from 'roosterjs-content-model-dom';
import { isNodeOfType, toArray } from 'roosterjs-content-model-dom';
import type { DOMHelper } from 'roosterjs-content-model-types';

class DOMHelperImpl implements DOMHelper {
Expand Down Expand Up @@ -40,6 +40,21 @@ class DOMHelperImpl implements DOMHelper {
getDomStyle<T extends keyof CSSStyleDeclaration>(style: T): CSSStyleDeclaration[T] {
return this.contentDiv.style[style];
}

findClosestElementAncestor(startFrom: Node, selector?: string): HTMLElement | null {
const startElement = isNodeOfType(startFrom, 'ELEMENT_NODE')
? startFrom
: startFrom.parentElement;
const closestElement = selector
? (startElement?.closest(selector) as HTMLElement | null)
: startElement;

return closestElement &&
this.isNodeInEditor(closestElement) &&
closestElement != this.contentDiv
? closestElement
: null;
}
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/roosterjs-content-model-core/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,6 @@ export { BulletListType } from './constants/BulletListType';
export { NumberingListType } from './constants/NumberingListType';
export { TableBorderFormat } from './constants/TableBorderFormat';

export { extractClipboardItems } from './utils/extractClipboardItems';

export { Editor } from './editor/Editor';
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const ContentHandlers: {
};

/**
* @internal
* Extract clipboard items to be a ClipboardData object for IE
* @param items The clipboard items retrieve from a DataTransfer object
* @param allowedCustomPasteType Allowed custom content type when paste besides text/plain, text/html and images
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,60 @@ describe('DOMHelperImpl', () => {

expect(result).toBe(mockedValue);
});

it('findClosestElementAncestor - text node, no parent, no selector', () => {
const startNode = document.createTextNode('test');
const container = document.createElement('div');

container.appendChild(startNode);

const domHelper = createDOMHelper(container);
const result = domHelper.findClosestElementAncestor(startNode);

expect(result).toBeNull();
});

it('findClosestElementAncestor - text node, has parent, no selector', () => {
const startNode = document.createTextNode('test');
const parent = document.createElement('span');
const container = document.createElement('div');

parent.appendChild(startNode);
container.appendChild(parent);

const domHelper = createDOMHelper(container);
const result = domHelper.findClosestElementAncestor(startNode);

expect(result).toBe(parent);
});

it('findClosestElementAncestor - element node, no selector', () => {
const startNode = document.createElement('span');
const container = document.createElement('div');

container.appendChild(startNode);

const domHelper = createDOMHelper(container);
const result = domHelper.findClosestElementAncestor(startNode);

expect(result).toBe(startNode);
});

it('findClosestElementAncestor - has selector', () => {
const startNode = document.createElement('span');
const parent1 = document.createElement('div');
const parent2 = document.createElement('div');
const container = document.createElement('div');

parent2.className = 'testClass';

parent1.appendChild(startNode);
parent2.appendChild(parent1);
container.appendChild(parent2);

const domHelper = createDOMHelper(container);
const result = domHelper.findClosestElementAncestor(startNode, '.testClass');

expect(result).toBe(parent2);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { EditorPlugin, IEditor, PluginEvent } from 'roosterjs-content-model-types';

/**
* Context Menu options for ContextMenu plugin
*/
export interface ContextMenuOptions<T> {
/**
* Render function for the context menu
* @param container The container HTML element, it will be located at the mouse click position,
* so the callback just need to render menu content into this container
* @param onDismiss The onDismiss callback, some menu render need to know this callback so that
* it can handle the dismiss event
*/
render: (container: HTMLElement, items: (T | null)[], onDismiss: () => void) => void;

/**
* Dismiss function for the context menu, it will be called when user wants to dismiss this context menu
* e.g. user click away so the menu should be dismissed
* @param container The container HTML element
*/
dismiss?: (container: HTMLElement) => void;

/**
* Whether the default context menu is allowed. @default false
*/
allowDefaultMenu?: boolean;
}

/**
* An editor plugin that support showing a context menu using render() function from options parameter
*/
export class ContextMenuPluginBase<T> implements EditorPlugin {
private container: HTMLElement | null = null;
private editor: IEditor | null = null;
private isMenuShowing: boolean = false;

/**
* Create a new instance of ContextMenu class
* @param options An options object to determine how to show/hide the context menu
*/
constructor(private options: ContextMenuOptions<T>) {}

/**
* Get a friendly name of this plugin
*/
getName() {
return 'ContextMenu';
}

/**
* Initialize this plugin
* @param editor The editor instance
*/
initialize(editor: IEditor) {
this.editor = editor;
}

/**
* Dispose this plugin
*/
dispose() {
this.onDismiss();

if (this.container?.parentNode) {
this.container.parentNode.removeChild(this.container);
this.container = null;
}
this.editor = null;
}

/**
* Handle events triggered from editor
* @param event PluginEvent object
*/
onPluginEvent(event: PluginEvent) {
if (event.eventType == 'contextMenu' && event.items.length > 0) {
const { rawEvent, items } = event;

this.onDismiss();

if (!this.options.allowDefaultMenu) {
rawEvent.preventDefault();
}

if (this.initContainer(rawEvent.pageX, rawEvent.pageY)) {
this.options.render(this.container!, items as T[], this.onDismiss);
this.isMenuShowing = true;
}
}
}

private initContainer(x: number, y: number) {
if (!this.container && this.editor) {
this.container = this.editor.getDocument().createElement('div');

this.container.style.position = 'fixed';
this.container.style.width = '0';
this.container.style.height = '0';
this.editor.getDocument().body.appendChild(this.container);
}
this.container?.style.setProperty('left', x + 'px');
this.container?.style.setProperty('top', y + 'px');
return !!this.container;
}

private onDismiss = () => {
if (this.container && this.isMenuShowing) {
this.options.dismiss?.(this.container);
this.isMenuShowing = false;
}
};
}
1 change: 1 addition & 0 deletions packages/roosterjs-content-model-plugins/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export {
} from './shortcut/shortcuts';
export { ShortcutPlugin } from './shortcut/ShortcutPlugin';
export { ShortcutKeyDefinition, ShortcutCommand } from './shortcut/ShortcutCommand';
export { ContextMenuPluginBase, ContextMenuOptions } from './contextMenuBase/ContextMenuPluginBase';
1 change: 1 addition & 0 deletions packages/roosterjs-content-model-types/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ export { TrustedHTMLHandler } from './parameter/TrustedHTMLHandler';
export { Rect } from './parameter/Rect';
export { ValueSanitizer } from './parameter/ValueSanitizer';
export { DOMHelper } from './parameter/DOMHelper';
export { ImageEditOperation, ImageEditor } from './parameter/ImageEditor';

export { BasePluginEvent, BasePluginDomEvent } from './event/BasePluginEvent';
export { BeforeCutCopyEvent } from './event/BeforeCutCopyEvent';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,13 @@ export interface DOMHelper {
* @param style Name of the style
*/
getDomStyle<T extends keyof CSSStyleDeclaration>(style: T): CSSStyleDeclaration[T];

/**
* Find closest element ancestor start from the given node which matches the given selector
* @param node Find ancestor start from this node
* @param selector The expected selector. If null, return the first HTML Element found from start node
* @returns An HTML element which matches the given selector. If the given start node matches the selector,
* returns the given node
*/
findClosestElementAncestor(node: Node, selector?: string): HTMLElement | null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Type of image editing operations
*/
export type ImageEditOperation =
/**
* Resize an image
*/
| 'resize'

/**
* Rotate an image
*/
| 'rotate'

/**
* Crop an image
*/
| 'crop';

/**
* Define the common operation of an image editor
*/
export interface ImageEditor {
/**
* Check if the given editing operation is allowed on current selected image
* @param operation The operation to check
* @returns True if the operation is allowed, otherwise false
*/
isOperationAllowed(operation: ImageEditOperation): boolean;

/**
* Check if the given image can be regenerated by this image editor
* @param image The image to check
* @returns True if the image can be regenerated, otherwise false
*/
canRegenerateImage(image: HTMLImageElement): boolean;

/**
* Rotate selected image to the given angle (in rad)
* @param angleRad The angle to rotate to
*/
rotateImage(angleRad: number): void;

/**
* Flip the image.
* @param direction Direction of flip, can be vertical or horizontal
*/
flipImage(direction: 'vertical' | 'horizontal'): void;

/**
* Start to crop selected image
*/
cropImage(): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ import {
collapseNodes,
contains,
deleteSelectedContent,
findClosestElementAncestor,
getBlockElementAtNode,
getRegionsFromRange,
getSelectionPath,
Expand Down Expand Up @@ -563,8 +562,7 @@ export class EditorAdapter extends Editor implements ILegacyEditor {
startFrom = position?.node;
}
return (
startFrom &&
findClosestElementAncestor(startFrom, this.getCore().physicalRoot, selector)
startFrom && this.getDOMHelper().findClosestElementAncestor(startFrom, selector)
);
}) ?? null
);
Expand Down

0 comments on commit 3f4928b

Please sign in to comment.