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

fix: open link on Mod-Click #130

Merged
merged 14 commits into from
Jul 20, 2022
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