Skip to content

Commit

Permalink
Rewrite Hyperlink plugin, fix #131 (#181)
Browse files Browse the repository at this point in the history
* Rewrite Hyperlink plugin, fix #131

* Fix comment

* fix comment

* fix comment
  • Loading branch information
JiuqingSong authored Dec 12, 2018
1 parent 197fe92 commit de7e9db
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 123 deletions.
13 changes: 13 additions & 0 deletions packages/roosterjs-editor-core/lib/editor/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,19 @@ export default class Editor {
});
}

/**
* Set DOM attribute of editor content DIV
* @param name Name of the attribute
* @param value Value of the attribute
*/
public setEditorDomAttribute(name: string, value: string) {
if (value === null) {
this.core.contentDiv.removeAttribute(name);
} else {
this.core.contentDiv.setAttribute(name, value);
}
}

//#endregion

//#region Deprecated methods
Expand Down
167 changes: 45 additions & 122 deletions packages/roosterjs-editor-plugins/lib/HyperLink/HyperLink.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import { Browser } from 'roosterjs-editor-dom';
import { ChangeSource, PluginEvent, PluginEventType } from 'roosterjs-editor-types';
import { Editor, EditorPlugin } from 'roosterjs-editor-core';

const TEMP_TITLE = 'istemptitle';
const TEMP_TITLE_REGEX = new RegExp(
`<a\\s+([^>]*\\s+)?(title|${TEMP_TITLE})="[^"]*"\\s*([^>]*)\\s+(title|${TEMP_TITLE})="[^"]*"(\\s+[^>]*)?>`,
'gm'
);
import { PluginEvent, PluginEventType } from 'roosterjs-editor-types';

/**
* An editor plugin that show a tooltip for existing link
*/
export default class HyperLink implements EditorPlugin {
private editor: Editor;
public name: 'HyperLink';
private editor: Editor;
private disposers: (() => void)[];

/**
* Create a new instance of HyperLink class
Expand All @@ -23,9 +18,9 @@ export default class HyperLink implements EditorPlugin {
* @param onLinkClick (Optional) Open link callback
*/
constructor(
private getTooltipCallback: (href: string) => string = href => href,
private getTooltipCallback: (href: string, a: HTMLAnchorElement) => string = href => href,
private target?: string,
private onLinkClick?: (anchor: HTMLAnchorElement, keyboardEvent: KeyboardEvent) => void
private onLinkClick?: (anchor: HTMLAnchorElement, mouseEvent: MouseEvent) => void
) {}

/**
Expand All @@ -34,13 +29,32 @@ export default class HyperLink implements EditorPlugin {
*/
public initialize(editor: Editor): void {
this.editor = editor;
this.disposers = this.getTooltipCallback
? [
editor.addDomEventHandler('mouseover', this.onMouse),
editor.addDomEventHandler('mouseout', this.onMouse),
]
: [];
}

protected onMouse = (e: MouseEvent) => {
const a = this.editor.getElementAtCursor('a[href]', e.srcElement) as HTMLAnchorElement;
const href = this.tryGetHref(a);

if (href) {
this.editor.setEditorDomAttribute(
'title',
e.type == 'mouseover' ? this.getTooltipCallback(href, a) : null
);
}
};

/**
* Dispose this plugin
*/
public dispose(): void {
this.editor.queryElements('a[href]', this.resetAnchor);
this.disposers.forEach(disposer => disposer());
this.disposers = null;
this.editor = null;
}

Expand All @@ -49,124 +63,33 @@ export default class HyperLink implements EditorPlugin {
* @param event The event object
*/
public onPluginEvent(event: PluginEvent): void {
switch (event.eventType) {
case PluginEventType.EditorReady:
this.editor.queryElements('a[href]', this.processLink);
break;

case PluginEventType.ContentChanged:
if (event.source == ChangeSource.CreateLink) {
this.resetAnchor(event.data as HTMLAnchorElement);
}

let anchors = this.editor.queryElements('a[href]');
if (anchors.length > 0) {
// 1. Cache existing snapshot
let snapshotBeforeProcessLink = this.getSnapshot();

// 2. Process links
anchors.forEach(this.processLink);

// 3. See if cached snapshot is the same with lastSnapshot
// Same snapshot means content isn't changed by other plugins,
// Then we can overwrite the sanpshot here to avoid Undo plugin
// adding undo snapshot for the link title attribute change
if (snapshotBeforeProcessLink == event.lastSnapshot) {
// Overwrite last snapshot to suppress undo for the temp properties
event.lastSnapshot = this.editor.getContent(false, true);
}
if (event.eventType == PluginEventType.MouseUp) {
const anchor = this.editor.getElementAtCursor(
'A',
event.rawEvent.srcElement
) as HTMLAnchorElement;

if (anchor) {
if (this.onLinkClick) {
this.onLinkClick(anchor, event.rawEvent);
return;
}

break;

case PluginEventType.ExtractContent:
event.content = this.removeTempTooltip(event.content);
break;
}
}

private getSnapshot() {
return this.editor.getContent(
false /*triggerContentChangedEvent*/,
true /*addSelectionMarker*/
);
}

private resetAnchor = (a: HTMLAnchorElement) => {
try {
if (a.getAttribute(TEMP_TITLE)) {
a.removeAttribute(TEMP_TITLE);
a.removeAttribute('title');
}
a.removeEventListener('mouseup', this.onLinkMouseUp);
} catch (e) {}
};

private processLink = (a: HTMLAnchorElement) => {
if (!a.title && this.getTooltipCallback) {
a.setAttribute(TEMP_TITLE, 'true');
a.title = this.getTooltipCallback(this.tryGetHref(a));
}
a.addEventListener('mouseup', this.onLinkMouseUp);
};

private removeTempTooltip(content: string): string {
return content.replace(
TEMP_TITLE_REGEX,
(...groups: string[]): string => {
const firstValue = groups[1] == null ? '' : groups[1].trim();
const secondValue = groups[3] == null ? '' : groups[3].trim();
const thirdValue = groups[5] == null ? '' : groups[5].trim();

// possible values (* is space, x, y, z are the first, second, and third value respectively):
// *** (no values) << empty case
// x** (first value only)
// *x* (second value only)
// **x (third value only)
// x*y* (first and second)
// x**z (first and third) << double spaces
// *y*z (second and third)
// x*y*z (all)
let href: string;
if (
firstValue.length === 0 &&
secondValue.length === 0 &&
thirdValue.length === 0
!Browser.isFirefox &&
(href = this.tryGetHref(anchor)) &&
(Browser.isMac ? event.rawEvent.metaKey : event.rawEvent.ctrlKey)
) {
return '<a>';
try {
const target = this.target || '_blank';
const window = this.editor.getDocument().defaultView;
window.open(href, target);
} catch {}
}

let result;
if (secondValue.length === 0) {
result = `${firstValue} ${thirdValue}`;
} else {
result = `${firstValue} ${secondValue} ${thirdValue}`;
}

return `<a ${result.trim()}>`;
}
);
}

private onLinkMouseUp = (keyboardEvent: KeyboardEvent) => {
const anchor = this.editor.getElementAtCursor('A', keyboardEvent.srcElement) as HTMLAnchorElement;
if (this.onLinkClick) {
this.onLinkClick(anchor, keyboardEvent);
return;
}

let href: string;
if (
!Browser.isFirefox &&
(href = this.tryGetHref(anchor)) &&
(Browser.isMac ? keyboardEvent.metaKey : keyboardEvent.ctrlKey)
) {
try {
const target = this.target || '_blank';
const window = this.editor.getDocument().defaultView;
window.open(href, target);
} catch {}
}
};
}

/**
* Try get href from an anchor element
Expand Down
2 changes: 1 addition & 1 deletion packages/roosterjs-plugin-image-resize/lib/ImageResize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ export default class ImageResize implements EditorPlugin {
private stopEvent = (e: UIEvent) => {
e.stopPropagation();
e.preventDefault();
}
};

private removeResizeDiv(resizeDiv: HTMLElement) {
if (this.editor && this.editor.contains(resizeDiv)) {
Expand Down

0 comments on commit de7e9db

Please sign in to comment.