medium-editor
Advanced tools
Comparing version 5.2.0 to 5.3.0
@@ -0,1 +1,10 @@ | ||
5.3.0 / 2015-07-07 | ||
================== | ||
* Fix issue with disabling image drag & drop via imageDragging option | ||
* Deprecate image-dragging extension | ||
* Introduce file-dragging extension | ||
* Ensure autolink urls respect targetBlank option | ||
* Expose importSelection and exportSelection as generic Selection helpers | ||
5.2.0 / 2015-06-29 | ||
@@ -2,0 +11,0 @@ ================== |
@@ -27,3 +27,3 @@ /*global module, require, process*/ | ||
'src/js/extensions/auto-link.js', | ||
'src/js/extensions/image-dragging.js', | ||
'src/js/extensions/file-dragging.js', | ||
'src/js/extensions/keyboard-commands.js', | ||
@@ -34,2 +34,3 @@ 'src/js/extensions/fontsize.js', | ||
'src/js/extensions/toolbar.js', | ||
'src/js/extensions/deprecated/image-dragging.js', | ||
'src/js/defaults/options.js', | ||
@@ -36,0 +37,0 @@ 'src/js/defaults/extensions.js', |
{ | ||
"name": "medium-editor", | ||
"version": "5.2.0", | ||
"version": "5.3.0", | ||
"author": "Davi Ferreira <hi@daviferreira.com>", | ||
@@ -5,0 +5,0 @@ "contributors": [ |
@@ -401,2 +401,18 @@ /*global describe, it, expect, beforeEach, afterEach, | ||
it('should create a link with target="_blank" when respective option is set to true', function () { | ||
this.el = this.createElement('div', 'editor-blank', ''); | ||
this.newMediumEditor('.editor-blank', { | ||
autoLink: true, | ||
targetBlank: true | ||
}); | ||
this.el.innerHTML = 'http://www.example.com'; | ||
selectElementContentsAndFire(this.el); | ||
triggerAutolinking(this.el); | ||
var links = this.el.getElementsByTagName('a'); | ||
expect(links.length).toBe(1); | ||
expect(links[0].getAttribute('href')).toBe('http://www.example.com'); | ||
expect(links[0].target).toBe('_blank'); | ||
}); | ||
it('should stop attempting to auto-link on keypress if an error is encountered', function () { | ||
@@ -403,0 +419,0 @@ var spy = spyOn(MediumEditor.extensions.autoLink.prototype, 'performLinking').and.throwError('DOM ERROR'); |
@@ -17,20 +17,24 @@ /*global describe, it, expect, spyOn, | ||
it('should do nothing when option is false', function () { | ||
var editor = this.newMediumEditor(this.el, { imageDragging: false }); | ||
fireEvent(editor.elements[0], 'dragover'); | ||
expect(editor.elements[0].className).not.toContain('medium-editor-dragover'); | ||
}); | ||
describe('drag', function () { | ||
it('should add medium-editor-dragover class', function () { | ||
var editor = this.newMediumEditor(this.el); | ||
fireEvent(editor.elements[0], 'dragover'); | ||
expect(editor.elements[0].className).toContain('medium-editor-dragover'); | ||
}); | ||
it('should add medium-editor-dragover class on drag over', function () { | ||
var editor = this.newMediumEditor(this.el); | ||
fireEvent(editor.elements[0], 'dragover'); | ||
expect(editor.elements[0].className).toContain('medium-editor-dragover'); | ||
}); | ||
it('should add medium-editor-dragover class even when data is invalid', function () { | ||
var editor = this.newMediumEditor(this.el, { | ||
imageDragging: false | ||
}); | ||
fireEvent(editor.elements[0], 'dragover'); | ||
expect(editor.elements[0].className).toContain('medium-editor-dragover'); | ||
}); | ||
it('should remove medium-editor-dragover class on drag leave', function () { | ||
var editor = this.newMediumEditor(this.el); | ||
fireEvent(editor.elements[0], 'dragover'); | ||
expect(editor.elements[0].className).toContain('medium-editor-dragover'); | ||
fireEvent(editor.elements[0], 'dragleave'); | ||
expect(editor.elements[0].className).not.toContain('medium-editor-dragover'); | ||
it('should remove medium-editor-dragover class on drag leave', function () { | ||
var editor = this.newMediumEditor(this.el); | ||
fireEvent(editor.elements[0], 'dragover'); | ||
expect(editor.elements[0].className).toContain('medium-editor-dragover'); | ||
fireEvent(editor.elements[0], 'dragleave'); | ||
expect(editor.elements[0].className).not.toContain('medium-editor-dragover'); | ||
}); | ||
}); | ||
@@ -51,3 +55,13 @@ | ||
}); | ||
it('should remove medium-editor-dragover class and NOT add the image to the editor content', function () { | ||
spyOn(Util, 'insertHTMLCommand').and.callThrough(); | ||
var editor = this.newMediumEditor(this.el, { imageDragging: false }); | ||
fireEvent(editor.elements[0], 'dragover'); | ||
expect(editor.elements[0].className).toContain('medium-editor-dragover'); | ||
fireEvent(editor.elements[0], 'drop'); | ||
expect(editor.elements[0].className).not.toContain('medium-editor-dragover'); | ||
expect(Util.insertHTMLCommand).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
}); |
@@ -166,6 +166,6 @@ /*global MediumEditor, describe, it, expect, spyOn, | ||
it('should not add default extensions', function () { | ||
it('should not add default extensions when overriden', function () { | ||
var editor, | ||
Preview, Placeholder, AutoLink, ImageDragging, | ||
extPreview, extPlaceholder, extAutoLink, extImageDragging; | ||
Preview, Placeholder, AutoLink, FileDragging, | ||
extPreview, extPlaceholder, extAutoLink, extFileDragging; | ||
@@ -175,3 +175,3 @@ Preview = Extension.extend({ name: 'anchor-preview' }); | ||
AutoLink = Extension.extend({ name: 'auto-link' }); | ||
ImageDragging = Extension.extend({ name: 'image-dragging' }); | ||
FileDragging = Extension.extend({ name: 'fileDragging' }); | ||
@@ -181,3 +181,3 @@ extPreview = new Preview(); | ||
extAutoLink = new AutoLink(); | ||
extImageDragging = new ImageDragging(); | ||
extFileDragging = new FileDragging(); | ||
@@ -189,3 +189,3 @@ editor = this.newMediumEditor('.editor', { | ||
'auto-link': extAutoLink, | ||
'image-dragging': extImageDragging | ||
'fileDragging': extFileDragging | ||
} | ||
@@ -197,3 +197,3 @@ }); | ||
expect(editor.getExtensionByName('auto-link')).toBe(extAutoLink); | ||
expect(editor.getExtensionByName('image-dragging')).toBe(extImageDragging); | ||
expect(editor.getExtensionByName('fileDragging')).toBe(extFileDragging); | ||
}); | ||
@@ -200,0 +200,0 @@ |
@@ -161,4 +161,9 @@ /*global atob, unescape, Uint8Array, Blob*/ | ||
}; | ||
if (!isIE9()) { | ||
evt.dataTransfer.files = [dataURItoBlob('data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7')]; | ||
// File API only allows access to 'files' on drop, not on any other event | ||
if (!isIE9() && eventName === 'drop') { | ||
var file = dataURItoBlob('data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'); | ||
if (!file.type) { | ||
file.type = 'image/gif'; | ||
} | ||
evt.dataTransfer.files = [file]; | ||
} | ||
@@ -165,0 +170,0 @@ } |
@@ -286,2 +286,9 @@ /*global Util, Selection, Extension, | ||
function shouldUseFileDraggingExtension() { | ||
// Since the file-dragging extension replaces the image-dragging extension, | ||
// we need to check if the user passed an overrided image-dragging extension. | ||
// If they have, to avoid breaking users, we won't use file-dragging extension. | ||
return !this.options.extensions['imageDragging']; | ||
} | ||
function createContentEditable(textarea, id) { | ||
@@ -388,2 +395,26 @@ var div = this.options.ownerDocument.createElement('div'), | ||
// 4 Cases for imageDragging + fileDragging extensons: | ||
// | ||
// 1. ImageDragging ON + No Custom Image Dragging Extension: | ||
// * Use fileDragging extension (default options) | ||
// 2. ImageDragging OFF + No Custom Image Dragging Extension: | ||
// * Use fileDragging extension w/ images turned off | ||
// 3. ImageDragging ON + Custom Image Dragging Extension: | ||
// * Don't use fileDragging (could interfere with custom image dragging extension) | ||
// 4. ImageDragging OFF + Custom Image Dragging: | ||
// * Don't use fileDragging (could interfere with custom image dragging extension) | ||
if (shouldUseFileDraggingExtension.call(this)) { | ||
var opts = this.options.fileDragging; | ||
if (!opts) { | ||
opts = {}; | ||
// Image is in the 'allowedTypes' list by default. | ||
// If imageDragging is off override the 'allowedTypes' list with an empty one | ||
if (!isImageDraggingEnabled.call(this)) { | ||
opts.allowedTypes = []; | ||
} | ||
} | ||
this.addBuiltInExtension('fileDragging', opts); | ||
} | ||
// Built-in extensions | ||
@@ -394,3 +425,2 @@ var builtIns = { | ||
autoLink: isAutoLinkEnabled.call(this), | ||
imageDragging: isImageDraggingEnabled.call(this), | ||
keyboardCommands: isKeyboardCommandsEnabled.call(this), | ||
@@ -692,8 +722,8 @@ placeholder: isPlaceholderEnabled.call(this) | ||
break; | ||
case 'fileDragging': | ||
extension = new MediumEditor.extensions.fileDragging(opts); | ||
break; | ||
case 'fontsize': | ||
extension = new MediumEditor.extensions.fontSize(opts); | ||
break; | ||
case 'imageDragging': | ||
extension = new MediumEditor.extensions.imageDragging(); | ||
break; | ||
case 'keyboardCommands': | ||
@@ -841,52 +871,15 @@ extension = new MediumEditor.extensions.keyboardCommands(this.options.keyboardCommands); | ||
// http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html | ||
// Tim Down | ||
// TODO: move to selection.js and clean up old methods there | ||
// Export the state of the selection in respect to one of this | ||
// instance of MediumEditor's elements | ||
exportSelection: function () { | ||
var selectionState = null, | ||
selection = this.options.contentWindow.getSelection(), | ||
range, | ||
preSelectionRange, | ||
start, | ||
editableElementIndex = -1; | ||
var selectionElement = Selection.getSelectionElement(this.options.contentWindow), | ||
editableElementIndex = this.elements.indexOf(selectionElement), | ||
selectionState = null; | ||
if (selection.rangeCount > 0) { | ||
range = selection.getRangeAt(0); | ||
preSelectionRange = range.cloneRange(); | ||
// Find element current selection is inside | ||
this.elements.some(function (el, index) { | ||
if (el === range.startContainer || Util.isDescendant(el, range.startContainer)) { | ||
editableElementIndex = index; | ||
return true; | ||
} | ||
return false; | ||
}); | ||
if (editableElementIndex > -1) { | ||
preSelectionRange.selectNodeContents(this.elements[editableElementIndex]); | ||
preSelectionRange.setEnd(range.startContainer, range.startOffset); | ||
start = preSelectionRange.toString().length; | ||
selectionState = { | ||
start: start, | ||
end: start + range.toString().length, | ||
editableElementIndex: editableElementIndex | ||
}; | ||
// If start = 0 there may still be an empty paragraph before it, but we don't care. | ||
if (start !== 0) { | ||
var emptyBlocksIndex = Selection.getIndexRelativeToAdjacentEmptyBlocks( | ||
this.options.ownerDocument, | ||
this.elements[editableElementIndex], | ||
range.startContainer, | ||
range.startOffset); | ||
if (emptyBlocksIndex !== 0) { | ||
selectionState.emptyBlocksIndex = emptyBlocksIndex; | ||
} | ||
} | ||
} | ||
if (editableElementIndex >= 0) { | ||
selectionState = Selection.exportSelection(selectionElement, this.options.ownerDocument); | ||
} | ||
if (selectionState !== null && selectionState.editableElementIndex === 0) { | ||
delete selectionState.editableElementIndex; | ||
if (selectionState !== null && editableElementIndex !== 0) { | ||
selectionState.editableElementIndex = editableElementIndex; | ||
} | ||
@@ -901,88 +894,11 @@ | ||
// http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html | ||
// Tim Down | ||
// TODO: move to selection.js and clean up old methods there | ||
// | ||
// {object} inSelectionState - the selection to import | ||
// {boolean} [favorLaterSelectionAnchor] - defaults to false. If true, import the cursor immediately | ||
// subsequent to an anchor tag if it would otherwise be placed right at the trailing edge inside the | ||
// anchor. This cursor positioning, even though visually equivalent to the user, can affect behavior | ||
// in MS IE. | ||
importSelection: function (inSelectionState, favorLaterSelectionAnchor) { | ||
if (!inSelectionState) { | ||
// Restore a selection based on a selectionState returned by a call | ||
// to MediumEditor.exportSelection | ||
importSelection: function (selectionState, favorLaterSelectionAnchor) { | ||
if (!selectionState) { | ||
return; | ||
} | ||
var editableElementIndex = inSelectionState.editableElementIndex === undefined ? | ||
0 : inSelectionState.editableElementIndex, | ||
selectionState = { | ||
editableElementIndex: editableElementIndex, | ||
start: inSelectionState.start, | ||
end: inSelectionState.end | ||
}, | ||
editableElement = this.elements[selectionState.editableElementIndex], | ||
charIndex = 0, | ||
range = this.options.ownerDocument.createRange(), | ||
nodeStack = [editableElement], | ||
node, | ||
foundStart = false, | ||
stop = false, | ||
i, | ||
sel, | ||
nextCharIndex; | ||
range.setStart(editableElement, 0); | ||
range.collapse(true); | ||
node = nodeStack.pop(); | ||
while (!stop && node) { | ||
if (node.nodeType === 3) { | ||
nextCharIndex = charIndex + node.length; | ||
if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) { | ||
range.setStart(node, selectionState.start - charIndex); | ||
foundStart = true; | ||
} | ||
if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) { | ||
range.setEnd(node, selectionState.end - charIndex); | ||
stop = true; | ||
} | ||
charIndex = nextCharIndex; | ||
} else { | ||
i = node.childNodes.length - 1; | ||
while (i >= 0) { | ||
nodeStack.push(node.childNodes[i]); | ||
i -= 1; | ||
} | ||
} | ||
if (!stop) { | ||
node = nodeStack.pop(); | ||
} | ||
} | ||
if (inSelectionState.emptyBlocksIndex && selectionState.end === nextCharIndex) { | ||
var targetNode = Util.getTopBlockContainer(range.startContainer), | ||
index = 0; | ||
// Skip over empty blocks until we hit the block we want the selection to be in | ||
while (index < inSelectionState.emptyBlocksIndex && targetNode.nextSibling) { | ||
targetNode = targetNode.nextSibling; | ||
index++; | ||
// If we find a non-empty block, ignore the emptyBlocksIndex and just put selection here | ||
if (targetNode.textContent.length > 0) { | ||
break; | ||
} | ||
} | ||
// We're selecting a high-level block node, so make sure the cursor gets moved into the deepest | ||
// element at the beginning of the block | ||
range.setStart(Util.getFirstSelectableLeafNode(targetNode), 0); | ||
range.collapse(true); | ||
} | ||
// If the selection is right at the ending edge of a link, put it outside the anchor tag instead of inside. | ||
if (favorLaterSelectionAnchor) { | ||
range = Selection.importSelectionMoveCursorPastAnchor(selectionState, range); | ||
} | ||
sel = this.options.contentWindow.getSelection(); | ||
sel.removeAllRanges(); | ||
sel.addRange(range); | ||
var editableElement = this.elements[selectionState.editableElementIndex || 0]; | ||
Selection.importSelection(selectionState, editableElement, this.options.ownerDocument, favorLaterSelectionAnchor); | ||
}, | ||
@@ -989,0 +905,0 @@ |
@@ -1,3 +0,3 @@ | ||
/*global Button, FormExtension, | ||
AnchorForm, AnchorPreview, AutoLink, | ||
/*global Button, FormExtension, AnchorForm, | ||
AnchorPreview, AutoLink, FileDragging, | ||
FontSizeForm, KeyboardCommands, ImageDragging, | ||
@@ -17,4 +17,5 @@ PasteHandler, Placeholder, Toolbar */ | ||
autoLink: AutoLink, | ||
fileDragging: FileDragging, | ||
fontSize: FontSizeForm, | ||
imageDragging: ImageDragging, | ||
imageDragging: ImageDragging, // deprecated | ||
keyboardCommands: KeyboardCommands, | ||
@@ -21,0 +22,0 @@ paste: PasteHandler, |
@@ -208,3 +208,3 @@ /*global Extension, Util */ | ||
href = Util.ensureUrlHasProtocol(href); | ||
var anchor = Util.createLink(this.document, textNodes, href), | ||
var anchor = Util.createLink(this.document, textNodes, href, this.getEditorOption('targetBlank') ? '_blank' : null), | ||
span = this.document.createElement('span'); | ||
@@ -211,0 +211,0 @@ span.setAttribute('data-auto-link', 'true'); |
@@ -37,2 +37,116 @@ /*global Util */ | ||
// http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html | ||
// Tim Down | ||
exportSelection: function (root, doc) { | ||
if (!root) { | ||
return null; | ||
} | ||
var selectionState = null, | ||
selection = doc.getSelection(); | ||
if (selection.rangeCount > 0) { | ||
var range = selection.getRangeAt(0), | ||
preSelectionRange = range.cloneRange(), | ||
start; | ||
preSelectionRange.selectNodeContents(root); | ||
preSelectionRange.setEnd(range.startContainer, range.startOffset); | ||
start = preSelectionRange.toString().length; | ||
selectionState = { | ||
start: start, | ||
end: start + range.toString().length | ||
}; | ||
// If start = 0 there may still be an empty paragraph before it, but we don't care. | ||
if (start !== 0) { | ||
var emptyBlocksIndex = this.getIndexRelativeToAdjacentEmptyBlocks(doc, root, range.startContainer, range.startOffset); | ||
if (emptyBlocksIndex !== 0) { | ||
selectionState.emptyBlocksIndex = emptyBlocksIndex; | ||
} | ||
} | ||
} | ||
return selectionState; | ||
}, | ||
// http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html | ||
// Tim Down | ||
// | ||
// {object} selectionState - the selection to import | ||
// {DOMElement} root - the root element the selection is being restored inside of | ||
// {Document} doc - the document to use for managing selection | ||
// {boolean} [favorLaterSelectionAnchor] - defaults to false. If true, import the cursor immediately | ||
// subsequent to an anchor tag if it would otherwise be placed right at the trailing edge inside the | ||
// anchor. This cursor positioning, even though visually equivalent to the user, can affect behavior | ||
// in MS IE. | ||
importSelection: function (selectionState, root, doc, favorLaterSelectionAnchor) { | ||
if (!selectionState || !root) { | ||
return; | ||
} | ||
var range = doc.createRange(); | ||
range.setStart(root, 0); | ||
range.collapse(true); | ||
var node = root, | ||
nodeStack = [], | ||
charIndex = 0, | ||
foundStart = false, | ||
stop = false, | ||
nextCharIndex; | ||
while (!stop && node) { | ||
if (node.nodeType === 3) { | ||
nextCharIndex = charIndex + node.length; | ||
if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) { | ||
range.setStart(node, selectionState.start - charIndex); | ||
foundStart = true; | ||
} | ||
if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) { | ||
range.setEnd(node, selectionState.end - charIndex); | ||
stop = true; | ||
} | ||
charIndex = nextCharIndex; | ||
} else { | ||
var i = node.childNodes.length - 1; | ||
while (i >= 0) { | ||
nodeStack.push(node.childNodes[i]); | ||
i -= 1; | ||
} | ||
} | ||
if (!stop) { | ||
node = nodeStack.pop(); | ||
} | ||
} | ||
if (selectionState.emptyBlocksIndex && selectionState.end === nextCharIndex) { | ||
var targetNode = Util.getTopBlockContainer(range.startContainer), | ||
index = 0; | ||
// Skip over empty blocks until we hit the block we want the selection to be in | ||
while (index < selectionState.emptyBlocksIndex && targetNode.nextSibling) { | ||
targetNode = targetNode.nextSibling; | ||
index++; | ||
// If we find a non-empty block, ignore the emptyBlocksIndex and just put selection here | ||
if (targetNode.textContent.length > 0) { | ||
break; | ||
} | ||
} | ||
// We're selecting a high-level block node, so make sure the cursor gets moved into the deepest | ||
// element at the beginning of the block | ||
range.setStart(Util.getFirstSelectableLeafNode(targetNode), 0); | ||
range.collapse(true); | ||
} | ||
// If the selection is right at the ending edge of a link, put it outside the anchor tag instead of inside. | ||
if (favorLaterSelectionAnchor) { | ||
range = Selection.importSelectionMoveCursorPastAnchor(selectionState, range); | ||
} | ||
var sel = doc.getSelection(); | ||
sel.removeAllRanges(); | ||
sel.addRange(range); | ||
}, | ||
// Utility method called from importSelection only | ||
@@ -39,0 +153,0 @@ importSelectionMoveCursorPastAnchor: function (selectionState, range) { |
@@ -109,6 +109,9 @@ /*global NodeFilter, Selection*/ | ||
*/ | ||
createLink: function (document, textNodes, href) { | ||
createLink: function (document, textNodes, href, target) { | ||
var anchor = document.createElement('a'); | ||
Util.moveTextRangeIntoElement(textNodes[0], textNodes[textNodes.length - 1], anchor); | ||
anchor.setAttribute('href', href); | ||
if (target) { | ||
anchor.setAttribute('target', target); | ||
} | ||
return anchor; | ||
@@ -728,2 +731,8 @@ }, | ||
getContainerEditorElement: function (element) { | ||
return Util.traverseUp(element, function (node) { | ||
return Util.isMediumEditorElement(node); | ||
}); | ||
}, | ||
isBlockContainer: function (element) { | ||
@@ -730,0 +739,0 @@ return element && element.nodeType !== 3 && this.blockContainerElementNames.indexOf(element.nodeName.toLowerCase()) !== -1; |
@@ -20,3 +20,3 @@ /*global MediumEditor */ | ||
// grunt-bump looks for this: | ||
'version': '5.2.0' | ||
'version': '5.3.0' | ||
}).version); |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
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
1392156
117
18007