Commit cca53491 authored by Mario's avatar Mario
Browse files

update to textcomplete v 1.8.0

parent 5abe7d2d
The MIT License (MIT)
Copyright (c) 2013-2014 Yuku Takahashi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
......@@ -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,33 +143,47 @@ 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(); });
}
}
Completer._getDefaults = function () {
if (!Completer.DEFAULTS) {
Completer.DEFAULTS = {
appendTo: $('body'),
zIndex: '100'
};
// 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();
});
});
}
}
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
......
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment