update to textcomplete v 1.8.0

This commit is contained in:
Mario Vavti
2017-03-30 12:21:15 +02:00
parent 5abe7d2dfb
commit cca5349110
4 changed files with 125 additions and 58 deletions

View File

@@ -136,10 +136,6 @@ if (typeof jQuery === 'undefined') {
return Object.prototype.toString.call(obj) === '[object String]';
};
var isFunction = function (obj) {
return Object.prototype.toString.call(obj) === '[object Function]';
};
var uniqueId = 0;
function Completer(element, option) {
@@ -147,32 +143,46 @@ if (typeof jQuery === 'undefined') {
this.id = 'textcomplete' + uniqueId++;
this.strategies = [];
this.views = [];
this.option = $.extend({}, Completer._getDefaults(), option);
this.option = $.extend({}, Completer.defaults, option);
if (!this.$el.is('input[type=text]') && !this.$el.is('input[type=search]') && !this.$el.is('textarea') && !element.isContentEditable && element.contentEditable != 'true') {
throw new Error('textcomplete must be called on a Textarea or a ContentEditable.');
}
if (element === document.activeElement) {
// use ownerDocument to fix iframe / IE issues
if (element === element.ownerDocument.activeElement) {
// element has already been focused. Initialize view objects immediately.
this.initialize()
} else {
// Initialize view objects lazily.
var self = this;
this.$el.one('focus.' + this.id, function () { self.initialize(); });
// Special handling for CKEditor: lazy init on instance load
if ((!this.option.adapter || this.option.adapter == 'CKEditor') && typeof CKEDITOR != 'undefined' && (this.$el.is('textarea'))) {
CKEDITOR.on("instanceReady", function(event) {
event.editor.once("focus", function(event2) {
// replace the element with the Iframe element and flag it as CKEditor
self.$el = $(event.editor.editable().$);
if (!self.option.adapter) {
self.option.adapter = $.fn.textcomplete['CKEditor'];
self.option.ckeditor_instance = event.editor;
}
self.initialize();
});
});
}
}
}
Completer._getDefaults = function () {
if (!Completer.DEFAULTS) {
Completer.DEFAULTS = {
appendTo: $('body'),
zIndex: '100'
};
}
return Completer.DEFAULTS;
}
Completer.defaults = {
appendTo: 'body',
className: '', // deprecated option
dropdownClassName: 'dropdown-menu textcomplete-dropdown',
maxCount: 10,
zIndex: '100',
rightEdgeOffset: 30
};
$.extend(Completer.prototype, {
// Public properties
@@ -184,12 +194,26 @@ if (typeof jQuery === 'undefined') {
adapter: null,
dropdown: null,
$el: null,
$iframe: null,
// Public methods
// --------------
initialize: function () {
var element = this.$el.get(0);
// check if we are in an iframe
// we need to alter positioning logic if using an iframe
if (this.$el.prop('ownerDocument') !== document && window.frames.length) {
for (var iframeIndex = 0; iframeIndex < window.frames.length; iframeIndex++) {
if (this.$el.prop('ownerDocument') === window.frames[iframeIndex].document) {
this.$iframe = $(window.frames[iframeIndex].frameElement);
break;
}
}
}
// Initialize view objects.
this.dropdown = new $.fn.textcomplete.Dropdown(element, this, this.option);
var Adapter, viewName;
@@ -281,7 +305,7 @@ if (typeof jQuery === 'undefined') {
var strategy = this.strategies[i];
var context = strategy.context(text);
if (context || context === '') {
var matchRegexp = isFunction(strategy.match) ? strategy.match(text) : strategy.match;
var matchRegexp = $.isFunction(strategy.match) ? strategy.match(text) : strategy.match;
if (isString(context)) { text = context; }
var match = text.match(matchRegexp);
if (match) { return [strategy, match[strategy.index], match]; }
@@ -399,7 +423,7 @@ if (typeof jQuery === 'undefined') {
var $parent = option.appendTo;
if (!($parent instanceof $)) { $parent = $($parent); }
var $el = $('<ul></ul>')
.addClass('dropdown-menu textcomplete-dropdown')
.addClass(option.dropdownClassName)
.attr('id', 'textcomplete-dropdown-' + option._oid)
.css({
display: 'none',
@@ -422,7 +446,7 @@ if (typeof jQuery === 'undefined') {
footer: null,
header: null,
id: null,
maxCount: 10,
maxCount: null,
placement: '',
shown: false,
data: [], // Shown zipped data.
@@ -445,8 +469,8 @@ if (typeof jQuery === 'undefined') {
render: function (zippedData) {
var contentsHtml = this._buildContents(zippedData);
var unzippedData = $.map(this.data, function (d) { return d.value; });
if (this.data.length) {
var unzippedData = $.map(zippedData, function (d) { return d.value; });
if (zippedData.length) {
var strategy = zippedData[0].strategy;
if (strategy.id) {
this.$el.attr('data-strategy', strategy.id);
@@ -480,7 +504,7 @@ if (typeof jQuery === 'undefined') {
return false;
if($(this).css('position') === 'fixed') {
pos.top -= $window.scrollTop();
pos.left -= $window.scrollLeft();
pos.left -= $window.scrollLeft();
position = 'fixed';
return false;
}
@@ -785,7 +809,10 @@ if (typeof jQuery === 'undefined') {
var windowScrollBottom = $window.scrollTop() + $window.height();
var height = this.$el.height();
if ((this.$el.position().top + height) > windowScrollBottom) {
this.$el.offset({top: windowScrollBottom - height});
// only do this if we are not in an iframe
if (!this.completer.$iframe) {
this.$el.offset({top: windowScrollBottom - height});
}
}
},
@@ -794,7 +821,7 @@ if (typeof jQuery === 'undefined') {
// to the document width so we don't know if we would have overrun it. As a heuristic to avoid that clipping
// (which makes our elements wrap onto the next line and corrupt the next item), if we're close to the right
// edge, move left. We don't know how far to move left, so just keep nudging a bit.
var tolerance = 30; // pixels. Make wider than vertical scrollbar because we might not be able to use that space.
var tolerance = this.option.rightEdgeOffset; // pixels. Make wider than vertical scrollbar because we might not be able to use that space.
var lastOffset = this.$el.offset().left, offset;
var width = this.$el.width();
var maxLeft = $window.width() - tolerance;
@@ -1005,8 +1032,14 @@ if (typeof jQuery === 'undefined') {
switch (clickEvent.keyCode) {
case 9: // TAB
case 13: // ENTER
case 16: // SHIFT
case 17: // CTRL
case 18: // ALT
case 33: // PAGEUP
case 34: // PAGEDOWN
case 40: // DOWN
case 38: // UP
case 27: // ESC
return true;
}
if (clickEvent.ctrlKey) switch (clickEvent.keyCode) {
@@ -1040,12 +1073,14 @@ if (typeof jQuery === 'undefined') {
var pre = this.getTextFromHeadToCaret();
var post = this.el.value.substring(this.el.selectionEnd);
var newSubstr = strategy.replace(value, e);
var regExp;
if (typeof newSubstr !== 'undefined') {
if ($.isArray(newSubstr)) {
post = newSubstr[1] + post;
newSubstr = newSubstr[0];
}
pre = pre.replace(strategy.match, newSubstr);
regExp = $.isFunction(strategy.match) ? strategy.match(pre) : strategy.match;
pre = pre.replace(regExp, newSubstr);
this.$el.val(pre + post);
this.el.selectionStart = this.el.selectionEnd = pre.length;
}
@@ -1062,7 +1097,8 @@ if (typeof jQuery === 'undefined') {
var p = $.fn.textcomplete.getCaretCoordinates(this.el, this.el.selectionStart);
return {
top: p.top + this._calculateLineHeight() - this.$el.scrollTop(),
left: p.left - this.$el.scrollLeft()
left: p.left - this.$el.scrollLeft(),
lineHeight: this._calculateLineHeight()
};
},
@@ -1111,12 +1147,14 @@ if (typeof jQuery === 'undefined') {
var pre = this.getTextFromHeadToCaret();
var post = this.el.value.substring(pre.length);
var newSubstr = strategy.replace(value, e);
var regExp;
if (typeof newSubstr !== 'undefined') {
if ($.isArray(newSubstr)) {
post = newSubstr[1] + post;
newSubstr = newSubstr[0];
}
pre = pre.replace(strategy.match, newSubstr);
regExp = $.isFunction(strategy.match) ? strategy.match(pre) : strategy.match;
pre = pre.replace(regExp, newSubstr);
this.$el.val(pre + post);
this.el.focus();
var range = this.el.createTextRange();
@@ -1162,30 +1200,35 @@ if (typeof jQuery === 'undefined') {
// When an dropdown item is selected, it is executed.
select: function (value, strategy, e) {
var pre = this.getTextFromHeadToCaret();
var sel = window.getSelection()
// 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];
}
pre = pre.replace(strategy.match, newSubstr);
regExp = $.isFunction(strategy.match) ? strategy.match(pre) : strategy.match;
pre = pre.replace(regExp, newSubstr)
.replace(/ $/, "&nbsp"); // &nbsp necessary at least for CKeditor to not eat spaces
range.selectNodeContents(range.startContainer);
range.deleteContents();
// create temporary elements
var preWrapper = document.createElement("div");
var preWrapper = this.el.ownerDocument.createElement("div");
preWrapper.innerHTML = pre;
var postWrapper = document.createElement("div");
var postWrapper = this.el.ownerDocument.createElement("div");
postWrapper.innerHTML = post;
// create the fragment thats inserted
var fragment = document.createDocumentFragment();
var fragment = this.el.ownerDocument.createDocumentFragment();
var childNode;
var lastOfPre;
while (childNode = preWrapper.firstChild) {
@@ -1218,8 +1261,8 @@ if (typeof jQuery === 'undefined') {
//
// Dropdown's position will be decided using the result.
_getCaretRelativePosition: function () {
var range = window.getSelection().getRangeAt(0).cloneRange();
var node = document.createElement('span');
var range = this.el.ownerDocument.getSelection().getRangeAt(0).cloneRange();
var node = this.el.ownerDocument.createElement('span');
range.insertNode(node);
range.selectNodeContents(node);
range.deleteContents();
@@ -1228,6 +1271,17 @@ if (typeof jQuery === 'undefined') {
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;
},
@@ -1241,7 +1295,7 @@ if (typeof jQuery === 'undefined') {
// this.getTextFromHeadToCaret()
// // => ' wor' // not '<b>hello</b> wor'
getTextFromHeadToCaret: function () {
var range = window.getSelection().getRangeAt(0);
var range = this.el.ownerDocument.getSelection().getRangeAt(0);
var selection = range.cloneRange();
selection.selectNodeContents(range.startContainer);
return selection.toString().substring(0, range.startOffset);
@@ -1251,6 +1305,39 @@ if (typeof jQuery === 'undefined') {
$.fn.textcomplete.ContentEditable = ContentEditable;
}(jQuery);
// 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';
// CKEditor adapter
// =======================
//
// Adapter for CKEditor, based on contenteditable elements.
function CKEditor (element, completer, option) {
this.initialize(element, completer, option);
}
$.extend(CKEditor.prototype, $.fn.textcomplete.ContentEditable.prototype, {
_bindEvents: function () {
var $this = this;
this.option.ckeditor_instance.on('key', function(event) {
var domEvent = event.data;
$this._onKeyup(domEvent);
if ($this.completer.dropdown.shown && $this._skipSearch(domEvent)) {
return false;
}
}, null, null, 1); // 1 = Priority = Important!
// we actually also need the native event, as the CKEditor one is happening to late
this.$el.on('keyup.' + this.id, $.proxy(this._onKeyup, this));
},
});
$.fn.textcomplete.CKEditor = CKEditor;
}(jQuery);
// The MIT License (MIT)
//
// Copyright (c) 2015 Jonathan Ong me@jongleberry.com