@atlaskit/editor-plugin-hyperlink 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +6 -0
- package/CHANGELOG.md +1 -0
- package/LICENSE.md +13 -0
- package/README.md +7 -0
- package/dist/cjs/Toolbar.js +280 -0
- package/dist/cjs/commands.js +242 -0
- package/dist/cjs/index.js +12 -0
- package/dist/cjs/plugin.js +122 -0
- package/dist/cjs/pm-plugins/fake-curor-for-toolbar-plugin-key.js +9 -0
- package/dist/cjs/pm-plugins/fake-cursor-for-toolbar.js +68 -0
- package/dist/cjs/pm-plugins/input-rule.js +95 -0
- package/dist/cjs/pm-plugins/keymap.js +75 -0
- package/dist/cjs/pm-plugins/main.js +257 -0
- package/dist/cjs/pm-plugins/toolbar-buttons.js +43 -0
- package/dist/cjs/version.json +5 -0
- package/dist/es2019/Toolbar.js +260 -0
- package/dist/es2019/commands.js +225 -0
- package/dist/es2019/index.js +1 -0
- package/dist/es2019/plugin.js +106 -0
- package/dist/es2019/pm-plugins/fake-curor-for-toolbar-plugin-key.js +2 -0
- package/dist/es2019/pm-plugins/fake-cursor-for-toolbar.js +63 -0
- package/dist/es2019/pm-plugins/input-rule.js +80 -0
- package/dist/es2019/pm-plugins/keymap.js +65 -0
- package/dist/es2019/pm-plugins/main.js +261 -0
- package/dist/es2019/pm-plugins/toolbar-buttons.js +39 -0
- package/dist/es2019/version.json +5 -0
- package/dist/esm/Toolbar.js +271 -0
- package/dist/esm/commands.js +222 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/plugin.js +114 -0
- package/dist/esm/pm-plugins/fake-curor-for-toolbar-plugin-key.js +2 -0
- package/dist/esm/pm-plugins/fake-cursor-for-toolbar.js +61 -0
- package/dist/esm/pm-plugins/input-rule.js +85 -0
- package/dist/esm/pm-plugins/keymap.js +67 -0
- package/dist/esm/pm-plugins/main.js +248 -0
- package/dist/esm/pm-plugins/toolbar-buttons.js +34 -0
- package/dist/esm/version.json +5 -0
- package/dist/types/Toolbar.d.ts +8 -0
- package/dist/types/commands.d.ts +20 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/plugin.d.ts +38 -0
- package/dist/types/pm-plugins/fake-curor-for-toolbar-plugin-key.d.ts +2 -0
- package/dist/types/pm-plugins/fake-cursor-for-toolbar.d.ts +3 -0
- package/dist/types/pm-plugins/input-rule.d.ts +8 -0
- package/dist/types/pm-plugins/keymap.d.ts +4 -0
- package/dist/types/pm-plugins/main.d.ts +7 -0
- package/dist/types/pm-plugins/toolbar-buttons.d.ts +21 -0
- package/dist/types-ts4.5/Toolbar.d.ts +8 -0
- package/dist/types-ts4.5/commands.d.ts +20 -0
- package/dist/types-ts4.5/index.d.ts +3 -0
- package/dist/types-ts4.5/plugin.d.ts +38 -0
- package/dist/types-ts4.5/pm-plugins/fake-curor-for-toolbar-plugin-key.d.ts +2 -0
- package/dist/types-ts4.5/pm-plugins/fake-cursor-for-toolbar.d.ts +3 -0
- package/dist/types-ts4.5/pm-plugins/input-rule.d.ts +8 -0
- package/dist/types-ts4.5/pm-plugins/keymap.d.ts +4 -0
- package/dist/types-ts4.5/pm-plugins/main.d.ts +7 -0
- package/dist/types-ts4.5/pm-plugins/toolbar-buttons.d.ts +21 -0
- package/package.json +107 -0
- package/tmp/api-report-tmp.d.ts +68 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { INPUT_METHOD } from '@atlaskit/editor-common/analytics';
|
|
2
|
+
import { addLinkMetadata } from '@atlaskit/editor-common/card';
|
|
3
|
+
import { findFilepaths, getLinkCreationAnalyticsEvent, isLinkInMatches, LinkMatcher, normalizeUrl } from '@atlaskit/editor-common/utils';
|
|
4
|
+
import { createPlugin, createRule } from '@atlaskit/prosemirror-input-rules';
|
|
5
|
+
export function createLinkInputRule(regexp, skipAnalytics = false, editorAnalyticsApi) {
|
|
6
|
+
// Plain typed text (eg, typing 'www.google.com') should convert to a hyperlink
|
|
7
|
+
return createRule(regexp, (state, match, start, end) => {
|
|
8
|
+
const {
|
|
9
|
+
schema
|
|
10
|
+
} = state;
|
|
11
|
+
if (state.doc.rangeHasMark(start, end, schema.marks.link)) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const link = match;
|
|
15
|
+
const url = normalizeUrl(link.url);
|
|
16
|
+
const markType = schema.mark('link', {
|
|
17
|
+
href: url
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Need access to complete text to check if last URL is part of a filepath before linkifying
|
|
21
|
+
const nodeBefore = state.selection.$from.nodeBefore;
|
|
22
|
+
if (!nodeBefore || !nodeBefore.isText || !nodeBefore.text) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const filepaths = findFilepaths(nodeBefore.text,
|
|
26
|
+
// The position referenced by 'start' is relative to the start of the document, findFilepaths deals with index in a node only.
|
|
27
|
+
start - (nodeBefore.text.length - link.text.length) // (start of link match) - (whole node text length - link length) gets start of text node, which is used as offset
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (isLinkInMatches(start, filepaths)) {
|
|
31
|
+
const tr = state.tr;
|
|
32
|
+
return tr;
|
|
33
|
+
}
|
|
34
|
+
const from = start;
|
|
35
|
+
const to = Math.min(start + link.text.length, state.doc.content.size);
|
|
36
|
+
const tr = state.tr.addMark(from, to, markType);
|
|
37
|
+
|
|
38
|
+
// Keep old behavior that will delete the space after the link
|
|
39
|
+
if (to === end) {
|
|
40
|
+
tr.insertText(' ');
|
|
41
|
+
}
|
|
42
|
+
addLinkMetadata(state.selection, tr, {
|
|
43
|
+
inputMethod: INPUT_METHOD.AUTO_DETECT
|
|
44
|
+
});
|
|
45
|
+
if (skipAnalytics) {
|
|
46
|
+
return tr;
|
|
47
|
+
}
|
|
48
|
+
editorAnalyticsApi === null || editorAnalyticsApi === void 0 ? void 0 : editorAnalyticsApi.attachAnalyticsEvent(getLinkCreationAnalyticsEvent(INPUT_METHOD.AUTO_DETECT, url))(tr);
|
|
49
|
+
return tr;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export function createInputRulePlugin(schema, skipAnalytics = false, featureFlags, editorAnalyticsApi) {
|
|
53
|
+
if (!schema.marks.link) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const urlWithASpaceRule = createLinkInputRule(LinkMatcher.create(), skipAnalytics, editorAnalyticsApi);
|
|
57
|
+
|
|
58
|
+
// [something](link) should convert to a hyperlink
|
|
59
|
+
const markdownLinkRule = createRule(/(^|[^!])\[(.*?)\]\((\S+)\)$/, (state, match, start, end) => {
|
|
60
|
+
const {
|
|
61
|
+
schema
|
|
62
|
+
} = state;
|
|
63
|
+
const [, prefix, linkText, linkUrl] = match;
|
|
64
|
+
const url = normalizeUrl(linkUrl).trim();
|
|
65
|
+
const markType = schema.mark('link', {
|
|
66
|
+
href: url
|
|
67
|
+
});
|
|
68
|
+
const tr = state.tr.replaceWith(start + prefix.length, end, schema.text((linkText || '').trim(), [markType]));
|
|
69
|
+
addLinkMetadata(state.selection, tr, {
|
|
70
|
+
inputMethod: INPUT_METHOD.FORMATTING
|
|
71
|
+
});
|
|
72
|
+
if (skipAnalytics) {
|
|
73
|
+
return tr;
|
|
74
|
+
}
|
|
75
|
+
editorAnalyticsApi === null || editorAnalyticsApi === void 0 ? void 0 : editorAnalyticsApi.attachAnalyticsEvent(getLinkCreationAnalyticsEvent(INPUT_METHOD.FORMATTING, url))(tr);
|
|
76
|
+
return tr;
|
|
77
|
+
});
|
|
78
|
+
return createPlugin('hyperlink', [urlWithASpaceRule, markdownLinkRule]);
|
|
79
|
+
}
|
|
80
|
+
export default createInputRulePlugin;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { keymap } from 'prosemirror-keymap';
|
|
2
|
+
import { getLinkMatch } from '@atlaskit/adf-schema';
|
|
3
|
+
import { INPUT_METHOD } from '@atlaskit/editor-common/analytics';
|
|
4
|
+
import { addLink, bindKeymapWithCommand, enter, escape, insertNewLine } from '@atlaskit/editor-common/keymaps';
|
|
5
|
+
import { findFilepaths, getLinkCreationAnalyticsEvent, isLinkInMatches } from '@atlaskit/editor-common/utils';
|
|
6
|
+
import { hideLinkToolbar, showLinkToolbar } from '../commands';
|
|
7
|
+
import { stateKey } from '../pm-plugins/main';
|
|
8
|
+
export function createKeymapPlugin(skipAnalytics = false, editorAnalyticsApi) {
|
|
9
|
+
const list = {};
|
|
10
|
+
bindKeymapWithCommand(addLink.common, showLinkToolbar(INPUT_METHOD.SHORTCUT, editorAnalyticsApi), list);
|
|
11
|
+
bindKeymapWithCommand(enter.common, mayConvertLastWordToHyperlink(skipAnalytics, editorAnalyticsApi), list);
|
|
12
|
+
bindKeymapWithCommand(insertNewLine.common, mayConvertLastWordToHyperlink(skipAnalytics, editorAnalyticsApi), list);
|
|
13
|
+
bindKeymapWithCommand(escape.common, (state, dispatch, view) => {
|
|
14
|
+
const hyperlinkPlugin = stateKey.getState(state);
|
|
15
|
+
if (hyperlinkPlugin.activeLinkMark) {
|
|
16
|
+
hideLinkToolbar()(state, dispatch);
|
|
17
|
+
if (view) {
|
|
18
|
+
view.focus();
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}, list);
|
|
24
|
+
return keymap(list);
|
|
25
|
+
}
|
|
26
|
+
const mayConvertLastWordToHyperlink = (skipAnalytics, editorAnalyticsApi) => {
|
|
27
|
+
return function (state, dispatch) {
|
|
28
|
+
const nodeBefore = state.selection.$from.nodeBefore;
|
|
29
|
+
if (!nodeBefore || !nodeBefore.isText || !nodeBefore.text) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const words = nodeBefore.text.split(' ');
|
|
33
|
+
const lastWord = words[words.length - 1];
|
|
34
|
+
const match = getLinkMatch(lastWord);
|
|
35
|
+
if (match) {
|
|
36
|
+
const hyperlinkedText = match.raw;
|
|
37
|
+
const start = state.selection.$from.pos - hyperlinkedText.length;
|
|
38
|
+
const end = state.selection.$from.pos;
|
|
39
|
+
if (state.doc.rangeHasMark(start, end, state.schema.marks.link)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const url = match.url;
|
|
43
|
+
const markType = state.schema.mark('link', {
|
|
44
|
+
href: url
|
|
45
|
+
});
|
|
46
|
+
const filepaths = findFilepaths(nodeBefore.text, start - (nodeBefore.text.length - hyperlinkedText.length) // The position referenced by 'start' is relative to the start of the document, findFilepaths deals with index in a node only.
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (isLinkInMatches(start, filepaths)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const tr = state.tr.addMark(start, end, markType);
|
|
53
|
+
if (dispatch) {
|
|
54
|
+
if (skipAnalytics) {
|
|
55
|
+
dispatch(tr);
|
|
56
|
+
} else {
|
|
57
|
+
editorAnalyticsApi === null || editorAnalyticsApi === void 0 ? void 0 : editorAnalyticsApi.attachAnalyticsEvent(getLinkCreationAnalyticsEvent(INPUT_METHOD.AUTO_DETECT, url))(tr);
|
|
58
|
+
dispatch(tr);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
export default createKeymapPlugin;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { PluginKey, TextSelection } from 'prosemirror-state';
|
|
2
|
+
import uuid from 'uuid';
|
|
3
|
+
import { InsertStatus, LinkAction } from '@atlaskit/editor-common/link';
|
|
4
|
+
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
5
|
+
import { canLinkBeCreatedInRange, shallowEqual } from '@atlaskit/editor-common/utils';
|
|
6
|
+
const isSelectionInsideLink = state => !!state.doc.type.schema.marks.link.isInSet(state.selection.$from.marks());
|
|
7
|
+
const isSelectionAroundLink = state => {
|
|
8
|
+
const {
|
|
9
|
+
$from,
|
|
10
|
+
$to
|
|
11
|
+
} = state.selection;
|
|
12
|
+
const node = $from.nodeAfter;
|
|
13
|
+
return !!node && $from.textOffset === 0 && $to.pos - $from.pos === node.nodeSize && !!state.doc.type.schema.marks.link.isInSet(node.marks);
|
|
14
|
+
};
|
|
15
|
+
const mapTransactionToState = (state, tr) => {
|
|
16
|
+
if (!state) {
|
|
17
|
+
return undefined;
|
|
18
|
+
} else if (state.type === InsertStatus.EDIT_LINK_TOOLBAR || state.type === InsertStatus.EDIT_INSERTED_TOOLBAR) {
|
|
19
|
+
const {
|
|
20
|
+
pos,
|
|
21
|
+
deleted
|
|
22
|
+
} = tr.mapping.mapResult(state.pos, 1);
|
|
23
|
+
const node = tr.doc.nodeAt(pos);
|
|
24
|
+
// If the position was not deleted & it is still a link
|
|
25
|
+
if (!deleted && !!node.type.schema.marks.link.isInSet(node.marks)) {
|
|
26
|
+
if (node === state.node && pos === state.pos) {
|
|
27
|
+
return state;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
...state,
|
|
31
|
+
pos,
|
|
32
|
+
node
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// If the position has been deleted, then require a navigation to show the toolbar again
|
|
36
|
+
return;
|
|
37
|
+
} else if (state.type === InsertStatus.INSERT_LINK_TOOLBAR) {
|
|
38
|
+
return {
|
|
39
|
+
...state,
|
|
40
|
+
from: tr.mapping.map(state.from),
|
|
41
|
+
to: tr.mapping.map(state.to)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
};
|
|
46
|
+
const toState = (state, action, editorState) => {
|
|
47
|
+
// Show insert or edit toolbar
|
|
48
|
+
if (!state) {
|
|
49
|
+
switch (action) {
|
|
50
|
+
case LinkAction.SHOW_INSERT_TOOLBAR:
|
|
51
|
+
{
|
|
52
|
+
const {
|
|
53
|
+
from,
|
|
54
|
+
to
|
|
55
|
+
} = editorState.selection;
|
|
56
|
+
if (canLinkBeCreatedInRange(from, to)(editorState)) {
|
|
57
|
+
return {
|
|
58
|
+
type: InsertStatus.INSERT_LINK_TOOLBAR,
|
|
59
|
+
from,
|
|
60
|
+
to
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
case LinkAction.SELECTION_CHANGE:
|
|
66
|
+
// If the user has moved their cursor, see if they're in a link
|
|
67
|
+
const link = getActiveLinkMark(editorState);
|
|
68
|
+
if (link) {
|
|
69
|
+
return {
|
|
70
|
+
...link,
|
|
71
|
+
type: InsertStatus.EDIT_LINK_TOOLBAR
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
default:
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Update toolbar state if selection changes, or if toolbar is hidden
|
|
80
|
+
if (state.type === InsertStatus.EDIT_LINK_TOOLBAR) {
|
|
81
|
+
switch (action) {
|
|
82
|
+
case LinkAction.EDIT_INSERTED_TOOLBAR:
|
|
83
|
+
{
|
|
84
|
+
const link = getActiveLinkMark(editorState);
|
|
85
|
+
if (link) {
|
|
86
|
+
if (link.pos === state.pos && link.node === state.node) {
|
|
87
|
+
return {
|
|
88
|
+
...state,
|
|
89
|
+
type: InsertStatus.EDIT_INSERTED_TOOLBAR
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
...link,
|
|
94
|
+
type: InsertStatus.EDIT_INSERTED_TOOLBAR
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
case LinkAction.SELECTION_CHANGE:
|
|
100
|
+
const link = getActiveLinkMark(editorState);
|
|
101
|
+
if (link) {
|
|
102
|
+
if (link.pos === state.pos && link.node === state.node) {
|
|
103
|
+
// Make sure we return the same object, if it's the same link
|
|
104
|
+
return state;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
...link,
|
|
108
|
+
type: InsertStatus.EDIT_LINK_TOOLBAR
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
case LinkAction.HIDE_TOOLBAR:
|
|
113
|
+
return undefined;
|
|
114
|
+
default:
|
|
115
|
+
return state;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Remove toolbar if user changes selection or toolbar is hidden
|
|
120
|
+
if (state.type === InsertStatus.INSERT_LINK_TOOLBAR) {
|
|
121
|
+
switch (action) {
|
|
122
|
+
case LinkAction.SELECTION_CHANGE:
|
|
123
|
+
case LinkAction.HIDE_TOOLBAR:
|
|
124
|
+
return undefined;
|
|
125
|
+
default:
|
|
126
|
+
return state;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
};
|
|
131
|
+
const getActiveLinkMark = state => {
|
|
132
|
+
const {
|
|
133
|
+
selection: {
|
|
134
|
+
$from
|
|
135
|
+
}
|
|
136
|
+
} = state;
|
|
137
|
+
if (isSelectionInsideLink(state) || isSelectionAroundLink(state)) {
|
|
138
|
+
const pos = $from.pos - $from.textOffset;
|
|
139
|
+
const node = state.doc.nodeAt(pos);
|
|
140
|
+
return node && node.isText ? {
|
|
141
|
+
node,
|
|
142
|
+
pos
|
|
143
|
+
} : undefined;
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
};
|
|
147
|
+
const getActiveText = selection => {
|
|
148
|
+
const currentSlice = selection.content();
|
|
149
|
+
if (currentSlice.size === 0) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (currentSlice.content.childCount === 1 && currentSlice.content.firstChild && selection instanceof TextSelection) {
|
|
153
|
+
return currentSlice.content.firstChild.textContent;
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
};
|
|
157
|
+
export const stateKey = new PluginKey('hyperlinkPlugin');
|
|
158
|
+
export const plugin = (dispatch, editorAppearance) => new SafePlugin({
|
|
159
|
+
state: {
|
|
160
|
+
init(_, state) {
|
|
161
|
+
const canInsertLink = canLinkBeCreatedInRange(state.selection.from, state.selection.to)(state);
|
|
162
|
+
return {
|
|
163
|
+
activeText: getActiveText(state.selection),
|
|
164
|
+
canInsertLink,
|
|
165
|
+
timesViewed: 0,
|
|
166
|
+
activeLinkMark: toState(undefined, LinkAction.SELECTION_CHANGE, state),
|
|
167
|
+
editorAppearance
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
apply(tr, pluginState, oldState, newState) {
|
|
171
|
+
let state = pluginState;
|
|
172
|
+
const action = tr.getMeta(stateKey) && tr.getMeta(stateKey).type;
|
|
173
|
+
const inputMethod = tr.getMeta(stateKey) && tr.getMeta(stateKey).inputMethod;
|
|
174
|
+
if (tr.docChanged) {
|
|
175
|
+
state = {
|
|
176
|
+
activeText: state.activeText,
|
|
177
|
+
canInsertLink: canLinkBeCreatedInRange(newState.selection.from, newState.selection.to)(newState),
|
|
178
|
+
timesViewed: state.timesViewed,
|
|
179
|
+
inputMethod,
|
|
180
|
+
activeLinkMark: mapTransactionToState(state.activeLinkMark, tr),
|
|
181
|
+
editorAppearance
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (action) {
|
|
185
|
+
const stateForAnalytics = [LinkAction.SHOW_INSERT_TOOLBAR, LinkAction.EDIT_INSERTED_TOOLBAR].includes(action) ? {
|
|
186
|
+
timesViewed: ++state.timesViewed,
|
|
187
|
+
searchSessionId: uuid()
|
|
188
|
+
} : {
|
|
189
|
+
timesViewed: state.timesViewed,
|
|
190
|
+
searchSessionId: state.searchSessionId
|
|
191
|
+
};
|
|
192
|
+
state = {
|
|
193
|
+
activeText: state.activeText,
|
|
194
|
+
canInsertLink: state.canInsertLink,
|
|
195
|
+
inputMethod,
|
|
196
|
+
activeLinkMark: toState(state.activeLinkMark, action, newState),
|
|
197
|
+
editorAppearance,
|
|
198
|
+
...stateForAnalytics
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const hasPositionChanged = oldState.selection.from !== newState.selection.from || oldState.selection.to !== newState.selection.to;
|
|
202
|
+
if (tr.selectionSet && hasPositionChanged) {
|
|
203
|
+
state = {
|
|
204
|
+
activeText: getActiveText(newState.selection),
|
|
205
|
+
canInsertLink: canLinkBeCreatedInRange(newState.selection.from, newState.selection.to)(newState),
|
|
206
|
+
activeLinkMark: toState(state.activeLinkMark, LinkAction.SELECTION_CHANGE, newState),
|
|
207
|
+
timesViewed: state.timesViewed,
|
|
208
|
+
searchSessionId: state.searchSessionId,
|
|
209
|
+
inputMethod,
|
|
210
|
+
editorAppearance
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (!shallowEqual(state, pluginState)) {
|
|
214
|
+
dispatch(stateKey, state);
|
|
215
|
+
}
|
|
216
|
+
return state;
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
key: stateKey,
|
|
220
|
+
props: {
|
|
221
|
+
handleDOMEvents: {
|
|
222
|
+
mouseup: (_, event) => {
|
|
223
|
+
// this prevents redundant selection transaction when clicking on link
|
|
224
|
+
// link state will be update on slection change which happens on mousedown
|
|
225
|
+
if (isLinkDirectTarget(event)) {
|
|
226
|
+
event.preventDefault();
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
},
|
|
231
|
+
mousedown: (view, event) => {
|
|
232
|
+
// since link clicks are disallowed by browsers inside contenteditable
|
|
233
|
+
// so we need to handle shift+click selection ourselves in this case
|
|
234
|
+
if (!event.shiftKey || !isLinkDirectTarget(event)) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
const {
|
|
238
|
+
state
|
|
239
|
+
} = view;
|
|
240
|
+
const {
|
|
241
|
+
selection: {
|
|
242
|
+
$anchor
|
|
243
|
+
}
|
|
244
|
+
} = state;
|
|
245
|
+
const newPosition = view.posAtCoords({
|
|
246
|
+
left: event.clientX,
|
|
247
|
+
top: event.clientY
|
|
248
|
+
});
|
|
249
|
+
if ((newPosition === null || newPosition === void 0 ? void 0 : newPosition.pos) != null && newPosition.pos !== $anchor.pos) {
|
|
250
|
+
const tr = state.tr.setSelection(TextSelection.create(state.doc, $anchor.pos, newPosition.pos));
|
|
251
|
+
view.dispatch(tr);
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
function isLinkDirectTarget(event) {
|
|
260
|
+
return (event === null || event === void 0 ? void 0 : event.target) instanceof HTMLElement && event.target.tagName === 'A';
|
|
261
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { PluginKey } from 'prosemirror-state';
|
|
2
|
+
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
3
|
+
export const toolbarKey = new PluginKey('hyperlinkToolbarItems');
|
|
4
|
+
export const prependToolbarButtons = ({
|
|
5
|
+
items,
|
|
6
|
+
onEscapeCallback,
|
|
7
|
+
onInsertLinkCallback,
|
|
8
|
+
view
|
|
9
|
+
}) => {
|
|
10
|
+
const {
|
|
11
|
+
state: {
|
|
12
|
+
tr
|
|
13
|
+
},
|
|
14
|
+
dispatch
|
|
15
|
+
} = view;
|
|
16
|
+
tr.setMeta(toolbarKey, {
|
|
17
|
+
items,
|
|
18
|
+
onEscapeCallback,
|
|
19
|
+
onInsertLinkCallback
|
|
20
|
+
});
|
|
21
|
+
dispatch(tr);
|
|
22
|
+
};
|
|
23
|
+
export const toolbarButtonsPlugin = () => {
|
|
24
|
+
return new SafePlugin({
|
|
25
|
+
key: toolbarKey,
|
|
26
|
+
state: {
|
|
27
|
+
init: (_, state) => {
|
|
28
|
+
return undefined;
|
|
29
|
+
},
|
|
30
|
+
apply: (tr, pluginState) => {
|
|
31
|
+
const metaState = tr.getMeta(toolbarKey);
|
|
32
|
+
if (metaState) {
|
|
33
|
+
return metaState;
|
|
34
|
+
}
|
|
35
|
+
return pluginState;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
};
|