forked from microsoft/roosterjs
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
741df8d
commit ed67dc2
Showing
1 changed file
with
206 additions
and
184 deletions.
There are no files selected for viewing
390 changes: 206 additions & 184 deletions
390
packages/roosterjs-editor-core/lib/corePlugins/PendingFormatStatePlugin.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,184 +1,206 @@ | ||
import { ChangeSource, Keys, PluginEventType, PositionType } from 'roosterjs-editor-types'; | ||
import { isCharacterValue, Position, setColor } from 'roosterjs-editor-dom'; | ||
import type { | ||
IEditor, | ||
NodePosition, | ||
PendingFormatStatePluginState, | ||
PluginEvent, | ||
PluginWithState, | ||
} from 'roosterjs-editor-types'; | ||
|
||
const ZERO_WIDTH_SPACE = '\u200B'; | ||
|
||
/** | ||
* @internal | ||
* PendingFormatStatePlugin handles pending format state management | ||
*/ | ||
export default class PendingFormatStatePlugin | ||
implements PluginWithState<PendingFormatStatePluginState> { | ||
private editor: IEditor | null = null; | ||
private state: PendingFormatStatePluginState; | ||
|
||
/** | ||
* Construct a new instance of PendingFormatStatePlugin | ||
* @param options The editor options | ||
* @param contentDiv The editor content DIV | ||
*/ | ||
constructor() { | ||
this.state = { | ||
pendableFormatPosition: null, | ||
pendableFormatState: null, | ||
pendableFormatSpan: null, | ||
}; | ||
} | ||
|
||
/** | ||
* Get a friendly name of this plugin | ||
*/ | ||
getName() { | ||
return 'PendingFormatState'; | ||
} | ||
|
||
/** | ||
* Initialize this plugin. This should only be called from Editor | ||
* @param editor Editor instance | ||
*/ | ||
initialize(editor: IEditor) { | ||
this.editor = editor; | ||
} | ||
|
||
/** | ||
* Dispose this plugin | ||
*/ | ||
dispose() { | ||
this.editor = null; | ||
this.clear(); | ||
} | ||
|
||
/** | ||
* Get plugin state object | ||
*/ | ||
getState() { | ||
return this.state; | ||
} | ||
|
||
/** | ||
* Handle events triggered from editor | ||
* @param event PluginEvent object | ||
*/ | ||
onPluginEvent(event: PluginEvent) { | ||
switch (event.eventType) { | ||
case PluginEventType.PendingFormatStateChanged: | ||
// Got PendingFormatStateChanged event, cache current position and pending format if a format is passed in | ||
// otherwise clear existing pending format. | ||
if (event.formatState) { | ||
this.state.pendableFormatPosition = this.getCurrentPosition(); | ||
this.state.pendableFormatState = event.formatState; | ||
this.state.pendableFormatSpan = event.formatCallback | ||
? this.createPendingFormatSpan(event.formatCallback) | ||
: null; | ||
} else { | ||
this.clear(); | ||
} | ||
|
||
break; | ||
case PluginEventType.KeyDown: | ||
case PluginEventType.MouseDown: | ||
case PluginEventType.ContentChanged: | ||
let currentPosition: NodePosition | null = null; | ||
if ( | ||
this.editor && | ||
event.eventType == PluginEventType.KeyDown && | ||
isCharacterValue(event.rawEvent) && | ||
this.state.pendableFormatSpan | ||
) { | ||
this.state.pendableFormatSpan.removeAttribute('contentEditable'); | ||
this.editor.insertNode(this.state.pendableFormatSpan); | ||
this.editor.select( | ||
this.state.pendableFormatSpan, | ||
PositionType.Begin, | ||
this.state.pendableFormatSpan, | ||
PositionType.End | ||
); | ||
this.clear(); | ||
} else if ( | ||
(event.eventType == PluginEventType.KeyDown && | ||
event.rawEvent.which >= Keys.PAGEUP && | ||
event.rawEvent.which <= Keys.DOWN) || | ||
(this.state.pendableFormatPosition && | ||
(currentPosition = this.getCurrentPosition()) && | ||
!this.state.pendableFormatPosition.equalTo(currentPosition)) || | ||
(event.eventType == PluginEventType.ContentChanged && | ||
(event.source == ChangeSource.SwitchToDarkMode || | ||
event.source == ChangeSource.SwitchToLightMode)) | ||
) { | ||
// If content or position is changed (by keyboard, mouse, or code), | ||
// check if current position is still the same with the cached one (if exist), | ||
// and clear cached format if position is changed since it is out-of-date now | ||
this.clear(); | ||
} | ||
|
||
break; | ||
} | ||
} | ||
|
||
private clear() { | ||
this.state.pendableFormatPosition = null; | ||
this.state.pendableFormatState = null; | ||
this.state.pendableFormatSpan = null; | ||
} | ||
|
||
private getCurrentPosition() { | ||
const range = this.editor?.getSelectionRange(); | ||
return (range && Position.getStart(range).normalize()) ?? null; | ||
} | ||
|
||
private createPendingFormatSpan( | ||
callback: (element: HTMLElement, isInnerNode?: boolean) => any | ||
) { | ||
let span = this.state.pendableFormatSpan; | ||
|
||
if (!span && this.editor) { | ||
const currentStyle = this.editor.getStyleBasedFormatState(); | ||
const doc = this.editor.getDocument(); | ||
const isDarkMode = this.editor.isDarkMode(); | ||
|
||
span = doc.createElement('span'); | ||
span.contentEditable = 'true'; | ||
span.appendChild(doc.createTextNode(ZERO_WIDTH_SPACE)); | ||
|
||
span.style.setProperty('font-family', currentStyle.fontName ?? null); | ||
span.style.setProperty('font-size', currentStyle.fontSize ?? null); | ||
|
||
const darkColorHandler = this.editor.getDarkColorHandler(); | ||
|
||
if (currentStyle.textColors || currentStyle.textColor) { | ||
setColor( | ||
span, | ||
(currentStyle.textColors || currentStyle.textColor)!, | ||
false /*isBackground*/, | ||
isDarkMode, | ||
false /*shouldAdaptFontColor*/, | ||
darkColorHandler | ||
); | ||
} | ||
|
||
if (currentStyle.backgroundColors || currentStyle.backgroundColor) { | ||
setColor( | ||
span, | ||
(currentStyle.backgroundColors || currentStyle.backgroundColor)!, | ||
true /*isBackground*/, | ||
isDarkMode, | ||
false /*shouldAdaptFontColor*/, | ||
darkColorHandler | ||
); | ||
} | ||
} | ||
|
||
if (span) { | ||
callback(span); | ||
} | ||
|
||
return span; | ||
} | ||
} | ||
import { ChangeSource, Keys, PluginEventType, PositionType } from 'roosterjs-editor-types'; | ||
import { isCharacterValue, Position, setColor } from 'roosterjs-editor-dom'; | ||
import type { | ||
IEditor, | ||
NodePosition, | ||
PendingFormatStatePluginState, | ||
PluginEvent, | ||
PluginWithState, | ||
} from 'roosterjs-editor-types'; | ||
|
||
const ZERO_WIDTH_SPACE = '\u200B'; | ||
|
||
/** | ||
* @internal | ||
* PendingFormatStatePlugin handles pending format state management | ||
*/ | ||
export default class PendingFormatStatePlugin | ||
implements PluginWithState<PendingFormatStatePluginState> { | ||
private editor: IEditor | null = null; | ||
private state: PendingFormatStatePluginState; | ||
private shouldHandleNextInputEvent = false; | ||
private androidInputEventHandler: AndroidInputEventHandler | null = null; | ||
|
||
/** | ||
* Construct a new instance of PendingFormatStatePlugin | ||
* @param options The editor options | ||
* @param contentDiv The editor content DIV | ||
*/ | ||
constructor() { | ||
this.state = { | ||
pendableFormatPosition: null, | ||
pendableFormatState: null, | ||
pendableFormatSpan: null, | ||
}; | ||
} | ||
|
||
/** | ||
* Get a friendly name of this plugin | ||
*/ | ||
getName() { | ||
return 'PendingFormatState'; | ||
} | ||
|
||
/** | ||
* Initialize this plugin. This should only be called from Editor | ||
* @param editor Editor instance | ||
*/ | ||
initialize(editor: IEditor) { | ||
this.editor = editor; | ||
this.androidInputEventHandler = new AndroidInputEventHandler(editor, data => { | ||
const isContentInserted = data !== null && data.length > 0; | ||
if (this.shouldHandleNextInputEvent && isContentInserted) { | ||
this.applyPendingFormat(); | ||
} | ||
this.shouldHandleNextInputEvent = false; | ||
}); | ||
} | ||
|
||
/** | ||
* Dispose this plugin | ||
*/ | ||
dispose() { | ||
this.editor = null; | ||
this.clear(); | ||
this.androidInputEventHandler?.dispose(); | ||
this.androidInputEventHandler = null; | ||
} | ||
|
||
/** | ||
* Get plugin state object | ||
*/ | ||
getState() { | ||
return this.state; | ||
} | ||
|
||
/** | ||
* Handle events triggered from editor | ||
* @param event PluginEvent object | ||
*/ | ||
onPluginEvent(event: PluginEvent) { | ||
switch (event.eventType) { | ||
case PluginEventType.PendingFormatStateChanged: | ||
// Got PendingFormatStateChanged event, cache current position and pending format if a format is passed in | ||
// otherwise clear existing pending format. | ||
if (event.formatState) { | ||
this.state.pendableFormatPosition = this.getCurrentPosition(); | ||
this.state.pendableFormatState = event.formatState; | ||
this.state.pendableFormatSpan = event.formatCallback | ||
? this.createPendingFormatSpan(event.formatCallback) | ||
: null; | ||
} else { | ||
this.clear(); | ||
} | ||
|
||
break; | ||
case PluginEventType.KeyDown: | ||
case PluginEventType.MouseDown: | ||
case PluginEventType.ContentChanged: | ||
let currentPosition: NodePosition | null = null; | ||
if ( | ||
this.editor && | ||
event.eventType == PluginEventType.KeyDown && | ||
isCharacterValue(event.rawEvent) && | ||
this.state.pendableFormatSpan | ||
) { | ||
this.applyPendingFormat(); | ||
} else if ( | ||
(event.eventType == PluginEventType.KeyDown && | ||
event.rawEvent.which >= Keys.PAGEUP && | ||
event.rawEvent.which <= Keys.DOWN) || | ||
(this.state.pendableFormatPosition && | ||
(currentPosition = this.getCurrentPosition()) && | ||
!this.state.pendableFormatPosition.equalTo(currentPosition)) || | ||
(event.eventType == PluginEventType.ContentChanged && | ||
(event.source == ChangeSource.SwitchToDarkMode || | ||
event.source == ChangeSource.SwitchToLightMode)) | ||
) { | ||
// If content or position is changed (by keyboard, mouse, or code), | ||
// check if current position is still the same with the cached one (if exist), | ||
// and clear cached format if position is changed since it is out-of-date now | ||
this.clear(); | ||
} else if ( | ||
event.eventType === PluginEventType.KeyDown && | ||
event.rawEvent.key === 'Unidentified' | ||
) { | ||
this.shouldHandleNextInputEvent = true; | ||
} | ||
|
||
break; | ||
} | ||
} | ||
|
||
private applyPendingFormat() { | ||
if (this.editor && this.state.pendableFormatSpan) { | ||
this.state.pendableFormatSpan.removeAttribute('contentEditable'); | ||
this.editor.insertNode(this.state.pendableFormatSpan); | ||
this.editor.select( | ||
this.state.pendableFormatSpan, | ||
PositionType.Begin, | ||
this.state.pendableFormatSpan, | ||
PositionType.End | ||
); | ||
this.clear(); | ||
} | ||
} | ||
|
||
private clear() { | ||
this.state.pendableFormatPosition = null; | ||
this.state.pendableFormatState = null; | ||
this.state.pendableFormatSpan = null; | ||
} | ||
|
||
private getCurrentPosition() { | ||
const range = this.editor?.getSelectionRange(); | ||
return (range && Position.getStart(range).normalize()) ?? null; | ||
} | ||
|
||
private createPendingFormatSpan( | ||
callback: (element: HTMLElement, isInnerNode?: boolean) => any | ||
) { | ||
let span = this.state.pendableFormatSpan; | ||
|
||
if (!span && this.editor) { | ||
const currentStyle = this.editor.getStyleBasedFormatState(); | ||
const doc = this.editor.getDocument(); | ||
const isDarkMode = this.editor.isDarkMode(); | ||
|
||
span = doc.createElement('span'); | ||
span.contentEditable = 'true'; | ||
span.appendChild(doc.createTextNode(ZERO_WIDTH_SPACE)); | ||
|
||
span.style.setProperty('font-family', currentStyle.fontName ?? null); | ||
span.style.setProperty('font-size', currentStyle.fontSize ?? null); | ||
|
||
const darkColorHandler = this.editor.getDarkColorHandler(); | ||
|
||
if (currentStyle.textColors || currentStyle.textColor) { | ||
setColor( | ||
span, | ||
(currentStyle.textColors || currentStyle.textColor)!, | ||
false /*isBackground*/, | ||
isDarkMode, | ||
false /*shouldAdaptFontColor*/, | ||
darkColorHandler | ||
); | ||
} | ||
|
||
if (currentStyle.backgroundColors || currentStyle.backgroundColor) { | ||
setColor( | ||
span, | ||
(currentStyle.backgroundColors || currentStyle.backgroundColor)!, | ||
true /*isBackground*/, | ||
isDarkMode, | ||
false /*shouldAdaptFontColor*/, | ||
darkColorHandler | ||
); | ||
} | ||
} | ||
|
||
if (span) { | ||
callback(span); | ||
} | ||
|
||
return span; | ||
} | ||
} |