Skip to content

Commit

Permalink
Port Hyperlink plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
JiuqingSong committed Apr 4, 2024
1 parent f34b360 commit 6ab15e4
Show file tree
Hide file tree
Showing 11 changed files with 642 additions and 41 deletions.
10 changes: 9 additions & 1 deletion demo/scripts/controlsV2/mainPane/MainPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { getDarkColor } from 'roosterjs-color-utils';
import { getPresetModelById } from '../sidePane/presets/allPresets/allPresets';
import { getTabs, tabNames } from '../tabs/getTabs';
import { getTheme } from '../theme/themes';
import { OptionState } from '../sidePane/editorOptions/OptionState';
import { OptionState, UrlPlaceholder } from '../sidePane/editorOptions/OptionState';
import { popoutButton } from '../demoButtons/popoutButton';
import { PresetPlugin } from '../sidePane/presets/PresetPlugin';
import { redoButton } from '../roosterjsReact/ribbon/buttons/redoButton';
Expand All @@ -47,6 +47,7 @@ import {
import {
AutoFormatPlugin,
EditPlugin,
HyperlinkPlugin,
MarkdownPlugin,
PastePlugin,
ShortcutPlugin,
Expand Down Expand Up @@ -476,6 +477,7 @@ export class MainPane extends React.Component<{}, MainPaneState> {
watermarkText,
markdownOptions,
autoFormatOptions,
linkTitle,
} = this.state.initState;
return [
pluginList.autoFormat && new AutoFormatPlugin(autoFormatOptions),
Expand All @@ -492,6 +494,12 @@ export class MainPane extends React.Component<{}, MainPaneState> {
pluginList.contextMenu && listMenu && createListEditMenuProvider(),
pluginList.contextMenu && tableMenu && createTableEditMenuProvider(),
pluginList.contextMenu && imageMenu && createImageEditMenuProvider(),
pluginList.hyperlink &&
new HyperlinkPlugin(
linkTitle?.indexOf(UrlPlaceholder) >= 0
? url => linkTitle.replace(UrlPlaceholder, url)
: linkTitle
),
].filter(x => !!x);
}
}
Expand Down
25 changes: 3 additions & 22 deletions demo/scripts/controlsV2/plugins/createLegacyPlugins.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,12 @@
import { Announce, ContentEdit, CustomReplace, ImageEdit } from 'roosterjs-editor-plugins';
import { EditorPlugin as LegacyEditorPlugin, KnownAnnounceStrings } from 'roosterjs-editor-types';
import {
Announce,
ContentEdit,
CustomReplace,
HyperLink,
ImageEdit,
} from 'roosterjs-editor-plugins';
import {
LegacyPluginList,
OptionState,
UrlPlaceholder,
} from '../sidePane/editorOptions/OptionState';
import { LegacyPluginList, OptionState } from '../sidePane/editorOptions/OptionState';

export function createLegacyPlugins(initState: OptionState): LegacyEditorPlugin[] {
const { pluginList, linkTitle } = initState;
const { pluginList } = initState;

const plugins: Record<keyof LegacyPluginList, LegacyEditorPlugin | null> = {
contentEdit: pluginList.contentEdit ? new ContentEdit(initState.contentEditFeatures) : null,
hyperlink: pluginList.hyperlink
? new HyperLink(
linkTitle?.indexOf(UrlPlaceholder) >= 0
? url => linkTitle.replace(UrlPlaceholder, url)
: linkTitle
? () => linkTitle
: null
)
: null,
imageEdit: pluginList.imageEdit
? new ImageEdit({
preserveRatio: initState.forcePreserveRatio,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ const initialState: OptionState = {
pasteOption: true,
sampleEntity: true,
markdown: true,
hyperlink: true,

// Legacy plugins
contentEdit: false,
hyperlink: false,
imageEdit: false,
customReplace: false,
announce: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import type { ContentModelSegmentFormat } from 'roosterjs-content-model-types';

export interface LegacyPluginList {
contentEdit: boolean;
hyperlink: boolean;
imageEdit: boolean;
customReplace: boolean;
announce: boolean;
Expand All @@ -23,6 +22,7 @@ export interface NewPluginList {
pasteOption: boolean;
sampleEntity: boolean;
markdown: boolean;
hyperlink: boolean;
}

export interface BuildInPluginList extends LegacyPluginList, NewPluginList {}
Expand Down
28 changes: 14 additions & 14 deletions demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import ContentEditFeatures from './ContentEditFeatures';
import { UrlPlaceholder } from './OptionState';
import type {
import {
UrlPlaceholder,
BuildInPluginList,
LegacyPluginList,
NewPluginList,
Expand Down Expand Up @@ -103,7 +103,6 @@ abstract class PluginsBase<PluginKey extends keyof BuildInPluginList> extends Re
}

export class LegacyPlugins extends PluginsBase<keyof LegacyPluginList> {
private linkTitle = React.createRef<HTMLInputElement>();
private forcePreserveRatio = React.createRef<HTMLInputElement>();

render() {
Expand All @@ -118,17 +117,6 @@ export class LegacyPlugins extends PluginsBase<keyof LegacyPluginList> {
resetState={this.props.resetState}
/>
)}
{this.renderPluginItem(
'hyperlink',
'Hyperlink Plugin',
this.renderInputBox(
'Label title: ',
this.linkTitle,
this.props.state.linkTitle,
'Use "' + UrlPlaceholder + '" for the url string',
(state, value) => (state.linkTitle = value)
)
)}
{this.renderPluginItem(
'imageEdit',
'Image Edit Plugin',
Expand All @@ -153,6 +141,7 @@ export class Plugins extends PluginsBase<keyof NewPluginList> {
private tableMenu = React.createRef<HTMLInputElement>();
private imageMenu = React.createRef<HTMLInputElement>();
private watermarkText = React.createRef<HTMLInputElement>();
private linkTitle = React.createRef<HTMLInputElement>();

render(): JSX.Element {
return (
Expand Down Expand Up @@ -210,6 +199,17 @@ export class Plugins extends PluginsBase<keyof NewPluginList> {
{this.renderPluginItem('emoji', 'Emoji')}
{this.renderPluginItem('pasteOption', 'PasteOptions')}
{this.renderPluginItem('sampleEntity', 'SampleEntity')}
{this.renderPluginItem(
'hyperlink',
'Hyperlink Plugin',
this.renderInputBox(
'Label title: ',
this.linkTitle,
this.props.state.linkTitle,
'Use "' + UrlPlaceholder + '" for the url string',
(state, value) => (state.linkTitle = value)
)
)}
</tbody>
</table>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class HyperLinkCode extends CodeElement {
}

getCode() {
return 'new roosterjsLegacy.HyperLink(' + this.getLinkCallback() + ')';
return 'new roosterjs.HyperlinkPlugin(' + this.getLinkCallback() + ')';
}

private getLinkCallback() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export class PluginsCode extends PluginsCodeBase {
pluginList.shortcut && new ShortcutPluginCode(),
pluginList.watermark && new WatermarkCode(state.watermarkText),
pluginList.markdown && new MarkdownCode(state.markdownOptions),
pluginList.hyperlink && new HyperLinkCode(state.linkTitle),
]);
}
}
Expand All @@ -56,7 +57,6 @@ export class LegacyPluginCode extends PluginsCodeBase {

const plugins: CodeElement[] = [
pluginList.contentEdit && new ContentEditCode(state.contentEditFeatures),
pluginList.hyperlink && new HyperLinkCode(state.linkTitle),
pluginList.imageEdit && new ImageEditCode(),
pluginList.customReplace && new CustomReplaceCode(),
];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { matchLink } from 'roosterjs-content-model-api';
import type { HyperlinkToolTip } from './HyperlinkToolTip';
import type {
DOMHelper,
EditorPlugin,
IEditor,
PluginEvent,
LinkData,
} from 'roosterjs-content-model-types';

const defaultToolTipCallback: HyperlinkToolTip = (url: string) => url;

/**
* Hyperlink plugin does the following jobs for a hyperlink in editor:
* 1. When hover on a link, show a tool tip
* 2. When Ctrl+Click on a link, open a new window with the link
* 3. When type directly on a link whose text matches its link url, update the link url with the link text
*/
export class HyperlinkPlugin implements EditorPlugin {
private editor: IEditor | null = null;
private domHelper: DOMHelper | null = null;
private isMac: boolean = false;
private disposer: (() => void) | null = null;

private currentNode: Node | null = null;
private currentLink: HTMLAnchorElement | null = null;

/**
* Create a new instance of HyperLink class
* @param tooltip Tooltip to show when mouse hover over a link
* Default value is to return the href itself. If null, there will be no tooltip text.
* @param target (Optional) Target window name for hyperlink. If null, will use "_blank"
* @param onLinkClick (Optional) Open link callback (return false to use default behavior)
*/
constructor(
private tooltip: HyperlinkToolTip = defaultToolTipCallback,
private target?: string,
private onLinkClick?: (anchor: HTMLAnchorElement, mouseEvent: MouseEvent) => boolean | void
) {}

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

/**
* Initialize this plugin
* @param editor The editor instance
*/
public initialize(editor: IEditor): void {
this.editor = editor;
this.domHelper = editor.getDOMHelper();
this.isMac = !!editor.getEnvironment().isMac;
this.disposer = editor.attachDomEvent({
mouseover: { beforeDispatch: this.onMouse },
mouseout: { beforeDispatch: this.onMouse },
});
}

/**
* Dispose this plugin
*/
public dispose(): void {
if (this.disposer) {
this.disposer();
this.disposer = null;
}
this.editor = null;
}

/**
* Handle events triggered from editor
* @param event PluginEvent object
*/
public onPluginEvent(event: PluginEvent): void {
let matchedLink: LinkData | null;

if (event.eventType == 'keyDown') {
const selection = this.editor?.getDOMSelection();
const node =
selection?.type == 'range' ? selection.range.commonAncestorContainer : null;

if (node && node != this.currentNode) {
this.currentNode = node;
this.currentLink = null;

this.runWithHyperlink(node, (href, a) => {
if (
node.textContent &&
(matchedLink = matchLink(node.textContent)) &&
matchedLink.normalizedUrl == href
) {
this.currentLink = a;
}
});
}
} else if (event.eventType == 'keyUp') {
const selection = this.editor?.getDOMSelection();
const node =
selection?.type == 'range' ? selection.range.commonAncestorContainer : null;

if (
node &&
node == this.currentNode &&
this.currentLink &&
this.currentLink.contains(node) &&
node.textContent &&
(matchedLink = matchLink(node.textContent))
) {
this.currentLink.setAttribute('href', matchedLink.normalizedUrl);
}
} else if (event.eventType == 'mouseUp' && event.isClicking) {
this.runWithHyperlink(event.rawEvent.target as Node, (href, anchor) => {
if (
!this.onLinkClick?.(anchor, event.rawEvent) &&
this.isCtrlOrMetaPressed(event.rawEvent) &&
event.rawEvent.button === 0
) {
event.rawEvent.preventDefault();

const target = this.target || '_blank';
const window = this.editor?.getDocument().defaultView;

try {
window?.open(href, target);
} catch {}
}
});
}
}

protected onMouse = (e: Event) => {
this.runWithHyperlink(e.target as Node, (href, a) => {
const tooltip =
e.type == 'mouseover'
? typeof this.tooltip == 'function'
? this.tooltip(href, a)
: this.tooltip
: null;
this.domHelper?.setDomAttribute('title', tooltip);
});
};

private runWithHyperlink(node: Node, callback: (href: string, a: HTMLAnchorElement) => void) {
const a = this.domHelper?.findClosestElementAncestor(
node,
'a[href]'
) as HTMLAnchorElement | null;
const href = a?.getAttribute('href');

if (href && a) {
callback(href, a);
}
}

private isCtrlOrMetaPressed(event: KeyboardEvent | MouseEvent): boolean {
return this.isMac ? event.metaKey : event.ctrlKey;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* A type to specify how to get a tool tip of hyperlink in editor
* string: Use this string as tooltip
* null: No tooltip
* function: Call this function to get a tooltip
*/
export type HyperlinkToolTip = string | null | ((url: string, anchor: HTMLAnchorElement) => string);
2 changes: 2 additions & 0 deletions packages/roosterjs-content-model-plugins/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ export { ContextMenuPluginBase, ContextMenuOptions } from './contextMenuBase/Con
export { WatermarkPlugin } from './watermark/WatermarkPlugin';
export { WatermarkFormat } from './watermark/WatermarkFormat';
export { MarkdownPlugin, MarkdownOptions } from './markdown/MarkdownPlugin';
export { HyperlinkPlugin } from './hyperlink/HyperlinkPlugin';
export { HyperlinkToolTip } from './hyperlink/HyperlinkToolTip';
Loading

0 comments on commit 6ab15e4

Please sign in to comment.