Skip to:
Content

BuddyPress.org

Ticket #3278: 3278.01.patch

File 3278.01.patch, 60.6 KB (added by DJPaul, 7 years ago)
Line 
1diff --git a/Gruntfile.js b/Gruntfile.js
2index ea6b69b..bb49281 100644
3--- a/Gruntfile.js
4+++ b/Gruntfile.js
5@@ -6,6 +6,7 @@ module.exports = function( grunt ) {
6        BUILD_DIR  = 'build/',
7 
8        BP_CSS = [
9+               'bp-activity/css/*.css',
10                'bp-activity/admin/css/*.css',
11                'bp-core/admin/css/*.css',
12                'bp-core/css/*.css',
13@@ -18,6 +19,7 @@ module.exports = function( grunt ) {
14        ],
15 
16        BP_JS = [
17+               'bp-activity/js/*.js',
18                'bp-activity/admin/js/*.js',
19                'bp-core/js/*.js',
20                'bp-friends/js/*.js',
21@@ -30,6 +32,8 @@ module.exports = function( grunt ) {
22        ],
23 
24        BP_EXCLUDED_JS = [
25+               '!bp-core/js/jquery.atwho.js',
26+               '!bp-core/js/jquery.caret.js',
27                '!bp-templates/bp-legacy/js/*.js'
28        ];
29 
30diff --git a/src/bp-activity/bp-activity-actions.php b/src/bp-activity/bp-activity-actions.php
31index d8e4c23..640a585 100644
32--- a/src/bp-activity/bp-activity-actions.php
33+++ b/src/bp-activity/bp-activity-actions.php
34@@ -647,3 +647,28 @@ function bp_activity_setup_akismet() {
35        // Instantiate Akismet for BuddyPress
36        $bp->activity->akismet = new BP_Akismet();
37 }
38+
39+/**
40+ * AJAX endpoint for Suggestions API lookups.
41+ *
42+ * @since BuddyPress (2.1.0)
43+ */
44+function bp_ajax_get_suggestions() {
45+       if ( ! bp_is_user_active() || empty( $_GET['term'] ) || empty( $_GET['type'] ) ) {
46+               wp_send_json_error( 'missing_parameter' );
47+               exit;
48+       }
49+
50+       $results = bp_core_get_suggestions( array(
51+               'term' => sanitize_text_field( $_GET['term'] ),
52+               'type' => sanitize_text_field( $_GET['type'] ),
53+       ) );
54+
55+       if ( is_wp_error( $results ) ) {
56+               wp_send_json_error( $results->get_error_message() );
57+               exit;
58+       }
59+
60+       wp_send_json_success( $results );
61+}
62+add_action( 'wp_ajax_bp_get_suggestions', 'bp_ajax_get_suggestions' );
63diff --git a/src/bp-activity/bp-activity-cssjs.php b/src/bp-activity/bp-activity-cssjs.php
64new file mode 100644
65index 0000000..f4fa7f3
66--- /dev/null
67+++ b/src/bp-activity/bp-activity-cssjs.php
68@@ -0,0 +1,61 @@
69+<?php
70+
71+/**
72+ * Activity component CSS/JS
73+ *
74+ * @package BuddyPress
75+ * @subpackage ActivityScripts
76+ */
77+
78+// Exit if accessed directly
79+if ( ! defined( 'ABSPATH' ) ) exit;
80+
81+/**
82+ * Enqueue @mentions JS.
83+ *
84+ * @since BuddyPress (2.1)
85+ */
86+function bp_activity_mentions_script() {
87+       if ( ! bp_activity_do_mentions() || ! bp_is_user_active() || ! ( bp_is_activity_component() || bp_is_blog_page() && is_singular() && comments_open() ) ) {
88+               return;
89+       }
90+
91+       $min  = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
92+       $file = is_rtl() ? "mentions-rtl{$min}.css" : "mentions{$min}.css";
93+
94+       wp_enqueue_script( 'bp-mentions', buddypress()->plugin_url . "bp-activity/js/mentions{$min}.js", array( 'jquery', 'jquery-atwho' ), bp_get_version(), true );
95+       wp_enqueue_style( 'bp-mentions-css', buddypress()->plugin_url . "bp-activity/css/{$file}", array(), bp_get_version() );
96+
97+       // Print a list of the current user's friends to the page for quicker @mentions lookups.
98+       do_action( 'bp_activity_mentions_prime_results' );
99+}
100+add_action( 'bp_enqueue_scripts', 'bp_activity_mentions_script' );
101+
102+/**
103+ * Enqueue @mentions JS in wp-admin.
104+ *
105+ * @since BuddyPress (2.1)
106+ */
107+function bp_activity_mentions_dashboard_script() {
108+       if ( ! bp_activity_do_mentions() || ! bp_is_user_active() || ! is_admin() ) {
109+               return;
110+       }
111+
112+       // Special handling for New/Edit screens in wp-admin
113+       if (
114+               ! get_current_screen() ||
115+               ! in_array( get_current_screen()->base, array( 'page', 'post' ) ) ||
116+               ! post_type_supports( get_current_screen()->post_type, 'editor' ) ) {
117+               return;
118+       }
119+
120+       $min  = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
121+       $file = is_rtl() ? "mentions-rtl{$min}.css" : "mentions{$min}.css";
122+
123+       wp_enqueue_script( 'bp-mentions', buddypress()->plugin_url . "bp-activity/js/mentions{$min}.js", array( 'jquery', 'jquery-atwho' ), bp_get_version(), true );
124+       wp_enqueue_style( 'bp-mentions-css', buddypress()->plugin_url . "bp-activity/css/{$file}", array(), bp_get_version() );
125+
126+       // Print a list of the current user's friends to the page for quicker @mentions lookups.
127+       do_action( 'bp_activity_mentions_prime_results' );
128+}
129+add_action( 'bp_admin_enqueue_scripts', 'bp_activity_mentions_dashboard_script' );
130\ No newline at end of file
131diff --git a/src/bp-activity/bp-activity-loader.php b/src/bp-activity/bp-activity-loader.php
132index c6fc540..bbf292d 100644
133--- a/src/bp-activity/bp-activity-loader.php
134+++ b/src/bp-activity/bp-activity-loader.php
135@@ -47,6 +47,7 @@ class BP_Activity_Component extends BP_Component {
136        public function includes( $includes = array() ) {
137                // Files to include
138                $includes = array(
139+                       'cssjs',
140                        'actions',
141                        'screens',
142                        'filters',
143diff --git a/src/bp-activity/css/mentions-rtl.css b/src/bp-activity/css/mentions-rtl.css
144new file mode 100644
145index 0000000..7185ed6
146--- /dev/null
147+++ b/src/bp-activity/css/mentions-rtl.css
148@@ -0,0 +1,78 @@
149+.atwho-view {
150+       background: #FFF;
151+       border-radius: 2px;
152+       border: 1px solid rgb(204, 204, 204);
153+       box-shadow: 0 0 5px rgba(204, 204, 204, 0.25), 0 0 1px #FFF;
154+       color: #D84800;
155+       display: none;
156+       font-family: sans-serif;
157+       right: 0;
158+       margin-top: 18px;
159+       position: absolute;
160+       top: 0;
161+       z-index: 1000; /* >999 for wp-admin */
162+}
163+.atwho-view ul {
164+       list-style: none;
165+       margin: auto;
166+       padding: 0;
167+}
168+.atwho-view ul li {
169+       border-bottom: 1px solid #EFEFEF;
170+       box-sizing: content-box;
171+       cursor: pointer;
172+       display: block;
173+       font-size: 14px;
174+       height: 20px;
175+       line-height: 20px;
176+       margin: 0;
177+       overflow: hidden;
178+       padding: 5px 10px;
179+}
180+.atwho-view img {
181+       border-radius: 2px;
182+       float: left;
183+       height: 20px;
184+       margin-right: 10px;
185+       width: 20px;
186+}
187+.atwho-view strong {
188+       background: #EFEFEF;
189+       font: bold;
190+}
191+.atwho-view .username strong {
192+       color: #D54E21;
193+}
194+.atwho-view small {
195+       color: #AAA;
196+       float: left;
197+       font-size: smaller;
198+       font-weight: normal;
199+       margin-right: 40px;
200+}
201+.atwho-view .cur {
202+       background: rgba(239, 239, 239, 0.5);
203+}
204+
205+@media (max-width: 900px) {
206+       .atwho-view img {
207+               float: right;
208+               margin-right: 0;
209+               margin-left: 10px;
210+       }
211+       .atwho-view small {
212+               display: none;
213+       }
214+}
215+@media (max-width: 400px) {
216+       .atwho-view ul li {
217+               font-size: 16px;
218+               line-height: 23px;
219+               padding: 13px;
220+       }
221+       .atwho-view ul li img {
222+               height: 30px;
223+               margin-top: -5px;
224+               width: 30px;
225+       }
226+}
227\ No newline at end of file
228diff --git a/src/bp-activity/css/mentions.css b/src/bp-activity/css/mentions.css
229new file mode 100644
230index 0000000..9bc5134
231--- /dev/null
232+++ b/src/bp-activity/css/mentions.css
233@@ -0,0 +1,78 @@
234+.atwho-view {
235+       background: #FFF;
236+       border-radius: 2px;
237+       border: 1px solid rgb(204, 204, 204);
238+       box-shadow: 0 0 5px rgba(204, 204, 204, 0.25), 0 0 1px #FFF;
239+       color: #D84800;
240+       display: none;
241+       font-family: sans-serif;
242+       left: 0;
243+       margin-top: 18px;
244+       position: absolute;
245+       top: 0;
246+       z-index: 1000; /* >999 for wp-admin */
247+}
248+.atwho-view ul {
249+       list-style: none;
250+       margin: auto;
251+       padding: 0;
252+}
253+.atwho-view ul li {
254+       border-bottom: 1px solid #EFEFEF;
255+       box-sizing: content-box;
256+       cursor: pointer;
257+       display: block;
258+       font-size: 14px;
259+       height: 20px;
260+       line-height: 20px;
261+       margin: 0;
262+       overflow: hidden;
263+       padding: 5px 10px;
264+}
265+.atwho-view img {
266+       border-radius: 2px;
267+       float: right;
268+       height: 20px;
269+       margin-left: 10px;
270+       width: 20px;
271+}
272+.atwho-view strong {
273+       background: #EFEFEF;
274+       font: bold;
275+}
276+.atwho-view .username strong {
277+       color: #D54E21;
278+}
279+.atwho-view small {
280+       color: #AAA;
281+       float: right;
282+       font-size: smaller;
283+       font-weight: normal;
284+       margin-left: 40px;
285+}
286+.atwho-view .cur {
287+       background: rgba(239, 239, 239, 0.5);
288+}
289+
290+@media (max-width: 900px) {
291+       .atwho-view img {
292+               float: left;
293+               margin-left: 0;
294+               margin-right: 10px;
295+       }
296+       .atwho-view small {
297+               display: none;
298+       }
299+}
300+@media (max-width: 400px) {
301+       .atwho-view ul li {
302+               font-size: 16px;
303+               line-height: 23px;
304+               padding: 13px;
305+       }
306+       .atwho-view ul li img {
307+               height: 30px;
308+               margin-top: -5px;
309+               width: 30px;
310+       }
311+}
312\ No newline at end of file
313diff --git a/src/bp-activity/js/mentions.js b/src/bp-activity/js/mentions.js
314new file mode 100644
315index 0000000..ae36039
316--- /dev/null
317+++ b/src/bp-activity/js/mentions.js
318@@ -0,0 +1,204 @@
319+(function( $, undefined ) {
320+       var mentionsQueryCache = [],
321+               mentionsItem;
322+
323+       /**
324+        * Adds BuddyPress @mentions to form inputs.
325+        *
326+        * @param {array|object} options If array, becomes the suggestions' data source. If object, passed as config to $.atwho().
327+        * @since BuddyPress (2.1.0)
328+        */
329+       $.fn.bp_mentions = function( options ) {
330+               if ( $.isArray( options ) ) {
331+                       options = { data: options };
332+               }
333+
334+               /**
335+                * Default options for at.js; see https://github.com/ichord/At.js/.
336+                */
337+               var suggestionsDefaults = {
338+                       delay:               200,
339+                       hide_without_suffix: true,
340+                       insert_tpl:          '</>${atwho-data-value}</>', // For contentEditable, the fake tags make jQuery insert a textNode.
341+                       limit:               10,
342+                       start_with_space:    false,
343+                       suffix:              '',
344+
345+                       callbacks: {
346+                               /**
347+                                * Custom filter to only match the start of spaced words.
348+                                * Based on the core/default one.
349+                                *
350+                                * @param {string} query
351+                                * @param {array} data
352+                                * @param {string} search_key
353+                                * @return {array}
354+                                * @since BuddyPress (2.1.0)
355+                                */
356+                               filter: function( query, data, search_key ) {
357+                                       var item, _i, _len, _results = [],
358+                                       regxp = new RegExp( '^' + query + '| ' + query, 'ig' ); // start of string, or preceded by a space.
359+
360+                                       for ( _i = 0, _len = data.length; _i < _len; _i++ ) {
361+                                               item = data[ _i ];
362+                                               if ( item[ search_key ].toLowerCase().match( regxp ) ) {
363+                                                       _results.push( item );
364+                                               }
365+                                       }
366+
367+                                       return _results;
368+                               },
369+
370+                               /**
371+                                * Removes some spaces around highlighted string and tweaks regex to allow spaces
372+                                * (to match display_name). Based on the core default.
373+                                *
374+                                * @param {unknown} li
375+                                * @param {string} query
376+                                * @return {string}
377+                                * @since BuddyPress (2.1.0)
378+                                */
379+                               highlighter: function( li, query ) {
380+                                       if ( ! query ) {
381+                                               return li;
382+                                       }
383+
384+                                       var regexp = new RegExp( '>(\\s*|[\\w\\s]*)(' + this.at.replace( '+', '\\+') + '?' + query.replace( '+', '\\+' ) + ')([\\w ]*)\\s*<', 'ig' );
385+                                       return li.replace( regexp, function( str, $1, $2, $3 ) {
386+                                               return '>' + $1 + '<strong>' + $2 + '</strong>' + $3 + '<';
387+                                       });
388+                               },
389+
390+                               /**
391+                                * Reposition the suggestion list dynamically.
392+                                *
393+                                * @param {unknown} offset
394+                                * @since BuddyPress (2.1.0)
395+                                */
396+                               before_reposition: function( offset ) {
397+                                       var $view = $( '#atwho-ground-' + this.id + ' .atwho-view' ),
398+                                       caret     = this.$inputor.caret( 'offset', { iframe: $( '#content_ifr' )[0] } ).left,
399+                                       move;
400+
401+                                       // If the caret is past horizontal half, then flip it, yo.
402+                                       if ( caret > ( $( 'body' ).width() / 2 ) ) {
403+                                               $view.addClass( 'flip' );
404+                                               move = caret - offset.left - this.view.$el.width();
405+                                       } else {
406+                                               $view.removeClass( 'flip' );
407+                                               move = caret - offset.left + 1;
408+                                       }
409+
410+                                       offset.top  += 1;
411+                                       offset.left += move;
412+                               },
413+
414+                               /**
415+                                * Override default behaviour which inserts junk tags in the WordPress Visual editor.
416+                                *
417+                                * @param {unknown} $inputor Element which we're inserting content into.
418+                                * @param {string) content The content that will be inserted.
419+                                * @param {string) suffix Applied to the end of the content string.
420+                                * @return {string}
421+                                * @since BuddyPress (2.1.0)
422+                                */
423+                               inserting_wrapper: function( $inputor, content, suffix ) {
424+                                       var new_suffix = ( suffix === '' ) ? suffix : suffix || ' ';
425+                                       return '' + content + new_suffix;
426+                               }
427+                       }
428+               },
429+
430+               /**
431+                * Default options for our @mentions; see https://github.com/ichord/At.js/.
432+                */
433+               mentionsDefaults = {
434+                       callbacks: {
435+                               /**
436+                                * If there are no matches for the query in this.data, then query BuddyPress.
437+                                *
438+                                * @param {string} query Partial @mention to search for.
439+                                * @param {function} render_view Render page callback function.
440+                                * @since BuddyPress (2.1.0)
441+                                */
442+                               remote_filter: function( query, render_view ) {
443+                                       var self = $( this );
444+
445+                                       mentionsItem = mentionsQueryCache[ query ];
446+                                       if ( typeof mentionsItem === 'object' ) {
447+                                               render_view( mentionsItem );
448+                                               return;
449+                                       }
450+
451+                                       if ( self.xhr ) {
452+                                               self.xhr.abort();
453+                                       }
454+
455+                                       self.xhr = $.getJSON( ajaxurl, { 'action': 'bp_get_suggestions', 'term': query, 'type': 'members' } )
456+                                               /**
457+                                                * Success callback for the @suggestions lookup.
458+                                                *
459+                                                * @param {object} response Details of users matching the query.
460+                                                * @since BuddyPress (2.1.0)
461+                                                */
462+                                               .done(function( response ) {
463+                                                       if ( ! response.success ) {
464+                                                               return;
465+                                                       }
466+
467+                                                       var data = $.map( response.data,
468+                                                               /**
469+                                                                * Create a composite index to determine ordering of results;
470+                                                                * nicename matches will appear on top.
471+                                                                *
472+                                                                * @param {array} suggestion A suggestion's original data.
473+                                                                * @return {array} A suggestion's new data.
474+                                                                * @since BuddyPress (2.1.0)
475+                                                                */
476+                                                               function( suggestion ) {
477+                                                                       suggestion.search = suggestion.search || suggestion.ID + ' ' + suggestion.name;
478+                                                                       return suggestion;
479+                                                               }
480+                                                       );
481+
482+                                                       mentionsQueryCache[ query ] = data;
483+                                                       render_view( data );
484+                                               });
485+                               }
486+                       },
487+
488+                       data: $.map( options.data,
489+                               /**
490+                                * Create a composite index to search against of nicename + display name.
491+                                * This will also determine ordering of results, so nicename matches will appear on top.
492+                                *
493+                                * @param {array} suggestion A suggestion's original data.
494+                                * @return {array} A suggestion's new data.
495+                                * @since BuddyPress (2.1.0)
496+                                */
497+                               function( suggestion ) {
498+                                       suggestion.search = suggestion.search || suggestion.ID + ' ' + suggestion.name;
499+                                       return suggestion;
500+                               }
501+                       ),
502+
503+                       at:         '@',
504+                       search_key: 'search',
505+                       tpl:        '<li data-value="@${ID}"><img src="${image}" /><span class="username">@${ID}</span><small>${name}</small></li>'
506+               },
507+
508+               opts = $.extend( true, {}, suggestionsDefaults, mentionsDefaults, options );
509+               return $.fn.atwho.call( this, opts );
510+       };
511+
512+       $( document ).ready(function() {
513+               var users = [];
514+
515+               if ( typeof window.BP_Suggestions === 'object' ) {
516+                       users = window.BP_Suggestions.friends || users;
517+               }
518+
519+               // Activity/reply, post comments, dashboard post 'text' editor.
520+               $( '.bp-suggestions, #comments form textarea, .wp-editor-area' ).bp_mentions( users );
521+       });
522+})( jQuery );
523\ No newline at end of file
524diff --git a/src/bp-core/bp-core-classes.php b/src/bp-core/bp-core-classes.php
525index 5bbdf0b..ed9c260 100644
526--- a/src/bp-core/bp-core-classes.php
527+++ b/src/bp-core/bp-core-classes.php
528@@ -368,17 +368,26 @@ class BP_User_Query {
529                // 'search_terms' searches user_login and user_nicename
530                // xprofile field matches happen in bp_xprofile_bp_user_query_search()
531                if ( false !== $search_terms ) {
532-                       $search_terms_like = bp_esc_like( $search_terms );
533+                       $search_terms = bp_esc_like( $search_terms );
534 
535                        if ( $search_wildcard === 'left' ) {
536-                               $search_terms_like = '%' . $search_terms_like;
537+                               $search_terms_nospace = '%' . $search_terms;
538+                               $search_terms_space   = '%' . $search_terms . ' %';
539                        } elseif ( $search_wildcard === 'right' ) {
540-                               $search_terms_like = $search_terms_like . '%';
541+                               $search_terms_nospace =        $search_terms . '%';
542+                               $search_terms_space   = '% ' . $search_terms . '%';
543                        } else {
544-                               $search_terms_like = '%' . $search_terms_like . '%';
545+                               $search_terms_nospace = '%' . $search_terms . '%';
546+                               $search_terms_space   = '%' . $search_terms . '%';
547                        }
548 
549-                       $sql['where']['search'] = $wpdb->prepare( "u.{$this->uid_name} IN ( SELECT ID FROM {$wpdb->users} WHERE ( user_login LIKE %s OR user_nicename LIKE %s ) )", $search_terms_like, $search_terms_like );
550+                       $sql['where']['search'] = $wpdb->prepare(
551+                               "u.{$this->uid_name} IN ( SELECT ID FROM {$wpdb->users} WHERE ( user_login LIKE %s OR user_login LIKE %s OR user_nicename LIKE %s OR user_nicename LIKE %s ) )",
552+                               $search_terms_nospace,
553+                               $search_terms_space,
554+                               $search_terms_nospace,
555+                               $search_terms_space
556+                       );
557                }
558 
559                // 'meta_key', 'meta_value' allow usermeta search
560@@ -2566,7 +2575,7 @@ class BP_Members_Suggestions extends BP_Suggestions {
561         * }
562         */
563        protected $default_args = array(
564-               'limit'        => 16,
565+               'limit'        => 10,
566                'only_friends' => false,
567                'term'         => '',
568                'type'         => '',
569@@ -2606,6 +2615,7 @@ class BP_Members_Suggestions extends BP_Suggestions {
570                        'page'            => 1,
571                        'per_page'        => $this->args['limit'],
572                        'search_terms'    => $this->args['term'],
573+                       'search_wildcard' => is_rtl() ? 'left' : 'right',
574                );
575 
576                // Only return matches of friends of this user.
577diff --git a/src/bp-core/bp-core-cssjs.php b/src/bp-core/bp-core-cssjs.php
578index b6fa396..c7f7011 100644
579--- a/src/bp-core/bp-core-cssjs.php
580+++ b/src/bp-core/bp-core-cssjs.php
581@@ -19,10 +19,15 @@ function bp_core_register_common_scripts() {
582        $url = buddypress()->plugin_url . 'bp-core/js/';
583       
584        $scripts = apply_filters( 'bp_core_register_common_scripts', array(
585+               // Legacy
586                'bp-confirm'        => array( 'file' => "{$url}confirm{$ext}",        'dependencies' => array( 'jquery' ) ),
587                'bp-widget-members' => array( 'file' => "{$url}widget-members{$ext}", 'dependencies' => array( 'jquery' ) ),
588                'bp-jquery-query'   => array( 'file' => "{$url}jquery-query{$ext}",   'dependencies' => array( 'jquery' ) ),
589                'bp-jquery-cookie'  => array( 'file' => "{$url}jquery-cookie{$ext}",  'dependencies' => array( 'jquery' ) ),
590+
591+               // 2.1
592+               'jquery-caret' => array( 'file' => "{$url}jquery.caret{$ext}", 'dependencies' => array( 'jquery' ) ),
593+               'jquery-atwho' => array( 'file' => "{$url}jquery.atwho{$ext}", 'dependencies' => array( 'jquery', 'jquery-caret' ) ),
594        ) );
595 
596        foreach ( $scripts as $id => $script ) {
597diff --git a/src/bp-core/js/jquery.atwho.js b/src/bp-core/js/jquery.atwho.js
598new file mode 100755
599index 0000000..d5eefe3
600--- /dev/null
601+++ b/src/bp-core/js/jquery.atwho.js
602@@ -0,0 +1,824 @@
603+/*! jquery.atwho - v0.5.0 - 2014-07-14
604+* Copyright (c) 2014 chord.luo <chord.luo@gmail.com>;
605+* homepage: http://ichord.github.com/At.js
606+* Licensed MIT
607+*/
608+
609+(function() {
610+  (function(factory) {
611+    if (typeof define === 'function' && define.amd) {
612+      return define(['jquery'], factory);
613+    } else {
614+      return factory(window.jQuery);
615+    }
616+  })(function($) {
617+
618+var $CONTAINER, Api, App, Controller, DEFAULT_CALLBACKS, KEY_CODE, Model, View,
619+  __slice = [].slice;
620+
621+App = (function() {
622+  function App(inputor) {
623+    this.current_flag = null;
624+    this.controllers = {};
625+    this.alias_maps = {};
626+    this.$inputor = $(inputor);
627+    this.iframe = null;
628+    this.setIframe();
629+    this.listen();
630+  }
631+
632+  App.prototype.setIframe = function(iframe) {
633+    if (iframe) {
634+      this.window = iframe.contentWindow;
635+      this.document = iframe.contentDocument || this.window.document;
636+      return this.iframe = iframe;
637+    } else {
638+      this.document = document;
639+      this.window = window;
640+      return this.iframe = null;
641+    }
642+  };
643+
644+  App.prototype.controller = function(at) {
645+    var c, current, current_flag, _ref;
646+    if (this.alias_maps[at]) {
647+      current = this.controllers[this.alias_maps[at]];
648+    } else {
649+      _ref = this.controllers;
650+      for (current_flag in _ref) {
651+        c = _ref[current_flag];
652+        if (current_flag === at) {
653+          current = c;
654+          break;
655+        }
656+      }
657+    }
658+    if (current) {
659+      return current;
660+    } else {
661+      return this.controllers[this.current_flag];
662+    }
663+  };
664+
665+  App.prototype.set_context_for = function(at) {
666+    this.current_flag = at;
667+    return this;
668+  };
669+
670+  App.prototype.reg = function(flag, setting) {
671+    var controller, _base;
672+    controller = (_base = this.controllers)[flag] || (_base[flag] = new Controller(this, flag));
673+    if (setting.alias) {
674+      this.alias_maps[setting.alias] = flag;
675+    }
676+    controller.init(setting);
677+    return this;
678+  };
679+
680+  App.prototype.listen = function() {
681+    return this.$inputor.on('keyup.atwhoInner', (function(_this) {
682+      return function(e) {
683+        return _this.on_keyup(e);
684+      };
685+    })(this)).on('keydown.atwhoInner', (function(_this) {
686+      return function(e) {
687+        return _this.on_keydown(e);
688+      };
689+    })(this)).on('scroll.atwhoInner', (function(_this) {
690+      return function(e) {
691+        var _ref;
692+        return (_ref = _this.controller()) != null ? _ref.view.hide(e) : void 0;
693+      };
694+    })(this)).on('blur.atwhoInner', (function(_this) {
695+      return function(e) {
696+        var c;
697+        if (c = _this.controller()) {
698+          return c.view.hide(e, c.get_opt("display_timeout"));
699+        }
700+      };
701+    })(this)).on('click.atwhoInner', (function(_this) {
702+      return function(e) {
703+        var _ref;
704+        return (_ref = _this.controller()) != null ? _ref.view.hide(e) : void 0;
705+      };
706+    })(this));
707+  };
708+
709+  App.prototype.shutdown = function() {
710+    var c, _, _ref;
711+    _ref = this.controllers;
712+    for (_ in _ref) {
713+      c = _ref[_];
714+      c.destroy();
715+      delete this.controllers[_];
716+    }
717+    return this.$inputor.off('.atwhoInner');
718+  };
719+
720+  App.prototype.dispatch = function() {
721+    return $.map(this.controllers, (function(_this) {
722+      return function(c) {
723+        var delay;
724+        if (delay = c.get_opt('delay')) {
725+          clearTimeout(_this.delayedCallback);
726+          return _this.delayedCallback = setTimeout(function() {
727+            if (c.look_up()) {
728+              return _this.set_context_for(c.at);
729+            }
730+          }, delay);
731+        } else {
732+          if (c.look_up()) {
733+            return _this.set_context_for(c.at);
734+          }
735+        }
736+      };
737+    })(this));
738+  };
739+
740+  App.prototype.on_keyup = function(e) {
741+    var _ref;
742+    switch (e.keyCode) {
743+      case KEY_CODE.ESC:
744+        e.preventDefault();
745+        if ((_ref = this.controller()) != null) {
746+          _ref.view.hide();
747+        }
748+        break;
749+      case KEY_CODE.DOWN:
750+      case KEY_CODE.UP:
751+      case KEY_CODE.CTRL:
752+        $.noop();
753+        break;
754+      case KEY_CODE.P:
755+      case KEY_CODE.N:
756+        if (!e.ctrlKey) {
757+          this.dispatch();
758+        }
759+        break;
760+      default:
761+        this.dispatch();
762+    }
763+  };
764+
765+  App.prototype.on_keydown = function(e) {
766+    var view, _ref;
767+    view = (_ref = this.controller()) != null ? _ref.view : void 0;
768+    if (!(view && view.visible())) {
769+      return;
770+    }
771+    switch (e.keyCode) {
772+      case KEY_CODE.ESC:
773+        e.preventDefault();
774+        view.hide(e);
775+        break;
776+      case KEY_CODE.UP:
777+        e.preventDefault();
778+        view.prev();
779+        break;
780+      case KEY_CODE.DOWN:
781+        e.preventDefault();
782+        view.next();
783+        break;
784+      case KEY_CODE.P:
785+        if (!e.ctrlKey) {
786+          return;
787+        }
788+        e.preventDefault();
789+        view.prev();
790+        break;
791+      case KEY_CODE.N:
792+        if (!e.ctrlKey) {
793+          return;
794+        }
795+        e.preventDefault();
796+        view.next();
797+        break;
798+      case KEY_CODE.TAB:
799+      case KEY_CODE.ENTER:
800+        if (!view.visible()) {
801+          return;
802+        }
803+        e.preventDefault();
804+        view.choose(e);
805+        break;
806+      default:
807+        $.noop();
808+    }
809+  };
810+
811+  return App;
812+
813+})();
814+
815+Controller = (function() {
816+  Controller.prototype.uid = function() {
817+    return (Math.random().toString(16) + "000000000").substr(2, 8) + (new Date().getTime());
818+  };
819+
820+  function Controller(app, at) {
821+    this.app = app;
822+    this.at = at;
823+    this.$inputor = this.app.$inputor;
824+    this.id = this.$inputor[0].id || this.uid();
825+    this.setting = null;
826+    this.query = null;
827+    this.pos = 0;
828+    this.cur_rect = null;
829+    this.range = null;
830+    $CONTAINER.append(this.$el = $("<div id='atwho-ground-" + this.id + "'></div>"));
831+    this.model = new Model(this);
832+    this.view = new View(this);
833+  }
834+
835+  Controller.prototype.init = function(setting) {
836+    this.setting = $.extend({}, this.setting || $.fn.atwho["default"], setting);
837+    this.view.init();
838+    return this.model.reload(this.setting.data);
839+  };
840+
841+  Controller.prototype.destroy = function() {
842+    this.trigger('beforeDestroy');
843+    this.model.destroy();
844+    this.view.destroy();
845+    return this.$el.remove();
846+  };
847+
848+  Controller.prototype.call_default = function() {
849+    var args, error, func_name;
850+    func_name = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
851+    try {
852+      return DEFAULT_CALLBACKS[func_name].apply(this, args);
853+    } catch (_error) {
854+      error = _error;
855+      return $.error("" + error + " Or maybe At.js doesn't have function " + func_name);
856+    }
857+  };
858+
859+  Controller.prototype.trigger = function(name, data) {
860+    var alias, event_name;
861+    if (data == null) {
862+      data = [];
863+    }
864+    data.push(this);
865+    alias = this.get_opt('alias');
866+    event_name = alias ? "" + name + "-" + alias + ".atwho" : "" + name + ".atwho";
867+    return this.$inputor.trigger(event_name, data);
868+  };
869+
870+  Controller.prototype.callbacks = function(func_name) {
871+    return this.get_opt("callbacks")[func_name] || DEFAULT_CALLBACKS[func_name];
872+  };
873+
874+  Controller.prototype.get_opt = function(at, default_value) {
875+    var e;
876+    try {
877+      return this.setting[at];
878+    } catch (_error) {
879+      e = _error;
880+      return null;
881+    }
882+  };
883+
884+  Controller.prototype.content = function() {
885+    if (this.$inputor.is('textarea, input')) {
886+      return this.$inputor.val();
887+    } else {
888+      return this.$inputor.text();
889+    }
890+  };
891+
892+  Controller.prototype.catch_query = function() {
893+    var caret_pos, content, end, query, start, subtext;
894+    content = this.content();
895+    caret_pos = this.$inputor.caret('pos', {
896+      iframe: this.app.iframe
897+    });
898+    subtext = content.slice(0, caret_pos);
899+    query = this.callbacks("matcher").call(this, this.at, subtext, this.get_opt('start_with_space'));
900+    if (typeof query === "string" && query.length <= this.get_opt('max_len', 20)) {
901+      start = caret_pos - query.length;
902+      end = start + query.length;
903+      this.pos = start;
904+      query = {
905+        'text': query,
906+        'head_pos': start,
907+        'end_pos': end
908+      };
909+      this.trigger("matched", [this.at, query.text]);
910+    } else {
911+      query = null;
912+      this.view.hide();
913+    }
914+    return this.query = query;
915+  };
916+
917+  Controller.prototype.rect = function() {
918+    var c, scale_bottom;
919+    if (!(c = this.$inputor.caret('offset', this.pos - 1, {
920+      iframe: this.app.iframe
921+    }))) {
922+      return;
923+    }
924+    if (this.$inputor.attr('contentEditable') === 'true') {
925+      c = (this.cur_rect || (this.cur_rect = c)) || c;
926+    }
927+    scale_bottom = this.app.document.selection ? 0 : 2;
928+    return {
929+      left: c.left,
930+      top: c.top,
931+      bottom: c.top + c.height + scale_bottom
932+    };
933+  };
934+
935+  Controller.prototype.reset_rect = function() {
936+    if (this.$inputor.attr('contentEditable') === 'true') {
937+      return this.cur_rect = null;
938+    }
939+  };
940+
941+  Controller.prototype.mark_range = function() {
942+    if (this.$inputor.attr('contentEditable') === 'true') {
943+      if (this.app.window.getSelection) {
944+        this.range = this.app.window.getSelection().getRangeAt(0);
945+      }
946+      if (this.app.document.selection) {
947+        return this.ie8_range = this.app.document.selection.createRange();
948+      }
949+    }
950+  };
951+
952+  Controller.prototype.insert_content_for = function($li) {
953+    var data, data_value, tpl;
954+    data_value = $li.data('value');
955+    tpl = this.get_opt('insert_tpl');
956+    if (this.$inputor.is('textarea, input') || !tpl) {
957+      return data_value;
958+    }
959+    data = $.extend({}, $li.data('item-data'), {
960+      'atwho-data-value': data_value,
961+      'atwho-at': this.at
962+    });
963+    return this.callbacks("tpl_eval").call(this, tpl, data);
964+  };
965+
966+  Controller.prototype.insert = function(content, $li) {
967+    var $inputor, content_node, pos, range, sel, source, start_str, text, wrapped_content;
968+    $inputor = this.$inputor;
969+    wrapped_content = this.callbacks('inserting_wrapper').call(this, $inputor, content, this.get_opt("suffix"));
970+    if ($inputor.is('textarea, input')) {
971+      source = $inputor.val();
972+      start_str = source.slice(0, Math.max(this.query.head_pos - this.at.length, 0));
973+      text = "" + start_str + wrapped_content + (source.slice(this.query['end_pos'] || 0));
974+      $inputor.val(text);
975+      $inputor.caret('pos', start_str.length + wrapped_content.length, {
976+        iframe: this.app.iframe
977+      });
978+    } else if (range = this.range) {
979+      pos = range.startOffset - (this.query.end_pos - this.query.head_pos) - this.at.length;
980+      range.setStart(range.endContainer, Math.max(pos, 0));
981+      range.setEnd(range.endContainer, range.endOffset);
982+      range.deleteContents();
983+      content_node = $(wrapped_content, this.app.document)[0];
984+      range.insertNode(content_node);
985+      range.setEndAfter(content_node);
986+      range.collapse(false);
987+      sel = this.app.window.getSelection();
988+      sel.removeAllRanges();
989+      sel.addRange(range);
990+    } else if (range = this.ie8_range) {
991+      range.moveStart('character', this.query.end_pos - this.query.head_pos - this.at.length);
992+      range.pasteHTML(wrapped_content);
993+      range.collapse(false);
994+      range.select();
995+    }
996+    if (!$inputor.is(':focus')) {
997+      $inputor.focus();
998+    }
999+    return $inputor.change();
1000+  };
1001+
1002+  Controller.prototype.render_view = function(data) {
1003+    var search_key;
1004+    search_key = this.get_opt("search_key");
1005+    data = this.callbacks("sorter").call(this, this.query.text, data.slice(0, 1001), search_key);
1006+    return this.view.render(data.slice(0, this.get_opt('limit')));
1007+  };
1008+
1009+  Controller.prototype.look_up = function() {
1010+    var query, _callback;
1011+    if (!(query = this.catch_query())) {
1012+      return;
1013+    }
1014+    _callback = function(data) {
1015+      if (data && data.length > 0) {
1016+        return this.render_view(data);
1017+      } else {
1018+        return this.view.hide();
1019+      }
1020+    };
1021+    this.model.query(query.text, $.proxy(_callback, this));
1022+    return query;
1023+  };
1024+
1025+  return Controller;
1026+
1027+})();
1028+
1029+Model = (function() {
1030+  function Model(context) {
1031+    this.context = context;
1032+    this.at = this.context.at;
1033+    this.storage = this.context.$inputor;
1034+  }
1035+
1036+  Model.prototype.destroy = function() {
1037+    return this.storage.data(this.at, null);
1038+  };
1039+
1040+  Model.prototype.saved = function() {
1041+    return this.fetch() > 0;
1042+  };
1043+
1044+  Model.prototype.query = function(query, callback) {
1045+    var data, search_key, _remote_filter;
1046+    data = this.fetch();
1047+    search_key = this.context.get_opt("search_key");
1048+    data = this.context.callbacks('filter').call(this.context, query, data, search_key) || [];
1049+    _remote_filter = this.context.callbacks('remote_filter');
1050+    if (data.length > 0 || (!_remote_filter && data.length === 0)) {
1051+      return callback(data);
1052+    } else {
1053+      return _remote_filter.call(this.context, query, callback);
1054+    }
1055+  };
1056+
1057+  Model.prototype.fetch = function() {
1058+    return this.storage.data(this.at) || [];
1059+  };
1060+
1061+  Model.prototype.save = function(data) {
1062+    return this.storage.data(this.at, this.context.callbacks("before_save").call(this.context, data || []));
1063+  };
1064+
1065+  Model.prototype.load = function(data) {
1066+    if (!(this.saved() || !data)) {
1067+      return this._load(data);
1068+    }
1069+  };
1070+
1071+  Model.prototype.reload = function(data) {
1072+    return this._load(data);
1073+  };
1074+
1075+  Model.prototype._load = function(data) {
1076+    if (typeof data === "string") {
1077+      return $.ajax(data, {
1078+        dataType: "json"
1079+      }).done((function(_this) {
1080+        return function(data) {
1081+          return _this.save(data);
1082+        };
1083+      })(this));
1084+    } else {
1085+      return this.save(data);
1086+    }
1087+  };
1088+
1089+  return Model;
1090+
1091+})();
1092+
1093+View = (function() {
1094+  function View(context) {
1095+    this.context = context;
1096+    this.$el = $("<div class='atwho-view'><ul class='atwho-view-ul'></ul></div>");
1097+    this.timeout_id = null;
1098+    this.context.$el.append(this.$el);
1099+    this.bind_event();
1100+  }
1101+
1102+  View.prototype.init = function() {
1103+    var id;
1104+    id = this.context.get_opt("alias") || this.context.at.charCodeAt(0);
1105+    return this.$el.attr({
1106+      'id': "at-view-" + id
1107+    });
1108+  };
1109+
1110+  View.prototype.destroy = function() {
1111+    return this.$el.remove();
1112+  };
1113+
1114+  View.prototype.bind_event = function() {
1115+    var $menu;
1116+    $menu = this.$el.find('ul');
1117+    return $menu.on('mouseenter.atwho-view', 'li', function(e) {
1118+      $menu.find('.cur').removeClass('cur');
1119+      return $(e.currentTarget).addClass('cur');
1120+    }).on('click', (function(_this) {
1121+      return function(e) {
1122+        _this.choose(e);
1123+        return e.preventDefault();
1124+      };
1125+    })(this));
1126+  };
1127+
1128+  View.prototype.visible = function() {
1129+    return this.$el.is(":visible");
1130+  };
1131+
1132+  View.prototype.choose = function(e) {
1133+    var $li, content;
1134+    if (($li = this.$el.find(".cur")).length) {
1135+      content = this.context.insert_content_for($li);
1136+      this.context.insert(this.context.callbacks("before_insert").call(this.context, content, $li), $li);
1137+      this.context.trigger("inserted", [$li, e]);
1138+      this.hide(e);
1139+    }
1140+    if (this.context.get_opt("hide_without_suffix")) {
1141+      return this.stop_showing = true;
1142+    }
1143+  };
1144+
1145+  View.prototype.reposition = function(rect) {
1146+    var offset, _ref;
1147+    if (rect.bottom + this.$el.height() - $(window).scrollTop() > $(window).height()) {
1148+      rect.bottom = rect.top - this.$el.height();
1149+    }
1150+    offset = {
1151+      left: rect.left,
1152+      top: rect.bottom
1153+    };
1154+    if ((_ref = this.context.callbacks("before_reposition")) != null) {
1155+      _ref.call(this.context, offset);
1156+    }
1157+    this.$el.offset(offset);
1158+    return this.context.trigger("reposition", [offset]);
1159+  };
1160+
1161+  View.prototype.next = function() {
1162+    var cur, next;
1163+    cur = this.$el.find('.cur').removeClass('cur');
1164+    next = cur.next();
1165+    if (!next.length) {
1166+      next = this.$el.find('li:first');
1167+    }
1168+    return next.addClass('cur');
1169+  };
1170+
1171+  View.prototype.prev = function() {
1172+    var cur, prev;
1173+    cur = this.$el.find('.cur').removeClass('cur');
1174+    prev = cur.prev();
1175+    if (!prev.length) {
1176+      prev = this.$el.find('li:last');
1177+    }
1178+    return prev.addClass('cur');
1179+  };
1180+
1181+  View.prototype.show = function() {
1182+    var rect;
1183+    if (this.stop_showing) {
1184+      this.stop_showing = false;
1185+      return;
1186+    }
1187+    this.context.mark_range();
1188+    if (!this.visible()) {
1189+      this.$el.show();
1190+      this.context.trigger('shown');
1191+    }
1192+    if (rect = this.context.rect()) {
1193+      return this.reposition(rect);
1194+    }
1195+  };
1196+
1197+  View.prototype.hide = function(e, time) {
1198+    var callback;
1199+    if (!this.visible()) {
1200+      return;
1201+    }
1202+    if (isNaN(time)) {
1203+      this.context.reset_rect();
1204+      this.$el.hide();
1205+      return this.context.trigger('hidden', [e]);
1206+    } else {
1207+      callback = (function(_this) {
1208+        return function() {
1209+          return _this.hide();
1210+        };
1211+      })(this);
1212+      clearTimeout(this.timeout_id);
1213+      return this.timeout_id = setTimeout(callback, time);
1214+    }
1215+  };
1216+
1217+  View.prototype.render = function(list) {
1218+    var $li, $ul, item, li, tpl, _i, _len;
1219+    if (!($.isArray(list) && list.length > 0)) {
1220+      this.hide();
1221+      return;
1222+    }
1223+    this.$el.find('ul').empty();
1224+    $ul = this.$el.find('ul');
1225+    tpl = this.context.get_opt('tpl');
1226+    for (_i = 0, _len = list.length; _i < _len; _i++) {
1227+      item = list[_i];
1228+      item = $.extend({}, item, {
1229+        'atwho-at': this.context.at
1230+      });
1231+      li = this.context.callbacks("tpl_eval").call(this.context, tpl, item);
1232+      $li = $(this.context.callbacks("highlighter").call(this.context, li, this.context.query.text));
1233+      $li.data("item-data", item);
1234+      $ul.append($li);
1235+    }
1236+    this.show();
1237+    if (this.context.get_opt('highlight_first')) {
1238+      return $ul.find("li:first").addClass("cur");
1239+    }
1240+  };
1241+
1242+  return View;
1243+
1244+})();
1245+
1246+KEY_CODE = {
1247+  DOWN: 40,
1248+  UP: 38,
1249+  ESC: 27,
1250+  TAB: 9,
1251+  ENTER: 13,
1252+  CTRL: 17,
1253+  P: 80,
1254+  N: 78
1255+};
1256+
1257+DEFAULT_CALLBACKS = {
1258+  before_save: function(data) {
1259+    var item, _i, _len, _results;
1260+    if (!$.isArray(data)) {
1261+      return data;
1262+    }
1263+    _results = [];
1264+    for (_i = 0, _len = data.length; _i < _len; _i++) {
1265+      item = data[_i];
1266+      if ($.isPlainObject(item)) {
1267+        _results.push(item);
1268+      } else {
1269+        _results.push({
1270+          name: item
1271+        });
1272+      }
1273+    }
1274+    return _results;
1275+  },
1276+  matcher: function(flag, subtext, should_start_with_space) {
1277+    var match, regexp;
1278+    flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
1279+    if (should_start_with_space) {
1280+      flag = '(?:^|\\s)' + flag;
1281+    }
1282+    regexp = new RegExp(flag + '([A-Za-z0-9_\+\-]*)$|' + flag + '([^\\x00-\\xff]*)$', 'gi');
1283+    match = regexp.exec(subtext);
1284+    if (match) {
1285+      return match[2] || match[1];
1286+    } else {
1287+      return null;
1288+    }
1289+  },
1290+  filter: function(query, data, search_key) {
1291+    var item, _i, _len, _results;
1292+    _results = [];
1293+    for (_i = 0, _len = data.length; _i < _len; _i++) {
1294+      item = data[_i];
1295+      if (~item[search_key].toLowerCase().indexOf(query.toLowerCase())) {
1296+        _results.push(item);
1297+      }
1298+    }
1299+    return _results;
1300+  },
1301+  remote_filter: null,
1302+  sorter: function(query, items, search_key) {
1303+    var item, _i, _len, _results;
1304+    if (!query) {
1305+      return items;
1306+    }
1307+    _results = [];
1308+    for (_i = 0, _len = items.length; _i < _len; _i++) {
1309+      item = items[_i];
1310+      item.atwho_order = item[search_key].toLowerCase().indexOf(query.toLowerCase());
1311+      if (item.atwho_order > -1) {
1312+        _results.push(item);
1313+      }
1314+    }
1315+    return _results.sort(function(a, b) {
1316+      return a.atwho_order - b.atwho_order;
1317+    });
1318+  },
1319+  tpl_eval: function(tpl, map) {
1320+    var error;
1321+    try {
1322+      return tpl.replace(/\$\{([^\}]*)\}/g, function(tag, key, pos) {
1323+        return map[key];
1324+      });
1325+    } catch (_error) {
1326+      error = _error;
1327+      return "";
1328+    }
1329+  },
1330+  highlighter: function(li, query) {
1331+    var regexp;
1332+    if (!query) {
1333+      return li;
1334+    }
1335+    regexp = new RegExp(">\\s*(\\w*?)(" + query.replace("+", "\\+") + ")(\\w*)\\s*<", 'ig');
1336+    return li.replace(regexp, function(str, $1, $2, $3) {
1337+      return '> ' + $1 + '<strong>' + $2 + '</strong>' + $3 + ' <';
1338+    });
1339+  },
1340+  before_insert: function(value, $li) {
1341+    return value;
1342+  },
1343+  inserting_wrapper: function($inputor, content, suffix) {
1344+    var new_suffix, wrapped_content;
1345+    new_suffix = suffix === "" ? suffix : suffix || " ";
1346+    if ($inputor.is('textarea, input')) {
1347+      return '' + content + new_suffix;
1348+    } else if ($inputor.attr('contentEditable') === 'true') {
1349+      new_suffix = suffix === "" ? suffix : suffix || "&nbsp;";
1350+      if (/firefox/i.test(navigator.userAgent)) {
1351+        wrapped_content = "<span>" + content + new_suffix + "</span>";
1352+      } else {
1353+        suffix = "<span contenteditable='false'>" + new_suffix + "<span>";
1354+        wrapped_content = "<span contenteditable='false'>" + content + suffix + "</span>";
1355+      }
1356+      if (this.app.document.selection) {
1357+        wrapped_content = "<span contenteditable='true'>" + content + "</span>";
1358+      }
1359+      return wrapped_content;
1360+    }
1361+  }
1362+};
1363+
1364+Api = {
1365+  load: function(at, data) {
1366+    var c;
1367+    if (c = this.controller(at)) {
1368+      return c.model.load(data);
1369+    }
1370+  },
1371+  setIframe: function(iframe) {
1372+    this.setIframe(iframe);
1373+    return null;
1374+  },
1375+  run: function() {
1376+    return this.dispatch();
1377+  },
1378+  destroy: function() {
1379+    this.shutdown();
1380+    return this.$inputor.data('atwho', null);
1381+  }
1382+};
1383+
1384+$CONTAINER = $("<div id='atwho-container'></div>");
1385+
1386+$.fn.atwho = function(method) {
1387+  var result, _args;
1388+  _args = arguments;
1389+  $('body').append($CONTAINER);
1390+  result = null;
1391+  this.filter('textarea, input, [contenteditable=true]').each(function() {
1392+    var $this, app;
1393+    if (!(app = ($this = $(this)).data("atwho"))) {
1394+      $this.data('atwho', (app = new App(this)));
1395+    }
1396+    if (typeof method === 'object' || !method) {
1397+      return app.reg(method.at, method);
1398+    } else if (Api[method] && app) {
1399+      return result = Api[method].apply(app, Array.prototype.slice.call(_args, 1));
1400+    } else {
1401+      return $.error("Method " + method + " does not exist on jQuery.caret");
1402+    }
1403+  });
1404+  return result || this;
1405+};
1406+
1407+$.fn.atwho["default"] = {
1408+  at: void 0,
1409+  alias: void 0,
1410+  data: null,
1411+  tpl: "<li data-value='${atwho-at}${name}'>${name}</li>",
1412+  insert_tpl: "<span id='${id}'>${atwho-data-value}</span>",
1413+  callbacks: DEFAULT_CALLBACKS,
1414+  search_key: "name",
1415+  suffix: void 0,
1416+  hide_without_suffix: false,
1417+  start_with_space: true,
1418+  highlight_first: true,
1419+  limit: 5,
1420+  max_len: 20,
1421+  display_timeout: 300,
1422+  delay: null
1423+};
1424+
1425+  });
1426+}).call(this);
1427diff --git a/src/bp-core/js/jquery.atwho.txt b/src/bp-core/js/jquery.atwho.txt
1428new file mode 100755
1429index 0000000..36cd1c1
1430--- /dev/null
1431+++ b/src/bp-core/js/jquery.atwho.txt
1432@@ -0,0 +1,22 @@
1433+Copyright (c) 2013 chord.luo@gmail.com
1434+
1435+Permission is hereby granted, free of charge, to any person
1436+obtaining a copy of this software and associated documentation
1437+files (the "Software"), to deal in the Software without
1438+restriction, including without limitation the rights to use,
1439+copy, modify, merge, publish, distribute, sublicense, and/or sell
1440+copies of the Software, and to permit persons to whom the
1441+Software is furnished to do so, subject to the following
1442+conditions:
1443+
1444+The above copyright notice and this permission notice shall be
1445+included in all copies or substantial portions of the Software.
1446+
1447+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
1448+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
1449+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
1450+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
1451+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
1452+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
1453+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1454+OTHER DEALINGS IN THE SOFTWARE.
1455diff --git a/src/bp-core/js/jquery.caret.js b/src/bp-core/js/jquery.caret.js
1456new file mode 100755
1457index 0000000..caa7876
1458--- /dev/null
1459+++ b/src/bp-core/js/jquery.caret.js
1460@@ -0,0 +1,366 @@
1461+/*
1462+  Implement Github like autocomplete mentions
1463+  http://ichord.github.com/At.js
1464+
1465+  Copyright (c) 2013 chord.luo@gmail.com
1466+  Licensed under the MIT license.
1467+*/
1468+
1469+
1470+/*
1471+本插件操䜜 textarea 或者 input 内的插入笊
1472+只实现了获埗插入笊圚文本框䞭的䜍眮我讟眮
1473+插入笊的䜍眮.
1474+*/
1475+
1476+
1477+(function() {
1478+  (function(factory) {
1479+    if (typeof define === 'function' && define.amd) {
1480+      return define(['jquery'], factory);
1481+    } else {
1482+      return factory(window.jQuery);
1483+    }
1484+  })(function($) {
1485+    "use strict";
1486+    var EditableCaret, InputCaret, Mirror, Utils, discoveryIframeOf, methods, oDocument, oFrame, oWindow, pluginName, setContextBy;
1487+    pluginName = 'caret';
1488+    EditableCaret = (function() {
1489+      function EditableCaret($inputor) {
1490+        this.$inputor = $inputor;
1491+        this.domInputor = this.$inputor[0];
1492+      }
1493+
1494+      EditableCaret.prototype.setPos = function(pos) {
1495+        return this.domInputor;
1496+      };
1497+
1498+      EditableCaret.prototype.getIEPosition = function() {
1499+        return $.noop();
1500+      };
1501+
1502+      EditableCaret.prototype.getPosition = function() {
1503+        return $.noop();
1504+      };
1505+
1506+      EditableCaret.prototype.getOldIEPos = function() {
1507+        var preCaretTextRange, textRange;
1508+        textRange = oDocument.selection.createRange();
1509+        preCaretTextRange = oDocument.body.createTextRange();
1510+        preCaretTextRange.moveToElementText(this.domInputor);
1511+        preCaretTextRange.setEndPoint("EndToEnd", textRange);
1512+        return preCaretTextRange.text.length;
1513+      };
1514+
1515+      EditableCaret.prototype.getPos = function() {
1516+        var clonedRange, pos, range;
1517+        if (range = this.range()) {
1518+          clonedRange = range.cloneRange();
1519+          clonedRange.selectNodeContents(this.domInputor);
1520+          clonedRange.setEnd(range.endContainer, range.endOffset);
1521+          pos = clonedRange.toString().length;
1522+          clonedRange.detach();
1523+          return pos;
1524+        } else if (oDocument.selection) {
1525+          return this.getOldIEPos();
1526+        }
1527+      };
1528+
1529+      EditableCaret.prototype.getOldIEOffset = function() {
1530+        var range, rect;
1531+        range = oDocument.selection.createRange().duplicate();
1532+        range.moveStart("character", -1);
1533+        rect = range.getBoundingClientRect();
1534+        return {
1535+          height: rect.bottom - rect.top,
1536+          left: rect.left,
1537+          top: rect.top
1538+        };
1539+      };
1540+
1541+      EditableCaret.prototype.getOffset = function(pos) {
1542+        var clonedRange, offset, range, rect;
1543+        if (oWindow.getSelection && (range = this.range())) {
1544+          if (range.endOffset - 1 < 0) {
1545+            return null;
1546+          }
1547+          clonedRange = range.cloneRange();
1548+          clonedRange.setStart(range.endContainer, range.endOffset - 1);
1549+          clonedRange.setEnd(range.endContainer, range.endOffset);
1550+          rect = clonedRange.getBoundingClientRect();
1551+          offset = {
1552+            height: rect.height,
1553+            left: rect.left + rect.width,
1554+            top: rect.top
1555+          };
1556+          clonedRange.detach();
1557+        } else if (oDocument.selection) {
1558+          offset = this.getOldIEOffset();
1559+        }
1560+        if (offset && !oFrame) {
1561+          offset.top += $(oWindow).scrollTop();
1562+          offset.left += $(oWindow).scrollLeft();
1563+        }
1564+        return offset;
1565+      };
1566+
1567+      EditableCaret.prototype.range = function() {
1568+        var sel;
1569+        if (!oWindow.getSelection) {
1570+          return;
1571+        }
1572+        sel = oWindow.getSelection();
1573+        if (sel.rangeCount > 0) {
1574+          return sel.getRangeAt(0);
1575+        } else {
1576+          return null;
1577+        }
1578+      };
1579+
1580+      return EditableCaret;
1581+
1582+    })();
1583+    InputCaret = (function() {
1584+      function InputCaret($inputor) {
1585+        this.$inputor = $inputor;
1586+        this.domInputor = this.$inputor[0];
1587+      }
1588+
1589+      InputCaret.prototype.getIEPos = function() {
1590+        var endRange, inputor, len, normalizedValue, pos, range, textInputRange;
1591+        inputor = this.domInputor;
1592+        range = oDocument.selection.createRange();
1593+        pos = 0;
1594+        if (range && range.parentElement() === inputor) {
1595+          normalizedValue = inputor.value.replace(/\r\n/g, "\n");
1596+          len = normalizedValue.length;
1597+          textInputRange = inputor.createTextRange();
1598+          textInputRange.moveToBookmark(range.getBookmark());
1599+          endRange = inputor.createTextRange();
1600+          endRange.collapse(false);
1601+          if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
1602+            pos = len;
1603+          } else {
1604+            pos = -textInputRange.moveStart("character", -len);
1605+          }
1606+        }
1607+        return pos;
1608+      };
1609+
1610+      InputCaret.prototype.getPos = function() {
1611+        if (oDocument.selection) {
1612+          return this.getIEPos();
1613+        } else {
1614+          return this.domInputor.selectionStart;
1615+        }
1616+      };
1617+
1618+      InputCaret.prototype.setPos = function(pos) {
1619+        var inputor, range;
1620+        inputor = this.domInputor;
1621+        if (oDocument.selection) {
1622+          range = inputor.createTextRange();
1623+          range.move("character", pos);
1624+          range.select();
1625+        } else if (inputor.setSelectionRange) {
1626+          inputor.setSelectionRange(pos, pos);
1627+        }
1628+        return inputor;
1629+      };
1630+
1631+      InputCaret.prototype.getIEOffset = function(pos) {
1632+        var h, textRange, x, y;
1633+        textRange = this.domInputor.createTextRange();
1634+        pos || (pos = this.getPos());
1635+        textRange.move('character', pos);
1636+        x = textRange.boundingLeft;
1637+        y = textRange.boundingTop;
1638+        h = textRange.boundingHeight;
1639+        return {
1640+          left: x,
1641+          top: y,
1642+          height: h
1643+        };
1644+      };
1645+
1646+      InputCaret.prototype.getOffset = function(pos) {
1647+        var $inputor, offset, position;
1648+        $inputor = this.$inputor;
1649+        if (oDocument.selection) {
1650+          offset = this.getIEOffset(pos);
1651+          offset.top += $(oWindow).scrollTop() + $inputor.scrollTop();
1652+          offset.left += $(oWindow).scrollLeft() + $inputor.scrollLeft();
1653+          return offset;
1654+        } else {
1655+          offset = $inputor.offset();
1656+          position = this.getPosition(pos);
1657+          return offset = {
1658+            left: offset.left + position.left - $inputor.scrollLeft(),
1659+            top: offset.top + position.top - $inputor.scrollTop(),
1660+            height: position.height
1661+          };
1662+        }
1663+      };
1664+
1665+      InputCaret.prototype.getPosition = function(pos) {
1666+        var $inputor, at_rect, end_range, format, html, mirror, start_range;
1667+        $inputor = this.$inputor;
1668+        format = function(value) {
1669+          return value.replace(/</g, '&lt').replace(/>/g, '&gt').replace(/`/g, '&#96').replace(/"/g, '&quot').replace(/\r\n|\r|\n/g, "<br />");
1670+        };
1671+        if (pos === void 0) {
1672+          pos = this.getPos();
1673+        }
1674+        start_range = $inputor.val().slice(0, pos);
1675+        end_range = $inputor.val().slice(pos);
1676+        html = "<span style='position: relative; display: inline;'>" + format(start_range) + "</span>";
1677+        html += "<span id='caret' style='position: relative; display: inline;'>|</span>";
1678+        html += "<span style='position: relative; display: inline;'>" + format(end_range) + "</span>";
1679+        mirror = new Mirror($inputor);
1680+        return at_rect = mirror.create(html).rect();
1681+      };
1682+
1683+      InputCaret.prototype.getIEPosition = function(pos) {
1684+        var h, inputorOffset, offset, x, y;
1685+        offset = this.getIEOffset(pos);
1686+        inputorOffset = this.$inputor.offset();
1687+        x = offset.left - inputorOffset.left;
1688+        y = offset.top - inputorOffset.top;
1689+        h = offset.height;
1690+        return {
1691+          left: x,
1692+          top: y,
1693+          height: h
1694+        };
1695+      };
1696+
1697+      return InputCaret;
1698+
1699+    })();
1700+    Mirror = (function() {
1701+      Mirror.prototype.css_attr = ["borderBottomWidth", "borderLeftWidth", "borderRightWidth", "borderTopStyle", "borderRightStyle", "borderBottomStyle", "borderLeftStyle", "borderTopWidth", "boxSizing", "fontFamily", "fontSize", "fontWeight", "height", "letterSpacing", "lineHeight", "marginBottom", "marginLeft", "marginRight", "marginTop", "outlineWidth", "overflow", "overflowX", "overflowY", "paddingBottom", "paddingLeft", "paddingRight", "paddingTop", "textAlign", "textOverflow", "textTransform", "whiteSpace", "wordBreak", "wordWrap"];
1702+
1703+      function Mirror($inputor) {
1704+        this.$inputor = $inputor;
1705+      }
1706+
1707+      Mirror.prototype.mirrorCss = function() {
1708+        var css,
1709+          _this = this;
1710+        css = {
1711+          position: 'absolute',
1712+          left: -9999,
1713+          top: 0,
1714+          zIndex: -20000
1715+        };
1716+        if (this.$inputor.prop('tagName') === 'TEXTAREA') {
1717+          this.css_attr.push('width');
1718+        }
1719+        $.each(this.css_attr, function(i, p) {
1720+          return css[p] = _this.$inputor.css(p);
1721+        });
1722+        return css;
1723+      };
1724+
1725+      Mirror.prototype.create = function(html) {
1726+        this.$mirror = $('<div></div>');
1727+        this.$mirror.css(this.mirrorCss());
1728+        this.$mirror.html(html);
1729+        this.$inputor.after(this.$mirror);
1730+        return this;
1731+      };
1732+
1733+      Mirror.prototype.rect = function() {
1734+        var $flag, pos, rect;
1735+        $flag = this.$mirror.find("#caret");
1736+        pos = $flag.position();
1737+        rect = {
1738+          left: pos.left,
1739+          top: pos.top,
1740+          height: $flag.height()
1741+        };
1742+        this.$mirror.remove();
1743+        return rect;
1744+      };
1745+
1746+      return Mirror;
1747+
1748+    })();
1749+    Utils = {
1750+      contentEditable: function($inputor) {
1751+        return !!($inputor[0].contentEditable && $inputor[0].contentEditable === 'true');
1752+      }
1753+    };
1754+    methods = {
1755+      pos: function(pos) {
1756+        if (pos || pos === 0) {
1757+          return this.setPos(pos);
1758+        } else {
1759+          return this.getPos();
1760+        }
1761+      },
1762+      position: function(pos) {
1763+        if (oDocument.selection) {
1764+          return this.getIEPosition(pos);
1765+        } else {
1766+          return this.getPosition(pos);
1767+        }
1768+      },
1769+      offset: function(pos) {
1770+        var iOffset, offset;
1771+        offset = this.getOffset(pos);
1772+        if (oFrame) {
1773+          iOffset = $(oFrame).offset();
1774+          offset.top += iOffset.top;
1775+          offset.left += iOffset.left;
1776+        }
1777+        return offset;
1778+      }
1779+    };
1780+    oDocument = null;
1781+    oWindow = null;
1782+    oFrame = null;
1783+    setContextBy = function(settings) {
1784+      var iframe;
1785+      if (iframe = settings != null ? settings.iframe : void 0) {
1786+        oFrame = iframe;
1787+        oWindow = iframe.contentWindow;
1788+        return oDocument = iframe.contentDocument || oWindow.document;
1789+      } else {
1790+        oFrame = void 0;
1791+        oWindow = window;
1792+        return oDocument = document;
1793+      }
1794+    };
1795+    discoveryIframeOf = function($dom) {
1796+      var error;
1797+      oDocument = $dom[0].ownerDocument;
1798+      oWindow = oDocument.defaultView || oDocument.parentWindow;
1799+      try {
1800+        return oFrame = oWindow.frameElement;
1801+      } catch (_error) {
1802+        error = _error;
1803+      }
1804+    };
1805+    $.fn.caret = function(method, value, settings) {
1806+      var caret;
1807+      if (methods[method]) {
1808+        if ($.isPlainObject(value)) {
1809+          setContextBy(value);
1810+          value = void 0;
1811+        } else {
1812+          setContextBy(settings);
1813+        }
1814+        caret = Utils.contentEditable(this) ? new EditableCaret(this) : new InputCaret(this);
1815+        return methods[method].apply(caret, [value]);
1816+      } else {
1817+        return $.error("Method " + method + " does not exist on jQuery.caret");
1818+      }
1819+    };
1820+    $.fn.caret.EditableCaret = EditableCaret;
1821+    $.fn.caret.InputCaret = InputCaret;
1822+    $.fn.caret.Utils = Utils;
1823+    return $.fn.caret.apis = methods;
1824+  });
1825+
1826+}).call(this);
1827diff --git a/src/bp-core/js/jquery.caret.txt b/src/bp-core/js/jquery.caret.txt
1828new file mode 100755
1829index 0000000..36cd1c1
1830--- /dev/null
1831+++ b/src/bp-core/js/jquery.caret.txt
1832@@ -0,0 +1,22 @@
1833+Copyright (c) 2013 chord.luo@gmail.com
1834+
1835+Permission is hereby granted, free of charge, to any person
1836+obtaining a copy of this software and associated documentation
1837+files (the "Software"), to deal in the Software without
1838+restriction, including without limitation the rights to use,
1839+copy, modify, merge, publish, distribute, sublicense, and/or sell
1840+copies of the Software, and to permit persons to whom the
1841+Software is furnished to do so, subject to the following
1842+conditions:
1843+
1844+The above copyright notice and this permission notice shall be
1845+included in all copies or substantial portions of the Software.
1846+
1847+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
1848+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
1849+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
1850+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
1851+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
1852+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
1853+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1854+OTHER DEALINGS IN THE SOFTWARE.
1855diff --git a/src/bp-friends/bp-friends-functions.php b/src/bp-friends/bp-friends-functions.php
1856index 5f797c7..9970c63 100644
1857--- a/src/bp-friends/bp-friends-functions.php
1858+++ b/src/bp-friends/bp-friends-functions.php
1859@@ -566,3 +566,41 @@ function friends_remove_data( $user_id ) {
1860 add_action( 'wpmu_delete_user',  'friends_remove_data' );
1861 add_action( 'delete_user',       'friends_remove_data' );
1862 add_action( 'bp_make_spam_user', 'friends_remove_data' );
1863+
1864+/**
1865+ * Used by the Activity component's @mentions to print a JSON list of the current user's friends.
1866+ *
1867+ * This is intended to speed up @mentions lookups for a majority of use cases.
1868+ *
1869+ * @see bp_activity_mentions_script()
1870+ */
1871+function bp_friends_prime_mentions_results() {
1872+       if ( ! bp_activity_do_mentions() || ! bp_is_user_active() ) {
1873+               return;
1874+       }
1875+
1876+       $friends_query = array(
1877+               'count_total'     => '',                    // Prevents total count
1878+               'populate_extras' => false,
1879+
1880+               'type'            => 'alphabetical',
1881+               'user_id'         => get_current_user_id(),
1882+       );
1883+
1884+       $friends_query = new BP_User_Query( $friends_query );
1885+       $results       = array();
1886+
1887+       foreach ( $friends_query->results as $user ) {
1888+               $result        = new stdClass();
1889+               $result->ID    = $user->user_nicename;
1890+               $result->image = bp_core_fetch_avatar( array( 'html' => false, 'item_id' => $user->ID ) );
1891+               $result->name  = bp_core_get_user_displayname( $user->ID );
1892+
1893+               $results[] = $result;
1894+       }
1895+
1896+       wp_localize_script( 'bp-mentions', 'BP_Suggestions', array(
1897+               'friends' => $results,
1898+       ) );
1899+}
1900+add_action( 'bp_activity_mentions_prime_results', 'bp_friends_prime_mentions_results' );
1901\ No newline at end of file
1902diff --git a/src/bp-groups/bp-groups-classes.php b/src/bp-groups/bp-groups-classes.php
1903index 02e26be..7121ea4 100644
1904--- a/src/bp-groups/bp-groups-classes.php
1905+++ b/src/bp-groups/bp-groups-classes.php
1906@@ -4387,6 +4387,7 @@ class BP_Groups_Member_Suggestions extends BP_Members_Suggestions {
1907                        'page'            => 1,
1908                        'per_page'        => $this->args['limit'],
1909                        'search_terms'    => $this->args['term'],
1910+                       'search_wildcard' => 'right',
1911                );
1912 
1913                // Only return matches of friends of this user.
1914diff --git a/src/bp-templates/bp-legacy/buddypress/activity/entry.php b/src/bp-templates/bp-legacy/buddypress/activity/entry.php
1915index 24e296b..2d759b9 100644
1916--- a/src/bp-templates/bp-legacy/buddypress/activity/entry.php
1917+++ b/src/bp-templates/bp-legacy/buddypress/activity/entry.php
1918@@ -97,7 +97,7 @@
1919                                        <div class="ac-reply-avatar"><?php bp_loggedin_user_avatar( 'width=' . BP_AVATAR_THUMB_WIDTH . '&height=' . BP_AVATAR_THUMB_HEIGHT ); ?></div>
1920                                        <div class="ac-reply-content">
1921                                                <div class="ac-textarea">
1922-                                                       <textarea id="ac-input-<?php bp_activity_id(); ?>" class="ac-input" name="ac_input_<?php bp_activity_id(); ?>"></textarea>
1923+                                                       <textarea id="ac-input-<?php bp_activity_id(); ?>" class="ac-input bp-suggestions" name="ac_input_<?php bp_activity_id(); ?>"></textarea>
1924                                                </div>
1925                                                <input type="submit" name="ac_form_submit" value="<?php esc_attr_e( 'Post', 'buddypress' ); ?>" /> &nbsp; <a href="#" class="ac-reply-cancel"><?php _e( 'Cancel', 'buddypress' ); ?></a>
1926                                                <input type="hidden" name="comment_form_id" value="<?php bp_activity_id(); ?>" />
1927diff --git a/src/bp-templates/bp-legacy/buddypress/activity/post-form.php b/src/bp-templates/bp-legacy/buddypress/activity/post-form.php
1928index acf7368..a45213a 100644
1929--- a/src/bp-templates/bp-legacy/buddypress/activity/post-form.php
1930+++ b/src/bp-templates/bp-legacy/buddypress/activity/post-form.php
1931@@ -27,7 +27,7 @@
1932 
1933        <div id="whats-new-content">
1934                <div id="whats-new-textarea">
1935-                       <textarea name="whats-new" id="whats-new" cols="50" rows="10"><?php if ( isset( $_GET['r'] ) ) : ?>@<?php echo esc_textarea( $_GET['r'] ); ?> <?php endif; ?></textarea>
1936+                       <textarea class="bp-suggestions" name="whats-new" id="whats-new" cols="50" rows="10"><?php if ( isset( $_GET['r'] ) ) : ?>@<?php echo esc_textarea( $_GET['r'] ); ?> <?php endif; ?></textarea>
1937                </div>
1938 
1939                <div id="whats-new-options">
1940diff --git a/src/bp-xprofile/bp-xprofile-functions.php b/src/bp-xprofile/bp-xprofile-functions.php
1941index 7cce675..8cedbb0 100644
1942--- a/src/bp-xprofile/bp-xprofile-functions.php
1943+++ b/src/bp-xprofile/bp-xprofile-functions.php
1944@@ -590,22 +590,29 @@ function bp_xprofile_bp_user_query_search( $sql, BP_User_Query $query ) {
1945 
1946        $bp = buddypress();
1947 
1948-       $search_terms_clean = esc_sql( esc_sql( $query->query_vars['search_terms'] ) );
1949+       $search_terms_clean = bp_esc_like( $query->query_vars['search_terms'] );
1950 
1951        if ( $query->query_vars['search_wildcard'] === 'left' ) {
1952-               $search_terms_clean = '%' . $search_terms_clean;
1953+               $search_terms_nospace = '%' . $search_terms_clean;
1954+               $search_terms_space   = '%' . $search_terms_clean . ' %';
1955        } elseif ( $query->query_vars['search_wildcard'] === 'right' ) {
1956-               $search_terms_clean = $search_terms_clean . '%';
1957+               $search_terms_nospace =        $search_terms_clean . '%';
1958+               $search_terms_space   = '% ' . $search_terms_clean . '%';
1959        } else {
1960-               $search_terms_clean = '%' . $search_terms_clean . '%';
1961+               $search_terms_nospace = '%' . $search_terms_clean . '%';
1962+               $search_terms_space   = '%' . $search_terms_clean . '%';
1963        }
1964 
1965        // Combine the core search (against wp_users) into a single OR clause
1966        // with the xprofile_data search
1967+       $search_xprofile = $wpdb->prepare(
1968+               "u.{$query->uid_name} IN ( SELECT user_id FROM {$bp->profile->table_name_data} WHERE value LIKE %s OR value LIKE %s )",
1969+               $search_terms_nospace,
1970+               $search_terms_space
1971+       );
1972+
1973        $search_core     = $sql['where']['search'];
1974-       $search_xprofile = "u.{$query->uid_name} IN ( SELECT user_id FROM {$bp->profile->table_name_data} WHERE value LIKE '{$search_terms_clean}' )";
1975        $search_combined = "( {$search_xprofile} OR {$search_core} )";
1976-
1977        $sql['where']['search'] = $search_combined;
1978 
1979        return $sql;