medium-editor
Advanced tools
Comparing version 5.6.0 to 5.6.1
@@ -0,1 +1,8 @@ | ||
5.6.1 / 2015-08-10 | ||
================== | ||
* Fix issue with creating anchors and restoring selection at the beginning of paragraphs | ||
* Fix issue with creating anchors and restoring selection within list items and nested blocks | ||
* Ensure CTRL + M is respected as a way to insert new lines | ||
5.6.0 / 2015-08-07 | ||
@@ -2,0 +9,0 @@ ================== |
{ | ||
"name": "medium-editor", | ||
"version": "5.6.0", | ||
"version": "5.6.1", | ||
"author": "Davi Ferreira <hi@daviferreira.com>", | ||
@@ -5,0 +5,0 @@ "contributors": [ |
@@ -538,3 +538,3 @@ # MediumEditor | ||
* __.serialize()__: returns a JSON object with elements contents | ||
* __.setContent(html, element)__: sets the `innerHTML` to `html` of the element at `index` | ||
* __.setContent(html, index)__: sets the `innerHTML` to `html` of the element at `index` | ||
@@ -541,0 +541,0 @@ ## Capturing DOM changes |
@@ -309,3 +309,4 @@ /*global MediumEditor, describe, it, expect, spyOn, | ||
it('should not select empty paragraphs when link is created at beginning of paragraph', function () { | ||
// https://github.com/yabwe/medium-editor/issues/757 | ||
it('should not select empty paragraphs when link is created at beginning of paragraph after empty paragraphs', function () { | ||
spyOn(MediumEditor.prototype, 'createLink').and.callThrough(); | ||
@@ -349,2 +350,42 @@ this.el.innerHTML = '<p>Some text</p><p><br/></p><p><br/></p><p>link text more text</p>'; | ||
// https://github.com/yabwe/medium-editor/issues/757 | ||
it('should not select empty paragraphs when link is created at beginning of paragraph after another paragraph', function () { | ||
spyOn(MediumEditor.prototype, 'createLink').and.callThrough(); | ||
this.el.innerHTML = '<p>Some text</p><p>link text more text</p>'; | ||
var editor = this.newMediumEditor('.editor'), | ||
lastP = this.el.lastChild, | ||
anchorExtension = editor.getExtensionByName('anchor'), | ||
toolbar = editor.getExtensionByName('toolbar'); | ||
// Select the text 'link text' in the last paragraph | ||
MediumEditor.selection.select(document, lastP.firstChild, 0, lastP.firstChild, 'link text'.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(); | ||
input.value = 'http://www.example.com'; | ||
fireEvent(input, 'keyup', { | ||
keyCode: Util.keyCode.ENTER | ||
}); | ||
expect(editor.createLink).toHaveBeenCalledWith({ | ||
url: 'http://www.example.com', | ||
target: '_self' | ||
}); | ||
// Make sure the <p> wasn't removed, and the <a> was added to the end | ||
expect(this.el.lastChild).toBe(lastP); | ||
expect(lastP.firstChild.nodeName.toLowerCase()).toBe('a'); | ||
// Make sure selection is only the link | ||
var range = window.getSelection().getRangeAt(0); | ||
expect(MediumEditor.util.isDescendant(lastP, range.startContainer, true)).toBe(true, 'The start of the selection is incorrect'); | ||
expect(range.startOffset).toBe(0); | ||
expect(MediumEditor.util.isDescendant(lastP.firstChild, range.endContainer, true)).toBe(true, 'The end of the selection is not contained within the link'); | ||
}); | ||
it('should not remove the <p> container when adding a link inside a top-level <p> with a single text-node child', function () { | ||
@@ -351,0 +392,0 @@ spyOn(MediumEditor.prototype, 'createLink').and.callThrough(); |
@@ -215,2 +215,107 @@ /*global describe, it, expect, spyOn, | ||
describe('when the ctrl key and m key is pressed', function () { | ||
it('should prevent new lines from being inserted when disableReturn options is true', function () { | ||
this.el.innerHTML = 'lorem ipsum'; | ||
var editor = this.newMediumEditor('.editor', { disableReturn: true }), | ||
evt; | ||
placeCursorInsideElement(editor.elements[0], 0); | ||
evt = prepareEvent(editor.elements[0], 'keydown', { | ||
ctrlKey: true, | ||
keyCode: Util.keyCode.M | ||
}); | ||
spyOn(evt, 'preventDefault').and.callThrough(); | ||
firePreparedEvent(evt, editor.elements[0], 'keydown'); | ||
expect(evt.preventDefault).toHaveBeenCalled(); | ||
}); | ||
it('should prevent new lines from being inserted when data-disable-return is defined', function () { | ||
this.el.innerHTML = 'lorem ipsum'; | ||
this.el.setAttribute('data-disable-return', true); | ||
var editor = this.newMediumEditor('.editor'), | ||
evt; | ||
placeCursorInsideElement(editor.elements[0], 0); | ||
evt = prepareEvent(editor.elements[0], 'keydown', { | ||
ctrlKey: true, | ||
keyCode: Util.keyCode.M | ||
}); | ||
spyOn(evt, 'preventDefault').and.callThrough(); | ||
firePreparedEvent(evt, editor.elements[0], 'keydown'); | ||
expect(evt.preventDefault).toHaveBeenCalled(); | ||
}); | ||
it('should prevent consecutive new lines from being inserted when disableDoubleReturn is true', function () { | ||
this.el.innerHTML = '<p><br></p>'; | ||
var editor = this.newMediumEditor('.editor', { disableDoubleReturn: true }), | ||
p = editor.elements[0].querySelector('p'), | ||
evt; | ||
placeCursorInsideElement(p, 0); | ||
evt = prepareEvent(p, 'keydown', { | ||
ctrlKey: true, | ||
keyCode: Util.keyCode.M | ||
}); | ||
spyOn(evt, 'preventDefault').and.callThrough(); | ||
firePreparedEvent(evt, p, 'keydown'); | ||
expect(evt.preventDefault).toHaveBeenCalled(); | ||
}); | ||
it('should prevent consecutive new lines from being inserted when data-disable-double-return is defined', function () { | ||
this.el.innerHTML = '<p><br></p>'; | ||
this.el.setAttribute('data-disable-double-return', true); | ||
var editor = this.newMediumEditor('.editor'), | ||
p = editor.elements[0].querySelector('p'), | ||
evt; | ||
placeCursorInsideElement(p, 0); | ||
evt = prepareEvent(p, 'keydown', { | ||
ctrlKey: true, | ||
keyCode: Util.keyCode.M | ||
}); | ||
spyOn(evt, 'preventDefault').and.callThrough(); | ||
firePreparedEvent(evt, p, 'keydown'); | ||
expect(evt.preventDefault).toHaveBeenCalled(); | ||
}); | ||
it('should prevent consecutive new lines from being inserted inside a sentence when disableDoubleReturn is true', function () { | ||
this.el.innerHTML = '<p>hello</p><p><br></p><p>word</p>'; | ||
var editor = this.newMediumEditor('.editor', { disableDoubleReturn: true }), | ||
p = editor.elements[0].getElementsByTagName('p')[2], | ||
evt; | ||
placeCursorInsideElement(p, 0); | ||
evt = prepareEvent(p, 'keydown', { | ||
ctrlKey: true, | ||
keyCode: Util.keyCode.M | ||
}); | ||
spyOn(evt, 'preventDefault').and.callThrough(); | ||
firePreparedEvent(evt, p, 'keydown'); | ||
expect(evt.preventDefault).toHaveBeenCalled(); | ||
}); | ||
}); | ||
describe('should unlink anchors', function () { | ||
@@ -217,0 +322,0 @@ it('when the user presses enter inside an anchor', function () { |
/*global MediumEditor, describe, it, expect, spyOn, | ||
afterEach, beforeEach, fireEvent, Util, | ||
afterEach, beforeEach, fireEvent, | ||
jasmine, selectElementContents, setupTestHelpers, | ||
@@ -116,2 +116,12 @@ selectElementContentsAndFire, Selection, | ||
it('should export a position indicating the cursor is at the beginning of a paragraph', function () { | ||
this.el.innerHTML = '<p><span>www.google.com</span></p><p><b>Whatever</b></p>'; | ||
var editor = this.newMediumEditor('.editor', { | ||
buttons: ['italic', 'underline', 'strikethrough'] | ||
}); | ||
placeCursorInsideElement(editor.elements[0].querySelector('b'), 0); // beginning of <b> tag | ||
var exportedSelection = editor.exportSelection(); | ||
expect(exportedSelection.emptyBlocksIndex).toEqual(0); | ||
}); | ||
it('should not export a position indicating the cursor is after an empty paragraph', function () { | ||
@@ -196,3 +206,3 @@ this.el.innerHTML = '<p><span>www.google.com</span></p><p><br /></p>' + | ||
var startParagraph = Util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'p'); | ||
var startParagraph = MediumEditor.util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'p'); | ||
expect(startParagraph).toBe(editor.elements[0].getElementsByTagName('p')[1], 'empty paragraph'); | ||
@@ -212,3 +222,3 @@ }); | ||
var startParagraph = Util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'p'); | ||
var startParagraph = MediumEditor.util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'p'); | ||
expect(startParagraph).toBe(editor.elements[0].getElementsByTagName('p')[2], 'paragraph after empty paragraph'); | ||
@@ -229,3 +239,3 @@ }); | ||
var startParagraph = Util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'p'); | ||
var startParagraph = MediumEditor.util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'p'); | ||
expect(startParagraph).toBe(editor.elements[1].getElementsByTagName('p')[2], 'paragraph after empty paragraph'); | ||
@@ -245,3 +255,3 @@ }); | ||
var startParagraph = Util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'h2'); | ||
var startParagraph = MediumEditor.util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'h2'); | ||
expect(startParagraph).toBe(editor.elements[0].querySelector('h2'), 'block element after empty block element'); | ||
@@ -261,3 +271,3 @@ }); | ||
var startParagraph = Util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'h2'); | ||
var startParagraph = MediumEditor.util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'h2'); | ||
expect(startParagraph).toBe(editor.elements[0].querySelector('h2'), 'block element after empty block element'); | ||
@@ -278,3 +288,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'); | ||
expect(MediumEditor.util.isDescendant(editor.elements[0].querySelector('i'), innerElement, true)).toBe(true, 'nested inline elment inside block element after empty block element'); | ||
}); | ||
@@ -317,3 +327,3 @@ | ||
// The behavior varies from browser to browser for this case, some select TH, some #textNode | ||
expect(Util.isDescendant(editor.elements[0].querySelector('th'), innerElement, true)) | ||
expect(MediumEditor.util.isDescendant(editor.elements[0].querySelector('th'), innerElement, true)) | ||
.toBe(true, 'expect selection to be of TH or a descendant'); | ||
@@ -336,5 +346,29 @@ expect(innerElement).toBe(window.getSelection().getRangeAt(0).endContainer); | ||
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'); | ||
expect(MediumEditor.util.isDescendant(editor.elements[0].querySelectorAll('p')[1], innerElement, true)).toBe(false, 'moved selection beyond non-empty block element'); | ||
expect(MediumEditor.util.isDescendant(editor.elements[0].querySelector('h2'), innerElement, true)).toBe(true, 'moved selection to element to incorrect block element'); | ||
}); | ||
// https://github.com/yabwe/medium-editor/issues/732 | ||
it('should support a selection correctly when space + newlines are separating block elements', function () { | ||
this.el.innerHTML = '<ul>\n' + | ||
' <li><a href="#">a link</a></li>\n' + | ||
' <li>a list item</li>\n' + | ||
' <li>target</li>\n' + | ||
'</ul>'; | ||
var editor = this.newMediumEditor('.editor'), | ||
lastLi = this.el.querySelectorAll('ul > li')[2]; | ||
// Select the <li> with 'target' | ||
selectElementContents(lastLi.firstChild); | ||
var selectionData = editor.exportSelection(); | ||
expect(selectionData.emptyBlocksIndex).toBe(0); | ||
editor.importSelection(selectionData); | ||
var range = window.getSelection().getRangeAt(0); | ||
expect(range.toString()).toBe('target', 'The selection is around the wrong element'); | ||
expect(MediumEditor.util.isDescendant(lastLi, range.startContainer, true)).toBe(true, 'The start of the selection is invalid'); | ||
expect(MediumEditor.util.isDescendant(lastLi, range.endContainer, true)).toBe(true, 'The end of the selection is invalid'); | ||
}); | ||
}); | ||
@@ -459,3 +493,3 @@ | ||
it('no selected elements on empty selection', function () { | ||
var elements = Selection.getSelectedElements(document); | ||
var elements = MediumEditor.selection.getSelectedElements(document); | ||
@@ -471,3 +505,3 @@ expect(elements.length).toBe(0); | ||
selectElementContents(editor.elements[0].querySelector('i').firstChild); | ||
elements = Selection.getSelectedElements(document); | ||
elements = MediumEditor.selection.getSelectedElements(document); | ||
@@ -484,3 +518,3 @@ expect(elements.length).toBe(1); | ||
selectElementContents(this.el); | ||
elements = Selection.getSelectedElements(document); | ||
elements = MediumEditor.selection.getSelectedElements(document); | ||
@@ -495,4 +529,4 @@ expect(elements.length).toBe(1); | ||
it('should return null on bad range', function () { | ||
expect(Selection.getSelectedParentElement(null)).toBe(null); | ||
expect(Selection.getSelectedParentElement(false)).toBe(null); | ||
expect(MediumEditor.selection.getSelectedParentElement(null)).toBe(null); | ||
expect(MediumEditor.selection.getSelectedParentElement(false)).toBe(null); | ||
}); | ||
@@ -512,3 +546,3 @@ | ||
element = Selection.getSelectedParentElement(range); | ||
element = MediumEditor.selection.getSelectedParentElement(range); | ||
@@ -515,0 +549,0 @@ expect(element).toBe(document); |
@@ -490,3 +490,3 @@ /*global Util*/ | ||
if (Util.isKey(event, Util.keyCode.ENTER)) { | ||
if (Util.isKey(event, Util.keyCode.ENTER) || (event.ctrlKey && Util.isKey(event, Util.keyCode.M))) { | ||
return this.triggerCustomEvent('editableKeydownEnter', event, event.currentTarget); | ||
@@ -493,0 +493,0 @@ } |
@@ -63,3 +63,3 @@ /*global Util */ | ||
var emptyBlocksIndex = this.getIndexRelativeToAdjacentEmptyBlocks(doc, root, range.startContainer, range.startOffset); | ||
if (emptyBlocksIndex !== 0) { | ||
if (emptyBlocksIndex !== -1) { | ||
selectionState.emptyBlocksIndex = emptyBlocksIndex; | ||
@@ -123,18 +123,4 @@ } | ||
if (selectionState.emptyBlocksIndex) { | ||
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); | ||
if (typeof selectionState.emptyBlocksIndex !== 'undefined') { | ||
range = Selection.importSelectionMoveCursorPastBlocks(doc, root, selectionState.emptyBlocksIndex, range); | ||
} | ||
@@ -185,9 +171,59 @@ | ||
// 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. | ||
// Uses the emptyBlocksIndex calculated by getIndexRelativeToAdjacentEmptyBlocks | ||
// to move the cursor back to the start of the correct paragraph | ||
importSelectionMoveCursorPastBlocks: function (doc, root, index, range) { | ||
var treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false), | ||
startContainer = range.startContainer, | ||
startBlock, | ||
targetNode, | ||
currIndex = 0; | ||
index = index || 1; // If index is 0, we still want to move to the next block | ||
// Chrome counts newlines and spaces that separate block elements as actual elements. | ||
// If the selection is inside one of these text nodes, and it has a previous sibling | ||
// which is a block element, we want the treewalker to start at the previous sibling | ||
// and NOT at the parent of the textnode | ||
if (startContainer.nodeType === 3 && Util.isBlockContainer(startContainer.previousSibling)) { | ||
startBlock = startContainer.previousSibling; | ||
} else { | ||
startBlock = Util.getClosestBlockContainer(startContainer); | ||
} | ||
// Skip over empty blocks until we hit the block we want the selection to be in | ||
while (treeWalker.nextNode()) { | ||
if (!targetNode) { | ||
// Loop through all blocks until we hit the starting block element | ||
if (startBlock === treeWalker.currentNode) { | ||
targetNode = treeWalker.currentNode; | ||
} | ||
} else { | ||
targetNode = treeWalker.currentNode; | ||
currIndex++; | ||
// We hit the target index, bail | ||
if (currIndex === index) { | ||
break; | ||
} | ||
// 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); | ||
return range; | ||
}, | ||
// Returns -1 unless the cursor is at the beginning of a paragraph/block | ||
// If the paragraph/block is preceeded by empty paragraphs/block (with no text) | ||
// it will return the number of empty paragraphs before the cursor. | ||
// Otherwise, it will return 0, which indicates the cursor is at the beginning | ||
// of a paragraph/block, and not at the end of the paragraph/block before it | ||
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; | ||
if (cursorContainer.textContent.length > 0 && cursorOffset > 0) { | ||
return -1; | ||
} | ||
@@ -198,7 +234,6 @@ | ||
if (node.nodeType !== 3) { | ||
//node = cursorContainer.childNodes.length === cursorOffset ? null : cursorContainer.childNodes[cursorOffset]; | ||
node = cursorContainer.childNodes[cursorOffset]; | ||
} | ||
if (node && !Util.isElementAtBeginningOfBlock(node)) { | ||
return 0; | ||
return -1; | ||
} | ||
@@ -208,3 +243,4 @@ | ||
// and the block the cursor is in | ||
var treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false), | ||
var closestBlock = Util.getClosestBlockContainer(cursorContainer), | ||
treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false), | ||
emptyBlocksCount = 0; | ||
@@ -216,3 +252,3 @@ while (treeWalker.nextNode()) { | ||
} | ||
if (Util.isDescendant(treeWalker.currentNode, cursorContainer, true)) { | ||
if (treeWalker.currentNode === closestBlock) { | ||
return emptyBlocksCount; | ||
@@ -219,0 +255,0 @@ } |
@@ -44,3 +44,4 @@ /*global NodeFilter, Selection*/ | ||
DELETE: 46, | ||
K: 75 // K keycode, and not k | ||
K: 75, // K keycode, and not k | ||
M: 77 | ||
}, | ||
@@ -92,3 +93,11 @@ | ||
blockContainerElementNames: ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre'], | ||
blockContainerElementNames: [ | ||
// elements our editor generates | ||
'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'ul', 'li', 'ol', | ||
// 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', 'table', 'tbody', 'tfoot', 'video' | ||
], | ||
emptyElementNames: ['br', 'col', 'colgroup', 'hr', 'img', 'input', 'source', 'wbr'], | ||
@@ -465,3 +474,3 @@ | ||
tagName = parentNode.nodeName.toLowerCase(); | ||
while (!this.isBlockContainer(parentNode) && tagName !== 'div') { | ||
while (tagName === 'li' || (!this.isBlockContainer(parentNode) && tagName !== 'div')) { | ||
if (tagName === 'li') { | ||
@@ -468,0 +477,0 @@ return true; |
@@ -20,3 +20,3 @@ /*global MediumEditor */ | ||
// grunt-bump looks for this: | ||
'version': '5.6.0' | ||
'version': '5.6.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
1530234
18797