jquery-textcomplete
Version:
[](http://badge.fury.io/js/jquery-textcomplete) [](http://badge.fury.io/bo/jquery-textcomplete) [ • 4.6 kB
JavaScript
// NOTE: TextComplete plugin has contenteditable support but it does not work
// fine especially on old IEs.
// Any pull requests are REALLY welcome.
+function ($) {
'use strict';
// ContentEditable adapter
// =======================
//
// Adapter for contenteditable elements.
function ContentEditable (element, completer, option) {
this.initialize(element, completer, option);
}
$.extend(ContentEditable.prototype, $.fn.textcomplete.Adapter.prototype, {
// Public methods
// --------------
// Update the content with the given value and strategy.
// When an dropdown item is selected, it is executed.
select: function (value, strategy, e) {
var pre = this.getTextFromHeadToCaret();
// use ownerDocument instead of window to support iframes
var sel = this.el.ownerDocument.getSelection();
var range = sel.getRangeAt(0);
var selection = range.cloneRange();
selection.selectNodeContents(range.startContainer);
var content = selection.toString();
var post = content.substring(range.startOffset);
var newSubstr = strategy.replace(value, e);
var regExp;
if (typeof newSubstr !== 'undefined') {
if ($.isArray(newSubstr)) {
post = newSubstr[1] + post;
newSubstr = newSubstr[0];
}
regExp = $.isFunction(strategy.match) ? strategy.match(pre) : strategy.match;
pre = pre.replace(regExp, newSubstr)
.replace(/ $/, " "); //   necessary at least for CKeditor to not eat spaces
range.selectNodeContents(range.startContainer);
range.deleteContents();
// create temporary elements
var preWrapper = this.el.ownerDocument.createElement("div");
preWrapper.innerHTML = pre;
var postWrapper = this.el.ownerDocument.createElement("div");
postWrapper.innerHTML = post;
// create the fragment thats inserted
var fragment = this.el.ownerDocument.createDocumentFragment();
var childNode;
var lastOfPre;
while (childNode = preWrapper.firstChild) {
lastOfPre = fragment.appendChild(childNode);
}
while (childNode = postWrapper.firstChild) {
fragment.appendChild(childNode);
}
// insert the fragment & jump behind the last node in "pre"
range.insertNode(fragment);
range.setStartAfter(lastOfPre);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
},
// Private methods
// ---------------
// Returns the caret's relative position from the contenteditable's
// left top corner.
//
// Examples
//
// this._getCaretRelativePosition()
// //=> { top: 18, left: 200, lineHeight: 16 }
//
// Dropdown's position will be decided using the result.
_getCaretRelativePosition: function () {
var range = this.el.ownerDocument.getSelection().getRangeAt(0).cloneRange();
var node = this.el.ownerDocument.createElement('span');
range.insertNode(node);
range.selectNodeContents(node);
range.deleteContents();
var $node = $(node);
var position = $node.offset();
position.left -= this.$el.offset().left;
position.top += $node.height() - this.$el.offset().top;
position.lineHeight = $node.height();
// special positioning logic for iframes
// this is typically used for contenteditables such as tinymce or ckeditor
if (this.completer.$iframe) {
var iframePosition = this.completer.$iframe.offset();
position.top += iframePosition.top;
position.left += iframePosition.left;
//subtract scrollTop from element in iframe
position.top -= this.$el.scrollTop();
}
$node.remove();
return position;
},
// Returns the string between the first character and the caret.
// Completer will be triggered with the result for start autocompleting.
//
// Example
//
// // Suppose the html is '<b>hello</b> wor|ld' and | is the caret.
// this.getTextFromHeadToCaret()
// // => ' wor' // not '<b>hello</b> wor'
getTextFromHeadToCaret: function () {
var range = this.el.ownerDocument.getSelection().getRangeAt(0);
var selection = range.cloneRange();
selection.selectNodeContents(range.startContainer);
return selection.toString().substring(0, range.startOffset);
}
});
$.fn.textcomplete.ContentEditable = ContentEditable;
}(jQuery);