From 901ce974f22d72d0989c01edc6c8f202153745c1 Mon Sep 17 00:00:00 2001 From: Patrick Woldberg Date: Tue, 18 Jul 2017 13:23:43 +0200 Subject: [PATCH 1/5] Updated all methods that set/get the editor content, extensions can change the html that is being set/get. Moved the extension init before the addElements method to allow extension hook into the setContent and getContent. Because of the move the elements are not available in the extension init method, the extension should subscribe to the add addElement and removeElement events. Always enable toolbar when not disabled in options, even when all elements on page have data-disable-toolbar set, it is possible that elements are added later that require the toolbar. --- spec/toolbar.spec.js | 3 +++ src/js/core.js | 54 ++++++++++++++++++++++++-------------------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/spec/toolbar.spec.js b/spec/toolbar.spec.js index 50fcc7a2e..a405e5f1f 100644 --- a/spec/toolbar.spec.js +++ b/spec/toolbar.spec.js @@ -528,12 +528,15 @@ describe('MediumEditor.extensions.toolbar TestCase', function () { expect(document.getElementsByClassName('medium-editor-toolbar-actions').length).toBe(0); }); + /* + // Only disable using options, it is possible to add elements later who don't have the disable-toolbar attr and will not work it('should not create the toolbar if all elements has data attr of disable-toolbar', function () { this.el.setAttribute('data-disable-toolbar', 'true'); var editor = this.newMediumEditor('.editor'); expect(document.getElementsByClassName('medium-editor-toolbar-actions').length).toBe(0); expect(editor.getExtensionByName('toolbar')).toBeUndefined(); }); + */ it('should not show the toolbar when one element has a data attr of disable-toolbar set and text is selected', function () { var element = this.createElement('div', 'editor', 'lorem ipsum'), diff --git a/src/js/core.js b/src/js/core.js index e71455b95..8607d0779 100644 --- a/src/js/core.js +++ b/src/js/core.js @@ -200,7 +200,10 @@ function handleEditableInput(event, editable) { var textarea = editable.parentNode.querySelector('textarea[medium-editor-textarea-id="' + editable.getAttribute('medium-editor-textarea-id') + '"]'); if (textarea) { - textarea.value = editable.innerHTML.trim(); + var index = this.elements.indexOf(editable); + if (index !== -1) { + textarea.value = this.getContent(index); + } } } @@ -315,14 +318,6 @@ } function isToolbarEnabled() { - // If any of the elements don't have the toolbar disabled - // We need a toolbar - if (this.elements.every(function (element) { - return !!element.getAttribute('data-disable-toolbar'); - })) { - return false; - } - return this.options.toolbar !== false; } @@ -694,17 +689,13 @@ this.events = new MediumEditor.Events(this); this.elements = []; - this.addElements(this.origElements); - - if (this.elements.length === 0) { - return; - } - - this.isActive = true; - // Call initialization helpers initExtensions.call(this); attachHandlers.call(this); + + this.addElements(this.origElements); + + this.isActive = true; }, destroy: function () { @@ -722,11 +713,9 @@ this.events.destroy(); - this.elements.forEach(function (element) { - // Reset elements content, fix for issue where after editor destroyed the red underlines on spelling errors are left - if (this.options.spellcheck) { - element.innerHTML = element.innerHTML; - } + this.elements.forEach(function (element, i) { + // Call the getContent to set innerHTML, extensions can do some cleanup + element.innerHTML = this.getContent(i); // cleanup extra added attributes element.removeAttribute('contentEditable'); @@ -798,7 +787,7 @@ for (i = 0; i < len; i += 1) { elementid = (this.elements[i].id !== '') ? this.elements[i].id : 'element-' + i; content[elementid] = { - value: this.elements[i].innerHTML.trim() + value: this.getContent(i) }; } return content; @@ -1190,7 +1179,15 @@ if (this.elements[index]) { var target = this.elements[index]; + + this.extensions.forEach(function (extension) { + if (typeof extension.setContent === 'function') { + html = extension.setContent(html); + } + }, this); + target.innerHTML = html; + this.checkContentChanged(target); } }, @@ -1199,8 +1196,17 @@ index = index || 0; if (this.elements[index]) { - return this.elements[index].innerHTML.trim(); + var html = this.elements[index].innerHTML.trim(); + + this.extensions.forEach(function (extension) { + if (typeof extension.getContent === 'function') { + html = extension.getContent(html); + } + }, this); + + return html; } + return null; }, From 20dc8234f6f3bc467fb66b617230afac1218186a Mon Sep 17 00:00:00 2001 From: Patrick Woldberg Date: Wed, 19 Jul 2017 10:35:23 +0200 Subject: [PATCH 2/5] call setContent directly after creating the contentEditable to allow extensions to update the editable html. tests needed an update because the innerHTML of the content editable was replaced after init. --- spec/util.spec.js | 16 +++++++--------- src/js/core.js | 2 ++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/util.spec.js b/spec/util.spec.js index de688b2bc..dfac18b41 100644 --- a/spec/util.spec.js +++ b/spec/util.spec.js @@ -570,10 +570,9 @@ describe('MediumEditor.util', function () { }); it('should return the parent editable if element is a text node child of the editor', function () { - var el = this.createElement('div', 'editable', '

text

'), - emptyTextNode = el.firstChild; + var el = this.createElement('div', 'editable', '

text

'); this.newMediumEditor('.editable'); - var container = MediumEditor.util.getClosestBlockContainer(emptyTextNode); + var container = MediumEditor.util.getClosestBlockContainer(el.firstChild); expect(container).toBe(el); }); }); @@ -587,10 +586,9 @@ describe('MediumEditor.util', function () { }); it('should return the parent editable if element is a text node child of the editor', function () { - var el = this.createElement('div', 'editable', '

text

'), - emptyTextNode = el.firstChild; + var el = this.createElement('div', 'editable', '

text

'); this.newMediumEditor('.editable'); - var container = MediumEditor.util.getTopBlockContainer(emptyTextNode); + var container = MediumEditor.util.getTopBlockContainer(el.firstChild); expect(container).toBe(el); }); }); @@ -613,10 +611,10 @@ describe('MediumEditor.util', function () { }); it('should not find a previous sibling if the element is at the beginning of an editor element', function () { - var el = this.createElement('div', 'editable', '

first second third

'), - first = el.querySelector('p').firstChild; + var el = this.createElement('div', 'editable', '

first second third

'); this.newMediumEditor('.editable'); - var prevSibling = MediumEditor.util.findPreviousSibling(first); + var first = el.querySelector('p').firstChild, + prevSibling = MediumEditor.util.findPreviousSibling(first); expect(prevSibling).toBeFalsy(); }); }); diff --git a/src/js/core.js b/src/js/core.js index 8607d0779..b5aafe908 100644 --- a/src/js/core.js +++ b/src/js/core.js @@ -1249,6 +1249,8 @@ // Add new elements to our internal elements array this.elements.push(element); + this.setContent(element.innerHTML, this.elements.length - 1); + // Trigger event so extensions can know when an element has been added this.trigger('addElement', { target: element, currentTarget: element }, element); }, this); From 3a9ee74e7db55058021e04d483244ac89b9d8d87 Mon Sep 17 00:00:00 2001 From: Patrick Woldberg Date: Thu, 20 Jul 2017 17:13:29 +0200 Subject: [PATCH 3/5] Fix for issue #976 --- spec/util.spec.js | 11 ++++++----- src/js/util.js | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/spec/util.spec.js b/spec/util.spec.js index dfac18b41..568103798 100644 --- a/spec/util.spec.js +++ b/spec/util.spec.js @@ -389,9 +389,10 @@ describe('MediumEditor.util', function () { it('should execute indent command when called with blockquote when isIE is true', function () { var origIsIE = MediumEditor.util.isIE, - el = this.createElement('div', '', '

Some Text

'); + el = this.createElement('div', 'editable', '

Some Text

'); + this.newMediumEditor('.editable'); MediumEditor.util.isIE = true; - el.setAttribute('contenteditable', true); + //el.setAttribute('contenteditable', true); selectElementContents(el.querySelector('b')); spyOn(document, 'execCommand'); @@ -579,9 +580,9 @@ describe('MediumEditor.util', function () { describe('getTopBlockContainer', function () { it('should return the highest level block container', function () { - var el = this.createElement('div', '', '

paragraph

  • list item
'), - span = el.querySelector('span'), - container = MediumEditor.util.getTopBlockContainer(span); + var el = this.createElement('div', 'editable', '

paragraph

  • list item
'); + this.newMediumEditor('.editable'); + var container = MediumEditor.util.getTopBlockContainer(el.querySelector('span')); expect(container).toBe(el.querySelector('blockquote')); }); diff --git a/src/js/util.js b/src/js/util.js index f3007be27..cb8b25088 100644 --- a/src/js/util.js +++ b/src/js/util.js @@ -112,7 +112,7 @@ // all other known block elements 'address', 'article', 'aside', 'audio', 'canvas', 'dd', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'header', 'hgroup', 'main', 'nav', - 'noscript', 'output', 'section', 'video', + 'noscript', 'output', 'section', 'video', 'div', 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td' ], @@ -926,7 +926,7 @@ }, isBlockContainer: function (element) { - return element && element.nodeType !== 3 && Util.blockContainerElementNames.indexOf(element.nodeName.toLowerCase()) !== -1; + return element && element.nodeType !== 3 && !Util.isMediumEditorElement(element) && Util.blockContainerElementNames.indexOf(element.nodeName.toLowerCase()) !== -1; }, /* Finds the closest ancestor which is a block container element From aac63d427b3061ae86bf4fa6484f6e6b61407636 Mon Sep 17 00:00:00 2001 From: Patrick Woldberg Date: Fri, 21 Jul 2017 12:22:24 +0200 Subject: [PATCH 4/5] by adding div to list of block containers it did not convert the divs created after a enter to a p tag, this is now catched. Fix for when applying block formatting when on a list item, it will remove list formatting first. fixed issue #1348 --- src/js/core.js | 4 ++++ src/js/util.js | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/js/core.js b/src/js/core.js index b5aafe908..b4f13391e 100644 --- a/src/js/core.js +++ b/src/js/core.js @@ -195,6 +195,10 @@ this.options.ownerDocument.execCommand('formatBlock', false, 'p'); } } + + if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) && node.nodeName.toLowerCase() === 'div') { + this.options.ownerDocument.execCommand('formatBlock', false, 'p'); + } } function handleEditableInput(event, editable) { diff --git a/src/js/util.js b/src/js/util.js index cb8b25088..3644642da 100644 --- a/src/js/util.js +++ b/src/js/util.js @@ -545,6 +545,21 @@ tagName = '<' + tagName + '>'; } + if (blockContainer) { + var blockTagName = blockContainer.nodeName.toLowerCase(); + + if (tagName !== blockTagName) { + // Changing list items to something else breaks, remove list item first + if (blockTagName === 'ul') { + doc.execCommand('insertunorderedlist', false); + } + + if (blockTagName === 'ol') { + doc.execCommand('insertorderedlist', false); + } + } + } + // When FF, IE and Edge, we have to handle blockquote node seperately as 'formatblock' does not work. // https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand#Commands if (blockContainer && blockContainer.nodeName.toLowerCase() === 'blockquote') { From e3706c20845fc9e1f4a7ae96abae09787fa4f94b Mon Sep 17 00:00:00 2001 From: Patrick Woldberg Date: Fri, 21 Jul 2017 12:50:57 +0200 Subject: [PATCH 5/5] added test for the setContent() and getContent() in extensions --- spec/extension.spec.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/spec/extension.spec.js b/spec/extension.spec.js index 0ef322567..de05357ae 100644 --- a/spec/extension.spec.js +++ b/spec/extension.spec.js @@ -285,6 +285,26 @@ describe('Extensions TestCase', function () { expect(tempExtension.getEditorOption('spellcheck')).toBe(editor.options.spellcheck); }); + it('should be able to manipulate the html via setContent() and getContent()', function () { + var TempExtension = MediumEditor.Extension.extend({ + setContent: function (html) { + return html.replace(/replace/g, '*******'); + }, + getContent: function (html) { + return html.replace(/\*\*\*\*\*\*\*/g, 'replace'); + } + }), + editable = this.createElement('div', 'editable', '

replace

'), + editor = this.newMediumEditor('.editable', { + extensions: { + 'temp-extension': new TempExtension() + } + }); + + expect(editable.innerHTML).toBe('

*******

'); + expect(editor.getContent()).toBe('

replace

'); + }); + it('should be able to prevent blur on the editor when user iteracts with extension elements', function () { var sampleElOne = this.createElement('button', null, 'Test Button'), sampleElTwo = this.createElement('textarea', null, 'Test Div'),