Skip to content

Commit

Permalink
feat(link-editor): open link on Mod-Click (#130)
Browse files Browse the repository at this point in the history
fixes #62
  • Loading branch information
dancormier committed Jul 20, 2022
1 parent d29ef9c commit ea8da4b
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 15 deletions.
53 changes: 42 additions & 11 deletions src/rich-text/plugins/link-editor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { toggleMark } from "prosemirror-commands";
import { Mark } from "prosemirror-model";
import { Mark, MarkType } from "prosemirror-model";
import {
EditorState,
PluginView,
Expand All @@ -13,7 +13,11 @@ import {
PluginInterfaceView,
} from "../../shared/prosemirror-plugins/interface-manager";
import { StatefulPlugin } from "../../shared/prosemirror-plugins/plugin-extensions";
import { escapeHTML, generateRandomId } from "../../shared/utils";
import {
escapeHTML,
generateRandomId,
getPlatformModKey,
} from "../../shared/utils";
import { CommonmarkParserFeatures } from "../../shared/view";

/**
Expand Down Expand Up @@ -482,9 +486,10 @@ class LinkTooltip {
return;
}

const link = start.node.marks.find(
(mark) => mark.type === state.schema.marks.link
);
const link = findMarksInSet(
start.node.marks,
state.schema.marks.link
)[0];
if (!link) {
return;
}
Expand Down Expand Up @@ -520,16 +525,12 @@ class LinkTooltip {
const linkMarks: Mark[][] = [];
const { to, from, $from, empty } = state.selection;
if (empty) {
return $from
.marks()
.filter((mark) => mark.type === state.schema.marks.link);
return findMarksInSet($from.marks(), state.schema.marks.link);
}
if (to > from) {
state.doc.nodesBetween(from, to, (node) => {
linkMarks.push(
node.marks.filter(
(mark) => mark.type === state.schema.marks.link
)
findMarksInSet(node.marks, state.schema.marks.link)
);
});
}
Expand Down Expand Up @@ -650,6 +651,27 @@ export const linkEditorPlugin = (features: CommonmarkParserFeatures) =>
return false;
},
},
/** Handle mod-click to open links in a new window */
handleClick(view, pos, event) {
const mark = findMarksInSet(
view.state.doc.resolve(pos).marks(),
view.state.schema.marks.link
)[0];

const modPressed =
getPlatformModKey() === "Cmd"
? event.metaKey
: event.ctrlKey;

const handled = mark && modPressed;

// a link was mod-clicked, so open it in a new window
if (handled) {
window.open(mark.attrs.href, "_blank");
}

return !!handled;
},
},
view(editorView): PluginView {
return new LinkEditor(editorView, features.validateLink);
Expand Down Expand Up @@ -691,3 +713,12 @@ export function hideLinkEditor(view: EditorView): void {
view.dispatch(tr);
}
}

/**
* Finds all marks in a set that are of the given type
* @param marks The set of marks to search
* @param type The type of mark to find
*/
function findMarksInSet(marks: readonly Mark[], type: MarkType): Mark[] {
return marks.filter((mark) => mark.type === type);
}
10 changes: 6 additions & 4 deletions src/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,11 @@ export function escapeHTML(
return output;
}

/** Gets the modifier key for the current platform; i.e. "Command" on macOS and "Control" elsewhere */
export function getPlatformModKey(): "Cmd" | "Ctrl" {
return /Mac|iP(hone|[oa]d)/.test(navigator.platform) ? "Cmd" : "Ctrl";
}

/**
* Returns a string containing the label and readable keyboard shortcut for button tooltips
* @param mapping Corresponding command mapping (keyboard shortcut)
Expand All @@ -234,10 +239,7 @@ export function getShortcut(mapping: string): string {
return mapping;
}

return (
(/Mac|iP(hone|[oa]d)/.test(navigator.platform) ? "Cmd" : "Ctrl") +
mapping.slice(3)
);
return getPlatformModKey() + mapping.slice(3);
}

/**
Expand Down
52 changes: 52 additions & 0 deletions test/rich-text/plugins/link-editor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,58 @@ describe("link-editor", () => {
return promise.then(() => cleanupPasteSupport());
});
});

describe("props", () => {
it("should open links in a new window on mod-click", () => {
const mock = jest.fn();

// mock the window.open function
window.open = mock;

const plugin = linkEditorPlugin({});
const view = createView(
createState(
`<p>test<a href="https://www.example.com">link</a></p>`,
[plugin]
)
);

// call the prop fn directly instead of playing around with event dispatching
// regular click
let result = plugin.props.handleClick(
view,
6,
new MouseEvent("click", {})
);

expect(result).toBe(false);
expect(mock).toHaveBeenCalledTimes(0);

// mod-click
result = plugin.props.handleClick(
view,
6,
new MouseEvent("click", {
ctrlKey: true,
})
);

expect(result).toBe(true);
expect(mock).toHaveBeenCalledTimes(1);

// mod-click, but not on a link
result = plugin.props.handleClick(
view,
1,
new MouseEvent("click", {
ctrlKey: true,
})
);

expect(result).toBe(false);
expect(mock).toHaveBeenCalledTimes(1);
});
});
});

function getDecorations(state: EditorState) {
Expand Down
21 changes: 21 additions & 0 deletions test/shared/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
escapeHTML,
stackOverflowValidateLink,
getShortcut,
getPlatformModKey,
} from "../../src/shared/utils";

const setNavigatorProperty = (
Expand Down Expand Up @@ -155,6 +156,26 @@ describe("utils", () => {
);
});

describe("getPlatformModKey", () => {
it("should return `Cmd` when platform contains `Mac` and `Ctrl` otherwise", () => {
// Set the platform to macOS
setNavigatorProperty("platform", "MacIntel");

let mod = getPlatformModKey();
expect(mod).toBe("Cmd");
expect(navigator.platform).toBe("MacIntel");

// Set the platform to Windows
setNavigatorProperty("platform", "Win32");
mod = getPlatformModKey();
expect(mod).toBe("Ctrl");
expect(navigator.platform).toBe("Win32");

// Reset the platform
setNavigatorProperty("platform", "");
});
});

describe("getShortcut", () => {
it("should replace `Mod` with `Cmd` when platform contains `Mac`", () => {
// Set the platform to macOS
Expand Down

0 comments on commit ea8da4b

Please sign in to comment.