Skip to content

Commit

Permalink
Merge pull request #625 from patrick-webs/auto-link-fixes
Browse files Browse the repository at this point in the history
Auto link fixes
  • Loading branch information
nmielnik committed May 21, 2015
2 parents 71f5554 + d8b55bc commit f2bee19
Show file tree
Hide file tree
Showing 7 changed files with 300 additions and 56 deletions.
99 changes: 83 additions & 16 deletions dist/js/medium-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -1600,6 +1600,39 @@ var Selection;
}, contentWindow);
},

// Utility method called from importSelection only
importSelectionMoveCursorPastAnchor: function (selectionState, range) {
var nodeInsideAnchorTagFunction = function (node) {
return node.nodeName.toLowerCase() === 'a';
};
if (selectionState.start === selectionState.end &&
range.startContainer.nodeType === 3 &&
range.startOffset === range.startContainer.nodeValue.length &&
Util.traverseUp(range.startContainer, nodeInsideAnchorTagFunction)) {
var prevNode = range.startContainer,
currentNode = range.startContainer.parentNode;
while (currentNode !== null && currentNode.nodeName.toLowerCase() !== 'a') {
if (currentNode.childNodes[currentNode.childNodes.length - 1] !== prevNode) {
currentNode = null;
} else {
prevNode = currentNode;
currentNode = currentNode.parentNode;
}
}
if (currentNode !== null && currentNode.nodeName.toLowerCase() === 'a') {
var currentNodeIndex = null;
for (var i = 0; currentNodeIndex === null && i < currentNode.parentNode.childNodes.length; i++) {
if (currentNode.parentNode.childNodes[i] === currentNode) {
currentNodeIndex = i;
}
}
range.setStart(currentNode.parentNode, currentNodeIndex + 1);
range.collapse(true);
}
}
return range;
},

selectionInContentEditableFalse: function (contentWindow) {
// determine if the current selection is exclusively inside
// a contenteditable="false", though treat the case of an
Expand Down Expand Up @@ -3818,9 +3851,9 @@ var AutoLink,
LINK_REGEXP_TEXT =
'(' +
// Version of Gruber URL Regexp optimized for JS: http://stackoverflow.com/a/17733640
'((?:[a-z][\\w-]+:(?:\\\/{1,3}|[a-z0-9%])|www\\d{0,3}[.]|[a-z0-9.\\-]+[.][a-z]{2,4}\\\/)\\S+(?:[^\\s`!\\[\\]{};:\'\".,?\u00AB\u00BB\u201C\u201D\u2018\u2019]))' +
// Addition to above Regexp to support bare domains with common non-i18n TLDs and without www prefix:
')|([a-z0-9\\-]+\\.(com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|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|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj| Ja|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw))';
'((?:(https?://|ftps?://|nntp://)|www\\d{0,3}[.]|[a-z0-9.\\-]+[.][a-z]{2,4}\\\/)\\S+(?:[^\\s`!\\[\\]{};:\'\".,?\u00AB\u00BB\u201C\u201D\u2018\u2019]))' +
// Addition to above Regexp to support bare domains/one level subdomains with common non-i18n TLDs and without www prefix:
')|(([a-z0-9\\-]+\\.)?[a-z0-9\\-]+\\.(com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|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|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj| Ja|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw))';

(function () {
'use strict';
Expand All @@ -3831,10 +3864,15 @@ LINK_REGEXP_TEXT =
init: function () {
this.disableEventHandling = false;
this.base.subscribe('editableKeypress', this.onKeypress.bind(this));
this.base.subscribe('editableBlur', this.onBlur.bind(this));
// MS IE has it's own auto-URL detect feature but ours is better in some ways. Be consistent.
this.base.options.ownerDocument.execCommand('AutoUrlDetect', false, false);
},

onBlur: function (blurEvent, editable) {
this.performLinking(editable);
},

onKeypress: function (keyPressEvent) {
if (this.disableEventHandling) {
return;
Expand All @@ -3849,7 +3887,9 @@ LINK_REGEXP_TEXT =
try {
var sel = this.base.exportSelection();
if (this.performLinking(keyPressEvent.target)) {
this.base.importSelection(sel);
// pass true for favorLaterSelectionAnchor - this is needed for links at the end of a
// paragraph in MS IE, or MS IE causes the link to be deleted right after adding it.
this.base.importSelection(sel, true);
}
} catch (e) {
if (window.console) {
Expand Down Expand Up @@ -3913,16 +3953,23 @@ LINK_REGEXP_TEXT =

findLinkableText: function (contenteditable) {
var linkRegExp = new RegExp(LINK_REGEXP_TEXT, 'gi'),
whitespaceChars = [' ', '\t', '\n', '\r', '\u00A0', '\u2000', '\u2001', '\u2002', '\u2003',
'\u2028', '\u2029'],
textContent = contenteditable.textContent,
match = null,
matches = [];

while ((match = linkRegExp.exec(textContent)) !== null) {
matches.push({
href: match[0],
start: match.index,
end: match.index + match[0].length
});
var matchEnd = match.index + match[0].length;
// If the regexp detected something as a link that has text immediately preceding/following it, bail out.
if ((match.index === 0 || whitespaceChars.indexOf(textContent[match.index - 1]) !== -1) &&
(matchEnd === textContent.length || whitespaceChars.indexOf(textContent[matchEnd]) !== -1)) {
matches.push({
href: match[0],
start: match.index,
end: matchEnd
});
}
}
return matches;
},
Expand Down Expand Up @@ -3966,16 +4013,25 @@ LINK_REGEXP_TEXT =
},

createLink: function (textNodes, href) {
var alreadyLinked = Util.traverseUp(textNodes[0], function (node) {
return node.nodeName.toLowerCase() === 'a';
});
if (alreadyLinked) {
var shouldNotLink = false;
for (var i = 0; i < textNodes.length && shouldNotLink === false; i++) {
// Do not link if the text node is either inside an anchor or inside span[data-auto-link]
shouldNotLink = !!Util.traverseUp(textNodes[i], function (node) {
return node.nodeName.toLowerCase() === 'a' ||
(node.getAttribute && node.getAttribute('data-auto-link') === 'true');
});
}
if (shouldNotLink) {
return false;
}

var anchor = document.createElement('a');
Util.moveTextRangeIntoElement(textNodes[0], textNodes[textNodes.length - 1], anchor);
var anchor = document.createElement('a'),
span = document.createElement('span');
Util.moveTextRangeIntoElement(textNodes[0], textNodes[textNodes.length - 1], span);
span.setAttribute('data-auto-link', 'true');
anchor.setAttribute('href', Util.ensureUrlHasProtocol(href));
span.parentNode.insertBefore(anchor, span);
anchor.appendChild(span);
return true;
}
});
Expand Down Expand Up @@ -6046,7 +6102,13 @@ function MediumEditor(elements, options) {
// http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html
// Tim Down
// TODO: move to selection.js and clean up old methods there
importSelection: function (inSelectionState) {
//
// {object} inSelectionState - the selection to import
// {boolean} [favorLaterSelectionAnchor] - defaults to false. If true, import the cursor immediately
// subsequent to an anchor tag if it would otherwise be placed right at the trailing edge inside the
// anchor. This cursor positioning, even though visually equivalent to the user, can affect behavior
// in MS IE.
importSelection: function (inSelectionState, favorLaterSelectionAnchor) {
if (!inSelectionState) {
return;
}
Expand Down Expand Up @@ -6097,6 +6159,11 @@ function MediumEditor(elements, options) {
}
}

// 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: 3 additions & 4 deletions dist/js/medium-editor.min.js

Large diffs are not rendered by default.

Loading

0 comments on commit f2bee19

Please sign in to comment.