Skip to:
Content

BuddyPress.org

Ticket #3278: 3278.02.patch

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