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