2019-02-12 11:29:50 +01:00
// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat
2016-01-19 18:11:40 +01:00
/ * *
2020-01-19 07:05:23 +01:00
* Friendica people autocomplete
2016-01-19 18:11:40 +01:00
*
* require jQuery , jquery . textcomplete
2017-01-27 04:50:27 +01:00
*
2016-02-01 18:22:26 +01:00
* for further documentation look at :
* http : //yuku-t.com/jquery-textcomplete/
2017-01-27 04:50:27 +01:00
*
2016-02-01 18:22:26 +01:00
* https : //github.com/yuku-t/jquery-textcomplete/blob/master/doc/how_to_use.md
2016-01-19 18:11:40 +01:00
* /
2016-02-01 18:22:26 +01:00
2016-02-02 22:33:14 +01:00
function contact _search ( term , callback , backend _url , type , mode ) {
2016-01-19 18:11:40 +01:00
2023-03-22 05:08:47 +01:00
// Check if there is a conversation id to include the unknown contacts of the conversation
2016-01-21 13:28:29 +01:00
var conv _id = document . activeElement . id . match ( /\d+$/ ) ;
2016-01-19 18:11:40 +01:00
// Check if there is a cached result that contains the same information we would get with a full server-side search
var bt = backend _url + type ;
if ( ! ( bt in contact _search . cache ) ) contact _search . cache [ bt ] = { } ;
var lterm = term . toLowerCase ( ) ; // Ignore case
for ( var t in contact _search . cache [ bt ] ) {
if ( lterm . indexOf ( t ) >= 0 ) { // A more broad search has been performed already, so use those results
// Filter old results locally
2023-05-14 01:54:35 +02:00
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 circles don't have one
2023-05-30 15:15:17 +02:00
matching . unshift ( { group : false , text : term , replace : term } ) ;
2016-01-19 18:11:40 +01:00
setTimeout ( function ( ) { callback ( matching ) ; } , 1 ) ; // Use "pseudo-thread" to avoid some problems
return ;
}
}
var postdata = {
start : 0 ,
count : 100 ,
search : term ,
type : type ,
} ;
2016-01-21 13:28:29 +01:00
if ( conv _id !== null )
postdata [ 'conversation' ] = conv _id [ 0 ] ;
2016-02-02 22:33:14 +01:00
if ( mode !== null )
2016-07-11 10:33:39 +02:00
postdata [ 'smode' ] = mode ;
2016-02-02 22:33:14 +01:00
2016-01-19 18:11:40 +01:00
$ . ajax ( {
type : 'POST' ,
url : backend _url ,
data : postdata ,
dataType : 'json' ,
success : function ( data ) {
// Cache results if we got them all (more information would not improve results)
// data.count represents the maximum number of items
if ( data . items . length - 1 < data . count ) {
contact _search . cache [ bt ] [ lterm ] = data . items ;
}
var items = data . items . slice ( 0 ) ;
items . unshift ( { taggable : false , text : term , replace : term } ) ;
callback ( items ) ;
} ,
} ) . fail ( function ( ) { callback ( [ ] ) ; } ) ; // Callback must be invoked even if something went wrong.
}
contact _search . cache = { } ;
function contact _format ( item ) {
// Show contact information if not explicitly told to show something else
if ( typeof item . text === 'undefined' ) {
var desc = ( ( item . label ) ? item . nick + ' ' + item . label : item . nick ) ;
2023-05-30 15:15:17 +02:00
var group = ( ( item . group ) ? 'group' : '' ) ;
2016-01-19 18:11:40 +01:00
if ( typeof desc === 'undefined' ) desc = '' ;
if ( desc ) desc = ' (' + desc + ')' ;
2023-05-30 15:15:17 +02:00
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 ( group , item . photo , item . name , desc , item . link ) ;
2016-01-19 18:11:40 +01:00
}
else
return "<div>" + item . text + "</div>" ;
}
2018-03-16 13:55:26 +01:00
function tag _format ( item ) {
return "<div class='dropdown-item'>" + '#' + item . text + "</div>" ;
}
2016-01-19 18:11:40 +01:00
function editor _replace ( item ) {
2017-05-05 13:55:01 +02:00
if ( typeof item . replace !== 'undefined' ) {
2016-01-19 18:11:40 +01:00
return '$1$2' + item . replace ;
}
2017-05-05 13:55:01 +02:00
if ( typeof item . addr !== 'undefined' ) {
return '$1$2' + item . addr + ' ' ;
}
2016-01-19 18:11:40 +01:00
// $2 ensures that prefix (@,@!) is preserved
var id = item . id ;
2016-04-16 15:37:34 +02:00
// don't add the id if it is empty (the id empty eg. if there are unknow contacts in thread)
2017-05-05 13:55:01 +02:00
if ( id . length < 1 ) {
2016-04-16 15:58:11 +02:00
return '$1$2' + item . nick . replace ( ' ' , '' ) + ' ' ;
2017-05-05 13:55:01 +02:00
}
2016-04-16 15:37:34 +02:00
// 16 chars of hash should be enough. Full hash could be used if it can be done in a visually appealing way.
2016-01-19 18:11:40 +01:00
// 16 chars is also the minimum length in the backend (otherwise it's interpreted as a local id).
2017-05-05 13:55:01 +02:00
if ( id . length > 16 ) {
2016-01-19 18:11:40 +01:00
id = item . id . substring ( 0 , 16 ) ;
2017-05-05 13:55:01 +02:00
}
2016-01-19 18:11:40 +01:00
return '$1$2' + item . nick . replace ( ' ' , '' ) + '+' + id + ' ' ;
}
function basic _replace ( item ) {
if ( typeof item . replace !== 'undefined' )
return '$1' + item . replace ;
return '$1' + item . name + ' ' ;
}
2016-05-01 08:45:11 +02:00
function webbie _replace ( item ) {
if ( typeof item . replace !== 'undefined' )
return '$1' + item . replace ;
return '$1' + item . nick + ' ' ;
}
2016-01-19 18:11:40 +01:00
function trim _replace ( item ) {
if ( typeof item . replace !== 'undefined' )
return '$1' + item . replace ;
return '$1' + item . name ;
}
function submit _form ( e ) {
$ ( e ) . parents ( 'form' ) . submit ( ) ;
}
2016-04-14 23:59:29 +02:00
function getWord ( text , caretPos ) {
var index = text . indexOf ( caretPos ) ;
var postText = text . substring ( caretPos , caretPos + 8 ) ;
if ( ( postText . indexOf ( "[/list]" ) > 0 ) || postText . indexOf ( "[/ul]" ) > 0 || postText . indexOf ( "[/ol]" ) > 0 ) {
return postText ;
}
}
function getCaretPosition ( ctrl ) {
var CaretPos = 0 ; // IE Support
if ( document . selection ) {
ctrl . focus ( ) ;
var Sel = document . selection . createRange ( ) ;
Sel . moveStart ( 'character' , - ctrl . value . length ) ;
CaretPos = Sel . text . length ;
}
// Firefox support
else if ( ctrl . selectionStart || ctrl . selectionStart == '0' )
CaretPos = ctrl . selectionStart ;
return ( CaretPos ) ;
}
function setCaretPosition ( ctrl , pos ) {
if ( ctrl . setSelectionRange ) {
ctrl . focus ( ) ;
ctrl . setSelectionRange ( pos , pos ) ;
}
else if ( ctrl . createTextRange ) {
var range = ctrl . createTextRange ( ) ;
range . collapse ( true ) ;
range . moveEnd ( 'character' , pos ) ;
range . moveStart ( 'character' , pos ) ;
range . select ( ) ;
}
}
function listNewLineAutocomplete ( id ) {
var text = document . getElementById ( id ) ;
var caretPos = getCaretPosition ( text )
var word = getWord ( text . value , caretPos ) ;
if ( word != null ) {
var textBefore = text . value . substring ( 0 , caretPos ) ;
var textAfter = text . value . substring ( caretPos , text . length ) ;
2017-04-19 04:04:23 +02:00
$ ( '#' + id ) . val ( textBefore + '\r\n[*] ' + textAfter ) . trigger ( 'change' ) ;
2016-04-14 23:59:29 +02:00
setCaretPosition ( text , caretPos + 5 ) ;
return true ;
}
2016-04-29 14:42:12 +02:00
else {
return false ;
}
2016-04-14 23:59:29 +02:00
}
function string2bb ( element ) {
if ( element == 'bold' ) return 'b' ;
else if ( element == 'italic' ) return 'i' ;
else if ( element == 'underline' ) return 'u' ;
else if ( element == 'overline' ) return 'o' ;
else if ( element == 'strike' ) return 's' ;
else return element ;
}
2016-01-19 18:11:40 +01:00
/ * *
* jQuery plugin 'editor_autocomplete'
* /
( function ( $ ) {
2020-07-22 16:48:02 +02:00
let textcompleteObjects = [ ] ;
// jQuery wrapper for yuku/old-textcomplete
// uses a local object directory to avoid recreating Textcomplete objects
$ . fn . textcomplete = function ( strategies , options ) {
2020-08-08 18:58:56 +02:00
return this . each ( function ( ) {
let $this = $ ( this ) ;
if ( ! ( $this . data ( 'textcompleteId' ) in textcompleteObjects ) ) {
let editor = new Textcomplete . editors . Textarea ( $this . get ( 0 ) ) ;
2020-07-22 16:48:02 +02:00
2020-08-08 18:58:56 +02:00
$this . data ( 'textcompleteId' , textcompleteObjects . length ) ;
textcompleteObjects . push ( new Textcomplete ( editor , options ) ) ;
}
2020-07-22 16:48:02 +02:00
2020-08-08 18:58:56 +02:00
textcompleteObjects [ $this . data ( 'textcompleteId' ) ] . register ( strategies ) ;
} ) ;
2020-07-22 16:48:02 +02:00
} ;
2020-06-23 04:25:15 +02:00
/ * *
* This function should be called immediately after $ . textcomplete ( ) to prevent the escape key press to propagate
* after the autocompletion dropdown has closed .
* This avoids the input textarea to lose focus , the modal window to close , etc ... when the expected behavior is
* to just close the autocomplete dropdown .
*
* The custom event listener name allows removing this specific event listener , the "real" event this listens to
* is the part before the first dot .
*
* @ returns { * }
* /
$ . fn . fixTextcompleteEscape = function ( ) {
if ( this . data ( 'textcompleteEscapeFixed' ) ) {
return this ;
}
this . data ( 'textcompleteEscapeFixed' , true ) ;
return this . on ( {
'textComplete:show' : function ( e ) {
$ ( this ) . on ( 'keydown.friendica.escape' , function ( e ) {
if ( e . key === 'Escape' ) {
e . stopPropagation ( ) ;
}
} ) ;
} ,
'textComplete:hide' : function ( e ) {
$ ( this ) . off ( 'keydown.friendica.escape' ) ;
} ,
} ) ;
}
2016-01-19 18:11:40 +01:00
$ . fn . editor _autocomplete = function ( backend _url ) {
// Autocomplete contacts
contacts = {
match : /(^|\s)(@\!*)([^ \n]+)$/ ,
index : 3 ,
search : function ( term , callback ) { contact _search ( term , callback , backend _url , 'c' ) ; } ,
replace : editor _replace ,
template : contact _format ,
} ;
2023-05-30 15:15:17 +02:00
// Autocomplete groups
groups = {
2017-10-31 20:33:23 +01:00
match : /(^|\s)(!\!*)([^ \n]+)$/ ,
index : 3 ,
search : function ( term , callback ) { contact _search ( term , callback , backend _url , 'f' ) ; } ,
replace : editor _replace ,
template : contact _format ,
} ;
2018-03-16 13:55:26 +01:00
// Autocomplete hashtags
tags = {
match : /(^|\s)(\#)([^ \n]{2,})$/ ,
index : 3 ,
2018-03-29 06:47:27 +02:00
search : function ( term , callback ) {
2020-01-13 21:10:13 +01:00
$ . getJSON ( baseurl + '/hashtag/' + '?t=' + term )
2018-03-29 06:47:27 +02:00
. done ( function ( data ) {
callback ( $ . map ( data , function ( entry ) {
// .toLowerCase() enables case-insensitive search
return entry . text . toLowerCase ( ) . indexOf ( term . toLowerCase ( ) ) === 0 ? entry : null ;
} ) ) ;
} ) ;
} ,
2018-03-16 13:55:26 +01:00
replace : function ( item ) { return "$1$2" + item . text + ' ' ; } ,
template : tag _format
} ;
2016-02-02 22:33:14 +01:00
// Autocomplete smilies e.g. ":like"
2016-01-19 18:11:40 +01:00
smilies = {
match : /(^|\s)(:[a-z]{2,})$/ ,
index : 2 ,
2016-02-01 18:22:26 +01:00
search : function ( term , callback ) { $ . getJSON ( 'smilies/json' ) . done ( function ( data ) { callback ( $ . map ( data , function ( entry ) { return entry . text . indexOf ( term ) === 0 ? entry : null ; } ) ) ; } ) ; } ,
template : function ( item ) { return item . icon + ' ' + item . text ; } ,
2016-01-19 18:11:40 +01:00
replace : function ( item ) { return "$1" + item . text + ' ' ; } ,
} ;
2016-02-01 18:22:26 +01:00
2016-01-19 18:11:40 +01:00
this . attr ( 'autocomplete' , 'off' ) ;
2023-05-30 15:15:17 +02:00
this . textcomplete ( [ contacts , groups , smilies , tags ] , { dropdown : { className : 'acpopup' } } ) ;
2020-06-23 04:25:15 +02:00
this . fixTextcompleteEscape ( ) ;
2020-07-22 16:48:02 +02:00
return this ;
2016-01-19 18:11:40 +01:00
} ;
$ . fn . search _autocomplete = function ( backend _url ) {
// Autocomplete contacts
contacts = {
match : /(^@)([^\n]{2,})$/ ,
index : 2 ,
2016-02-02 22:33:14 +01:00
search : function ( term , callback ) { contact _search ( term , callback , backend _url , 'x' , 'contact' ) ; } ,
2016-05-01 08:45:11 +02:00
replace : webbie _replace ,
2016-02-02 22:33:14 +01:00
template : contact _format ,
} ;
2023-05-30 15:15:17 +02:00
// Autocomplete group accounts
2016-02-02 22:33:14 +01:00
community = {
match : /(^!)([^\n]{2,})$/ ,
index : 2 ,
search : function ( term , callback ) { contact _search ( term , callback , backend _url , 'x' , 'community' ) ; } ,
2016-05-01 08:45:11 +02:00
replace : webbie _replace ,
2016-01-19 18:11:40 +01:00
template : contact _format ,
} ;
2018-03-16 13:55:26 +01:00
// Autocomplete hashtags
tags = {
match : /(^|\s)(\#)([^ \n]{2,})$/ ,
index : 3 ,
2020-01-13 21:10:13 +01:00
search : function ( term , callback ) { $ . getJSON ( baseurl + '/hashtag/' + '?t=' + term ) . done ( function ( data ) { callback ( $ . map ( data , function ( entry ) { return entry . text . indexOf ( term ) === 0 ? entry : null ; } ) ) ; } ) ; } ,
2018-03-16 13:55:26 +01:00
replace : function ( item ) { return "$1$2" + item . text ; } ,
template : tag _format
} ;
2016-01-19 18:11:40 +01:00
this . attr ( 'autocomplete' , 'off' ) ;
2020-08-08 21:19:04 +02:00
this . textcomplete ( [ contacts , community , tags ] , { dropdown : { className : 'acpopup' , maxCount : 100 } } ) ;
2020-06-23 04:25:15 +02:00
this . fixTextcompleteEscape ( ) ;
this . on ( 'textComplete:select' , function ( e , value , strategy ) { submit _form ( this ) ; } ) ;
2020-07-22 16:48:02 +02:00
return this ;
2016-01-19 18:11:40 +01:00
} ;
2019-09-03 21:30:02 +02:00
$ . fn . name _autocomplete = function ( backend _url , typ , autosubmit , onselect ) {
if ( typeof typ === 'undefined' ) typ = '' ;
if ( typeof autosubmit === 'undefined' ) autosubmit = false ;
// Autocomplete contacts
names = {
match : /(^)([^\n]+)$/ ,
index : 2 ,
search : function ( term , callback ) { contact _search ( term , callback , backend _url , typ ) ; } ,
replace : trim _replace ,
template : contact _format ,
} ;
this . attr ( 'autocomplete' , 'off' ) ;
2020-08-08 21:19:04 +02:00
this . textcomplete ( [ names ] , { dropdown : { className : 'acpopup' } } ) ;
2020-06-23 04:25:15 +02:00
this . fixTextcompleteEscape ( ) ;
2019-09-03 21:30:02 +02:00
2020-06-23 04:25:15 +02:00
if ( autosubmit ) {
this . on ( 'textComplete:select' , function ( e , value , strategy ) { submit _form ( this ) ; } ) ;
}
2019-09-03 21:30:02 +02:00
2020-06-23 04:25:15 +02:00
if ( typeof onselect !== 'undefined' ) {
this . on ( 'textComplete:select' , function ( e , value , strategy ) { onselect ( value ) ; } ) ;
}
2020-07-22 16:48:02 +02:00
return this ;
2019-09-03 21:30:02 +02:00
} ;
2016-04-14 23:59:29 +02:00
$ . fn . bbco _autocomplete = function ( type ) {
2020-06-23 04:25:15 +02:00
if ( type === 'bbcode' ) {
2022-10-07 07:54:17 +02:00
var open _close _elements = [ 'bold' , 'italic' , 'underline' , 'overline' , 'strike' , 'quote' , 'code' , 'spoiler' , 'map' , 'img' , 'url' , 'audio' , 'video' , 'embed' , 'youtube' , 'vimeo' , 'list' , 'ul' , 'ol' , 'li' , 'table' , 'tr' , 'th' , 'td' , 'center' , 'color' , 'font' , 'size' , 'h1' , 'h2' , 'h3' , 'h4' , 'h5' , 'h6' , 'nobb' , 'noparse' , 'pre' , 'abstract' , 'share' ] ;
2016-04-14 23:59:29 +02:00
var open _elements = [ '*' , 'hr' ] ;
var elements = open _close _elements . concat ( open _elements ) ;
}
bbco = {
match : /\[(\w*\**)$/ ,
search : function ( term , callback ) {
callback ( $ . map ( elements , function ( element ) {
return element . indexOf ( term ) === 0 ? element : null ;
} ) ) ;
} ,
index : 1 ,
replace : function ( element ) {
element = string2bb ( element ) ;
if ( open _elements . indexOf ( element ) < 0 ) {
if ( element === 'list' || element === 'ol' || element === 'ul' ) {
return [ '\[' + element + '\]' + '\n\[*\] ' , '\n\[/' + element + '\]' ] ;
}
else if ( element === 'table' ) {
return [ '\[' + element + '\]' + '\n\[tr\]' , '\[/tr\]\n\[/' + element + '\]' ] ;
}
else {
return [ '\[' + element + '\]' , '\[/' + element + '\]' ] ;
}
}
else {
return '\[' + element + '\] ' ;
}
}
} ;
this . attr ( 'autocomplete' , 'off' ) ;
2020-08-08 21:19:04 +02:00
this . textcomplete ( [ bbco ] , { dropdown : { className : 'acpopup' } } ) ;
2020-06-23 04:25:15 +02:00
this . fixTextcompleteEscape ( ) ;
2016-04-14 23:59:29 +02:00
2020-06-23 04:25:15 +02:00
this . on ( 'textComplete:select' , function ( e , value , strategy ) { value ; } ) ;
2016-04-14 23:59:29 +02:00
2020-06-23 04:25:15 +02:00
this . keypress ( function ( e ) {
2016-04-14 23:59:29 +02:00
if ( e . keyCode == 13 ) {
var x = listNewLineAutocomplete ( this . id ) ;
2016-04-29 14:42:12 +02:00
if ( x ) {
e . stopImmediatePropagation ( ) ;
2016-04-14 23:59:29 +02:00
e . preventDefault ( ) ;
2016-04-29 14:42:12 +02:00
}
2016-04-14 23:59:29 +02:00
}
} ) ;
2020-07-22 16:48:02 +02:00
return this ;
2016-04-14 23:59:29 +02:00
} ;
} ) ( jQuery ) ;
2019-02-12 11:29:50 +01:00
// @license-end