Skip to:
Content

BuddyPress.org

Ticket #3278: 3278.02.patch

File 3278.02.patch, 60.5 KB (added by imath, 10 years ago)
  • Gruntfile.js

    diff --git Gruntfile.js Gruntfile.js
    index ea6b69b..bb49281 100644
    module.exports = function( grunt ) { 
    66        BUILD_DIR  = 'build/',
    77
    88        BP_CSS = [
     9                'bp-activity/css/*.css',
    910                'bp-activity/admin/css/*.css',
    1011                'bp-core/admin/css/*.css',
    1112                'bp-core/css/*.css',
    module.exports = function( grunt ) { 
    1819        ],
    1920
    2021        BP_JS = [
     22                'bp-activity/js/*.js',
    2123                'bp-activity/admin/js/*.js',
    2224                'bp-core/js/*.js',
    2325                'bp-friends/js/*.js',
    module.exports = function( grunt ) { 
    3032        ],
    3133
    3234        BP_EXCLUDED_JS = [
     35                '!bp-core/js/jquery.atwho.js',
     36                '!bp-core/js/jquery.caret.js',
    3337                '!bp-templates/bp-legacy/js/*.js'
    3438        ];
    3539
  • src/bp-activity/bp-activity-actions.php

    diff --git src/bp-activity/bp-activity-actions.php src/bp-activity/bp-activity-actions.php
    index d8e4c23..640a585 100644
    function bp_activity_setup_akismet() { 
    647647        // Instantiate Akismet for BuddyPress
    648648        $bp->activity->akismet = new BP_Akismet();
    649649}
     650
     651/**
     652 * AJAX endpoint for Suggestions API lookups.
     653 *
     654 * @since BuddyPress (2.1.0)
     655 */
     656function bp_ajax_get_suggestions() {
     657        if ( ! bp_is_user_active() || empty( $_GET['term'] ) || empty( $_GET['type'] ) ) {
     658                wp_send_json_error( 'missing_parameter' );
     659                exit;
     660        }
     661
     662        $results = bp_core_get_suggestions( array(
     663                'term' => sanitize_text_field( $_GET['term'] ),
     664                'type' => sanitize_text_field( $_GET['type'] ),
     665        ) );
     666
     667        if ( is_wp_error( $results ) ) {
     668                wp_send_json_error( $results->get_error_message() );
     669                exit;
     670        }
     671
     672        wp_send_json_success( $results );
     673}
     674add_action( 'wp_ajax_bp_get_suggestions', 'bp_ajax_get_suggestions' );
  • src/bp-activity/bp-activity-cssjs.php

    diff --git src/bp-activity/bp-activity-cssjs.php src/bp-activity/bp-activity-cssjs.php
    index e69de29..f4fa7f3 100644
     
     1<?php
     2
     3/**
     4 * Activity component CSS/JS
     5 *
     6 * @package BuddyPress
     7 * @subpackage ActivityScripts
     8 */
     9
     10// Exit if accessed directly
     11if ( ! defined( 'ABSPATH' ) ) exit;
     12
     13/**
     14 * Enqueue @mentions JS.
     15 *
     16 * @since BuddyPress (2.1)
     17 */
     18function bp_activity_mentions_script() {
     19        if ( ! bp_activity_do_mentions() || ! bp_is_user_active() || ! ( bp_is_activity_component() || bp_is_blog_page() && is_singular() && comments_open() ) ) {
     20                return;
     21        }
     22
     23        $min  = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
     24        $file = is_rtl() ? "mentions-rtl{$min}.css" : "mentions{$min}.css";
     25
     26        wp_enqueue_script( 'bp-mentions', buddypress()->plugin_url . "bp-activity/js/mentions{$min}.js", array( 'jquery', 'jquery-atwho' ), bp_get_version(), true );
     27        wp_enqueue_style( 'bp-mentions-css', buddypress()->plugin_url . "bp-activity/css/{$file}", array(), bp_get_version() );
     28
     29        // Print a list of the current user's friends to the page for quicker @mentions lookups.
     30        do_action( 'bp_activity_mentions_prime_results' );
     31}
     32add_action( 'bp_enqueue_scripts', 'bp_activity_mentions_script' );
     33
     34/**
     35 * Enqueue @mentions JS in wp-admin.
     36 *
     37 * @since BuddyPress (2.1)
     38 */
     39function bp_activity_mentions_dashboard_script() {
     40        if ( ! bp_activity_do_mentions() || ! bp_is_user_active() || ! is_admin() ) {
     41                return;
     42        }
     43
     44        // Special handling for New/Edit screens in wp-admin
     45        if (
     46                ! get_current_screen() ||
     47                ! in_array( get_current_screen()->base, array( 'page', 'post' ) ) ||
     48                ! post_type_supports( get_current_screen()->post_type, 'editor' ) ) {
     49                return;
     50        }
     51
     52        $min  = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
     53        $file = is_rtl() ? "mentions-rtl{$min}.css" : "mentions{$min}.css";
     54
     55        wp_enqueue_script( 'bp-mentions', buddypress()->plugin_url . "bp-activity/js/mentions{$min}.js", array( 'jquery', 'jquery-atwho' ), bp_get_version(), true );
     56        wp_enqueue_style( 'bp-mentions-css', buddypress()->plugin_url . "bp-activity/css/{$file}", array(), bp_get_version() );
     57
     58        // Print a list of the current user's friends to the page for quicker @mentions lookups.
     59        do_action( 'bp_activity_mentions_prime_results' );
     60}
     61add_action( 'bp_admin_enqueue_scripts', 'bp_activity_mentions_dashboard_script' );
     62 No newline at end of file
  • src/bp-activity/bp-activity-loader.php

    diff --git src/bp-activity/bp-activity-loader.php src/bp-activity/bp-activity-loader.php
    index b0c556c..0f43660 100644
    class BP_Activity_Component extends BP_Component { 
    4747        public function includes( $includes = array() ) {
    4848                // Files to include
    4949                $includes = array(
     50                        'cssjs',
    5051                        'actions',
    5152                        'screens',
    5253                        'filters',
  • src/bp-activity/css/mentions-rtl.css

    diff --git src/bp-activity/css/mentions-rtl.css src/bp-activity/css/mentions-rtl.css
    index e69de29..7185ed6 100644
     
     1.atwho-view {
     2        background: #FFF;
     3        border-radius: 2px;
     4        border: 1px solid rgb(204, 204, 204);
     5        box-shadow: 0 0 5px rgba(204, 204, 204, 0.25), 0 0 1px #FFF;
     6        color: #D84800;
     7        display: none;
     8        font-family: sans-serif;
     9        right: 0;
     10        margin-top: 18px;
     11        position: absolute;
     12        top: 0;
     13        z-index: 1000; /* >999 for wp-admin */
     14}
     15.atwho-view ul {
     16        list-style: none;
     17        margin: auto;
     18        padding: 0;
     19}
     20.atwho-view ul li {
     21        border-bottom: 1px solid #EFEFEF;
     22        box-sizing: content-box;
     23        cursor: pointer;
     24        display: block;
     25        font-size: 14px;
     26        height: 20px;
     27        line-height: 20px;
     28        margin: 0;
     29        overflow: hidden;
     30        padding: 5px 10px;
     31}
     32.atwho-view img {
     33        border-radius: 2px;
     34        float: left;
     35        height: 20px;
     36        margin-right: 10px;
     37        width: 20px;
     38}
     39.atwho-view strong {
     40        background: #EFEFEF;
     41        font: bold;
     42}
     43.atwho-view .username strong {
     44        color: #D54E21;
     45}
     46.atwho-view small {
     47        color: #AAA;
     48        float: left;
     49        font-size: smaller;
     50        font-weight: normal;
     51        margin-right: 40px;
     52}
     53.atwho-view .cur {
     54        background: rgba(239, 239, 239, 0.5);
     55}
     56
     57@media (max-width: 900px) {
     58        .atwho-view img {
     59                float: right;
     60                margin-right: 0;
     61                margin-left: 10px;
     62        }
     63        .atwho-view small {
     64                display: none;
     65        }
     66}
     67@media (max-width: 400px) {
     68        .atwho-view ul li {
     69                font-size: 16px;
     70                line-height: 23px;
     71                padding: 13px;
     72        }
     73        .atwho-view ul li img {
     74                height: 30px;
     75                margin-top: -5px;
     76                width: 30px;
     77        }
     78}
     79 No newline at end of file
  • src/bp-activity/css/mentions.css

    diff --git src/bp-activity/css/mentions.css src/bp-activity/css/mentions.css
    index e69de29..9bc5134 100644
     
     1.atwho-view {
     2        background: #FFF;
     3        border-radius: 2px;
     4        border: 1px solid rgb(204, 204, 204);
     5        box-shadow: 0 0 5px rgba(204, 204, 204, 0.25), 0 0 1px #FFF;
     6        color: #D84800;
     7        display: none;
     8        font-family: sans-serif;
     9        left: 0;
     10        margin-top: 18px;
     11        position: absolute;
     12        top: 0;
     13        z-index: 1000; /* >999 for wp-admin */
     14}
     15.atwho-view ul {
     16        list-style: none;
     17        margin: auto;
     18        padding: 0;
     19}
     20.atwho-view ul li {
     21        border-bottom: 1px solid #EFEFEF;
     22        box-sizing: content-box;
     23        cursor: pointer;
     24        display: block;
     25        font-size: 14px;
     26        height: 20px;
     27        line-height: 20px;
     28        margin: 0;
     29        overflow: hidden;
     30        padding: 5px 10px;
     31}
     32.atwho-view img {
     33        border-radius: 2px;
     34        float: right;
     35        height: 20px;
     36        margin-left: 10px;
     37        width: 20px;
     38}
     39.atwho-view strong {
     40        background: #EFEFEF;
     41        font: bold;
     42}
     43.atwho-view .username strong {
     44        color: #D54E21;
     45}
     46.atwho-view small {
     47        color: #AAA;
     48        float: right;
     49        font-size: smaller;
     50        font-weight: normal;
     51        margin-left: 40px;
     52}
     53.atwho-view .cur {
     54        background: rgba(239, 239, 239, 0.5);
     55}
     56
     57@media (max-width: 900px) {
     58        .atwho-view img {
     59                float: left;
     60                margin-left: 0;
     61                margin-right: 10px;
     62        }
     63        .atwho-view small {
     64                display: none;
     65        }
     66}
     67@media (max-width: 400px) {
     68        .atwho-view ul li {
     69                font-size: 16px;
     70                line-height: 23px;
     71                padding: 13px;
     72        }
     73        .atwho-view ul li img {
     74                height: 30px;
     75                margin-top: -5px;
     76                width: 30px;
     77        }
     78}
     79 No newline at end of file
  • src/bp-activity/js/mentions.js

    diff --git src/bp-activity/js/mentions.js src/bp-activity/js/mentions.js
    index e69de29..ae36039 100644
     
     1(function( $, undefined ) {
     2        var mentionsQueryCache = [],
     3                mentionsItem;
     4
     5        /**
     6         * Adds BuddyPress @mentions to form inputs.
     7         *
     8         * @param {array|object} options If array, becomes the suggestions' data source. If object, passed as config to $.atwho().
     9         * @since BuddyPress (2.1.0)
     10         */
     11        $.fn.bp_mentions = function( options ) {
     12                if ( $.isArray( options ) ) {
     13                        options = { data: options };
     14                }
     15
     16                /**
     17                 * Default options for at.js; see https://github.com/ichord/At.js/.
     18                 */
     19                var suggestionsDefaults = {
     20                        delay:               200,
     21                        hide_without_suffix: true,
     22                        insert_tpl:          '</>${atwho-data-value}</>', // For contentEditable, the fake tags make jQuery insert a textNode.
     23                        limit:               10,
     24                        start_with_space:    false,
     25                        suffix:              '',
     26
     27                        callbacks: {
     28                                /**
     29                                 * Custom filter to only match the start of spaced words.
     30                                 * Based on the core/default one.
     31                                 *
     32                                 * @param {string} query
     33                                 * @param {array} data
     34                                 * @param {string} search_key
     35                                 * @return {array}
     36                                 * @since BuddyPress (2.1.0)
     37                                 */
     38                                filter: function( query, data, search_key ) {
     39                                        var item, _i, _len, _results = [],
     40                                        regxp = new RegExp( '^' + query + '| ' + query, 'ig' ); // start of string, or preceded by a space.
     41
     42                                        for ( _i = 0, _len = data.length; _i < _len; _i++ ) {
     43                                                item = data[ _i ];
     44                                                if ( item[ search_key ].toLowerCase().match( regxp ) ) {
     45                                                        _results.push( item );
     46                                                }
     47                                        }
     48
     49                                        return _results;
     50                                },
     51
     52                                /**
     53                                 * Removes some spaces around highlighted string and tweaks regex to allow spaces
     54                                 * (to match display_name). Based on the core default.
     55                                 *
     56                                 * @param {unknown} li
     57                                 * @param {string} query
     58                                 * @return {string}
     59                                 * @since BuddyPress (2.1.0)
     60                                 */
     61                                highlighter: function( li, query ) {
     62                                        if ( ! query ) {
     63                                                return li;
     64                                        }
     65
     66                                        var regexp = new RegExp( '>(\\s*|[\\w\\s]*)(' + this.at.replace( '+', '\\+') + '?' + query.replace( '+', '\\+' ) + ')([\\w ]*)\\s*<', 'ig' );
     67                                        return li.replace( regexp, function( str, $1, $2, $3 ) {
     68                                                return '>' + $1 + '<strong>' + $2 + '</strong>' + $3 + '<';
     69                                        });
     70                                },
     71
     72                                /**
     73                                 * Reposition the suggestion list dynamically.
     74                                 *
     75                                 * @param {unknown} offset
     76                                 * @since BuddyPress (2.1.0)
     77                                 */
     78                                before_reposition: function( offset ) {
     79                                        var $view = $( '#atwho-ground-' + this.id + ' .atwho-view' ),
     80                                        caret     = this.$inputor.caret( 'offset', { iframe: $( '#content_ifr' )[0] } ).left,
     81                                        move;
     82
     83                                        // If the caret is past horizontal half, then flip it, yo.
     84                                        if ( caret > ( $( 'body' ).width() / 2 ) ) {
     85                                                $view.addClass( 'flip' );
     86                                                move = caret - offset.left - this.view.$el.width();
     87                                        } else {
     88                                                $view.removeClass( 'flip' );
     89                                                move = caret - offset.left + 1;
     90                                        }
     91
     92                                        offset.top  += 1;
     93                                        offset.left += move;
     94                                },
     95
     96                                /**
     97                                 * Override default behaviour which inserts junk tags in the WordPress Visual editor.
     98                                 *
     99                                 * @param {unknown} $inputor Element which we're inserting content into.
     100                                 * @param {string) content The content that will be inserted.
     101                                 * @param {string) suffix Applied to the end of the content string.
     102                                 * @return {string}
     103                                 * @since BuddyPress (2.1.0)
     104                                 */
     105                                inserting_wrapper: function( $inputor, content, suffix ) {
     106                                        var new_suffix = ( suffix === '' ) ? suffix : suffix || ' ';
     107                                        return '' + content + new_suffix;
     108                                }
     109                        }
     110                },
     111
     112                /**
     113                 * Default options for our @mentions; see https://github.com/ichord/At.js/.
     114                 */
     115                mentionsDefaults = {
     116                        callbacks: {
     117                                /**
     118                                 * If there are no matches for the query in this.data, then query BuddyPress.
     119                                 *
     120                                 * @param {string} query Partial @mention to search for.
     121                                 * @param {function} render_view Render page callback function.
     122                                 * @since BuddyPress (2.1.0)
     123                                 */
     124                                remote_filter: function( query, render_view ) {
     125                                        var self = $( this );
     126
     127                                        mentionsItem = mentionsQueryCache[ query ];
     128                                        if ( typeof mentionsItem === 'object' ) {
     129                                                render_view( mentionsItem );
     130                                                return;
     131                                        }
     132
     133                                        if ( self.xhr ) {
     134                                                self.xhr.abort();
     135                                        }
     136
     137                                        self.xhr = $.getJSON( ajaxurl, { 'action': 'bp_get_suggestions', 'term': query, 'type': 'members' } )
     138                                                /**
     139                                                 * Success callback for the @suggestions lookup.
     140                                                 *
     141                                                 * @param {object} response Details of users matching the query.
     142                                                 * @since BuddyPress (2.1.0)
     143                                                 */
     144                                                .done(function( response ) {
     145                                                        if ( ! response.success ) {
     146                                                                return;
     147                                                        }
     148
     149                                                        var data = $.map( response.data,
     150                                                                /**
     151                                                                 * Create a composite index to determine ordering of results;
     152                                                                 * nicename matches will appear on top.
     153                                                                 *
     154                                                                 * @param {array} suggestion A suggestion's original data.
     155                                                                 * @return {array} A suggestion's new data.
     156                                                                 * @since BuddyPress (2.1.0)
     157                                                                 */
     158                                                                function( suggestion ) {
     159                                                                        suggestion.search = suggestion.search || suggestion.ID + ' ' + suggestion.name;
     160                                                                        return suggestion;
     161                                                                }
     162                                                        );
     163
     164                                                        mentionsQueryCache[ query ] = data;
     165                                                        render_view( data );
     166                                                });
     167                                }
     168                        },
     169
     170                        data: $.map( options.data,
     171                                /**
     172                                 * Create a composite index to search against of nicename + display name.
     173                                 * This will also determine ordering of results, so nicename matches will appear on top.
     174                                 *
     175                                 * @param {array} suggestion A suggestion's original data.
     176                                 * @return {array} A suggestion's new data.
     177                                 * @since BuddyPress (2.1.0)
     178                                 */
     179                                function( suggestion ) {
     180                                        suggestion.search = suggestion.search || suggestion.ID + ' ' + suggestion.name;
     181                                        return suggestion;
     182                                }
     183                        ),
     184
     185                        at:         '@',
     186                        search_key: 'search',
     187                        tpl:        '<li data-value="@${ID}"><img src="${image}" /><span class="username">@${ID}</span><small>${name}</small></li>'
     188                },
     189
     190                opts = $.extend( true, {}, suggestionsDefaults, mentionsDefaults, options );
     191                return $.fn.atwho.call( this, opts );
     192        };
     193
     194        $( document ).ready(function() {
     195                var users = [];
     196
     197                if ( typeof window.BP_Suggestions === 'object' ) {
     198                        users = window.BP_Suggestions.friends || users;
     199                }
     200
     201                // Activity/reply, post comments, dashboard post 'text' editor.
     202                $( '.bp-suggestions, #comments form textarea, .wp-editor-area' ).bp_mentions( users );
     203        });
     204})( jQuery );
     205 No newline at end of file
  • src/bp-core/bp-core-classes.php

    diff --git src/bp-core/bp-core-classes.php src/bp-core/bp-core-classes.php
    index 5bbdf0b..ed9c260 100644
    class BP_User_Query { 
    368368                // 'search_terms' searches user_login and user_nicename
    369369                // xprofile field matches happen in bp_xprofile_bp_user_query_search()
    370370                if ( false !== $search_terms ) {
    371                         $search_terms_like = bp_esc_like( $search_terms );
     371                        $search_terms = bp_esc_like( $search_terms );
    372372
    373373                        if ( $search_wildcard === 'left' ) {
    374                                 $search_terms_like = '%' . $search_terms_like;
     374                                $search_terms_nospace = '%' . $search_terms;
     375                                $search_terms_space   = '%' . $search_terms . ' %';
    375376                        } elseif ( $search_wildcard === 'right' ) {
    376                                 $search_terms_like = $search_terms_like . '%';
     377                                $search_terms_nospace =        $search_terms . '%';
     378                                $search_terms_space   = '% ' . $search_terms . '%';
    377379                        } else {
    378                                 $search_terms_like = '%' . $search_terms_like . '%';
     380                                $search_terms_nospace = '%' . $search_terms . '%';
     381                                $search_terms_space   = '%' . $search_terms . '%';
    379382                        }
    380383
    381                         $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 );
     384                        $sql['where']['search'] = $wpdb->prepare(
     385                                "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 ) )",
     386                                $search_terms_nospace,
     387                                $search_terms_space,
     388                                $search_terms_nospace,
     389                                $search_terms_space
     390                        );
    382391                }
    383392
    384393                // 'meta_key', 'meta_value' allow usermeta search
    class BP_Members_Suggestions extends BP_Suggestions { 
    25662575         * }
    25672576         */
    25682577        protected $default_args = array(
    2569                 'limit'        => 16,
     2578                'limit'        => 10,
    25702579                'only_friends' => false,
    25712580                'term'         => '',
    25722581                'type'         => '',
    class BP_Members_Suggestions extends BP_Suggestions { 
    26062615                        'page'            => 1,
    26072616                        'per_page'        => $this->args['limit'],
    26082617                        'search_terms'    => $this->args['term'],
     2618                        'search_wildcard' => is_rtl() ? 'left' : 'right',
    26092619                );
    26102620
    26112621                // Only return matches of friends of this user.
  • src/bp-core/bp-core-cssjs.php

    diff --git src/bp-core/bp-core-cssjs.php src/bp-core/bp-core-cssjs.php
    index b6fa396..c7f7011 100644
    function bp_core_register_common_scripts() { 
    1919        $url = buddypress()->plugin_url . 'bp-core/js/';
    2020       
    2121        $scripts = apply_filters( 'bp_core_register_common_scripts', array(
     22                // Legacy
    2223                'bp-confirm'        => array( 'file' => "{$url}confirm{$ext}",        'dependencies' => array( 'jquery' ) ),
    2324                'bp-widget-members' => array( 'file' => "{$url}widget-members{$ext}", 'dependencies' => array( 'jquery' ) ),
    2425                'bp-jquery-query'   => array( 'file' => "{$url}jquery-query{$ext}",   'dependencies' => array( 'jquery' ) ),
    2526                'bp-jquery-cookie'  => array( 'file' => "{$url}jquery-cookie{$ext}",  'dependencies' => array( 'jquery' ) ),
     27
     28                // 2.1
     29                'jquery-caret' => array( 'file' => "{$url}jquery.caret{$ext}", 'dependencies' => array( 'jquery' ) ),
     30                'jquery-atwho' => array( 'file' => "{$url}jquery.atwho{$ext}", 'dependencies' => array( 'jquery', 'jquery-caret' ) ),
    2631        ) );
    2732
    2833        foreach ( $scripts as $id => $script ) {
  • src/bp-core/js/jquery.atwho.js

    diff --git src/bp-core/js/jquery.atwho.js src/bp-core/js/jquery.atwho.js
    index e69de29..d5eefe3 100644
     
     1/*! jquery.atwho - v0.5.0 - 2014-07-14
     2* Copyright (c) 2014 chord.luo <chord.luo@gmail.com>;
     3* homepage: http://ichord.github.com/At.js
     4* Licensed MIT
     5*/
     6
     7(function() {
     8  (function(factory) {
     9    if (typeof define === 'function' && define.amd) {
     10      return define(['jquery'], factory);
     11    } else {
     12      return factory(window.jQuery);
     13    }
     14  })(function($) {
     15
     16var $CONTAINER, Api, App, Controller, DEFAULT_CALLBACKS, KEY_CODE, Model, View,
     17  __slice = [].slice;
     18
     19App = (function() {
     20  function App(inputor) {
     21    this.current_flag = null;
     22    this.controllers = {};
     23    this.alias_maps = {};
     24    this.$inputor = $(inputor);
     25    this.iframe = null;
     26    this.setIframe();
     27    this.listen();
     28  }
     29
     30  App.prototype.setIframe = function(iframe) {
     31    if (iframe) {
     32      this.window = iframe.contentWindow;
     33      this.document = iframe.contentDocument || this.window.document;
     34      return this.iframe = iframe;
     35    } else {
     36      this.document = document;
     37      this.window = window;
     38      return this.iframe = null;
     39    }
     40  };
     41
     42  App.prototype.controller = function(at) {
     43    var c, current, current_flag, _ref;
     44    if (this.alias_maps[at]) {
     45      current = this.controllers[this.alias_maps[at]];
     46    } else {
     47      _ref = this.controllers;
     48      for (current_flag in _ref) {
     49        c = _ref[current_flag];
     50        if (current_flag === at) {
     51          current = c;
     52          break;
     53        }
     54      }
     55    }
     56    if (current) {
     57      return current;
     58    } else {
     59      return this.controllers[this.current_flag];
     60    }
     61  };
     62
     63  App.prototype.set_context_for = function(at) {
     64    this.current_flag = at;
     65    return this;
     66  };
     67
     68  App.prototype.reg = function(flag, setting) {
     69    var controller, _base;
     70    controller = (_base = this.controllers)[flag] || (_base[flag] = new Controller(this, flag));
     71    if (setting.alias) {
     72      this.alias_maps[setting.alias] = flag;
     73    }
     74    controller.init(setting);
     75    return this;
     76  };
     77
     78  App.prototype.listen = function() {
     79    return this.$inputor.on('keyup.atwhoInner', (function(_this) {
     80      return function(e) {
     81        return _this.on_keyup(e);
     82      };
     83    })(this)).on('keydown.atwhoInner', (function(_this) {
     84      return function(e) {
     85        return _this.on_keydown(e);
     86      };
     87    })(this)).on('scroll.atwhoInner', (function(_this) {
     88      return function(e) {
     89        var _ref;
     90        return (_ref = _this.controller()) != null ? _ref.view.hide(e) : void 0;
     91      };
     92    })(this)).on('blur.atwhoInner', (function(_this) {
     93      return function(e) {
     94        var c;
     95        if (c = _this.controller()) {
     96          return c.view.hide(e, c.get_opt("display_timeout"));
     97        }
     98      };
     99    })(this)).on('click.atwhoInner', (function(_this) {
     100      return function(e) {
     101        var _ref;
     102        return (_ref = _this.controller()) != null ? _ref.view.hide(e) : void 0;
     103      };
     104    })(this));
     105  };
     106
     107  App.prototype.shutdown = function() {
     108    var c, _, _ref;
     109    _ref = this.controllers;
     110    for (_ in _ref) {
     111      c = _ref[_];
     112      c.destroy();
     113      delete this.controllers[_];
     114    }
     115    return this.$inputor.off('.atwhoInner');
     116  };
     117
     118  App.prototype.dispatch = function() {
     119    return $.map(this.controllers, (function(_this) {
     120      return function(c) {
     121        var delay;
     122        if (delay = c.get_opt('delay')) {
     123          clearTimeout(_this.delayedCallback);
     124          return _this.delayedCallback = setTimeout(function() {
     125            if (c.look_up()) {
     126              return _this.set_context_for(c.at);
     127            }
     128          }, delay);
     129        } else {
     130          if (c.look_up()) {
     131            return _this.set_context_for(c.at);
     132          }
     133        }
     134      };
     135    })(this));
     136  };
     137
     138  App.prototype.on_keyup = function(e) {
     139    var _ref;
     140    switch (e.keyCode) {
     141      case KEY_CODE.ESC:
     142        e.preventDefault();
     143        if ((_ref = this.controller()) != null) {
     144          _ref.view.hide();
     145        }
     146        break;
     147      case KEY_CODE.DOWN:
     148      case KEY_CODE.UP:
     149      case KEY_CODE.CTRL:
     150        $.noop();
     151        break;
     152      case KEY_CODE.P:
     153      case KEY_CODE.N:
     154        if (!e.ctrlKey) {
     155          this.dispatch();
     156        }
     157        break;
     158      default:
     159        this.dispatch();
     160    }
     161  };
     162
     163  App.prototype.on_keydown = function(e) {
     164    var view, _ref;
     165    view = (_ref = this.controller()) != null ? _ref.view : void 0;
     166    if (!(view && view.visible())) {
     167      return;
     168    }
     169    switch (e.keyCode) {
     170      case KEY_CODE.ESC:
     171        e.preventDefault();
     172        view.hide(e);
     173        break;
     174      case KEY_CODE.UP:
     175        e.preventDefault();
     176        view.prev();
     177        break;
     178      case KEY_CODE.DOWN:
     179        e.preventDefault();
     180        view.next();
     181        break;
     182      case KEY_CODE.P:
     183        if (!e.ctrlKey) {
     184          return;
     185        }
     186        e.preventDefault();
     187        view.prev();
     188        break;
     189      case KEY_CODE.N:
     190        if (!e.ctrlKey) {
     191          return;
     192        }
     193        e.preventDefault();
     194        view.next();
     195        break;
     196      case KEY_CODE.TAB:
     197      case KEY_CODE.ENTER:
     198        if (!view.visible()) {
     199          return;
     200        }
     201        e.preventDefault();
     202        view.choose(e);
     203        break;
     204      default:
     205        $.noop();
     206    }
     207  };
     208
     209  return App;
     210
     211})();
     212
     213Controller = (function() {
     214  Controller.prototype.uid = function() {
     215    return (Math.random().toString(16) + "000000000").substr(2, 8) + (new Date().getTime());
     216  };
     217
     218  function Controller(app, at) {
     219    this.app = app;
     220    this.at = at;
     221    this.$inputor = this.app.$inputor;
     222    this.id = this.$inputor[0].id || this.uid();
     223    this.setting = null;
     224    this.query = null;
     225    this.pos = 0;
     226    this.cur_rect = null;
     227    this.range = null;
     228    $CONTAINER.append(this.$el = $("<div id='atwho-ground-" + this.id + "'></div>"));
     229    this.model = new Model(this);
     230    this.view = new View(this);
     231  }
     232
     233  Controller.prototype.init = function(setting) {
     234    this.setting = $.extend({}, this.setting || $.fn.atwho["default"], setting);
     235    this.view.init();
     236    return this.model.reload(this.setting.data);
     237  };
     238
     239  Controller.prototype.destroy = function() {
     240    this.trigger('beforeDestroy');
     241    this.model.destroy();
     242    this.view.destroy();
     243    return this.$el.remove();
     244  };
     245
     246  Controller.prototype.call_default = function() {
     247    var args, error, func_name;
     248    func_name = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
     249    try {
     250      return DEFAULT_CALLBACKS[func_name].apply(this, args);
     251    } catch (_error) {
     252      error = _error;
     253      return $.error("" + error + " Or maybe At.js doesn't have function " + func_name);
     254    }
     255  };
     256
     257  Controller.prototype.trigger = function(name, data) {
     258    var alias, event_name;
     259    if (data == null) {
     260      data = [];
     261    }
     262    data.push(this);
     263    alias = this.get_opt('alias');
     264    event_name = alias ? "" + name + "-" + alias + ".atwho" : "" + name + ".atwho";
     265    return this.$inputor.trigger(event_name, data);
     266  };
     267
     268  Controller.prototype.callbacks = function(func_name) {
     269    return this.get_opt("callbacks")[func_name] || DEFAULT_CALLBACKS[func_name];
     270  };
     271
     272  Controller.prototype.get_opt = function(at, default_value) {
     273    var e;
     274    try {
     275      return this.setting[at];
     276    } catch (_error) {
     277      e = _error;
     278      return null;
     279    }
     280  };
     281
     282  Controller.prototype.content = function() {
     283    if (this.$inputor.is('textarea, input')) {
     284      return this.$inputor.val();
     285    } else {
     286      return this.$inputor.text();
     287    }
     288  };
     289
     290  Controller.prototype.catch_query = function() {
     291    var caret_pos, content, end, query, start, subtext;
     292    content = this.content();
     293    caret_pos = this.$inputor.caret('pos', {
     294      iframe: this.app.iframe
     295    });
     296    subtext = content.slice(0, caret_pos);
     297    query = this.callbacks("matcher").call(this, this.at, subtext, this.get_opt('start_with_space'));
     298    if (typeof query === "string" && query.length <= this.get_opt('max_len', 20)) {
     299      start = caret_pos - query.length;
     300      end = start + query.length;
     301      this.pos = start;
     302      query = {
     303        'text': query,
     304        'head_pos': start,
     305        'end_pos': end
     306      };
     307      this.trigger("matched", [this.at, query.text]);
     308    } else {
     309      query = null;
     310      this.view.hide();
     311    }
     312    return this.query = query;
     313  };
     314
     315  Controller.prototype.rect = function() {
     316    var c, scale_bottom;
     317    if (!(c = this.$inputor.caret('offset', this.pos - 1, {
     318      iframe: this.app.iframe
     319    }))) {
     320      return;
     321    }
     322    if (this.$inputor.attr('contentEditable') === 'true') {
     323      c = (this.cur_rect || (this.cur_rect = c)) || c;
     324    }
     325    scale_bottom = this.app.document.selection ? 0 : 2;
     326    return {
     327      left: c.left,
     328      top: c.top,
     329      bottom: c.top + c.height + scale_bottom
     330    };
     331  };
     332
     333  Controller.prototype.reset_rect = function() {
     334    if (this.$inputor.attr('contentEditable') === 'true') {
     335      return this.cur_rect = null;
     336    }
     337  };
     338
     339  Controller.prototype.mark_range = function() {
     340    if (this.$inputor.attr('contentEditable') === 'true') {
     341      if (this.app.window.getSelection) {
     342        this.range = this.app.window.getSelection().getRangeAt(0);
     343      }
     344      if (this.app.document.selection) {
     345        return this.ie8_range = this.app.document.selection.createRange();
     346      }
     347    }
     348  };
     349
     350  Controller.prototype.insert_content_for = function($li) {
     351    var data, data_value, tpl;
     352    data_value = $li.data('value');
     353    tpl = this.get_opt('insert_tpl');
     354    if (this.$inputor.is('textarea, input') || !tpl) {
     355      return data_value;
     356    }
     357    data = $.extend({}, $li.data('item-data'), {
     358      'atwho-data-value': data_value,
     359      'atwho-at': this.at
     360    });
     361    return this.callbacks("tpl_eval").call(this, tpl, data);
     362  };
     363
     364  Controller.prototype.insert = function(content, $li) {
     365    var $inputor, content_node, pos, range, sel, source, start_str, text, wrapped_content;
     366    $inputor = this.$inputor;
     367    wrapped_content = this.callbacks('inserting_wrapper').call(this, $inputor, content, this.get_opt("suffix"));
     368    if ($inputor.is('textarea, input')) {
     369      source = $inputor.val();
     370      start_str = source.slice(0, Math.max(this.query.head_pos - this.at.length, 0));
     371      text = "" + start_str + wrapped_content + (source.slice(this.query['end_pos'] || 0));
     372      $inputor.val(text);
     373      $inputor.caret('pos', start_str.length + wrapped_content.length, {
     374        iframe: this.app.iframe
     375      });
     376    } else if (range = this.range) {
     377      pos = range.startOffset - (this.query.end_pos - this.query.head_pos) - this.at.length;
     378      range.setStart(range.endContainer, Math.max(pos, 0));
     379      range.setEnd(range.endContainer, range.endOffset);
     380      range.deleteContents();
     381      content_node = $(wrapped_content, this.app.document)[0];
     382      range.insertNode(content_node);
     383      range.setEndAfter(content_node);
     384      range.collapse(false);
     385      sel = this.app.window.getSelection();
     386      sel.removeAllRanges();
     387      sel.addRange(range);
     388    } else if (range = this.ie8_range) {
     389      range.moveStart('character', this.query.end_pos - this.query.head_pos - this.at.length);
     390      range.pasteHTML(wrapped_content);
     391      range.collapse(false);
     392      range.select();
     393    }
     394    if (!$inputor.is(':focus')) {
     395      $inputor.focus();
     396    }
     397    return $inputor.change();
     398  };
     399
     400  Controller.prototype.render_view = function(data) {
     401    var search_key;
     402    search_key = this.get_opt("search_key");
     403    data = this.callbacks("sorter").call(this, this.query.text, data.slice(0, 1001), search_key);
     404    return this.view.render(data.slice(0, this.get_opt('limit')));
     405  };
     406
     407  Controller.prototype.look_up = function() {
     408    var query, _callback;
     409    if (!(query = this.catch_query())) {
     410      return;
     411    }
     412    _callback = function(data) {
     413      if (data && data.length > 0) {
     414        return this.render_view(data);
     415      } else {
     416        return this.view.hide();
     417      }
     418    };
     419    this.model.query(query.text, $.proxy(_callback, this));
     420    return query;
     421  };
     422
     423  return Controller;
     424
     425})();
     426
     427Model = (function() {
     428  function Model(context) {
     429    this.context = context;
     430    this.at = this.context.at;
     431    this.storage = this.context.$inputor;
     432  }
     433
     434  Model.prototype.destroy = function() {
     435    return this.storage.data(this.at, null);
     436  };
     437
     438  Model.prototype.saved = function() {
     439    return this.fetch() > 0;
     440  };
     441
     442  Model.prototype.query = function(query, callback) {
     443    var data, search_key, _remote_filter;
     444    data = this.fetch();
     445    search_key = this.context.get_opt("search_key");
     446    data = this.context.callbacks('filter').call(this.context, query, data, search_key) || [];
     447    _remote_filter = this.context.callbacks('remote_filter');
     448    if (data.length > 0 || (!_remote_filter && data.length === 0)) {
     449      return callback(data);
     450    } else {
     451      return _remote_filter.call(this.context, query, callback);
     452    }
     453  };
     454
     455  Model.prototype.fetch = function() {
     456    return this.storage.data(this.at) || [];
     457  };
     458
     459  Model.prototype.save = function(data) {
     460    return this.storage.data(this.at, this.context.callbacks("before_save").call(this.context, data || []));
     461  };
     462
     463  Model.prototype.load = function(data) {
     464    if (!(this.saved() || !data)) {
     465      return this._load(data);
     466    }
     467  };
     468
     469  Model.prototype.reload = function(data) {
     470    return this._load(data);
     471  };
     472
     473  Model.prototype._load = function(data) {
     474    if (typeof data === "string") {
     475      return $.ajax(data, {
     476        dataType: "json"
     477      }).done((function(_this) {
     478        return function(data) {
     479          return _this.save(data);
     480        };
     481      })(this));
     482    } else {
     483      return this.save(data);
     484    }
     485  };
     486
     487  return Model;
     488
     489})();
     490
     491View = (function() {
     492  function View(context) {
     493    this.context = context;
     494    this.$el = $("<div class='atwho-view'><ul class='atwho-view-ul'></ul></div>");
     495    this.timeout_id = null;
     496    this.context.$el.append(this.$el);
     497    this.bind_event();
     498  }
     499
     500  View.prototype.init = function() {
     501    var id;
     502    id = this.context.get_opt("alias") || this.context.at.charCodeAt(0);
     503    return this.$el.attr({
     504      'id': "at-view-" + id
     505    });
     506  };
     507
     508  View.prototype.destroy = function() {
     509    return this.$el.remove();
     510  };
     511
     512  View.prototype.bind_event = function() {
     513    var $menu;
     514    $menu = this.$el.find('ul');
     515    return $menu.on('mouseenter.atwho-view', 'li', function(e) {
     516      $menu.find('.cur').removeClass('cur');
     517      return $(e.currentTarget).addClass('cur');
     518    }).on('click', (function(_this) {
     519      return function(e) {
     520        _this.choose(e);
     521        return e.preventDefault();
     522      };
     523    })(this));
     524  };
     525
     526  View.prototype.visible = function() {
     527    return this.$el.is(":visible");
     528  };
     529
     530  View.prototype.choose = function(e) {
     531    var $li, content;
     532    if (($li = this.$el.find(".cur")).length) {
     533      content = this.context.insert_content_for($li);
     534      this.context.insert(this.context.callbacks("before_insert").call(this.context, content, $li), $li);
     535      this.context.trigger("inserted", [$li, e]);
     536      this.hide(e);
     537    }
     538    if (this.context.get_opt("hide_without_suffix")) {
     539      return this.stop_showing = true;
     540    }
     541  };
     542
     543  View.prototype.reposition = function(rect) {
     544    var offset, _ref;
     545    if (rect.bottom + this.$el.height() - $(window).scrollTop() > $(window).height()) {
     546      rect.bottom = rect.top - this.$el.height();
     547    }
     548    offset = {
     549      left: rect.left,
     550      top: rect.bottom
     551    };
     552    if ((_ref = this.context.callbacks("before_reposition")) != null) {
     553      _ref.call(this.context, offset);
     554    }
     555    this.$el.offset(offset);
     556    return this.context.trigger("reposition", [offset]);
     557  };
     558
     559  View.prototype.next = function() {
     560    var cur, next;
     561    cur = this.$el.find('.cur').removeClass('cur');
     562    next = cur.next();
     563    if (!next.length) {
     564      next = this.$el.find('li:first');
     565    }
     566    return next.addClass('cur');
     567  };
     568
     569  View.prototype.prev = function() {
     570    var cur, prev;
     571    cur = this.$el.find('.cur').removeClass('cur');
     572    prev = cur.prev();
     573    if (!prev.length) {
     574      prev = this.$el.find('li:last');
     575    }
     576    return prev.addClass('cur');
     577  };
     578
     579  View.prototype.show = function() {
     580    var rect;
     581    if (this.stop_showing) {
     582      this.stop_showing = false;
     583      return;
     584    }
     585    this.context.mark_range();
     586    if (!this.visible()) {
     587      this.$el.show();
     588      this.context.trigger('shown');
     589    }
     590    if (rect = this.context.rect()) {
     591      return this.reposition(rect);
     592    }
     593  };
     594
     595  View.prototype.hide = function(e, time) {
     596    var callback;
     597    if (!this.visible()) {
     598      return;
     599    }
     600    if (isNaN(time)) {
     601      this.context.reset_rect();
     602      this.$el.hide();
     603      return this.context.trigger('hidden', [e]);
     604    } else {
     605      callback = (function(_this) {
     606        return function() {
     607          return _this.hide();
     608        };
     609      })(this);
     610      clearTimeout(this.timeout_id);
     611      return this.timeout_id = setTimeout(callback, time);
     612    }
     613  };
     614
     615  View.prototype.render = function(list) {
     616    var $li, $ul, item, li, tpl, _i, _len;
     617    if (!($.isArray(list) && list.length > 0)) {
     618      this.hide();
     619      return;
     620    }
     621    this.$el.find('ul').empty();
     622    $ul = this.$el.find('ul');
     623    tpl = this.context.get_opt('tpl');
     624    for (_i = 0, _len = list.length; _i < _len; _i++) {
     625      item = list[_i];
     626      item = $.extend({}, item, {
     627        'atwho-at': this.context.at
     628      });
     629      li = this.context.callbacks("tpl_eval").call(this.context, tpl, item);
     630      $li = $(this.context.callbacks("highlighter").call(this.context, li, this.context.query.text));
     631      $li.data("item-data", item);
     632      $ul.append($li);
     633    }
     634    this.show();
     635    if (this.context.get_opt('highlight_first')) {
     636      return $ul.find("li:first").addClass("cur");
     637    }
     638  };
     639
     640  return View;
     641
     642})();
     643
     644KEY_CODE = {
     645  DOWN: 40,
     646  UP: 38,
     647  ESC: 27,
     648  TAB: 9,
     649  ENTER: 13,
     650  CTRL: 17,
     651  P: 80,
     652  N: 78
     653};
     654
     655DEFAULT_CALLBACKS = {
     656  before_save: function(data) {
     657    var item, _i, _len, _results;
     658    if (!$.isArray(data)) {
     659      return data;
     660    }
     661    _results = [];
     662    for (_i = 0, _len = data.length; _i < _len; _i++) {
     663      item = data[_i];
     664      if ($.isPlainObject(item)) {
     665        _results.push(item);
     666      } else {
     667        _results.push({
     668          name: item
     669        });
     670      }
     671    }
     672    return _results;
     673  },
     674  matcher: function(flag, subtext, should_start_with_space) {
     675    var match, regexp;
     676    flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
     677    if (should_start_with_space) {
     678      flag = '(?:^|\\s)' + flag;
     679    }
     680    regexp = new RegExp(flag + '([A-Za-z0-9_\+\-]*)$|' + flag + '([^\\x00-\\xff]*)$', 'gi');
     681    match = regexp.exec(subtext);
     682    if (match) {
     683      return match[2] || match[1];
     684    } else {
     685      return null;
     686    }
     687  },
     688  filter: function(query, data, search_key) {
     689    var item, _i, _len, _results;
     690    _results = [];
     691    for (_i = 0, _len = data.length; _i < _len; _i++) {
     692      item = data[_i];
     693      if (~item[search_key].toLowerCase().indexOf(query.toLowerCase())) {
     694        _results.push(item);
     695      }
     696    }
     697    return _results;
     698  },
     699  remote_filter: null,
     700  sorter: function(query, items, search_key) {
     701    var item, _i, _len, _results;
     702    if (!query) {
     703      return items;
     704    }
     705    _results = [];
     706    for (_i = 0, _len = items.length; _i < _len; _i++) {
     707      item = items[_i];
     708      item.atwho_order = item[search_key].toLowerCase().indexOf(query.toLowerCase());
     709      if (item.atwho_order > -1) {
     710        _results.push(item);
     711      }
     712    }
     713    return _results.sort(function(a, b) {
     714      return a.atwho_order - b.atwho_order;
     715    });
     716  },
     717  tpl_eval: function(tpl, map) {
     718    var error;
     719    try {
     720      return tpl.replace(/\$\{([^\}]*)\}/g, function(tag, key, pos) {
     721        return map[key];
     722      });
     723    } catch (_error) {
     724      error = _error;
     725      return "";
     726    }
     727  },
     728  highlighter: function(li, query) {
     729    var regexp;
     730    if (!query) {
     731      return li;
     732    }
     733    regexp = new RegExp(">\\s*(\\w*?)(" + query.replace("+", "\\+") + ")(\\w*)\\s*<", 'ig');
     734    return li.replace(regexp, function(str, $1, $2, $3) {
     735      return '> ' + $1 + '<strong>' + $2 + '</strong>' + $3 + ' <';
     736    });
     737  },
     738  before_insert: function(value, $li) {
     739    return value;
     740  },
     741  inserting_wrapper: function($inputor, content, suffix) {
     742    var new_suffix, wrapped_content;
     743    new_suffix = suffix === "" ? suffix : suffix || " ";
     744    if ($inputor.is('textarea, input')) {
     745      return '' + content + new_suffix;
     746    } else if ($inputor.attr('contentEditable') === 'true') {
     747      new_suffix = suffix === "" ? suffix : suffix || "&nbsp;";
     748      if (/firefox/i.test(navigator.userAgent)) {
     749        wrapped_content = "<span>" + content + new_suffix + "</span>";
     750      } else {
     751        suffix = "<span contenteditable='false'>" + new_suffix + "<span>";
     752        wrapped_content = "<span contenteditable='false'>" + content + suffix + "</span>";
     753      }
     754      if (this.app.document.selection) {
     755        wrapped_content = "<span contenteditable='true'>" + content + "</span>";
     756      }
     757      return wrapped_content;
     758    }
     759  }
     760};
     761
     762Api = {
     763  load: function(at, data) {
     764    var c;
     765    if (c = this.controller(at)) {
     766      return c.model.load(data);
     767    }
     768  },
     769  setIframe: function(iframe) {
     770    this.setIframe(iframe);
     771    return null;
     772  },
     773  run: function() {
     774    return this.dispatch();
     775  },
     776  destroy: function() {
     777    this.shutdown();
     778    return this.$inputor.data('atwho', null);
     779  }
     780};
     781
     782$CONTAINER = $("<div id='atwho-container'></div>");
     783
     784$.fn.atwho = function(method) {
     785  var result, _args;
     786  _args = arguments;
     787  $('body').append($CONTAINER);
     788  result = null;
     789  this.filter('textarea, input, [contenteditable=true]').each(function() {
     790    var $this, app;
     791    if (!(app = ($this = $(this)).data("atwho"))) {
     792      $this.data('atwho', (app = new App(this)));
     793    }
     794    if (typeof method === 'object' || !method) {
     795      return app.reg(method.at, method);
     796    } else if (Api[method] && app) {
     797      return result = Api[method].apply(app, Array.prototype.slice.call(_args, 1));
     798    } else {
     799      return $.error("Method " + method + " does not exist on jQuery.caret");
     800    }
     801  });
     802  return result || this;
     803};
     804
     805$.fn.atwho["default"] = {
     806  at: void 0,
     807  alias: void 0,
     808  data: null,
     809  tpl: "<li data-value='${atwho-at}${name}'>${name}</li>",
     810  insert_tpl: "<span id='${id}'>${atwho-data-value}</span>",
     811  callbacks: DEFAULT_CALLBACKS,
     812  search_key: "name",
     813  suffix: void 0,
     814  hide_without_suffix: false,
     815  start_with_space: true,
     816  highlight_first: true,
     817  limit: 5,
     818  max_len: 20,
     819  display_timeout: 300,
     820  delay: null
     821};
     822
     823  });
     824}).call(this);
  • src/bp-core/js/jquery.atwho.txt

    diff --git src/bp-core/js/jquery.atwho.txt src/bp-core/js/jquery.atwho.txt
    index e69de29..36cd1c1 100644
     
     1Copyright (c) 2013 chord.luo@gmail.com
     2
     3Permission is hereby granted, free of charge, to any person
     4obtaining a copy of this software and associated documentation
     5files (the "Software"), to deal in the Software without
     6restriction, including without limitation the rights to use,
     7copy, modify, merge, publish, distribute, sublicense, and/or sell
     8copies of the Software, and to permit persons to whom the
     9Software is furnished to do so, subject to the following
     10conditions:
     11
     12The above copyright notice and this permission notice shall be
     13included in all copies or substantial portions of the Software.
     14
     15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
     16EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
     17OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
     18NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
     19HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
     20WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
     21FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
     22OTHER DEALINGS IN THE SOFTWARE.
  • src/bp-core/js/jquery.caret.js

    diff --git src/bp-core/js/jquery.caret.js src/bp-core/js/jquery.caret.js
    index e69de29..caa7876 100644
     
     1/*
     2  Implement Github like autocomplete mentions
     3  http://ichord.github.com/At.js
     4
     5  Copyright (c) 2013 chord.luo@gmail.com
     6  Licensed under the MIT license.
     7*/
     8
     9
     10/*
     11本插件操作 textarea 或者 input 内的插入符
     12只实现了获得插入符在文本框中的位置,我设置
     13插入符的位置.
     14*/
     15
     16
     17(function() {
     18  (function(factory) {
     19    if (typeof define === 'function' && define.amd) {
     20      return define(['jquery'], factory);
     21    } else {
     22      return factory(window.jQuery);
     23    }
     24  })(function($) {
     25    "use strict";
     26    var EditableCaret, InputCaret, Mirror, Utils, discoveryIframeOf, methods, oDocument, oFrame, oWindow, pluginName, setContextBy;
     27    pluginName = 'caret';
     28    EditableCaret = (function() {
     29      function EditableCaret($inputor) {
     30        this.$inputor = $inputor;
     31        this.domInputor = this.$inputor[0];
     32      }
     33
     34      EditableCaret.prototype.setPos = function(pos) {
     35        return this.domInputor;
     36      };
     37
     38      EditableCaret.prototype.getIEPosition = function() {
     39        return $.noop();
     40      };
     41
     42      EditableCaret.prototype.getPosition = function() {
     43        return $.noop();
     44      };
     45
     46      EditableCaret.prototype.getOldIEPos = function() {
     47        var preCaretTextRange, textRange;
     48        textRange = oDocument.selection.createRange();
     49        preCaretTextRange = oDocument.body.createTextRange();
     50        preCaretTextRange.moveToElementText(this.domInputor);
     51        preCaretTextRange.setEndPoint("EndToEnd", textRange);
     52        return preCaretTextRange.text.length;
     53      };
     54
     55      EditableCaret.prototype.getPos = function() {
     56        var clonedRange, pos, range;
     57        if (range = this.range()) {
     58          clonedRange = range.cloneRange();
     59          clonedRange.selectNodeContents(this.domInputor);
     60          clonedRange.setEnd(range.endContainer, range.endOffset);
     61          pos = clonedRange.toString().length;
     62          clonedRange.detach();
     63          return pos;
     64        } else if (oDocument.selection) {
     65          return this.getOldIEPos();
     66        }
     67      };
     68
     69      EditableCaret.prototype.getOldIEOffset = function() {
     70        var range, rect;
     71        range = oDocument.selection.createRange().duplicate();
     72        range.moveStart("character", -1);
     73        rect = range.getBoundingClientRect();
     74        return {
     75          height: rect.bottom - rect.top,
     76          left: rect.left,
     77          top: rect.top
     78        };
     79      };
     80
     81      EditableCaret.prototype.getOffset = function(pos) {
     82        var clonedRange, offset, range, rect;
     83        if (oWindow.getSelection && (range = this.range())) {
     84          if (range.endOffset - 1 < 0) {
     85            return null;
     86          }
     87          clonedRange = range.cloneRange();
     88          clonedRange.setStart(range.endContainer, range.endOffset - 1);
     89          clonedRange.setEnd(range.endContainer, range.endOffset);
     90          rect = clonedRange.getBoundingClientRect();
     91          offset = {
     92            height: rect.height,
     93            left: rect.left + rect.width,
     94            top: rect.top
     95          };
     96          clonedRange.detach();
     97        } else if (oDocument.selection) {
     98          offset = this.getOldIEOffset();
     99        }
     100        if (offset && !oFrame) {
     101          offset.top += $(oWindow).scrollTop();
     102          offset.left += $(oWindow).scrollLeft();
     103        }
     104        return offset;
     105      };
     106
     107      EditableCaret.prototype.range = function() {
     108        var sel;
     109        if (!oWindow.getSelection) {
     110          return;
     111        }
     112        sel = oWindow.getSelection();
     113        if (sel.rangeCount > 0) {
     114          return sel.getRangeAt(0);
     115        } else {
     116          return null;
     117        }
     118      };
     119
     120      return EditableCaret;
     121
     122    })();
     123    InputCaret = (function() {
     124      function InputCaret($inputor) {
     125        this.$inputor = $inputor;
     126        this.domInputor = this.$inputor[0];
     127      }
     128
     129      InputCaret.prototype.getIEPos = function() {
     130        var endRange, inputor, len, normalizedValue, pos, range, textInputRange;
     131        inputor = this.domInputor;
     132        range = oDocument.selection.createRange();
     133        pos = 0;
     134        if (range && range.parentElement() === inputor) {
     135          normalizedValue = inputor.value.replace(/\r\n/g, "\n");
     136          len = normalizedValue.length;
     137          textInputRange = inputor.createTextRange();
     138          textInputRange.moveToBookmark(range.getBookmark());
     139          endRange = inputor.createTextRange();
     140          endRange.collapse(false);
     141          if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
     142            pos = len;
     143          } else {
     144            pos = -textInputRange.moveStart("character", -len);
     145          }
     146        }
     147        return pos;
     148      };
     149
     150      InputCaret.prototype.getPos = function() {
     151        if (oDocument.selection) {
     152          return this.getIEPos();
     153        } else {
     154          return this.domInputor.selectionStart;
     155        }
     156      };
     157
     158      InputCaret.prototype.setPos = function(pos) {
     159        var inputor, range;
     160        inputor = this.domInputor;
     161        if (oDocument.selection) {
     162          range = inputor.createTextRange();
     163          range.move("character", pos);
     164          range.select();
     165        } else if (inputor.setSelectionRange) {
     166          inputor.setSelectionRange(pos, pos);
     167        }
     168        return inputor;
     169      };
     170
     171      InputCaret.prototype.getIEOffset = function(pos) {
     172        var h, textRange, x, y;
     173        textRange = this.domInputor.createTextRange();
     174        pos || (pos = this.getPos());
     175        textRange.move('character', pos);
     176        x = textRange.boundingLeft;
     177        y = textRange.boundingTop;
     178        h = textRange.boundingHeight;
     179        return {
     180          left: x,
     181          top: y,
     182          height: h
     183        };
     184      };
     185
     186      InputCaret.prototype.getOffset = function(pos) {
     187        var $inputor, offset, position;
     188        $inputor = this.$inputor;
     189        if (oDocument.selection) {
     190          offset = this.getIEOffset(pos);
     191          offset.top += $(oWindow).scrollTop() + $inputor.scrollTop();
     192          offset.left += $(oWindow).scrollLeft() + $inputor.scrollLeft();
     193          return offset;
     194        } else {
     195          offset = $inputor.offset();
     196          position = this.getPosition(pos);
     197          return offset = {
     198            left: offset.left + position.left - $inputor.scrollLeft(),
     199            top: offset.top + position.top - $inputor.scrollTop(),
     200            height: position.height
     201          };
     202        }
     203      };
     204
     205      InputCaret.prototype.getPosition = function(pos) {
     206        var $inputor, at_rect, end_range, format, html, mirror, start_range;
     207        $inputor = this.$inputor;
     208        format = function(value) {
     209          return value.replace(/</g, '&lt').replace(/>/g, '&gt').replace(/`/g, '&#96').replace(/"/g, '&quot').replace(/\r\n|\r|\n/g, "<br />");
     210        };
     211        if (pos === void 0) {
     212          pos = this.getPos();
     213        }
     214        start_range = $inputor.val().slice(0, pos);
     215        end_range = $inputor.val().slice(pos);
     216        html = "<span style='position: relative; display: inline;'>" + format(start_range) + "</span>";
     217        html += "<span id='caret' style='position: relative; display: inline;'>|</span>";
     218        html += "<span style='position: relative; display: inline;'>" + format(end_range) + "</span>";
     219        mirror = new Mirror($inputor);
     220        return at_rect = mirror.create(html).rect();
     221      };
     222
     223      InputCaret.prototype.getIEPosition = function(pos) {
     224        var h, inputorOffset, offset, x, y;
     225        offset = this.getIEOffset(pos);
     226        inputorOffset = this.$inputor.offset();
     227        x = offset.left - inputorOffset.left;
     228        y = offset.top - inputorOffset.top;
     229        h = offset.height;
     230        return {
     231          left: x,
     232          top: y,
     233          height: h
     234        };
     235      };
     236
     237      return InputCaret;
     238
     239    })();
     240    Mirror = (function() {
     241      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"];
     242
     243      function Mirror($inputor) {
     244        this.$inputor = $inputor;
     245      }
     246
     247      Mirror.prototype.mirrorCss = function() {
     248        var css,
     249          _this = this;
     250        css = {
     251          position: 'absolute',
     252          left: -9999,
     253          top: 0,
     254          zIndex: -20000
     255        };
     256        if (this.$inputor.prop('tagName') === 'TEXTAREA') {
     257          this.css_attr.push('width');
     258        }
     259        $.each(this.css_attr, function(i, p) {
     260          return css[p] = _this.$inputor.css(p);
     261        });
     262        return css;
     263      };
     264
     265      Mirror.prototype.create = function(html) {
     266        this.$mirror = $('<div></div>');
     267        this.$mirror.css(this.mirrorCss());
     268        this.$mirror.html(html);
     269        this.$inputor.after(this.$mirror);
     270        return this;
     271      };
     272
     273      Mirror.prototype.rect = function() {
     274        var $flag, pos, rect;
     275        $flag = this.$mirror.find("#caret");
     276        pos = $flag.position();
     277        rect = {
     278          left: pos.left,
     279          top: pos.top,
     280          height: $flag.height()
     281        };
     282        this.$mirror.remove();
     283        return rect;
     284      };
     285
     286      return Mirror;
     287
     288    })();
     289    Utils = {
     290      contentEditable: function($inputor) {
     291        return !!($inputor[0].contentEditable && $inputor[0].contentEditable === 'true');
     292      }
     293    };
     294    methods = {
     295      pos: function(pos) {
     296        if (pos || pos === 0) {
     297          return this.setPos(pos);
     298        } else {
     299          return this.getPos();
     300        }
     301      },
     302      position: function(pos) {
     303        if (oDocument.selection) {
     304          return this.getIEPosition(pos);
     305        } else {
     306          return this.getPosition(pos);
     307        }
     308      },
     309      offset: function(pos) {
     310        var iOffset, offset;
     311        offset = this.getOffset(pos);
     312        if (oFrame) {
     313          iOffset = $(oFrame).offset();
     314          offset.top += iOffset.top;
     315          offset.left += iOffset.left;
     316        }
     317        return offset;
     318      }
     319    };
     320    oDocument = null;
     321    oWindow = null;
     322    oFrame = null;
     323    setContextBy = function(settings) {
     324      var iframe;
     325      if (iframe = settings != null ? settings.iframe : void 0) {
     326        oFrame = iframe;
     327        oWindow = iframe.contentWindow;
     328        return oDocument = iframe.contentDocument || oWindow.document;
     329      } else {
     330        oFrame = void 0;
     331        oWindow = window;
     332        return oDocument = document;
     333      }
     334    };
     335    discoveryIframeOf = function($dom) {
     336      var error;
     337      oDocument = $dom[0].ownerDocument;
     338      oWindow = oDocument.defaultView || oDocument.parentWindow;
     339      try {
     340        return oFrame = oWindow.frameElement;
     341      } catch (_error) {
     342        error = _error;
     343      }
     344    };
     345    $.fn.caret = function(method, value, settings) {
     346      var caret;
     347      if (methods[method]) {
     348        if ($.isPlainObject(value)) {
     349          setContextBy(value);
     350          value = void 0;
     351        } else {
     352          setContextBy(settings);
     353        }
     354        caret = Utils.contentEditable(this) ? new EditableCaret(this) : new InputCaret(this);
     355        return methods[method].apply(caret, [value]);
     356      } else {
     357        return $.error("Method " + method + " does not exist on jQuery.caret");
     358      }
     359    };
     360    $.fn.caret.EditableCaret = EditableCaret;
     361    $.fn.caret.InputCaret = InputCaret;
     362    $.fn.caret.Utils = Utils;
     363    return $.fn.caret.apis = methods;
     364  });
     365
     366}).call(this);
  • src/bp-core/js/jquery.caret.txt

    diff --git src/bp-core/js/jquery.caret.txt src/bp-core/js/jquery.caret.txt
    index e69de29..36cd1c1 100644
     
     1Copyright (c) 2013 chord.luo@gmail.com
     2
     3Permission is hereby granted, free of charge, to any person
     4obtaining a copy of this software and associated documentation
     5files (the "Software"), to deal in the Software without
     6restriction, including without limitation the rights to use,
     7copy, modify, merge, publish, distribute, sublicense, and/or sell
     8copies of the Software, and to permit persons to whom the
     9Software is furnished to do so, subject to the following
     10conditions:
     11
     12The above copyright notice and this permission notice shall be
     13included in all copies or substantial portions of the Software.
     14
     15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
     16EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
     17OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
     18NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
     19HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
     20WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
     21FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
     22OTHER DEALINGS IN THE SOFTWARE.
  • src/bp-friends/bp-friends-functions.php

    diff --git src/bp-friends/bp-friends-functions.php src/bp-friends/bp-friends-functions.php
    index 5f797c7..9970c63 100644
    function friends_remove_data( $user_id ) { 
    566566add_action( 'wpmu_delete_user',  'friends_remove_data' );
    567567add_action( 'delete_user',       'friends_remove_data' );
    568568add_action( 'bp_make_spam_user', 'friends_remove_data' );
     569
     570/**
     571 * Used by the Activity component's @mentions to print a JSON list of the current user's friends.
     572 *
     573 * This is intended to speed up @mentions lookups for a majority of use cases.
     574 *
     575 * @see bp_activity_mentions_script()
     576 */
     577function bp_friends_prime_mentions_results() {
     578        if ( ! bp_activity_do_mentions() || ! bp_is_user_active() ) {
     579                return;
     580        }
     581
     582        $friends_query = array(
     583                'count_total'     => '',                    // Prevents total count
     584                'populate_extras' => false,
     585
     586                'type'            => 'alphabetical',
     587                'user_id'         => get_current_user_id(),
     588        );
     589
     590        $friends_query = new BP_User_Query( $friends_query );
     591        $results       = array();
     592
     593        foreach ( $friends_query->results as $user ) {
     594                $result        = new stdClass();
     595                $result->ID    = $user->user_nicename;
     596                $result->image = bp_core_fetch_avatar( array( 'html' => false, 'item_id' => $user->ID ) );
     597                $result->name  = bp_core_get_user_displayname( $user->ID );
     598
     599                $results[] = $result;
     600        }
     601
     602        wp_localize_script( 'bp-mentions', 'BP_Suggestions', array(
     603                'friends' => $results,
     604        ) );
     605}
     606add_action( 'bp_activity_mentions_prime_results', 'bp_friends_prime_mentions_results' );
     607 No newline at end of file
  • src/bp-groups/bp-groups-classes.php

    diff --git src/bp-groups/bp-groups-classes.php src/bp-groups/bp-groups-classes.php
    index 02e26be..7121ea4 100644
    class BP_Groups_Member_Suggestions extends BP_Members_Suggestions { 
    43874387                        'page'            => 1,
    43884388                        'per_page'        => $this->args['limit'],
    43894389                        'search_terms'    => $this->args['term'],
     4390                        'search_wildcard' => 'right',
    43904391                );
    43914392
    43924393                // Only return matches of friends of this user.
  • src/bp-templates/bp-legacy/buddypress/activity/entry.php

    diff --git src/bp-templates/bp-legacy/buddypress/activity/entry.php src/bp-templates/bp-legacy/buddypress/activity/entry.php
    index 24e296b..2d759b9 100644
     
    9797                                        <div class="ac-reply-avatar"><?php bp_loggedin_user_avatar( 'width=' . BP_AVATAR_THUMB_WIDTH . '&height=' . BP_AVATAR_THUMB_HEIGHT ); ?></div>
    9898                                        <div class="ac-reply-content">
    9999                                                <div class="ac-textarea">
    100                                                         <textarea id="ac-input-<?php bp_activity_id(); ?>" class="ac-input" name="ac_input_<?php bp_activity_id(); ?>"></textarea>
     100                                                        <textarea id="ac-input-<?php bp_activity_id(); ?>" class="ac-input bp-suggestions" name="ac_input_<?php bp_activity_id(); ?>"></textarea>
    101101                                                </div>
    102102                                                <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>
    103103                                                <input type="hidden" name="comment_form_id" value="<?php bp_activity_id(); ?>" />
  • src/bp-templates/bp-legacy/buddypress/activity/post-form.php

    diff --git src/bp-templates/bp-legacy/buddypress/activity/post-form.php src/bp-templates/bp-legacy/buddypress/activity/post-form.php
    index acf7368..a45213a 100644
     
    2727
    2828        <div id="whats-new-content">
    2929                <div id="whats-new-textarea">
    30                         <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>
     30                        <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>
    3131                </div>
    3232
    3333                <div id="whats-new-options">
  • src/bp-xprofile/bp-xprofile-functions.php

    diff --git src/bp-xprofile/bp-xprofile-functions.php src/bp-xprofile/bp-xprofile-functions.php
    index 7cce675..8cedbb0 100644
    function bp_xprofile_bp_user_query_search( $sql, BP_User_Query $query ) { 
    590590
    591591        $bp = buddypress();
    592592
    593         $search_terms_clean = esc_sql( esc_sql( $query->query_vars['search_terms'] ) );
     593        $search_terms_clean = bp_esc_like( $query->query_vars['search_terms'] );
    594594
    595595        if ( $query->query_vars['search_wildcard'] === 'left' ) {
    596                 $search_terms_clean = '%' . $search_terms_clean;
     596                $search_terms_nospace = '%' . $search_terms_clean;
     597                $search_terms_space   = '%' . $search_terms_clean . ' %';
    597598        } elseif ( $query->query_vars['search_wildcard'] === 'right' ) {
    598                 $search_terms_clean = $search_terms_clean . '%';
     599                $search_terms_nospace =        $search_terms_clean . '%';
     600                $search_terms_space   = '% ' . $search_terms_clean . '%';
    599601        } else {
    600                 $search_terms_clean = '%' . $search_terms_clean . '%';
     602                $search_terms_nospace = '%' . $search_terms_clean . '%';
     603                $search_terms_space   = '%' . $search_terms_clean . '%';
    601604        }
    602605
    603606        // Combine the core search (against wp_users) into a single OR clause
    604607        // with the xprofile_data search
     608        $search_xprofile = $wpdb->prepare(
     609                "u.{$query->uid_name} IN ( SELECT user_id FROM {$bp->profile->table_name_data} WHERE value LIKE %s OR value LIKE %s )",
     610                $search_terms_nospace,
     611                $search_terms_space
     612        );
     613
    605614        $search_core     = $sql['where']['search'];
    606         $search_xprofile = "u.{$query->uid_name} IN ( SELECT user_id FROM {$bp->profile->table_name_data} WHERE value LIKE '{$search_terms_clean}' )";
    607615        $search_combined = "( {$search_xprofile} OR {$search_core} )";
    608 
    609616        $sql['where']['search'] = $search_combined;
    610617
    611618        return $sql;