forked from yabwe/medium-editor
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpaste.js
296 lines (243 loc) · 12.2 KB
/
paste.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
(function () {
'use strict';
/*jslint regexp: true*/
/*
jslint does not allow character negation, because the negation
will not match any unicode characters. In the regexes in this
block, negation is used specifically to match the end of an html
tag, and in fact unicode characters *should* be allowed.
*/
function createReplacements() {
return [
// replace two bogus tags that begin pastes from google docs
[new RegExp(/<[^>]*docs-internal-guid[^>]*>/gi), ''],
[new RegExp(/<\/b>(<br[^>]*>)?$/gi), ''],
// un-html spaces and newlines inserted by OS X
[new RegExp(/<span class="Apple-converted-space">\s+<\/span>/g), ' '],
[new RegExp(/<br class="Apple-interchange-newline">/g), '<br>'],
// replace google docs italics+bold with a span to be replaced once the html is inserted
[new RegExp(/<span[^>]*(font-style:italic;font-weight:bold|font-weight:bold;font-style:italic)[^>]*>/gi), '<span class="replace-with italic bold">'],
// replace google docs italics with a span to be replaced once the html is inserted
[new RegExp(/<span[^>]*font-style:italic[^>]*>/gi), '<span class="replace-with italic">'],
//[replace google docs bolds with a span to be replaced once the html is inserted
[new RegExp(/<span[^>]*font-weight:bold[^>]*>/gi), '<span class="replace-with bold">'],
// replace manually entered b/i/a tags with real ones
[new RegExp(/<(\/?)(i|b|a)>/gi), '<$1$2>'],
// replace manually a tags with real ones, converting smart-quotes from google docs
[new RegExp(/<a(?:(?!href).)+href=(?:"|”|“|"|“|”)(((?!"|”|“|"|“|”).)*)(?:"|”|“|"|“|”)(?:(?!>).)*>/gi), '<a href="$1">'],
// Newlines between paragraphs in html have no syntactic value,
// but then have a tendency to accidentally become additional paragraphs down the line
[new RegExp(/<\/p>\n+/gi), '</p>'],
[new RegExp(/\n+<p/gi), '<p'],
// Microsoft Word makes these odd tags, like <o:p></o:p>
[new RegExp(/<\/?o:[a-z]*>/gi), ''],
// cleanup comments added by Chrome when pasting html
['<!--EndFragment-->', ''],
['<!--StartFragment-->', '']
];
}
/*jslint regexp: false*/
var PasteHandler = MediumEditor.Extension.extend({
/* Paste Options */
/* forcePlainText: [boolean]
* Forces pasting as plain text.
*/
forcePlainText: true,
/* cleanPastedHTML: [boolean]
* cleans pasted content from different sources, like google docs etc.
*/
cleanPastedHTML: false,
/* preCleanReplacements: [Array]
* custom pairs (2 element arrays) of RegExp and replacement text to use during past when
* __forcePlainText__ or __cleanPastedHTML__ are `true` OR when calling `cleanPaste(text)` helper method.
* These replacements are executed before any medium editor defined replacements.
*/
preCleanReplacements: [],
/* cleanReplacements: [Array]
* custom pairs (2 element arrays) of RegExp and replacement text to use during paste when
* __forcePlainText__ or __cleanPastedHTML__ are `true` OR when calling `cleanPaste(text)` helper method.
* These replacements are executed after any medium editor defined replacements.
*/
cleanReplacements: [],
/* cleanAttrs:: [Array]
* list of element attributes to remove during paste when __cleanPastedHTML__ is `true` or when
* calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.
*/
cleanAttrs: ['class', 'style', 'dir'],
/* cleanTags: [Array]
* list of element tag names to remove during paste when __cleanPastedHTML__ is `true` or when
* calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.
*/
cleanTags: ['meta'],
init: function () {
MediumEditor.Extension.prototype.init.apply(this, arguments);
if (this.forcePlainText || this.cleanPastedHTML) {
this.subscribe('editablePaste', this.handlePaste.bind(this));
}
},
handlePaste: function (event, element) {
var paragraphs,
html = '',
p,
dataFormatHTML = 'text/html',
dataFormatPlain = 'text/plain',
pastedHTML,
pastedPlain;
if (this.window.clipboardData && event.clipboardData === undefined) {
event.clipboardData = this.window.clipboardData;
// If window.clipboardData exists, but event.clipboardData doesn't exist,
// we're probably in IE. IE only has two possibilities for clipboard
// data format: 'Text' and 'URL'.
//
// Of the two, we want 'Text':
dataFormatHTML = 'Text';
dataFormatPlain = 'Text';
}
if (event.clipboardData &&
event.clipboardData.getData &&
!event.defaultPrevented) {
event.preventDefault();
pastedHTML = event.clipboardData.getData(dataFormatHTML);
pastedPlain = event.clipboardData.getData(dataFormatPlain);
if (this.cleanPastedHTML && pastedHTML) {
return this.cleanPaste(pastedHTML);
}
if (!(this.getEditorOption('disableReturn') || element.getAttribute('data-disable-return'))) {
paragraphs = pastedPlain.split(/[\r\n]+/g);
// If there are no \r\n in data, don't wrap in <p>
if (paragraphs.length > 1) {
for (p = 0; p < paragraphs.length; p += 1) {
if (paragraphs[p] !== '') {
html += '<p>' + MediumEditor.util.htmlEntities(paragraphs[p]) + '</p>';
}
}
} else {
html = MediumEditor.util.htmlEntities(paragraphs[0]);
}
} else {
html = MediumEditor.util.htmlEntities(pastedPlain);
}
MediumEditor.util.insertHTMLCommand(this.document, html);
}
},
cleanPaste: function (text) {
var i, elList, tmp, workEl,
multiline = /<p|<br|<div/.test(text),
replacements = [].concat(
this.preCleanReplacements || [],
createReplacements(),
this.cleanReplacements || []);
for (i = 0; i < replacements.length; i += 1) {
text = text.replace(replacements[i][0], replacements[i][1]);
}
if (!multiline) {
return this.pasteHTML(text);
}
// create a temporary div to cleanup block elements
tmp = this.document.createElement('div');
// double br's aren't converted to p tags, but we want paragraphs.
tmp.innerHTML = '<p>' + text.split('<br><br>').join('</p><p>') + '</p>';
// block element cleanup
elList = tmp.querySelectorAll('a,p,div,br');
for (i = 0; i < elList.length; i += 1) {
workEl = elList[i];
// Microsoft Word replaces some spaces with newlines.
// While newlines between block elements are meaningless, newlines within
// elements are sometimes actually spaces.
workEl.innerHTML = workEl.innerHTML.replace(/\n/gi, ' ');
switch (workEl.nodeName.toLowerCase()) {
case 'p':
case 'div':
this.filterCommonBlocks(workEl);
break;
case 'br':
this.filterLineBreak(workEl);
break;
}
}
this.pasteHTML(tmp.innerHTML);
},
pasteHTML: function (html, options) {
options = MediumEditor.util.defaults({}, options, {
cleanAttrs: this.cleanAttrs,
cleanTags: this.cleanTags
});
var elList, workEl, i, fragmentBody, pasteBlock = this.document.createDocumentFragment();
pasteBlock.appendChild(this.document.createElement('body'));
fragmentBody = pasteBlock.querySelector('body');
fragmentBody.innerHTML = html;
this.cleanupSpans(fragmentBody);
elList = fragmentBody.querySelectorAll('*');
for (i = 0; i < elList.length; i += 1) {
workEl = elList[i];
if ('a' === workEl.nodeName.toLowerCase() && this.getEditorOption('targetBlank')) {
MediumEditor.util.setTargetBlank(workEl);
}
MediumEditor.util.cleanupAttrs(workEl, options.cleanAttrs);
MediumEditor.util.cleanupTags(workEl, options.cleanTags);
}
MediumEditor.util.insertHTMLCommand(this.document, fragmentBody.innerHTML.replace(/ /g, ' '));
},
isCommonBlock: function (el) {
return (el && (el.nodeName.toLowerCase() === 'p' || el.nodeName.toLowerCase() === 'div'));
},
filterCommonBlocks: function (el) {
if (/^\s*$/.test(el.textContent) && el.parentNode) {
el.parentNode.removeChild(el);
}
},
filterLineBreak: function (el) {
if (this.isCommonBlock(el.previousElementSibling)) {
// remove stray br's following common block elements
this.removeWithParent(el);
} else if (this.isCommonBlock(el.parentNode) && (el.parentNode.firstChild === el || el.parentNode.lastChild === el)) {
// remove br's just inside open or close tags of a div/p
this.removeWithParent(el);
} else if (el.parentNode && el.parentNode.childElementCount === 1 && el.parentNode.textContent === '') {
// and br's that are the only child of elements other than div/p
this.removeWithParent(el);
}
},
// remove an element, including its parent, if it is the only element within its parent
removeWithParent: function (el) {
if (el && el.parentNode) {
if (el.parentNode.parentNode && el.parentNode.childElementCount === 1) {
el.parentNode.parentNode.removeChild(el.parentNode);
} else {
el.parentNode.removeChild(el);
}
}
},
cleanupSpans: function (containerEl) {
var i,
el,
newEl,
spans = containerEl.querySelectorAll('.replace-with'),
isCEF = function (el) {
return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false');
};
for (i = 0; i < spans.length; i += 1) {
el = spans[i];
newEl = this.document.createElement(el.classList.contains('bold') ? 'b' : 'i');
if (el.classList.contains('bold') && el.classList.contains('italic')) {
// add an i tag as well if this has both italics and bold
newEl.innerHTML = '<i>' + el.innerHTML + '</i>';
} else {
newEl.innerHTML = el.innerHTML;
}
el.parentNode.replaceChild(newEl, el);
}
spans = containerEl.querySelectorAll('span');
for (i = 0; i < spans.length; i += 1) {
el = spans[i];
// bail if span is in contenteditable = false
if (MediumEditor.util.traverseUp(el, isCEF)) {
return false;
}
// remove empty spans, replace others with their contents
MediumEditor.util.unwrap(el, this.document);
}
}
});
MediumEditor.extensions.paste = PasteHandler;
}());