|
| 1 | +/* https://github.com/DiemenDesign/summernote-cleaner */ |
| 2 | +/* Version: 1.1.0 */ |
| 3 | +(function (factory) { |
| 4 | + if (typeof define === 'function' && define.amd) { |
| 5 | + define(['jquery'], factory); |
| 6 | + } else if (typeof module === 'object' && module.exports) { |
| 7 | + module.exports = factory(require('jquery')); |
| 8 | + } else { |
| 9 | + factory(window.jQuery); |
| 10 | + } |
| 11 | +} |
| 12 | + (function ($) { |
| 13 | + $.extend(true, $.summernote.lang, { |
| 14 | + 'en-US': { |
| 15 | + cleaner: { |
| 16 | + tooltip: 'Cleaner', |
| 17 | + not: 'Text has been cleaned!', |
| 18 | + limitText: 'Text', |
| 19 | + limitHTML: 'HTML' |
| 20 | + } |
| 21 | + }, |
| 22 | + 'de-DE': { |
| 23 | + cleaner: { |
| 24 | + tooltip: 'Bereinigen', |
| 25 | + not: 'Inhalt wurde bereinigt!', |
| 26 | + limitText: 'Text', |
| 27 | + limitHTML: 'HTML' |
| 28 | + } |
| 29 | + }, |
| 30 | + }); |
| 31 | + $.extend($.summernote.options, { |
| 32 | + cleaner: { |
| 33 | + action: 'both', // both|button|paste 'button' only cleans via toolbar button, 'paste' only clean when pasting content, both does both options. |
| 34 | + icon: '<i class="note-icon"><svg xmlns="http://www.w3.org/2000/svg" id="libre-paintbrush" viewBox="0 0 14 14" width="14" height="14"><path d="m 11.821425,1 q 0.46875,0 0.82031,0.311384 0.35157,0.311384 0.35157,0.780134 0,0.421875 -0.30134,1.01116 -2.22322,4.212054 -3.11384,5.035715 -0.64956,0.609375 -1.45982,0.609375 -0.84375,0 -1.44978,-0.61942 -0.60603,-0.61942 -0.60603,-1.469866 0,-0.857143 0.61608,-1.419643 l 4.27232,-3.877232 Q 11.345985,1 11.821425,1 z m -6.08705,6.924107 q 0.26116,0.508928 0.71317,0.870536 0.45201,0.361607 1.00781,0.508928 l 0.007,0.475447 q 0.0268,1.426339 -0.86719,2.32366 Q 5.700895,13 4.261155,13 q -0.82366,0 -1.45982,-0.311384 -0.63616,-0.311384 -1.0212,-0.853795 -0.38505,-0.54241 -0.57924,-1.225446 -0.1942,-0.683036 -0.1942,-1.473214 0.0469,0.03348 0.27455,0.200893 0.22768,0.16741 0.41518,0.29799 0.1875,0.130581 0.39509,0.24442 0.20759,0.113839 0.30804,0.113839 0.27455,0 0.3683,-0.247767 0.16741,-0.441965 0.38505,-0.753349 0.21763,-0.311383 0.4654,-0.508928 0.24776,-0.197545 0.58928,-0.31808 0.34152,-0.120536 0.68974,-0.170759 0.34821,-0.05022 0.83705,-0.07031 z"/></svg></i>', |
| 35 | + keepHtml: true, |
| 36 | + keepTagContents: ['span'], //Remove tags and keep the contents |
| 37 | + badTags: ['applet', 'col', 'colgroup', 'embed', 'noframes', 'noscript', 'script', 'style', 'title', 'meta', 'link', 'head'], //Remove full tags with contents |
| 38 | + badAttributes: ['bgcolor', 'border', 'height', 'cellpadding', 'cellspacing', 'lang', 'start', 'style', 'valign', 'width', 'data-(.*?)'], //Remove attributes from remaining tags, NB. 'data-(.*?)' would fail when cleaning with jQuery |
| 39 | + limitChars: 0, // 0|# 0 disables option |
| 40 | + limitDisplay: 'both', // none|text|html|both |
| 41 | + limitStop: false, // true/false |
| 42 | + limitType: 'text', // text|html |
| 43 | + notTimeOut: 850, //time before status message is hidden in miliseconds |
| 44 | + keepImages: true, |
| 45 | + imagePlaceholder: 'https://via.placeholder.com/200' |
| 46 | + } |
| 47 | + }); |
| 48 | + $.extend($.summernote.plugins, { |
| 49 | + 'cleaner': function (context) { |
| 50 | + var ui = $.summernote.ui, |
| 51 | + $note = context.layoutInfo.note, |
| 52 | + $editor = context.layoutInfo.editor, |
| 53 | + options = context.options, |
| 54 | + lang = options.langInfo; |
| 55 | + |
| 56 | + if (! options.cleaner.hasOwnProperty('limitType')) { |
| 57 | + options.cleaner.limitType = 'text'; |
| 58 | + } |
| 59 | + |
| 60 | + if (options.cleaner.action === 'both' || options.cleaner.action === 'button') { |
| 61 | + context.memo('button.cleaner', function () { |
| 62 | + var button = ui.button({ |
| 63 | + contents: options.cleaner.icon, |
| 64 | + container: options.container, |
| 65 | + tooltip: lang.cleaner.tooltip, |
| 66 | + placement: options.placement, |
| 67 | + click: function () { |
| 68 | + if ($note.summernote('createRange').toString()) { |
| 69 | + $note.summernote('pasteHTML', $note.summernote('createRange').toString()); |
| 70 | + } else { |
| 71 | + $note.summernote('code', cleanPaste($note.summernote('code'), options.cleaner.badTags, options.cleaner.keepTagContents, options.cleaner.badAttributes, options.cleaner.keepImages, options.cleaner.imagePlaceholder, true)); |
| 72 | + } |
| 73 | + showCleanedAlert(); |
| 74 | + } |
| 75 | + }); |
| 76 | + return button.render(); |
| 77 | + }); |
| 78 | + } |
| 79 | + this.events = { |
| 80 | + 'summernote.init': function () { |
| 81 | + updateCleanerStatus(); |
| 82 | + }, |
| 83 | + 'summernote.keydown': function (we, event) { |
| 84 | + if (options.cleaner.limitChars !== 0 && options.cleaner.limitStop === true) { |
| 85 | + var testLength = (options.cleaner.limitType === 'html') ? $editor.find('.note-editable').html().length : $editor.find(".note-editable").text().replace(/(<([^>]+)>)/ig, "").replace(/( )/, " ").length; |
| 86 | + if (testLength >= options.cleaner.limitChars) { |
| 87 | + var key = event.keyCode; |
| 88 | + allowed_keys = [8, 37, 38, 39, 40, 46]; |
| 89 | + if ($.inArray(key, allowed_keys) !== -1) { |
| 90 | + return true; |
| 91 | + } else { |
| 92 | + event.preventDefault(); |
| 93 | + event.stopPropagation(); |
| 94 | + } |
| 95 | + } |
| 96 | + } |
| 97 | + }, |
| 98 | + 'summernote.keyup': function (we, event) { |
| 99 | + updateCleanerStatus(); |
| 100 | + }, |
| 101 | + 'summernote.paste': function (we, event) { |
| 102 | + if (options.cleaner.action === 'both' || options.cleaner.action === 'paste') { |
| 103 | + event.preventDefault(); |
| 104 | + |
| 105 | + // delete selected text when pasting and paste it where the deleted text was |
| 106 | + if (document.getSelection().toString().length > 0) { |
| 107 | + rng = $.summernote.range; |
| 108 | + r = rng.createFromSelection(); |
| 109 | + r = r.deleteContents(); |
| 110 | + $note.summernote('editor.setLastRange', r.select()); |
| 111 | + } |
| 112 | + |
| 113 | + var ua = window.navigator.userAgent; |
| 114 | + var msie = ua.indexOf("MSIE "); |
| 115 | + msie = msie > 0 || !!navigator.userAgent.match(/Trident.*rv:11\./); |
| 116 | + var ffox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; |
| 117 | + var text; var isHtmlData = false; |
| 118 | + if (msie) |
| 119 | + text = window.clipboardData.getData("Text"); |
| 120 | + else { |
| 121 | + var dataType = 'text/plain'; |
| 122 | + // only get the html data if its available else use plain text |
| 123 | + if (options.cleaner.keepHtml && event.originalEvent.clipboardData.types.indexOf('text/html') > -1) { |
| 124 | + dataType = 'text/html'; |
| 125 | + isHtmlData = true; |
| 126 | + } |
| 127 | + text = event.originalEvent.clipboardData.getData(dataType); |
| 128 | + } |
| 129 | + if (text) { |
| 130 | + // clean the text first to prevent issues where code view wasn't updating correctly |
| 131 | + var cleanedContent = cleanPaste(text, options.cleaner.badTags, options.cleaner.keepTagContents, options.cleaner.badAttributes, options.cleaner.keepImages, options.cleaner.imagePlaceholder, isHtmlData); |
| 132 | + if (msie || ffox) { |
| 133 | + setTimeout(function () { |
| 134 | + $note.summernote('pasteHTML', cleanedContent); |
| 135 | + }, 1); |
| 136 | + } else { |
| 137 | + $note.summernote('pasteHTML', cleanedContent); |
| 138 | + } |
| 139 | + |
| 140 | + showCleanedAlert(); |
| 141 | + } |
| 142 | + } else { |
| 143 | + updateCleanerStatus(); |
| 144 | + } |
| 145 | + } |
| 146 | + } |
| 147 | + |
| 148 | + var updateCleanerStatus = function() { |
| 149 | + if (options.cleaner.limitChars !== 0 || options.cleaner.limitDisplay !== 'none') { |
| 150 | + var $cleanerStatus = $editor.find('.cleanerLimit'); |
| 151 | + if ($cleanerStatus.length === 0) { |
| 152 | + $editor.find('.note-status-output').html('<small class="cleanerLimit pull-right"> </small>'); |
| 153 | + $cleanerStatus = $editor.find('.cleanerLimit'); |
| 154 | + } |
| 155 | + |
| 156 | + var textLength = $editor.find(".note-editable").text().replace(/(<([^>]+)>)/ig, "").replace(/( )/, " ").length; |
| 157 | + var codeLength = $editor.find('.note-editable').html().length; |
| 158 | + var testLength = (options.cleaner.limitType === 'html') ? codeLength : textLength; |
| 159 | + var lengthStatus = ''; |
| 160 | + |
| 161 | + if (testLength > options.cleaner.limitChars && options.cleaner.limitChars > 0) { |
| 162 | + $cleanerStatus.addClass('text-danger'); |
| 163 | + } else { |
| 164 | + $cleanerStatus.removeClass('text-danger'); |
| 165 | + } |
| 166 | + |
| 167 | + if (options.cleaner.limitDisplay === 'text' || options.cleaner.limitDisplay === 'both') { |
| 168 | + lengthStatus += lang.cleaner.limitText + ': ' + textLength; |
| 169 | + if (options.cleaner.limitType === 'text') { |
| 170 | + lengthStatus += ' / ' + options.cleaner.limitChars; |
| 171 | + } |
| 172 | + } |
| 173 | + if (options.cleaner.limitDisplay === 'both') { |
| 174 | + lengthStatus += ' | '; |
| 175 | + } |
| 176 | + if (options.cleaner.limitDisplay === 'html' || options.cleaner.limitDisplay === 'both') { |
| 177 | + lengthStatus += lang.cleaner.limitHTML + ': ' + codeLength; |
| 178 | + if (options.cleaner.limitType === 'html') { |
| 179 | + lengthStatus += ' / ' + options.cleaner.limitChars; |
| 180 | + } |
| 181 | + } |
| 182 | + |
| 183 | + lengthStatus += ' '; |
| 184 | + |
| 185 | + $cleanerStatus.html(lengthStatus); |
| 186 | + } |
| 187 | + } |
| 188 | + |
| 189 | + var showCleanedAlert = function() { |
| 190 | + |
| 191 | + if ($editor.find('.note-status-output').length > 0) { |
| 192 | + $editor.find('.note-status-output').html(lang.cleaner.not); |
| 193 | + // now set a timeout to clear out the message |
| 194 | + setTimeout(function () { |
| 195 | + if ($editor.find('.note-status-output').html() === lang.cleaner.not) { |
| 196 | + // lets fade out the text, then clear it and show the control ready for next time |
| 197 | + $editor.find('.note-status-output').fadeOut(function () { |
| 198 | + $(this).html(""); |
| 199 | + $(this).fadeIn(); |
| 200 | + updateCleanerStatus(); |
| 201 | + }); |
| 202 | + } |
| 203 | + }, options.cleaner.notTimeOut) |
| 204 | + } |
| 205 | + |
| 206 | + } |
| 207 | + |
| 208 | + var cleanPaste = function (input, badTags, keepTagContents, badAttributes, keepImages, imagePlaceholder, isHtmlData) { |
| 209 | + if (isHtmlData) { |
| 210 | + return cleanHtmlPaste(input, badTags, keepTagContents, badAttributes, keepImages, imagePlaceholder); |
| 211 | + } else { |
| 212 | + return cleanTextPaste(input); |
| 213 | + } |
| 214 | + }; |
| 215 | + |
| 216 | + var cleanTextPaste = function (input) { |
| 217 | + var newLines = /(\r\n|\r|\n)/g; |
| 218 | + // lets only replace < and > as these are the culprit for HTML tag recognition |
| 219 | + let inputEscapedHtml = input.replace('<', '<').replace('>', '>'); |
| 220 | + var parsedInput = inputEscapedHtml.split(newLines); |
| 221 | + if (parsedInput.length === 1) { return inputEscapedHtml; } |
| 222 | + var output = ""; |
| 223 | + // for larger blocks of text (such as multiple paragraphs) match summernote markup |
| 224 | + for (let contentIndex = 0; contentIndex < parsedInput.length; contentIndex++) { |
| 225 | + const element = parsedInput[contentIndex]; |
| 226 | + if (!newLines.test(element)) { |
| 227 | + var line = element === '' ? '<br>' : element; |
| 228 | + output += '<p>' + line + '</p>' |
| 229 | + } |
| 230 | + } |
| 231 | + return output; |
| 232 | + } |
| 233 | + |
| 234 | + var cleanHtmlPaste = function (input, badTags, keepTagContents, badAttributes, keepImages, imagePlaceholder) { |
| 235 | + if (typeof (window.jQuery) === 'function') { |
| 236 | + return cleanHtmlPasteWithjQuery(input, badTags, keepTagContents, badAttributes, keepImages, imagePlaceholder) |
| 237 | + } else { |
| 238 | + return cleanHtmlPasteWithRegExp(input, badTags, keepTagContents, badAttributes, keepImages, imagePlaceholder) |
| 239 | + } |
| 240 | + } |
| 241 | + |
| 242 | + var cleanHtmlPasteWithRegExp = function (input, badTags, keepTagContents, badAttributes, keepImages, imagePlaceholder) { |
| 243 | + let i; |
| 244 | + var stringStripper = /( class=(")?Mso[a-zA-Z]+(")?)/gmi; |
| 245 | + // remove MS office class crud |
| 246 | + var output = input.replace(stringStripper, ''); |
| 247 | + var commentStripper = new RegExp('<!--(.*?)-->', 'gmi'); |
| 248 | + output = output.replace(commentStripper, ''); |
| 249 | + // remove MS office comment if else crud |
| 250 | + var commentIfStripper = new RegExp('<![^>\v]*>', 'gmi'); |
| 251 | + output = output.replace(commentIfStripper, ''); |
| 252 | + var tagStripper = new RegExp('<(/)*(\\?xml:|st1:|o:|v:)[^>\v]*>', 'gmi'); |
| 253 | + if (!keepImages) { |
| 254 | + output = output.replace(/ src="(.*?)"/gmi, ' src="' + imagePlaceholder + '"'); |
| 255 | + } |
| 256 | + output = output.replace(/ name="(.*?)"/gmi, ' data-title="$1" alt="$1"'); |
| 257 | + // remove MS office tag crud |
| 258 | + output = output.replace(tagStripper, ''); |
| 259 | + for (i = 0; i < badTags.length; i++) { |
| 260 | + const badTag = badTags[i]; |
| 261 | + // remove the tag and its contents |
| 262 | + tagStripper = new RegExp('<' + badTag + '(.|\r|\n)*</' + badTag + '[^>\v]*>', 'gmi'); |
| 263 | + output = output.replace(tagStripper, ''); |
| 264 | + // remove tags with no ending tag or rogue ending tags |
| 265 | + var singletonTagStripper = new RegExp('</?' + badTag + '[^>\v]*>', 'gmi'); |
| 266 | + output = output.replace(singletonTagStripper, ''); |
| 267 | + } |
| 268 | + for (i = 0; i < keepTagContents.length; i++) { |
| 269 | + // remove tags only |
| 270 | + tagStripper = new RegExp('</?' + keepTagContents[i] + '[^>\v]*>', 'gmi'); |
| 271 | + output = output.replace(tagStripper, ' '); |
| 272 | + } |
| 273 | + for (i = 0; i < badAttributes.length; i++) { |
| 274 | + const badAttribute = badAttributes[i]; |
| 275 | + // for attribute matching ensure we match a new line or some kind of space to prevents partial matching for attributes (e.g. color would modify bgcolor tag to be just bg) |
| 276 | + var attributeWithSpeechMarksStripper = new RegExp('(\s|\r\n|\r|\n| )' + badAttribute + '="[^"\v]*"', 'gmi'); |
| 277 | + output = output.replace(attributeWithSpeechMarksStripper, ''); |
| 278 | + var attributeWithApostropheStripper = new RegExp('(\s|\r\n|\r|\n| )' + badAttribute + "='[^'\v]*'", 'gmi'); |
| 279 | + output = output.replace(attributeWithApostropheStripper, ''); |
| 280 | + } |
| 281 | + output = output.replace(/ align="(.*?)"/gi, ' class="text-$1"'); |
| 282 | + output = output.replace(/ class="western"/gi, ''); |
| 283 | + output = output.replace(/ class=""/gi, ''); |
| 284 | + output = output.replace(/<b>(.*?)<\/b>/gi, '<strong>$1</strong>'); |
| 285 | + output = output.replace(/<i>(.*?)<\/i>/gi, '<em>$1</em>'); |
| 286 | + output = output.replace(/\s{2,}/g, ' ').trim(); |
| 287 | + return output; |
| 288 | + } |
| 289 | + |
| 290 | + // Doing similar thing as RegExp one but keeping the classes and <b> <i> as is |
| 291 | + var cleanHtmlPasteWithjQuery = function (input, badTags, keepTagContents, badAttributes, keepImages, imagePlaceholder) { |
| 292 | + let i; |
| 293 | + var newLines = /(\r\n|\r|\n)/g; |
| 294 | + input = input.replace(newLines, ' '); |
| 295 | + |
| 296 | + sanidom = $('<div></div>').html(input) |
| 297 | + |
| 298 | + if (!keepImages) { |
| 299 | + sanidom.find("img").attr('src', imagePlaceholder) |
| 300 | + } |
| 301 | + // regEx version includes this conversion: output = output.replace(/ name="(.*?)"/gmi, ' data-title="$1" alt="$1"'); |
| 302 | + sanidom.find('[name]').each(function(i, e) { |
| 303 | + var $e = $(e); |
| 304 | + var attV = $e.attr('name'); |
| 305 | + $e.attr({'data-title': attV, alt: attV}); |
| 306 | + }); |
| 307 | + |
| 308 | + for (i = 0; i < badTags.length; i++) { |
| 309 | + sanidom.find(badTags[i]).remove() |
| 310 | + } |
| 311 | + |
| 312 | + sanidom.find(':empty').remove(); |
| 313 | + |
| 314 | + for (i = 0; i < keepTagContents.length; i++) { |
| 315 | + sanidom.find(keepTagContents[i]).replaceWith(function() { |
| 316 | + return cleanReplacement(keepTagContents[i], $(this).html()); |
| 317 | + }); |
| 318 | + } |
| 319 | + |
| 320 | + for (i = 0; i < badAttributes.length; i++) { |
| 321 | + sanidom.find("[" + badAttributes[i] + "]").removeAttr(badAttributes[i]) |
| 322 | + } |
| 323 | + |
| 324 | + sanidom.find('[align]').each(function () { |
| 325 | + me = $(this) |
| 326 | + me.addClass("text-" + me.attr('align')) |
| 327 | + }); |
| 328 | + |
| 329 | + // output = output.replace(/<b>(.*?)<\/b>/gi, '<strong>$1</strong>'); |
| 330 | + sanidom.find('b').replaceWith(function() { |
| 331 | + return $('<strong>').append($(this).html()); |
| 332 | + }); |
| 333 | + |
| 334 | + // output = output.replace(/<i>(.*?)<\/i>/gi, '<em>$1</em>'); |
| 335 | + sanidom.find('i').replaceWith(function() { |
| 336 | + return $('<em>').append($(this).html()); |
| 337 | + }); |
| 338 | + |
| 339 | + sanidom.contents().filter(function () { |
| 340 | + return ((this.nodeType === Node.TEXT_NODE && !/\S/.test(this.nodeValue)) || this.nodeType === Node.COMMENT_NODE) |
| 341 | + }).remove() |
| 342 | + |
| 343 | + return sanidom.html().replace(newLines, ''); |
| 344 | + } |
| 345 | + |
| 346 | + var cleanReplacement = function(targetTag, replacement) { |
| 347 | + var $test = $('<div />').append(replacement); |
| 348 | + if ($test.find(targetTag).length > 0) { |
| 349 | + $test.find(targetTag).replaceWith(function() { |
| 350 | + return cleanReplacement(targetTag, $(this).html()) |
| 351 | + }); |
| 352 | + return $test.html(); |
| 353 | + } |
| 354 | + return replacement; |
| 355 | + } |
| 356 | + |
| 357 | + } |
| 358 | + }); |
| 359 | + })); |
0 commit comments