@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.
Files changed (59) hide show
  1. package/.eslintrc.js +6 -0
  2. package/CHANGELOG.md +1 -0
  3. package/LICENSE.md +13 -0
  4. package/README.md +7 -0
  5. package/dist/cjs/Toolbar.js +280 -0
  6. package/dist/cjs/commands.js +242 -0
  7. package/dist/cjs/index.js +12 -0
  8. package/dist/cjs/plugin.js +122 -0
  9. package/dist/cjs/pm-plugins/fake-curor-for-toolbar-plugin-key.js +9 -0
  10. package/dist/cjs/pm-plugins/fake-cursor-for-toolbar.js +68 -0
  11. package/dist/cjs/pm-plugins/input-rule.js +95 -0
  12. package/dist/cjs/pm-plugins/keymap.js +75 -0
  13. package/dist/cjs/pm-plugins/main.js +257 -0
  14. package/dist/cjs/pm-plugins/toolbar-buttons.js +43 -0
  15. package/dist/cjs/version.json +5 -0
  16. package/dist/es2019/Toolbar.js +260 -0
  17. package/dist/es2019/commands.js +225 -0
  18. package/dist/es2019/index.js +1 -0
  19. package/dist/es2019/plugin.js +106 -0
  20. package/dist/es2019/pm-plugins/fake-curor-for-toolbar-plugin-key.js +2 -0
  21. package/dist/es2019/pm-plugins/fake-cursor-for-toolbar.js +63 -0
  22. package/dist/es2019/pm-plugins/input-rule.js +80 -0
  23. package/dist/es2019/pm-plugins/keymap.js +65 -0
  24. package/dist/es2019/pm-plugins/main.js +261 -0
  25. package/dist/es2019/pm-plugins/toolbar-buttons.js +39 -0
  26. package/dist/es2019/version.json +5 -0
  27. package/dist/esm/Toolbar.js +271 -0
  28. package/dist/esm/commands.js +222 -0
  29. package/dist/esm/index.js +1 -0
  30. package/dist/esm/plugin.js +114 -0
  31. package/dist/esm/pm-plugins/fake-curor-for-toolbar-plugin-key.js +2 -0
  32. package/dist/esm/pm-plugins/fake-cursor-for-toolbar.js +61 -0
  33. package/dist/esm/pm-plugins/input-rule.js +85 -0
  34. package/dist/esm/pm-plugins/keymap.js +67 -0
  35. package/dist/esm/pm-plugins/main.js +248 -0
  36. package/dist/esm/pm-plugins/toolbar-buttons.js +34 -0
  37. package/dist/esm/version.json +5 -0
  38. package/dist/types/Toolbar.d.ts +8 -0
  39. package/dist/types/commands.d.ts +20 -0
  40. package/dist/types/index.d.ts +3 -0
  41. package/dist/types/plugin.d.ts +38 -0
  42. package/dist/types/pm-plugins/fake-curor-for-toolbar-plugin-key.d.ts +2 -0
  43. package/dist/types/pm-plugins/fake-cursor-for-toolbar.d.ts +3 -0
  44. package/dist/types/pm-plugins/input-rule.d.ts +8 -0
  45. package/dist/types/pm-plugins/keymap.d.ts +4 -0
  46. package/dist/types/pm-plugins/main.d.ts +7 -0
  47. package/dist/types/pm-plugins/toolbar-buttons.d.ts +21 -0
  48. package/dist/types-ts4.5/Toolbar.d.ts +8 -0
  49. package/dist/types-ts4.5/commands.d.ts +20 -0
  50. package/dist/types-ts4.5/index.d.ts +3 -0
  51. package/dist/types-ts4.5/plugin.d.ts +38 -0
  52. package/dist/types-ts4.5/pm-plugins/fake-curor-for-toolbar-plugin-key.d.ts +2 -0
  53. package/dist/types-ts4.5/pm-plugins/fake-cursor-for-toolbar.d.ts +3 -0
  54. package/dist/types-ts4.5/pm-plugins/input-rule.d.ts +8 -0
  55. package/dist/types-ts4.5/pm-plugins/keymap.d.ts +4 -0
  56. package/dist/types-ts4.5/pm-plugins/main.d.ts +7 -0
  57. package/dist/types-ts4.5/pm-plugins/toolbar-buttons.d.ts +21 -0
  58. package/package.json +107 -0
  59. 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
+ };
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "@atlaskit/editor-plugin-hyperlink",
3
+ "version": "0.1.0",
4
+ "sideEffects": false
5
+ }