]> git.mxchange.org Git - friendica.git/blob - js/autocomplete.js
rework autocomplete: add bbcode autocompletion to editor
[friendica.git] / js / autocomplete.js
1 /**
2  * @brief Friendica people autocomplete
3  *
4  * require jQuery, jquery.textcomplete
5  * 
6  * for further documentation look at:
7  * http://yuku-t.com/jquery-textcomplete/
8  * 
9  * https://github.com/yuku-t/jquery-textcomplete/blob/master/doc/how_to_use.md
10  */
11
12
13 function contact_search(term, callback, backend_url, type) {
14
15         // Check if there is a conversation id to include the unkonwn contacts of the conversation
16         var conv_id = document.activeElement.id.match(/\d+$/);
17
18
19         // Check if there is a cached result that contains the same information we would get with a full server-side search
20         var bt = backend_url+type;
21         if(!(bt in contact_search.cache)) contact_search.cache[bt] = {};
22
23         var lterm = term.toLowerCase(); // Ignore case
24         for(var t in contact_search.cache[bt]) {
25                 if(lterm.indexOf(t) >= 0) { // A more broad search has been performed already, so use those results
26                         // Filter old results locally
27                         var matching = contact_search.cache[bt][t].filter(function (x) { return (x.name.toLowerCase().indexOf(lterm) >= 0 || (typeof x.nick !== 'undefined' && x.nick.toLowerCase().indexOf(lterm) >= 0)); }); // Need to check that nick exists because groups don't have one
28                         matching.unshift({taggable:false, text: term, replace: term});
29                         setTimeout(function() { callback(matching); } , 1); // Use "pseudo-thread" to avoid some problems
30                         return;
31                 }
32         }
33
34         var postdata = {
35                 start:0,
36                 count:100,
37                 search:term,
38                 type:type,
39         };
40
41         if(conv_id !== null)
42                 postdata['conversation'] = conv_id[0];
43
44
45         $.ajax({
46                 type:'POST',
47                 url: backend_url,
48                 data: postdata,
49                 dataType: 'json',
50                 success: function(data){
51                         // Cache results if we got them all (more information would not improve results)
52                         // data.count represents the maximum number of items
53                         if(data.items.length -1 < data.count) {
54                                 contact_search.cache[bt][lterm] = data.items;
55                         }
56                         var items = data.items.slice(0);
57                         items.unshift({taggable:false, text: term, replace: term});
58                         callback(items);
59                 },
60         }).fail(function () {callback([]); }); // Callback must be invoked even if something went wrong.
61 }
62 contact_search.cache = {};
63
64
65 function contact_format(item) {
66         // Show contact information if not explicitly told to show something else
67         if(typeof item.text === 'undefined') {
68                 var desc = ((item.label) ? item.nick + ' ' + item.label : item.nick);
69                 if(typeof desc === 'undefined') desc = '';
70                 if(desc) desc = ' ('+desc+')';
71                 return "<div class='{0}' title='{4}'><img class='acpopup-img' src='{1}'><span class='acpopup-contactname'>{2}</span><span class='acpopup-sub-text'>{3}</span><div class='clear'></div></div>".format(item.taggable, item.photo, item.name, desc, item.link);
72         }
73         else
74                 return "<div>" + item.text + "</div>";
75 }
76
77 function editor_replace(item) {
78         if(typeof item.replace !== 'undefined') {
79                 return '$1$2' + item.replace;
80         }
81
82         // $2 ensures that prefix (@,@!) is preserved
83         var id = item.id;
84          // 16 chars of hash should be enough. Full hash could be used if it can be done in a visually appealing way.
85         // 16 chars is also the minimum length in the backend (otherwise it's interpreted as a local id).
86         if(id.length > 16) 
87                 id = item.id.substring(0,16);
88
89         return '$1$2' + item.nick.replace(' ', '') + '+' + id + ' ';
90 }
91
92 function basic_replace(item) {
93         if(typeof item.replace !== 'undefined')
94                 return '$1'+item.replace;
95
96         return '$1'+item.name+' ';
97 }
98
99 function trim_replace(item) {
100         if(typeof item.replace !== 'undefined')
101                 return '$1'+item.replace;
102
103         return '$1'+item.name;
104 }
105
106
107 function submit_form(e) {
108         $(e).parents('form').submit();
109 }
110
111 /**
112  * jQuery plugin 'editor_autocomplete'
113  */
114 (function( $ ) {
115         $.fn.editor_autocomplete = function(backend_url) {
116
117                 // list of supported bbtags
118                 var bbelements = ['b', 'u', 'i', 'img', 'url', 'quote', 'code', 'spoiler', 'audio', 'video', 'youtube', 'map', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 's', 'o', 'list', 'center', 'nosmile', 'vimeo' ];
119
120                 // Autocomplete contacts
121                 contacts = {
122                         match: /(^|\s)(@\!*)([^ \n]+)$/,
123                         index: 3,
124                         search: function(term, callback) { contact_search(term, callback, backend_url, 'c'); },
125                         replace: editor_replace,
126                         template: contact_format,
127                 };
128
129                 smilies = {
130                         match: /(^|\s)(:[a-z]{2,})$/,
131                         index: 2,
132                         search: function(term, callback) { $.getJSON('smilies/json').done(function(data) { callback($.map(data, function(entry) { return entry.text.indexOf(term) === 0 ? entry : null; })); }); },
133                         template: function(item) { return item.icon + ' ' + item.text; },
134                         replace: function(item) { return "$1" + item.text + ' '; },
135                 };
136
137                 bbtags = {
138                         match: /\[(\w*)$/,
139                         index: 1,
140                         search: function (term, callback) { callback($.map(bbelements, function (element) { return element.indexOf(term) === 0 ? element : null; })); },
141                         replace: function (element) { return ['[' + element + ']', '[/' + element + ']']; },
142                 };
143                 this.attr('autocomplete','off');
144                 this.textcomplete([contacts,smilies, bbtags], {className:'acpopup', zIndex:1020});
145         };
146 })( jQuery );
147
148 /**
149  * jQuery plugin 'search_autocomplete'
150  */
151 (function( $ ) {
152         $.fn.search_autocomplete = function(backend_url) {
153                 // Autocomplete contacts
154                 contacts = {
155                         match: /(^@)([^\n]{2,})$/,
156                         index: 2,
157                         search: function(term, callback) { contact_search(term, callback, backend_url, 'x'); },
158                         replace: basic_replace,
159                         template: contact_format,
160                 };
161                 this.attr('autocomplete', 'off');
162                 var a = this.textcomplete([contacts], {className:'acpopup', maxCount:100, zIndex: 1020, appendTo:'nav'});
163                 a.on('textComplete:select', function(e, value, strategy) { submit_form(this); });
164         };
165 })( jQuery );
166
167 (function( $ ) {
168         $.fn.contact_autocomplete = function(backend_url, typ, autosubmit, onselect) {
169                 if(typeof typ === 'undefined') typ = '';
170                 if(typeof autosubmit === 'undefined') autosubmit = false;
171
172                 // Autocomplete contacts
173                 contacts = {
174                         match: /(^)([^\n]+)$/,
175                         index: 2,
176                         search: function(term, callback) { contact_search(term, callback, backend_url, typ); },
177                         replace: basic_replace,
178                         template: contact_format,
179                 };
180
181                 this.attr('autocomplete','off');
182                 var a = this.textcomplete([contacts], {className:'acpopup', zIndex:1020});
183
184                 if(autosubmit)
185                         a.on('textComplete:select', function(e,value,strategy) { submit_form(this); });
186
187                 if(typeof onselect !== 'undefined')
188                         a.on('textComplete:select', function(e, value, strategy) { onselect(value); });
189         };
190 })( jQuery );
191
192
193 (function( $ ) {
194         $.fn.name_autocomplete = function(backend_url, typ, autosubmit, onselect) {
195                 if(typeof typ === 'undefined') typ = '';
196                 if(typeof autosubmit === 'undefined') autosubmit = false;
197
198                 // Autocomplete contacts
199                 names = {
200                         match: /(^)([^\n]+)$/,
201                         index: 2,
202                         search: function(term, callback) { contact_search(term, callback, backend_url, typ); },
203                         replace: trim_replace,
204                         template: contact_format,
205                 };
206
207                 this.attr('autocomplete','off');
208                 var a = this.textcomplete([names], {className:'acpopup', zIndex:1020});
209
210                 if(autosubmit)
211                         a.on('textComplete:select', function(e,value,strategy) { submit_form(this); });
212
213                 if(typeof onselect !== 'undefined')
214                         a.on('textComplete:select', function(e, value, strategy) { onselect(value); });
215         };
216 })( jQuery );
217
218
219 /**
220  * Friendica people autocomplete legacy
221  * code which is needed for tinymce
222  *
223  * require jQuery, jquery.textareas
224  */
225
226 function ACPopup(elm,backend_url){
227         this.idsel=-1;
228         this.element = elm;
229         this.searchText="";
230         this.ready=true;
231         this.kp_timer = false;
232         this.url = backend_url;
233
234         this.conversation_id = null;
235         var conv_id = this.element.id.match(/\d+$/);
236         if (conv_id) this.conversation_id = conv_id[0];
237         console.log("ACPopup elm id",this.element.id,"conversation",this.conversation_id);
238
239         var w = 530;
240         var h = 130;
241
242
243         if(tinyMCE.activeEditor == null) {
244                 style = $(elm).offset();
245                 w = $(elm).width();
246                 h = $(elm).height();
247         }
248         else {
249                 // I can't find an "official" way to get the element who get all
250                 // this fraking thing that is tinyMCE.
251                 // This code will broke again at some point...
252                 var container = $(tinyMCE.activeEditor.getContainer()).find("table");
253                 style = $(container).offset();
254                 w = $(container).width();
255                 h = $(container).height();
256         }
257
258         style.top=style.top+h;
259         style.width = w;
260         style.position = 'absolute';
261         /*      style['max-height'] = '150px';
262                 style.border = '1px solid red';
263                 style.background = '#cccccc';
264
265                 style.overflow = 'auto';
266                 style['z-index'] = '100000';
267         */
268         style.display = 'none';
269
270         this.cont = $("<div class='acpopup-mce'></div>");
271         this.cont.css(style);
272
273         $("body").append(this.cont);
274     }
275
276 ACPopup.prototype.close = function(){
277         $(this.cont).remove();
278         this.ready=false;
279 }
280 ACPopup.prototype.search = function(text){
281         var that = this;
282         this.searchText=text;
283         if (this.kp_timer) clearTimeout(this.kp_timer);
284         this.kp_timer = setTimeout( function(){that._search();}, 500);
285 }
286
287 ACPopup.prototype._search = function(){
288         console.log("_search");
289         var that = this;
290         var postdata = {
291                 start:0,
292                 count:100,
293                 search:this.searchText,
294                 type:'c',
295                 conversation: this.conversation_id,
296         }
297
298         $.ajax({
299                 type:'POST',
300                 url: this.url,
301                 data: postdata,
302                 dataType: 'json',
303                 success:function(data){
304                         that.cont.html("");
305                         if (data.tot>0){
306                                 that.cont.show();
307                                 $(data.items).each(function(){
308                                         var html = "<img src='{0}' height='16px' width='16px'>{1} ({2})".format(this.photo, this.name, this.nick);
309                                         var nick = this.nick.replace(' ','');
310                                         if (this.id!=='')  nick += '+' + this.id;
311                                         that.add(html, nick + ' - ' + this.link);
312                                 });
313                         } else {
314                                 that.cont.hide();
315                         }
316                 }
317         });
318
319 }
320
321 ACPopup.prototype.add = function(label, value){
322         var that=this;
323         var elm = $("<div class='acpopupitem' title='"+value+"'>"+label+"</div>");
324         elm.click(function(e){
325                 t = $(this).attr('title').replace(new RegExp(' \- .*'),'');
326                 if(typeof(that.element.container) === "undefined") {
327                         el=$(that.element);
328                         sel = el.getSelection();
329                         sel.start = sel.start- that.searchText.length;
330                         el.setSelection(sel.start,sel.end).replaceSelectedText(t+' ').collapseSelection(false);
331                         that.close();
332                 }
333                 else {
334                         txt = tinyMCE.activeEditor.getContent();
335                         //                      alert(that.searchText + ':' + t);
336                         newtxt = txt.replace('@' + that.searchText,'@' + t +' ');
337                         tinyMCE.activeEditor.setContent(newtxt);
338                         tinyMCE.activeEditor.focus();
339                         that.close();
340                 }
341         });
342         $(this.cont).append(elm);
343 }
344
345 ACPopup.prototype.onkey = function(event){
346         if (event.keyCode == '13') {
347                 if(this.idsel>-1) {
348                         this.cont.children()[this.idsel].click();
349                         event.preventDefault();
350                 }
351                 else
352                         this.close();
353         }
354         if (event.keyCode == '38') { //cursor up
355                 cmax = this.cont.children().size()-1;
356                 this.idsel--;
357                 if (this.idsel<0) this.idsel=cmax;
358                 event.preventDefault();
359         }
360         if (event.keyCode == '40' || event.keyCode == '9') { //cursor down
361                 cmax = this.cont.children().size()-1;
362                 this.idsel++;
363                 if (this.idsel>cmax) this.idsel=0;
364                 event.preventDefault();
365         }
366
367         if (event.keyCode == '38' || event.keyCode == '40' || event.keyCode == '9') {
368                 this.cont.children().removeClass('selected');
369                 $(this.cont.children()[this.idsel]).addClass('selected');
370         }
371
372         if (event.keyCode == '27') { //ESC
373                 this.close();
374         }
375 }
376