From fe79879e2ab27e4345c79c485d94eb008f1084bf Mon Sep 17 00:00:00 2001 From: Ahmet Sezgin Duran Date: Sun, 13 Apr 2014 09:48:49 +0300 Subject: [PATCH] Update Medium Editor files --- vendor/assets/javascripts/medium-editor.js | 417 ++++++++++++++++++--- 1 file changed, 359 insertions(+), 58 deletions(-) diff --git a/vendor/assets/javascripts/medium-editor.js b/vendor/assets/javascripts/medium-editor.js index 1a665b7..40fd568 100644 --- a/vendor/assets/javascripts/medium-editor.js +++ b/vendor/assets/javascripts/medium-editor.js @@ -85,12 +85,20 @@ if (typeof module === 'object') { return html; } + // https://github.com/jashkenas/underscore + function isElement(obj) { + return !!(obj && obj.nodeType === 1); + } + MediumEditor.prototype = { defaults: { allowMultiParagraphSelection: true, anchorInputPlaceholder: 'Paste or type a link', + anchorPreviewHideDelay: 500, buttons: ['bold', 'italic', 'underline', 'anchor', 'header1', 'header2', 'quote'], buttonLabels: false, + checkLinkFormat: false, + cleanPastedHTML: false, delay: 0, diffLeft: 0, diffTop: -10, @@ -102,7 +110,7 @@ if (typeof module === 'object') { placeholder: 'Type your text', secondHeader: 'h4', targetBlank: false, - anchorPreviewHideDelay: 500 + extensions: {} }, // http://stackoverflow.com/questions/17907445/how-to-detect-ie11#comment30165888_17907562 @@ -141,7 +149,7 @@ if (typeof module === 'object') { this.elements[i].setAttribute('data-placeholder', this.options.placeholder); } this.elements[i].setAttribute('data-medium-element', true); - this.bindParagraphCreation(i).bindReturn(i).bindTab(i).bindAnchorPreview(i); + this.bindParagraphCreation(i).bindReturn(i).bindTab(i); if (!this.options.disableToolbar && !this.elements[i].getAttribute('data-disable-toolbar')) { addToolbar = true; } @@ -150,7 +158,8 @@ if (typeof module === 'object') { if (addToolbar) { this.initToolbar() .bindButtons() - .bindAnchorForm(); + .bindAnchorForm() + .bindAnchorPreview(); } return this; }, @@ -168,6 +177,32 @@ if (typeof module === 'object') { return content; }, + /** + * Helper function to call a method with a number of parameters on all registered extensions. + * The function assures that the function exists before calling. + * + * @param {string} funcName name of the function to call + * @param [args] arguments passed into funcName + */ + callExtensions: function (funcName) { + if (arguments.length < 1) { + return; + } + + var args = Array.prototype.slice.call(arguments, 1), + ext, + name; + + for (name in this.options.extensions) { + if (this.options.extensions.hasOwnProperty(name)) { + ext = this.options.extensions[name]; + if (ext[funcName] !== undefined) { + ext[funcName].apply(ext, args); + } + } + } + }, + bindParagraphCreation: function (index) { var self = this; this.elements[index].addEventListener('keyup', function (e) { @@ -244,22 +279,22 @@ if (typeof module === 'object') { buttonTemplate: function (btnType) { var buttonLabels = this.getButtonLabels(this.options.buttonLabels), buttonTemplates = { - 'bold': '
  • ', - 'italic': '
  • ', - 'underline': '
  • ', - 'strikethrough': '
  • ', - 'superscript': '
  • ', - 'subscript': '
  • ', - 'anchor': '
  • ', - 'image': '
  • ', - 'header1': '
  • ', - 'header2': '
  • ', - 'quote': '
  • ', - 'orderedlist': '
  • ', - 'unorderedlist': '
  • ', - 'pre': '
  • ', - 'indent': '
  • ', - 'outdent': '
  • ' + 'bold': '', + 'italic': '', + 'underline': '', + 'strikethrough': '', + 'superscript': '', + 'subscript': '', + 'anchor': '', + 'image': '', + 'header1': '', + 'header2': '', + 'quote': '', + 'orderedlist': '', + 'unorderedlist': '', + 'pre': '', + 'indent': '', + 'outdent': '' }; return buttonTemplates[btnType] || false; }, @@ -314,27 +349,6 @@ if (typeof module === 'object') { return buttonLabels; }, - //TODO: actionTemplate - toolbarTemplate: function () { - var btns = this.options.buttons, - html = '' + - '
    ' + - ' ' + - ' ×' + - '
    '; - return html; - }, - initToolbar: function () { if (this.toolbar) { return this; @@ -353,11 +367,65 @@ if (typeof module === 'object') { var toolbar = document.createElement('div'); toolbar.id = 'medium-editor-toolbar-' + this.id; toolbar.className = 'medium-editor-toolbar'; - toolbar.innerHTML = this.toolbarTemplate(); + toolbar.appendChild(this.toolbarButtons()); + toolbar.appendChild(this.toolbarFormAnchor()); document.body.appendChild(toolbar); return toolbar; }, + //TODO: actionTemplate + toolbarButtons: function () { + var btns = this.options.buttons, + ul = document.createElement('ul'), + li, + i, + btn, + ext; + + ul.id = 'medium-editor-toolbar-actions'; + ul.className = 'medium-editor-toolbar-actions clearfix'; + + for (i = 0; i < btns.length; i += 1) { + if (this.options.extensions.hasOwnProperty(btns[i])) { + ext = this.options.extensions[btns[i]]; + btn = ext.getButton !== undefined ? ext.getButton() : null; + } else { + btn = this.buttonTemplate(btns[i]); + } + + if (btn) { + li = document.createElement('li'); + if (isElement(btn)) { + li.appendChild(btn); + } else { + li.innerHTML = btn; + } + ul.appendChild(li); + } + } + + return ul; + }, + + toolbarFormAnchor: function () { + var anchor = document.createElement('div'), + input = document.createElement('input'), + a = document.createElement('a'); + + a.setAttribute('href', '#'); + a.innerHTML = '×'; + + input.setAttribute('type', 'text'); + input.setAttribute('placeholder', this.options.anchorInputPlaceholder); + + anchor.className = 'medium-editor-toolbar-form-anchor'; + anchor.id = 'medium-editor-toolbar-form-anchor'; + anchor.appendChild(input); + anchor.appendChild(a); + + return anchor; + }, + bindSelect: function () { var self = this, timer = '', @@ -438,9 +506,7 @@ if (typeof module === 'object') { getSelectionElement: function () { var selection = window.getSelection(), - range = selection.getRangeAt(0), - current = range.commonAncestorContainer, - parent = current.parentNode, + range, current, parent, result, getMediumElement = function (e) { var localParent = e; @@ -455,6 +521,10 @@ if (typeof module === 'object') { }; // First try on current node try { + range = selection.getRangeAt(0); + current = range.commonAncestorContainer; + parent = current.parentNode; + if (current.getAttribute('data-medium-element')) { result = current; } else { @@ -508,12 +578,19 @@ if (typeof module === 'object') { }, checkActiveButtons: function () { - var parentNode = this.selection.anchorNode; + var elements = Array.prototype.slice.call(this.elements), + parentNode = this.selection.anchorNode; if (!parentNode.tagName) { parentNode = this.selection.anchorNode.parentNode; } while (parentNode.tagName !== undefined && this.parentElements.indexOf(parentNode.tagName.toLowerCase) === -1) { this.activateButton(parentNode.tagName.toLowerCase()); + this.callExtensions('checkState', parentNode); + + // we can abort the search upwards if we leave the contentEditable element + if (elements.indexOf(parentNode) !== -1) { + break; + } parentNode = parentNode.parentNode; } }, @@ -540,7 +617,9 @@ if (typeof module === 'object') { } else { this.className += ' medium-editor-button-active'; } - self.execAction(this.getAttribute('data-action'), e); + if (this.hasAttribute('data-action')) { + self.execAction(this.getAttribute('data-action'), e); + } }; for (i = 0; i < buttons.length; i += 1) { buttons[i].addEventListener('click', triggerAction); @@ -550,8 +629,10 @@ if (typeof module === 'object') { }, setFirstAndLastItems: function (buttons) { - buttons[0].className += ' medium-editor-button-first'; - buttons[buttons.length - 1].className += ' medium-editor-button-last'; + if (buttons.length > 0) { + buttons[0].className += ' medium-editor-button-first'; + buttons[buttons.length - 1].className += ' medium-editor-button-last'; + } return this; }, @@ -638,7 +719,9 @@ if (typeof module === 'object') { hideToolbarActions: function () { this.keepToolbarAlive = false; - this.toolbar.classList.remove('medium-editor-toolbar-active'); + if (this.toolbar !== undefined) { + this.toolbar.classList.remove('medium-editor-toolbar-active'); + } }, showToolbarActions: function () { @@ -649,7 +732,7 @@ if (typeof module === 'object') { this.keepToolbarAlive = false; clearTimeout(timer); timer = setTimeout(function () { - if (!self.toolbar.classList.contains('medium-editor-toolbar-active')) { + if (self.toolbar && !self.toolbar.classList.contains('medium-editor-toolbar-active')) { self.toolbar.classList.add('medium-editor-toolbar-active'); } }, 100); @@ -718,7 +801,7 @@ if (typeof module === 'object') { clearTimeout(timer); timer = setTimeout(function () { - if (!self.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) { + if (self.anchorPreview && !self.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) { self.anchorPreview.classList.add('medium-editor-anchor-preview-active'); } }, 100); @@ -811,7 +894,9 @@ if (typeof module === 'object') { sel.removeAllRanges(); sel.addRange(range); setTimeout(function () { - self.showAnchorForm(self.activeAnchor.href); + if (self.activeAnchor) { + self.showAnchorForm(self.activeAnchor.href); + } self.keepToolbarAlive = false; }, 100 + self.options.delay); @@ -832,7 +917,7 @@ if (typeof module === 'object') { if (e.target && e.target.tagName.toLowerCase() === 'a') { // Detect empty href attributes - // The browser will make href="" or href="#top" + // The browser will make href="" or href="#top" // into absolute urls when accessed as e.targed.href, so check the html if (!/href=["']\S+["']/.test(e.target.outerHTML) || /href=["']#\S+["']/.test(e.target.outerHTML)) { return true; @@ -858,13 +943,24 @@ if (typeof module === 'object') { }, bindAnchorPreview: function (index) { - var self = this; - this.elements[index].addEventListener('mouseover', function (e) { + var i, self = this; + this.editorAnchorObserverWrapper = function (e) { self.editorAnchorObserver(e); - }); + }; + for (i = 0; i < this.elements.length; i += 1) { + this.elements[i].addEventListener('mouseover', this.editorAnchorObserverWrapper); + } return this; }, + checkLinkFormat: function (value) { + var re = /^https?:\/\//; + if (value.match(re)) { + return value; + } + return "http://" + value; + }, + setTargetBlank: function () { var el = getSelectionStart(), i; @@ -880,6 +976,9 @@ if (typeof module === 'object') { createLink: function (input) { restoreSelection(this.savedSelection); + if (this.options.checkLinkFormat) { + input.value = this.checkLinkFormat(input.value); + } document.execCommand('createLink', false, input.value); if (this.options.targetBlank) { this.setTargetBlank(); @@ -930,6 +1029,7 @@ if (typeof module === 'object') { window.removeEventListener('resize', this.windowResizeHandler); for (i = 0; i < this.elements.length; i += 1) { + this.elements[i].removeEventListener('mouseover', this.editorAnchorObserverWrapper); this.elements[i].removeEventListener('keyup', this.checkSelectionWrapper); this.elements[i].removeEventListener('blur', this.checkSelectionWrapper); this.elements[i].removeEventListener('paste', this.pasteWrapper); @@ -939,6 +1039,12 @@ if (typeof module === 'object') { }, + htmlEntities: function (str) { + // converts special characters (like <) into their escaped/encoded values (like <). + // This allows you to show to display the string without the browser reading it as HTML. + return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + }, + bindPaste: function () { var i, self = this; this.pasteWrapper = function (e) { @@ -947,17 +1053,25 @@ if (typeof module === 'object') { p; this.classList.remove('medium-editor-placeholder'); - if (!self.options.forcePlainText) { + if (!self.options.forcePlainText && !self.options.cleanPastedHTML) { return this; } if (e.clipboardData && e.clipboardData.getData && !e.defaultPrevented) { e.preventDefault(); + + if (self.options.cleanPastedHTML && e.clipboardData.getData('text/html')) { + return self.cleanPaste(e.clipboardData.getData('text/html')); + } if (!self.options.disableReturn) { paragraphs = e.clipboardData.getData('text/plain').split(/[\r\n]/g); for (p = 0; p < paragraphs.length; p += 1) { if (paragraphs[p] !== '') { - html += '

    ' + paragraphs[p] + '

    '; + if (navigator.userAgent.match(/firefox/i) && p === 0) { + html += self.htmlEntities(paragraphs[p]); + } else { + html += '

    ' + self.htmlEntities(paragraphs[p]) + '

    '; + } } } document.execCommand('insertHTML', false, html); @@ -991,6 +1105,193 @@ if (typeof module === 'object') { this.elements[i].addEventListener('keypress', placeholderWrapper); } return this; + }, + + cleanPaste: function (text) { + + /*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. + */ + var i, elList, workEl, + el = this.getSelectionElement(), + multiline = /]*docs-internal-guid[^>]*>/gi), ""], + [new RegExp(/<\/b>(]*>)?$/gi), ""], + + // un-html spaces and newlines inserted by OS X + [new RegExp(/\s+<\/span>/g), ' '], + [new RegExp(/
    /g), '
    '], + + // replace google docs italics+bold with a span to be replaced once the html is inserted + [new RegExp(/]*(font-style:italic;font-weight:bold|font-weight:bold;font-style:italic)[^>]*>/gi), ''], + + // replace google docs italics with a span to be replaced once the html is inserted + [new RegExp(/]*font-style:italic[^>]*>/gi), ''], + + //[replace google docs bolds with a span to be replaced once the html is inserted + [new RegExp(/]*font-weight:bold[^>]*>/gi), ''], + + // 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\s+href=("|”|“|“|”)([^&]+)("|”|“|“|”)>/gi), ''] + + ]; + /*jslint regexp: false*/ + + for (i = 0; i < replacements.length; i += 1) { + text = text.replace(replacements[i][0], replacements[i][1]); + } + + if (multiline) { + + // double br's aren't converted to p tags, but we want paragraphs. + elList = text.split('

    '); + + this.pasteHTML('

    ' + elList.join('

    ') + '

    '); + document.execCommand('insertText', false, "\n"); + + // block element cleanup + elList = el.querySelectorAll('p,div,br'); + for (i = 0; i < elList.length; i += 1) { + + workEl = elList[i]; + + switch (workEl.tagName.toLowerCase()) { + case 'p': + case 'div': + this.filterCommonBlocks(workEl); + break; + case 'br': + this.filterLineBreak(workEl); + break; + } + + } + + + } else { + + this.pasteHTML(text); + + } + + }, + + pasteHTML: function (html) { + var elList, workEl, i, fragmentBody, pasteBlock = document.createDocumentFragment(); + + pasteBlock.appendChild(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]; + + // delete ugly attributes + workEl.removeAttribute('class'); + workEl.removeAttribute('style'); + workEl.removeAttribute('dir'); + + if (workEl.tagName.toLowerCase() === 'meta') { + workEl.parentNode.removeChild(workEl); + } + + } + document.execCommand('insertHTML', false, fragmentBody.innerHTML.replace(/ /g, ' ')); + }, + isCommonBlock: function (el) { + return (el && (el.tagName.toLowerCase() === 'p' || el.tagName.toLowerCase() === 'div')); + }, + filterCommonBlocks: function (el) { + if (/^\s*$/.test(el.innerText)) { + el.parentNode.removeChild(el); + } + }, + filterLineBreak: function (el) { + if (this.isCommonBlock(el.previousElementSibling)) { + + // remove stray br's following common block elements + el.parentNode.removeChild(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 + el.parentNode.removeChild(el); + + } else if (el.parentNode.childElementCount === 1) { + + // and br's that are the only child of a 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.parentNode); + } + } + }, + + cleanupSpans: function (container_el) { + + var i, + el, + new_el, + spans = container_el.querySelectorAll('.replace-with'); + + for (i = 0; i < spans.length; i += 1) { + + el = spans[i]; + new_el = 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 + new_el.innerHTML = '' + el.innerHTML + ''; + + } else { + + new_el.innerHTML = el.innerHTML; + + } + el.parentNode.replaceChild(new_el, el); + + } + + spans = container_el.querySelectorAll('span'); + for (i = 0; i < spans.length; i += 1) { + + el = spans[i]; + + // remove empty spans, replace others with their contents + if (/^\s*$/.test()) { + el.parentNode.removeChild(el); + } else { + el.parentNode.replaceChild(document.createTextNode(el.innerText), el); + } + + } + } };