@atlaskit/editor-plugin-paste 0.1.22 → 0.2.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 (78) hide show
  1. package/.eslintrc.js +18 -0
  2. package/CHANGELOG.md +6 -0
  3. package/dist/cjs/actions.js +12 -0
  4. package/dist/cjs/commands.js +255 -0
  5. package/dist/cjs/edge-cases/index.js +88 -0
  6. package/dist/cjs/edge-cases/lists.js +107 -0
  7. package/dist/cjs/handlers.js +939 -0
  8. package/dist/cjs/index.js +8 -1
  9. package/dist/cjs/plugin.js +43 -0
  10. package/dist/cjs/plugins/media.js +207 -0
  11. package/dist/cjs/pm-plugins/analytics.js +376 -0
  12. package/dist/cjs/pm-plugins/clipboard-text-serializer.js +43 -0
  13. package/dist/cjs/pm-plugins/main.js +484 -0
  14. package/dist/cjs/pm-plugins/plugin-factory.js +42 -0
  15. package/dist/cjs/reducer.js +41 -0
  16. package/dist/cjs/util/index.js +214 -0
  17. package/dist/cjs/util/tinyMCE.js +183 -0
  18. package/dist/es2019/actions.js +6 -0
  19. package/dist/es2019/commands.js +236 -0
  20. package/dist/es2019/edge-cases/index.js +87 -0
  21. package/dist/es2019/edge-cases/lists.js +113 -0
  22. package/dist/es2019/handlers.js +919 -0
  23. package/dist/es2019/index.js +1 -1
  24. package/dist/es2019/plugin.js +38 -0
  25. package/dist/es2019/plugins/media.js +204 -0
  26. package/dist/es2019/pm-plugins/analytics.js +332 -0
  27. package/dist/es2019/pm-plugins/clipboard-text-serializer.js +37 -0
  28. package/dist/es2019/pm-plugins/main.js +453 -0
  29. package/dist/es2019/pm-plugins/plugin-factory.js +30 -0
  30. package/dist/es2019/reducer.js +32 -0
  31. package/dist/es2019/util/index.js +209 -0
  32. package/dist/es2019/util/tinyMCE.js +168 -0
  33. package/dist/esm/actions.js +6 -0
  34. package/dist/esm/commands.js +249 -0
  35. package/dist/esm/edge-cases/index.js +81 -0
  36. package/dist/esm/edge-cases/lists.js +98 -0
  37. package/dist/esm/handlers.js +918 -0
  38. package/dist/esm/index.js +1 -1
  39. package/dist/esm/plugin.js +37 -0
  40. package/dist/esm/plugins/media.js +199 -0
  41. package/dist/esm/pm-plugins/analytics.js +364 -0
  42. package/dist/esm/pm-plugins/clipboard-text-serializer.js +37 -0
  43. package/dist/esm/pm-plugins/main.js +471 -0
  44. package/dist/esm/pm-plugins/plugin-factory.js +36 -0
  45. package/dist/esm/reducer.js +34 -0
  46. package/dist/esm/util/index.js +194 -0
  47. package/dist/esm/util/tinyMCE.js +176 -0
  48. package/dist/types/actions.d.ts +21 -0
  49. package/dist/types/commands.d.ts +29 -0
  50. package/dist/types/edge-cases/index.d.ts +11 -0
  51. package/dist/types/edge-cases/lists.d.ts +18 -0
  52. package/dist/types/handlers.d.ts +55 -0
  53. package/dist/types/index.d.ts +1 -0
  54. package/dist/types/plugin.d.ts +2 -0
  55. package/dist/types/plugins/media.d.ts +23 -0
  56. package/dist/types/pm-plugins/analytics.d.ts +44 -0
  57. package/dist/types/pm-plugins/clipboard-text-serializer.d.ts +13 -0
  58. package/dist/types/pm-plugins/main.d.ts +12 -0
  59. package/dist/types/pm-plugins/plugin-factory.d.ts +3 -0
  60. package/dist/types/reducer.d.ts +3 -0
  61. package/dist/types/util/index.d.ts +21 -0
  62. package/dist/types/util/tinyMCE.d.ts +32 -0
  63. package/dist/types-ts4.5/actions.d.ts +21 -0
  64. package/dist/types-ts4.5/commands.d.ts +29 -0
  65. package/dist/types-ts4.5/edge-cases/index.d.ts +11 -0
  66. package/dist/types-ts4.5/edge-cases/lists.d.ts +18 -0
  67. package/dist/types-ts4.5/handlers.d.ts +55 -0
  68. package/dist/types-ts4.5/index.d.ts +1 -0
  69. package/dist/types-ts4.5/plugin.d.ts +2 -0
  70. package/dist/types-ts4.5/plugins/media.d.ts +23 -0
  71. package/dist/types-ts4.5/pm-plugins/analytics.d.ts +44 -0
  72. package/dist/types-ts4.5/pm-plugins/clipboard-text-serializer.d.ts +13 -0
  73. package/dist/types-ts4.5/pm-plugins/main.d.ts +12 -0
  74. package/dist/types-ts4.5/pm-plugins/plugin-factory.d.ts +3 -0
  75. package/dist/types-ts4.5/reducer.d.ts +3 -0
  76. package/dist/types-ts4.5/util/index.d.ts +21 -0
  77. package/dist/types-ts4.5/util/tinyMCE.d.ts +32 -0
  78. package/package.json +17 -5
@@ -0,0 +1,209 @@
1
+ import { ACTION_SUBJECT, EVENT_TYPE, INPUT_METHOD, TABLE_ACTION } from '@atlaskit/editor-common/analytics';
2
+ import { sortByOrderWithTypeName } from '@atlaskit/editor-common/legacy-rank-plugins';
3
+ import { isSupportedInParent, mapChildren } from '@atlaskit/editor-common/utils';
4
+ import { Fragment, Mark, Slice } from '@atlaskit/editor-prosemirror/model';
5
+ import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
6
+ import { findParentNodeOfType } from '@atlaskit/editor-prosemirror/utils';
7
+ import { getSelectedTableInfo, isTableSelected } from '@atlaskit/editor-tables/utils';
8
+ import { isMediaBlobUrl } from '@atlaskit/media-client';
9
+ export function isPastedFromWord(html) {
10
+ return !!html && html.indexOf('urn:schemas-microsoft-com:office:word') >= 0;
11
+ }
12
+ export function isPastedFromExcel(html) {
13
+ return !!html && html.indexOf('urn:schemas-microsoft-com:office:excel') >= 0;
14
+ }
15
+ function isPastedFromDropboxPaper(html) {
16
+ return !!html && !!html.match(/class=\"\s?author-d-.+"/gim);
17
+ }
18
+ function isPastedFromGoogleDocs(html) {
19
+ return !!html && !!html.match(/id=\"docs-internal-guid-.+"/gim);
20
+ }
21
+ function isPastedFromGoogleSpreadSheets(html) {
22
+ return !!html && !!html.match(/data-sheets-.+=/gim);
23
+ }
24
+ function isPastedFromPages(html) {
25
+ return !!html && html.indexOf('content="Cocoa HTML Writer"') >= 0;
26
+ }
27
+ function isPastedFromFabricEditor(html) {
28
+ return !!html && html.indexOf('data-pm-slice="') >= 0;
29
+ }
30
+ export const isSingleLine = text => {
31
+ return !!text && text.trim().split('\n').length === 1;
32
+ };
33
+ export function htmlContainsSingleFile(html) {
34
+ return !!html.match(/<img .*>/) && !isMediaBlobUrl(html);
35
+ }
36
+ export function getPasteSource(event) {
37
+ const html = event.clipboardData.getData('text/html');
38
+ if (isPastedFromDropboxPaper(html)) {
39
+ return 'dropbox-paper';
40
+ } else if (isPastedFromWord(html)) {
41
+ return 'microsoft-word';
42
+ } else if (isPastedFromExcel(html)) {
43
+ return 'microsoft-excel';
44
+ } else if (isPastedFromGoogleDocs(html)) {
45
+ return 'google-docs';
46
+ } else if (isPastedFromGoogleSpreadSheets(html)) {
47
+ return 'google-spreadsheets';
48
+ } else if (isPastedFromPages(html)) {
49
+ return 'apple-pages';
50
+ } else if (isPastedFromFabricEditor(html)) {
51
+ return 'fabric-editor';
52
+ }
53
+ return 'uncategorized';
54
+ }
55
+
56
+ // @see https://product-fabric.atlassian.net/browse/ED-3159
57
+ // @see https://github.com/markdown-it/markdown-it/issues/38
58
+ export function escapeLinks(text) {
59
+ return text.replace(/(\[([^\]]+)\]\()?((https?|ftp|jamfselfservice):\/\/[^\s"'>]+)/g, str => {
60
+ return str.match(/^(https?|ftp|jamfselfservice):\/\/[^\s"'>]+$/) ? `<${str}>` : str;
61
+ });
62
+ }
63
+ export function hasOnlyNodesOfType(...nodeTypes) {
64
+ return slice => {
65
+ let hasOnlyNodesOfType = true;
66
+ slice.content.descendants(node => {
67
+ hasOnlyNodesOfType = hasOnlyNodesOfType && nodeTypes.indexOf(node.type) > -1;
68
+ return hasOnlyNodesOfType;
69
+ });
70
+ return hasOnlyNodesOfType;
71
+ };
72
+ }
73
+ export function applyTextMarksToSlice(schema, marks) {
74
+ return slice => {
75
+ const {
76
+ marks: {
77
+ code: codeMark,
78
+ link: linkMark,
79
+ annotation: annotationMark
80
+ }
81
+ } = schema;
82
+ if (!Array.isArray(marks) || marks.length === 0) {
83
+ return slice;
84
+ }
85
+ const sliceCopy = Slice.fromJSON(schema, slice.toJSON() || {});
86
+
87
+ // allow links and annotations to be pasted
88
+ const allowedMarksToPaste = [linkMark, annotationMark];
89
+ sliceCopy.content.descendants((node, _pos, parent) => {
90
+ if (node.isText && parent && parent.isBlock) {
91
+ // @ts-ignore - [unblock prosemirror bump] assigning to readonly prop
92
+ node.marks = [
93
+ // remove all marks from pasted slice when applying code mark
94
+ // and exclude all marks that are not allowed to be pasted
95
+ ...(node.marks && !codeMark.isInSet(marks) && node.marks.filter(mark => allowedMarksToPaste.includes(mark.type)) || []),
96
+ // add marks to a slice if they're allowed in parent node
97
+ // and exclude link marks
98
+ ...parent.type.allowedMarks(marks).filter(mark => mark.type !== linkMark)].sort(sortByOrderWithTypeName('marks'));
99
+ return false;
100
+ }
101
+ return true;
102
+ });
103
+ return sliceCopy;
104
+ };
105
+ }
106
+ export function isEmptyNode(node) {
107
+ if (!node) {
108
+ return false;
109
+ }
110
+ const {
111
+ type: nodeType
112
+ } = node;
113
+ const emptyNode = nodeType.createAndFill();
114
+ return emptyNode && emptyNode.nodeSize === node.nodeSize && emptyNode.content.eq(node.content) && Mark.sameSet(emptyNode.marks, node.marks);
115
+ }
116
+ export function isCursorSelectionAtTextStartOrEnd(selection) {
117
+ return selection instanceof TextSelection && selection.empty && selection.$cursor && (!selection.$cursor.nodeBefore || !selection.$cursor.nodeAfter);
118
+ }
119
+ export function isPanelNode(node) {
120
+ return Boolean(node && node.type.name === 'panel');
121
+ }
122
+ export function isSelectionInsidePanel(selection) {
123
+ if (selection instanceof NodeSelection && isPanelNode(selection.node)) {
124
+ return selection.node;
125
+ }
126
+ const {
127
+ doc: {
128
+ type: {
129
+ schema: {
130
+ nodes: {
131
+ panel
132
+ }
133
+ }
134
+ }
135
+ }
136
+ } = selection.$from;
137
+ const panelPosition = findParentNodeOfType(panel)(selection);
138
+ if (panelPosition) {
139
+ return panelPosition.node;
140
+ }
141
+ return null;
142
+ }
143
+
144
+ // https://product-fabric.atlassian.net/browse/ED-11714
145
+ // Checks for broken html that comes from links in a list item copied from Notion
146
+ export const htmlHasInvalidLinkTags = html => {
147
+ return !!html && (html.includes('</a></a>') || html.includes('"></a><a'));
148
+ };
149
+
150
+ // https://product-fabric.atlassian.net/browse/ED-11714
151
+ // Example of broken html edge case we're solving
152
+ // <li><a href="http://www.atlassian.com\"<a> href="http://www.atlassian.com\"http://www.atlassian.com</a></a></li>">
153
+ export const removeDuplicateInvalidLinks = html => {
154
+ if (htmlHasInvalidLinkTags(html)) {
155
+ const htmlArray = html.split(/(?=<a)/);
156
+ const htmlArrayWithoutInvalidLinks = htmlArray.filter(item => {
157
+ return !(item.includes('<a') && item.includes('"></a>')) && !(item.includes('<a') && !item.includes('</a>'));
158
+ });
159
+ const fixedHtml = htmlArrayWithoutInvalidLinks.join('').replace(/<\/a><\/a>/gi, '</a>').replace(/<a>/gi, '<a');
160
+ return fixedHtml;
161
+ }
162
+ return html;
163
+ };
164
+ export const addReplaceSelectedTableAnalytics = (state, tr, editorAnalyticsAPI) => {
165
+ if (isTableSelected(state.selection)) {
166
+ const {
167
+ totalRowCount,
168
+ totalColumnCount
169
+ } = getSelectedTableInfo(state.selection);
170
+ editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent({
171
+ action: TABLE_ACTION.REPLACED,
172
+ actionSubject: ACTION_SUBJECT.TABLE,
173
+ attributes: {
174
+ totalColumnCount,
175
+ totalRowCount,
176
+ inputMethod: INPUT_METHOD.CLIPBOARD
177
+ },
178
+ eventType: EVENT_TYPE.TRACK
179
+ })(tr);
180
+ return tr;
181
+ }
182
+ return state.tr;
183
+ };
184
+ export const transformUnsupportedBlockCardToInline = (slice, state, cardOptions) => {
185
+ const {
186
+ blockCard,
187
+ inlineCard
188
+ } = state.schema.nodes;
189
+ const children = [];
190
+ mapChildren(slice.content, (node, i, frag) => {
191
+ var _cardOptions$allowBlo;
192
+ if (node.type === blockCard && !isBlockCardSupported(state, frag, (_cardOptions$allowBlo = cardOptions === null || cardOptions === void 0 ? void 0 : cardOptions.allowBlockCards) !== null && _cardOptions$allowBlo !== void 0 ? _cardOptions$allowBlo : false)) {
193
+ children.push(inlineCard.createChecked(node.attrs, node.content, node.marks));
194
+ } else {
195
+ children.push(node);
196
+ }
197
+ });
198
+ return new Slice(Fragment.fromArray(children), slice.openStart, slice.openEnd);
199
+ };
200
+ /**
201
+ * Function to determine if a block card is supported by the editor
202
+ * @param state
203
+ * @param frag
204
+ * @param allowBlockCards
205
+ * @returns
206
+ */
207
+ const isBlockCardSupported = (state, frag, allowBlockCards) => {
208
+ return allowBlockCards && isSupportedInParent(state, frag);
209
+ };
@@ -0,0 +1,168 @@
1
+ import chunk from 'lodash/chunk';
2
+ const isPastedFromTinyMCE = pasteEvent => {
3
+ var _pasteEvent$clipboard, _pasteEvent$clipboard2, _pasteEvent$clipboard3;
4
+ return (_pasteEvent$clipboard = pasteEvent === null || pasteEvent === void 0 ? void 0 : (_pasteEvent$clipboard2 = pasteEvent.clipboardData) === null || _pasteEvent$clipboard2 === void 0 ? void 0 : (_pasteEvent$clipboard3 = _pasteEvent$clipboard2.types) === null || _pasteEvent$clipboard3 === void 0 ? void 0 : _pasteEvent$clipboard3.some(mimeType => mimeType === 'x-tinymce/html')) !== null && _pasteEvent$clipboard !== void 0 ? _pasteEvent$clipboard : false;
5
+ };
6
+ export const isPastedFromTinyMCEConfluence = (pasteEvent, html) => {
7
+ return isPastedFromTinyMCE(pasteEvent) && !!html && !!html.match(/class=\"\s?(confluenceTd|confluenceTh|confluenceTable).+"/gim);
8
+ };
9
+
10
+ /**
11
+ * Wraps html markup with a `<table>` and uses `DOMParser` to generate
12
+ * and return both the table-wrapped and non-table-wrapped DOM document
13
+ * instances.
14
+ */
15
+ export const wrapWithTable = html => {
16
+ const parser = new DOMParser();
17
+ const nonTableWrappedDoc = parser.parseFromString(html, 'text/html');
18
+ const tableWrappedDoc = parser.parseFromString(`<table>${html}</table>`, 'text/html');
19
+ tableWrappedDoc.body.querySelectorAll('meta').forEach(meta => {
20
+ tableWrappedDoc.head.prepend(meta);
21
+ });
22
+ return {
23
+ tableWrappedDoc,
24
+ nonTableWrappedDoc
25
+ };
26
+ };
27
+ const exactlyDivisible = (larger, smaller) => larger % smaller === 0;
28
+ const getTableElementsInfo = doc => {
29
+ const cellCount = doc.querySelectorAll('td').length;
30
+ const thCount = doc.querySelectorAll('th').length;
31
+ const mergedCellCount = doc.querySelectorAll('td[colspan]:not([colspan="1"])').length;
32
+ let hasThAfterTd = false;
33
+ const thsAndCells = doc.querySelectorAll('th,td');
34
+ for (let i = 0, cellFound = false; i < thsAndCells.length; i++) {
35
+ if (cellFound && thsAndCells[i].nodeName === 'TH') {
36
+ hasThAfterTd = true;
37
+ break;
38
+ }
39
+ if (thsAndCells[i].nodeName === 'TD') {
40
+ cellFound = true;
41
+ }
42
+ }
43
+ const onlyTh = thCount > 0 && cellCount === 0;
44
+ const onlyCells = cellCount > 0 && thCount === 0;
45
+ const hasCompleteRow =
46
+ // we take header-only and cell-only tables to be
47
+ // row-complete
48
+ onlyTh || onlyCells ||
49
+ // if headers and cells can "fit" against each other,
50
+ // then we assume a complete row exists
51
+ (exactlyDivisible(thCount, cellCount) || exactlyDivisible(cellCount, thCount)) &&
52
+ // all numbers are divisible by 1, so we carve out a specific
53
+ // check for when there is only 1 table cell, and more than 1
54
+ // table header.
55
+ !(thCount > 1 && cellCount === 1);
56
+ return {
57
+ cellCount,
58
+ thCount,
59
+ mergedCellCount,
60
+ hasThAfterTd,
61
+ hasIncompleteRow: !hasCompleteRow
62
+ };
63
+ };
64
+ const configureTableRows = (doc, colsInRow) => {
65
+ var _Array$from;
66
+ const tableHeadersAndCells = Array.from(doc.body.querySelectorAll('th,td'));
67
+ const evenlySplitChunks = chunk(tableHeadersAndCells, colsInRow);
68
+ const tableBody = doc.body.querySelector('tbody');
69
+ evenlySplitChunks.forEach(chunk => {
70
+ const tr = doc.createElement('tr');
71
+ tableBody === null || tableBody === void 0 ? void 0 : tableBody.append(tr);
72
+ tr.append(...chunk);
73
+ });
74
+ // We remove any leftover empty rows which may cause fabric editor
75
+ // to no-op when parsing the table
76
+ const emptyRows = (_Array$from = Array.from(tableBody.querySelectorAll('tr'))) === null || _Array$from === void 0 ? void 0 : _Array$from.filter(row => row.innerHTML.trim().length === 0);
77
+ emptyRows.forEach(row => row.remove());
78
+ return doc.body.innerHTML;
79
+ };
80
+ const fillIncompleteRowWithEmptyCells = (doc, thCount, cellCount) => {
81
+ var _lastCell$parentEleme;
82
+ let extraCellsCount = 0;
83
+ while (!exactlyDivisible(cellCount + extraCellsCount, thCount)) {
84
+ extraCellsCount++;
85
+ }
86
+ const extraEmptyCells = Array.from(Array(extraCellsCount)).map(() => doc.createElement('td'));
87
+ const lastCell = doc.body.querySelector('td:last-of-type');
88
+ lastCell === null || lastCell === void 0 ? void 0 : (_lastCell$parentEleme = lastCell.parentElement) === null || _lastCell$parentEleme === void 0 ? void 0 : _lastCell$parentEleme.append(...extraEmptyCells);
89
+ return {
90
+ updatedCellCount: cellCount + extraCellsCount
91
+ };
92
+ };
93
+
94
+ /**
95
+ * Given a DOM document, it will try to rebuild table rows by using the
96
+ * table headers count as an initial starting point for the assumed
97
+ * number of columns that make up a row (`colsInRow`). It will slowly
98
+ * decrease that `colsInRow` count until it finds exact fit for table
99
+ * headers and cells with `colsInRow` else it returns the original
100
+ * document's markup.
101
+ *
102
+ * NOTE: It will NOT try to rebuild table rows if it encounters merged cells
103
+ * or compex table configurations (where table headers exist after normal
104
+ * table cells). It will build a single column table if NO table
105
+ * headers exist.
106
+ */
107
+ export const tryReconstructTableRows = doc => {
108
+ let {
109
+ cellCount,
110
+ thCount,
111
+ mergedCellCount,
112
+ hasThAfterTd,
113
+ hasIncompleteRow
114
+ } = getTableElementsInfo(doc);
115
+ if (mergedCellCount || hasThAfterTd) {
116
+ // bail out to avoid handling more complex table structures
117
+ return doc.body.innerHTML;
118
+ }
119
+ if (!thCount) {
120
+ // if no table headers exist for reference, fallback to a single column table structure
121
+ return configureTableRows(doc, 1);
122
+ }
123
+ if (hasIncompleteRow) {
124
+ // if shift-click selection copies a partial table row to the clipboard,
125
+ // and we do have table headers for reference, then we add empty table cells
126
+ // to fill out the partial row
127
+ const {
128
+ updatedCellCount
129
+ } = fillIncompleteRowWithEmptyCells(doc, thCount, cellCount);
130
+ cellCount = updatedCellCount;
131
+ }
132
+ for (let possibleColsInRow = thCount; possibleColsInRow > 0; possibleColsInRow--) {
133
+ if (exactlyDivisible(thCount, possibleColsInRow) && exactlyDivisible(cellCount, possibleColsInRow)) {
134
+ return configureTableRows(doc, possibleColsInRow);
135
+ }
136
+ }
137
+ return doc.body.innerHTML;
138
+ };
139
+ export const htmlHasIncompleteTable = html => {
140
+ return !html.includes('<table ') && (html.includes('<td ') || html.includes('<th '));
141
+ };
142
+
143
+ /**
144
+ * Strictly for ED-7331. Given incomplete table html from tinyMCE, it will try to rebuild
145
+ * a whole valid table. If it rebuilds the table, it may first rebuild it as a single
146
+ * row table, so this also then tries to reconstruct the table rows/columns if
147
+ * possible (best effort).
148
+ */
149
+ export const tryRebuildCompleteTableHtml = incompleteTableHtml => {
150
+ // first we try wrapping the table elements with <table> and let DOMParser try to rebuild
151
+ // a valid DOM tree. we also keep the non-wrapped table for comparison purposes.
152
+ const {
153
+ nonTableWrappedDoc,
154
+ tableWrappedDoc
155
+ } = wrapWithTable(incompleteTableHtml);
156
+ const didPreserveTableElements = Boolean(!nonTableWrappedDoc.body.querySelector('th, td') && tableWrappedDoc.body.querySelector('th, td'));
157
+ const isExpectedStructure = tableWrappedDoc.querySelectorAll('body > table:only-child') && !tableWrappedDoc.querySelector(`body > table > tbody > tr > :not(th,td)`);
158
+
159
+ // if DOMParser saves table elements that we would otherwise lose, and
160
+ // if the table html is what we'd expect (a single table, with no extraneous
161
+ // elements in table rows other than th, td), then we can now also try to
162
+ // rebuild table rows/columns.
163
+ if (didPreserveTableElements && isExpectedStructure) {
164
+ const completeTableHtml = tryReconstructTableRows(tableWrappedDoc);
165
+ return completeTableHtml;
166
+ }
167
+ return null;
168
+ };
@@ -0,0 +1,6 @@
1
+ export var PastePluginActionTypes = /*#__PURE__*/function (PastePluginActionTypes) {
2
+ PastePluginActionTypes["START_TRACKING_PASTED_MACRO_POSITIONS"] = "START_TRACKING_PASTED_MACRO_POSITIONS";
3
+ PastePluginActionTypes["STOP_TRACKING_PASTED_MACRO_POSITIONS"] = "STOP_TRACKING_PASTED_MACRO_POSITIONS";
4
+ PastePluginActionTypes["ON_PASTE"] = "ON_PASTE";
5
+ return PastePluginActionTypes;
6
+ }({});
@@ -0,0 +1,249 @@
1
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
2
+ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
3
+ import { isListNode, mapChildren, mapSlice } from '@atlaskit/editor-common/utils';
4
+ import { autoJoin } from '@atlaskit/editor-prosemirror/commands';
5
+ import { Fragment, Slice } from '@atlaskit/editor-prosemirror/model';
6
+ import { EditorState } from '@atlaskit/editor-prosemirror/state';
7
+ import { PastePluginActionTypes as ActionTypes } from './actions';
8
+ import { createCommand } from './pm-plugins/plugin-factory';
9
+
10
+ /**
11
+ * Use this to register macro link positions during a paste operation, that you
12
+ * want to track in a document over time, through any document changes.
13
+ *
14
+ * @param positions a map of string keys (custom position references) and position values e.g. { ['my-key-1']: 11 }
15
+ *
16
+ * **Context**: This is neccessary if there is an async process or an unknown period of time
17
+ * between obtaining an original position, and wanting to know about what its final eventual
18
+ * value. In that scenario, positions will need to be actively tracked and mapped in plugin
19
+ * state so that they can be mapped through any other independent document change transactions being
20
+ * dispatched to the editor that could affect their value.
21
+ */
22
+ export var startTrackingPastedMacroPositions = function startTrackingPastedMacroPositions(pastedMacroPositions) {
23
+ return createCommand(function () {
24
+ return {
25
+ type: ActionTypes.START_TRACKING_PASTED_MACRO_POSITIONS,
26
+ pastedMacroPositions: pastedMacroPositions
27
+ };
28
+ });
29
+ };
30
+ export var stopTrackingPastedMacroPositions = function stopTrackingPastedMacroPositions(pastedMacroPositionKeys) {
31
+ return createCommand(function () {
32
+ return {
33
+ type: ActionTypes.STOP_TRACKING_PASTED_MACRO_POSITIONS,
34
+ pastedMacroPositionKeys: pastedMacroPositionKeys
35
+ };
36
+ });
37
+ };
38
+
39
+ // matchers for text lists
40
+ var bullets = /^\s*[\*\-\u2022](\s+|\s+$)/;
41
+ var numbers = /^\s*\d[\.\)](\s+|$)/;
42
+ var getListType = function getListType(node, schema) {
43
+ if (!node.text) {
44
+ return null;
45
+ }
46
+ var _schema$nodes = schema.nodes,
47
+ bulletList = _schema$nodes.bulletList,
48
+ orderedList = _schema$nodes.orderedList;
49
+ return [{
50
+ node: bulletList,
51
+ matcher: bullets
52
+ }, {
53
+ node: orderedList,
54
+ matcher: numbers
55
+ }].reduce(function (lastMatch, listType) {
56
+ if (lastMatch) {
57
+ return lastMatch;
58
+ }
59
+ var match = node.text.match(listType.matcher);
60
+ return match ? [listType.node, match[0].length] : lastMatch;
61
+ }, null);
62
+ };
63
+ var extractListFromParagraph = function extractListFromParagraph(node, parent, schema) {
64
+ var _schema$nodes2 = schema.nodes,
65
+ hardBreak = _schema$nodes2.hardBreak,
66
+ bulletList = _schema$nodes2.bulletList,
67
+ orderedList = _schema$nodes2.orderedList;
68
+ var content = mapChildren(node.content, function (node) {
69
+ return node;
70
+ });
71
+ var listTypes = [bulletList, orderedList];
72
+
73
+ // wrap each line into a listItem and a containing list
74
+ var listified = content.map(function (child, index) {
75
+ var listMatch = getListType(child, schema);
76
+ var prevChild = index > 0 && content[index - 1];
77
+
78
+ // only extract list when preceded by a hardbreak
79
+ if (prevChild && prevChild.type !== hardBreak) {
80
+ return child;
81
+ }
82
+ if (!listMatch || !child.text) {
83
+ return child;
84
+ }
85
+ var _listMatch = _slicedToArray(listMatch, 2),
86
+ nodeType = _listMatch[0],
87
+ length = _listMatch[1];
88
+
89
+ // convert to list item
90
+ var newText = child.text.substr(length);
91
+ var listItemNode = schema.nodes.listItem.createAndFill(undefined, schema.nodes.paragraph.createChecked(undefined, newText.length ? schema.text(newText) : undefined));
92
+ if (!listItemNode) {
93
+ return child;
94
+ }
95
+ var newList = nodeType.createChecked(undefined, [listItemNode]);
96
+ // Check whether our new list is valid content in our current structure,
97
+ // otherwise dont convert.
98
+ if (parent && !parent.type.validContent(Fragment.from(newList))) {
99
+ return child;
100
+ }
101
+ return newList;
102
+ }).filter(function (child, idx, arr) {
103
+ // remove hardBreaks that have a list node on either side
104
+
105
+ // wasn't hardBreak, leave as-is
106
+ if (child.type !== hardBreak) {
107
+ return child;
108
+ }
109
+ if (idx > 0 && listTypes.indexOf(arr[idx - 1].type) > -1) {
110
+ // list node on the left
111
+ return null;
112
+ }
113
+ if (idx < arr.length - 1 && listTypes.indexOf(arr[idx + 1].type) > -1) {
114
+ // list node on the right
115
+ return null;
116
+ }
117
+ return child;
118
+ });
119
+
120
+ // try to join
121
+ var mockState = EditorState.create({
122
+ schema: schema
123
+ });
124
+ var joinedListsTr;
125
+ var mockDispatch = function mockDispatch(tr) {
126
+ joinedListsTr = tr;
127
+ };
128
+ autoJoin(function (state, dispatch) {
129
+ if (!dispatch) {
130
+ return false;
131
+ }
132
+
133
+ // Return false to prevent replaceWith from wrapping the text node in a paragraph
134
+ // paragraph since that will be done later. If it's done here, it will fail
135
+ // the paragraph.validContent check.
136
+ // Dont return false if there are lists, as they arent validContent for paragraphs
137
+ // and will result in hanging textNodes
138
+ var containsList = listified.some(function (node) {
139
+ return node.type === bulletList || node.type === orderedList;
140
+ });
141
+ if (listified.some(function (node) {
142
+ return node.isText;
143
+ }) && !containsList) {
144
+ return false;
145
+ }
146
+ dispatch(state.tr.replaceWith(0, 2, listified));
147
+ return true;
148
+ }, function (before, after) {
149
+ return isListNode(before) && isListNode(after);
150
+ })(mockState, mockDispatch);
151
+ var fragment = joinedListsTr ? joinedListsTr.doc.content : Fragment.from(listified);
152
+
153
+ // try to re-wrap fragment in paragraph (which is the original node we unwrapped)
154
+ var paragraph = schema.nodes.paragraph;
155
+ if (paragraph.validContent(fragment)) {
156
+ return Fragment.from(paragraph.create(node.attrs, fragment, node.marks));
157
+ }
158
+
159
+ // fragment now contains other nodes, get Prosemirror to wrap with ContentMatch later
160
+ return fragment;
161
+ };
162
+
163
+ // above will wrap everything in paragraphs for us
164
+ export var upgradeTextToLists = function upgradeTextToLists(slice, schema) {
165
+ return mapSlice(slice, function (node, parent) {
166
+ if (node.type === schema.nodes.paragraph) {
167
+ return extractListFromParagraph(node, parent, schema);
168
+ }
169
+ return node;
170
+ });
171
+ };
172
+ export var splitParagraphs = function splitParagraphs(slice, schema) {
173
+ // exclude Text nodes with a code mark, since we transform those later
174
+ // into a codeblock
175
+ var hasCodeMark = false;
176
+ slice.content.forEach(function (child) {
177
+ hasCodeMark = hasCodeMark || child.marks.some(function (mark) {
178
+ return mark.type === schema.marks.code;
179
+ });
180
+ });
181
+
182
+ // slice might just be a raw text string
183
+ if (schema.nodes.paragraph.validContent(slice.content) && !hasCodeMark) {
184
+ var replSlice = splitIntoParagraphs({
185
+ fragment: slice.content,
186
+ schema: schema
187
+ });
188
+ return new Slice(replSlice, slice.openStart + 1, slice.openEnd + 1);
189
+ }
190
+ return mapSlice(slice, function (node) {
191
+ if (node.type === schema.nodes.paragraph) {
192
+ return splitIntoParagraphs({
193
+ fragment: node.content,
194
+ blockMarks: node.marks,
195
+ schema: schema
196
+ });
197
+ }
198
+ return node;
199
+ });
200
+ };
201
+
202
+ /**
203
+ * Walks the slice, creating paragraphs that were previously separated by hardbreaks.
204
+ * Returns the original paragraph node (as a fragment), or a fragment containing multiple nodes.
205
+ */
206
+ export var splitIntoParagraphs = function splitIntoParagraphs(_ref) {
207
+ var fragment = _ref.fragment,
208
+ _ref$blockMarks = _ref.blockMarks,
209
+ blockMarks = _ref$blockMarks === void 0 ? [] : _ref$blockMarks,
210
+ schema = _ref.schema;
211
+ var paragraphs = [];
212
+ var curChildren = [];
213
+ var lastNode = null;
214
+ var _schema$nodes3 = schema.nodes,
215
+ hardBreak = _schema$nodes3.hardBreak,
216
+ paragraph = _schema$nodes3.paragraph;
217
+ fragment.forEach(function (node, i) {
218
+ var isNodeValidContentForParagraph = schema.nodes.paragraph.validContent(Fragment.from(node));
219
+ if (!isNodeValidContentForParagraph) {
220
+ paragraphs.push(node);
221
+ return;
222
+ }
223
+ // ED-14725 Fixed the issue that it make duplicated line
224
+ // when pasting <br /> from google docs.
225
+ if (i === 0 && node.type === hardBreak) {
226
+ paragraphs.push(paragraph.createChecked(undefined, curChildren, _toConsumableArray(blockMarks)));
227
+ lastNode = node;
228
+ return;
229
+ } else if (lastNode && lastNode.type === hardBreak && node.type === hardBreak) {
230
+ // double hardbreak
231
+
232
+ // backtrack a little; remove the trailing hardbreak we added last loop
233
+ curChildren.pop();
234
+
235
+ // create a new paragraph
236
+ paragraphs.push(paragraph.createChecked(undefined, curChildren, _toConsumableArray(blockMarks)));
237
+ curChildren = [];
238
+ return;
239
+ }
240
+
241
+ // add to this paragraph
242
+ curChildren.push(node);
243
+ lastNode = node;
244
+ });
245
+ if (curChildren.length) {
246
+ paragraphs.push(paragraph.createChecked(undefined, curChildren, _toConsumableArray(blockMarks)));
247
+ }
248
+ return Fragment.from(paragraphs.length ? paragraphs : [paragraph.createAndFill(undefined, undefined, _toConsumableArray(blockMarks))]);
249
+ };