From cca5349110b06a939d035b6c94e88aafcd3d9cba Mon Sep 17 00:00:00 2001 From: Mario Vavti Date: Thu, 30 Mar 2017 12:21:15 +0200 Subject: update to textcomplete v 1.8.0 --- library/jquery-textcomplete/jquery.textcomplete.js | 157 ++++++++++++++++----- 1 file changed, 122 insertions(+), 35 deletions(-) (limited to 'library/jquery-textcomplete/jquery.textcomplete.js') diff --git a/library/jquery-textcomplete/jquery.textcomplete.js b/library/jquery-textcomplete/jquery.textcomplete.js index 95e75149c..0dd9fd827 100644 --- a/library/jquery-textcomplete/jquery.textcomplete.js +++ b/library/jquery-textcomplete/jquery.textcomplete.js @@ -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 = $('') - .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(/ $/, " "); //   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 'hello 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 -- cgit v1.2.3