medium-editor
Version:
Medium.com WYSIWYG editor clone.
513 lines (426 loc) • 29 kB
JavaScript
/*global selectElementContents, placeCursorInsideElement, isSafari */
describe('MediumEditor.selection TestCase', function () {
'use strict';
beforeEach(function () {
setupTestHelpers.call(this);
this.el = this.createElement('div', 'editor', 'lorem ipsum');
});
afterEach(function () {
this.cleanupTest();
});
describe('Exposure', function () {
it('is exposed on the MediumEditor ctor', function () {
expect(MediumEditor.selection).toBeTruthy();
});
});
describe('exportSelection', function () {
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>';
placeCursorInsideElement(this.el.querySelector('span'), 1); // end of first span
var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);
expect(exportedSelection.emptyBlocksIndex).toEqual(undefined);
});
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>';
placeCursorInsideElement(this.el.querySelector('b'), 0); // beginning of <b> tag
var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);
expect(exportedSelection.emptyBlocksIndex).toEqual(0);
});
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>';
// After the 'W' in whatever
placeCursorInsideElement(this.el.querySelector('p.target').firstChild, 1);
var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);
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>';
// Before the 'e' in whatever
placeCursorInsideElement(this.el.querySelector('span.target').firstChild, 0);
var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);
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>';
// Before the 'e' in whatever
placeCursorInsideElement(this.el.querySelector('span.target'), 0);
var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);
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>';
placeCursorInsideElement(this.el.getElementsByTagName('p')[1], 0);
var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);
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>';
placeCursorInsideElement(this.el.getElementsByTagName('p')[2], 0);
var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);
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>';
placeCursorInsideElement(this.el.querySelector('h2'), 0);
var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);
expect(exportedSelection.emptyBlocksIndex).toEqual(2);
});
it('should export a selection that specifies an image is the selection', function () {
this.el.innerHTML = '<p>lorem ipsum <a href="#"><img src="../demo/img/medium-editor.jpg" /></a> dolor</p>';
selectElementContents(this.el.querySelector('a'));
var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);
expect(exportedSelection.trailingImageCount).toBe(1);
expect(exportedSelection.start).toBe(12);
expect(exportedSelection.end).toBe(12);
});
it('should export a selection that can be imported when the selection starts with an image', function () {
this.el.innerHTML = '<p>lorem ipsum <a href="#"><img src="../demo/img/medium-editor.jpg" />img</a> dolor</p>';
selectElementContents(this.el.querySelector('a'));
var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);
expect(exportedSelection.trailingImageCount).toBe(undefined);
expect(exportedSelection.start).toBe(12);
expect(exportedSelection.end).toBe(15);
selectElementContents(this.el);
MediumEditor.selection.importSelection(exportedSelection, this.el, document);
var range = window.getSelection().getRangeAt(0);
expect(range.toString()).toBe('img');
if (range.startContainer.nodeName.toLowerCase() === 'a') {
expect(range.startContainer).toBe(this.el.querySelector('a'));
expect(range.startOffset).toBe(0);
} else {
expect(range.startContainer.nextSibling).toBe(this.el.querySelector('a'));
expect(range.startOffset).toBe(12);
}
});
it('should export a selection that specifies an image is at the end of a selection', function () {
this.el.innerHTML = '<p>lorem ipsum <a href="#">img<b><img src="../demo/img/medium-editor.jpg" /><img src="../demo/img/roman.jpg" /></b></a> dolor</p>';
selectElementContents(this.el.querySelector('a'));
var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);
expect(exportedSelection.trailingImageCount).toBe(2);
expect(exportedSelection.start).toBe(12);
expect(exportedSelection.end).toBe(15);
});
});
describe('importSelection', function () {
it('should be able to import an exported selection', function () {
this.el.innerHTML = 'lorem <i>ipsum</i> dolor';
selectElementContents(this.el.querySelector('i'));
var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);
expect(Object.keys(exportedSelection).sort()).toEqual(['end', 'start']);
selectElementContents(this.el);
expect(exportedSelection).not.toEqual(MediumEditor.selection.exportSelection(this.el, document));
MediumEditor.selection.importSelection(exportedSelection, this.el, document);
expect(exportedSelection).toEqual(MediumEditor.selection.exportSelection(this.el, document));
});
it('should be able to import an exported selection that contain nodeTypes > 3', function (done) {
this.el.innerHTML = '<div><p>stuff here <!-- comment nodeType is 8 --> additional stuff here </p></div>';
selectElementContents(this.el.querySelector('p'));
var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);
expect(Object.keys(exportedSelection).sort()).toEqual(['end', 'start']);
selectElementContents(this.el);
expect(exportedSelection).toEqual(MediumEditor.selection.exportSelection(this.el, document));
MediumEditor.selection.importSelection(exportedSelection, this.el, document);
expect(exportedSelection).toEqual(MediumEditor.selection.exportSelection(this.el, document));
done();
});
it('should import an exported selection outside any anchor tag', function () {
this.el.innerHTML = '<p id=1>Hello world: <a href="#">http://www.example.com</a></p><p id=2><br></p>';
var link = this.el.getElementsByTagName('a')[0];
placeCursorInsideElement(link.childNodes[0], link.childNodes[0].nodeValue.length);
expect(MediumEditor.util.isDescendant(link, window.getSelection().getRangeAt(0).startContainer, true)).toBe(true);
var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);
MediumEditor.selection.importSelection(exportedSelection, this.el, document, true);
var range = window.getSelection().getRangeAt(0),
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
// down to the text node. Setting the range to use the P tag for the start is necessary to support
// MSIE, where it removes the link when the cursor is placed at the end of the text node in the anchor.
while (node.nodeName.toLowerCase() !== 'p') {
node = node.parentNode;
}
expect(node.nodeName.toLowerCase()).toBe('p');
expect(node.getAttribute('id')).toBe('1');
});
// https://github.com/yabwe/medium-editor/issues/738
it('should import an exported non-collapsed selection after an empty paragraph', function () {
this.el.innerHTML = '<p>This is <a href="#">a link</a></p><p><br/></p><p>not a link</p>';
var lastTextNode = this.el.childNodes[2].firstChild;
MediumEditor.selection.select(document, lastTextNode, 0, lastTextNode, 'not a link'.length);
var exportedSelection = MediumEditor.selection.exportSelection(this.el, document);
expect(exportedSelection).toEqual({ start: 14, end: 24, emptyBlocksIndex: 2 });
MediumEditor.selection.importSelection(exportedSelection, this.el, document);
var range = window.getSelection().getRangeAt(0);
expect(range.startContainer === lastTextNode || range.startContainer === lastTextNode.parentNode)
.toBe(true, 'The selection is starting at the wrong element');
expect(range.startOffset).toBe(0, 'The start of the selection is not at the beginning of the text node');
expect(range.endContainer === lastTextNode || range.endContainer === lastTextNode.parentNode)
.toBe(true, 'The selection is ending at the wrong element');
expect(range.endOffset).toBe('not a link'.length, 'The end of the selection is not at the end of the text node');
});
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>';
MediumEditor.selection.importSelection({
'start': 14,
'end': 14,
'emptyBlocksIndex': 1
}, this.el, document);
var startParagraph = MediumEditor.util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'p');
expect(startParagraph).toBe(this.el.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>';
MediumEditor.selection.importSelection({
'start': 14,
'end': 14,
'emptyBlocksIndex': 2
}, this.el, document);
var startParagraph = MediumEditor.util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'p');
expect(startParagraph).toBe(this.el.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 () {
var el = this.createElement('div', 'editor', '<p><span>www.google.com</span></p><p><br /></p><p>Whatever</p>');
MediumEditor.selection.importSelection({
'start': 14,
'end': 14,
'editableElementIndex': 1,
'emptyBlocksIndex': 2
}, el, document);
var startParagraph = MediumEditor.util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'p');
expect(startParagraph).toBe(el.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>';
MediumEditor.selection.importSelection({
'start': 14,
'end': 14,
'emptyBlocksIndex': 2
}, this.el, document);
var startParagraph = MediumEditor.util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'h2');
expect(startParagraph).toBe(this.el.querySelector('h2'), 'block element after empty block element');
});
it('should import a position with the cursor after an empty block element when there are nested block elements', function () {
this.el.innerHTML = '<blockquote><p><span>www.google.com</span></p></blockquote><h1><br /></h1><h2><br /></h2><p>Whatever</p>';
MediumEditor.selection.importSelection({
'start': 14,
'end': 14,
'emptyBlocksIndex': 2
}, this.el, document);
var startParagraph = MediumEditor.util.getClosestTag(window.getSelection().getRangeAt(0).startContainer, 'h2');
expect(startParagraph).toBe(this.el.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>';
MediumEditor.selection.importSelection({
'start': 14,
'end': 14,
'emptyBlocksIndex': 3
}, this.el, document);
var innerElement = window.getSelection().getRangeAt(0).startContainer;
expect(MediumEditor.util.isDescendant(this.el.querySelector('i'), innerElement, true)).toBe(true, 'nested inline elment inside block element after empty block element');
});
['br', 'img'].forEach(function (tagName) {
it('should not import a selection into focusing on the element \'' + tagName + '\' that cannot have children', function () {
this.el.innerHTML = '<p>Hello</p><p><' + tagName + ' /></p><p>World<p>';
MediumEditor.selection.importSelection({
'start': 5,
'end': 5,
'emptyBlocksIndex': 1
}, this.el, document);
var innerElement = window.getSelection().getRangeAt(0).startContainer;
expect(innerElement.nodeName.toLowerCase()).toBe('p', 'focused element nodeName');
expect(innerElement).toBe(window.getSelection().getRangeAt(0).endContainer);
expect(innerElement.previousSibling.nodeName.toLowerCase()).toBe('p', 'previous sibling name');
expect(innerElement.nextSibling.nodeName.toLowerCase()).toBe('p', 'next sibling name');
});
});
it('should not import a selection into focusing on an empty element in a table', function () {
this.el.innerHTML = '<p>Hello</p><table><colgroup><col /></colgroup>' +
'<thead><tr><th>Head</th></tr></thead>' +
'<tbody><tr><td>Body</td></tr></tbody></table><p>World<p>';
MediumEditor.selection.importSelection({
'start': 5,
'end': 5,
'emptyBlocksIndex': 1
}, this.el, document);
var innerElement = window.getSelection().getRangeAt(0).startContainer;
// The behavior varies from browser to browser for this case, some select TH, some #textNode
expect(MediumEditor.util.isDescendant(this.el.querySelector('th'), innerElement, true))
.toBe(true, 'expect selection to be of TH or a descendant');
expect(innerElement).toBe(window.getSelection().getRangeAt(0).endContainer);
});
it('should 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>';
// 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')
MediumEditor.selection.importSelection({
'start': 14,
'end': 14,
'emptyBlocksIndex': 3
}, this.el, document);
var innerElement = window.getSelection().getRangeAt(0).startContainer;
expect(MediumEditor.util.isDescendant(this.el.querySelectorAll('p')[1], innerElement, true)).toBe(false, 'moved selection beyond non-empty block element');
expect(MediumEditor.util.isDescendant(this.el.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 lastLi = this.el.querySelectorAll('ul > li')[2];
// Select the <li> with 'target'
selectElementContents(lastLi.firstChild);
var selectionData = MediumEditor.selection.exportSelection(this.el, document);
expect(selectionData.emptyBlocksIndex).toBeUndefined();
MediumEditor.selection.importSelection(selectionData, this.el, document);
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');
});
it('should support a selection that specifies an image is the selection', function () {
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, startsWithImage: true, trailingImageCount: 1 }, this.el, document);
var range = window.getSelection().getRangeAt(0);
expect(range.toString()).toBe('');
expect(MediumEditor.util.isDescendant(range.endContainer, this.el.querySelector('img'), true)).toBe(true, 'the image is not within the selection');
});
it('should support a selection that starts with an image', function () {
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, startsWithImage: true }, this.el, document);
var range = window.getSelection().getRangeAt(0);
expect(range.toString()).toBe('img');
if (range.startContainer.nodeName.toLowerCase() === 'a') {
expect(range.startContainer).toBe(this.el.querySelector('a'));
expect(range.startOffset).toBe(0);
} else {
expect(range.startContainer.nextSibling).toBe(this.el.querySelector('a'));
expect(range.startOffset).toBe(12);
}
});
it('should support a selection that ends with an image', function () {
this.el.innerHTML = '<p>lorem ipsum <a href="#">img<img src="../demo/img/medium-editor.jpg" /><img src="../demo/img/roman.jpg" /></a> dolor</p>';
MediumEditor.selection.importSelection({ start: 12, end: 15, trailingImageCount: 2 }, this.el, document);
var range = window.getSelection().getRangeAt(0);
expect(range.toString()).toBe('img');
expect(MediumEditor.util.isDescendant(range.endContainer, this.el.querySelector('img'), true)).toBe(true, 'the image is not within the selection');
});
// 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);
});
});
describe('getSelectedElements', function () {
it('no selected elements on empty selection', function () {
window.getSelection().removeAllRanges();
var elements = MediumEditor.selection.getSelectedElements(document);
expect(elements.length).toBe(0);
});
it('should select element from selection', function () {
this.el.innerHTML = 'lorem <i>ipsum</i> dolor';
selectElementContents(this.el.querySelector('i').firstChild);
var elements = MediumEditor.selection.getSelectedElements(document);
expect(elements.length).toBe(1);
expect(elements[0].nodeName.toLowerCase()).toBe('i');
expect(elements[0].innerHTML).toBe('ipsum');
});
it('should select first element when selection is global (ie: all the editor)', function () {
this.el.innerHTML = 'lorem <i>ipsum</i> dolor';
selectElementContents(this.el);
var elements = MediumEditor.selection.getSelectedElements(document);
expect(elements.length).toBe(1);
expect(elements[0].nodeName.toLowerCase()).toBe('i');
expect(elements[0].innerHTML).toBe('ipsum');
});
});
describe('getSelectedParentElement', function () {
it('should return null on bad range', function () {
expect(MediumEditor.selection.getSelectedParentElement(null)).toBe(null);
expect(MediumEditor.selection.getSelectedParentElement(false)).toBe(null);
});
it('should select the document', function () {
this.el.innerHTML = '<p>lorem <i>ipsum</i> dolor <span>hello</span> <b>you</b> </p>';
var range = document.createRange(),
sel = window.getSelection(),
element;
range.setStart(document, 0);
range.setEnd(this.el.querySelector('b').firstChild, 2);
sel.removeAllRanges();
sel.addRange(range);
element = MediumEditor.selection.getSelectedParentElement(range);
expect(element).toBe(document);
});
});
describe('selectionContainsContent', function () {
it('should return true for non-empty text', function () {
this.el.innerHTML = '<p>this is<span> </span>text</p>';
selectElementContents(this.el.querySelector('p'));
expect(MediumEditor.selection.selectionContainsContent(document)).toBe(true);
});
it('should return false for white-space only selections', function () {
this.el.innerHTML = '<p>this is<span> </span>text</p>';
selectElementContents(this.el.querySelector('span'));
expect(MediumEditor.selection.selectionContainsContent(document)).toBe(false);
});
it('should return true for image with link selections', function () {
this.el.innerHTML = '<p>this is <a href="#"><img src="../demo/img/medium-editor.jpg" /></a> image test</p>';
selectElementContents(this.el.querySelector('a'));
expect(MediumEditor.selection.selectionContainsContent(document)).toBe(true);
});
});
describe('clearSelection', function () {
it('should clear the selection and set the caret to the start of the prior range when specified', function () {
this.el.innerHTML = '<p>this is<span> </span>text</p>';
selectElementContents(this.el.querySelector('p'));
var selectionStart = document.getSelection().anchorOffset;
MediumEditor.selection.clearSelection(document, true);
expect(document.getSelection().focusOffset).toBe(selectionStart);
var newSelectionEnd = document.getSelection().focusOffset;
expect(newSelectionEnd).toBe(selectionStart);
});
it('should clear the selection and set the caret to the end of the prior range by default', function () {
this.el.innerHTML = '<p>this is<span> </span>text</p>';
selectElementContents(this.el.querySelector('p'));
var selectionEnd = document.getSelection().focusOffset;
MediumEditor.selection.clearSelection(document);
expect(document.getSelection().anchorOffset).toBe(selectionEnd);
var newSelectionStart = document.getSelection().anchorOffset;
expect(newSelectionStart).toBe(selectionEnd);
});
});
});