Skip to content

Commit 6396b26

Browse files
Patch Bump Plugins package (#3116)
* Fix #3110 (#3113) * Add logical root plugin and related presets; enhance event handling with capture option * Update packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts Co-authored-by: Copilot <[email protected]> * Update demo/scripts/controlsV2/sidePane/presets/PresetPane.tsx Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]> * Add roosterjs-content-model-plugins version 9.33.1 to overrides --------- Co-authored-by: Copilot <[email protected]>
1 parent 210f317 commit 6396b26

File tree

11 files changed

+367
-18
lines changed

11 files changed

+367
-18
lines changed

demo/scripts/controlsV2/mainPane/MainPane.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as ReactDOM from 'react-dom';
33
import SampleEntityPlugin from '../plugins/SampleEntityPlugin';
44
import { ApiPlaygroundPlugin } from '../sidePane/apiPlayground/ApiPlaygroundPlugin';
55
import { ContentModelPanePlugin } from '../sidePane/contentModel/ContentModelPanePlugin';
6+
import { createLogicalRootPlugin } from '../plugins/createLogicalRootPlugin';
67
import { darkModeButton } from '../demoButtons/darkModeButton';
78
import { defaultDomToModelOption } from '../options/defaultDomToModelOption';
89
import { defaultModelToDomOption } from '../options/defaultModelToDomOption';
@@ -372,6 +373,8 @@ export class MainPane extends React.Component<{}, MainPaneState> {
372373
plugins.push(...this.getSidePanePlugins());
373374
}
374375

376+
plugins.push(createLogicalRootPlugin());
377+
375378
this.updateContentPlugin.update();
376379

377380
return (
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { EditorPlugin, IEditor, PluginEvent } from 'roosterjs-content-model-types';
2+
import { isElementOfType, isNodeOfType } from 'roosterjs-content-model-dom';
3+
4+
export function createLogicalRootPlugin(): EditorPlugin {
5+
return new LogicalRootPlugin();
6+
}
7+
8+
/**
9+
* Plugin just for demo purpose of Logical root feature.
10+
*/
11+
class LogicalRootPlugin implements EditorPlugin {
12+
private editor: IEditor | null = null;
13+
private disposer: (() => void) | null = null;
14+
private lastTarget: HTMLElement | null = null;
15+
16+
getName: () => string = () => 'LogicalRootPlugin';
17+
18+
initialize(editor: IEditor) {
19+
this.editor = editor;
20+
this.disposer = this.editor.attachDomEvent({
21+
mousedown: {
22+
capture: true,
23+
beforeDispatch: (event: MouseEvent) => {
24+
if (this.editor && this.editor.isDisposed()) {
25+
return;
26+
}
27+
const target = event.target as HTMLElement;
28+
const closestEntity = target.closest('._Entity._EType_LogicalRoot');
29+
if (closestEntity) {
30+
this.editor.setLogicalRoot(closestEntity as HTMLDivElement);
31+
32+
if (isElementOfType(target, 'img')) {
33+
this.editor.setDOMSelection({
34+
type: 'image',
35+
image: target,
36+
});
37+
}
38+
} else {
39+
this.editor.setLogicalRoot(null);
40+
}
41+
},
42+
},
43+
});
44+
}
45+
46+
dispose() {
47+
this.disposer?.();
48+
this.disposer = null;
49+
this.editor = null;
50+
}
51+
52+
onPluginEvent(event: PluginEvent) {
53+
if (event.eventType === 'logicalRootChanged') {
54+
// Handle logical root change if needed
55+
event.logicalRoot.focus();
56+
if (
57+
isElementOfType(this.lastTarget, 'img') &&
58+
event.logicalRoot.contains(this.lastTarget)
59+
) {
60+
this.editor?.setDOMSelection({
61+
type: 'image',
62+
image: this.lastTarget,
63+
});
64+
}
65+
}
66+
if (
67+
event.eventType === 'mouseUp' &&
68+
isNodeOfType(event.rawEvent.target as Node, 'ELEMENT_NODE')
69+
) {
70+
this.lastTarget = event.rawEvent.target as HTMLElement;
71+
}
72+
}
73+
}

demo/scripts/controlsV2/sidePane/presets/PresetPane.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from 'react';
22
import { allPresets } from './allPresets/allPresets';
3-
import { IEditor } from 'roosterjs-content-model-types';
3+
import { ContentModelEntity, IEditor } from 'roosterjs-content-model-types';
4+
import { mutateBlock } from 'roosterjs-content-model-dom';
45
import { Preset } from './allPresets/Preset';
56
import { SidePaneElementProps } from '../SidePaneElement';
67

@@ -39,10 +40,21 @@ export default class PresetPane extends React.Component<PresetPaneProps, PresetP
3940
}
4041

4142
setPreset(editor: IEditor, preset: Preset) {
42-
editor?.formatContentModel(model => {
43-
model.blocks = preset.content.blocks;
44-
return true;
45-
});
43+
editor?.formatContentModel(
44+
(model, ctx) => {
45+
model.blocks = preset.content.blocks;
46+
// Get the entity blocks
47+
const entityBlocks = (model.blocks.filter(
48+
block => block.blockType === 'Entity'
49+
) as ContentModelEntity[]).map(mutateBlock);
50+
ctx.newEntities.push(...entityBlocks);
51+
52+
return true;
53+
},
54+
{
55+
apiName: 'setPreset',
56+
}
57+
);
4658

4759
const url = new URL(window.location.href);
4860
url.searchParams.set('preset', preset.id);

demo/scripts/controlsV2/sidePane/presets/allPresets/allPresets.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logicalRootPreset from './logicalRootPreset';
12
import { allTextFormats } from './textPresets';
23
import { htmlParagraphs, mixedParagraphs } from './paragraphPresets';
34
import { image64x64Black, image64x64Gradient, image64x64White } from './imagePresets';
@@ -30,6 +31,7 @@ export const allPresets: Preset[] = [
3031
image64x64Black,
3132
image64x64White,
3233
undeleteableText,
34+
logicalRootPreset,
3335
];
3436

3537
export function getPresetModelById(id: string) {
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { Preset } from './Preset';
2+
3+
const logicalRootPreset: Preset = {
4+
buttonName: 'Logical Root',
5+
id: 'logicalRoot',
6+
content: {
7+
blockGroupType: 'Document',
8+
blocks: [
9+
{
10+
segments: [
11+
{
12+
segmentType: 'Br',
13+
format: {
14+
fontFamily: 'Calibri',
15+
fontSize: '11pt',
16+
textColor: '#000000',
17+
},
18+
},
19+
],
20+
segmentFormat: {
21+
fontFamily: 'Calibri',
22+
fontSize: '11pt',
23+
textColor: '#000000',
24+
},
25+
blockType: 'Paragraph',
26+
format: {},
27+
},
28+
{
29+
wrapper: createEntity(),
30+
entityFormat: {
31+
id: 'LogicalRoot_5',
32+
entityType: 'LogicalRoot',
33+
isReadonly: true,
34+
},
35+
blockType: 'Entity',
36+
format: {},
37+
segmentType: 'Entity',
38+
},
39+
{
40+
segments: [
41+
{
42+
isSelected: true,
43+
segmentType: 'SelectionMarker',
44+
format: {
45+
fontFamily: 'Calibri',
46+
fontSize: '11pt',
47+
textColor: 'rgb(0, 0, 0)',
48+
},
49+
},
50+
{
51+
segmentType: 'Br',
52+
format: {
53+
fontFamily: 'Calibri',
54+
fontSize: '11pt',
55+
textColor: 'rgb(0, 0, 0)',
56+
},
57+
},
58+
],
59+
segmentFormat: {
60+
fontFamily: 'Calibri',
61+
fontSize: '11pt',
62+
textColor: 'rgb(0, 0, 0)',
63+
},
64+
blockType: 'Paragraph',
65+
format: {},
66+
},
67+
{
68+
wrapper: createEntity(),
69+
entityFormat: {
70+
id: 'LogicalRoot_6',
71+
entityType: 'LogicalRoot',
72+
isReadonly: true,
73+
},
74+
blockType: 'Entity',
75+
format: {},
76+
segmentType: 'Entity',
77+
},
78+
{
79+
segments: [
80+
{
81+
segmentType: 'Br',
82+
format: {
83+
fontFamily: 'Calibri',
84+
fontSize: '11pt',
85+
textColor: 'rgb(0, 0, 0)',
86+
},
87+
},
88+
],
89+
segmentFormat: {
90+
fontFamily: 'Calibri',
91+
fontSize: '11pt',
92+
textColor: 'rgb(0, 0, 0)',
93+
},
94+
blockType: 'Paragraph',
95+
format: {},
96+
},
97+
],
98+
format: {
99+
fontFamily: 'Calibri',
100+
fontSize: '11pt',
101+
textColor: 'rgb(0, 0, 0)',
102+
},
103+
},
104+
};
105+
106+
export default logicalRootPreset;
107+
108+
let id = 0;
109+
function createEntity(): HTMLElement {
110+
const div = document.createElement('div');
111+
const newId = ++id;
112+
div.className = `_Entity _EType_LogicalRoot _EId_LogicalRoot_${newId} _EReadonly_1`;
113+
div.contentEditable = 'false';
114+
div.style.width = '400px';
115+
div.style.height = '400px';
116+
div.style.border = '2px solid blue';
117+
return div;
118+
}

packages/roosterjs-content-model-core/lib/coreApi/attachDomEvent/attachDomEvent.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { AttachDomEvent, PluginEvent } from 'roosterjs-content-model-types'
1111
*/
1212
export const attachDomEvent: AttachDomEvent = (core, eventMap) => {
1313
const disposers = getObjectKeys(eventMap || {}).map(key => {
14-
const { pluginEventType, beforeDispatch } = eventMap[key];
14+
const { pluginEventType, beforeDispatch, capture } = eventMap[key];
1515
const eventName = key as keyof HTMLElementEventMap;
1616
const onEvent = (event: HTMLElementEventMap[typeof eventName]) => {
1717
if (beforeDispatch) {
@@ -30,10 +30,12 @@ export const attachDomEvent: AttachDomEvent = (core, eventMap) => {
3030
}
3131
};
3232

33-
core.logicalRoot.addEventListener(eventName, onEvent);
33+
core.logicalRoot.addEventListener(eventName, onEvent, { capture });
3434

3535
return () => {
36-
core.logicalRoot.removeEventListener(eventName, onEvent);
36+
core.logicalRoot.removeEventListener(eventName, onEvent, {
37+
capture,
38+
});
3739
};
3840
});
3941

packages/roosterjs-content-model-core/test/coreApi/attachDomEvent/attachDomEventTest.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,35 @@ describe('attachDomEvent', () => {
7474
disposer();
7575
});
7676

77+
it('Check event dispatched with capture', () => {
78+
const triggerEventSpy = jasmine.createSpy();
79+
const beforeDispatch = jasmine.createSpy();
80+
core.api.triggerEvent = triggerEventSpy;
81+
core.logicalRoot = <any>{
82+
addEventListener: jasmine.createSpy('addEventListener'),
83+
removeEventListener: jasmine.createSpy('removeEventListener'),
84+
};
85+
86+
const disposer = attachDomEvent(core, {
87+
keydown: { pluginEventType: 'keyDown', capture: true, beforeDispatch },
88+
});
89+
const event = document.createEvent('KeyboardEvent');
90+
event.initEvent('keydown');
91+
div.dispatchEvent(event);
92+
93+
disposer();
94+
expect(core.logicalRoot.addEventListener).toHaveBeenCalledWith(
95+
'keydown',
96+
jasmine.any(Function),
97+
{ capture: true }
98+
);
99+
expect(core.logicalRoot.removeEventListener).toHaveBeenCalledWith(
100+
'keydown',
101+
jasmine.any(Function),
102+
{ capture: true }
103+
);
104+
});
105+
77106
it('Check event dispatched via triggerEvent and callback', () => {
78107
const triggerEventSpy = jasmine.createSpy();
79108
const callback = jasmine.createSpy();

packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -469,15 +469,7 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin {
469469
) {
470470
// Image dimensions are zero and loading is incomplete, wait for image to load.
471471
image.onload = () => {
472-
this.imageEditInfo = {
473-
...this.imageEditInfo,
474-
widthPx: image.clientWidth,
475-
heightPx: image.clientHeight,
476-
naturalWidth: image.naturalWidth,
477-
naturalHeight: image.naturalHeight,
478-
};
479-
image.style.width = this.imageEditInfo.widthPx + 'px';
480-
image.style.height = this.imageEditInfo.heightPx + 'px';
472+
this.updateImageDimensionsIfZero(image);
481473
this.startEditingInternal(editor, image, apiOperation);
482474
image.onload = null;
483475
image.onerror = null;
@@ -487,10 +479,18 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin {
487479
image.onerror = null;
488480
};
489481
} else {
482+
this.updateImageDimensionsIfZero(image);
490483
this.startEditingInternal(editor, image, apiOperation);
491484
}
492485
}
493486

487+
private updateImageDimensionsIfZero(image: HTMLImageElement) {
488+
if (this.imageEditInfo?.widthPx === 0 || this.imageEditInfo?.heightPx === 0) {
489+
this.imageEditInfo.widthPx = image.clientWidth;
490+
this.imageEditInfo.heightPx = image.clientHeight;
491+
}
492+
}
493+
494494
private startEditingInternal(
495495
editor: IEditor,
496496
image: HTMLImageElement,

0 commit comments

Comments
 (0)