medium-editor
Advanced tools
Comparing version 4.12.2 to 4.12.3
{ | ||
"name": "medium-editor", | ||
"version": "4.12.2", | ||
"homepage": "http://yabwe.github.io/medium-editor/", | ||
@@ -5,0 +4,0 @@ "authors": [ |
@@ -0,1 +1,6 @@ | ||
4.12.3 / 2015-06-12 | ||
================== | ||
* Fix bug with un-linked auto-links causing unexpected cursor positioning | ||
4.12.2 / 2015-06-10 | ||
@@ -2,0 +7,0 @@ ================== |
@@ -298,3 +298,3 @@ /*global module, require, process*/ | ||
options: { | ||
files: ['bower.json', 'package.json', 'src/js/version.js'], | ||
files: ['package.json', 'src/js/version.js'], | ||
updateConfigs: [], | ||
@@ -301,0 +301,0 @@ commit: false, |
{ | ||
"name": "medium-editor", | ||
"version": "4.12.2", | ||
"version": "4.12.3", | ||
"author": "Davi Ferreira <hi@daviferreira.com>", | ||
@@ -5,0 +5,0 @@ "contributors": [ |
@@ -186,3 +186,3 @@ /*global describe, it, expect, beforeEach, afterEach, | ||
links = this.el.getElementsByTagName('a'); | ||
expect(links.length).toBe(1); | ||
expect(links.length).toBe(1, 'links length after ENTER'); | ||
expect(links[0].getAttribute('href')).toBe('http://www.example.enter'); | ||
@@ -197,3 +197,3 @@ expect(links[0].firstChild.getAttribute('data-auto-link')).toBe('true'); | ||
links = this.el.getElementsByTagName('a'); | ||
expect(links.length).toBe(1); | ||
expect(links.length).toBe(1, 'links length after SPACE'); | ||
expect(links[0].getAttribute('href')).toBe('http://www.example.space'); | ||
@@ -200,0 +200,0 @@ expect(links[0].firstChild.getAttribute('data-auto-link')).toBe('true'); |
/*global MediumEditor, describe, it, expect, spyOn, AnchorForm, | ||
beforeAll, afterAll, | ||
afterEach, beforeEach, jasmine, fireEvent, setupTestHelpers, | ||
@@ -8,2 +9,14 @@ selectElementContentsAndFire, isOldIE, isIE */ | ||
var textarea; | ||
beforeAll(function () { | ||
textarea = document.createElement('textarea'); | ||
textarea.innerHTML = 'Ignore me please, placed here to make create an image test pass in Gecko'; | ||
document.body.appendChild(textarea); | ||
textarea.focus(); | ||
}); | ||
afterAll(function () { | ||
document.body.removeChild(textarea); | ||
}); | ||
beforeEach(function () { | ||
@@ -10,0 +23,0 @@ setupTestHelpers.call(this); |
/*global MediumEditor, describe, it, expect, spyOn, | ||
afterEach, beforeEach, fireEvent, | ||
afterEach, beforeEach, fireEvent, Util, | ||
jasmine, selectElementContents, setupTestHelpers, | ||
@@ -77,2 +77,172 @@ selectElementContentsAndFire, Selection, placeCursorInsideElement */ | ||
}); | ||
it('should not export a position indicating the cursor is before an empty paragraph', function () { | ||
this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p><p>Whatever</p>'; | ||
var editor = this.newMediumEditor('.editor', { | ||
buttons: ['italic', 'underline', 'strikethrough'] | ||
}); | ||
placeCursorInsideElement(editor.elements[0].querySelector('span'), 1); // end of first span | ||
var exportedSelection = editor.exportSelection(); | ||
expect(exportedSelection.emptyBlocksIndex).toEqual(undefined); | ||
}); | ||
it('should not export a position indicating the cursor is after an empty paragraph', function () { | ||
this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p>' + | ||
'<p class="target">Whatever</p>'; | ||
var editor = this.newMediumEditor('.editor', { | ||
buttons: ['italic', 'underline', 'strikethrough'] | ||
}); | ||
// After the 'W' in whatever | ||
placeCursorInsideElement(editor.elements[0].querySelector('p.target').firstChild, 1); | ||
var exportedSelection = editor.exportSelection(); | ||
expect(exportedSelection.emptyBlocksIndex).toEqual(undefined); | ||
}); | ||
it('should not export a position indicating the cursor is after an empty paragraph (in a complicated markup case)', | ||
function () { | ||
this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p>' + | ||
'<p>What<span class="target">ever</span></p>'; | ||
var editor = this.newMediumEditor('.editor', { | ||
buttons: ['italic', 'underline', 'strikethrough'] | ||
}); | ||
// Before the 'e' in whatever | ||
placeCursorInsideElement(editor.elements[0].querySelector('span.target').firstChild, 0); | ||
var exportedSelection = editor.exportSelection(); | ||
expect(exportedSelection.emptyBlocksIndex).toEqual(undefined); | ||
}); | ||
it('should not export a position indicating the cursor is after an empty paragraph ' + | ||
'(in a complicated markup with selection on the element)', function () { | ||
this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p>' + | ||
'<p>What<span class="target">ever</span></p>'; | ||
var editor = this.newMediumEditor('.editor', { | ||
buttons: ['italic', 'underline', 'strikethrough'] | ||
}); | ||
// Before the 'e' in whatever | ||
placeCursorInsideElement(editor.elements[0].querySelector('span.target'), 0); | ||
var exportedSelection = editor.exportSelection(); | ||
expect(exportedSelection.emptyBlocksIndex).toEqual(undefined); | ||
}); | ||
it('should export a position indicating the cursor is in an empty paragraph', function () { | ||
this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p><p>Whatever</p>'; | ||
var editor = this.newMediumEditor('.editor', { | ||
buttons: ['italic', 'underline', 'strikethrough'] | ||
}); | ||
placeCursorInsideElement(editor.elements[0].getElementsByTagName('p')[1], 0); | ||
var exportedSelection = editor.exportSelection(); | ||
expect(exportedSelection.emptyBlocksIndex).toEqual(1); | ||
}); | ||
it('should export a position indicating the cursor is after an empty paragraph', function () { | ||
this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p><p>Whatever</p>'; | ||
var editor = this.newMediumEditor('.editor', { | ||
buttons: ['italic', 'underline', 'strikethrough'] | ||
}); | ||
placeCursorInsideElement(editor.elements[0].getElementsByTagName('p')[2], 0); | ||
var exportedSelection = editor.exportSelection(); | ||
expect(exportedSelection.emptyBlocksIndex).toEqual(2); | ||
}); | ||
it('should export a position indicating the cursor is after an empty block element', function () { | ||
this.el.innerHTML = '<p><span>www.google.com</span></p><h1><br /></h1><h2><br /></h2><p>Whatever</p>'; | ||
var editor = this.newMediumEditor('.editor', { | ||
buttons: ['italic', 'underline', 'strikethrough'] | ||
}); | ||
placeCursorInsideElement(editor.elements[0].querySelector('h2'), 0); | ||
var exportedSelection = editor.exportSelection(); | ||
expect(exportedSelection.emptyBlocksIndex).toEqual(2); | ||
}); | ||
it('should import a position with the cursor in an empty paragraph', function () { | ||
this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p><p>Whatever</p>'; | ||
var editor = this.newMediumEditor('.editor', { | ||
buttons: ['italic', 'underline', 'strikethrough'] | ||
}); | ||
editor.importSelection({ | ||
'start': 14, | ||
'end': 14, | ||
'emptyBlocksIndex': 1 | ||
}); | ||
var startParagraph = Util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'p'); | ||
expect(startParagraph).toBe(editor.elements[0].getElementsByTagName('p')[1], 'empty paragraph'); | ||
}); | ||
it('should import a position with the cursor after an empty paragraph', function () { | ||
this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p><p>Whatever</p>'; | ||
var editor = this.newMediumEditor('.editor', { | ||
buttons: ['italic', 'underline', 'strikethrough'] | ||
}); | ||
editor.importSelection({ | ||
'start': 14, | ||
'end': 14, | ||
'emptyBlocksIndex': 2 | ||
}); | ||
var startParagraph = Util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'p'); | ||
expect(startParagraph).toBe(editor.elements[0].getElementsByTagName('p')[2], 'paragraph after empty paragraph'); | ||
}); | ||
it('should import a position with the cursor after an empty paragraph when there are multipled editable elements', function () { | ||
this.createElement('div', 'editor', '<p><span>www.google.com</span></p><p><br /></p><p>Whatever</p>'); | ||
var editor = this.newMediumEditor('.editor', { | ||
buttons: ['italic', 'underline', 'strikethrough'] | ||
}); | ||
editor.importSelection({ | ||
'start': 14, | ||
'end': 14, | ||
'editableElementIndex': 1, | ||
'emptyBlocksIndex': 2 | ||
}); | ||
var startParagraph = Util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'p'); | ||
expect(startParagraph).toBe(editor.elements[1].getElementsByTagName('p')[2], 'paragraph after empty paragraph'); | ||
}); | ||
it('should import a position with the cursor after an empty block element', function () { | ||
this.el.innerHTML = '<p><span>www.google.com</span></p><h1><br /></h1><h2><br /></h2><p>Whatever</p>'; | ||
var editor = this.newMediumEditor('.editor', { | ||
buttons: ['italic', 'underline', 'strikethrough'] | ||
}); | ||
editor.importSelection({ | ||
'start': 14, | ||
'end': 14, | ||
'emptyBlocksIndex': 2 | ||
}); | ||
var startParagraph = Util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'h2'); | ||
expect(startParagraph).toBe(editor.elements[0].querySelector('h2'), 'block element after empty block element'); | ||
}); | ||
it('should import a position with the cursor after an empty block element inside an element with various children', function () { | ||
this.el.innerHTML = '<p><span>www.google.com</span></p><h1><br /></h1><h2><br /></h2><p><b><i>Whatever</i></b></p>'; | ||
var editor = this.newMediumEditor('.editor', { | ||
buttons: ['italic', 'underline', 'strikethrough'] | ||
}); | ||
editor.importSelection({ | ||
'start': 14, | ||
'end': 14, | ||
'emptyBlocksIndex': 3 | ||
}); | ||
var innerElement = window.getSelection().getRangeAt(0).startContainer; | ||
expect(Util.isDescendant(editor.elements[0].querySelector('i'), innerElement, true)).toBe(true, 'nested inline elment inside block element after empty block element'); | ||
}); | ||
it('should import not import a selection beyond any block elements that have text, even when emptyBlocksIndex indicates it should ', function () { | ||
this.el.innerHTML = '<p><span>www.google.com</span></p><h1><br /></h1><h2>Not Empty</h2><p><b><i>Whatever</i></b></p>'; | ||
var editor = this.newMediumEditor('.editor', { | ||
buttons: ['italic', 'underline', 'strikethrough'] | ||
}); | ||
// Import a selection that indicates the text should be at the end of the 'www.google.com' word, but in the 3rd paragraph (at the beginning of 'Whatever') | ||
editor.importSelection({ | ||
'start': 14, | ||
'end': 14, | ||
'emptyBlocksIndex': 3 | ||
}); | ||
var innerElement = window.getSelection().getRangeAt(0).startContainer; | ||
expect(Util.isDescendant(editor.elements[0].querySelectorAll('p')[1], innerElement, true)).toBe(false, 'moved selection beyond non-empty block element'); | ||
expect(Util.isDescendant(editor.elements[0].querySelector('h2'), innerElement, true)).toBe(true, 'moved selection to element to incorrect block element'); | ||
}); | ||
}); | ||
@@ -79,0 +249,0 @@ |
@@ -917,2 +917,13 @@ /*global Util, ButtonsData, Selection, Extension, | ||
}; | ||
// 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; | ||
} | ||
} | ||
} | ||
@@ -992,2 +1003,21 @@ } | ||
if (inSelectionState.emptyBlocksIndex && selectionState.end === nextCharIndex) { | ||
var targetNode = Util.getBlockContainer(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.getFirstLeafNode(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. | ||
@@ -997,3 +1027,2 @@ if (favorLaterSelectionAnchor) { | ||
} | ||
sel = this.options.contentWindow.getSelection(); | ||
@@ -1000,0 +1029,0 @@ sel.removeAllRanges(); |
@@ -58,5 +58,6 @@ var AnchorForm; | ||
var selectedParentElement = Selection.getSelectedParentElement(Selection.getSelectionRange(this.document)); | ||
if (selectedParentElement.tagName && | ||
selectedParentElement.tagName.toLowerCase() === 'a') { | ||
var selectedParentElement = Selection.getSelectedParentElement(Selection.getSelectionRange(this.document)), | ||
firstTextNode = Util.getFirstTextNode(selectedParentElement); | ||
if (Util.getClosestTag(firstTextNode, 'a')) { | ||
return this.execAction('unlink'); | ||
@@ -63,0 +64,0 @@ } |
/*global Extension, Util */ | ||
var AutoLink, | ||
WHITESPACE_CHARS, | ||
KNOWN_TLDS_FRAGMENT, | ||
LINK_REGEXP_TEXT; | ||
WHITESPACE_CHARS = [' ', '\t', '\n', '\r', '\u00A0', '\u2000', '\u2001', '\u2002', '\u2003', | ||
'\u2028', '\u2029']; | ||
KNOWN_TLDS_FRAGMENT = 'com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|' + | ||
@@ -128,8 +131,12 @@ 'xxx|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|' + | ||
documentModified = true; | ||
// Some editing has happened to the span, so just remove it entirely. The user can put it back | ||
// around just the href content if they need to prevent it from linking | ||
while (spans[i].childNodes.length > 0) { | ||
spans[i].parentNode.insertBefore(spans[i].firstChild, spans[i]); | ||
var trimmedTextContent = textContent.replace(/\s+$/, ''); | ||
if (spans[i].getAttribute('data-href') === trimmedTextContent) { | ||
var charactersTrimmed = textContent.length - trimmedTextContent.length, | ||
subtree = Util.splitOffDOMTree(spans[i], this.splitTextBeforeEnd(spans[i], charactersTrimmed)); | ||
spans[i].parentNode.insertBefore(subtree, spans[i].nextSibling); | ||
} else { | ||
// Some editing has happened to the span, so just remove it entirely. The user can put it back | ||
// around just the href content if they need to prevent it from linking | ||
Util.unwrap(spans[i], this.document); | ||
} | ||
spans[i].parentNode.removeChild(spans[i]); | ||
} | ||
@@ -140,2 +147,28 @@ } | ||
splitTextBeforeEnd: function (element, characterCount) { | ||
var treeWalker = this.document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false), | ||
lastChildNotExhausted = true; | ||
// Start the tree walker at the last descendant of the span | ||
while (lastChildNotExhausted) { | ||
lastChildNotExhausted = treeWalker.lastChild() !== null; | ||
} | ||
var currentNode, | ||
currentNodeValue, | ||
previousNode; | ||
while (characterCount > 0 && previousNode !== null) { | ||
currentNode = treeWalker.currentNode; | ||
currentNodeValue = currentNode.nodeValue; | ||
if (currentNodeValue.length > characterCount) { | ||
previousNode = currentNode.splitText(currentNodeValue.length - characterCount); | ||
characterCount = 0; | ||
} else { | ||
previousNode = treeWalker.previousNode(); | ||
characterCount -= currentNodeValue.length; | ||
} | ||
} | ||
return previousNode; | ||
}, | ||
performLinkingWithinElement: function (element) { | ||
@@ -154,4 +187,2 @@ var matches = this.findLinkableText(element), | ||
var linkRegExp = new RegExp(LINK_REGEXP_TEXT, 'gi'), | ||
whitespaceChars = [' ', '\t', '\n', '\r', '\u00A0', '\u2000', '\u2001', '\u2002', '\u2003', | ||
'\u2028', '\u2029'], | ||
textContent = contenteditable.textContent, | ||
@@ -165,4 +196,4 @@ match = null, | ||
// If the regexp detected something as a link that has text immediately preceding/following it, bail out. | ||
matchOk = (match.index === 0 || whitespaceChars.indexOf(textContent[match.index - 1]) !== -1) && | ||
(matchEnd === textContent.length || whitespaceChars.indexOf(textContent[matchEnd]) !== -1); | ||
matchOk = (match.index === 0 || WHITESPACE_CHARS.indexOf(textContent[match.index - 1]) !== -1) && | ||
(matchEnd === textContent.length || WHITESPACE_CHARS.indexOf(textContent[matchEnd]) !== -1); | ||
// If the regexp detected a bare domain that doesn't use one of our expected TLDs, bail out. | ||
@@ -169,0 +200,0 @@ matchOk = matchOk && (match[0].indexOf('/') !== -1 || |
@@ -7,2 +7,10 @@ /*global Util */ | ||
function filterOnlyParentElements(node) { | ||
if (Util.parentElements.indexOf(node.nodeName.toLowerCase()) !== -1) { | ||
return NodeFilter.FILTER_ACCEPT; | ||
} else { | ||
return NodeFilter.FILTER_SKIP; | ||
} | ||
} | ||
Selection = { | ||
@@ -63,2 +71,41 @@ findMatchingSelectionParent: function (testElementFunction, contentWindow) { | ||
// Returns 0 unless the cursor is within or preceded by empty paragraphs/blocks, | ||
// in which case it returns the count of such preceding paragraphs, including | ||
// the empty paragraph in which the cursor itself may be embedded. | ||
getIndexRelativeToAdjacentEmptyBlocks: function (doc, root, cursorContainer, cursorOffset) { | ||
// If there is text in front of the cursor, that means there isn't only empty blocks before it | ||
if (cursorContainer.nodeType === 3 && cursorOffset > 0) { | ||
return 0; | ||
} | ||
// Check if the block that contains the cursor has any other text in front of the cursor | ||
var node = cursorContainer; | ||
if (node.nodeType !== 3) { | ||
//node = cursorContainer.childNodes.length === cursorOffset ? null : cursorContainer.childNodes[cursorOffset]; | ||
node = cursorContainer.childNodes[cursorOffset]; | ||
} | ||
if (node && !Util.isElementAtBeginningOfBlock(node)) { | ||
return 0; | ||
} | ||
// Walk over block elements, counting number of empty blocks between last piece of text | ||
// and the block the cursor is in | ||
var treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false), | ||
emptyBlocksCount = 0; | ||
while (treeWalker.nextNode()) { | ||
var blockIsEmpty = treeWalker.currentNode.textContent === ''; | ||
if (blockIsEmpty || emptyBlocksCount > 0) { | ||
emptyBlocksCount += 1; | ||
} | ||
if (Util.isDescendant(treeWalker.currentNode, cursorContainer, true)) { | ||
return emptyBlocksCount; | ||
} | ||
if (!blockIsEmpty) { | ||
emptyBlocksCount = 0; | ||
} | ||
} | ||
return emptyBlocksCount; | ||
}, | ||
selectionInContentEditableFalse: function (contentWindow) { | ||
@@ -65,0 +112,0 @@ // determine if the current selection is exclusively inside |
@@ -1,2 +0,2 @@ | ||
/*global NodeFilter, console, Selection*/ | ||
/*global NodeFilter, Selection*/ | ||
@@ -652,2 +652,46 @@ var Util; | ||
isElementAtBeginningOfBlock: function (node) { | ||
var textVal, | ||
sibling; | ||
while (node.nodeType === 3 || | ||
(this.parentElements.indexOf(node.tagName.toLowerCase()) === -1 && !node.getAttribute('data-medium-element'))) { // TODO: Change this in v5.0.0 | ||
sibling = node; | ||
while (sibling = sibling.previousSibling) { | ||
textVal = sibling.nodeType === 3 ? sibling.nodeValue : sibling.textContent; | ||
if (textVal.length > 0) { | ||
return false; | ||
} | ||
} | ||
node = node.parentNode; | ||
} | ||
return true; | ||
}, | ||
getBlockContainer: function (element) { | ||
return this.traverseUp(element, function (el) { | ||
return Util.parentElements.indexOf(el.tagName.toLowerCase()) !== -1; | ||
}); | ||
}, | ||
getFirstLeafNode: function (element) { | ||
while (element && element.firstChild) { | ||
element = element.firstChild; | ||
} | ||
return element; | ||
}, | ||
getFirstTextNode: function (element) { | ||
if (element.nodeType === 3) { | ||
return element; | ||
} | ||
for (var i = 0; i < element.childNodes.length; i++) { | ||
var textNode = this.getFirstTextNode(element.childNodes[i]); | ||
if (textNode !== null) { | ||
return textNode; | ||
} | ||
} | ||
return null; | ||
}, | ||
ensureUrlHasProtocol: function (url) { | ||
@@ -662,3 +706,3 @@ if (url.indexOf('://') === -1) { | ||
if (window.console !== undefined && typeof window.console.warn === 'function') { | ||
window.console.warn.apply(console, arguments); | ||
window.console.warn.apply(window.console, arguments); | ||
} | ||
@@ -665,0 +709,0 @@ }, |
@@ -14,3 +14,3 @@ /*global MediumEditor */ | ||
// grunt-bump looks for this: | ||
'version': '4.12.2' | ||
'version': '4.12.3' | ||
}).version.split('.')); |
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
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
1380955
109
18122
0