Skip to content

Commit

Permalink
Merge branch 'master' into 4.12.x
Browse files Browse the repository at this point in the history
Conflicts:
	bower.json
  • Loading branch information
nmielnik committed Jun 12, 2015
2 parents caf3232 + 9936fca commit e9d1626
Show file tree
Hide file tree
Showing 10 changed files with 354 additions and 20 deletions.
2 changes: 1 addition & 1 deletion Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ module.exports = function (grunt) {

gruntConfig.bump = {
options: {
files: ['bower.json', 'package.json', 'src/js/version.js'],
files: ['package.json', 'src/js/version.js'],
updateConfigs: [],
commit: false,
createTag: false,
Expand Down
1 change: 0 additions & 1 deletion bower.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"name": "medium-editor",
"version": "4.12.2",
"homepage": "http://yabwe.github.io/medium-editor/",
"authors": [
"Davi Ferreira <hi@daviferreira.com>",
Expand Down
4 changes: 2 additions & 2 deletions spec/auto-link.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ describe('Autolink', function () {
selectElementContentsAndFire(this.el);
triggerAutolinking(this.el, Util.keyCode.ENTER);
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');
expect(links[0].firstChild.getAttribute('data-auto-link')).toBe('true');
expect(links[0].textContent).toBe('http://www.example.enter');
Expand All @@ -193,7 +193,7 @@ describe('Autolink', function () {
selectElementContentsAndFire(this.el);
triggerAutolinking(this.el, Util.keyCode.SPACE);
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');
expect(links[0].firstChild.getAttribute('data-auto-link')).toBe('true');
expect(links[0].textContent).toBe('http://www.example.space');
Expand Down
13 changes: 13 additions & 0 deletions spec/buttons.spec.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
/*global MediumEditor, describe, it, expect, spyOn, AnchorForm,
beforeAll, afterAll,
afterEach, beforeEach, jasmine, fireEvent, setupTestHelpers,
selectElementContentsAndFire, isOldIE, isIE */

describe('Buttons TestCase', function () {
'use strict';

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 () {
setupTestHelpers.call(this);
this.el = this.createElement('div', 'editor', 'lorem ipsum');
Expand Down
172 changes: 171 additions & 1 deletion spec/selection.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*global MediumEditor, describe, it, expect, spyOn,
afterEach, beforeEach, fireEvent,
afterEach, beforeEach, fireEvent, Util,
jasmine, selectElementContents, setupTestHelpers,
selectElementContentsAndFire, Selection, placeCursorInsideElement */

Expand Down Expand Up @@ -74,6 +74,176 @@ describe('Selection TestCase', function () {
expect(Object.keys(exportedSelection).sort()).toEqual(['editableElementIndex', 'end', 'start']);
expect(exportedSelection.editableElementIndex).toEqual(1);
});

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');
});
});

describe('Saving Selection', function () {
Expand Down
31 changes: 30 additions & 1 deletion src/js/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,17 @@ function MediumEditor(elements, options) {
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;
}
}
}
}

Expand Down Expand Up @@ -988,11 +999,29 @@ function MediumEditor(elements, options) {
}
}

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.
if (favorLaterSelectionAnchor) {
range = Selection.importSelectionMoveCursorPastAnchor(selectionState, range);
}

sel = this.options.contentWindow.getSelection();
sel.removeAllRanges();
sel.addRange(range);
Expand Down
7 changes: 4 additions & 3 deletions src/js/extensions/anchor.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ var AnchorForm;
event.preventDefault();
event.stopPropagation();

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');
}

Expand Down
Loading

0 comments on commit e9d1626

Please sign in to comment.