medium-editor
Advanced tools
Comparing version 5.5.4 to 5.6.0
@@ -0,1 +1,8 @@ | ||
5.6.0 / 2015-08-07 | ||
================== | ||
* Add new 'tim' theme for medium-editor toolbar | ||
* Fix issue Chrome generated comment tags when pasting | ||
* Fix issue where 'editableInput' is triggered multiple times when creating links | ||
5.5.4 / 2015-08-04 | ||
@@ -2,0 +9,0 @@ ================== |
@@ -134,3 +134,4 @@ /*global module, require, process*/ | ||
} | ||
}] | ||
}], | ||
files: srcFiles.concat('!src/js/extensions/deprecated/*') | ||
}, | ||
@@ -186,3 +187,4 @@ summary: true | ||
'dist/css/themes/mani.css': 'src/sass/themes/mani.scss', | ||
'dist/css/themes/roman.css': 'src/sass/themes/roman.scss' | ||
'dist/css/themes/roman.css': 'src/sass/themes/roman.scss', | ||
'dist/css/themes/tim.css': 'src/sass/themes/tim.scss' | ||
} | ||
@@ -189,0 +191,0 @@ } |
{ | ||
"name": "medium-editor", | ||
"version": "5.5.4", | ||
"version": "5.6.0", | ||
"author": "Davi Ferreira <hi@daviferreira.com>", | ||
@@ -5,0 +5,0 @@ "contributors": [ |
@@ -266,2 +266,45 @@ /*global MediumEditor, describe, it, expect, spyOn, | ||
it('should fire editableInput only once when the user creates a link open to a new window,' + | ||
' and it should fire at the end of the DOM and selection modifications', function () { | ||
spyOn(MediumEditor.prototype, 'createLink').and.callThrough(); | ||
this.el.innerHTML = '<p>Lorem ipsum et dolitur sunt.</p>'; | ||
var editor = this.newMediumEditor('.editor', { | ||
anchor: { | ||
targetCheckbox: true | ||
} | ||
}), | ||
p = this.el.lastChild, | ||
anchorExtension = editor.getExtensionByName('anchor'), | ||
toolbar = editor.getExtensionByName('toolbar'), | ||
selectionWhenEventsFired = [], | ||
listener = function () { | ||
selectionWhenEventsFired.push(window.getSelection().toString()); | ||
}; | ||
MediumEditor.selection.select(document, p.firstChild, 'Lorem '.length, p.firstChild, 'Lorem ipsum'.length); | ||
fireEvent(editor.elements[0], 'focus'); | ||
jasmine.clock().tick(1); | ||
// Click the 'anchor' button in the toolbar | ||
fireEvent(toolbar.getToolbarElement().querySelector('[data-action="createLink"]'), 'click'); | ||
// Input a url and save | ||
var input = anchorExtension.getInput(), | ||
checkbox = anchorExtension.getAnchorTargetCheckbox(); | ||
input.value = 'http://www.example.com'; | ||
checkbox.checked = true; | ||
editor.subscribe('editableInput', listener); | ||
fireEvent(input, 'keyup', { | ||
keyCode: Util.keyCode.ENTER | ||
}); | ||
expect(editor.createLink).toHaveBeenCalledWith({ | ||
url: 'http://www.example.com', | ||
target: '_blank' | ||
}); | ||
expect(window.getSelection().toString()).toBe('ipsum', 'selected text should remain selected'); | ||
expect(selectionWhenEventsFired.length).toBe(1, 'only one editableInput event should have been registered'); | ||
expect(selectionWhenEventsFired[0]).toBe('ipsum', 'selected text should have been the same when event fired'); | ||
}); | ||
it('should not select empty paragraphs when link is created at beginning of paragraph', function () { | ||
@@ -268,0 +311,0 @@ spyOn(MediumEditor.prototype, 'createLink').and.callThrough(); |
@@ -55,2 +55,16 @@ /*global describe, it, expect, jasmine, | ||
}); | ||
it('can be disabled for a temporary period of time on a named basis', function () { | ||
var editor = this.newMediumEditor('.editor'), | ||
spy = jasmine.createSpy('handler'), | ||
tempData = { temp: 'data' }; | ||
editor.subscribe('myIncredibleEvent', spy); | ||
expect(spy).not.toHaveBeenCalled(); | ||
editor.events.disableCustomEvent('myIncredibleEvent'); | ||
editor.trigger('myIncredibleEvent', tempData, editor.elements[0]); | ||
expect(spy).not.toHaveBeenCalled(); | ||
editor.events.enableCustomEvent('myIncredibleEvent'); | ||
editor.trigger('myIncredibleEvent', tempData, editor.elements[0]); | ||
expect(spy).toHaveBeenCalledWith(tempData, editor.elements[0]); | ||
}); | ||
}); | ||
@@ -57,0 +71,0 @@ |
@@ -900,122 +900,129 @@ /*global Util, Selection, Extension, | ||
createLink: function (opts) { | ||
var customEvent, i; | ||
var currentEditor, customEvent, i; | ||
if (opts.url && opts.url.trim().length > 0) { | ||
var currentSelection = this.options.contentWindow.getSelection(); | ||
if (currentSelection) { | ||
var currRange = currentSelection.getRangeAt(0), | ||
commonAncestorContainer = currRange.commonAncestorContainer, | ||
exportedSelection, | ||
startContainerParentElement, | ||
endContainerParentElement, | ||
textNodes; | ||
try { | ||
this.events.disableCustomEvent('editableInput'); | ||
if (opts.url && opts.url.trim().length > 0) { | ||
var currentSelection = this.options.contentWindow.getSelection(); | ||
if (currentSelection) { | ||
var currRange = currentSelection.getRangeAt(0), | ||
commonAncestorContainer = currRange.commonAncestorContainer, | ||
exportedSelection, | ||
startContainerParentElement, | ||
endContainerParentElement, | ||
textNodes; | ||
// If the selection is contained within a single text node | ||
// and the selection starts at the beginning of the text node, | ||
// MSIE still says the startContainer is the parent of the text node. | ||
// If the selection is contained within a single text node, we | ||
// want to just use the default browser 'createLink', so we need | ||
// to account for this case and adjust the commonAncestorContainer accordingly | ||
if (currRange.endContainer.nodeType === 3 && | ||
currRange.startContainer.nodeType !== 3 && | ||
currRange.startOffset === 0 && | ||
currRange.startContainer.firstChild === currRange.endContainer) { | ||
commonAncestorContainer = currRange.endContainer; | ||
} | ||
// If the selection is contained within a single text node | ||
// and the selection starts at the beginning of the text node, | ||
// MSIE still says the startContainer is the parent of the text node. | ||
// If the selection is contained within a single text node, we | ||
// want to just use the default browser 'createLink', so we need | ||
// to account for this case and adjust the commonAncestorContainer accordingly | ||
if (currRange.endContainer.nodeType === 3 && | ||
currRange.startContainer.nodeType !== 3 && | ||
currRange.startOffset === 0 && | ||
currRange.startContainer.firstChild === currRange.endContainer) { | ||
commonAncestorContainer = currRange.endContainer; | ||
} | ||
startContainerParentElement = Util.getClosestBlockContainer(currRange.startContainer); | ||
endContainerParentElement = Util.getClosestBlockContainer(currRange.endContainer); | ||
startContainerParentElement = Util.getClosestBlockContainer(currRange.startContainer); | ||
endContainerParentElement = Util.getClosestBlockContainer(currRange.endContainer); | ||
// If the selection is not contained within a single text node | ||
// but the selection is contained within the same block element | ||
// we want to make sure we create a single link, and not multiple links | ||
// which can happen with the built in browser functionality | ||
if (commonAncestorContainer.nodeType !== 3 && startContainerParentElement === endContainerParentElement) { | ||
// If the selection is not contained within a single text node | ||
// but the selection is contained within the same block element | ||
// we want to make sure we create a single link, and not multiple links | ||
// which can happen with the built in browser functionality | ||
if (commonAncestorContainer.nodeType !== 3 && startContainerParentElement === endContainerParentElement) { | ||
var currentEditor = Selection.getSelectionElement(this.options.contentWindow), | ||
parentElement = (startContainerParentElement || currentEditor), | ||
fragment = this.options.ownerDocument.createDocumentFragment(); | ||
currentEditor = Selection.getSelectionElement(this.options.contentWindow); | ||
var parentElement = (startContainerParentElement || currentEditor), | ||
fragment = this.options.ownerDocument.createDocumentFragment(); | ||
// since we are going to create a link from an extracted text, | ||
// be sure that if we are updating a link, we won't let an empty link behind (see #754) | ||
// (Workaroung for Chrome) | ||
this.execAction('unlink'); | ||
// since we are going to create a link from an extracted text, | ||
// be sure that if we are updating a link, we won't let an empty link behind (see #754) | ||
// (Workaroung for Chrome) | ||
this.execAction('unlink'); | ||
exportedSelection = this.exportSelection(); | ||
fragment.appendChild(parentElement.cloneNode(true)); | ||
exportedSelection = this.exportSelection(); | ||
fragment.appendChild(parentElement.cloneNode(true)); | ||
if (currentEditor === parentElement) { | ||
// We have to avoid the editor itself being wiped out when it's the only block element, | ||
// as our reference inside this.elements gets detached from the page when insertHTML runs. | ||
// If we just use [parentElement, 0] and [parentElement, parentElement.childNodes.length] | ||
// as the range boundaries, this happens whenever parentElement === currentEditor. | ||
// The tradeoff to this workaround is that a orphaned tag can sometimes be left behind at | ||
// the end of the editor's content. | ||
// In Gecko: | ||
// as an empty <strong></strong> if parentElement.lastChild is a <strong> tag. | ||
// In WebKit: | ||
// an invented <br /> tag at the end in the same situation | ||
Selection.select( | ||
if (currentEditor === parentElement) { | ||
// We have to avoid the editor itself being wiped out when it's the only block element, | ||
// as our reference inside this.elements gets detached from the page when insertHTML runs. | ||
// If we just use [parentElement, 0] and [parentElement, parentElement.childNodes.length] | ||
// as the range boundaries, this happens whenever parentElement === currentEditor. | ||
// The tradeoff to this workaround is that a orphaned tag can sometimes be left behind at | ||
// the end of the editor's content. | ||
// In Gecko: | ||
// as an empty <strong></strong> if parentElement.lastChild is a <strong> tag. | ||
// In WebKit: | ||
// an invented <br /> tag at the end in the same situation | ||
Selection.select( | ||
this.options.ownerDocument, | ||
parentElement.firstChild, | ||
0, | ||
parentElement.lastChild, | ||
parentElement.lastChild.nodeType === 3 ? | ||
parentElement.lastChild.nodeValue.length : parentElement.lastChild.childNodes.length | ||
); | ||
} else { | ||
Selection.select( | ||
this.options.ownerDocument, | ||
parentElement, | ||
0, | ||
parentElement, | ||
parentElement.childNodes.length | ||
); | ||
} | ||
var modifiedExportedSelection = this.exportSelection(); | ||
textNodes = Util.findOrCreateMatchingTextNodes( | ||
this.options.ownerDocument, | ||
parentElement.firstChild, | ||
0, | ||
parentElement.lastChild, | ||
parentElement.lastChild.nodeType === 3 ? | ||
parentElement.lastChild.nodeValue.length : parentElement.lastChild.childNodes.length | ||
fragment, | ||
{ | ||
start: exportedSelection.start - modifiedExportedSelection.start, | ||
end: exportedSelection.end - modifiedExportedSelection.start, | ||
editableElementIndex: exportedSelection.editableElementIndex | ||
} | ||
); | ||
} else { | ||
Selection.select( | ||
this.options.ownerDocument, | ||
parentElement, | ||
0, | ||
parentElement, | ||
parentElement.childNodes.length | ||
); | ||
} | ||
var modifiedExportedSelection = this.exportSelection(); | ||
// Creates the link in the document fragment | ||
Util.createLink(this.options.ownerDocument, textNodes, opts.url.trim()); | ||
textNodes = Util.findOrCreateMatchingTextNodes( | ||
this.options.ownerDocument, | ||
fragment, | ||
{ | ||
start: exportedSelection.start - modifiedExportedSelection.start, | ||
end: exportedSelection.end - modifiedExportedSelection.start, | ||
editableElementIndex: exportedSelection.editableElementIndex | ||
} | ||
); | ||
// Chrome trims the leading whitespaces when inserting HTML, which messes up restoring the selection. | ||
var leadingWhitespacesCount = (fragment.firstChild.innerHTML.match(/^\s+/) || [''])[0].length; | ||
// Creates the link in the document fragment | ||
Util.createLink(this.options.ownerDocument, textNodes, opts.url.trim()); | ||
// Now move the created link back into the original document in a way to preserve undo/redo history | ||
Util.insertHTMLCommand(this.options.ownerDocument, fragment.firstChild.innerHTML.replace(/^\s+/, '')); | ||
exportedSelection.start -= leadingWhitespacesCount; | ||
exportedSelection.end -= leadingWhitespacesCount; | ||
// Chrome trims the leading whitespaces when inserting HTML, which messes up restoring the selection. | ||
var leadingWhitespacesCount = (fragment.firstChild.innerHTML.match(/^\s+/) || [''])[0].length; | ||
this.importSelection(exportedSelection); | ||
} else { | ||
this.options.ownerDocument.execCommand('createLink', false, opts.url); | ||
} | ||
// Now move the created link back into the original document in a way to preserve undo/redo history | ||
Util.insertHTMLCommand(this.options.ownerDocument, fragment.firstChild.innerHTML.replace(/^\s+/, '')); | ||
exportedSelection.start -= leadingWhitespacesCount; | ||
exportedSelection.end -= leadingWhitespacesCount; | ||
if (this.options.targetBlank || opts.target === '_blank') { | ||
Util.setTargetBlank(Selection.getSelectionStart(this.options.ownerDocument), opts.url); | ||
} | ||
this.importSelection(exportedSelection); | ||
} else { | ||
this.options.ownerDocument.execCommand('createLink', false, opts.url); | ||
if (opts.buttonClass) { | ||
Util.addClassToAnchors(Selection.getSelectionStart(this.options.ownerDocument), opts.buttonClass); | ||
} | ||
} | ||
if (this.options.targetBlank || opts.target === '_blank') { | ||
Util.setTargetBlank(Selection.getSelectionStart(this.options.ownerDocument), opts.url); | ||
} | ||
// Fire input event for backwards compatibility if anyone was listening directly to the DOM input event | ||
if (this.options.targetBlank || opts.target === '_blank' || opts.buttonClass) { | ||
customEvent = this.options.ownerDocument.createEvent('HTMLEvents'); | ||
customEvent.initEvent('input', true, true, this.options.contentWindow); | ||
for (i = 0; i < this.elements.length; i += 1) { | ||
this.elements[i].dispatchEvent(customEvent); | ||
} | ||
if (opts.buttonClass) { | ||
Util.addClassToAnchors(Selection.getSelectionStart(this.options.ownerDocument), opts.buttonClass); | ||
} | ||
} | ||
} finally { | ||
this.events.enableCustomEvent('editableInput'); | ||
} | ||
if (this.options.targetBlank || opts.target === '_blank' || opts.buttonClass) { | ||
customEvent = this.options.ownerDocument.createEvent('HTMLEvents'); | ||
customEvent.initEvent('input', true, true, this.options.contentWindow); | ||
for (i = 0; i < this.elements.length; i += 1) { | ||
this.elements[i].dispatchEvent(customEvent); | ||
} | ||
} | ||
// Fire our custom editableInput event | ||
this.events.triggerCustomEvent('editableInput', customEvent, currentEditor); | ||
}, | ||
@@ -1022,0 +1029,0 @@ |
@@ -12,2 +12,3 @@ /*global Util*/ | ||
this.events = []; | ||
this.disabledEvents = {}; | ||
this.customEvents = {}; | ||
@@ -55,2 +56,12 @@ this.listeners = {}; | ||
enableCustomEvent: function (event) { | ||
if (this.disabledEvents[event] !== undefined) { | ||
delete this.disabledEvents[event]; | ||
} | ||
}, | ||
disableCustomEvent: function (event) { | ||
this.disabledEvents[event] = true; | ||
}, | ||
// custom events | ||
@@ -87,3 +98,3 @@ attachCustomEvent: function (event, listener) { | ||
triggerCustomEvent: function (name, data, editable) { | ||
if (this.customEvents[name]) { | ||
if (this.customEvents[name] && !this.disabledEvents[name]) { | ||
this.customEvents[name].forEach(function (listener) { | ||
@@ -90,0 +101,0 @@ listener(data, editable); |
@@ -15,3 +15,2 @@ /*global Util, Extension */ | ||
return [ | ||
// replace two bogus tags that begin pastes from google docs | ||
@@ -46,3 +45,7 @@ [new RegExp(/<[^>]*docs-internal-guid[^>]*>/gi), ''], | ||
// Microsoft Word makes these odd tags, like <o:p></o:p> | ||
[new RegExp(/<\/?o:[a-z]*>/gi), ''] | ||
[new RegExp(/<\/?o:[a-z]*>/gi), ''], | ||
// cleanup comments added by Chrome when pasting html | ||
['<!--EndFragment-->', ''], | ||
['<!--StartFragment-->', ''] | ||
]; | ||
@@ -49,0 +52,0 @@ } |
@@ -20,3 +20,3 @@ /*global MediumEditor */ | ||
// grunt-bump looks for this: | ||
'version': '5.5.4' | ||
'version': '5.6.0' | ||
}).version); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1516833
127
18583