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