|
|
|
|
@ -17,7 +17,7 @@
|
|
|
|
|
* Repository: https://github.com/yuku-t/jquery-textcomplete
|
|
|
|
|
* License: MIT (https://github.com/yuku-t/jquery-textcomplete/blob/master/LICENSE)
|
|
|
|
|
* Author: Yuku Takahashi
|
|
|
|
|
* Version: 1.3.4
|
|
|
|
|
* Version: 1.7.3
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
if (typeof jQuery === 'undefined') {
|
|
|
|
|
@ -137,10 +137,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) {
|
|
|
|
|
@ -148,32 +144,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
|
|
|
|
|
@ -185,12 +195,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;
|
|
|
|
|
@ -282,7 +306,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]; }
|
|
|
|
|
@ -400,7 +424,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',
|
|
|
|
|
@ -423,7 +447,7 @@ if (typeof jQuery === 'undefined') {
|
|
|
|
|
footer: null,
|
|
|
|
|
header: null,
|
|
|
|
|
id: null,
|
|
|
|
|
maxCount: 10,
|
|
|
|
|
maxCount: null,
|
|
|
|
|
placement: '',
|
|
|
|
|
shown: false,
|
|
|
|
|
data: [], // Shown zipped data.
|
|
|
|
|
@ -446,8 +470,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);
|
|
|
|
|
@ -481,7 +505,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;
|
|
|
|
|
}
|
|
|
|
|
@ -786,7 +810,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});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
@ -795,7 +822,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;
|
|
|
|
|
@ -1008,6 +1035,7 @@ if (typeof jQuery === 'undefined') {
|
|
|
|
|
case 13: // ENTER
|
|
|
|
|
case 40: // DOWN
|
|
|
|
|
case 38: // UP
|
|
|
|
|
case 27: // ESC
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (clickEvent.ctrlKey) switch (clickEvent.keyCode) {
|
|
|
|
|
@ -1041,12 +1069,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;
|
|
|
|
|
}
|
|
|
|
|
@ -1063,7 +1093,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()
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
@ -1112,12 +1143,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();
|
|
|
|
|
@ -1163,30 +1196,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(/ $/, " "); //   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) {
|
|
|
|
|
@ -1219,8 +1257,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();
|
|
|
|
|
@ -1229,6 +1267,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;
|
|
|
|
|
},
|
|
|
|
|
@ -1242,7 +1291,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);
|
|
|
|
|
@ -1252,6 +1301,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
|
|
|
|
|
|