medium-editor
Advanced tools
Comparing version 5.14.0 to 5.14.1
@@ -34,5 +34,8 @@ { | ||
"src", | ||
"README.md", | ||
"CHANGES.md" | ||
"CHANGES.md", | ||
"RELEASE-PROCESS.md", | ||
"CODE_OF_CONDUCT.md", | ||
"CONTRIBUTING.md", | ||
"UPGRADE-5.md" | ||
] | ||
} |
@@ -0,1 +1,10 @@ | ||
5.14.1 / 2016-02-05 | ||
================== | ||
* Fix issue with saving selection after newline and whitespace text nodes | ||
* Fix import/export selection to prefer start of nodes over end of nodes | ||
* Fix for getClosestBlockContainer utility function | ||
* Fix for getTopBlockContainer utility function | ||
* Deprecate getFirstTextNode utility function | ||
5.14.0 / 2016-01-31 | ||
@@ -2,0 +11,0 @@ ================== |
{ | ||
"name": "medium-editor", | ||
"version": "5.14.0", | ||
"version": "5.14.1", | ||
"author": "Davi Ferreira <hi@daviferreira.com>", | ||
@@ -5,0 +5,0 @@ "contributors": [ |
@@ -67,2 +67,6 @@ /*global atob, unescape, Uint8Array, Blob*/ | ||
function isSafari() { | ||
return navigator.userAgent.toLowerCase().indexOf('safari') !== -1; | ||
} | ||
function dataURItoBlob(dataURI) { | ||
@@ -69,0 +73,0 @@ // convert base64/URLEncoded data component to raw binary data held in a string |
@@ -1,2 +0,2 @@ | ||
/*global selectElementContents, placeCursorInsideElement */ | ||
/*global selectElementContents, placeCursorInsideElement, isSafari */ | ||
@@ -160,2 +160,3 @@ describe('MediumEditor.selection TestCase', function () { | ||
placeCursorInsideElement(link.childNodes[0], link.childNodes[0].nodeValue.length); | ||
expect(MediumEditor.util.isDescendant(link, window.getSelection().getRangeAt(0).startContainer, true)).toBe(true); | ||
@@ -166,2 +167,9 @@ var exportedSelection = MediumEditor.selection.exportSelection(this.el, document); | ||
node = range.startContainer; | ||
// For some reason, Safari mucks with the selection range and makes this case not hold | ||
// since we only really care about whether this works in IE, and it works as expected | ||
// in other browsers, just skip this assertion for Safari | ||
if (!isSafari()) { | ||
expect(MediumEditor.util.isDescendant(link, node, true)).toBe(false); | ||
} | ||
// Even though we set the range to use the P tag as the start container, Safari normalizes the range | ||
@@ -331,3 +339,3 @@ // down to the text node. Setting the range to use the P tag for the start is necessary to support | ||
var selectionData = MediumEditor.selection.exportSelection(this.el, document); | ||
expect(selectionData.emptyBlocksIndex).toBe(0); | ||
expect(selectionData.emptyBlocksIndex).toBeUndefined(); | ||
@@ -344,3 +352,3 @@ MediumEditor.selection.importSelection(selectionData, this.el, document); | ||
this.el.innerHTML = '<p>lorem ipsum <a href="#"><img src="../demo/img/medium-editor.jpg" /></a> dolor</p>'; | ||
MediumEditor.selection.importSelection({ start: 12, end: 12, trailingImageCount: 1 }, this.el, document); | ||
MediumEditor.selection.importSelection({ start: 12, end: 12, startsWithImage: true, trailingImageCount: 1 }, this.el, document); | ||
var range = window.getSelection().getRangeAt(0); | ||
@@ -353,3 +361,3 @@ expect(range.toString()).toBe(''); | ||
this.el.innerHTML = '<p>lorem ipsum <a href="#"><img src="../demo/img/medium-editor.jpg" />img</a> dolor</p>'; | ||
MediumEditor.selection.importSelection({ start: 12, end: 15 }, this.el, document); | ||
MediumEditor.selection.importSelection({ start: 12, end: 15, startsWithImage: true }, this.el, document); | ||
var range = window.getSelection().getRangeAt(0); | ||
@@ -373,2 +381,35 @@ expect(range.toString()).toBe('img'); | ||
}); | ||
// https://github.com/yabwe/medium-editor/issues/935 | ||
it('should support a selection that is after white-space at the beginning of a paragraph', function () { | ||
this.el.innerHTML = ' <p>one two<br><a href="transindex.hu">three</a><br></p><p><a href="amazon.com">one</a> two three</p>'; | ||
this.newMediumEditor(this.el); | ||
var firstText = this.el.querySelector('p').firstChild; | ||
MediumEditor.selection.select(document, firstText, 0, firstText, 'one'.length); | ||
var exported = MediumEditor.selection.exportSelection(this.el, document); | ||
MediumEditor.selection.importSelection(exported, this.el, document); | ||
var range = window.getSelection().getRangeAt(0); | ||
expect(range.toString()).toBe('one'); | ||
}); | ||
it('should support importing a collapsed selection at the end of all content', function () { | ||
this.el.innerHTML = '<p>lorem ipsum <b>dolor</b></p>'; | ||
var boldText = this.el.querySelector('b').firstChild; | ||
placeCursorInsideElement(boldText, boldText.length); | ||
var range = window.getSelection().getRangeAt(0); | ||
expect(range.collapsed).toBe(true); | ||
expect(MediumEditor.util.isDescendant(boldText.parentNode, range.startContainer, true)).toBe(true); | ||
expect(MediumEditor.util.isDescendant(boldText.parentNode, range.endContainer, true)).toBe(true); | ||
var exported = MediumEditor.selection.exportSelection(this.el, document); | ||
expect(exported.start).toBe('lorem ipsum dolor'.length); | ||
expect(exported.end).toBe('lorem ipsum dolor'.length); | ||
MediumEditor.selection.importSelection(exported, this.el, document); | ||
range = window.getSelection().getRangeAt(0); | ||
expect(range.collapsed).toBe(true); | ||
expect(MediumEditor.util.isDescendant(boldText.parentNode, range.startContainer, true)).toBe(true); | ||
expect(MediumEditor.util.isDescendant(boldText.parentNode, range.endContainer, true)).toBe(true); | ||
}); | ||
}); | ||
@@ -375,0 +416,0 @@ |
@@ -509,2 +509,62 @@ /*global selectElementContents*/ | ||
describe('getClosestBlockContainer', function () { | ||
it('should return the closest block container', function () { | ||
var el = this.createElement('div', '', '<blockquote><p>paragraph</p><ul><li><span>list item</span></li></ul></blockquote>'), | ||
span = el.querySelector('span'), | ||
container = MediumEditor.util.getClosestBlockContainer(span); | ||
expect(container).toBe(el.querySelector('li')); | ||
}); | ||
it('should return the parent editable if element is a text node child of the editor', function () { | ||
var el = this.createElement('div', 'editable', ' <p>text</p>'), | ||
emptyTextNode = el.firstChild; | ||
this.newMediumEditor('.editable'); | ||
var container = MediumEditor.util.getClosestBlockContainer(emptyTextNode); | ||
expect(container).toBe(el); | ||
}); | ||
}); | ||
describe('getTopBlockContainer', function () { | ||
it('should return the highest level block container', function () { | ||
var el = this.createElement('div', '', '<blockquote><p>paragraph</p><ul><li><span>list item</span></li></ul></blockquote>'), | ||
span = el.querySelector('span'), | ||
container = MediumEditor.util.getTopBlockContainer(span); | ||
expect(container).toBe(el.querySelector('blockquote')); | ||
}); | ||
it('should return the parent editable if element is a text node child of the editor', function () { | ||
var el = this.createElement('div', 'editable', ' <p>text</p>'), | ||
emptyTextNode = el.firstChild; | ||
this.newMediumEditor('.editable'); | ||
var container = MediumEditor.util.getTopBlockContainer(emptyTextNode); | ||
expect(container).toBe(el); | ||
}); | ||
}); | ||
describe('findPreviousSibling', function () { | ||
it('should return the previous sibling of an element if it exists', function () { | ||
var el = this.createElement('div', '', '<p>first <b>second </b><i>third</i></p><ul><li>fourth</li></ul>'), | ||
second = el.querySelector('b'), | ||
third = el.querySelector('i'), | ||
prevSibling = MediumEditor.util.findPreviousSibling(third); | ||
expect(prevSibling).toBe(second); | ||
}); | ||
it('should return the previous sibling on an ancestor if a previous sibling does not exist', function () { | ||
var el = this.createElement('div', '', '<p>first <b>second </b><i>third</i></p><ul><li>fourth</li></ul>'), | ||
fourth = el.querySelector('li').firstChild, | ||
p = el.querySelector('p'), | ||
prevSibling = MediumEditor.util.findPreviousSibling(fourth); | ||
expect(prevSibling).toBe(p); | ||
}); | ||
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', '<p>first <b>second </b><i>third</i></p><ul><li>fourth</li></ul>'), | ||
first = el.querySelector('p').firstChild; | ||
this.newMediumEditor('.editable'); | ||
var prevSibling = MediumEditor.util.findPreviousSibling(first); | ||
expect(prevSibling).toBeFalsy(); | ||
}); | ||
}); | ||
describe('findOrCreateMatchingTextNodes', function () { | ||
@@ -581,2 +641,28 @@ it('should return text nodes within an element', function () { | ||
}); | ||
// TODO: Remove these tests when getFirstTextNode is deprecated in 6.0.0 | ||
describe('getFirstTextNode', function () { | ||
it('should find the first text node within an element', function () { | ||
var el = this.createElement('div', '', '<p><b><i><u><a href="#">First</a> text</u> in</i> editor</b>!</p>'), | ||
anchorText = el.querySelector('a').firstChild, | ||
firstText = MediumEditor.util.getFirstTextNode(el); | ||
expect(firstText).toBe(anchorText); | ||
}); | ||
it('should return the text node if passed a text node', function () { | ||
var el = this.createElement('div', '', '<p>text</p>'), | ||
textNode = el.querySelector('p').firstChild, | ||
firstText = MediumEditor.util.getFirstTextNode(textNode); | ||
expect(firstText).toBe(textNode); | ||
}); | ||
it('should return null if no text node exists in element', function () { | ||
var el = this.createElement('div'), | ||
firstText = MediumEditor.util.getFirstTextNode(el); | ||
expect(firstText).toBeNull(); | ||
}); | ||
}); | ||
}); |
@@ -58,10 +58,16 @@ (function () { | ||
// Range contains an image, check to see if the selection ends with that image | ||
if (range.endOffset !== 0 && (range.endContainer.nodeName.toLowerCase() === 'img' || (range.endContainer.nodeType === 1 && range.endContainer.querySelector('img')))) { | ||
var trailingImageCount = this.getTrailingImageCount(root, selectionState, range.endContainer, range.endOffset); | ||
if (trailingImageCount) { | ||
selectionState.trailingImageCount = trailingImageCount; | ||
} | ||
// Check to see if the selection starts with any images | ||
// if so we need to make sure the the beginning of the selection is | ||
// set correctly when importing selection | ||
if (this.doesRangeStartWithImages(range, doc)) { | ||
selectionState.startsWithImage = true; | ||
} | ||
// Check to see if the selection has any trailing images | ||
// if so, this this means we need to look for them when we import selection | ||
var trailingImageCount = this.getTrailingImageCount(root, selectionState, range.endContainer, range.endOffset); | ||
if (trailingImageCount) { | ||
selectionState.trailingImageCount = trailingImageCount; | ||
} | ||
// If start = 0 there may still be an empty paragraph before it, but we don't care. | ||
@@ -105,4 +111,23 @@ if (start !== 0) { | ||
stop = false, | ||
nextCharIndex; | ||
nextCharIndex, | ||
allowRangeToStartAtEndOfNode = false, | ||
lastTextNode = null; | ||
// When importing selection, the start of the selection may lie at the end of an element | ||
// or at the beginning of an element. Since visually there is no difference between these 2 | ||
// we will try to move the selection to the beginning of an element since this is generally | ||
// what users will expect and it's a more predictable behavior. | ||
// | ||
// However, there are some specific cases when we don't want to do this: | ||
// 1) We're attempting to move the cursor outside of the end of an anchor [favorLaterSelectionAnchor = true] | ||
// 2) The selection starts with an image, which is special since an image doesn't have any 'content' | ||
// as far as selection and ranges are concerned | ||
// 3) The selection starts after a specified number of empty block elements (selectionState.emptyBlocksIndex) | ||
// | ||
// For these cases, we want the selection to start at a very specific location, so we should NOT | ||
// automatically move the cursor to the beginning of the first actual chunk of text | ||
if (favorLaterSelectionAnchor || selectionState.startsWithImage || typeof selectionState.emptyBlocksIndex !== 'undefined') { | ||
allowRangeToStartAtEndOfNode = true; | ||
} | ||
while (!stop && node) { | ||
@@ -118,6 +143,19 @@ // Only iterate over elements and text nodes | ||
nextCharIndex = charIndex + node.length; | ||
// Check if we're at or beyond the start of the selection we're importing | ||
if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) { | ||
range.setStart(node, selectionState.start - charIndex); | ||
foundStart = true; | ||
// NOTE: We only want to allow a selection to start at the END of an element if | ||
// allowRangeToStartAtEndOfNode is true | ||
if (allowRangeToStartAtEndOfNode || selectionState.start < nextCharIndex) { | ||
range.setStart(node, selectionState.start - charIndex); | ||
foundStart = true; | ||
} | ||
// We're at the end of a text node where the selection could start but we shouldn't | ||
// make the selection start here because allowRangeToStartAtEndOfNode is false. | ||
// However, we should keep a reference to this node in case there aren't any more | ||
// text nodes after this, so that we have somewhere to import the selection to | ||
else { | ||
lastTextNode = node; | ||
} | ||
} | ||
// We've found the start of the selection, check if we're at or beyond the end of the selection we're importing | ||
if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) { | ||
@@ -164,2 +202,10 @@ if (!selectionState.trailingImageCount) { | ||
// If we've gone through the entire text but didn't find the beginning of a text node | ||
// to make the selection start at, we should fall back to starting the selection | ||
// at the END of the last text node we found | ||
if (!foundStart && lastTextNode) { | ||
range.setStart(lastTextNode, lastTextNode.length); | ||
range.setEnd(lastTextNode, lastTextNode.length); | ||
} | ||
if (typeof selectionState.emptyBlocksIndex !== 'undefined') { | ||
@@ -253,2 +299,6 @@ range = this.importSelectionMoveCursorPastBlocks(doc, root, selectionState.emptyBlocksIndex, range); | ||
if (!targetNode) { | ||
targetNode = startBlock; | ||
} | ||
// We're selecting a high-level block node, so make sure the cursor gets moved into the deepest | ||
@@ -277,4 +327,17 @@ // element at the beginning of the block | ||
} | ||
if (node && !MediumEditor.util.isElementAtBeginningOfBlock(node)) { | ||
return -1; | ||
if (node) { | ||
// The element isn't at the beginning of a block, so it has content before it | ||
if (!MediumEditor.util.isElementAtBeginningOfBlock(node)) { | ||
return -1; | ||
} | ||
var previousSibling = MediumEditor.util.findPreviousSibling(node); | ||
// If there is no previous sibling, this is the first text element in the editor | ||
if (!previousSibling) { | ||
return -1; | ||
} | ||
// If the previous sibling has text, then there are no empty blocks before this | ||
else if (previousSibling.nodeValue) { | ||
return -1; | ||
} | ||
} | ||
@@ -303,3 +366,49 @@ | ||
// Returns true if the selection range begins with an image tag | ||
// Returns false if the range starts with any non empty text nodes | ||
doesRangeStartWithImages: function (range, doc) { | ||
if (range.startOffset !== 0 || range.startContainer.nodeType !== 1) { | ||
return false; | ||
} | ||
if (range.startContainer.nodeName.toLowerCase() === 'img') { | ||
return true; | ||
} | ||
var img = range.startContainer.querySelector('img'); | ||
if (!img) { | ||
return false; | ||
} | ||
var treeWalker = doc.createTreeWalker(range.startContainer, NodeFilter.SHOW_ALL, null, false); | ||
while (treeWalker.nextNode()) { | ||
var next = treeWalker.currentNode; | ||
// If we hit the image, then there isn't any text before the image so | ||
// the image is at the beginning of the range | ||
if (next === img) { | ||
break; | ||
} | ||
// If we haven't hit the iamge, but found text that contains content | ||
// then the range doesn't start with an image | ||
if (next.nodeValue) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
}, | ||
getTrailingImageCount: function (root, selectionState, endContainer, endOffset) { | ||
// If the endOffset of a range is 0, the endContainer doesn't contain images | ||
// If the endContainer is a text node, there are no trailing images | ||
if (endOffset === 0 || endContainer.nodeType !== 1) { | ||
return 0; | ||
} | ||
// If the endContainer isn't an image, and doesn't have an image descendants | ||
// there are no trailing images | ||
if (endContainer.nodeName.toLowerCase() !== 'img' && !endContainer.querySelector('img')) { | ||
return 0; | ||
} | ||
var lastNode = endContainer.childNodes[endOffset - 1]; | ||
@@ -363,3 +472,3 @@ while (lastNode.hasChildNodes()) { | ||
// determine if the current selection contains any 'content' | ||
// content being and non-white space text or an image | ||
// content being any non-white space text or an image | ||
selectionContainsContent: function (doc) { | ||
@@ -366,0 +475,0 @@ var sel = doc.getSelection(); |
@@ -319,2 +319,18 @@ /*global NodeFilter*/ | ||
// Find an element's previous sibling within a medium-editor element | ||
// If one doesn't exist, find the closest ancestor's previous sibling | ||
findPreviousSibling: function (node) { | ||
if (!node || Util.isMediumEditorElement(node)) { | ||
return false; | ||
} | ||
var previousSibling = node.previousSibling; | ||
while (!previousSibling && !Util.isMediumEditorElement(node.parentNode)) { | ||
node = node.parentNode; | ||
previousSibling = node.previousSibling; | ||
} | ||
return previousSibling; | ||
}, | ||
isDescendant: function isDescendant(parent, child, checkEquality) { | ||
@@ -868,10 +884,18 @@ if (!parent || !child) { | ||
/* Finds the closest ancestor which is a block container element | ||
* If element is within editor element but not within any other block element, | ||
* the editor element is returned | ||
*/ | ||
getClosestBlockContainer: function (node) { | ||
return Util.traverseUp(node, function (node) { | ||
return Util.isBlockContainer(node); | ||
return Util.isBlockContainer(node) || Util.isMediumEditorElement(node); | ||
}); | ||
}, | ||
/* Finds highest level ancestor element which is a block container element | ||
* If element is within editor element but not within any other block element, | ||
* the editor element is returned | ||
*/ | ||
getTopBlockContainer: function (element) { | ||
var topBlock = element; | ||
var topBlock = Util.isBlockContainer(element) ? element : false; | ||
Util.traverseUp(element, function (el) { | ||
@@ -881,2 +905,6 @@ if (Util.isBlockContainer(el)) { | ||
} | ||
if (!topBlock && Util.isMediumEditorElement(el)) { | ||
topBlock = el; | ||
return true; | ||
} | ||
return false; | ||
@@ -906,3 +934,9 @@ }); | ||
// TODO: remove getFirstTextNode AND _getFirstTextNode when jumping in 6.0.0 (no code references) | ||
getFirstTextNode: function (element) { | ||
Util.warn('getFirstTextNode is deprecated and will be removed in version 6.0.0'); | ||
return Util._getFirstTextNode(element); | ||
}, | ||
_getFirstTextNode: function (element) { | ||
if (element.nodeType === 3) { | ||
@@ -913,3 +947,3 @@ return element; | ||
for (var i = 0; i < element.childNodes.length; i++) { | ||
var textNode = Util.getFirstTextNode(element.childNodes[i]); | ||
var textNode = Util._getFirstTextNode(element.childNodes[i]); | ||
if (textNode !== null) { | ||
@@ -916,0 +950,0 @@ return textNode; |
@@ -18,3 +18,3 @@ MediumEditor.parseVersionString = function (release) { | ||
// grunt-bump looks for this: | ||
'version': '5.14.0' | ||
'version': '5.14.1' | ||
}).version); |
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
1680334
135
20948