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>( ' + elList.join(' ') + '
]*>)?$/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('