Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Markdown and Autoformat plugins to use formatTextSegmentBeforeSelectionMarker #2557

Merged
merged 10 commits into from
Apr 4, 2024
9 changes: 2 additions & 7 deletions demo/scripts/controlsV2/mainPane/MainPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -475,15 +475,10 @@ export class MainPane extends React.Component<{}, MainPaneState> {
imageMenu,
watermarkText,
markdownOptions,
autoFormatOptions,
} = this.state.initState;
return [
pluginList.autoFormat &&
new AutoFormatPlugin({
autoBullet: true,
autoNumbering: true,
autoUnlink: true,
autoLink: true,
}),
pluginList.autoFormat && new AutoFormatPlugin(autoFormatOptions),
pluginList.edit && new EditPlugin(),
pluginList.paste && new PastePlugin(allowExcelNoBorderTable),
pluginList.shortcut && new ShortcutPlugin(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const initialState: OptionState = {
autoLink: true,
autoNumbering: true,
autoUnlink: false,
autoHyphen: true,
},
markdownOptions: {
bold: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { createLink } from './link/createLink';
import { createLinkAfterSpace } from './link/createLinkAfterSpace';
import { formatTextSegmentBeforeSelectionMarker } from '../pluginUtils/formatTextSegmentBeforeSelectionMarker';
import { keyboardListTrigger } from './list/keyboardListTrigger';
import { transformHyphen } from './hyphen/transformHyphen';
import { unlink } from './link/unlink';
import type {
ContentChangedEvent,
Expand Down Expand Up @@ -34,6 +36,11 @@ export type AutoFormatOptions = {
* When paste content, create hyperlink for the pasted link
*/
autoLink: boolean;

/**
* Transform -- into hyphen, if typed between two words
*/
autoHyphen: boolean;
};

/**
Expand All @@ -44,6 +51,7 @@ const DefaultOptions: Required<AutoFormatOptions> = {
autoNumbering: false,
autoUnlink: false,
autoLink: false,
autoHyphen: false,
};

/**
Expand All @@ -52,13 +60,13 @@ const DefaultOptions: Required<AutoFormatOptions> = {
*/
export class AutoFormatPlugin implements EditorPlugin {
private editor: IEditor | null = null;

/**
* @param options An optional parameter that takes in an object of type AutoFormatOptions, which includes the following properties:
* - autoBullet: A boolean that enables or disables automatic bullet list formatting. Defaults to false.
* - autoNumbering: A boolean that enables or disables automatic numbering formatting. Defaults to false.
* - autoLink: A boolean that enables or disables automatic hyperlink creation when pasting or typing content. Defaults to false.
* - autoUnlink: A boolean that enables or disables automatic hyperlink removal when pressing backspace. Defaults to false.
* - autoHyphen: A boolean that enables or disables automatic hyphen transformation. Defaults to false.
*/
constructor(private options: AutoFormatOptions = DefaultOptions) {}

Expand Down Expand Up @@ -112,14 +120,52 @@ export class AutoFormatPlugin implements EditorPlugin {

private handleEditorInputEvent(editor: IEditor, event: EditorInputEvent) {
const rawEvent = event.rawEvent;
if (rawEvent.inputType === 'insertText') {
const selection = editor.getDOMSelection();
if (
rawEvent.inputType === 'insertText' &&
selection &&
selection.type === 'range' &&
selection.range.collapsed
) {
switch (rawEvent.data) {
case ' ':
const { autoBullet, autoNumbering, autoLink } = this.options;
keyboardListTrigger(editor, autoBullet, autoNumbering);
if (autoLink) {
createLinkAfterSpace(editor);
}
formatTextSegmentBeforeSelectionMarker(
editor,
(model, previousSegment, paragraph, context) => {
const {
autoBullet,
autoNumbering,
autoLink,
autoHyphen,
} = this.options;
let shouldHyphen = false;
let shouldLink = false;

if (autoLink) {
shouldLink = createLinkAfterSpace(
previousSegment,
paragraph,
context
);
}

if (autoHyphen) {
shouldHyphen = transformHyphen(previousSegment, paragraph, context);
}

return (
keyboardListTrigger(
model,
paragraph,
context,
autoBullet,
autoNumbering
) ||
shouldHyphen ||
shouldLink
);
}
);
break;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { splitTextSegment } from '../../pluginUtils/splitTextSegment';
import type {
ContentModelParagraph,
ContentModelText,
FormatContentModelContext,
} from 'roosterjs-content-model-types';

/**
* @internal
*/
export function transformHyphen(
previousSegment: ContentModelText,
paragraph: ContentModelParagraph,
context: FormatContentModelContext
): boolean {
const segments = previousSegment.text.split(' ');
const dashes = segments[segments.length - 2];
if (dashes === '--') {
const textIndex = previousSegment.text.lastIndexOf('--');
const textSegment = splitTextSegment(previousSegment, paragraph, textIndex, textIndex + 2);

textSegment.text = textSegment.text.replace('--', '—');
context.canUndoByBackspace = true;
return true;
} else {
const text = segments.pop();
const hasDashes = text && text?.indexOf('--') > -1;
if (hasDashes && text.trim() !== '--') {
const textIndex = previousSegment.text.indexOf(text);
const textSegment = splitTextSegment(
previousSegment,
paragraph,
textIndex,
textIndex + text.length - 1
);

const textLength = textSegment.text.length;
if (textSegment.text[0] !== '-' && textSegment.text[textLength - 1] !== '-') {
textSegment.text = textSegment.text.replace('--', '—');
context.canUndoByBackspace = true;
return true;
}
}
}
return false;
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { addLink } from 'roosterjs-content-model-dom';
import { getLinkSegment } from './getLinkSegment';
import type { IEditor } from 'roosterjs-content-model-types';
import { formatTextSegmentBeforeSelectionMarker } from '../../pluginUtils/formatTextSegmentBeforeSelectionMarker';
import { matchLink } from 'roosterjs-content-model-api';
import type { IEditor, LinkData } from 'roosterjs-content-model-types';

/**
* @internal
*/
export function createLink(editor: IEditor) {
editor.formatContentModel(model => {
const link = getLinkSegment(model);
if (link && !link.link) {
addLink(link, {
formatTextSegmentBeforeSelectionMarker(editor, (_model, linkSegment, _paragraph) => {
let linkData: LinkData | null = null;
if (!linkSegment.link && (linkData = matchLink(linkSegment.text))) {
addLink(linkSegment, {
format: {
href: link.text,
href: linkData.normalizedUrl,
underline: true,
},
dataset: {},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,57 +1,41 @@
import { getSelectedSegmentsAndParagraphs } from 'roosterjs-content-model-dom';
import { matchLink } from 'roosterjs-content-model-api';
import { splitTextSegment } from '../../pluginUtils/splitTextSegment';
import type { IEditor, LinkData } from 'roosterjs-content-model-types';
import type {
ContentModelParagraph,
ContentModelText,
FormatContentModelContext,
LinkData,
} from 'roosterjs-content-model-types';

/**
* @internal
*/
export function createLinkAfterSpace(editor: IEditor) {
editor.formatContentModel((model, context) => {
const selectedSegmentsAndParagraphs = getSelectedSegmentsAndParagraphs(
model,
false /* includingFormatHolder */
export function createLinkAfterSpace(
previousSegment: ContentModelText,
paragraph: ContentModelParagraph,
context: FormatContentModelContext
) {
const link = previousSegment.text.split(' ').pop();
const url = link?.trim();
let linkData: LinkData | null = null;
if (url && link && (linkData = matchLink(url))) {
const linkSegment = splitTextSegment(
previousSegment,
paragraph,
previousSegment.text.length - link.trimLeft().length,
previousSegment.text.trimRight().length
);
if (selectedSegmentsAndParagraphs.length > 0 && selectedSegmentsAndParagraphs[0][1]) {
const markerIndex = selectedSegmentsAndParagraphs[0][1].segments.findIndex(
segment => segment.segmentType == 'SelectionMarker'
);
const paragraph = selectedSegmentsAndParagraphs[0][1];
if (markerIndex > 0) {
const textSegment = paragraph.segments[markerIndex - 1];
const marker = paragraph.segments[markerIndex];
if (
marker.segmentType == 'SelectionMarker' &&
textSegment &&
textSegment.segmentType == 'Text' &&
!textSegment.link
) {
const link = textSegment.text.split(' ').pop();
const url = link?.trim();
let linkData: LinkData | null = null;
if (url && link && (linkData = matchLink(url))) {
const linkSegment = splitTextSegment(
textSegment,
paragraph,
textSegment.text.length - link.trimLeft().length,
textSegment.text.trimRight().length
);
linkSegment.link = {
format: {
href: linkData.normalizedUrl,
underline: true,
},
dataset: {},
};
linkSegment.link = {
format: {
href: linkData.normalizedUrl,
underline: true,
},
dataset: {},
};

context.canUndoByBackspace = true;
context.canUndoByBackspace = true;

return true;
}
}
}
}

return false;
});
return true;
}
return false;
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { getLinkSegment } from './getLinkSegment';
import { formatTextSegmentBeforeSelectionMarker } from '../../pluginUtils/formatTextSegmentBeforeSelectionMarker';

import type { IEditor } from 'roosterjs-content-model-types';

/**
* @internal
*/
export function unlink(editor: IEditor, rawEvent: KeyboardEvent) {
editor.formatContentModel(model => {
const link = getLinkSegment(model);
if (link?.link) {
link.link = undefined;
formatTextSegmentBeforeSelectionMarker(editor, (_model, linkSegment, _paragraph) => {
if (linkSegment?.link) {
linkSegment.link = undefined;
rawEvent.preventDefault();

return true;
}

return false;
});
}
Loading
Loading