├── .editorconfig ├── .github └── workflows │ ├── cs-lint.yml │ └── integrations.yml ├── .gitignore ├── .phpcs.xml.dist ├── bin └── install-wp-tests.sh ├── class-document-feedback.php ├── composer.json ├── css └── document-feedback-admin.css ├── document-feedback.php ├── js └── jquery.sparkline.min.js ├── languages ├── document-feedback-bg_BG.mo ├── document-feedback-bg_BG.po ├── document-feedback-it_IT.mo ├── document-feedback-it_IT.po ├── document-feedback-tr_TR.mo ├── document-feedback-tr_TR.po ├── document-feedback-zh_CN.mo ├── document-feedback-zh_CN.po └── document-feedback.pot ├── phpunit.xml.dist ├── readme.md ├── readme.txt ├── screenshot-1.png ├── screenshot-2.png ├── screenshot-3.png └── tests ├── bootstrap.php └── document-feedback-testcase.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs. 2 | # It is based on https://core.trac.wordpress.org/browser/trunk/.editorconfig. 3 | # See https://editorconfig.org for more information about the standard. 4 | 5 | # WordPress Coding Standards 6 | # https://make.wordpress.org/core/handbook/coding-standards/ 7 | 8 | root = true 9 | 10 | [*] 11 | charset = utf-8 12 | end_of_line = lf 13 | insert_final_newline = true 14 | trim_trailing_whitespace = true 15 | indent_style = tab 16 | 17 | [*.yml] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | 24 | [*.txt] 25 | end_of_line = crlf 26 | -------------------------------------------------------------------------------- /.github/workflows/cs-lint.yml: -------------------------------------------------------------------------------- 1 | name: CS & Lint 2 | 3 | on: 4 | # Run on all pushes and on all pull requests. 5 | # Prevent the "push" build from running when there are only irrelevant changes. 6 | push: 7 | paths-ignore: 8 | - "**.md" 9 | pull_request: 10 | # Allow manually triggering the workflow. 11 | workflow_dispatch: 12 | 13 | jobs: 14 | checkcs: 15 | name: "Basic CS and QA checks" 16 | runs-on: ubuntu-latest 17 | 18 | env: 19 | XMLLINT_INDENT: " " 20 | 21 | steps: 22 | - name: Setup PHP 23 | uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: "7.4" 26 | coverage: none 27 | tools: cs2pr 28 | 29 | # Show PHP lint violations inline in the file diff. 30 | # @link https://github.com/marketplace/actions/xmllint-problem-matcher 31 | - name: Register PHP lint violations to appear as file diff comments 32 | uses: korelstar/phplint-problem-matcher@v1 33 | 34 | # Show XML violations inline in the file diff. 35 | # @link https://github.com/marketplace/actions/xmllint-problem-matcher 36 | - name: Register XML violations to appear as file diff comments 37 | uses: korelstar/xmllint-problem-matcher@v1 38 | 39 | - name: Checkout code 40 | uses: actions/checkout@v2 41 | 42 | # Validate the composer.json file. 43 | # @link https://getcomposer.org/doc/03-cli.md#validate 44 | - name: Validate Composer installation 45 | run: composer validate --no-check-all 46 | 47 | # Install dependencies and handle caching in one go. 48 | # @link https://github.com/marketplace/actions/install-composer-dependencies 49 | - name: Install Composer dependencies 50 | uses: ramsey/composer-install@v1 51 | 52 | # Lint PHP. 53 | - name: Lint PHP against parse errors 54 | run: composer lint-ci | cs2pr 55 | 56 | # Needed as runs-on: system doesn't have xml-lint by default. 57 | # @link https://github.com/marketplace/actions/xml-lint 58 | - name: Lint phpunit.xml.dist 59 | uses: ChristophWurst/xmllint-action@v1 60 | with: 61 | xml-file: ./phpunit.xml.dist 62 | xml-schema-file: ./vendor/phpunit/phpunit/phpunit.xsd 63 | 64 | # Check the code-style consistency of the PHP files. 65 | # - name: Check PHP code style 66 | # continue-on-error: true 67 | # run: vendor/bin/phpcs --report-full --report-checkstyle=./phpcs-report.xml 68 | 69 | # - name: Show PHPCS results in PR 70 | # run: cs2pr ./phpcs-report.xml 71 | -------------------------------------------------------------------------------- /.github/workflows/integrations.yml: -------------------------------------------------------------------------------- 1 | name: Run PHPUnit 2 | 3 | on: 4 | # Run on all pushes and on all pull requests. 5 | # Prevent the "push" build from running when there are only irrelevant changes. 6 | push: 7 | paths-ignore: 8 | - "**.md" 9 | pull_request: 10 | # Allow manually triggering the workflow. 11 | workflow_dispatch: 12 | 13 | jobs: 14 | test: 15 | name: WP ${{ matrix.wordpress }} on PHP ${{ matrix.php }} 16 | # Ubuntu-20.x includes MySQL 8.0, which causes `caching_sha2_password` issues with PHP < 7.4 17 | # https://www.php.net/manual/en/mysqli.requirements.php 18 | # TODO: change to ubuntu-latest when we no longer support PHP < 7.4 19 | runs-on: ubuntu-18.04 20 | 21 | env: 22 | WP_VERSION: ${{ matrix.wordpress }} 23 | 24 | strategy: 25 | matrix: 26 | wordpress: ["5.5", "5.6", "5.7"] 27 | php: ["5.6", "7.0", "7.1", "7.2", "7.3", "7.4"] 28 | include: 29 | - php: "8.0" 30 | # Ignore platform requirements, so that PHPUnit 7.5 can be installed on PHP 8.0 (and above). 31 | composer-options: "--ignore-platform-reqs" 32 | extensions: pcov 33 | ini-values: pcov.directory=., "pcov.exclude=\"~(vendor|tests)~\"" 34 | coverage: pcov 35 | exclude: 36 | - php: "8.0" 37 | wordpress: "5.5" 38 | fail-fast: false 39 | 40 | steps: 41 | - name: Checkout code 42 | uses: actions/checkout@v2 43 | 44 | - name: Setup PHP ${{ matrix.php }} 45 | uses: shivammathur/setup-php@v2 46 | with: 47 | php-version: ${{ matrix.php }} 48 | extensions: ${{ matrix.extensions }} 49 | ini-values: ${{ matrix.ini-values }} 50 | coverage: ${{ matrix.coverage }} 51 | 52 | - name: Setup problem matchers for PHP 53 | run: echo "::add-matcher::${{ runner.tool_cache }}/php.json" 54 | 55 | # Setup PCOV since we're using PHPUnit < 8 which has it integrated. Requires PHP 7.1. 56 | # Ignore platform reqs to make it install on PHP 8. 57 | # https://github.com/krakjoe/pcov-clobber 58 | - name: Setup PCOV 59 | if: ${{ matrix.php == 8.0 }} 60 | run: | 61 | composer require pcov/clobber --ignore-platform-reqs 62 | vendor/bin/pcov clobber 63 | 64 | - name: Setup Problem Matchers for PHPUnit 65 | run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 66 | 67 | - name: Install Composer dependencies 68 | uses: ramsey/composer-install@v1 69 | with: 70 | composer-options: "${{ matrix.composer-options }}" 71 | 72 | - name: Start MySQL Service 73 | run: sudo systemctl start mysql.service 74 | 75 | - name: Prepare environment for integration tests 76 | run: composer prepare-ci 77 | 78 | - name: Run integration tests (single site) 79 | if: ${{ matrix.php != 8.0 }} 80 | run: composer test 81 | - name: Run integration tests (single site with code coverage) 82 | if: ${{ matrix.php == 8.0 }} 83 | run: composer coverage-ci 84 | - name: Run integration tests (multisite) 85 | run: composer test-ms 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | /vendor 3 | -------------------------------------------------------------------------------- /.phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | Custom ruleset for document-feedback plugin. 4 | 5 | 6 | 7 | 8 | 9 | . 10 | 12 | /vendor/ 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 31 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" 5 | exit 1 6 | fi 7 | 8 | DB_NAME=$1 9 | DB_USER=$2 10 | DB_PASS=$3 11 | DB_HOST=${4-localhost} 12 | WP_VERSION=${5-latest} 13 | SKIP_DB_CREATE=${6-false} 14 | 15 | TMPDIR=${TMPDIR-/tmp} 16 | TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") 17 | WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} 18 | WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress} 19 | 20 | download() { 21 | if [ `which curl` ]; then 22 | curl -s "$1" > "$2"; 23 | elif [ `which wget` ]; then 24 | wget -nv -O "$2" "$1" 25 | fi 26 | } 27 | 28 | if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then 29 | WP_BRANCH=${WP_VERSION%\-*} 30 | WP_TESTS_TAG="branches/$WP_BRANCH" 31 | 32 | elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then 33 | WP_TESTS_TAG="branches/$WP_VERSION" 34 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then 35 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 36 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 37 | WP_TESTS_TAG="tags/${WP_VERSION%??}" 38 | else 39 | WP_TESTS_TAG="tags/$WP_VERSION" 40 | fi 41 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 42 | WP_TESTS_TAG="trunk" 43 | else 44 | # http serves a single offer, whereas https serves multiple. we only want one 45 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 46 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 47 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 48 | if [[ -z "$LATEST_VERSION" ]]; then 49 | echo "Latest WordPress version could not be found" 50 | exit 1 51 | fi 52 | WP_TESTS_TAG="tags/$LATEST_VERSION" 53 | fi 54 | set -ex 55 | 56 | install_wp() { 57 | 58 | if [ -d $WP_CORE_DIR ]; then 59 | return; 60 | fi 61 | 62 | mkdir -p $WP_CORE_DIR 63 | 64 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 65 | mkdir -p $TMPDIR/wordpress-trunk 66 | rm -rf $TMPDIR/wordpress-trunk/* 67 | svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress 68 | mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR 69 | else 70 | if [ $WP_VERSION == 'latest' ]; then 71 | local ARCHIVE_NAME='latest' 72 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then 73 | # https serves multiple offers, whereas http serves single. 74 | download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json 75 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 76 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 77 | LATEST_VERSION=${WP_VERSION%??} 78 | else 79 | # otherwise, scan the releases and get the most up to date minor version of the major release 80 | local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` 81 | LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) 82 | fi 83 | if [[ -z "$LATEST_VERSION" ]]; then 84 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 85 | else 86 | local ARCHIVE_NAME="wordpress-$LATEST_VERSION" 87 | fi 88 | else 89 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 90 | fi 91 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz 92 | tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR 93 | fi 94 | 95 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 96 | } 97 | 98 | install_test_suite() { 99 | # portable in-place argument for both GNU sed and Mac OSX sed 100 | if [[ $(uname -s) == 'Darwin' ]]; then 101 | local ioption='-i.bak' 102 | else 103 | local ioption='-i' 104 | fi 105 | 106 | # set up testing suite if it doesn't yet exist 107 | if [ ! -d $WP_TESTS_DIR ]; then 108 | # set up testing suite 109 | mkdir -p $WP_TESTS_DIR 110 | rm -rf $WP_TESTS_DIR/{includes,data} 111 | svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 112 | svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data 113 | fi 114 | 115 | if [ ! -f wp-tests-config.php ]; then 116 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 117 | # remove all forward slashes in the end 118 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") 119 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 120 | sed $ioption "s:__DIR__ . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 121 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 122 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 123 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 124 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 125 | fi 126 | 127 | } 128 | 129 | recreate_db() { 130 | shopt -s nocasematch 131 | if [[ $1 =~ ^(y|yes)$ ]] 132 | then 133 | mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA 134 | create_db 135 | echo "Recreated the database ($DB_NAME)." 136 | else 137 | echo "Leaving the existing database ($DB_NAME) in place." 138 | fi 139 | shopt -u nocasematch 140 | } 141 | 142 | create_db() { 143 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 144 | } 145 | 146 | install_db() { 147 | 148 | if [ ${SKIP_DB_CREATE} = "true" ]; then 149 | return 0 150 | fi 151 | 152 | # parse DB_HOST for port or socket references 153 | local PARTS=(${DB_HOST//\:/ }) 154 | local DB_HOSTNAME=${PARTS[0]}; 155 | local DB_SOCK_OR_PORT=${PARTS[1]}; 156 | local EXTRA="" 157 | 158 | if ! [ -z $DB_HOSTNAME ] ; then 159 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 160 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 161 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 162 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 163 | elif ! [ -z $DB_HOSTNAME ] ; then 164 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 165 | fi 166 | fi 167 | 168 | # create database 169 | if [ $(mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ] 170 | then 171 | echo "Reinstalling will delete the existing test database ($DB_NAME)" 172 | read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB 173 | recreate_db $DELETE_EXISTING_DB 174 | else 175 | create_db 176 | fi 177 | } 178 | 179 | install_wp 180 | install_test_suite 181 | install_db 182 | 183 | -------------------------------------------------------------------------------- /class-document-feedback.php: -------------------------------------------------------------------------------- 1 | setup_actions(); 36 | } 37 | return self::$instance; 38 | } 39 | 40 | /** 41 | * Prevent cloning 42 | */ 43 | public function __clone() { 44 | wp_die( esc_html__( 'Cheatin’ uh?' ) ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain -- core 45 | } 46 | 47 | /** 48 | * Prevent wakeup 49 | */ 50 | public function __wakeup() { 51 | wp_die( esc_html__( 'Cheatin’ uh?' ) ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain -- core 52 | } 53 | 54 | /** 55 | * __isset 56 | * 57 | * @param string $key Key. 58 | * @return bool 59 | */ 60 | public function __isset( $key ) { 61 | return isset( $this->data[ $key ] ); 62 | } 63 | 64 | /** 65 | * __get 66 | * 67 | * @param string $key Key. 68 | * @return mixed 69 | */ 70 | public function __get( $key ) { 71 | return isset( $this->data[ $key ] ) ? $this->data[ $key ] : null; 72 | } 73 | 74 | /** 75 | * __set 76 | * 77 | * @param string $key Key. 78 | * @param mixed $value Value. 79 | */ 80 | public function __set( $key, $value ) { 81 | $this->data[ $key ] = $value; 82 | } 83 | 84 | /** 85 | * __construct 86 | */ 87 | private function __construct() { 88 | /** Do nothing */ 89 | } 90 | 91 | /** 92 | * Setup actions for the plugin 93 | * 94 | * @since 1.0 95 | */ 96 | private function setup_actions() { 97 | add_action( 'init', array( $this, 'action_init_initialize_plugin' ) ); 98 | add_action( 'admin_init', array( $this, 'action_admin_init_add_meta_box' ) ); 99 | add_action( 'wp_enqueue_scripts', array( $this, 'action_wp_enqueue_scripts_add_jquery' ) ); 100 | add_action( 'admin_enqueue_scripts', array( $this, 'action_admin_enqueue_scripts_add_scripts' ) ); 101 | add_action( 'wp_head', array( $this, 'ensure_ajaxurl' ), 11 ); 102 | add_action( 'wp_ajax_document_feedback_form_submission', array( $this, 'action_wp_ajax_handle_form_submission' ) ); 103 | add_action( 'document_feedback_submitted', array( $this, 'set_throttle_transient' ), 10, 2 ); 104 | add_action( 'document_feedback_submitted', array( $this, 'send_notification' ), 10, 2 ); 105 | add_filter( 'the_content', array( $this, 'filter_the_content_append_feedback_form' ) ); 106 | } 107 | 108 | /** 109 | * Initialize all of the plugin components 110 | * Other plugins can register filters to modify how the plugin runs 111 | * 112 | * @since 1.0 113 | */ 114 | public function action_init_initialize_plugin() { 115 | load_plugin_textdomain( 'document-feedback', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' ); 116 | 117 | // Set up all of our plugin options but they can only be modified by filter. 118 | $this->options = array( 119 | 'send_notification' => true, // Send an email to the author and contributors. 120 | 'throttle_limit' => 3600, // How often (seconds) a user can submit a feedback. 121 | 'transient_prefix' => 'document_feedback_', // format is prefix . user_id . post_id. 122 | ); 123 | $this->options = apply_filters( 'document_feedback_options', $this->options ); 124 | 125 | // Prepare the strings used in the plugin. 126 | $this->strings = array( 127 | 'prompt' => __( 'Did this document answer your question?', 'document-feedback' ), 128 | 'accept' => __( 'Yes', 'document-feedback' ), 129 | 'decline' => __( 'No', 'document-feedback' ), 130 | 'prompt_response' => __( 'Thanks for responding.', 'document-feedback' ), 131 | 'accept_prompt' => __( 'What details were useful to you?', 'document-feedback' ), 132 | 'decline_prompt' => __( 'What details are you still looking for?', 'document-feedback' ), 133 | 'final_response' => __( "Thanks for the feedback! We'll use it to improve our documentation.", 'document-feedback' ), 134 | ); 135 | $this->strings = apply_filters( 'document_feedback_strings', $this->strings ); 136 | 137 | // Establish the post types to request feedback on. 138 | $this->post_types = array( 139 | 'page', 140 | ); 141 | $this->post_types = apply_filters( 'document_feedback_post_types', $this->post_types ); 142 | } 143 | 144 | /** 145 | * Hooks and such only to run in the admin 146 | * 147 | * @since 1.0 148 | */ 149 | public function action_admin_init_add_meta_box() { 150 | foreach ( $this->post_types as $post_type ) { 151 | add_meta_box( 'document-feedback', __( 'Document Feedback', 'document-feedback' ), array( $this, 'post_meta_box' ), $post_type, 'advanced', 'high' ); 152 | } 153 | } 154 | 155 | /** 156 | * Add jQuery on relevant pages because we need it 157 | * 158 | * @since 1.0 159 | */ 160 | public function action_wp_enqueue_scripts_add_jquery() { 161 | global $post; 162 | if ( is_singular() && in_array( $post->post_type, $this->post_types ) && is_user_logged_in() ) { 163 | wp_enqueue_script( 'jquery' ); 164 | } 165 | } 166 | /** 167 | * Add jQuery admin scripts for pie charts 168 | * 169 | * @since 1.0 170 | * @param string $hook The current admin page. 171 | */ 172 | public function action_admin_enqueue_scripts_add_scripts( $hook ) { 173 | if ( 'post.php' === $hook ) { 174 | // Load pie chart related scripts. 175 | wp_enqueue_script( 176 | 'jquery.sparkline', 177 | plugins_url( '/js/jquery.sparkline.min.js', __FILE__ ), 178 | array( 'jquery' ), 179 | '1.0', 180 | true 181 | ); 182 | 183 | // Custom Document Feedback JS for pies. 184 | wp_enqueue_style( 'document-feedback', plugins_url( '/css/document-feedback-admin.css', __FILE__ ), array(), '1.0' ); 185 | } 186 | } 187 | 188 | /** 189 | * Ensure there's an 'ajaxurl' var for us to reference on the frontend 190 | * 191 | * @since 1.0 192 | */ 193 | public function ensure_ajaxurl() { 194 | if ( is_admin() || ! is_user_logged_in() ) { 195 | return; 196 | } 197 | 198 | // Accommodate mapped domains. 199 | if ( home_url() != site_url() ) { 200 | $ajaxurl = home_url( '/wp-admin/admin-ajax.php' ); 201 | } else { 202 | $ajaxurl = admin_url( 'admin-ajax.php' ); 203 | } 204 | 205 | ?> 206 | 211 | ID; 222 | 223 | // Get feedback. 224 | $feedback_comments = $this->get_feedback_comments( $post_id ); 225 | 226 | if ( 0 < count( $feedback_comments ) ) { 227 | 228 | // Get an array with the count of accept and decline feedback comments. 229 | $feedback_stats = $this->get_feedback_stats( $feedback_comments ); 230 | ?> 231 | 250 |
251 |
252 |

"strings['prompt'] ); ?>"

253 |
254 |
255 |
strings['accept'] ); ?>
256 |
strings['decline'] ); ?>
257 |
258 |
259 |
260 |
261 | comment_content ) ) { 269 | continue; 270 | } 271 | 272 | ?> 273 |
274 |
275 |
276 | said:', 'document-feedback' ), 281 | sprintf( '%s', esc_html( $comment->comment_author ) ), 282 | sprintf( 283 | '', 284 | esc_html( get_comment_time( 'c' ) ), 285 | /* translators: 1: date, 2: time */ 286 | sprintf( esc_html__( '%1$s at %2$s', 'document-feedback' ), get_comment_date(), get_comment_time() ) 287 | ) 288 | ); 289 | //phpcs:enable 290 | ?> 291 |
292 |
293 | 294 |
295 |

comment_content ); ?>

296 |
297 |
298 | 302 |
303 |
304 |
305 | 308 |

309 | $post_id, 323 | 'type' => 'document-feedback', 324 | 'order' => 'DESC', 325 | ); 326 | 327 | // Fetch the comments with the correct status as a filter to the where clause. 328 | add_filter( 'comments_clauses', array( $this, 'filter_feedback_comments_clauses' ), 10, 2 ); 329 | $feedback_comments = get_comments( $comment_args ); 330 | remove_filter( 'comments_clauses', array( $this, 'filter_feedback_comments_clauses' ) ); 331 | 332 | return $feedback_comments; 333 | } 334 | 335 | /** 336 | * Count the accept and decline feedback 337 | * 338 | * @since 1.0 339 | * 340 | * @todo looping feedback to save two SQL count queries, optimize if needed (run 2 count queries and one select with limit) 341 | * 342 | * @param array $feedback_comments An array with the comment objects. 343 | * @return array Counts of accept and decline types. 344 | */ 345 | private function get_feedback_stats( $feedback_comments ) { 346 | $accept = 0; 347 | $decline = 0; 348 | 349 | // Count feedback. 350 | foreach ( $feedback_comments as $comment ) { 351 | if ( 'df-accept' === $comment->comment_approved ) { 352 | $accept++; 353 | } elseif ( 'df-decline' === $comment->comment_approved ) { 354 | $decline++; 355 | } 356 | } 357 | 358 | // Array to return with stats. 359 | $feedback_stats = array( 360 | 'accept' => $accept, 361 | 'decline' => $decline, 362 | ); 363 | 364 | return $feedback_stats; 365 | } 366 | 367 | /** 368 | * Handle a Document Feedback form submission 369 | * 370 | * @since 1.0 371 | */ 372 | public function action_wp_ajax_handle_form_submission() { 373 | 374 | // User must be logged in for all actions. 375 | if ( ! is_user_logged_in() ) { 376 | $this->do_ajax_response( 'error', array( 'message' => __( 'You need to be logged in to submit feedback.', 'document-feedback' ) ) ); 377 | } 378 | 379 | // Nonce check. 380 | if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( $_POST['nonce'] ), 'document-feedback' ) ) { 381 | $this->do_ajax_response( 'error', array( 'message' => __( 'Nonce error. Are you sure you are who you say you are?', 'document-feedback' ) ) ); 382 | } 383 | 384 | // Feedback must be left on a valid post. 385 | $post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0; 386 | $post = get_post( $post_id ); 387 | if ( false === $post ) { 388 | $this->do_ajax_response( 'error', array( 'message' => __( 'Invalid post for feedback.', 'document-feedback' ) ) ); 389 | } 390 | 391 | // Check that the comment exists if we're passed a valid comment ID. 392 | $comment_id = isset( $_POST['comment_id'] ) ? (int) $_POST['comment_id'] : 0; 393 | if ( $comment_id ) { 394 | $comment = get_comment( $comment_id ); 395 | if ( false === $comment ) { 396 | $this->do_ajax_response( 'error', array( 'message' => __( 'Invalid comment.', 'document-feedback' ) ) ); 397 | } 398 | } 399 | 400 | // @todo Ensure the user isn't hitting the throttle limit. 401 | 402 | $current_user = wp_get_current_user(); 403 | 404 | // Form submission for the initial prompt. 405 | // Create a new comment of accept or decline type against the current user. 406 | if ( isset( $_POST['form'] ) && 'prompt' === $_POST['form'] ) { 407 | 408 | // Set up all of the base data for our comment. 409 | $comment_data = array( 410 | 'comment_post_ID' => $post_id, 411 | 'comment_author' => $current_user->display_name, 412 | 'comment_author_email' => $current_user->user_email, 413 | 'comment_author_url' => $current_user->user_url, 414 | 'user_id' => $current_user->ID, 415 | ); 416 | 417 | // Set the comment type based on the value of the response. 418 | if ( isset( $_POST['response'] ) && 'accept' === $_POST['response'] ) { 419 | $comment_data['comment_approved'] = 'df-accept'; 420 | } 421 | if ( isset( $_POST['response'] ) && 'decline' === $_POST['response'] ) { 422 | $comment_data['comment_approved'] = 'df-decline'; 423 | } 424 | 425 | // Document feedbacks are always a special type. 426 | $comment_data['comment_type'] = 'document-feedback'; 427 | 428 | $comment_id = wp_insert_comment( $comment_data ); 429 | 430 | do_action( 'document_feedback_submitted', $comment_id, $post_id ); 431 | 432 | $response = array( 433 | 'message' => 'comment-id-' . $comment_id, 434 | 'comment_id' => $comment_id, 435 | ); 436 | $this->do_ajax_response( 'success', $response ); 437 | } 438 | // Follow up response form submission. 439 | // Save the message submitted as the message in the comment. 440 | 441 | $comment = get_comment( $comment_id, ARRAY_A ); 442 | 443 | if ( ! $comment ) { 444 | $this->do_ajax_response( 'error', array( 'message' => __( 'Invalid comment entry.', 'document-feedback' ) ) ); 445 | } 446 | 447 | if ( (int) $comment['user_id'] != $current_user->ID ) { 448 | $this->do_ajax_response( 'error', array( 'message' => __( 'Invalid user ID for comment.', 'document-feedback' ) ) ); 449 | } 450 | 451 | // Manage comment and update if existing and if the comment author is the same as the feedback author. 452 | $comment['comment_content'] = sanitize_text_field( ( isset( $_POST['response'] ) ? $_POST['response'] : '' ) ); 453 | $is_comment_updated = wp_update_comment( $comment ); 454 | if ( ! $is_comment_updated ) { 455 | $this->do_ajax_response( 'error', array( 'message' => __( 'Comment not updated.', 'document-feedback' ) ) ); 456 | } 457 | 458 | do_action( 'document_feedback_submitted', $comment_id, $post_id ); 459 | 460 | // send a happy response. 461 | $response = array( 462 | 'message' => 'final_response', 463 | ); 464 | $this->do_ajax_response( 'success', $response ); 465 | } 466 | 467 | /** 468 | * Do an ajax response 469 | * 470 | * @param string $status 'success' or 'error'. 471 | * @param array $data Any additional data. 472 | */ 473 | private function do_ajax_response( $status, $data = array() ) { 474 | header( 'Content-type: application/json' ); 475 | 476 | $response = array( 477 | 'status' => $status, 478 | ); 479 | $response = array_merge( $response, $data ); 480 | echo wp_json_encode( $response ); 481 | exit; 482 | } 483 | 484 | /** 485 | * Set the throttle transient 486 | * 487 | * @since 1.0 488 | * @param int $comment_id Comment ID. 489 | * @param int $post_id Post ID. 490 | */ 491 | public function set_throttle_transient( $comment_id, $post_id ) { 492 | $comment = get_comment( $comment_id ); 493 | $transient_option = $this->options['transient_prefix'] . $comment->user_id . '_' . $post_id; 494 | set_transient( $transient_option, $transient_option, $this->options['throttle_limit'] ); 495 | } 496 | 497 | /** 498 | * Send the document author a notification when feedback 499 | * is submitted 500 | * 501 | * @since 1.0 502 | * 503 | * @param int $comment_id The feedback ID. 504 | * @param int $post_id The post ID for the relevant document. 505 | * @return null|void 506 | */ 507 | public function send_notification( $comment_id, $post_id ) { 508 | if ( ! $this->options['send_notification'] ) { 509 | return; 510 | } 511 | 512 | // Only send a notification if there was qualitative feedback. 513 | $comment = get_comment( $comment_id ); 514 | if ( ! $comment || empty( $comment->comment_content ) ) { 515 | return; 516 | } 517 | 518 | // Make sure the post exists too. 519 | $post = get_post( $post_id ); 520 | if ( ! $post ) { 521 | return; 522 | } 523 | 524 | $feedback_type = ( 'df-accept' == $comment->comment_approved ) ? __( 'positive', 'document-feedback' ) : __( 'constructive', 'document-feedback' ); 525 | 526 | /* translators: 1: Post title */ 527 | $subject = '[' . get_bloginfo( 'name' ) . '] ' . sprintf( __( "Feedback received on '%s'", 'document-feedback' ), $post->post_title ); 528 | /* translators: 1: Type of feedback, 2: Author name, 3: Author email */ 529 | $message = sprintf( __( 'You\'ve received new %1$s feedback from %2$s (%3$s):', 'document-feedback' ), $feedback_type, $comment->comment_author, $comment->comment_author_email ) . PHP_EOL . PHP_EOL; 530 | $message .= '"' . $comment->comment_content . '"' . PHP_EOL . PHP_EOL; 531 | $message .= sprintf( __( 'You can view/edit the document here: ', 'document-feedback' ) ) . get_edit_post_link( $post_id, '' ); 532 | 533 | $document_author = get_user_by( 'id', $post->post_author ); 534 | $notification_recipients = apply_filters( 'document_feedback_notification_recipients', array( $document_author->user_email ), $comment_id, $post_id ); 535 | foreach ( $notification_recipients as $recipient ) { 536 | wp_mail( $recipient, $subject, $message ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail -- is transactional 537 | } 538 | } 539 | 540 | /** 541 | * Append the document feedback form to the document 542 | * We're using ob_*() functions to maintain readability of the form 543 | * 544 | * @since 1.0 545 | * 546 | * @param string $the_content The content. 547 | * @return string Modified content 548 | */ 549 | public function filter_the_content_append_feedback_form( $the_content ) { 550 | global $post; 551 | 552 | if ( ! is_singular() || ! in_array( $post->post_type, $this->post_types ) || ! is_user_logged_in() ) { 553 | return $the_content; 554 | } 555 | 556 | // @todo Show a message if the user submitted a response in the last X minutes. 557 | $current_user = wp_get_current_user(); 558 | $post_id = $post->ID; 559 | $current_user_id = $current_user->ID; 560 | 561 | // get transient if the user already sent the feedback. 562 | $transient_option = $this->options['transient_prefix'] . $current_user_id . '_' . $post_id; 563 | $transient = get_transient( $transient_option ); 564 | 565 | // display the form if transient is empty. 566 | if ( ! $transient ) { 567 | // Javascript for the form. 568 | ob_start(); 569 | ?> 570 | 622 | 629 | 650 | 657 |
strings['final_response'] ); ?>
658 |
659 | 660 | 661 | 662 |
663 | 670 |
671 | 672 | 673 | 674 |
675 | 682 |
683 | 684 | 685 | 686 |
687 | 694 | 695 | 696 | 697 | ' . $prompt . $accept . $decline . $data . ''; 702 | } else { 703 | ob_start(); 704 | ?> 705 |
strings['final_response'] ); ?>
706 | 707 | ' . $data . ''; 712 | } 713 | return $the_content; 714 | } 715 | 716 | /** 717 | * Filter the feedback comments - add accept and decline clauses as comment_approved 718 | * 719 | * @since 1.0 720 | * @param string[] $clauses An associative array of comment query clauses. 721 | * @param WP_Comment_Query $query Current instance of WP_Comment_Query (passed by reference). 722 | */ 723 | public function filter_feedback_comments_clauses( $clauses, $query ) { 724 | $expected_type_clause = "( comment_approved = '0' OR comment_approved = '1' )"; 725 | // filter if we are looking for the feedback comments. 726 | if ( isset( $clauses['where'] ) && false !== strpos( $clauses['where'], $expected_type_clause ) ) { 727 | $correct_type_clause = "comment_approved IN ( 'df-accept', 'df-decline' ) "; 728 | 729 | $clauses['where'] = str_replace( $expected_type_clause, $correct_type_clause, $clauses['where'] ); 730 | } 731 | 732 | return $clauses; 733 | } 734 | } 735 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "automattic/document-feedback", 3 | "type": "wordpress-plugin", 4 | "description": "get feedback from readers on the documentation you write", 5 | "homepage": "https://github.com/Automattic/document-feedback/", 6 | "license": "GPL-2.0-or-later", 7 | "authors": [ 8 | { 9 | "name": "Automattic", 10 | "homepage": "https://automattic.com/" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.6", 15 | "composer/installers": "~1.0" 16 | }, 17 | "require-dev": { 18 | "automattic/vipwpcs": "^2.2", 19 | "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7", 20 | "php-parallel-lint/php-parallel-lint": "^1.0", 21 | "phpcompatibility/phpcompatibility-wp": "^2.1", 22 | "phpunit/phpunit": "^4 || ^5 || ^6 || ^7", 23 | "squizlabs/php_codesniffer": "^3.5", 24 | "wp-coding-standards/wpcs": "^2.3.0", 25 | "yoast/phpunit-polyfills": "^0.2.0" 26 | }, 27 | "scripts": { 28 | "cbf": [ 29 | "@php ./vendor/bin/phpcbf" 30 | ], 31 | "coverage": [ 32 | "@php ./vendor/bin/phpunit --coverage-html ./build/coverage-html" 33 | ], 34 | "coverage-ci": [ 35 | "@php ./vendor/bin/phpunit" 36 | ], 37 | "cs": [ 38 | "@php ./vendor/bin/phpcs" 39 | ], 40 | "lint": [ 41 | "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --exclude vendor --exclude .git" 42 | ], 43 | "lint-ci": [ 44 | "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --exclude vendor --exclude .git --checkstyle" 45 | ], 46 | "prepare-ci": [ 47 | "bash bin/install-wp-tests.sh wordpress_test root root localhost" 48 | ], 49 | "test": [ 50 | "@php ./vendor/bin/phpunit --testsuite WP_Tests" 51 | ], 52 | "test-ms": [ 53 | "@putenv WP_MULTISITE=1", 54 | "@composer test" 55 | ] 56 | }, 57 | "support": { 58 | "issues": "https://github.com/Automattic/document-feedback/issues", 59 | "source": "https://github.com/Automattic/document-feedback" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /css/document-feedback-admin.css: -------------------------------------------------------------------------------- 1 | /* Verbose */ 2 | 3 | .left { 4 | float: left; 5 | } 6 | .right { 7 | float: right; 8 | } 9 | 10 | /* General metabox positioning */ 11 | 12 | #document-feedback-metabox { 13 | width: 100%; 14 | max-width: 850px; 15 | overflow: hidden; 16 | } 17 | 18 | #document-feedback-chart { 19 | width: 240px; 20 | height: 250px; 21 | } 22 | 23 | #document-feedback-comment-wrapper { 24 | width: 400px; 25 | overflow: hidden; 26 | float: right; 27 | } 28 | 29 | /* Comment styling */ 30 | 31 | #document-feedback-comment-wrapper .comment-content { 32 | background-color: #DDD; 33 | border: 1px solid #DDD; 34 | -webkit-border-radius: 3px; 35 | -moz-border-radius: 3px; 36 | border-radius: 3px; 37 | padding-left: 10px; 38 | padding-right: 10px; 39 | font-size: 13px; 40 | margin: 5px 0 15px 0; 41 | } 42 | 43 | #document-feedback-comment-wrapper .comment-content.df-accept { 44 | border: 3px solid #009344; 45 | } 46 | 47 | #document-feedback-comment-wrapper .comment-content.df-decline { 48 | border: 3px solid #BE1E2D; 49 | } 50 | 51 | /* Legend styling */ 52 | 53 | #document-feedback-legend-accept { 54 | background-color: #009344; 55 | color: white; 56 | font-size: 16px; 57 | width: 100px; 58 | height: 20px; 59 | text-align: center; 60 | line-height: 22px; 61 | padding-top: 3px; 62 | padding-bottom: 3px; 63 | } 64 | #document-feedback-legend-decline { 65 | background-color: #BE1E2D; 66 | color: white; 67 | font-size: 16px; 68 | width: 100px; 69 | height: 20px; 70 | text-align: center; 71 | line-height: 22px; 72 | padding-top: 3px; 73 | padding-bottom: 3px; 74 | } -------------------------------------------------------------------------------- /document-feedback.php: -------------------------------------------------------------------------------- 1 | ● {{prefix}}{{y}}{{suffix}}')},bar:{barColor:"#3366cc",negBarColor:"#f44",stackedBarColor:["#3366cc","#dc3912","#ff9900","#109618","#66aa00","#dd4477","#0099c6","#990099"],zeroColor:c,nullColor:c,zeroAxis:!0,barWidth:4,barSpacing:1,chartRangeMax:c,chartRangeMin:c,chartRangeClip:!1,colorMap:c,tooltipFormat:new h(' {{prefix}}{{value}}{{suffix}}')},tristate:{barWidth:4,barSpacing:1,posBarColor:"#6f6",negBarColor:"#f44",zeroBarColor:"#999",colorMap:{},tooltipFormat:new h(' {{value:map}}'),tooltipValueLookups:{map:{"-1":"Loss",0:"Draw",1:"Win"}}},discrete:{lineHeight:"auto",thresholdColor:c,thresholdValue:0,chartRangeMax:c,chartRangeMin:c,chartRangeClip:!1,tooltipFormat:new h("{{prefix}}{{value}}{{suffix}}")},bullet:{targetColor:"#f33",targetWidth:3,performanceColor:"#33f",rangeColors:["#d3dafe","#a8b6ff","#7f94ff"],base:c,tooltipFormat:new h("{{fieldkey:fields}} - {{value}}"),tooltipValueLookups:{fields:{r:"Range",p:"Performance",t:"Target"}}},pie:{offset:0,sliceColors:["#3366cc","#dc3912","#ff9900","#109618","#66aa00","#dd4477","#0099c6","#990099"],borderWidth:0,borderColor:"#000",tooltipFormat:new h(' {{value}} ({{percent.1}}%)')},box:{raw:!1,boxLineColor:"#000",boxFillColor:"#cdf",whiskerColor:"#000",outlierLineColor:"#333",outlierFillColor:"#fff",medianColor:"#f00",showOutliers:!0,outlierIQR:1.5,spotRadius:1.5,target:c,targetColor:"#4a2",chartRangeMax:c,chartRangeMin:c,tooltipFormat:new h("{{field:fields}}: {{value}}"),tooltipFormatFieldlistKey:"field",tooltipValueLookups:{fields:{lq:"Lower Quartile",med:"Median",uq:"Upper Quartile",lo:"Left Outlier",ro:"Right Outlier",lw:"Left Whisker",rw:"Right Whisker"}}}}},E='.jqstooltip { position: absolute;left: 0px;top: 0px;visibility: hidden;background: rgb(0, 0, 0) transparent;background-color: rgba(0,0,0,0.6);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000);-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000)";color: white;font: 10px arial, san serif;text-align: left;white-space: nowrap;padding: 5px;border: 1px solid white;z-index: 10000;}.jqsfield { color: white;font: 10px arial, san serif;text-align: left;}',g=function(){var a,b;return a=function(){this.init.apply(this,arguments)},arguments.length>1?(arguments[0]?(a.prototype=d.extend(new arguments[0],arguments[arguments.length-1]),a._super=arguments[0].prototype):a.prototype=arguments[arguments.length-1],arguments.length>2&&(b=Array.prototype.slice.call(arguments,1,-1),b.unshift(a.prototype),d.extend.apply(d,b))):a.prototype=arguments[0],a.prototype.cls=a,a},d.SPFormatClass=h=g({fre:/\{\{([\w.]+?)(:(.+?))?\}\}/g,precre:/(\w+)\.(\d+)/,init:function(a,b){this.format=a,this.fclass=b},render:function(a,b,d){var e=this,f=a,g,h,i,j,k;return this.format.replace(this.fre,function(){var a;return h=arguments[1],i=arguments[3],g=e.precre.exec(h),g?(k=g[2],h=g[1]):k=!1,j=f[h],j===c?"":i&&b&&b[i]?(a=b[i],a.get?b[i].get(j)||j:b[i][j]||j):(n(j)&&(d.get("numberFormatter")?j=d.get("numberFormatter")(j):j=s(j,k,d.get("numberDigitGroupCount"),d.get("numberDigitGroupSep"),d.get("numberDecimalMark"))),j)})}}),d.spformat=function(a,b){return new h(a,b)},i=function(a,b,c){return ac?c:a},j=function(a,c){var d;return c===2?(d=b.floor(a.length/2),a.length%2?a[d]:(a[d-1]+a[d])/2):a.length%2?(d=(a.length*c+c)/4,d%1?(a[b.floor(d)]+a[b.floor(d)-1])/2:a[d-1]):(d=(a.length*c+2)/4,d%1?(a[b.floor(d)]+a[b.floor(d)-1])/2:a[d-1])},k=function(a){var b;switch(a){case"undefined":a=c;break;case"null":a=null;break;case"true":a=!0;break;case"false":a=!1;break;default:b=parseFloat(a),a==b&&(a=b)}return a},l=function(a){var b,c=[];for(b=a.length;b--;)c[b]=k(a[b]);return c},m=function(a,b){var c,d,e=[];for(c=0,d=a.length;c0;h-=c)a.splice(h,0,e);return a.join("")},o=function(a,b,c){var d;for(d=b.length;d--;){if(c&&b[d]===null)continue;if(b[d]!==a)return!1}return!0},p=function(a){var b=0,c;for(c=a.length;c--;)b+=typeof a[c]=="number"?a[c]:0;return b},r=function(a){return d.isArray(a)?a:[a]},q=function(b){var c;a.createStyleSheet?a.createStyleSheet().cssText=b:(c=a.createElement("style"),c.type="text/css",a.getElementsByTagName("head")[0].appendChild(c),c[typeof a.body.style.WebkitAppearance=="string"?"innerText":"innerHTML"]=b)},d.fn.simpledraw=function(b,e,f,g){var h,i;if(f&&(h=this.data("_jqs_vcanvas")))return h;if(d.fn.sparkline.canvas===!1)return!1;if(d.fn.sparkline.canvas===c){var j=a.createElement("canvas");if(!j.getContext||!j.getContext("2d")){if(!a.namespaces||!!a.namespaces.v)return d.fn.sparkline.canvas=!1,!1;a.namespaces.add("v","urn:schemas-microsoft-com:vml","#default#VML"),d.fn.sparkline.canvas=function(a,b,c,d){return new J(a,b,c)}}else d.fn.sparkline.canvas=function(a,b,c,d){return new I(a,b,c,d)}}return b===c&&(b=d(this).innerWidth()),e===c&&(e=d(this).innerHeight()),h=d.fn.sparkline.canvas(b,e,this,g),i=d(this).data("_jqs_mhandler"),i&&i.registerCanvas(h),h},d.fn.cleardraw=function(){var a=this.data("_jqs_vcanvas");a&&a.reset()},d.RangeMapClass=t=g({init:function(a){var b,c,d=[];for(b in a)a.hasOwnProperty(b)&&typeof b=="string"&&b.indexOf(":")>-1&&(c=b.split(":"),c[0]=c[0].length===0?-Infinity:parseFloat(c[0]),c[1]=c[1].length===0?Infinity:parseFloat(c[1]),c[2]=a[b],d.push(c));this.map=a,this.rangelist=d||!1},get:function(a){var b=this.rangelist,d,e,f;if((f=this.map[a])!==c)return f;if(b)for(d=b.length;d--;){e=b[d];if(e[0]<=a&&e[1]>=a)return e[2]}return c}}),d.range_map=function(a){return new t(a)},u=g({init:function(a,b){var c=d(a);this.$el=c,this.options=b,this.currentPageX=0,this.currentPageY=0,this.el=a,this.splist=[],this.tooltip=null,this.over=!1,this.displayTooltips=!b.get("disableTooltips"),this.highlightEnabled=!b.get("disableHighlight")},registerSparkline:function(a){this.splist.push(a),this.over&&this.updateDisplay()},registerCanvas:function(a){var b=d(a.canvas);this.canvas=a,this.$canvas=b,b.mouseenter(d.proxy(this.mouseenter,this)),b.mouseleave(d.proxy(this.mouseleave,this)),b.click(d.proxy(this.mouseclick,this))},reset:function(a){this.splist=[],this.tooltip&&a&&(this.tooltip.remove(),this.tooltip=c)},mouseclick:function(a){var b=d.Event("sparklineClick");b.originalEvent=a,b.sparklines=this.splist,this.$el.trigger(b)},mouseenter:function(b){d(a.body).unbind("mousemove.jqs"),d(a.body).bind("mousemove.jqs",d.proxy(this.mousemove,this)),this.over=!0,this.currentPageX=b.pageX,this.currentPageY=b.pageY,this.currentEl=b.target,!this.tooltip&&this.displayTooltips&&(this.tooltip=new v(this.options),this.tooltip.updatePosition(b.pageX,b.pageY)),this.updateDisplay()},mouseleave:function(){d(a.body).unbind("mousemove.jqs");var b=this.splist,c=b.length,e=!1,f,g;this.over=!1,this.currentEl=null,this.tooltip&&(this.tooltip.remove(),this.tooltip=null);for(g=0;g