SendGrid - Version 1.1

Version Description

  • Added SendGrid Statistics

=

Download this release

Release Info

Developer team-rs
Plugin Icon 128x128 SendGrid
Version 1.1
Comparing to
See all releases

Version 1.1

Files changed (66) hide show
  1. assets/screenshot-1.png +0 -0
  2. assets/screenshot-2.png +0 -0
  3. assets/screenshot-3.png +0 -0
  4. assets/screenshot-4.png +0 -0
  5. assets/screenshot-5.png +0 -0
  6. assets/screenshot-6.png +0 -0
  7. assets/screenshot-7.png +0 -0
  8. lib/SendGridSettings.php +154 -0
  9. lib/SendGridStats.php +131 -0
  10. lib/sendgrid-php/.gitignore +4 -0
  11. lib/sendgrid-php/.travis.yml +6 -0
  12. lib/sendgrid-php/MIT.LICENSE +15 -0
  13. lib/sendgrid-php/Makefile +37 -0
  14. lib/sendgrid-php/README.md +234 -0
  15. lib/sendgrid-php/SendGrid.php +44 -0
  16. lib/sendgrid-php/SendGrid/Api.php +17 -0
  17. lib/sendgrid-php/SendGrid/Mail.php +721 -0
  18. lib/sendgrid-php/SendGrid/MailInterface.php +10 -0
  19. lib/sendgrid-php/SendGrid/Smtp.php +159 -0
  20. lib/sendgrid-php/SendGrid/Web.php +147 -0
  21. lib/sendgrid-php/SendGrid_loader.php +14 -0
  22. lib/sendgrid-php/Test/Mock/Mock_loader.php +14 -0
  23. lib/sendgrid-php/Test/Mock/SmtpMock.php +14 -0
  24. lib/sendgrid-php/Test/Mock/WebMock.php +19 -0
  25. lib/sendgrid-php/Test/SendGrid/ApiTest.php +0 -0
  26. lib/sendgrid-php/Test/SendGrid/MailTest.php +549 -0
  27. lib/sendgrid-php/Test/SendGrid/SmtpTest.php +91 -0
  28. lib/sendgrid-php/Test/SendGrid/WebTest.php +101 -0
  29. lib/sendgrid-php/Test/SendGridTest.php +36 -0
  30. lib/sendgrid-php/Test/a_loaderTest.php +7 -0
  31. lib/sendgrid-php/Test/phpunit.xml +7 -0
  32. lib/sendgrid-php/composer.json +16 -0
  33. lib/sendgrid-php/composer.lock +70 -0
  34. readme.txt +111 -0
  35. view/css/sendgrid.css +239 -0
  36. view/css/smoothness/images/animated-overlay.gif +0 -0
  37. view/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  38. view/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  39. view/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  40. view/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  41. view/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  42. view/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  43. view/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  44. view/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  45. view/css/smoothness/images/ui-icons_222222_256x240.png +0 -0
  46. view/css/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
  47. view/css/smoothness/images/ui-icons_454545_256x240.png +0 -0
  48. view/css/smoothness/images/ui-icons_888888_256x240.png +0 -0
  49. view/css/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
  50. view/css/smoothness/jquery-ui-1.10.3.custom.css +677 -0
  51. view/images/loader.gif +0 -0
  52. view/images/logo.png +0 -0
  53. view/images/logo32.png +0 -0
  54. view/js/jquery.flot.js +2696 -0
  55. view/js/jquery.flot.symbol.js +71 -0
  56. view/js/jquery.flot.time.js +431 -0
  57. view/js/jquery.flot.togglelegend.js +319 -0
  58. view/js/jquery.ui.datepicker.js +2038 -0
  59. view/js/sendgrid-stats.js +434 -0
  60. view/partials/sendgrid_stats_compliance.php +12 -0
  61. view/partials/sendgrid_stats_deliveries.php +12 -0
  62. view/partials/sendgrid_stats_engagement.php +12 -0
  63. view/partials/sendgrid_stats_widget.php +101 -0
  64. view/sendgrid_settings.php +148 -0
  65. view/sendgrid_stats.php +26 -0
  66. wpsendgrid.php +420 -0
assets/screenshot-1.png ADDED
Binary file
assets/screenshot-2.png ADDED
Binary file
assets/screenshot-3.png ADDED
Binary file
assets/screenshot-4.png ADDED
Binary file
assets/screenshot-5.png ADDED
Binary file
assets/screenshot-6.png ADDED
Binary file
assets/screenshot-7.png ADDED
Binary file
lib/SendGridSettings.php ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ class wpSendGridSettings
3
+ {
4
+ public function __construct()
5
+ {
6
+ add_action('admin_menu', array(__CLASS__, 'sendgridPluginMenu'));
7
+ }
8
+
9
+ /**
10
+ * Add settings page
11
+ */
12
+ public function sendgridPluginMenu()
13
+ {
14
+ add_options_page(__('SendGrid'), __('SendGrid'), 'manage_options', 'sendgrid-settings.php',
15
+ array(__CLASS__, 'show_settings_page'));
16
+ }
17
+
18
+ /**
19
+ * Check username/password
20
+ *
21
+ * @param string $username sendgrid username
22
+ * @param string $password sendgrid password
23
+ * @return bool
24
+ */
25
+ public static function checkUsernamePassword($username, $password)
26
+ {
27
+ $url = "https://sendgrid.com/api/profile.get.json?";
28
+ $url .= "api_user=". $username . "&api_key=" . $password;
29
+
30
+ $ch = curl_init();
31
+ curl_setopt($ch, CURLOPT_URL, $url);
32
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
33
+
34
+ $data = curl_exec($ch);
35
+ curl_close($ch);
36
+
37
+ $response = json_decode($data, true);
38
+
39
+ if (isset($response['error']))
40
+ {
41
+ return false;
42
+ }
43
+
44
+ return true;
45
+ }
46
+
47
+ /**
48
+ * Display settings page
49
+ */
50
+ public function show_settings_page()
51
+ {
52
+ if ($_SERVER['REQUEST_METHOD'] == 'POST')
53
+ {
54
+ if ($_POST['email_test'])
55
+ {
56
+ $to = $_POST['sendgrid_to'];
57
+ $subject = $_POST['sendgrid_subj'];
58
+ $body = $_POST['sendgrid_body'];
59
+ $headers = $_POST['sendgrid_headers'];
60
+ $sent = wp_mail($to, $subject, $body, $headers);
61
+ if (get_option('sendgrid_api') == 'api')
62
+ {
63
+ $sent = json_decode($sent);
64
+ if ($sent->message == "success")
65
+ {
66
+ $message = 'Email sent.';
67
+ $status = 'send-success';
68
+ }
69
+ else
70
+ {
71
+ $errors = ($sent->errors[0]) ? $sent->errors[0] : $sent;
72
+ $message = 'Email not sent. ' . $errors;
73
+ $status = 'send-failed';
74
+ }
75
+
76
+ }
77
+ elseif (get_option('sendgrid_api') == 'smtp')
78
+ {
79
+ if ($sent === true)
80
+ {
81
+ $message = 'Email sent.';
82
+ $status = 'send-success';
83
+ }
84
+ else
85
+ {
86
+ $message = 'Email not sent. ' . $sent;
87
+ $status = 'send-failed';
88
+ }
89
+ }
90
+ }
91
+ else
92
+ {
93
+ $message = 'Options saved.';
94
+ $status = 'save-success';
95
+
96
+ $user = $_POST['sendgrid_user'];
97
+ update_option('sendgrid_user', $user);
98
+
99
+ $password = $_POST['sendgrid_pwd'];
100
+ update_option('sendgrid_pwd', $password);
101
+
102
+ $method = $_POST['sendgrid_api'];
103
+ if ($method == 'smtp' and !class_exists('Swift'))
104
+ {
105
+ $message = 'You must have <a href="http://wordpress.org/plugins/swift-mailer/" target="_blank">' .
106
+ 'Swift-mailer plugin</a> installed and activated';
107
+ $status = 'save-error';
108
+ update_option('sendgrid_api', 'api');
109
+ }
110
+ else
111
+ {
112
+ update_option('sendgrid_api', $method);
113
+ }
114
+
115
+ $name = $_POST['sendgrid_name'];
116
+ update_option('sendgrid_from_name', $name);
117
+
118
+ $email = $_POST['sendgrid_email'];
119
+ update_option('sendgrid_from_email', $email);
120
+
121
+ $reply_to = $_POST['sendgrid_reply_to'];
122
+ update_option('sendgrid_reply_to', $reply_to);
123
+ }
124
+ }
125
+
126
+ $user = get_option('sendgrid_user');
127
+ $password = get_option('sendgrid_pwd');
128
+ $method = get_option('sendgrid_api');
129
+ $name = get_option('sendgrid_from_name');
130
+ $email = get_option('sendgrid_from_email');
131
+ $reply_to = get_option('sendgrid_reply_to');
132
+
133
+ if ($user and $password)
134
+ {
135
+ if (in_array('curl', get_loaded_extensions()))
136
+ {
137
+ $valid_credentials = self::checkUsernamePassword($user, $password);
138
+
139
+ if (!$valid_credentials)
140
+ {
141
+ $message = 'Invalid username/password';
142
+ $status = 'save-error';
143
+ }
144
+ }
145
+ else
146
+ {
147
+ $message = 'You must have PHP-curl extension enabled';
148
+ $status = 'save-error';
149
+ }
150
+ }
151
+
152
+ require_once dirname(__FILE__) . '/../view/sendgrid_settings.php';
153
+ }
154
+ }
lib/SendGridStats.php ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /*
3
+ * Display statistics on dashboard
4
+ */
5
+
6
+ /**
7
+ * Verify if SendGrid username and password provided are correct and
8
+ * initialize function for add widget in dashboard
9
+ *
10
+ * @return void
11
+ */
12
+ function my_custom_dashboard_widgets()
13
+ {
14
+ $sendgridSettings = new wpSendGridSettings();
15
+ if (!$sendgridSettings->checkUsernamePassword(get_option('sendgrid_user'),get_option('sendgrid_pwd')))
16
+ {
17
+ return;
18
+ }
19
+
20
+ add_meta_box('sendgrid_statistics_widget', 'SendGrid Statistics', 'sendgrid_dashboard_statistics', 'dashboard', 'normal', 'high');
21
+ }
22
+ add_action('wp_dashboard_setup', 'my_custom_dashboard_widgets');
23
+
24
+ /**
25
+ * Add widget content to wordpress admin dashboard
26
+ *
27
+ * @return void
28
+ */
29
+ function sendgrid_dashboard_statistics()
30
+ {
31
+ require plugin_dir_path( __FILE__ ) . '../view/partials/sendgrid_stats_widget.php';
32
+ }
33
+
34
+ /**
35
+ * Add new SendGrid statistics page in wordpress admin menu
36
+ *
37
+ * @return void
38
+ */
39
+ function add_dashboard_menu()
40
+ {
41
+ add_dashboard_page( "SendGrid Statistics", "SendGrid Statistics", "manage_options", "sendgrid-statistics", "sendgrid_statistics_page");
42
+ }
43
+ add_action('admin_menu', 'add_dashboard_menu');
44
+
45
+ /**
46
+ * Set content for SendGrid statistics page
47
+ *
48
+ * @return void
49
+ */
50
+ function sendgrid_statistics_page()
51
+ {
52
+ require plugin_dir_path( __FILE__ ) . '../view/sendgrid_stats.php';
53
+ }
54
+
55
+ /**
56
+ * Include javascripts we need for SendGrid statistics page and widget
57
+ *
58
+ * @return void;
59
+ */
60
+ function sendgrid_load_script($hook)
61
+ {
62
+ if ($hook != "index.php" && $hook != "dashboard_page_sendgrid-statistics")
63
+ {
64
+ return;
65
+ }
66
+
67
+ wp_enqueue_script('sendgrid-stats', plugin_dir_url(__FILE__) . '../view/js/sendgrid-stats.js', array('jquery'));
68
+ wp_enqueue_script('jquery-flot', plugin_dir_url(__FILE__) . '../view/js/jquery.flot.js', array('jquery'));
69
+ wp_enqueue_script('jquery-flot-time', plugin_dir_url(__FILE__) . '../view/js/jquery.flot.time.js', array('jquery'));
70
+ wp_enqueue_script('jquery-flot-tofflelegend', plugin_dir_url(__FILE__) . '../view/js/jquery.flot.togglelegend.js', array('jquery'));
71
+ wp_enqueue_script('jquery-flot-symbol', plugin_dir_url(__FILE__) . '../view/js/jquery.flot.symbol.js', array('jquery'));
72
+ wp_enqueue_script('jquery-ui-datepicker', plugin_dir_url(__FILE__) . '../view/js/jquery.ui.datepicker.js', array('jquery', 'jquery-ui-core'));
73
+ wp_enqueue_style('jquery-ui-datepicker', plugin_dir_url(__FILE__) . '../view/css/smoothness/jquery-ui-1.10.3.custom.css');
74
+ wp_enqueue_style('sendgrid', plugin_dir_url(__FILE__) . '../view/css/sendgrid.css');
75
+ wp_localize_script('sendgrid-stats', 'sendgrid_vars', array(
76
+ 'sendgrid_nonce' => wp_create_nonce('sendgrid-nonce')
77
+ ));
78
+ }
79
+ add_action('admin_enqueue_scripts', 'sendgrid_load_script');
80
+
81
+ /**
82
+ * Get SendGrid stats from API and return JSON response,
83
+ * this function work like a page and is used for ajax request by javascript functions
84
+ *
85
+ * @return void;
86
+ */
87
+ function sendgrid_process_stats()
88
+ {
89
+ if (!isset($_POST['sendgrid_nonce']) || !wp_verify_nonce($_POST['sendgrid_nonce'], 'sendgrid-nonce'))
90
+ {
91
+ die('Permissions check failed');
92
+ }
93
+
94
+ $parameters = array();
95
+ $parameters['api_user'] = get_option('sendgrid_user');
96
+ $parameters['api_key'] = get_option('sendgrid_pwd');
97
+ $parameters['data_type'] = 'global';
98
+ $parameters['metric'] = 'all';
99
+
100
+ if (array_key_exists('days', $_POST))
101
+ {
102
+ $parameters['days'] = $_POST['days'];
103
+ }
104
+ else
105
+ {
106
+ $parameters['start_date'] = $_POST['start_date'];
107
+ $parameters['end_date'] = $_POST['end_date'];
108
+ }
109
+
110
+ echo _processRequest('api/stats.get.json', $parameters);
111
+
112
+ die();
113
+ }
114
+ add_action('wp_ajax_sendgrid_get_stats', 'sendgrid_process_stats');
115
+
116
+ /**
117
+ * Make cURL request to SendGrid API for required statistics
118
+ *
119
+ * @param type $api
120
+ * @param type $parameters
121
+ * @return json
122
+ */
123
+ function _processRequest($api = 'api/stats.get.json', $parameters = array())
124
+ {
125
+ $data = urldecode(http_build_query($parameters));
126
+ $process = curl_init();
127
+ curl_setopt($process, CURLOPT_URL, 'http://sendgrid.com/' . $api . '?' . $data);
128
+ curl_setopt($process, CURLOPT_RETURNTRANSFER, 1);
129
+
130
+ return curl_exec($process);
131
+ }
lib/sendgrid-php/.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
1
+ Test/coverage/*
2
+ examples/*
3
+ dist/
4
+ vendor/*
lib/sendgrid-php/.travis.yml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
1
+ language: php
2
+ php:
3
+ - 5.4
4
+ - 5.3
5
+ before_install: composer install --prefer-source
6
+ script: make test
lib/sendgrid-php/MIT.LICENSE ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright (c) 2011 SendGrid
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
4
+ documentation files (the "Software"), to deal in the Software without restriction, including without limitation
5
+ the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
6
+ and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7
+
8
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of
9
+ the Software.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
12
+ THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
13
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
14
+ CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
15
+ DEALINGS IN THE SOFTWARE.
lib/sendgrid-php/Makefile ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Twilio API helper library.
2
+ # See LICENSE file for copyright and license details.
3
+
4
+ define LICENSE
5
+ <?php
6
+
7
+ /**
8
+ * SendGrid API helper library.
9
+ *
10
+ * @category Services
11
+ * @package Services_SendGrid
12
+ * @license http://creativecommons.org/licenses/MIT/ MIT
13
+ * @link https://github.com/sendgrid/sendgrid-php
14
+ */
15
+ endef
16
+ export LICENSE
17
+
18
+ all: test
19
+
20
+ clean:
21
+ @rm -rf dist
22
+
23
+ PHP_FILES = `find dist -name \*.php`
24
+ dist: clean
25
+ @mkdir dist
26
+ @git archive master | (cd dist; tar xf -)
27
+ @for php in $(PHP_FILES); do\
28
+ echo "$$LICENSE" > $$php.new; \
29
+ tail -n+2 $$php >> $$php.new; \
30
+ mv $$php.new $$php; \
31
+ done
32
+
33
+ test:
34
+ @echo running tests
35
+ @phpunit --strict --colors --configuration Test/phpunit.xml
36
+
37
+ .PHONY: all clean dist test
lib/sendgrid-php/README.md ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # sendgrid-php
2
+ This library allows you to quickly and easily send emails through SendGrid using PHP.
3
+
4
+ ## Installation
5
+
6
+ There are a number of ways to install the SendGrid PHP helper library. Choose from the options outlined below:
7
+
8
+ ### Composer
9
+
10
+ The easier way to install the SendGrid php library is using [Composer](http://getcomposer.org/). Composer makes it easy
11
+ to install the library and all of its dependencies:
12
+
13
+ #### 1. Update your composer.json
14
+
15
+ If you already have a `composer.json`, just add the following to your require section:
16
+
17
+ ```json
18
+ {
19
+ "require": {
20
+ "sendgrid/sendgrid": "~1.0.0"
21
+ }
22
+ }
23
+ ```
24
+ *For more info on creating a `composer.json`, check out [this guide](http://getcomposer.org/doc/01-basic-usage.md#composer-json-project-setup).*
25
+
26
+ #### 2. Install from packagist
27
+
28
+ To install the library and it's dependencies, make sure you have [composer installed](http://getcomposer.org/doc/01-basic-usage.md#installation) and type the following:
29
+
30
+ ```bash
31
+ composer install
32
+ ```
33
+
34
+ #### 3. Include autoload.php
35
+
36
+ Now that we have everything installed, all we need to do is require it from our php script. Add the following to the top of your php script:
37
+
38
+ ```php
39
+ require 'vendor/autoload.php';
40
+ ```
41
+
42
+ This will include both the SendGrid library, and the SwiftMailer dependency.
43
+
44
+ ### Git
45
+
46
+ You can also install the package from github, although you will have to manually install the dependencies (see the section on installing dependencies below):
47
+
48
+ ```bash
49
+ git clone https://github.com/sendgrid/sendgrid-php.git
50
+ ```
51
+
52
+ And the require the autoloader from your php script:
53
+
54
+ ```php
55
+ require '../path/to/sendgrid-php/SendGrid_loader.php';
56
+ ```
57
+
58
+ ## Installing Dependenices
59
+
60
+ If you installed the library using composer or you're not planning on sending using SMTP, you can skip this section. Otherwise, you will need to install
61
+ SwiftMailer (which sendgrid-php depends on). You can install from pear using the following:
62
+
63
+ ```bash
64
+ pear channel-discover pear.swiftmailer.org
65
+ pear install swift/swift
66
+ ```
67
+
68
+
69
+ ## Testing ##
70
+
71
+ The existing tests in the `Test` directory can be run using [PHPUnit](https://github.com/sebastianbergmann/phpunit/) with the following command:
72
+
73
+ ````
74
+ phpunit Test/
75
+ ```
76
+
77
+ ## SendGrid APIs ##
78
+ SendGrid provides two methods of sending email: the Web API, and SMTP API. SendGrid recommends using the SMTP API for sending emails.
79
+ For an explanation of the benefits of each, refer to http://docs.sendgrid.com/documentation/get-started/integrate/examples/smtp-vs-rest/.
80
+
81
+ This library implements a common interface to make it very easy to use either API.
82
+
83
+ ## Mail Pre-Usage ##
84
+
85
+ Before we begin using the library, its important to understand a few things about the library architecture...
86
+
87
+ * The SendGrid Mail object is the means of setting mail data. In general, data can be set in three ways for most elements:
88
+ 1. set - reset the data, and initialize it to the given element. This will destroy previous data
89
+ 2. set (List) - for array based elements, we provide a way of passing the entire array in at once. This will also destroy previous data.
90
+ 3. add - append data to the list of elements.
91
+
92
+ * Sending an email is as simple as :
93
+ 1. Creating a SendGrid Instance
94
+ 1. Creating a SendGrid Mail object, and setting its data
95
+ 1. Sending the mail using either SMTP API or Web API.
96
+
97
+ ## Mail Usage ##
98
+
99
+ To begin using this library, initialize the SendGrid object with your SendGrid credentials
100
+
101
+ ```php
102
+ $sendgrid = new SendGrid('username', 'password');
103
+ ```
104
+
105
+ Create a new SendGrid Mail object and add your message details
106
+
107
+ ```php
108
+ $mail = new SendGrid\Mail();
109
+ $mail->addTo('foo@bar.com')->
110
+ setFrom('me@bar.com')->
111
+ setSubject('Subject goes here')->
112
+ setText('Hello World!')->
113
+ setHtml('<strong>Hello World!</strong>');
114
+ ```
115
+
116
+ Send it using the API of your choice (SMTP or Web)
117
+
118
+ ```php
119
+ $sendgrid->smtp->send($mail);
120
+ ```
121
+ Or
122
+
123
+ ```php
124
+ $sendgrid->web->send($mail);
125
+ ```
126
+
127
+ ### Using Categories ###
128
+
129
+ Categories are used to group email statistics provided by SendGrid.
130
+
131
+ To use a category, simply set the category name. Note: there is a maximum of 10 categories per email.
132
+
133
+ ```php
134
+ $mail = new SendGrid\Mail();
135
+ $mail->addTo('foo@bar.com')->
136
+ ...
137
+ addCategory("Category 1")->
138
+ addCategory("Category 2");
139
+ ```
140
+
141
+
142
+ ### Using Attachments ###
143
+
144
+ Attachments are currently file based only, with future plans for an in memory implementation as well.
145
+
146
+ File attachments are limited to 7 MB per file.
147
+
148
+ ```php
149
+ $mail = new SendGrid\Mail();
150
+ $mail->addTo('foo@bar.com')->
151
+ ...
152
+ addAttachment("../path/to/file.txt");
153
+ ```
154
+
155
+ ### Using From-Name and Reply-To
156
+
157
+ There are two handy helper methods for setting the From-Name and Reply-To for a
158
+ message
159
+
160
+ ```php
161
+ $mail = new SendGrid\Mail();
162
+ $mail->addTo('foo@bar.com')->
163
+ setReplyTo('someone.else@example.com')->
164
+ setFromName('John Doe')->
165
+ ...
166
+ ```
167
+
168
+ ### Using Substitutions ###
169
+
170
+ Substitutions can be used to customize multi-recipient emails, and tailor them for the user
171
+
172
+ ```php
173
+ $mail = new SendGrid\Mail();
174
+ $mail->addTo('john@somewhere.com')->
175
+ addTo("harry@somewhere.com")->
176
+ addTo("Bob@somewhere.com")->
177
+ ...
178
+ setHtml("Hey %name%, we've seen that you've been gone for a while")->
179
+ addSubstitution("%name%", array("John", "Harry", "Bob"));
180
+ ```
181
+
182
+ ### Using Sections ###
183
+
184
+ Sections can be used to further customize messages for the end users. A section is only useful in conjunction with a substition value.
185
+
186
+ ```php
187
+ $mail = new SendGrid\Mail();
188
+ $mail->addTo('john@somewhere.com')->
189
+ addTo("harry@somewhere.com")->
190
+ addTo("Bob@somewhere.com")->
191
+ ...
192
+ setHtml("Hey %name%, you work at %place%")->
193
+ addSubstitution("%name%", array("John", "Harry", "Bob"))->
194
+ addSubstitution("%place%", array("%office%", "%office%", "%home%"))->
195
+ addSection("%office%", "an office")->
196
+ addSection("%home%", "your house");
197
+ ```
198
+
199
+ ### Using Unique Arguments ###
200
+
201
+ Unique Arguments are used for tracking purposes
202
+
203
+ ```php
204
+ $mail = new SendGrid\Mail();
205
+ $mail->addTo('foo@bar.com')->
206
+ ...
207
+ addUniqueArgument("Customer", "Someone")->
208
+ addUniqueArgument("location", "Somewhere");
209
+ ```
210
+
211
+ ### Using Filter Settings ###
212
+
213
+ Filter Settings are used to enable and disable apps, and to pass parameters to those apps.
214
+
215
+ ```php
216
+ $mail = new SendGrid\Mail();
217
+ $mail->addTo('foo@bar.com')->
218
+ ...
219
+ addFilterSetting("gravatar", "enable", 1)->
220
+ addFilterSetting("footer", "enable", 1)->
221
+ addFilterSetting("footer", "text/plain", "Here is a plain text footer")->
222
+ addFilterSetting("footer", "text/html", "<p style='color:red;'>Here is an HTML footer</p>");
223
+ ```
224
+
225
+ ### Using Headers ###
226
+
227
+ Headers can be used to add existing sendgrid functionality (such as for categories or filters), or custom headers can be added as necessary.
228
+
229
+ ```php
230
+ $mail = new SendGrid\Mail();
231
+ $mail->addTo('foo@bar.com')->
232
+ ...
233
+ addHeader("category", "My New Category");
234
+ ```
lib/sendgrid-php/SendGrid.php ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class SendGrid
4
+ {
5
+ const VERSION = "1.0.0";
6
+
7
+ protected $namespace = "SendGrid",
8
+ $username,
9
+ $password;
10
+
11
+ // Available transport mechanisms
12
+ protected $web,
13
+ $smtp;
14
+
15
+ public function __construct($username, $password)
16
+ {
17
+ $this->username = $username;
18
+ $this->password = $password;
19
+ }
20
+
21
+ public function __get($api)
22
+ {
23
+ $name = $api;
24
+
25
+ if($this->$name != null)
26
+ {
27
+ return $this->$name;
28
+ }
29
+
30
+ $api = $this->namespace . "\\" . ucwords($api);
31
+ $class_name = str_replace('\\', '/', "$api.php");
32
+ $file = __dir__ . DIRECTORY_SEPARATOR . $class_name;
33
+
34
+ if (!file_exists($file))
35
+ {
36
+ throw new Exception("Api '$class_name' not found.");
37
+ }
38
+ require_once $file;
39
+
40
+ $this->$name = new $api($this->username, $this->password);
41
+ return $this->$name;
42
+ }
43
+
44
+ }
lib/sendgrid-php/SendGrid/Api.php ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace SendGrid;
4
+
5
+ class Api
6
+ {
7
+
8
+ protected $username,
9
+ $password;
10
+
11
+ public function __construct($username, $password)
12
+ {
13
+ $this->username = $username;
14
+ $this->password = $password;
15
+ }
16
+
17
+ }
lib/sendgrid-php/SendGrid/Mail.php ADDED
@@ -0,0 +1,721 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace SendGrid;
4
+
5
+ class Mail
6
+ {
7
+
8
+ private $to_list,
9
+ $from,
10
+ $from_name,
11
+ $reply_to,
12
+ $cc_list,
13
+ $bcc_list,
14
+ $subject,
15
+ $text,
16
+ $html,
17
+ $attachment_list,
18
+ $header_list = array();
19
+
20
+ protected $use_headers;
21
+
22
+ public function __construct()
23
+ {
24
+ $this->from_name = false;
25
+ $this->reply_to = false;
26
+ }
27
+
28
+ /**
29
+ * _removeFromList
30
+ * Given a list of key/value pairs, removes the associated keys
31
+ * where a value matches the given string ($item)
32
+ * @param Array $list - the list of key/value pairs
33
+ * @param String $item - the value to be removed
34
+ */
35
+ private function _removeFromList(&$list, $item, $key_field = null)
36
+ {
37
+ foreach ($list as $key => $val)
38
+ {
39
+ if($key_field)
40
+ {
41
+ if($val[$key_field] == $item)
42
+ {
43
+ unset($list[$key]);
44
+ }
45
+ }
46
+ else
47
+ {
48
+ if ($val == $item)
49
+ {
50
+ unset($list[$key]);
51
+ }
52
+ }
53
+ }
54
+ //repack the indices
55
+ $list = array_values($list);
56
+ }
57
+
58
+ /**
59
+ * getTos
60
+ * Return the list of recipients
61
+ * @return list of recipients
62
+ */
63
+ public function getTos()
64
+ {
65
+ return $this->to_list;
66
+ }
67
+
68
+ /**
69
+ * setTos
70
+ * Initialize an array for the recipient 'to' field
71
+ * Destroy previous recipient 'to' data.
72
+ * @param Array $email_list - an array of email addresses
73
+ * @return the SendGrid\Mail object.
74
+ */
75
+ public function setTos(array $email_list)
76
+ {
77
+ $this->to_list = $email_list;
78
+ return $this;
79
+ }
80
+
81
+ /**
82
+ * setTo
83
+ * Initialize a single email for the recipient 'to' field
84
+ * Destroy previous recipient 'to' data.
85
+ * @param String $email - a list of email addresses
86
+ * @return the SendGrid\Mail object.
87
+ */
88
+ public function setTo($email)
89
+ {
90
+ $this->to_list = array($email);
91
+ return $this;
92
+ }
93
+
94
+ /**
95
+ * addTo
96
+ * append an email address to the existing list of addresses
97
+ * Preserve previous recipient 'to' data.
98
+ * @param String $email - a single email address
99
+ * @return the SendGrid\Mail object.
100
+ */
101
+ public function addTo($email, $name=null)
102
+ {
103
+ $this->to_list[] = ($name ? $name . "<" . $email . ">" : $email);
104
+
105
+ return $this;
106
+ }
107
+
108
+ /**
109
+ * removeTo
110
+ * remove an email address from the list of recipient addresses
111
+ * @param String $search_term - the regex value to be removed
112
+ * @return the SendGrid\Mail object.
113
+ */
114
+ public function removeTo($search_term)
115
+ {
116
+ $this->to_list = array_values(array_filter($this->to_list, function($item) use($search_term) {
117
+ return !preg_match("/" . $search_term . "/", $item);
118
+ }));
119
+ return $this;
120
+ }
121
+
122
+ /**
123
+ * getFrom
124
+ * get the from email address
125
+ * @param Boolean $as_array - return the from as an assocative array
126
+ * @return the from email address
127
+ */
128
+ public function getFrom($as_array = false)
129
+ {
130
+ if($as_array && ($name = $this->getFromName())) {
131
+ return array("$this->from" => $name);
132
+ } else {
133
+ return $this->from;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * setFrom
139
+ * set the from email
140
+ * @param String $email - an email address
141
+ * @return the SendGrid\Mail object.
142
+ */
143
+ public function setFrom($email)
144
+ {
145
+ $this->from = $email;
146
+ return $this;
147
+ }
148
+
149
+ /**
150
+ * getFromName
151
+ * get the from name
152
+ * @return the from name
153
+ */
154
+ public function getFromName()
155
+ {
156
+ return $this->from_name;
157
+ }
158
+
159
+ /**
160
+ * setFromName
161
+ * set the name appended to the from email
162
+ * @param String $name - a name to append
163
+ * @return the SendGrid\Mail object.
164
+ */
165
+ public function setFromName($name)
166
+ {
167
+ $this->from_name = $name;
168
+ return $this;
169
+ }
170
+
171
+ /**
172
+ * getReplyTo
173
+ * get the reply-to address
174
+ * @return the reply to address
175
+ */
176
+ public function getReplyTo()
177
+ {
178
+ return $this->reply_to;
179
+ }
180
+
181
+ /**
182
+ * setReplyTo
183
+ * set the reply-to address
184
+ * @param String $email - the email to reply to
185
+ * @return the SendGrid\Mail object.
186
+ */
187
+ public function setReplyTo($email)
188
+ {
189
+ $this->reply_to = $email;
190
+ return $this;
191
+ }
192
+ /**
193
+ * getCc
194
+ * get the Carbon Copy list of recipients
195
+ * @return Array the list of recipients
196
+ */
197
+ public function getCcs()
198
+ {
199
+ return $this->cc_list;
200
+ }
201
+
202
+ /**
203
+ * setCcs
204
+ * Set the list of Carbon Copy recipients
205
+ * @param String $email - a list of email addresses
206
+ * @return the SendGrid\Mail object.
207
+ */
208
+ public function setCcs(array $email_list)
209
+ {
210
+ $this->cc_list = $email_list;
211
+ return $this;
212
+ }
213
+
214
+ /**
215
+ * setCc
216
+ * Initialize the list of Carbon Copy recipients
217
+ * destroy previous recipient data
218
+ * @param String $email - a list of email addresses
219
+ * @return the SendGrid\Mail object.
220
+ */
221
+ public function setCc($email)
222
+ {
223
+ $this->cc_list = array($email);
224
+ return $this;
225
+ }
226
+
227
+ /**
228
+ * addCc
229
+ * Append an address to the list of Carbon Copy recipients
230
+ * @param String $email - an email address
231
+ * @return the SendGrid\Mail object.
232
+ */
233
+ public function addCc($email)
234
+ {
235
+ $this->cc_list[] = $email;
236
+ return $this;
237
+ }
238
+
239
+ /**
240
+ * removeCc
241
+ * remove an address from the list of Carbon Copy recipients
242
+ * @param String $email - an email address
243
+ * @return the SendGrid\Mail object.
244
+ */
245
+ public function removeCc($email)
246
+ {
247
+ $this->_removeFromList($this->cc_list, $email);
248
+
249
+ return $this;
250
+ }
251
+
252
+ /**
253
+ * getBccs
254
+ * return the list of Blind Carbon Copy recipients
255
+ * @return Array - the list of Blind Carbon Copy recipients
256
+ */
257
+ public function getBccs()
258
+ {
259
+ return $this->bcc_list;
260
+ }
261
+
262
+ /**
263
+ * setBccs
264
+ * set the list of Blind Carbon Copy Recipients
265
+ * @param Array $email_list - the list of email recipients to
266
+ * @return the SendGrid\Mail object.
267
+ */
268
+ public function setBccs($email_list)
269
+ {
270
+ $this->bcc_list = $email_list;
271
+ return $this;
272
+ }
273
+
274
+ /**
275
+ * setBcc
276
+ * Initialize the list of Carbon Copy recipients
277
+ * destroy previous recipient Blind Carbon Copy data
278
+ * @param String $email - an email address
279
+ * @return the SendGrid\Mail object.
280
+ */
281
+ public function setBcc($email)
282
+ {
283
+ $this->bcc_list = array($email);
284
+ return $this;
285
+ }
286
+
287
+ /**
288
+ * addBcc
289
+ * Append an email address to the list of Blind Carbon Copy
290
+ * recipients
291
+ * @param String $email - an email address
292
+ */
293
+ public function addBcc($email)
294
+ {
295
+ $this->bcc_list[] = $email;
296
+ return $this;
297
+ }
298
+
299
+ /**
300
+ * removeBcc
301
+ * remove an email address from the list of Blind Carbon Copy
302
+ * addresses
303
+ * @param String $email - the email to remove
304
+ * @return the SendGrid\Mail object.
305
+ */
306
+ public function removeBcc($email)
307
+ {
308
+ $this->_removeFromList($this->bcc_list, $email);
309
+ return $this;
310
+ }
311
+
312
+ /**
313
+ * getSubject
314
+ * get the email subject
315
+ * @return the email subject
316
+ */
317
+ public function getSubject()
318
+ {
319
+ return $this->subject;
320
+ }
321
+
322
+ /**
323
+ * setSubject
324
+ * set the email subject
325
+ * @param String $subject - the email subject
326
+ * @return the SendGrid\Mail object
327
+ */
328
+ public function setSubject($subject)
329
+ {
330
+ $this->subject = $subject;
331
+ return $this;
332
+ }
333
+
334
+ /**
335
+ * getText
336
+ * get the plain text part of the email
337
+ * @return the plain text part of the email
338
+ */
339
+ public function getText()
340
+ {
341
+ return $this->text;
342
+ }
343
+
344
+ /**
345
+ * setText
346
+ * Set the plain text part of the email
347
+ * @param String $text - the plain text of the email
348
+ * @return the SendGrid\Mail object.
349
+ */
350
+ public function setText($text)
351
+ {
352
+ $this->text = $text;
353
+ return $this;
354
+ }
355
+
356
+ /**
357
+ * getHtml
358
+ * Get the HTML part of the email
359
+ * @param String $html - the HTML part of the email
360
+ * @return the HTML part of the email.
361
+ */
362
+ public function getHtml()
363
+ {
364
+ return $this->html;
365
+ }
366
+
367
+ /**
368
+ * setHTML
369
+ * Set the HTML part of the email
370
+ * @param String $html - the HTML part of the email
371
+ * @return the SendGrid\Mail object.
372
+ */
373
+ public function setHtml($html)
374
+ {
375
+ $this->html = $html;
376
+ return $this;
377
+ }
378
+
379
+ /**
380
+ * getAttachments
381
+ * Get the list of file attachments
382
+ * @return Array of indexed file attachments
383
+ */
384
+ public function getAttachments()
385
+ {
386
+ return $this->attachment_list;
387
+ }
388
+
389
+ /**
390
+ * setAttachments
391
+ * add multiple file attachments at once
392
+ * destroys previous attachment data.
393
+ * @param array $files - The list of files to attach
394
+ * @return the SendGrid\Mail object
395
+ */
396
+ public function setAttachments(array $files)
397
+ {
398
+ $this->attachment_list = array();
399
+ foreach($files as $file)
400
+ {
401
+ $this->addAttachment($file);
402
+ }
403
+
404
+ return $this;
405
+ }
406
+
407
+ /**
408
+ * setAttachment
409
+ * Initialize the list of attachments, and add the given file
410
+ * destroys previous attachment data.
411
+ * @param String $file - the file to attach
412
+ * @return the SendGrid\Mail object.
413
+ */
414
+ public function setAttachment($file)
415
+ {
416
+ $this->attachment_list = array($this->_getAttachmentInfo($file));
417
+ return $this;
418
+ }
419
+
420
+ /**
421
+ * addAttachment
422
+ * Add a new email attachment, given the file name.
423
+ * @param String $file - The file to attach.
424
+ * @return the SendGrid\Mail object.
425
+ */
426
+ public function addAttachment($file)
427
+ {
428
+ $this->attachment_list[] = $this->_getAttachmentInfo($file);
429
+ return $this;
430
+ }
431
+
432
+ /**
433
+ * removeAttachment
434
+ * Remove a previously added file attachment, given the file name.
435
+ * @param String $file - the file attachment to remove.
436
+ * @return the SendGrid\Mail object.
437
+ */
438
+ public function removeAttachment($file)
439
+ {
440
+ $this->_removeFromList($this->attachment_list, $file, "file");
441
+ return $this;
442
+ }
443
+
444
+ private function _getAttachmentInfo($file)
445
+ {
446
+ $info = pathinfo($file);
447
+ $info['file'] = $file;
448
+ return $info;
449
+ }
450
+
451
+ /**
452
+ * setCategories
453
+ * Set the list of category headers
454
+ * destroys previous category header data
455
+ * @param Array $category_list - the list of category values
456
+ * @return the SendGrid\Mail object.
457
+ */
458
+ public function setCategories($category_list)
459
+ {
460
+ $this->header_list['category'] = $category_list;
461
+ return $this;
462
+ }
463
+
464
+ /**
465
+ * setCategory
466
+ * Clears the category list and adds the given category
467
+ * @param String $category - the new category to append
468
+ * @return the SendGrid\Mail object.
469
+ */
470
+ public function setCategory($category)
471
+ {
472
+ $this->header_list['category'] = array($category);
473
+ return $this;
474
+ }
475
+
476
+ /**
477
+ * addCategory
478
+ * Append a category to the list of categories
479
+ * @param String $category - the new category to append
480
+ * @return the SendGrid\Mail object.
481
+ */
482
+ public function addCategory($category)
483
+ {
484
+ $this->header_list['category'][] = $category;
485
+ return $this;
486
+ }
487
+
488
+ /**
489
+ * removeCategory
490
+ * Given a category name, remove that category from the list
491
+ * of category headers
492
+ * @param String $category - the category to be removed
493
+ * @return the SendGrid\Mail object.
494
+ */
495
+ public function removeCategory($category)
496
+ {
497
+ $this->_removeFromList($this->header_list['category'], $category);
498
+ return $this;
499
+ }
500
+
501
+ /**
502
+ * SetSubstitutions
503
+ *
504
+ * Substitute a value for list of values, where each value corresponds
505
+ * to the list emails in a one to one relationship. (IE, value[0] = email[0],
506
+ * value[1] = email[1])
507
+ *
508
+ * @param array $key_value_pairs - key/value pairs where the value is an array of values
509
+ * @return the SendGrid\Mail object.
510
+ */
511
+ public function setSubstitutions($key_value_pairs)
512
+ {
513
+ $this->header_list['sub'] = $key_value_pairs;
514
+ return $this;
515
+ }
516
+
517
+ /**
518
+ * addSubstitution
519
+ * Substitute a value for list of values, where each value corresponds
520
+ * to the list emails in a one to one relationship. (IE, value[0] = email[0],
521
+ * value[1] = email[1])
522
+ *
523
+ * @param string $from_key - the value to be replaced
524
+ * @param array $to_values - an array of values to replace the $from_value
525
+ * @return the SendGrid\Mail object.
526
+ */
527
+ public function addSubstitution($from_value, array $to_values)
528
+ {
529
+ $this->header_list['sub'][$from_value] = $to_values;
530
+ return $this;
531
+ }
532
+
533
+ /**
534
+ * setSection
535
+ * Set a list of section values
536
+ * @param Array $key_value_pairs
537
+ * @return the SendGrid\Mail object.
538
+ */
539
+ public function setSections(array $key_value_pairs)
540
+ {
541
+ $this->header_list['section'] = $key_value_pairs;
542
+ return $this;
543
+ }
544
+
545
+ /**
546
+ * addSection
547
+ * append a section value to the list of section values
548
+ * @param String $from_value - the value to be replaced
549
+ * @param String $to_value - the value to replace
550
+ * @return the SendGrid\Mail object.
551
+ */
552
+ public function addSection($from_value, $to_value)
553
+ {
554
+ $this->header_list['section'][$from_value] = $to_value;
555
+ return $this;
556
+ }
557
+
558
+ /**
559
+ * setUniqueArguments
560
+ * Set a list of unique arguments, to be used for tracking purposes
561
+ * @param array $key_value_pairs - list of unique arguments
562
+ */
563
+ public function setUniqueArguments(array $key_value_pairs)
564
+ {
565
+ $this->header_list['unique_args'] = $key_value_pairs;
566
+ return $this;
567
+ }
568
+
569
+ /**
570
+ * addUniqueArgument
571
+ * Set a key/value pair of unique arguments, to be used for tracking purposes
572
+ * @param string $key - key
573
+ * @param string $value - value
574
+ */
575
+ public function addUniqueArgument($key, $value)
576
+ {
577
+ $this->header_list['unique_args'][$key] = $value;
578
+ return $this;
579
+ }
580
+
581
+ /**
582
+ * setFilterSettings
583
+ * Set filter/app settings
584
+ * @param array $filter_settings - array of fiter settings
585
+ */
586
+ public function setFilterSettings($filter_settings)
587
+ {
588
+ $this->header_list['filters'] = $filter_settings;
589
+ return $this;
590
+ }
591
+
592
+ /**
593
+ * addFilterSetting
594
+ * Append a filter setting to the list of filter settings
595
+ * @param string $filter_name - filter name
596
+ * @param string $parameter_name - parameter name
597
+ * @param string $parameter_value - setting value
598
+ */
599
+ public function addFilterSetting($filter_name, $parameter_name, $parameter_value)
600
+ {
601
+ $this->header_list['filters'][$filter_name]['settings'][$parameter_name] = $parameter_value;
602
+ return $this;
603
+ }
604
+
605
+ /**
606
+ * getHeaders
607
+ * return the list of headers
608
+ * @return Array the list of headers
609
+ */
610
+ public function getHeaders()
611
+ {
612
+ return $this->header_list;
613
+ }
614
+
615
+ /**
616
+ * getHeaders
617
+ * return the list of headers
618
+ * @return Array the list of headers
619
+ */
620
+ public function getHeadersJson()
621
+ {
622
+ if (count($this->getHeaders()) <= 0)
623
+ {
624
+ return "{}";
625
+ }
626
+ return json_encode($this->getHeaders(), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
627
+ }
628
+
629
+ /**
630
+ * setHeaders
631
+ * Sets the list headers
632
+ * destroys previous header data
633
+ * @param Array $key_value_pairs - the list of header data
634
+ * @return the SendGrid\Mail object.
635
+ */
636
+ public function setHeaders($key_value_pairs)
637
+ {
638
+ $this->header_list = $key_value_pairs;
639
+ return $this;
640
+ }
641
+
642
+ /**
643
+ * addHeaders
644
+ * append the header to the list of headers
645
+ * @param String $key - the header key
646
+ * @param String $value - the header value
647
+ */
648
+ public function addHeader($key, $value)
649
+ {
650
+ $this->header_list[$key] = $value;
651
+ return $this;
652
+ }
653
+
654
+ /**
655
+ * removeHeaders
656
+ * remove a header key
657
+ * @param String $key - the key to remove
658
+ * @return the SendGrid\Mail object.
659
+ */
660
+ public function removeHeader($key)
661
+ {
662
+ unset($this->header_list[$key]);
663
+ return $this;
664
+ }
665
+
666
+ /**
667
+ * useHeaders
668
+ * Checks to see whether or not we can or should you headers. In most cases,
669
+ * we prefer to send our recipients through the headers, but in some cases,
670
+ * we actually don't want to. However, there are certain circumstances in
671
+ * which we have to.
672
+ */
673
+ public function useHeaders()
674
+ {
675
+ return !($this->_preferNotToUseHeaders() && !$this->_isHeadersRequired());
676
+ }
677
+
678
+ public function setRecipientsInHeader($preference)
679
+ {
680
+ $this->use_headers = $preference;
681
+
682
+ return $this;
683
+ }
684
+
685
+ /**
686
+ * isHeaderRequired
687
+ * determines whether or not we need to force recipients through the smtpapi headers
688
+ * @return boolean, if true headers are required
689
+ */
690
+ protected function _isHeadersRequired()
691
+ {
692
+ if(count($this->getAttachments()) > 0 || $this->use_headers )
693
+ {
694
+ return true;
695
+ }
696
+ return false;
697
+ }
698
+
699
+ /**
700
+ * _preferNotToUseHeaders
701
+ * There are certain cases in which headers are not a preferred choice
702
+ * to send email, as it limits some basic email functionality. Here, we
703
+ * check for any of those rules, and add them in to decide whether or
704
+ * not to use headers
705
+ * @return boolean, if true we don't
706
+ */
707
+ protected function _preferNotToUseHeaders()
708
+ {
709
+ if (count($this->getBccs()) > 0 || count($this->getCcs()) > 0)
710
+ {
711
+ return true;
712
+ }
713
+ if ($this->use_headers !== null && !$this->use_headers)
714
+ {
715
+ return true;
716
+ }
717
+
718
+ return false;
719
+ }
720
+
721
+ }
lib/sendgrid-php/SendGrid/MailInterface.php ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace SendGrid;
4
+
5
+ interface MailInterface
6
+ {
7
+ public function send(Mail $mail);
8
+
9
+
10
+ }
lib/sendgrid-php/SendGrid/Smtp.php ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace SendGrid;
4
+
5
+ class Smtp extends Api implements MailInterface
6
+ {
7
+ //the available ports
8
+ const TLS = 587;
9
+ const TLS_ALTERNATIVE = 25;
10
+ const SSL = 465;
11
+
12
+ //the list of port instances, to be recycled
13
+ private $swift_instances = array();
14
+ protected $port;
15
+
16
+ public function __construct($username, $password)
17
+ {
18
+ /* check for SwiftMailer,
19
+ * if it doesn't exist, try loading
20
+ * it from Pear
21
+ */
22
+ if (!class_exists('Swift')) {
23
+ require_once 'swift_required.php';
24
+ }
25
+ call_user_func_array("parent::__construct", func_get_args());
26
+
27
+ //set the default port
28
+ $this->port = Smtp::TLS;
29
+ }
30
+
31
+ /* setPort
32
+ * set the SMTP outgoing port number
33
+ * @param Int $port - the port number to use
34
+ * @return the SMTP object
35
+ */
36
+ public function setPort($port)
37
+ {
38
+ $this->port = $port;
39
+
40
+ return $this;
41
+ }
42
+
43
+ /* _getSwiftInstance
44
+ * initialize and return the swift transport instance
45
+ * @return the Swift_Mailer instance
46
+ */
47
+ private function _getSwiftInstance($port)
48
+ {
49
+ if (!isset($this->swift_instances[$port]))
50
+ {
51
+ $transport = \Swift_SmtpTransport::newInstance('smtp.sendgrid.net', $port);
52
+ $transport->setUsername($this->username);
53
+ $transport->setPassword($this->password);
54
+
55
+ $swift = \Swift_Mailer::newInstance($transport);
56
+
57
+ $this->swift_instances[$port] = $swift;
58
+ }
59
+
60
+ return $this->swift_instances[$port];
61
+ }
62
+
63
+ /* _mapToSwift
64
+ * Maps the SendGridMail Object to the SwiftMessage object
65
+ * @param Mail $mail - the SendGridMail object
66
+ * @return the SwiftMessage object
67
+ */
68
+ protected function _mapToSwift(Mail $mail)
69
+ {
70
+ $message = new \Swift_Message($mail->getSubject());
71
+
72
+ /*
73
+ * Since we're sending transactional email, we want the message to go to one person at a time, rather
74
+ * than a bulk send on one message. In order to do this, we'll have to send the list of recipients through the headers
75
+ * but Swift still requires a 'to' address. So we'll falsify it with the from address, as it will be
76
+ * ignored anyway.
77
+ */
78
+ $message->setTo($mail->getFrom());
79
+ $message->setFrom($mail->getFrom(true));
80
+ $message->setCc($mail->getCcs());
81
+ $message->setBcc($mail->getBccs());
82
+
83
+ if ($mail->getHtml())
84
+ {
85
+ $message->setBody($mail->getHtml(), 'text/html');
86
+ if ($mail->getText()) $message->addPart($mail->getText(), 'text/plain');
87
+ }
88
+ else
89
+ {
90
+ $message->setBody($mail->getText(), 'text/plain');
91
+ }
92
+
93
+ if(($replyto = $mail->getReplyTo())) {
94
+ $message->setReplyTo($replyto);
95
+ }
96
+
97
+ // determine whether or not we can use SMTP recipients (non header based)
98
+ if($mail->useHeaders())
99
+ {
100
+ //send header based email
101
+ $message->setTo($mail->getFrom());
102
+
103
+ //here we'll add the recipients list to the headers
104
+ $headers = $mail->getHeaders();
105
+ $headers['to'] = $mail->getTos();
106
+ $mail->setHeaders($headers);
107
+ }
108
+ else
109
+ {
110
+ $recipients = array();
111
+ foreach ($mail->getTos() as $recipient)
112
+ {
113
+ if(preg_match("/(.*)<(.*)>/", $recipient, $results))
114
+ {
115
+ $recipients[trim($results[2])] = trim($results[1]);
116
+ }
117
+ else
118
+ {
119
+ $recipients[] = $recipient;
120
+ }
121
+ }
122
+
123
+ $message->setTo($recipients);
124
+ }
125
+
126
+ $attachments = $mail->getAttachments();
127
+
128
+ //add any attachments that were added
129
+ if ($attachments)
130
+ {
131
+ foreach ($attachments as $attachment)
132
+ {
133
+ $message->attach(\Swift_Attachment::fromPath($attachment['file']));
134
+ }
135
+ }
136
+
137
+ //add all the headers
138
+ $headers = $message->getHeaders();
139
+ $headers->addTextHeader('X-SMTPAPI', $mail->getHeadersJson());
140
+
141
+ return $message;
142
+ }
143
+
144
+ /* send
145
+ * Send the Mail Message
146
+ * @param Mail $mail - the SendGridMailMessage to be sent
147
+ * @return true if mail was sendable (not necessarily sent)
148
+ */
149
+ public function send(Mail $mail)
150
+ {
151
+ $swift = $this->_getSwiftInstance($this->port);
152
+
153
+ $message = $this->_mapToSwift($mail);
154
+
155
+ $swift->send($message, $failures);
156
+
157
+ return true;
158
+ }
159
+ }
lib/sendgrid-php/SendGrid/Web.php ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace SendGrid;
4
+
5
+ class Web extends Api implements MailInterface
6
+ {
7
+
8
+ protected $domain = "http://sendgrid.com/";
9
+ protected $endpoint = "api/mail.send.json";
10
+
11
+ /**
12
+ * __construct
13
+ * Create a new Web instance
14
+ */
15
+ public function __construct($username, $password)
16
+ {
17
+ call_user_func_array("parent::__construct", func_get_args());
18
+ }
19
+
20
+ /**
21
+ * _prepMessageData
22
+ * Takes the mail message and returns a url friendly querystring
23
+ * @param Mail $mail [description]
24
+ * @return String - the data query string to be posted
25
+ */
26
+ protected function _prepMessageData(Mail $mail)
27
+ {
28
+
29
+ /* the api expects a 'to' parameter, but this parameter will be ignored
30
+ * since we're sending the recipients through the header. The from
31
+ * address will be used as a placeholder.
32
+ */
33
+ $params =
34
+ array(
35
+ 'api_user' => $this->username,
36
+ 'api_key' => $this->password,
37
+ 'subject' => $mail->getSubject(),
38
+ 'from' => $mail->getFrom(),
39
+ 'to' => $mail->getFrom(),
40
+ 'x-smtpapi' => $mail->getHeadersJson()
41
+ );
42
+
43
+ if($mail->getHtml()) {
44
+ $params['html'] = $mail->getHtml();
45
+ }
46
+
47
+ if($mail->getText()) {
48
+ $params['text'] = $mail->getText();
49
+ }
50
+
51
+ if(($fromname = $mail->getFromName())) {
52
+ $params['fromname'] = $fromname;
53
+ }
54
+
55
+ if(($replyto = $mail->getReplyTo())) {
56
+ $params['replyto'] = $replyto;
57
+ }
58
+
59
+ // determine if we should send our recipients through our headers,
60
+ // and set the properties accordingly
61
+ if($mail->useHeaders())
62
+ {
63
+ // workaround for posting recipients through SendGrid headers
64
+ $headers = $mail->getHeaders();
65
+ $headers['to'] = $mail->getTos();
66
+ $mail->setHeaders($headers);
67
+
68
+ $params['x-smtpapi'] = $mail->getHeadersJson();
69
+ }
70
+ else
71
+ {
72
+ $params['to'] = $mail->getTos();
73
+ }
74
+
75
+
76
+ if($mail->getAttachments())
77
+ {
78
+ foreach($mail->getAttachments() as $attachment)
79
+ {
80
+ $params['files['.$attachment['filename'].'.'.$attachment['extension'].']'] = '@'.$attachment['file'];
81
+ }
82
+ }
83
+
84
+ return $params;
85
+ }
86
+
87
+ /**
88
+ * _arrayToUrlPart
89
+ * Converts an array to a url friendly string
90
+ * @param array $array - the array to convert
91
+ * @param String $token - the name of parameter
92
+ * @return String - a url part that can be concatenated to a url request
93
+ */
94
+ protected function _arrayToUrlPart($array, $token)
95
+ {
96
+ $string = "";
97
+
98
+ if ($array)
99
+ {
100
+ foreach ($array as $value)
101
+ {
102
+ $string.= "&" . $token . "[]=" . urlencode($value);
103
+ }
104
+ }
105
+
106
+ return $string;
107
+ }
108
+
109
+ /**
110
+ * send
111
+ * Send an email
112
+ * @param Mail $mail - The message to send
113
+ * @return String the json response
114
+ */
115
+ public function send(Mail $mail)
116
+ {
117
+ $data = $this->_prepMessageData($mail);
118
+
119
+ //if we're not using headers, we need to send a url friendly post
120
+ if(!$mail->useHeaders())
121
+ {
122
+ $data = http_build_query($data);
123
+ }
124
+
125
+ $request = $this->domain . $this->endpoint;
126
+
127
+ // we'll append the Bcc and Cc recipients to the url endpoint (GET)
128
+ // so that we can still post attachments (via cURL array).
129
+ $request.= "?" .
130
+ substr($this->_arrayToUrlPart($mail->getBccs(), "bcc"), 1) .
131
+ $this->_arrayToUrlPart($mail->getCcs(), "cc");
132
+
133
+ $session = curl_init($request);
134
+ curl_setopt($session, CURLOPT_POST, true);
135
+ curl_setopt($session, CURLOPT_POSTFIELDS, $data);
136
+ curl_setopt($session, CURLOPT_HEADER, false);
137
+ curl_setopt($session, CURLOPT_RETURNTRANSFER, true);
138
+ curl_setopt($session, CURLOPT_CONNECTTIMEOUT, 5);
139
+ curl_setopt($session, CURLOPT_TIMEOUT, 30);
140
+
141
+ // obtain response
142
+ $response = curl_exec($session);
143
+ curl_close($session);
144
+
145
+ return $response;
146
+ }
147
+ }
lib/sendgrid-php/SendGrid_loader.php ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ define("ROOT_DIR", __dir__ . DIRECTORY_SEPARATOR);
4
+
5
+ function sendGridLoader($string)
6
+ {
7
+ if(preg_match("/SendGrid/", $string))
8
+ {
9
+ $file = str_replace('\\', '/', "$string.php");
10
+ require_once ROOT_DIR . $file;
11
+ }
12
+ }
13
+
14
+ spl_autoload_register("sendGridLoader");
lib/sendgrid-php/Test/Mock/Mock_loader.php ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ define("MOCK_ROOT", __dir__ . DIRECTORY_SEPARATOR);
4
+
5
+ function mockLoader($string)
6
+ {
7
+ if(preg_match("/Mock/", $string))
8
+ {
9
+ $file = str_replace('\\', '/', "$string.php");
10
+ require_once MOCK_ROOT . $file;
11
+ }
12
+ }
13
+
14
+ spl_autoload_register("mockLoader");
lib/sendgrid-php/Test/Mock/SmtpMock.php ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class SmtpMock extends SendGrid\Smtp
4
+ {
5
+ public function __construct($username, $password)
6
+ {
7
+ parent::__construct($username, $password);
8
+ }
9
+
10
+ public function getPort()
11
+ {
12
+ return $this->port;
13
+ }
14
+ }
lib/sendgrid-php/Test/Mock/WebMock.php ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WebMock extends SendGrid\Web
4
+ {
5
+ public function __construct($username, $password)
6
+ {
7
+ parent::__construct($username, $password);
8
+ }
9
+
10
+ public function testPrepMessageData(SendGrid\Mail $mail)
11
+ {
12
+ return $this->_prepMessageData($mail);
13
+ }
14
+
15
+ public function testArrayToUrlPart($array, $token)
16
+ {
17
+ return $this->_arrayToUrlPart($array, $token);
18
+ }
19
+ }
lib/sendgrid-php/Test/SendGrid/ApiTest.php ADDED
File without changes
lib/sendgrid-php/Test/SendGrid/MailTest.php ADDED
@@ -0,0 +1,549 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+
4
+ class MailTest extends PHPUnit_Framework_TestCase
5
+ {
6
+
7
+ public function testToAccessors()
8
+ {
9
+ $message = new SendGrid\Mail();
10
+
11
+ // setTo instanciates and overrides existing data
12
+ $message->setTo('bar');
13
+ $message->setTo('foo');
14
+
15
+ $this->assertEquals(1, count($message->getTos()));
16
+
17
+ $to_list = $message->getTos();
18
+
19
+ $this->assertEquals('foo', $to_list[0]);
20
+
21
+
22
+ // setTos instanciates and overrides existing data
23
+ $message->setTos(array('raz', 'ber'));
24
+
25
+ $this->assertEquals(2, count($message->getTos()));
26
+
27
+ $to_list = $message->getTos();
28
+
29
+ $this->assertEquals('raz', $to_list[0]);
30
+ $this->assertEquals('ber', $to_list[1]);
31
+
32
+ // addTo appends to existing data
33
+ $message->addTo('foo');
34
+ $message->addTo('raz');
35
+
36
+ $this->assertEquals(4, count($message->getTos()));
37
+
38
+ $to_list = $message->getTos();
39
+
40
+ $this->assertEquals('raz', $to_list[0]);
41
+ $this->assertEquals('ber', $to_list[1]);
42
+ $this->assertEquals('foo', $to_list[2]);
43
+ $this->assertEquals('raz', $to_list[3]);
44
+
45
+ // removeTo removes all occurences of data
46
+ $message->removeTo('raz');
47
+
48
+ $this->assertEquals(2, count($message->getTos()));
49
+
50
+ $to_list = $message->getTos();
51
+
52
+ $this->assertEquals('ber', $to_list[0]);
53
+ $this->assertEquals('foo', $to_list[1]);
54
+ }
55
+
56
+ public function testFromAccessors()
57
+ {
58
+ $message = new SendGrid\Mail();
59
+
60
+ $message->setFrom("foo@bar.com");
61
+ $message->setFromName("John Doe");
62
+
63
+ $this->assertEquals("foo@bar.com", $message->getFrom());
64
+ $this->assertEquals(array("foo@bar.com" => "John Doe"), $message->getFrom(true));
65
+ }
66
+
67
+ public function testFromNameAccessors()
68
+ {
69
+ $message = new SendGrid\Mail();
70
+
71
+ // Defaults to false
72
+ $this->assertFalse($message->getFromName());
73
+
74
+ $message->setFromName("Swift");
75
+
76
+ $this->assertEquals("Swift", $message->getFromName());
77
+ }
78
+
79
+ public function testReplyToAccessors()
80
+ {
81
+ $message = new SendGrid\Mail();
82
+
83
+ // Defaults to false
84
+ $this->assertFalse($message->getReplyTo());
85
+
86
+ $message->setReplyTo("swift@sendgrid.com");
87
+
88
+ $this->assertEquals("swift@sendgrid.com", $message->getReplyTo());
89
+ }
90
+
91
+ public function testCcAccessors()
92
+ {
93
+ $message = new SendGrid\Mail();
94
+
95
+ // setTo instanciates and overrides existing data
96
+ $message->setCc('bar');
97
+ $message->setCc('foo');
98
+
99
+ $this->assertEquals(1, count($message->getCcs()));
100
+
101
+ $cc_list = $message->getCcs();
102
+
103
+ $this->assertEquals('foo', $cc_list[0]);
104
+
105
+
106
+ // setTos instanciates and overrides existing data
107
+ $message->setCcs(array('raz', 'ber'));
108
+
109
+ $this->assertEquals(2, count($message->getCcs()));
110
+
111
+ $cc_list = $message->getCcs();
112
+
113
+ $this->assertEquals('raz', $cc_list[0]);
114
+ $this->assertEquals('ber', $cc_list[1]);
115
+
116
+ // addTo appends to existing data
117
+ $message->addCc('foo');
118
+ $message->addCc('raz');
119
+
120
+ $this->assertEquals(4, count($message->getCcs()));
121
+
122
+ $cc_list = $message->getCcs();
123
+
124
+ $this->assertEquals('raz', $cc_list[0]);
125
+ $this->assertEquals('ber', $cc_list[1]);
126
+ $this->assertEquals('foo', $cc_list[2]);
127
+ $this->assertEquals('raz', $cc_list[3]);
128
+
129
+ // removeTo removes all occurences of data
130
+ $message->removeCc('raz');
131
+
132
+ $this->assertEquals(2, count($message->getCcs()));
133
+
134
+ $cc_list = $message->getCcs();
135
+
136
+ $this->assertEquals('ber', $cc_list[0]);
137
+ $this->assertEquals('foo', $cc_list[1]);
138
+ }
139
+
140
+ public function testBccAccessors()
141
+ {
142
+ $message = new SendGrid\Mail();
143
+
144
+ // setTo instanciates and overrides existing data
145
+ $message->setBcc('bar');
146
+ $message->setBcc('foo');
147
+
148
+ $this->assertEquals(1, count($message->getBccs()));
149
+
150
+ $bcc_list = $message->getBccs();
151
+
152
+ $this->assertEquals('foo', $bcc_list[0]);
153
+
154
+
155
+ // setTos instanciates and overrides existing data
156
+ $message->setBccs(array('raz', 'ber'));
157
+
158
+ $this->assertEquals(2, count($message->getBccs()));
159
+
160
+ $bcc_list = $message->getBccs();
161
+
162
+ $this->assertEquals('raz', $bcc_list[0]);
163
+ $this->assertEquals('ber', $bcc_list[1]);
164
+
165
+ // addTo appends to existing data
166
+ $message->addBcc('foo');
167
+ $message->addBcc('raz');
168
+
169
+ $this->assertEquals(4, count($message->getBccs()));
170
+
171
+ $bcc_list = $message->getBccs();
172
+
173
+ $this->assertEquals('raz', $bcc_list[0]);
174
+ $this->assertEquals('ber', $bcc_list[1]);
175
+ $this->assertEquals('foo', $bcc_list[2]);
176
+ $this->assertEquals('raz', $bcc_list[3]);
177
+
178
+ // removeTo removes all occurences of data
179
+ $message->removeBcc('raz');
180
+
181
+ $this->assertEquals(2, count($message->getBccs()));
182
+
183
+ $bcc_list = $message->getBccs();
184
+
185
+ $this->assertEquals('ber', $bcc_list[0]);
186
+ $this->assertEquals('foo', $bcc_list[1]);
187
+ }
188
+
189
+ public function testSubjectAccessors()
190
+ {
191
+ $message = new SendGrid\Mail();
192
+
193
+ $message->setSubject("Test Subject");
194
+
195
+ $this->assertEquals("Test Subject", $message->getSubject());
196
+ }
197
+
198
+ public function testTextAccessors()
199
+ {
200
+ $message = new SendGrid\Mail();
201
+
202
+ $text = "sample plain text";
203
+
204
+ $message->setText($text);
205
+
206
+ $this->assertEquals($text, $message->getText());
207
+ }
208
+
209
+ public function testHTMLAccessors()
210
+ {
211
+ $message = new SendGrid\Mail();
212
+
213
+ $html = "<p style = 'color:red;'>Sample HTML text</p>";
214
+
215
+ $message->setHtml($html);
216
+
217
+ $this->assertEquals($html, $message->getHtml());
218
+ }
219
+
220
+ public function testAttachmentAccessors()
221
+ {
222
+ $message = new SendGrid\Mail();
223
+
224
+ $attachments =
225
+ array(
226
+ "path/to/file/file_1.txt",
227
+ "../file_2.txt",
228
+ "../file_3.txt"
229
+ );
230
+
231
+ $message->setAttachments($attachments);
232
+
233
+ $msg_attachments = $message->getAttachments();
234
+
235
+ $this->assertEquals(count($attachments), count($msg_attachments));
236
+
237
+ for($i = 0; $i < count($attachments); $i++)
238
+ {
239
+ $this->assertEquals($attachments[$i], $msg_attachments[$i]['file']);
240
+ }
241
+
242
+ //ensure that addAttachment appends to the list of attachments
243
+ $message->addAttachment("../file_4.png");
244
+
245
+ $attachments[] = "../file_4.png";
246
+
247
+ $msg_attachments = $message->getAttachments();
248
+ $this->assertEquals($attachments[count($attachments) - 1], $msg_attachments[count($msg_attachments) - 1]['file']);
249
+
250
+
251
+ //Setting an attachment removes all other files
252
+ $message->setAttachment("only_attachment.sad");
253
+
254
+ $this->assertEquals(1, count($message->getAttachments()));
255
+
256
+ //Remove an attachment
257
+ $message->removeAttachment("only_attachment.sad");
258
+ $this->assertEquals(0, count($message->getAttachments()));
259
+ }
260
+
261
+ public function testCategoryAccessors()
262
+ {
263
+ $message = new SendGrid\Mail();
264
+
265
+ $message->setCategory('category_0');
266
+ $this->assertEquals("{\"category\":[\"category_0\"]}", $message->getHeadersJson());
267
+
268
+ $categories = array(
269
+ "category_1",
270
+ "category_2",
271
+ "category_3",
272
+ "category_4"
273
+ );
274
+
275
+ $message->setCategories($categories);
276
+
277
+ $header = $message->getHeaders();
278
+
279
+ // ensure that the array is the same
280
+ $this->assertEquals($categories, $header['category']);
281
+
282
+ // uses valid json
283
+ $this->assertEquals("{\"category\":[\"category_1\",\"category_2\",\"category_3\",\"category_4\"]}", $message->getHeadersJson());
284
+
285
+ // ensure that addCategory appends to the list of categories
286
+ $category = "category_5";
287
+ $message->addCategory($category);
288
+
289
+ $header = $message->getHeaders();
290
+
291
+ $this->assertEquals(5, count($header['category']));
292
+
293
+ $categories[] = $category;
294
+
295
+ $this->assertEquals($categories, $header['category']);
296
+
297
+
298
+ // removeCategory removes all occurrences of a category
299
+ $message->removeCategory("category_3");
300
+
301
+ $header = $message->getHeaders();
302
+
303
+ unset($categories[2]);
304
+ $categories = array_values($categories);
305
+
306
+ $this->assertEquals(4, count($header['category']));
307
+
308
+ $this->assertEquals($categories, $header['category']);
309
+ }
310
+
311
+ public function testSubstitutionAccessors()
312
+ {
313
+ $message = new SendGrid\Mail();
314
+
315
+ $substitutions = array(
316
+ "sub_1" => array("val_1.1", "val_1.2", "val_1.3"),
317
+ "sub_2" => array("val_2.1", "val_2.2"),
318
+ "sub_3" => array("val_3.1", "val_3.2", "val_3.3", "val_3.4"),
319
+ "sub_4" => array("val_4.1", "val_4.2", "val_4.3")
320
+ );
321
+
322
+ $message->setSubstitutions($substitutions);
323
+
324
+ $header = $message->getHeaders();
325
+
326
+ $this->assertEquals($substitutions, $header['sub']);
327
+
328
+ $this->assertEquals("{\"sub\":{\"sub_1\":[\"val_1.1\",\"val_1.2\",\"val_1.3\"],\"sub_2\":[\"val_2.1\",\"val_2.2\"],\"sub_3\":[\"val_3.1\",\"val_3.2\",\"val_3.3\",\"val_3.4\"],\"sub_4\":[\"val_4.1\",\"val_4.2\",\"val_4.3\"]}}", $message->getHeadersJson());
329
+
330
+ // ensure that addSubstitution appends to the list of substitutions
331
+
332
+ $sub_vals = array("val_5.1", "val_5.2", "val_5.3", "val_5.4");
333
+ $message->addSubstitution("sub_5", $sub_vals);
334
+
335
+ $substitutions["sub_5"] = $sub_vals;
336
+
337
+ $header = $message->getHeaders();
338
+
339
+ $this->assertEquals(5, count($header['sub']));
340
+ $this->assertEquals($substitutions, $header['sub']);
341
+ }
342
+
343
+ public function testSectionAccessors()
344
+ {
345
+ $message = new SendGrid\Mail();
346
+
347
+ $sections = array(
348
+ "sub_1" => array("val_1.1", "val_1.2", "val_1.3"),
349
+ "sub_2" => array("val_2.1", "val_2.2"),
350
+ "sub_3" => array("val_3.1", "val_3.2", "val_3.3", "val_3.4"),
351
+ "sub_4" => array("val_4.1", "val_4.2", "val_4.3")
352
+ );
353
+
354
+ $message->setSections($sections);
355
+
356
+ $header = $message->getHeaders();
357
+
358
+ $this->assertEquals($sections, $header['section']);
359
+
360
+ $this->assertEquals("{\"section\":{\"sub_1\":[\"val_1.1\",\"val_1.2\",\"val_1.3\"],\"sub_2\":[\"val_2.1\",\"val_2.2\"],\"sub_3\":[\"val_3.1\",\"val_3.2\",\"val_3.3\",\"val_3.4\"],\"sub_4\":[\"val_4.1\",\"val_4.2\",\"val_4.3\"]}}", $message->getHeadersJson());
361
+
362
+ // ensure that addSubstitution appends to the list of substitutions
363
+
364
+ $section_vals = array("val_5.1", "val_5.2", "val_5.3", "val_5.4");
365
+ $message->addSection("sub_5", $section_vals);
366
+
367
+ $sections["sub_5"] = $section_vals;
368
+
369
+ $header = $message->getHeaders();
370
+
371
+ $this->assertEquals(5, count($header['section']));
372
+ $this->assertEquals($sections, $header['section']);
373
+ }
374
+
375
+ public function testUniqueArgumentsAccessors()
376
+ {
377
+ $message = new SendGrid\Mail();
378
+
379
+ $unique_arguments = array(
380
+ "sub_1" => array("val_1.1", "val_1.2", "val_1.3"),
381
+ "sub_2" => array("val_2.1", "val_2.2"),
382
+ "sub_3" => array("val_3.1", "val_3.2", "val_3.3", "val_3.4"),
383
+ "sub_4" => array("val_4.1", "val_4.2", "val_4.3")
384
+ );
385
+
386
+ $message->setUniqueArguments($unique_arguments);
387
+
388
+ $header = $message->getHeaders();
389
+
390
+ $this->assertEquals($unique_arguments, $header['unique_args']);
391
+
392
+ $this->assertEquals("{\"unique_args\":{\"sub_1\":[\"val_1.1\",\"val_1.2\",\"val_1.3\"],\"sub_2\":[\"val_2.1\",\"val_2.2\"],\"sub_3\":[\"val_3.1\",\"val_3.2\",\"val_3.3\",\"val_3.4\"],\"sub_4\":[\"val_4.1\",\"val_4.2\",\"val_4.3\"]}}", $message->getHeadersJson());
393
+
394
+ // ensure that addSubstitution appends to the list of substitutions
395
+
396
+ $unique_vals = array("val_5.1", "val_5.2", "val_5.3", "val_5.4");
397
+ $message->addUniqueArgument("sub_5", $unique_vals);
398
+
399
+ $unique_arguments["sub_5"] = $unique_vals;
400
+
401
+ $header = $message->getHeaders();
402
+
403
+ $this->assertEquals(5, count($header['unique_args']));
404
+ $this->assertEquals($unique_arguments, $header['unique_args']);
405
+ }
406
+
407
+ public function testFilterSettingsAccessors()
408
+ {
409
+ $message = new SendGrid\Mail();
410
+
411
+ $filters =
412
+ array(
413
+ "filter_1" =>
414
+ array(
415
+ "settings" =>
416
+ array(
417
+ "enable" => 1,
418
+ "setting_1" => "setting_val_1"
419
+ )
420
+ ),
421
+ "filter_2" =>
422
+ array(
423
+ "settings" =>
424
+ array(
425
+ "enable" => 0,
426
+ "setting_2" => "setting_val_2",
427
+ "setting_3" => "setting_val_3"
428
+ )
429
+ ),
430
+ "filter_3" =>
431
+ array(
432
+ "settings" =>
433
+ array(
434
+ "enable" => 0,
435
+ "setting_4" => "setting_val_4",
436
+ "setting_5" => "setting_val_5"
437
+ )
438
+ ),
439
+ );
440
+
441
+ $message->setFilterSettings($filters);
442
+
443
+ $header = $message->getHeaders();
444
+
445
+ $this->assertEquals(count($filters), count($header['filters']));
446
+
447
+ $this->assertEquals($filters, $header['filters']);
448
+
449
+
450
+ //the addFilter appends to the filter list
451
+ $message->addFilterSetting("filter_4", "enable", 0);
452
+ $message->addFilterSetting("filter_4", "setting_6", "setting_val_6");
453
+ $message->addFilterSetting("filter_4", "setting_7", "setting_val_7");
454
+
455
+ $filters["filter_4"] =
456
+ array(
457
+ "settings" =>
458
+ array(
459
+ "enable" => 0,
460
+ "setting_6" => "setting_val_6",
461
+ "setting_7" => "setting_val_7"
462
+ )
463
+ );
464
+
465
+ $header = $message->getHeaders();
466
+
467
+ $this->assertEquals($filters, $header['filters']);
468
+ }
469
+
470
+ public function testHeaderAccessors()
471
+ {
472
+ $message = new SendGrid\Mail();
473
+
474
+ $this->assertEquals("{}", $message->getHeadersJson());
475
+
476
+
477
+ $headers =
478
+ array(
479
+ "header_1" =>
480
+ array(
481
+ "item_1" => "value_1",
482
+ "item_2" => "value_2",
483
+ "item_3" => "value_3"
484
+ ),
485
+ "header_2" => "value_4",
486
+ "header_3" => "value_4",
487
+ "header_4" =>
488
+ array(
489
+ "item_4" =>
490
+ array(
491
+ "sub_item_1" => "sub_value_1",
492
+ "sub_item_2" => "sub_value_2"
493
+ )
494
+ )
495
+ );
496
+
497
+
498
+ $message->setHeaders($headers);
499
+
500
+
501
+ $this->assertEquals($headers, $message->getHeaders());
502
+
503
+ $message->addHeader("simple_header", "simple_value");
504
+
505
+ $headers["simple_header"] = "simple_value";
506
+
507
+ $this->assertEquals($headers, $message->getHeaders());
508
+ $this->assertEquals("{\"header_1\":{\"item_1\":\"value_1\",\"item_2\":\"value_2\",\"item_3\":\"value_3\"},\"header_2\":\"value_4\",\"header_3\":\"value_4\",\"header_4\":{\"item_4\":{\"sub_item_1\":\"sub_value_1\",\"sub_item_2\":\"sub_value_2\"}},\"simple_header\":\"simple_value\"}", $message->getHeadersJson());
509
+
510
+ //remove a header
511
+ $message->removeHeader("simple_header");
512
+
513
+ unset($headers["simple_header"]);
514
+
515
+ $this->assertEquals($headers, $message->getHeaders());
516
+ }
517
+
518
+ public function testUseHeaders()
519
+ {
520
+ $mail = new SendGrid\Mail();
521
+
522
+ $mail->addTo('foo@bar.com')->
523
+ addBcc('baa@bar.com')->
524
+ setFrom('boo@foo.com')->
525
+ setSubject('Subject')->
526
+ setHtml('Hello You');
527
+
528
+ $this->assertFalse($mail->useHeaders());
529
+
530
+ $mail->removeBcc('baa@bar.com');
531
+ $this->assertTrue($mail->useHeaders());
532
+
533
+ $mail->addCc('bot@bar.com');
534
+ $this->assertFalse($mail->useHeaders());
535
+
536
+ $mail->removeCc('bot@bar.com')->
537
+ setRecipientsinHeader(true);
538
+ $this->assertTrue($mail->useHeaders());
539
+
540
+ $mail->setRecipientsinHeader(false);
541
+ $this->assertFalse($mail->useHeaders());
542
+
543
+ $mail->
544
+ addBcc('baa@bar.com')->
545
+ addAttachment('attachment.ext');
546
+
547
+ $this->assertTrue($mail->useHeaders());
548
+ }
549
+ }
lib/sendgrid-php/Test/SendGrid/SmtpTest.php ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class SmtpTest extends PHPUnit_Framework_TestCase
4
+ {
5
+ public function testConstruction()
6
+ {
7
+ $sendgrid = new SendGrid("foo", "bar");
8
+
9
+ $smtp = $sendgrid->smtp;
10
+
11
+ $this->assertEquals(new SendGrid\Smtp("foo", "bar"), $smtp);
12
+
13
+ $message = new SendGrid\Mail();
14
+ $message->
15
+ setFrom('bar@foo.com')->
16
+ setFromName('John Doe')->
17
+ setSubject('foobar subject')->
18
+ setText('foobar text')->
19
+ addTo('foo@bar.com')->
20
+ addAttachment("mynewattachment.jpg");
21
+
22
+ $this->assertEquals(get_class($smtp), 'SendGrid\Smtp');
23
+
24
+ $this->setExpectedException('Swift_TransportException');
25
+ $smtp->send($message);
26
+ }
27
+
28
+ public function testPorts()
29
+ {
30
+ $this->assertEquals(587, SendGrid\Smtp::TLS);
31
+ $this->assertEquals(25, SendGrid\Smtp::TLS_ALTERNATIVE);
32
+ $this->assertEquals(465, SendGrid\Smtp::SSL);
33
+
34
+ $sendgrid = new SendGrid("foo", "bar");
35
+
36
+ //we can't check that the port works, but we can check that it doesn't throw an exception
37
+ $object = $sendgrid->smtp->setPort(SendGrid\Smtp::TLS);
38
+
39
+ $this->assertEquals($sendgrid->smtp, $object);
40
+ $this->assertEquals(get_class($object), 'SendGrid\Smtp');
41
+
42
+
43
+ $mock = new SmtpMock('foo', 'bar');
44
+
45
+ $mock->setPort('52');
46
+ $this->assertEquals('52', $mock->getPort());
47
+ }
48
+
49
+ public function testEmailBodyAttachments()
50
+ {
51
+ $_mapToSwift = new ReflectionMethod('SendGrid\Smtp', '_mapToSwift');
52
+ $_mapToSwift->setAccessible(true);
53
+
54
+ $sendgrid = new SendGrid("foo", "bar");
55
+ $message = new SendGrid\Mail();
56
+ $message->
57
+ setFrom('bar@foo.com')->
58
+ setFromName('John Doe')->
59
+ setSubject('foobar subject')->
60
+ setHtml('foobar html')->
61
+ addTo('foo@bar.com');
62
+
63
+ $swift_message = $_mapToSwift->invoke($sendgrid->smtp, $message);
64
+ $this->assertEquals(count($swift_message->getChildren()), 0);
65
+
66
+ $message->setText('foobar text');
67
+
68
+ $swift_message = $_mapToSwift->invoke($sendgrid->smtp, $message);
69
+ $this->assertEquals(count($swift_message->getChildren()), 1);
70
+ $body_attachments = $swift_message->getChildren();
71
+ $this->assertEquals($body_attachments[0]->getContentType(), 'text/plain');
72
+ }
73
+
74
+ public function testEmailTextBodyAttachments()
75
+ {
76
+ $_mapToSwift = new ReflectionMethod('SendGrid\Smtp', '_mapToSwift');
77
+ $_mapToSwift->setAccessible(true);
78
+
79
+ $sendgrid = new SendGrid("foo", "bar");
80
+ $message = new SendGrid\Mail();
81
+ $message->
82
+ setFrom('bar@foo.com')->
83
+ setFromName('John Doe')->
84
+ setSubject('foobar subject')->
85
+ setText('foobar text')->
86
+ addTo('foo@bar.com');
87
+
88
+ $swift_message = $_mapToSwift->invoke($sendgrid->smtp, $message);
89
+ $this->assertEquals(count($swift_message->getChildren()), 0);
90
+ }
91
+ }
lib/sendgrid-php/Test/SendGrid/WebTest.php ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class WebTest extends PHPUnit_Framework_TestCase
4
+ {
5
+ public function testConstruction()
6
+ {
7
+ $sendgrid = new SendGrid("foo", "bar");
8
+
9
+ $web = $sendgrid->web;
10
+
11
+ $this->assertEquals(new SendGrid\Web("foo", "bar"), $web);
12
+ $this->assertEquals(get_class($web), "SendGrid\Web");
13
+ }
14
+
15
+ public function testMockFunctions()
16
+ {
17
+ $message = new SendGrid\Mail();
18
+
19
+ $message->
20
+ setFrom('bar@foo.com')->
21
+ setSubject('foobar subject')->
22
+ setText('foobar text')->
23
+ setHtml('foobar html')->
24
+ addTo('foo@bar.com')->
25
+ addAttachment("mynewattachment.jpg");
26
+
27
+ $mock = new WebMock("foo", "bar");
28
+ $data = $mock->testPrepMessageData($message);
29
+
30
+ $expected =
31
+ array(
32
+ 'api_user' => 'foo',
33
+ 'api_key' => 'bar',
34
+ 'subject' => 'foobar subject',
35
+ 'html' => 'foobar html',
36
+ 'text' => 'foobar text',
37
+ 'from' => 'bar@foo.com',
38
+ 'to' => 'bar@foo.com',
39
+ 'x-smtpapi' => '{"to":["foo@bar.com"]}',
40
+ 'files[mynewattachment.jpg]' => '@mynewattachment.jpg'
41
+ );
42
+
43
+ $this->assertEquals($expected, $data);
44
+
45
+
46
+ $array =
47
+ array(
48
+ "foo",
49
+ "bar",
50
+ "car",
51
+ "doo"
52
+ );
53
+
54
+ $url_part = $mock->testArrayToUrlPart($array, "param");
55
+
56
+ $this->assertEquals("&param[]=foo&param[]=bar&param[]=car&param[]=doo", $url_part);
57
+ }
58
+
59
+ public function testOptionalParamters()
60
+ {
61
+ $message = new SendGrid\Mail();
62
+ $mock = new WebMock("foo", "bar");
63
+
64
+ // Default Values
65
+ $actual_without_optional_params = $mock->testPrepMessageData($message);
66
+
67
+ $this->assertArrayNotHasKey('html', $actual_without_optional_params);
68
+ $this->assertArrayNotHasKey('text', $actual_without_optional_params);
69
+ $this->assertArrayNotHasKey('fromname', $actual_without_optional_params);
70
+ $this->assertArrayNotHasKey('replyto', $actual_without_optional_params);
71
+
72
+ // Set optional params
73
+ $message->setFromName('John Doe');
74
+ $message->setReplyTo('swift@sendgrid.com');
75
+
76
+ $actual_with_optional_params = $mock->testPrepMessageData($message);
77
+
78
+ $this->assertArrayHasKey('fromname', $actual_with_optional_params);
79
+ $this->assertEquals('John Doe', $actual_with_optional_params['fromname']);
80
+
81
+ $this->assertArrayHasKey('replyto', $actual_with_optional_params);
82
+ $this->assertEquals('swift@sendgrid.com', $actual_with_optional_params['replyto']);
83
+ }
84
+
85
+ public function testSendResponse()
86
+ {
87
+ $sendgrid = new SendGrid("foo", "bar");
88
+
89
+ $message = new SendGrid\Mail();
90
+
91
+ $message->
92
+ setFrom('bar@foo.com')->
93
+ setSubject('foobar subject')->
94
+ setText('foobar text')->
95
+ addTo('foo@bar.com');
96
+
97
+ $response = $sendgrid->web->send($message);
98
+
99
+ $this->assertEquals("{\"message\": \"error\", \"errors\": [\"Bad username / password\"]}", $response);
100
+ }
101
+ }
lib/sendgrid-php/Test/SendGridTest.php ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class SendGridTest extends PHPUnit_Framework_TestCase
4
+ {
5
+
6
+ public function testConstruction()
7
+ {
8
+ $sendgrid = new SendGrid("fake_username", "fake_password");
9
+
10
+ $this->assertEquals("SendGrid", get_class($sendgrid));
11
+ }
12
+
13
+ public function testInitializers()
14
+ {
15
+ $sendgrid = new SendGrid("fake_username", "fake_password");
16
+
17
+ // test the working initializers that we currently have
18
+ $smtp = $sendgrid->smtp;
19
+ $web = $sendgrid->web;
20
+
21
+ $this->assertEquals("SendGrid\Smtp", get_class($smtp));
22
+ $this->assertEquals("SendGrid\Web", get_class($web));
23
+
24
+ try
25
+ {
26
+ $sendgrid->notanapi;
27
+ }
28
+ catch (Exception $e)
29
+ {
30
+ return;
31
+ }
32
+
33
+ $this->fail('A non object was instanciated');
34
+
35
+ }
36
+ }
lib/sendgrid-php/Test/a_loaderTest.php ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ //we'll use this class to autoload the appropriate files
4
+ require_once __dir__ . "/../vendor/autoload.php";
5
+
6
+ //include any mock classes
7
+ require_once __dir__ . "/Mock/Mock_loader.php";
lib/sendgrid-php/Test/phpunit.xml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
1
+ <phpunit bootstrap="./a_loaderTest.php">
2
+ <testsuites>
3
+ <testsuite name="Services SendGrid Test Suite">
4
+ <directory>./</directory>
5
+ </testsuite>
6
+ </testsuites>
7
+ </phpunit>
lib/sendgrid-php/composer.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "sendgrid/sendgrid",
3
+ "description": "This library allows you to quickly and easily send emails through SendGrid using PHP.",
4
+ "version": "1.0.0",
5
+ "homepage": "http://sendgrid.com",
6
+ "license": "MIT",
7
+ "autoload": {
8
+ "files": ["SendGrid_loader.php"]
9
+ },
10
+ "require": {
11
+ "swiftmailer/swiftmailer": "v4.3.0"
12
+ },
13
+ "replace": {
14
+ "sendgrid/sendgrid-php": "*"
15
+ }
16
+ }
lib/sendgrid-php/composer.lock ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "hash": "59c1f81ac7d6ed6c902c71c92db62d84",
3
+ "packages": [
4
+ {
5
+ "name": "swiftmailer/swiftmailer",
6
+ "version": "v4.3.0",
7
+ "source": {
8
+ "type": "git",
9
+ "url": "git://github.com/swiftmailer/swiftmailer.git",
10
+ "reference": "v4.3.0"
11
+ },
12
+ "dist": {
13
+ "type": "zip",
14
+ "url": "https://github.com/swiftmailer/swiftmailer/archive/v4.3.0.zip",
15
+ "reference": "v4.3.0",
16
+ "shasum": ""
17
+ },
18
+ "require": {
19
+ "php": ">=5.2.4"
20
+ },
21
+ "type": "library",
22
+ "extra": {
23
+ "branch-alias": {
24
+ "dev-master": "4.3-dev"
25
+ }
26
+ },
27
+ "autoload": {
28
+ "files": [
29
+ "lib/swift_required.php"
30
+ ]
31
+ },
32
+ "notification-url": "https://packagist.org/downloads/",
33
+ "license": [
34
+ "LGPL"
35
+ ],
36
+ "authors": [
37
+ {
38
+ "name": "Fabien Potencier",
39
+ "email": "fabien@symfony.com"
40
+ },
41
+ {
42
+ "name": "Chris Corbyn"
43
+ }
44
+ ],
45
+ "description": "Swiftmailer, free feature-rich PHP mailer",
46
+ "homepage": "http://swiftmailer.org",
47
+ "keywords": [
48
+ "mail",
49
+ "mailer"
50
+ ],
51
+ "time": "2013-01-08 15:50:34"
52
+ }
53
+ ],
54
+ "packages-dev": [
55
+
56
+ ],
57
+ "aliases": [
58
+
59
+ ],
60
+ "minimum-stability": "stable",
61
+ "stability-flags": [
62
+
63
+ ],
64
+ "platform": [
65
+
66
+ ],
67
+ "platform-dev": [
68
+
69
+ ]
70
+ }
readme.txt ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ === SendGrid ===
2
+ Contributors: team-rs
3
+ Donate link: http://sendgrid.com/
4
+ Tags: email, email reliability, email templates, sendgrid, smtp, transactional email, wp_mail,email infrastructure, email marketing, marketing email, deliverability, email deliverability, email delivery, email server, mail server, email integration, cloud email
5
+ Requires at least: 3.3
6
+ Tested up to: 3.5.1
7
+ Stable tag: 1.1
8
+ License: GPLv2 or later
9
+ License URI: http://www.gnu.org/licenses/gpl-2.0.html
10
+
11
+ Email Delivery. Simplified.
12
+
13
+ == Description ==
14
+
15
+ SendGrid's cloud-based email infrastructure relieves businesses of the cost and complexity of maintaining custom email systems. SendGrid provides reliable delivery, scalability and real-time analytics along with flexible APIs that make custom integration a breeze.
16
+
17
+ The SendGrid plugin uses SMTP or API integration to send outgoing emails from your WordPress installation. It replaces the wp_mail function included with WordPress.
18
+
19
+ First, you need to have PHP-curl extension enabled. To send emails through SMTP you need to install also the 'Swift Mailer' plugin. After installing 'Swift Mailer' plugin, you must have PHP-short_open_tag setting enabled in your php.ini file.
20
+
21
+ To have the SendGrid plugin running after you have activated it, go to the plugin's settings page and set the SendGrid credentials, and how your email will be sent - either through SMTP or API.
22
+
23
+ You can also set default values for the "Name", "Sending Address" and the "Reply Address", so that you don't need to set these headers every time you want to send an email from your application.
24
+
25
+ Emails are tracked and automatically tagged for statistics within the SendGrid Dashboard. You can also add general tags to every email sent, as well as particular tags based on selected emails defined by your requirements.
26
+
27
+ There are a couple levels of integration between your WordPress installation and the SendGrid plugin:
28
+
29
+ * The simplest option is to Install it, Configure it, and the SendGrid plugin for WordPress will start sending your emails through SendGrid.
30
+ * We amended wp_mail() function so all email sends from wordpress should go through SendGrid. The wp_mail function is sending text emails as default, but you have an option of sending an email with HTML content.
31
+
32
+ How to use `wp_mail()` function:
33
+
34
+ We amended `wp_mail()` function so all email sends from wordpress should go through SendGrid.
35
+
36
+ You can send emails using the following function: `wp_mail($to, $subject, $message, $headers = '', $attachments = array())`
37
+
38
+ Where:
39
+
40
+ * `$to` - Array or comma-separated list of email addresses to send message.
41
+ * `$subject` - Email subject
42
+ * `$message` - Message contents
43
+ * `$headers` - Array or "\n" separated list of additional headers. Optional.
44
+ * `$attachments` - Array or "\n"/"," separated list of files to attach. Optional.
45
+
46
+ The wp_mail function is sending text emails as default. If you want to send an email with HTML content you have to set the content type to 'text/html' running `add_filter('wp_mail_content_type', 'set_html_content_type');` function before to `wp_mail()` one.
47
+
48
+ After wp_mail function you need to run the `remove_filter('wp_mail_content_type', 'set_html_content_type');` to remove the 'text/html' filter to avoid conflicts --http://core.trac.wordpress.org/ticket/23578
49
+
50
+ Example about how to send an HTML email using different headers:
51
+
52
+ `$subject = 'test plugin';
53
+ $message = 'testing wordpress plugin';
54
+ $to = 'address1@sendgrid.com, Address2 <address2@sendgrid.com@>, address3@sendgrid.com';
55
+ or
56
+ $to = array('address1@sendgrid.com', 'Address2 <address2@sendgrid.com>', 'address3@sendgrid.com');
57
+
58
+ $headers = array();
59
+ $headers[] = 'From: Me Myself <me@example.net>';
60
+ $headers[] = 'Cc: address4@sendgrid.com';
61
+ $headers[] = 'Bcc: address5@sendgrid.com';
62
+
63
+ $attachments = array('/tmp/img1.jpg', '/tmp/img2.jpg');
64
+
65
+ add_filter('wp_mail_content_type', 'set_html_content_type');
66
+ $mail = wp_mail($to, $subject, $message, $headers, $attachments);
67
+
68
+ remove_filter('wp_mail_content_type', 'set_html_content_type');`
69
+
70
+ == Installation ==
71
+
72
+ To upload the SendGrid Plugin .ZIP file:
73
+
74
+ 1. Upload the WordPress SendGrid Plugin to the /wp-contents/plugins/ folder.
75
+ 2. Activate the plugin from the "Plugins" menu in WordPress.
76
+ 3. Navigate to "Settings" -> "SendGrid Settings" and enter your SendGrid credentials
77
+
78
+ To auto install the SendGrid Plugin from the WordPress admin:
79
+
80
+ 1. Navigate to "Plugins" -> "Add New"
81
+ 2. Search for "SendGrid Plugin" and click "Install Now" for the "SendGrid Plugin" listing
82
+ 3. Activate the plugin from the "Plugins" menu in WordPress, or from the plugin installation screen.
83
+ 4. Navigate to "Settings" -> "SendGrid Settings" and enter your SendGrid credentials
84
+
85
+ == Frequently asked questions ==
86
+
87
+ = What credentials do I need to add on settings page =
88
+
89
+ SendGrid account credentials.
90
+
91
+ == Screenshots ==
92
+
93
+ 1. Go to Admin Panel, section Plugins and activate the SendGrid plugin. If you want to send emails through SMTP you need to install also the 'Swift Mailer' plugin.
94
+ 2. After activation "Settings" link will appear.
95
+ 3. Go to settings page and provide your SendGrid credentials. On this page you can set also the default "Name", "Sending Address" and "Reply Address".
96
+ 4. If you provide valid credentials, a form which can be used to send test emails will appear. Here you can test the plugin sending some emails.
97
+ 5. Header provided in the send test email form.
98
+ 6. If you click in the right corner from the top of the page on the "Help" button, a popup window with more information will appear.
99
+ 7. Select the time interval for which you want to see SendGrid statistics and charts.
100
+
101
+ == Changelog ==
102
+
103
+ = 1.0 =
104
+ * Fixed issue: Add error message when PHP-curl extension is not enabled.
105
+ = 1.1 =
106
+ * Added SendGrid Statistics
107
+
108
+ == Upgrade notice ==
109
+
110
+ = 1.1 =
111
+ * SendGrid Statistics can be used by selecting the time interval for which you want to see your statistics.
view/css/sendgrid.css ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ root {
2
+ display: block;
3
+ }
4
+
5
+ h2.title {
6
+ text-align: center;
7
+ }
8
+
9
+ .stuffbox {
10
+ margin: 10px;
11
+ width: 740px;
12
+ display: block;
13
+ clear: both;
14
+ }
15
+
16
+ .stuffbox .button {
17
+ margin-top: 10px;
18
+ }
19
+
20
+ .submit-button {
21
+ width: 100%;
22
+ text-align: center;
23
+ }
24
+
25
+ .stuffbox p {
26
+ padding: 7px 7px 7px 7px;
27
+ }
28
+ .stuffbox input, select, textarea {
29
+ width: 60%;
30
+ padding: 5px 0 5px 0;
31
+ }
32
+
33
+ .stuffbox h3 {
34
+ cursor: default !important;
35
+ padding: 3px 3px 3px 10px;
36
+ }
37
+
38
+ .pull-left {
39
+ float: left;
40
+ }
41
+ .pull-right {
42
+ float: right;
43
+ }
44
+
45
+ .padding5 {
46
+ padding: 10px 10px 10px 10px;
47
+ }
48
+ .clearfix:before, .clearfix:after {
49
+ content: ".";
50
+ display: block;
51
+ height: 0;
52
+ overflow: hidden;
53
+ }
54
+
55
+ .clearfix:after {
56
+ clear: both;
57
+ }
58
+
59
+ .clearfix {
60
+ zoom: 1; /* IE < 8 */
61
+ }
62
+
63
+ .send-failed, .save-error {
64
+ text-align: center;
65
+ padding: 7px 0 7px 0;
66
+ background-color: #ffebe8;
67
+ border-color: #c00;
68
+ margin: 5px 0 15px;
69
+ -webkit-border-radius: 3px;
70
+ border-radius: 3px;
71
+ border-width: 1px;
72
+ border-style: solid;
73
+ }
74
+
75
+ .send-success, .save-success {
76
+ text-align: center;
77
+ padding: 7px 0 7px 0;
78
+ background-color: #ffffe0;
79
+ border-color: #e6db55;
80
+ margin: 5px 0 15px;
81
+ -webkit-border-radius: 3px;
82
+ border-radius: 3px;
83
+ border-width: 1px;
84
+ border-style: solid;
85
+ }
86
+
87
+ .code {
88
+ background-color: #F8F8F8;
89
+ border: 1px solid #DDDDDD;
90
+ border-radius: 3px 3px 3px 3px;
91
+ font-size: 13px;
92
+ line-height: 19px;
93
+ overflow: auto;
94
+ padding: 6px 10px;
95
+ }
96
+
97
+ /* SendGrid stats */
98
+ .sendgrid-filters-container {
99
+ float:right;
100
+ margin-right:2.4%;
101
+ }
102
+
103
+ .sendgrid-filters-container .loading {
104
+ float: right;
105
+ padding-top: 2px;
106
+ margin-top: 4px;
107
+ margin-right: 4px;
108
+ }
109
+
110
+ #sendgrid-filters {
111
+ float: right;
112
+ }
113
+
114
+ #sendgrid-start-date, #sendgrid-end-date {
115
+ width: 120px;
116
+ }
117
+
118
+ .sendgrid-container {
119
+ width: 100%;
120
+ position: relative;
121
+ }
122
+
123
+ .sendgrid-container .loading {
124
+ position: absolute;
125
+ width: 48px;
126
+ height: 48px;
127
+ left: 50%;
128
+ top: 50%;
129
+ margin-left: -24px;
130
+ margin-top: -24px;
131
+ }
132
+
133
+ .sendgrid-container .sendgrid-stats {
134
+ width: 100%;
135
+ height: 300px;
136
+ }
137
+
138
+ .sendgrid-container .widget {
139
+ float:left;
140
+ width: 189px;
141
+ margin-top: 15px;
142
+ margin-left: 15px;
143
+ cursor: default;
144
+ }
145
+
146
+ .sendgrid-container .widget .widget-top {
147
+ cursor: default;
148
+ }
149
+
150
+ .sendgrid-container .widget .widget-inside {
151
+ display:block;
152
+ text-align: center;
153
+ min-height: 70px;
154
+ }
155
+
156
+ .sendgrid-container .widget .widget-inside h2 {
157
+ font-weight: bold;
158
+ }
159
+
160
+ .sendgrid-container .widget:first-of-type {
161
+ margin-left: 0px;
162
+ }
163
+
164
+ #sendgrid_statistics_widget .inside {
165
+ padding-left: 0px;
166
+ padding-right: 0px;
167
+ padding-bottom: 0px;
168
+ margin-bottom: 0px;
169
+ }
170
+
171
+ .sendgrid-container .widget.others {
172
+ font-weight: bold;
173
+ width: 30%;
174
+ margin-left: 2.3%;
175
+ margin-bottom: 15px;
176
+ }
177
+
178
+ .sendgrid-container .widget.others:last-of-type {
179
+ margin-left: 0px;
180
+ margin-right: 2.3%;
181
+ float: right;
182
+ }
183
+
184
+ .sendgrid-container .widget.others .widget-title h4 {
185
+ line-height: 15px;
186
+ }
187
+
188
+ .sendgrid-container .widget.others .row {
189
+ margin-bottom: 2px;
190
+ }
191
+
192
+ .sendgrid-container .widget.others .row:last-of-type {
193
+ margin-bottom: 0px;
194
+ }
195
+
196
+ .sendgrid-container .widget.others .row .square {
197
+ display: inline-block;
198
+ width: 10px;
199
+ height: 10px;
200
+ margin-right: 5px;
201
+ -webkit-box-shadow: 1px 1px 0 rgba(40, 40, 40, 0.3) inset;
202
+ -moz-box-shadow: 1px 1px 0 rgba(40, 40, 40, 0.3) inset;
203
+ box-shadow: 1px 1px 0 rgba(40, 40, 40, 0.3) inset;
204
+ -webkit-box-shadow: 1px 1px 0 rgba(40, 40, 40, 0.3) inset;
205
+ -moz-box-shadow: 1px 1px 0 rgba(40, 40, 40, 0.3) inset;
206
+ box-shadow: 1px 1px 0 rgba(40, 40, 40, 0.3) inset;
207
+ }
208
+
209
+ .sendgrid-container .more-statistics {
210
+ float: right;
211
+ margin-bottom: 15px;
212
+ margin-right: 2.3%;
213
+ }
214
+
215
+ .sendgrid-stats-legend {
216
+ width: 100%;
217
+ height: 22px;
218
+ margin-top: 15px;
219
+ }
220
+
221
+ .sendgrid-stats-legend .legendLabel {
222
+ padding-right: 10px;
223
+ }
224
+
225
+ .sendgrid-stats-legend .legendLabel:last-of-type {
226
+ padding-right: 0px;
227
+ }
228
+
229
+ #sendgrid-statistics-page .postbox .hndle {
230
+ cursor: default;
231
+ }
232
+
233
+ #icon-sendgrid {
234
+ background: url("../images/logo32.png") no-repeat;
235
+ }
236
+
237
+ #ui-datepicker-div {
238
+ display: none;
239
+ }
view/css/smoothness/images/animated-overlay.gif ADDED
Binary file
view/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png ADDED
Binary file
view/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png ADDED
Binary file
view/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png ADDED
Binary file
view/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png ADDED
Binary file
view/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png ADDED
Binary file
view/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png ADDED
Binary file
view/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png ADDED
Binary file
view/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png ADDED
Binary file
view/css/smoothness/images/ui-icons_222222_256x240.png ADDED
Binary file
view/css/smoothness/images/ui-icons_2e83ff_256x240.png ADDED
Binary file
view/css/smoothness/images/ui-icons_454545_256x240.png ADDED
Binary file
view/css/smoothness/images/ui-icons_888888_256x240.png ADDED
Binary file
view/css/smoothness/images/ui-icons_cd0a0a_256x240.png ADDED
Binary file
view/css/smoothness/jquery-ui-1.10.3.custom.css ADDED
@@ -0,0 +1,677 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*! jQuery UI - v1.10.3 - 2013-08-27
2
+ * http://jqueryui.com
3
+ * Includes: jquery.ui.core.css, jquery.ui.datepicker.css
4
+ * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
5
+ * Copyright 2013 jQuery Foundation and other contributors Licensed MIT */
6
+
7
+ /* Layout helpers
8
+ ----------------------------------*/
9
+ .ui-helper-hidden {
10
+ display: none;
11
+ }
12
+ .ui-helper-hidden-accessible {
13
+ border: 0;
14
+ clip: rect(0 0 0 0);
15
+ height: 1px;
16
+ margin: -1px;
17
+ overflow: hidden;
18
+ padding: 0;
19
+ position: absolute;
20
+ width: 1px;
21
+ }
22
+ .ui-helper-reset {
23
+ margin: 0;
24
+ padding: 0;
25
+ border: 0;
26
+ outline: 0;
27
+ line-height: 1.3;
28
+ text-decoration: none;
29
+ font-size: 100%;
30
+ list-style: none;
31
+ }
32
+ .ui-helper-clearfix:before,
33
+ .ui-helper-clearfix:after {
34
+ content: "";
35
+ display: table;
36
+ border-collapse: collapse;
37
+ }
38
+ .ui-helper-clearfix:after {
39
+ clear: both;
40
+ }
41
+ .ui-helper-clearfix {
42
+ min-height: 0; /* support: IE7 */
43
+ }
44
+ .ui-helper-zfix {
45
+ width: 100%;
46
+ height: 100%;
47
+ top: 0;
48
+ left: 0;
49
+ position: absolute;
50
+ opacity: 0;
51
+ filter:Alpha(Opacity=0);
52
+ }
53
+
54
+ .ui-front {
55
+ z-index: 100;
56
+ }
57
+
58
+
59
+ /* Interaction Cues
60
+ ----------------------------------*/
61
+ .ui-state-disabled {
62
+ cursor: default !important;
63
+ }
64
+
65
+
66
+ /* Icons
67
+ ----------------------------------*/
68
+
69
+ /* states and images */
70
+ .ui-icon {
71
+ display: block;
72
+ text-indent: -99999px;
73
+ overflow: hidden;
74
+ background-repeat: no-repeat;
75
+ }
76
+
77
+
78
+ /* Misc visuals
79
+ ----------------------------------*/
80
+
81
+ /* Overlays */
82
+ .ui-widget-overlay {
83
+ position: fixed;
84
+ top: 0;
85
+ left: 0;
86
+ width: 100%;
87
+ height: 100%;
88
+ }
89
+ .ui-datepicker {
90
+ width: 17em;
91
+ padding: .2em .2em 0;
92
+ display: none;
93
+ }
94
+ .ui-datepicker .ui-datepicker-header {
95
+ position: relative;
96
+ padding: .2em 0;
97
+ }
98
+ .ui-datepicker .ui-datepicker-prev,
99
+ .ui-datepicker .ui-datepicker-next {
100
+ position: absolute;
101
+ top: 2px;
102
+ width: 1.8em;
103
+ height: 1.8em;
104
+ margin-top: 3px;
105
+ }
106
+ .ui-datepicker .ui-datepicker-next {
107
+ margin-right: 3px;
108
+ }
109
+ .ui-datepicker .ui-datepicker-prev {
110
+ margin-left: 3px;
111
+ }
112
+ .ui-datepicker .ui-datepicker-prev-hover,
113
+ .ui-datepicker .ui-datepicker-next-hover {
114
+ top: 1px;
115
+ }
116
+ .ui-datepicker .ui-datepicker-prev {
117
+ left: 2px;
118
+ }
119
+ .ui-datepicker .ui-datepicker-next {
120
+ right: 2px;
121
+ }
122
+ .ui-datepicker .ui-datepicker-prev-hover {
123
+ left: 1px;
124
+ }
125
+ .ui-datepicker .ui-datepicker-next-hover {
126
+ right: 1px;
127
+ }
128
+ .ui-datepicker .ui-datepicker-prev span,
129
+ .ui-datepicker .ui-datepicker-next span {
130
+ display: block;
131
+ position: absolute;
132
+ left: 50%;
133
+ margin-left: -8px;
134
+ top: 50%;
135
+ margin-top: -8px;
136
+ }
137
+ .ui-datepicker .ui-datepicker-title {
138
+ margin: 0 2.3em;
139
+ line-height: 1.8em;
140
+ text-align: center;
141
+ }
142
+ .ui-datepicker .ui-datepicker-title select {
143
+ font-size: 1em;
144
+ margin: 1px 0;
145
+ }
146
+ .ui-datepicker select.ui-datepicker-month-year {
147
+ width: 100%;
148
+ }
149
+ .ui-datepicker select.ui-datepicker-month,
150
+ .ui-datepicker select.ui-datepicker-year {
151
+ width: 49%;
152
+ }
153
+ .ui-datepicker table {
154
+ width: 100%;
155
+ font-size: .9em;
156
+ border-collapse: collapse;
157
+ margin: 0 0 .4em;
158
+ }
159
+ .ui-datepicker th {
160
+ padding: .7em .3em;
161
+ text-align: center;
162
+ font-weight: normal;
163
+ border: 0;
164
+ }
165
+ .ui-datepicker td {
166
+ border: 0;
167
+ padding: 1px;
168
+ }
169
+ .ui-datepicker td span,
170
+ .ui-datepicker td a {
171
+ display: block;
172
+ padding: .2em;
173
+ text-align: right;
174
+ text-decoration: none;
175
+ }
176
+ .ui-datepicker .ui-datepicker-buttonpane {
177
+ background-image: none;
178
+ margin: .7em 0 0 0;
179
+ padding: 0 .2em;
180
+ border-left: 0;
181
+ border-right: 0;
182
+ border-bottom: 0;
183
+ }
184
+ .ui-datepicker .ui-datepicker-buttonpane button {
185
+ float: right;
186
+ margin: .5em .2em .4em;
187
+ cursor: pointer;
188
+ padding: .2em .6em .3em .6em;
189
+ width: auto;
190
+ overflow: visible;
191
+ }
192
+ .ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current {
193
+ float: left;
194
+ }
195
+
196
+ /* with multiple calendars */
197
+ .ui-datepicker.ui-datepicker-multi {
198
+ width: auto;
199
+ }
200
+ .ui-datepicker-multi .ui-datepicker-group {
201
+ float: left;
202
+ }
203
+ .ui-datepicker-multi .ui-datepicker-group table {
204
+ width: 95%;
205
+ margin: 0 auto .4em;
206
+ }
207
+ .ui-datepicker-multi-2 .ui-datepicker-group {
208
+ width: 50%;
209
+ }
210
+ .ui-datepicker-multi-3 .ui-datepicker-group {
211
+ width: 33.3%;
212
+ }
213
+ .ui-datepicker-multi-4 .ui-datepicker-group {
214
+ width: 25%;
215
+ }
216
+ .ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,
217
+ .ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header {
218
+ border-left-width: 0;
219
+ }
220
+ .ui-datepicker-multi .ui-datepicker-buttonpane {
221
+ clear: left;
222
+ }
223
+ .ui-datepicker-row-break {
224
+ clear: both;
225
+ width: 100%;
226
+ font-size: 0;
227
+ }
228
+
229
+ /* RTL support */
230
+ .ui-datepicker-rtl {
231
+ direction: rtl;
232
+ }
233
+ .ui-datepicker-rtl .ui-datepicker-prev {
234
+ right: 2px;
235
+ left: auto;
236
+ }
237
+ .ui-datepicker-rtl .ui-datepicker-next {
238
+ left: 2px;
239
+ right: auto;
240
+ }
241
+ .ui-datepicker-rtl .ui-datepicker-prev:hover {
242
+ right: 1px;
243
+ left: auto;
244
+ }
245
+ .ui-datepicker-rtl .ui-datepicker-next:hover {
246
+ left: 1px;
247
+ right: auto;
248
+ }
249
+ .ui-datepicker-rtl .ui-datepicker-buttonpane {
250
+ clear: right;
251
+ }
252
+ .ui-datepicker-rtl .ui-datepicker-buttonpane button {
253
+ float: left;
254
+ }
255
+ .ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,
256
+ .ui-datepicker-rtl .ui-datepicker-group {
257
+ float: right;
258
+ }
259
+ .ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,
260
+ .ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header {
261
+ border-right-width: 0;
262
+ border-left-width: 1px;
263
+ }
264
+
265
+ /* Component containers
266
+ ----------------------------------*/
267
+ .ui-widget {
268
+ font-family: Verdana,Arial,sans-serif;
269
+ font-size: 1.1em;
270
+ }
271
+ .ui-widget .ui-widget {
272
+ font-size: 1em;
273
+ }
274
+ .ui-widget input,
275
+ .ui-widget select,
276
+ .ui-widget textarea,
277
+ .ui-widget button {
278
+ font-family: Verdana,Arial,sans-serif;
279
+ font-size: 1em;
280
+ }
281
+ .ui-widget-content {
282
+ border: 1px solid #dfdfdf;
283
+ -webkit-box-shadow: inset 0 1px 0 #fff;
284
+ box-shadow: inset 0 1px 0 #fff;
285
+ background: #ffffff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x;
286
+ color: #222222;
287
+ background: #ffffff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x;
288
+ color: #222222;
289
+ }
290
+ .ui-widget-content a {
291
+ color: #222222;
292
+ }
293
+ .ui-widget-header {
294
+ background: #f1f1f1;
295
+ background-image: -webkit-gradient(linear,left bottom,left top,from(#ececec),to(#f9f9f9));
296
+ background-image: -webkit-linear-gradient(bottom,#ececec,#f9f9f9);
297
+ background-image: -moz-linear-gradient(bottom,#ececec,#f9f9f9);
298
+ background-image: -o-linear-gradient(bottom,#ececec,#f9f9f9);
299
+ background-image: linear-gradient(to top,#ececec,#f9f9f9);
300
+ border:1px solid #dfdfdf;
301
+ -webkit-box-shadow: inset 0 1px 0 #fff;
302
+ box-shadow: inset 0 1px 0 #fff;
303
+ -webkit-border-radius: 3px;
304
+ border-radius: 3px;
305
+ color: #464646;
306
+ font-weight: normal;
307
+ }
308
+ .ui-widget-header a {
309
+ color: #222222;
310
+ }
311
+
312
+ /* Interaction states
313
+ ----------------------------------*/
314
+ .ui-state-default,
315
+ .ui-widget-content .ui-state-default,
316
+ .ui-widget-header .ui-state-default {
317
+ border: 1px solid #d3d3d3;
318
+ background: #e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x;
319
+ font-weight: normal;
320
+ color: #555555;
321
+
322
+ background: #f1f1f1;
323
+ background-image: -webkit-gradient(linear,left bottom,left top,from(#ececec),to(#f9f9f9));
324
+ background-image: -webkit-linear-gradient(bottom,#ececec,#f9f9f9);
325
+ background-image: -moz-linear-gradient(bottom,#ececec,#f9f9f9);
326
+ background-image: -o-linear-gradient(bottom,#ececec,#f9f9f9);
327
+ background-image: linear-gradient(to top,#ececec,#f9f9f9);
328
+ border-color: #dfdfdf;
329
+ -webkit-box-shadow: inset 0 1px 0 #fff;
330
+ box-shadow: inset 0 1px 0 #fff;
331
+ color: #464646;
332
+ }
333
+ .ui-state-default a,
334
+ .ui-state-default a:link,
335
+ .ui-state-default a:visited {
336
+ color: #555555;
337
+ text-decoration: none;
338
+ }
339
+ .ui-state-hover,
340
+ .ui-widget-content .ui-state-hover,
341
+ .ui-widget-header .ui-state-hover,
342
+ .ui-state-focus,
343
+ .ui-widget-content .ui-state-focus,
344
+ .ui-widget-header .ui-state-focus {
345
+ border: 1px solid #999999;
346
+ }
347
+ .ui-state-hover a,
348
+ .ui-state-hover a:hover,
349
+ .ui-state-hover a:link,
350
+ .ui-state-hover a:visited {
351
+ color: #212121;
352
+ text-decoration: none;
353
+ }
354
+ .ui-state-active,
355
+ .ui-widget-content .ui-state-active,
356
+ .ui-widget-header .ui-state-active {
357
+ border: 1px solid #aaaaaa;
358
+ background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x;
359
+ font-weight: normal;
360
+ color: #212121;
361
+ }
362
+ .ui-state-active a,
363
+ .ui-state-active a:link,
364
+ .ui-state-active a:visited {
365
+ color: #212121;
366
+ text-decoration: none;
367
+ }
368
+
369
+ /* Interaction Cues
370
+ ----------------------------------*/
371
+ .ui-state-highlight,
372
+ .ui-widget-content .ui-state-highlight,
373
+ .ui-widget-header .ui-state-highlight {
374
+ border: 1px solid #fcefa1;
375
+ background: #fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x;
376
+ color: #363636;
377
+ }
378
+ .ui-state-highlight a,
379
+ .ui-widget-content .ui-state-highlight a,
380
+ .ui-widget-header .ui-state-highlight a {
381
+ color: #363636;
382
+ }
383
+ .ui-state-error,
384
+ .ui-widget-content .ui-state-error,
385
+ .ui-widget-header .ui-state-error {
386
+ border: 1px solid #cd0a0a;
387
+ background: #fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x;
388
+ color: #cd0a0a;
389
+ }
390
+ .ui-state-error a,
391
+ .ui-widget-content .ui-state-error a,
392
+ .ui-widget-header .ui-state-error a {
393
+ color: #cd0a0a;
394
+ }
395
+ .ui-state-error-text,
396
+ .ui-widget-content .ui-state-error-text,
397
+ .ui-widget-header .ui-state-error-text {
398
+ color: #cd0a0a;
399
+ }
400
+ .ui-priority-primary,
401
+ .ui-widget-content .ui-priority-primary,
402
+ .ui-widget-header .ui-priority-primary {
403
+ font-weight: normal;
404
+ }
405
+ .ui-priority-secondary,
406
+ .ui-widget-content .ui-priority-secondary,
407
+ .ui-widget-header .ui-priority-secondary {
408
+ opacity: .7;
409
+ filter:Alpha(Opacity=70);
410
+ font-weight: normal;
411
+ }
412
+ .ui-state-disabled,
413
+ .ui-widget-content .ui-state-disabled,
414
+ .ui-widget-header .ui-state-disabled {
415
+ opacity: .35;
416
+ filter:Alpha(Opacity=35);
417
+ background-image: none;
418
+ }
419
+ .ui-state-disabled .ui-icon {
420
+ filter:Alpha(Opacity=35); /* For IE8 - See #6059 */
421
+ }
422
+
423
+ /* Icons
424
+ ----------------------------------*/
425
+
426
+ /* states and images */
427
+ .ui-icon {
428
+ width: 16px;
429
+ height: 16px;
430
+ }
431
+ .ui-icon,
432
+ .ui-widget-content .ui-icon {
433
+ background-image: url(images/ui-icons_222222_256x240.png);
434
+ }
435
+ .ui-widget-header .ui-icon {
436
+ background-image: url(images/ui-icons_222222_256x240.png);
437
+ }
438
+ .ui-state-default .ui-icon {
439
+ background-image: url(images/ui-icons_888888_256x240.png);
440
+ }
441
+ .ui-state-hover .ui-icon,
442
+ .ui-state-focus .ui-icon {
443
+ background-image: url(images/ui-icons_454545_256x240.png);
444
+ }
445
+ .ui-state-active .ui-icon {
446
+ background-image: url(images/ui-icons_454545_256x240.png);
447
+ }
448
+ .ui-state-highlight .ui-icon {
449
+ background-image: url(images/ui-icons_2e83ff_256x240.png);
450
+ }
451
+ .ui-state-error .ui-icon,
452
+ .ui-state-error-text .ui-icon {
453
+ background-image: url(images/ui-icons_cd0a0a_256x240.png);
454
+ }
455
+
456
+ /* positioning */
457
+ .ui-icon-blank { background-position: 16px 16px; }
458
+ .ui-icon-carat-1-n { background-position: 0 0; }
459
+ .ui-icon-carat-1-ne { background-position: -16px 0; }
460
+ .ui-icon-carat-1-e { background-position: -32px 0; }
461
+ .ui-icon-carat-1-se { background-position: -48px 0; }
462
+ .ui-icon-carat-1-s { background-position: -64px 0; }
463
+ .ui-icon-carat-1-sw { background-position: -80px 0; }
464
+ .ui-icon-carat-1-w { background-position: -96px 0; }
465
+ .ui-icon-carat-1-nw { background-position: -112px 0; }
466
+ .ui-icon-carat-2-n-s { background-position: -128px 0; }
467
+ .ui-icon-carat-2-e-w { background-position: -144px 0; }
468
+ .ui-icon-triangle-1-n { background-position: 0 -16px; }
469
+ .ui-icon-triangle-1-ne { background-position: -16px -16px; }
470
+ .ui-icon-triangle-1-e { background-position: -32px -16px; }
471
+ .ui-icon-triangle-1-se { background-position: -48px -16px; }
472
+ .ui-icon-triangle-1-s { background-position: -64px -16px; }
473
+ .ui-icon-triangle-1-sw { background-position: -80px -16px; }
474
+ .ui-icon-triangle-1-w { background-position: -96px -16px; }
475
+ .ui-icon-triangle-1-nw { background-position: -112px -16px; }
476
+ .ui-icon-triangle-2-n-s { background-position: -128px -16px; }
477
+ .ui-icon-triangle-2-e-w { background-position: -144px -16px; }
478
+ .ui-icon-arrow-1-n { background-position: 0 -32px; }
479
+ .ui-icon-arrow-1-ne { background-position: -16px -32px; }
480
+ .ui-icon-arrow-1-e { background-position: -32px -32px; }
481
+ .ui-icon-arrow-1-se { background-position: -48px -32px; }
482
+ .ui-icon-arrow-1-s { background-position: -64px -32px; }
483
+ .ui-icon-arrow-1-sw { background-position: -80px -32px; }
484
+ .ui-icon-arrow-1-w { background-position: -96px -32px; }
485
+ .ui-icon-arrow-1-nw { background-position: -112px -32px; }
486
+ .ui-icon-arrow-2-n-s { background-position: -128px -32px; }
487
+ .ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
488
+ .ui-icon-arrow-2-e-w { background-position: -160px -32px; }
489
+ .ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
490
+ .ui-icon-arrowstop-1-n { background-position: -192px -32px; }
491
+ .ui-icon-arrowstop-1-e { background-position: -208px -32px; }
492
+ .ui-icon-arrowstop-1-s { background-position: -224px -32px; }
493
+ .ui-icon-arrowstop-1-w { background-position: -240px -32px; }
494
+ .ui-icon-arrowthick-1-n { background-position: 0 -48px; }
495
+ .ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
496
+ .ui-icon-arrowthick-1-e { background-position: -32px -48px; }
497
+ .ui-icon-arrowthick-1-se { background-position: -48px -48px; }
498
+ .ui-icon-arrowthick-1-s { background-position: -64px -48px; }
499
+ .ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
500
+ .ui-icon-arrowthick-1-w { background-position: -96px -48px; }
501
+ .ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
502
+ .ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
503
+ .ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
504
+ .ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
505
+ .ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
506
+ .ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
507
+ .ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
508
+ .ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
509
+ .ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
510
+ .ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
511
+ .ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
512
+ .ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
513
+ .ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
514
+ .ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
515
+ .ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
516
+ .ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
517
+ .ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
518
+ .ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
519
+ .ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
520
+ .ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
521
+ .ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
522
+ .ui-icon-arrow-4 { background-position: 0 -80px; }
523
+ .ui-icon-arrow-4-diag { background-position: -16px -80px; }
524
+ .ui-icon-extlink { background-position: -32px -80px; }
525
+ .ui-icon-newwin { background-position: -48px -80px; }
526
+ .ui-icon-refresh { background-position: -64px -80px; }
527
+ .ui-icon-shuffle { background-position: -80px -80px; }
528
+ .ui-icon-transfer-e-w { background-position: -96px -80px; }
529
+ .ui-icon-transferthick-e-w { background-position: -112px -80px; }
530
+ .ui-icon-folder-collapsed { background-position: 0 -96px; }
531
+ .ui-icon-folder-open { background-position: -16px -96px; }
532
+ .ui-icon-document { background-position: -32px -96px; }
533
+ .ui-icon-document-b { background-position: -48px -96px; }
534
+ .ui-icon-note { background-position: -64px -96px; }
535
+ .ui-icon-mail-closed { background-position: -80px -96px; }
536
+ .ui-icon-mail-open { background-position: -96px -96px; }
537
+ .ui-icon-suitcase { background-position: -112px -96px; }
538
+ .ui-icon-comment { background-position: -128px -96px; }
539
+ .ui-icon-person { background-position: -144px -96px; }
540
+ .ui-icon-print { background-position: -160px -96px; }
541
+ .ui-icon-trash { background-position: -176px -96px; }
542
+ .ui-icon-locked { background-position: -192px -96px; }
543
+ .ui-icon-unlocked { background-position: -208px -96px; }
544
+ .ui-icon-bookmark { background-position: -224px -96px; }
545
+ .ui-icon-tag { background-position: -240px -96px; }
546
+ .ui-icon-home { background-position: 0 -112px; }
547
+ .ui-icon-flag { background-position: -16px -112px; }
548
+ .ui-icon-calendar { background-position: -32px -112px; }
549
+ .ui-icon-cart { background-position: -48px -112px; }
550
+ .ui-icon-pencil { background-position: -64px -112px; }
551
+ .ui-icon-clock { background-position: -80px -112px; }
552
+ .ui-icon-disk { background-position: -96px -112px; }
553
+ .ui-icon-calculator { background-position: -112px -112px; }
554
+ .ui-icon-zoomin { background-position: -128px -112px; }
555
+ .ui-icon-zoomout { background-position: -144px -112px; }
556
+ .ui-icon-search { background-position: -160px -112px; }
557
+ .ui-icon-wrench { background-position: -176px -112px; }
558
+ .ui-icon-gear { background-position: -192px -112px; }
559
+ .ui-icon-heart { background-position: -208px -112px; }
560
+ .ui-icon-star { background-position: -224px -112px; }
561
+ .ui-icon-link { background-position: -240px -112px; }
562
+ .ui-icon-cancel { background-position: 0 -128px; }
563
+ .ui-icon-plus { background-position: -16px -128px; }
564
+ .ui-icon-plusthick { background-position: -32px -128px; }
565
+ .ui-icon-minus { background-position: -48px -128px; }
566
+ .ui-icon-minusthick { background-position: -64px -128px; }
567
+ .ui-icon-close { background-position: -80px -128px; }
568
+ .ui-icon-closethick { background-position: -96px -128px; }
569
+ .ui-icon-key { background-position: -112px -128px; }
570
+ .ui-icon-lightbulb { background-position: -128px -128px; }
571
+ .ui-icon-scissors { background-position: -144px -128px; }
572
+ .ui-icon-clipboard { background-position: -160px -128px; }
573
+ .ui-icon-copy { background-position: -176px -128px; }
574
+ .ui-icon-contact { background-position: -192px -128px; }
575
+ .ui-icon-image { background-position: -208px -128px; }
576
+ .ui-icon-video { background-position: -224px -128px; }
577
+ .ui-icon-script { background-position: -240px -128px; }
578
+ .ui-icon-alert { background-position: 0 -144px; }
579
+ .ui-icon-info { background-position: -16px -144px; }
580
+ .ui-icon-notice { background-position: -32px -144px; }
581
+ .ui-icon-help { background-position: -48px -144px; }
582
+ .ui-icon-check { background-position: -64px -144px; }
583
+ .ui-icon-bullet { background-position: -80px -144px; }
584
+ .ui-icon-radio-on { background-position: -96px -144px; }
585
+ .ui-icon-radio-off { background-position: -112px -144px; }
586
+ .ui-icon-pin-w { background-position: -128px -144px; }
587
+ .ui-icon-pin-s { background-position: -144px -144px; }
588
+ .ui-icon-play { background-position: 0 -160px; }
589
+ .ui-icon-pause { background-position: -16px -160px; }
590
+ .ui-icon-seek-next { background-position: -32px -160px; }
591
+ .ui-icon-seek-prev { background-position: -48px -160px; }
592
+ .ui-icon-seek-end { background-position: -64px -160px; }
593
+ .ui-icon-seek-start { background-position: -80px -160px; }
594
+ /* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
595
+ .ui-icon-seek-first { background-position: -80px -160px; }
596
+ .ui-icon-stop { background-position: -96px -160px; }
597
+ .ui-icon-eject { background-position: -112px -160px; }
598
+ .ui-icon-volume-off { background-position: -128px -160px; }
599
+ .ui-icon-volume-on { background-position: -144px -160px; }
600
+ .ui-icon-power { background-position: 0 -176px; }
601
+ .ui-icon-signal-diag { background-position: -16px -176px; }
602
+ .ui-icon-signal { background-position: -32px -176px; }
603
+ .ui-icon-battery-0 { background-position: -48px -176px; }
604
+ .ui-icon-battery-1 { background-position: -64px -176px; }
605
+ .ui-icon-battery-2 { background-position: -80px -176px; }
606
+ .ui-icon-battery-3 { background-position: -96px -176px; }
607
+ .ui-icon-circle-plus { background-position: 0 -192px; }
608
+ .ui-icon-circle-minus { background-position: -16px -192px; }
609
+ .ui-icon-circle-close { background-position: -32px -192px; }
610
+ .ui-icon-circle-triangle-e { background-position: -48px -192px; }
611
+ .ui-icon-circle-triangle-s { background-position: -64px -192px; }
612
+ .ui-icon-circle-triangle-w { background-position: -80px -192px; }
613
+ .ui-icon-circle-triangle-n { background-position: -96px -192px; }
614
+ .ui-icon-circle-arrow-e { background-position: -112px -192px; }
615
+ .ui-icon-circle-arrow-s { background-position: -128px -192px; }
616
+ .ui-icon-circle-arrow-w { background-position: -144px -192px; }
617
+ .ui-icon-circle-arrow-n { background-position: -160px -192px; }
618
+ .ui-icon-circle-zoomin { background-position: -176px -192px; }
619
+ .ui-icon-circle-zoomout { background-position: -192px -192px; }
620
+ .ui-icon-circle-check { background-position: -208px -192px; }
621
+ .ui-icon-circlesmall-plus { background-position: 0 -208px; }
622
+ .ui-icon-circlesmall-minus { background-position: -16px -208px; }
623
+ .ui-icon-circlesmall-close { background-position: -32px -208px; }
624
+ .ui-icon-squaresmall-plus { background-position: -48px -208px; }
625
+ .ui-icon-squaresmall-minus { background-position: -64px -208px; }
626
+ .ui-icon-squaresmall-close { background-position: -80px -208px; }
627
+ .ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
628
+ .ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
629
+ .ui-icon-grip-solid-vertical { background-position: -32px -224px; }
630
+ .ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
631
+ .ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
632
+ .ui-icon-grip-diagonal-se { background-position: -80px -224px; }
633
+
634
+
635
+ /* Misc visuals
636
+ ----------------------------------*/
637
+
638
+ /* Corner radius */
639
+ .ui-corner-all,
640
+ .ui-corner-top,
641
+ .ui-corner-left,
642
+ .ui-corner-tl {
643
+ border-top-left-radius: 4px;
644
+ }
645
+ .ui-corner-all,
646
+ .ui-corner-top,
647
+ .ui-corner-right,
648
+ .ui-corner-tr {
649
+ border-top-right-radius: 4px;
650
+ }
651
+ .ui-corner-all,
652
+ .ui-corner-bottom,
653
+ .ui-corner-left,
654
+ .ui-corner-bl {
655
+ border-bottom-left-radius: 4px;
656
+ }
657
+ .ui-corner-all,
658
+ .ui-corner-bottom,
659
+ .ui-corner-right,
660
+ .ui-corner-br {
661
+ border-bottom-right-radius: 4px;
662
+ }
663
+
664
+ /* Overlays */
665
+ .ui-widget-overlay {
666
+ background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;
667
+ opacity: .3;
668
+ filter: Alpha(Opacity=30);
669
+ }
670
+ .ui-widget-shadow {
671
+ margin: -8px 0 0 -8px;
672
+ padding: 8px;
673
+ background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;
674
+ opacity: .3;
675
+ filter: Alpha(Opacity=30);
676
+ border-radius: 8px;
677
+ }
view/images/loader.gif ADDED
Binary file
view/images/logo.png ADDED
Binary file
view/images/logo32.png ADDED
Binary file
view/js/jquery.flot.js ADDED
@@ -0,0 +1,2696 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Javascript plotting library for jQuery, version 0.8 alpha.
2
+
3
+ Copyright (c) 2007-2012 IOLA and Ole Laursen.
4
+ Licensed under the MIT license.
5
+
6
+ */
7
+
8
+ // first an inline dependency, jquery.colorhelpers.js, we inline it here
9
+ // for convenience
10
+
11
+ /* Plugin for jQuery for working with colors.
12
+ *
13
+ * Version 1.1.
14
+ *
15
+ * Inspiration from jQuery color animation plugin by John Resig.
16
+ *
17
+ * Released under the MIT license by Ole Laursen, October 2009.
18
+ *
19
+ * Examples:
20
+ *
21
+ * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
22
+ * var c = $.color.extract($("#mydiv"), 'background-color');
23
+ * console.log(c.r, c.g, c.b, c.a);
24
+ * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
25
+ *
26
+ * Note that .scale() and .add() return the same modified object
27
+ * instead of making a new one.
28
+ *
29
+ * V. 1.1: Fix error handling so e.g. parsing an empty string does
30
+ * produce a color rather than just crashing.
31
+ */
32
+ (function(B){B.color={};B.color.make=function(F,E,C,D){var G={};G.r=F||0;G.g=E||0;G.b=C||0;G.a=D!=null?D:1;G.add=function(J,I){for(var H=0;H<J.length;++H){G[J.charAt(H)]+=I}return G.normalize()};G.scale=function(J,I){for(var H=0;H<J.length;++H){G[J.charAt(H)]*=I}return G.normalize()};G.toString=function(){if(G.a>=1){return"rgb("+[G.r,G.g,G.b].join(",")+")"}else{return"rgba("+[G.r,G.g,G.b,G.a].join(",")+")"}};G.normalize=function(){function H(J,K,I){return K<J?J:(K>I?I:K)}G.r=H(0,parseInt(G.r),255);G.g=H(0,parseInt(G.g),255);G.b=H(0,parseInt(G.b),255);G.a=H(0,G.a,1);return G};G.clone=function(){return B.color.make(G.r,G.b,G.g,G.a)};return G.normalize()};B.color.extract=function(D,C){var E;do{E=D.css(C).toLowerCase();if(E!=""&&E!="transparent"){break}D=D.parent()}while(!B.nodeName(D.get(0),"body"));if(E=="rgba(0, 0, 0, 0)"){E="transparent"}return B.color.parse(E)};B.color.parse=function(F){var E,C=B.color.make;if(E=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10))}if(E=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10),parseFloat(E[4]))}if(E=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55)}if(E=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55,parseFloat(E[4]))}if(E=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(F)){return C(parseInt(E[1],16),parseInt(E[2],16),parseInt(E[3],16))}if(E=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(F)){return C(parseInt(E[1]+E[1],16),parseInt(E[2]+E[2],16),parseInt(E[3]+E[3],16))}var D=B.trim(F).toLowerCase();if(D=="transparent"){return C(255,255,255,0)}else{E=A[D]||[0,0,0];return C(E[0],E[1],E[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);
33
+
34
+ // the actual Flot code
35
+ (function($) {
36
+ function Plot(placeholder, data_, options_, plugins) {
37
+ // data is on the form:
38
+ // [ series1, series2 ... ]
39
+ // where series is either just the data as [ [x1, y1], [x2, y2], ... ]
40
+ // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... }
41
+
42
+ var series = [],
43
+ options = {
44
+ // the color theme used for graphs
45
+ colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"],
46
+ legend: {
47
+ show: true,
48
+ noColumns: 1, // number of colums in legend table
49
+ labelFormatter: null, // fn: string -> string
50
+ labelBoxBorderColor: "#ccc", // border color for the little label boxes
51
+ container: null, // container (as jQuery object) to put legend in, null means default on top of graph
52
+ position: "ne", // position of default legend container within plot
53
+ margin: 5, // distance from grid edge to default legend container within plot
54
+ backgroundColor: null, // null means auto-detect
55
+ backgroundOpacity: 0.85, // set to 0 to avoid background
56
+ sorted: null // default to no legend sorting
57
+ },
58
+ xaxis: {
59
+ show: null, // null = auto-detect, true = always, false = never
60
+ position: "bottom", // or "top"
61
+ mode: null, // null or "time"
62
+ timezone: null, // "browser" for local to the client or timezone for timezone-js
63
+ font: null, // null (derived from CSS in placeholder) or object like { size: 11, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" }
64
+ color: null, // base color, labels, ticks
65
+ tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)"
66
+ transform: null, // null or f: number -> number to transform axis
67
+ inverseTransform: null, // if transform is set, this should be the inverse function
68
+ min: null, // min. value to show, null means set automatically
69
+ max: null, // max. value to show, null means set automatically
70
+ autoscaleMargin: null, // margin in % to add if auto-setting min/max
71
+ ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks
72
+ tickFormatter: null, // fn: number -> string
73
+ labelWidth: null, // size of tick labels in pixels
74
+ labelHeight: null,
75
+ reserveSpace: null, // whether to reserve space even if axis isn't shown
76
+ tickLength: null, // size in pixels of ticks, or "full" for whole line
77
+ alignTicksWithAxis: null, // axis number or null for no sync
78
+
79
+ // mode specific options
80
+ tickDecimals: null, // no. of decimals, null means auto
81
+ tickSize: null, // number or [number, "unit"]
82
+ minTickSize: null, // number or [number, "unit"]
83
+ monthNames: null, // list of names of months
84
+ timeformat: null, // format string to use
85
+ twelveHourClock: false // 12 or 24 time in time mode
86
+ },
87
+ yaxis: {
88
+ autoscaleMargin: 0.02,
89
+ position: "left" // or "right"
90
+ },
91
+ xaxes: [],
92
+ yaxes: [],
93
+ series: {
94
+ points: {
95
+ show: false,
96
+ radius: 3,
97
+ lineWidth: 2, // in pixels
98
+ fill: true,
99
+ fillColor: "#ffffff",
100
+ symbol: "circle" // or callback
101
+ },
102
+ lines: {
103
+ // we don't put in show: false so we can see
104
+ // whether lines were actively disabled
105
+ lineWidth: 2, // in pixels
106
+ fill: false,
107
+ fillColor: null,
108
+ steps: false
109
+ // Omit 'zero', so we can later default its value to
110
+ // match that of the 'fill' option.
111
+ },
112
+ bars: {
113
+ show: false,
114
+ lineWidth: 2, // in pixels
115
+ barWidth: 1, // in units of the x axis
116
+ fill: true,
117
+ fillColor: null,
118
+ align: "left", // "left", "right", or "center"
119
+ horizontal: false,
120
+ zero: true
121
+ },
122
+ shadowSize: 3,
123
+ highlightColor: null
124
+ },
125
+ grid: {
126
+ show: true,
127
+ aboveData: false,
128
+ color: "#545454", // primary color used for outline and labels
129
+ backgroundColor: null, // null for transparent, else color
130
+ borderColor: null, // set if different from the grid color
131
+ tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)"
132
+ margin: 0, // distance from the canvas edge to the grid
133
+ labelMargin: 5, // in pixels
134
+ axisMargin: 8, // in pixels
135
+ borderWidth: 2, // in pixels
136
+ minBorderMargin: null, // in pixels, null means taken from points radius
137
+ markings: null, // array of ranges or fn: axes -> array of ranges
138
+ markingsColor: "#f4f4f4",
139
+ markingsLineWidth: 2,
140
+ // interactive stuff
141
+ clickable: false,
142
+ hoverable: false,
143
+ autoHighlight: true, // highlight in case mouse is near
144
+ mouseActiveRadius: 10 // how far the mouse can be away to activate an item
145
+ },
146
+ interaction: {
147
+ redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow
148
+ },
149
+ hooks: {}
150
+ },
151
+ canvas = null, // the canvas for the plot itself
152
+ overlay = null, // canvas for interactive stuff on top of plot
153
+ eventHolder = null, // jQuery object that events should be bound to
154
+ ctx = null, octx = null,
155
+ xaxes = [], yaxes = [],
156
+ plotOffset = { left: 0, right: 0, top: 0, bottom: 0},
157
+ canvasWidth = 0, canvasHeight = 0,
158
+ plotWidth = 0, plotHeight = 0,
159
+ hooks = {
160
+ processOptions: [],
161
+ processRawData: [],
162
+ processDatapoints: [],
163
+ processOffset: [],
164
+ drawBackground: [],
165
+ drawSeries: [],
166
+ draw: [],
167
+ bindEvents: [],
168
+ drawOverlay: [],
169
+ legendInserted: [],
170
+ shutdown: []
171
+ },
172
+ plot = this;
173
+
174
+ // public functions
175
+ plot.setData = setData;
176
+ plot.setupGrid = setupGrid;
177
+ plot.draw = draw;
178
+ plot.getPlaceholder = function() { return placeholder; };
179
+ plot.getCanvas = function() { return canvas; };
180
+ plot.getPlotOffset = function() { return plotOffset; };
181
+ plot.width = function () { return plotWidth; };
182
+ plot.height = function () { return plotHeight; };
183
+ plot.offset = function () {
184
+ var o = eventHolder.offset();
185
+ o.left += plotOffset.left;
186
+ o.top += plotOffset.top;
187
+ return o;
188
+ };
189
+ plot.getData = function () { return series; };
190
+ plot.getAxes = function () {
191
+ var res = {}, i;
192
+ $.each(xaxes.concat(yaxes), function (_, axis) {
193
+ if (axis)
194
+ res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis;
195
+ });
196
+ return res;
197
+ };
198
+ plot.getXAxes = function () { return xaxes; };
199
+ plot.getYAxes = function () { return yaxes; };
200
+ plot.c2p = canvasToAxisCoords;
201
+ plot.p2c = axisToCanvasCoords;
202
+ plot.getOptions = function () { return options; };
203
+ plot.highlight = highlight;
204
+ plot.unhighlight = unhighlight;
205
+ plot.triggerRedrawOverlay = triggerRedrawOverlay;
206
+ plot.pointOffset = function(point) {
207
+ return {
208
+ left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10),
209
+ top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10)
210
+ };
211
+ };
212
+ plot.shutdown = shutdown;
213
+ plot.resize = function () {
214
+ getCanvasDimensions();
215
+ resizeCanvas(canvas);
216
+ resizeCanvas(overlay);
217
+ };
218
+
219
+ // public attributes
220
+ plot.hooks = hooks;
221
+
222
+ // initialize
223
+ initPlugins(plot);
224
+ parseOptions(options_);
225
+ setupCanvases();
226
+ setData(data_);
227
+ setupGrid();
228
+ draw();
229
+ bindEvents();
230
+
231
+
232
+ function executeHooks(hook, args) {
233
+ args = [plot].concat(args);
234
+ for (var i = 0; i < hook.length; ++i)
235
+ hook[i].apply(this, args);
236
+ }
237
+
238
+ function initPlugins() {
239
+ for (var i = 0; i < plugins.length; ++i) {
240
+ var p = plugins[i];
241
+ p.init(plot);
242
+ if (p.options)
243
+ $.extend(true, options, p.options);
244
+ }
245
+ }
246
+
247
+ function parseOptions(opts) {
248
+ var i;
249
+
250
+ $.extend(true, options, opts);
251
+
252
+ if (options.xaxis.color == null)
253
+ options.xaxis.color = options.grid.color;
254
+ if (options.yaxis.color == null)
255
+ options.yaxis.color = options.grid.color;
256
+
257
+ if (options.xaxis.tickColor == null) // backwards-compatibility
258
+ options.xaxis.tickColor = options.grid.tickColor;
259
+ if (options.yaxis.tickColor == null) // backwards-compatibility
260
+ options.yaxis.tickColor = options.grid.tickColor;
261
+
262
+ if (options.grid.borderColor == null)
263
+ options.grid.borderColor = options.grid.color;
264
+ if (options.grid.tickColor == null)
265
+ options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString();
266
+
267
+ // fill in defaults in axes, copy at least always the
268
+ // first as the rest of the code assumes it'll be there
269
+ for (i = 0; i < Math.max(1, options.xaxes.length); ++i)
270
+ options.xaxes[i] = $.extend(true, {}, options.xaxis, options.xaxes[i]);
271
+ for (i = 0; i < Math.max(1, options.yaxes.length); ++i)
272
+ options.yaxes[i] = $.extend(true, {}, options.yaxis, options.yaxes[i]);
273
+
274
+ // backwards compatibility, to be removed in future
275
+ if (options.xaxis.noTicks && options.xaxis.ticks == null)
276
+ options.xaxis.ticks = options.xaxis.noTicks;
277
+ if (options.yaxis.noTicks && options.yaxis.ticks == null)
278
+ options.yaxis.ticks = options.yaxis.noTicks;
279
+ if (options.x2axis) {
280
+ options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis);
281
+ options.xaxes[1].position = "top";
282
+ }
283
+ if (options.y2axis) {
284
+ options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis);
285
+ options.yaxes[1].position = "right";
286
+ }
287
+ if (options.grid.coloredAreas)
288
+ options.grid.markings = options.grid.coloredAreas;
289
+ if (options.grid.coloredAreasColor)
290
+ options.grid.markingsColor = options.grid.coloredAreasColor;
291
+ if (options.lines)
292
+ $.extend(true, options.series.lines, options.lines);
293
+ if (options.points)
294
+ $.extend(true, options.series.points, options.points);
295
+ if (options.bars)
296
+ $.extend(true, options.series.bars, options.bars);
297
+ if (options.shadowSize != null)
298
+ options.series.shadowSize = options.shadowSize;
299
+ if (options.highlightColor != null)
300
+ options.series.highlightColor = options.highlightColor;
301
+
302
+ // save options on axes for future reference
303
+ for (i = 0; i < options.xaxes.length; ++i)
304
+ getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i];
305
+ for (i = 0; i < options.yaxes.length; ++i)
306
+ getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i];
307
+
308
+ // add hooks from options
309
+ for (var n in hooks)
310
+ if (options.hooks[n] && options.hooks[n].length)
311
+ hooks[n] = hooks[n].concat(options.hooks[n]);
312
+
313
+ executeHooks(hooks.processOptions, [options]);
314
+ }
315
+
316
+ function setData(d) {
317
+ series = parseData(d);
318
+ fillInSeriesOptions();
319
+ processData();
320
+ }
321
+
322
+ function parseData(d) {
323
+ var res = [];
324
+ for (var i = 0; i < d.length; ++i) {
325
+ var s = $.extend(true, {}, options.series);
326
+
327
+ if (d[i].data != null) {
328
+ s.data = d[i].data; // move the data instead of deep-copy
329
+ delete d[i].data;
330
+
331
+ $.extend(true, s, d[i]);
332
+
333
+ d[i].data = s.data;
334
+ }
335
+ else
336
+ s.data = d[i];
337
+ res.push(s);
338
+ }
339
+
340
+ return res;
341
+ }
342
+
343
+ function axisNumber(obj, coord) {
344
+ var a = obj[coord + "axis"];
345
+ if (typeof a == "object") // if we got a real axis, extract number
346
+ a = a.n;
347
+ if (typeof a != "number")
348
+ a = 1; // default to first axis
349
+ return a;
350
+ }
351
+
352
+ function allAxes() {
353
+ // return flat array without annoying null entries
354
+ return $.grep(xaxes.concat(yaxes), function (a) { return a; });
355
+ }
356
+
357
+ function canvasToAxisCoords(pos) {
358
+ // return an object with x/y corresponding to all used axes
359
+ var res = {}, i, axis;
360
+ for (i = 0; i < xaxes.length; ++i) {
361
+ axis = xaxes[i];
362
+ if (axis && axis.used)
363
+ res["x" + axis.n] = axis.c2p(pos.left);
364
+ }
365
+
366
+ for (i = 0; i < yaxes.length; ++i) {
367
+ axis = yaxes[i];
368
+ if (axis && axis.used)
369
+ res["y" + axis.n] = axis.c2p(pos.top);
370
+ }
371
+
372
+ if (res.x1 !== undefined)
373
+ res.x = res.x1;
374
+ if (res.y1 !== undefined)
375
+ res.y = res.y1;
376
+
377
+ return res;
378
+ }
379
+
380
+ function axisToCanvasCoords(pos) {
381
+ // get canvas coords from the first pair of x/y found in pos
382
+ var res = {}, i, axis, key;
383
+
384
+ for (i = 0; i < xaxes.length; ++i) {
385
+ axis = xaxes[i];
386
+ if (axis && axis.used) {
387
+ key = "x" + axis.n;
388
+ if (pos[key] == null && axis.n == 1)
389
+ key = "x";
390
+
391
+ if (pos[key] != null) {
392
+ res.left = axis.p2c(pos[key]);
393
+ break;
394
+ }
395
+ }
396
+ }
397
+
398
+ for (i = 0; i < yaxes.length; ++i) {
399
+ axis = yaxes[i];
400
+ if (axis && axis.used) {
401
+ key = "y" + axis.n;
402
+ if (pos[key] == null && axis.n == 1)
403
+ key = "y";
404
+
405
+ if (pos[key] != null) {
406
+ res.top = axis.p2c(pos[key]);
407
+ break;
408
+ }
409
+ }
410
+ }
411
+
412
+ return res;
413
+ }
414
+
415
+ function getOrCreateAxis(axes, number) {
416
+ if (!axes[number - 1])
417
+ axes[number - 1] = {
418
+ n: number, // save the number for future reference
419
+ direction: axes == xaxes ? "x" : "y",
420
+ options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis)
421
+ };
422
+
423
+ return axes[number - 1];
424
+ }
425
+
426
+ function fillInSeriesOptions() {
427
+
428
+ var neededColors = series.length, maxIndex = -1, i;
429
+
430
+ // Subtract the number of series that already have fixed colors or
431
+ // color indexes from the number that we still need to generate.
432
+
433
+ for (i = 0; i < series.length; ++i) {
434
+ var sc = series[i].color;
435
+ if (sc != null) {
436
+ neededColors--;
437
+ if (typeof sc == "number" && sc > maxIndex) {
438
+ maxIndex = sc;
439
+ }
440
+ }
441
+ }
442
+
443
+ // If any of the series have fixed color indexes, then we need to
444
+ // generate at least as many colors as the highest index.
445
+
446
+ if (neededColors <= maxIndex) {
447
+ neededColors = maxIndex + 1;
448
+ }
449
+
450
+ // Generate all the colors, using first the option colors and then
451
+ // variations on those colors once they're exhausted.
452
+
453
+ var c, colors = [], colorPool = options.colors,
454
+ colorPoolSize = colorPool.length, variation = 0;
455
+
456
+ for (i = 0; i < neededColors; i++) {
457
+
458
+ c = $.color.parse(colorPool[i % colorPoolSize] || "#666");
459
+
460
+ // Each time we exhaust the colors in the pool we adjust
461
+ // a scaling factor used to produce more variations on
462
+ // those colors. The factor alternates negative/positive
463
+ // to produce lighter/darker colors.
464
+
465
+ // Reset the variation after every few cycles, or else
466
+ // it will end up producing only white or black colors.
467
+
468
+ if (i % colorPoolSize == 0 && i) {
469
+ if (variation >= 0) {
470
+ if (variation < 0.5) {
471
+ variation = -variation - 0.2;
472
+ } else variation = 0;
473
+ } else variation = -variation;
474
+ }
475
+
476
+ colors[i] = c.scale('rgb', 1 + variation);
477
+ }
478
+
479
+ // Finalize the series options, filling in their colors
480
+
481
+ var colori = 0, s;
482
+ for (i = 0; i < series.length; ++i) {
483
+ s = series[i];
484
+
485
+ // assign colors
486
+ if (s.color == null) {
487
+ s.color = colors[colori].toString();
488
+ ++colori;
489
+ }
490
+ else if (typeof s.color == "number")
491
+ s.color = colors[s.color].toString();
492
+
493
+ // turn on lines automatically in case nothing is set
494
+ if (s.lines.show == null) {
495
+ var v, show = true;
496
+ for (v in s)
497
+ if (s[v] && s[v].show) {
498
+ show = false;
499
+ break;
500
+ }
501
+ if (show)
502
+ s.lines.show = true;
503
+ }
504
+
505
+ // If nothing was provided for lines.zero, default it to match
506
+ // lines.fill, since areas by default should extend to zero.
507
+
508
+ if (s.lines.zero == null) {
509
+ s.lines.zero = !!s.lines.fill;
510
+ }
511
+
512
+ // setup axes
513
+ s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x"));
514
+ s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y"));
515
+ }
516
+ }
517
+
518
+ function processData() {
519
+ var topSentry = Number.POSITIVE_INFINITY,
520
+ bottomSentry = Number.NEGATIVE_INFINITY,
521
+ fakeInfinity = Number.MAX_VALUE,
522
+ i, j, k, m, length,
523
+ s, points, ps, x, y, axis, val, f, p,
524
+ data, format;
525
+
526
+ function updateAxis(axis, min, max) {
527
+ if (min < axis.datamin && min != -fakeInfinity)
528
+ axis.datamin = min;
529
+ if (max > axis.datamax && max != fakeInfinity)
530
+ axis.datamax = max;
531
+ }
532
+
533
+ $.each(allAxes(), function (_, axis) {
534
+ // init axis
535
+ axis.datamin = topSentry;
536
+ axis.datamax = bottomSentry;
537
+ axis.used = false;
538
+ });
539
+
540
+ for (i = 0; i < series.length; ++i) {
541
+ s = series[i];
542
+ s.datapoints = { points: [] };
543
+
544
+ executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]);
545
+ }
546
+
547
+ // first pass: clean and copy data
548
+ for (i = 0; i < series.length; ++i) {
549
+ s = series[i];
550
+
551
+ data = s.data;
552
+ format = s.datapoints.format;
553
+
554
+ if (!format) {
555
+ format = [];
556
+ // find out how to copy
557
+ format.push({ x: true, number: true, required: true });
558
+ format.push({ y: true, number: true, required: true });
559
+
560
+ if (s.bars.show || (s.lines.show && s.lines.fill)) {
561
+ var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero));
562
+ format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale });
563
+ if (s.bars.horizontal) {
564
+ delete format[format.length - 1].y;
565
+ format[format.length - 1].x = true;
566
+ }
567
+ }
568
+
569
+ s.datapoints.format = format;
570
+ }
571
+
572
+ if (s.datapoints.pointsize != null)
573
+ continue; // already filled in
574
+
575
+ s.datapoints.pointsize = format.length;
576
+
577
+ ps = s.datapoints.pointsize;
578
+ points = s.datapoints.points;
579
+
580
+ var insertSteps = s.lines.show && s.lines.steps;
581
+ s.xaxis.used = s.yaxis.used = true;
582
+
583
+ for (j = k = 0; j < data.length; ++j, k += ps) {
584
+ p = data[j];
585
+
586
+ var nullify = p == null;
587
+ if (!nullify) {
588
+ for (m = 0; m < ps; ++m) {
589
+ val = p[m];
590
+ f = format[m];
591
+
592
+ if (f) {
593
+ if (f.number && val != null) {
594
+ val = +val; // convert to number
595
+ if (isNaN(val))
596
+ val = null;
597
+ else if (val == Infinity)
598
+ val = fakeInfinity;
599
+ else if (val == -Infinity)
600
+ val = -fakeInfinity;
601
+ }
602
+
603
+ if (val == null) {
604
+ if (f.required)
605
+ nullify = true;
606
+
607
+ if (f.defaultValue != null)
608
+ val = f.defaultValue;
609
+ }
610
+ }
611
+
612
+ points[k + m] = val;
613
+ }
614
+ }
615
+
616
+ if (nullify) {
617
+ for (m = 0; m < ps; ++m) {
618
+ val = points[k + m];
619
+ if (val != null) {
620
+ f = format[m];
621
+ // extract min/max info
622
+ if (f.x)
623
+ updateAxis(s.xaxis, val, val);
624
+ if (f.y)
625
+ updateAxis(s.yaxis, val, val);
626
+ }
627
+ points[k + m] = null;
628
+ }
629
+ }
630
+ else {
631
+ // a little bit of line specific stuff that
632
+ // perhaps shouldn't be here, but lacking
633
+ // better means...
634
+ if (insertSteps && k > 0
635
+ && points[k - ps] != null
636
+ && points[k - ps] != points[k]
637
+ && points[k - ps + 1] != points[k + 1]) {
638
+ // copy the point to make room for a middle point
639
+ for (m = 0; m < ps; ++m)
640
+ points[k + ps + m] = points[k + m];
641
+
642
+ // middle point has same y
643
+ points[k + 1] = points[k - ps + 1];
644
+
645
+ // we've added a point, better reflect that
646
+ k += ps;
647
+ }
648
+ }
649
+ }
650
+ }
651
+
652
+ // give the hooks a chance to run
653
+ for (i = 0; i < series.length; ++i) {
654
+ s = series[i];
655
+
656
+ executeHooks(hooks.processDatapoints, [ s, s.datapoints]);
657
+ }
658
+
659
+ // second pass: find datamax/datamin for auto-scaling
660
+ for (i = 0; i < series.length; ++i) {
661
+ s = series[i];
662
+ points = s.datapoints.points,
663
+ ps = s.datapoints.pointsize;
664
+ format = s.datapoints.format;
665
+
666
+ var xmin = topSentry, ymin = topSentry,
667
+ xmax = bottomSentry, ymax = bottomSentry;
668
+
669
+ for (j = 0; j < points.length; j += ps) {
670
+ if (points[j] == null)
671
+ continue;
672
+
673
+ for (m = 0; m < ps; ++m) {
674
+ val = points[j + m];
675
+ f = format[m];
676
+ if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity)
677
+ continue;
678
+
679
+ if (f.x) {
680
+ if (val < xmin)
681
+ xmin = val;
682
+ if (val > xmax)
683
+ xmax = val;
684
+ }
685
+ if (f.y) {
686
+ if (val < ymin)
687
+ ymin = val;
688
+ if (val > ymax)
689
+ ymax = val;
690
+ }
691
+ }
692
+ }
693
+
694
+ if (s.bars.show) {
695
+ // make sure we got room for the bar on the dancing floor
696
+ var delta;
697
+
698
+ switch (s.bars.align) {
699
+ case "left":
700
+ delta = 0;
701
+ break;
702
+ case "right":
703
+ delta = -s.bars.barWidth;
704
+ break;
705
+ case "center":
706
+ delta = -s.bars.barWidth / 2;
707
+ break;
708
+ default:
709
+ throw new Error("Invalid bar alignment: " + s.bars.align);
710
+ }
711
+
712
+ if (s.bars.horizontal) {
713
+ ymin += delta;
714
+ ymax += delta + s.bars.barWidth;
715
+ }
716
+ else {
717
+ xmin += delta;
718
+ xmax += delta + s.bars.barWidth;
719
+ }
720
+ }
721
+
722
+ updateAxis(s.xaxis, xmin, xmax);
723
+ updateAxis(s.yaxis, ymin, ymax);
724
+ }
725
+
726
+ $.each(allAxes(), function (_, axis) {
727
+ if (axis.datamin == topSentry)
728
+ axis.datamin = null;
729
+ if (axis.datamax == bottomSentry)
730
+ axis.datamax = null;
731
+ });
732
+ }
733
+
734
+ //////////////////////////////////////////////////////////////////////////////////
735
+ // Returns the display's ratio between physical and device-independent pixels.
736
+ //
737
+ // This is the ratio between the width that the browser advertises and the number
738
+ // of pixels actually available in that space. The iPhone 4, for example, has a
739
+ // device-independent width of 320px, but its screen is actually 640px wide. It
740
+ // therefore has a pixel ratio of 2, while most normal devices have a ratio of 1.
741
+
742
+ function getPixelRatio(cctx) {
743
+ var devicePixelRatio = window.devicePixelRatio || 1;
744
+ var backingStoreRatio =
745
+ cctx.webkitBackingStorePixelRatio ||
746
+ cctx.mozBackingStorePixelRatio ||
747
+ cctx.msBackingStorePixelRatio ||
748
+ cctx.oBackingStorePixelRatio ||
749
+ cctx.backingStorePixelRatio || 1;
750
+
751
+ return devicePixelRatio / backingStoreRatio;
752
+ }
753
+
754
+ function makeCanvas(cls) {
755
+
756
+ var c = document.createElement('canvas');
757
+ c.className = cls;
758
+
759
+ $(c).css({ direction: "ltr", position: "absolute", left: 0, top: 0 })
760
+ .appendTo(placeholder);
761
+
762
+ // If HTML5 Canvas isn't available, fall back to Excanvas
763
+
764
+ if (!c.getContext) {
765
+ if (window.G_vmlCanvasManager) {
766
+ c = window.G_vmlCanvasManager.initElement(c);
767
+ } else {
768
+ throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode.");
769
+ }
770
+ }
771
+
772
+ var cctx = c.getContext("2d");
773
+
774
+ // Increase the canvas density based on the display's pixel ratio; basically
775
+ // giving the canvas more pixels without increasing the size of its element,
776
+ // to take advantage of the fact that retina displays have that many more
777
+ // pixels than they actually use for page & element widths.
778
+
779
+ var pixelRatio = getPixelRatio(cctx);
780
+
781
+ c.width = canvasWidth * pixelRatio;
782
+ c.height = canvasHeight * pixelRatio;
783
+ c.style.width = canvasWidth + "px";
784
+ c.style.height = canvasHeight + "px";
785
+
786
+ // Save the context so we can reset in case we get replotted
787
+
788
+ cctx.save();
789
+
790
+ // Scale the coordinate space to match the display density; so even though we
791
+ // may have twice as many pixels, we still want lines and other drawing to
792
+ // appear at the same size; the extra pixels will just make them crisper.
793
+
794
+ cctx.scale(pixelRatio, pixelRatio);
795
+
796
+ return c;
797
+ }
798
+
799
+ function getCanvasDimensions() {
800
+ canvasWidth = placeholder.width();
801
+ canvasHeight = placeholder.height();
802
+
803
+ if (canvasWidth <= 0 || canvasHeight <= 0)
804
+ throw new Error("Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight);
805
+ }
806
+
807
+ function resizeCanvas(c) {
808
+
809
+ var cctx = c.getContext("2d");
810
+
811
+ // Handle pixel ratios > 1 for retina displays, as explained in makeCanvas
812
+
813
+ var pixelRatio = getPixelRatio(cctx);
814
+
815
+ // Resizing should reset the state (excanvas seems to be buggy though)
816
+
817
+ if (c.style.width != canvasWidth) {
818
+ c.width = canvasWidth * pixelRatio;
819
+ c.style.width = canvasWidth + "px";
820
+ }
821
+
822
+ if (c.style.height != canvasHeight) {
823
+ c.height = canvasHeight * pixelRatio;
824
+ c.style.height = canvasHeight + "px";
825
+ }
826
+
827
+ // so try to get back to the initial state (even if it's
828
+ // gone now, this should be safe according to the spec)
829
+ cctx.restore();
830
+
831
+ // and save again
832
+ cctx.save();
833
+
834
+ // Apply scaling for retina displays, as explained in makeCanvas
835
+
836
+ cctx.scale(pixelRatio, pixelRatio);
837
+ }
838
+
839
+ function setupCanvases() {
840
+ var reused,
841
+ existingCanvas = placeholder.children("canvas.flot-base"),
842
+ existingOverlay = placeholder.children("canvas.flot-overlay");
843
+
844
+ if (existingCanvas.length == 0 || existingOverlay == 0) {
845
+ // init everything
846
+
847
+ placeholder.html(""); // make sure placeholder is clear
848
+
849
+ placeholder.css({ padding: 0 }); // padding messes up the positioning
850
+
851
+ if (placeholder.css("position") == 'static')
852
+ placeholder.css("position", "relative"); // for positioning labels and overlay
853
+
854
+ getCanvasDimensions();
855
+
856
+ canvas = makeCanvas("flot-base");
857
+ overlay = makeCanvas("flot-overlay"); // overlay canvas for interactive features
858
+
859
+ reused = false;
860
+ }
861
+ else {
862
+ // reuse existing elements
863
+
864
+ canvas = existingCanvas.get(0);
865
+ overlay = existingOverlay.get(0);
866
+
867
+ reused = true;
868
+ }
869
+
870
+ ctx = canvas.getContext("2d");
871
+ octx = overlay.getContext("2d");
872
+
873
+ // define which element we're listening for events on
874
+ eventHolder = $(overlay);
875
+
876
+ if (reused) {
877
+ // run shutdown in the old plot object
878
+ placeholder.data("plot").shutdown();
879
+
880
+ // reset reused canvases
881
+ plot.resize();
882
+
883
+ // make sure overlay pixels are cleared (canvas is cleared when we redraw)
884
+ octx.clearRect(0, 0, canvasWidth, canvasHeight);
885
+
886
+ // then whack any remaining obvious garbage left
887
+ eventHolder.unbind();
888
+ placeholder.children().not([canvas, overlay]).remove();
889
+ }
890
+
891
+ // save in case we get replotted
892
+ placeholder.data("plot", plot);
893
+ }
894
+
895
+ function bindEvents() {
896
+ // bind events
897
+ if (options.grid.hoverable) {
898
+ eventHolder.mousemove(onMouseMove);
899
+
900
+ // Use bind, rather than .mouseleave, because we officially
901
+ // still support jQuery 1.2.6, which doesn't define a shortcut
902
+ // for mouseenter or mouseleave. This was a bug/oversight that
903
+ // was fixed somewhere around 1.3.x. We can return to using
904
+ // .mouseleave when we drop support for 1.2.6.
905
+
906
+ eventHolder.bind("mouseleave", onMouseLeave);
907
+ }
908
+
909
+ if (options.grid.clickable)
910
+ eventHolder.click(onClick);
911
+
912
+ executeHooks(hooks.bindEvents, [eventHolder]);
913
+ }
914
+
915
+ function shutdown() {
916
+ if (redrawTimeout)
917
+ clearTimeout(redrawTimeout);
918
+
919
+ eventHolder.unbind("mousemove", onMouseMove);
920
+ eventHolder.unbind("mouseleave", onMouseLeave);
921
+ eventHolder.unbind("click", onClick);
922
+
923
+ executeHooks(hooks.shutdown, [eventHolder]);
924
+ }
925
+
926
+ function setTransformationHelpers(axis) {
927
+ // set helper functions on the axis, assumes plot area
928
+ // has been computed already
929
+
930
+ function identity(x) { return x; }
931
+
932
+ var s, m, t = axis.options.transform || identity,
933
+ it = axis.options.inverseTransform;
934
+
935
+ // precompute how much the axis is scaling a point
936
+ // in canvas space
937
+ if (axis.direction == "x") {
938
+ s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min));
939
+ m = Math.min(t(axis.max), t(axis.min));
940
+ }
941
+ else {
942
+ s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min));
943
+ s = -s;
944
+ m = Math.max(t(axis.max), t(axis.min));
945
+ }
946
+
947
+ // data point to canvas coordinate
948
+ if (t == identity) // slight optimization
949
+ axis.p2c = function (p) { return (p - m) * s; };
950
+ else
951
+ axis.p2c = function (p) { return (t(p) - m) * s; };
952
+ // canvas coordinate to data point
953
+ if (!it)
954
+ axis.c2p = function (c) { return m + c / s; };
955
+ else
956
+ axis.c2p = function (c) { return it(m + c / s); };
957
+ }
958
+
959
+ function measureTickLabels(axis) {
960
+ var opts = axis.options, ticks = axis.ticks || [],
961
+ axisw = opts.labelWidth || 0, axish = opts.labelHeight || 0,
962
+ f = axis.font;
963
+
964
+ ctx.save();
965
+ ctx.font = f.style + " " + f.variant + " " + f.weight + " " + f.size + "px '" + f.family + "'";
966
+
967
+ for (var i = 0; i < ticks.length; ++i) {
968
+ var t = ticks[i];
969
+
970
+ t.lines = [];
971
+ t.width = t.height = 0;
972
+
973
+ if (!t.label)
974
+ continue;
975
+
976
+ // accept various kinds of newlines, including HTML ones
977
+ // (you can actually split directly on regexps in Javascript,
978
+ // but IE < 9 is unfortunately broken)
979
+ var lines = (t.label + "").replace(/<br ?\/?>|\r\n|\r/g, "\n").split("\n");
980
+ for (var j = 0; j < lines.length; ++j) {
981
+ var line = { text: lines[j] },
982
+ m = ctx.measureText(line.text);
983
+
984
+ line.width = m.width;
985
+ // m.height might not be defined, not in the
986
+ // standard yet
987
+ line.height = m.height != null ? m.height : f.size;
988
+
989
+ // add a bit of margin since font rendering is
990
+ // not pixel perfect and cut off letters look
991
+ // bad, this also doubles as spacing between
992
+ // lines
993
+ line.height += Math.round(f.size * 0.15);
994
+
995
+ t.width = Math.max(line.width, t.width);
996
+ t.height += line.height;
997
+
998
+ t.lines.push(line);
999
+ }
1000
+
1001
+ if (opts.labelWidth == null)
1002
+ axisw = Math.max(axisw, t.width);
1003
+ if (opts.labelHeight == null)
1004
+ axish = Math.max(axish, t.height);
1005
+ }
1006
+ ctx.restore();
1007
+
1008
+ axis.labelWidth = Math.ceil(axisw);
1009
+ axis.labelHeight = Math.ceil(axish);
1010
+ }
1011
+
1012
+ function allocateAxisBoxFirstPhase(axis) {
1013
+ // find the bounding box of the axis by looking at label
1014
+ // widths/heights and ticks, make room by diminishing the
1015
+ // plotOffset; this first phase only looks at one
1016
+ // dimension per axis, the other dimension depends on the
1017
+ // other axes so will have to wait
1018
+
1019
+ var lw = axis.labelWidth,
1020
+ lh = axis.labelHeight,
1021
+ pos = axis.options.position,
1022
+ tickLength = axis.options.tickLength,
1023
+ axisMargin = options.grid.axisMargin,
1024
+ padding = options.grid.labelMargin,
1025
+ all = axis.direction == "x" ? xaxes : yaxes,
1026
+ index, innermost;
1027
+
1028
+ // determine axis margin
1029
+ var samePosition = $.grep(all, function (a) {
1030
+ return a && a.options.position == pos && a.reserveSpace;
1031
+ });
1032
+ if ($.inArray(axis, samePosition) == samePosition.length - 1)
1033
+ axisMargin = 0; // outermost
1034
+
1035
+ // determine tick length - if we're innermost, we can use "full"
1036
+ if (tickLength == null) {
1037
+ var sameDirection = $.grep(all, function (a) {
1038
+ return a && a.reserveSpace;
1039
+ });
1040
+
1041
+ innermost = $.inArray(axis, sameDirection) == 0;
1042
+ if (innermost)
1043
+ tickLength = "full";
1044
+ else
1045
+ tickLength = 5;
1046
+ }
1047
+
1048
+ if (!isNaN(+tickLength))
1049
+ padding += +tickLength;
1050
+
1051
+ // compute box
1052
+ if (axis.direction == "x") {
1053
+ lh += padding;
1054
+
1055
+ if (pos == "bottom") {
1056
+ plotOffset.bottom += lh + axisMargin;
1057
+ axis.box = { top: canvasHeight - plotOffset.bottom, height: lh };
1058
+ }
1059
+ else {
1060
+ axis.box = { top: plotOffset.top + axisMargin, height: lh };
1061
+ plotOffset.top += lh + axisMargin;
1062
+ }
1063
+ }
1064
+ else {
1065
+ lw += padding;
1066
+
1067
+ if (pos == "left") {
1068
+ axis.box = { left: plotOffset.left + axisMargin, width: lw };
1069
+ plotOffset.left += lw + axisMargin;
1070
+ }
1071
+ else {
1072
+ plotOffset.right += lw + axisMargin;
1073
+ axis.box = { left: canvasWidth - plotOffset.right, width: lw };
1074
+ }
1075
+ }
1076
+
1077
+ // save for future reference
1078
+ axis.position = pos;
1079
+ axis.tickLength = tickLength;
1080
+ axis.box.padding = padding;
1081
+ axis.innermost = innermost;
1082
+ }
1083
+
1084
+ function allocateAxisBoxSecondPhase(axis) {
1085
+ // now that all axis boxes have been placed in one
1086
+ // dimension, we can set the remaining dimension coordinates
1087
+ if (axis.direction == "x") {
1088
+ axis.box.left = plotOffset.left - axis.labelWidth / 2;
1089
+ axis.box.width = canvasWidth - plotOffset.left - plotOffset.right + axis.labelWidth;
1090
+ }
1091
+ else {
1092
+ axis.box.top = plotOffset.top - axis.labelHeight / 2;
1093
+ axis.box.height = canvasHeight - plotOffset.bottom - plotOffset.top + axis.labelHeight;
1094
+ }
1095
+ }
1096
+
1097
+ function adjustLayoutForThingsStickingOut() {
1098
+ // possibly adjust plot offset to ensure everything stays
1099
+ // inside the canvas and isn't clipped off
1100
+
1101
+ var minMargin = options.grid.minBorderMargin,
1102
+ margins = { x: 0, y: 0 }, i, axis;
1103
+
1104
+ // check stuff from the plot (FIXME: this should just read
1105
+ // a value from the series, otherwise it's impossible to
1106
+ // customize)
1107
+ if (minMargin == null) {
1108
+ minMargin = 0;
1109
+ for (i = 0; i < series.length; ++i)
1110
+ minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2));
1111
+ }
1112
+
1113
+ margins.x = margins.y = Math.ceil(minMargin);
1114
+
1115
+ // check axis labels, note we don't check the actual
1116
+ // labels but instead use the overall width/height to not
1117
+ // jump as much around with replots
1118
+ $.each(allAxes(), function (_, axis) {
1119
+ var dir = axis.direction;
1120
+ if (axis.reserveSpace)
1121
+ margins[dir] = Math.ceil(Math.max(margins[dir], (dir == "x" ? axis.labelWidth : axis.labelHeight) / 2));
1122
+ });
1123
+
1124
+ plotOffset.left = Math.max(margins.x, plotOffset.left);
1125
+ plotOffset.right = Math.max(margins.x, plotOffset.right);
1126
+ plotOffset.top = Math.max(margins.y, plotOffset.top);
1127
+ plotOffset.bottom = Math.max(margins.y, plotOffset.bottom);
1128
+ }
1129
+
1130
+ function setupGrid() {
1131
+ var i, axes = allAxes(), showGrid = options.grid.show;
1132
+
1133
+ // Initialize the plot's offset from the edge of the canvas
1134
+
1135
+ for (var a in plotOffset) {
1136
+ var margin = options.grid.margin || 0;
1137
+ plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0;
1138
+ }
1139
+
1140
+ executeHooks(hooks.processOffset, [plotOffset]);
1141
+
1142
+ // If the grid is visible, add its border width to the offset
1143
+
1144
+ for (var a in plotOffset) {
1145
+ if(typeof(options.grid.borderWidth) == "object") {
1146
+ plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0;
1147
+ }
1148
+ else {
1149
+ plotOffset[a] += showGrid ? options.grid.borderWidth : 0;
1150
+ }
1151
+ }
1152
+
1153
+ // init axes
1154
+ $.each(axes, function (_, axis) {
1155
+ axis.show = axis.options.show;
1156
+ if (axis.show == null)
1157
+ axis.show = axis.used; // by default an axis is visible if it's got data
1158
+
1159
+ axis.reserveSpace = axis.show || axis.options.reserveSpace;
1160
+
1161
+ setRange(axis);
1162
+ });
1163
+
1164
+ if (showGrid) {
1165
+ // determine from the placeholder the font size ~ height of font ~ 1 em
1166
+ var fontDefaults = {
1167
+ style: placeholder.css("font-style"),
1168
+ size: Math.round(0.8 * (+placeholder.css("font-size").replace("px", "") || 13)),
1169
+ variant: placeholder.css("font-variant"),
1170
+ weight: placeholder.css("font-weight"),
1171
+ family: placeholder.css("font-family")
1172
+ };
1173
+
1174
+ var allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; });
1175
+
1176
+ $.each(allocatedAxes, function (_, axis) {
1177
+ // make the ticks
1178
+ setupTickGeneration(axis);
1179
+ setTicks(axis);
1180
+ snapRangeToTicks(axis, axis.ticks);
1181
+
1182
+ // find labelWidth/Height for axis
1183
+ axis.font = $.extend({}, fontDefaults, axis.options.font);
1184
+ measureTickLabels(axis);
1185
+ });
1186
+
1187
+ // with all dimensions calculated, we can compute the
1188
+ // axis bounding boxes, start from the outside
1189
+ // (reverse order)
1190
+ for (i = allocatedAxes.length - 1; i >= 0; --i)
1191
+ allocateAxisBoxFirstPhase(allocatedAxes[i]);
1192
+
1193
+ // make sure we've got enough space for things that
1194
+ // might stick out
1195
+ adjustLayoutForThingsStickingOut();
1196
+
1197
+ $.each(allocatedAxes, function (_, axis) {
1198
+ allocateAxisBoxSecondPhase(axis);
1199
+ });
1200
+ }
1201
+
1202
+ plotWidth = canvasWidth - plotOffset.left - plotOffset.right;
1203
+ plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top;
1204
+
1205
+ // now we got the proper plot dimensions, we can compute the scaling
1206
+ $.each(axes, function (_, axis) {
1207
+ setTransformationHelpers(axis);
1208
+ });
1209
+
1210
+ insertLegend();
1211
+ }
1212
+
1213
+ function setRange(axis) {
1214
+ var opts = axis.options,
1215
+ min = +(opts.min != null ? opts.min : axis.datamin),
1216
+ max = +(opts.max != null ? opts.max : axis.datamax),
1217
+ delta = max - min;
1218
+
1219
+ if (delta == 0.0) {
1220
+ // degenerate case
1221
+ var widen = max == 0 ? 1 : 0.01;
1222
+
1223
+ if (opts.min == null)
1224
+ min -= widen;
1225
+ // always widen max if we couldn't widen min to ensure we
1226
+ // don't fall into min == max which doesn't work
1227
+ if (opts.max == null || opts.min != null)
1228
+ max += widen;
1229
+ }
1230
+ else {
1231
+ // consider autoscaling
1232
+ var margin = opts.autoscaleMargin;
1233
+ if (margin != null) {
1234
+ if (opts.min == null) {
1235
+ min -= delta * margin;
1236
+ // make sure we don't go below zero if all values
1237
+ // are positive
1238
+ if (min < 0 && axis.datamin != null && axis.datamin >= 0)
1239
+ min = 0;
1240
+ }
1241
+ if (opts.max == null) {
1242
+ max += delta * margin;
1243
+ if (max > 0 && axis.datamax != null && axis.datamax <= 0)
1244
+ max = 0;
1245
+ }
1246
+ }
1247
+ }
1248
+ axis.min = min;
1249
+ axis.max = max;
1250
+ }
1251
+
1252
+ function setupTickGeneration(axis) {
1253
+ var opts = axis.options;
1254
+
1255
+ // estimate number of ticks
1256
+ var noTicks;
1257
+ if (typeof opts.ticks == "number" && opts.ticks > 0)
1258
+ noTicks = opts.ticks;
1259
+ else
1260
+ // heuristic based on the model a*sqrt(x) fitted to
1261
+ // some data points that seemed reasonable
1262
+ noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? canvasWidth : canvasHeight);
1263
+
1264
+ axis.delta = (axis.max - axis.min) / noTicks;
1265
+
1266
+ // Time mode was moved to a plug-in in 0.8, but since so many people use this
1267
+ // we'll add an especially friendly make sure they remembered to include it.
1268
+
1269
+ if (opts.mode == "time" && !axis.tickGenerator) {
1270
+ throw new Error("Time mode requires the flot.time plugin.");
1271
+ }
1272
+
1273
+ // Flot supports base-10 axes; any other mode else is handled by a plug-in,
1274
+ // like flot.time.js.
1275
+
1276
+ if (!axis.tickGenerator) {
1277
+
1278
+ axis.tickGenerator = function (axis) {
1279
+ var maxDec = opts.tickDecimals,
1280
+ dec = -Math.floor(Math.log(axis.delta) / Math.LN10);
1281
+
1282
+ if (maxDec != null && dec > maxDec)
1283
+ dec = maxDec;
1284
+
1285
+ var magn = Math.pow(10, -dec),
1286
+ norm = axis.delta / magn, // norm is between 1.0 and 10.0
1287
+ size,
1288
+
1289
+ ticks = [],
1290
+ start,
1291
+ i = 0,
1292
+ v = Number.NaN,
1293
+ prev;
1294
+
1295
+ if (norm < 1.5)
1296
+ size = 1;
1297
+ else if (norm < 3) {
1298
+ size = 2;
1299
+ // special case for 2.5, requires an extra decimal
1300
+ if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
1301
+ size = 2.5;
1302
+ ++dec;
1303
+ }
1304
+ }
1305
+ else if (norm < 7.5)
1306
+ size = 5;
1307
+ else size = 10;
1308
+
1309
+ size *= magn;
1310
+
1311
+ if (opts.minTickSize != null && size < opts.minTickSize)
1312
+ size = opts.minTickSize;
1313
+
1314
+ axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
1315
+ axis.tickSize = opts.tickSize || size;
1316
+
1317
+ start = floorInBase(axis.min, axis.tickSize)
1318
+
1319
+ do {
1320
+ prev = v;
1321
+ v = start + i * axis.tickSize;
1322
+ ticks.push(v);
1323
+ ++i;
1324
+ } while (v < axis.max && v != prev);
1325
+ return ticks;
1326
+ };
1327
+
1328
+ axis.tickFormatter = function (value, axis) {
1329
+
1330
+ var factor = Math.pow(10, axis.tickDecimals);
1331
+ var formatted = "" + Math.round(value * factor) / factor;
1332
+
1333
+ // If tickDecimals was specified, ensure that we have exactly that
1334
+ // much precision; otherwise default to the value's own precision.
1335
+
1336
+ if (axis.tickDecimals != null) {
1337
+ var decimal = formatted.indexOf(".");
1338
+ var precision = decimal == -1 ? 0 : formatted.length - decimal - 1;
1339
+ if (precision < axis.tickDecimals) {
1340
+ return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision);
1341
+ }
1342
+ }
1343
+
1344
+ return formatted;
1345
+ };
1346
+ }
1347
+
1348
+ if ($.isFunction(opts.tickFormatter))
1349
+ axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); };
1350
+
1351
+ if (opts.alignTicksWithAxis != null) {
1352
+ var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1];
1353
+ if (otherAxis && otherAxis.used && otherAxis != axis) {
1354
+ // consider snapping min/max to outermost nice ticks
1355
+ var niceTicks = axis.tickGenerator(axis);
1356
+ if (niceTicks.length > 0) {
1357
+ if (opts.min == null)
1358
+ axis.min = Math.min(axis.min, niceTicks[0]);
1359
+ if (opts.max == null && niceTicks.length > 1)
1360
+ axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]);
1361
+ }
1362
+
1363
+ axis.tickGenerator = function (axis) {
1364
+ // copy ticks, scaled to this axis
1365
+ var ticks = [], v, i;
1366
+ for (i = 0; i < otherAxis.ticks.length; ++i) {
1367
+ v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min);
1368
+ v = axis.min + v * (axis.max - axis.min);
1369
+ ticks.push(v);
1370
+ }
1371
+ return ticks;
1372
+ };
1373
+
1374
+ // we might need an extra decimal since forced
1375
+ // ticks don't necessarily fit naturally
1376
+ if (!axis.mode && opts.tickDecimals == null) {
1377
+ var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1),
1378
+ ts = axis.tickGenerator(axis);
1379
+
1380
+ // only proceed if the tick interval rounded
1381
+ // with an extra decimal doesn't give us a
1382
+ // zero at end
1383
+ if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec))))
1384
+ axis.tickDecimals = extraDec;
1385
+ }
1386
+ }
1387
+ }
1388
+ }
1389
+
1390
+ function setTicks(axis) {
1391
+ var oticks = axis.options.ticks, ticks = [];
1392
+ if (oticks == null || (typeof oticks == "number" && oticks > 0))
1393
+ ticks = axis.tickGenerator(axis);
1394
+ else if (oticks) {
1395
+ if ($.isFunction(oticks))
1396
+ // generate the ticks
1397
+ ticks = oticks(axis);
1398
+ else
1399
+ ticks = oticks;
1400
+ }
1401
+
1402
+ // clean up/labelify the supplied ticks, copy them over
1403
+ var i, v;
1404
+ axis.ticks = [];
1405
+ for (i = 0; i < ticks.length; ++i) {
1406
+ var label = null;
1407
+ var t = ticks[i];
1408
+ if (typeof t == "object") {
1409
+ v = +t[0];
1410
+ if (t.length > 1)
1411
+ label = t[1];
1412
+ }
1413
+ else
1414
+ v = +t;
1415
+ if (label == null)
1416
+ label = axis.tickFormatter(v, axis);
1417
+ if (!isNaN(v))
1418
+ axis.ticks.push({ v: v, label: label });
1419
+ }
1420
+ }
1421
+
1422
+ function snapRangeToTicks(axis, ticks) {
1423
+ if (axis.options.autoscaleMargin && ticks.length > 0) {
1424
+ // snap to ticks
1425
+ if (axis.options.min == null)
1426
+ axis.min = Math.min(axis.min, ticks[0].v);
1427
+ if (axis.options.max == null && ticks.length > 1)
1428
+ axis.max = Math.max(axis.max, ticks[ticks.length - 1].v);
1429
+ }
1430
+ }
1431
+
1432
+ function draw() {
1433
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
1434
+
1435
+ executeHooks(hooks.drawBackground, [ctx]);
1436
+
1437
+ var grid = options.grid;
1438
+
1439
+ // draw background, if any
1440
+ if (grid.show && grid.backgroundColor)
1441
+ drawBackground();
1442
+
1443
+ if (grid.show && !grid.aboveData) {
1444
+ drawGrid();
1445
+ drawAxisLabels();
1446
+ }
1447
+
1448
+ for (var i = 0; i < series.length; ++i) {
1449
+ executeHooks(hooks.drawSeries, [ctx, series[i]]);
1450
+ drawSeries(series[i]);
1451
+ }
1452
+
1453
+ executeHooks(hooks.draw, [ctx]);
1454
+
1455
+ if (grid.show && grid.aboveData) {
1456
+ drawGrid();
1457
+ drawAxisLabels();
1458
+ }
1459
+ }
1460
+
1461
+ function extractRange(ranges, coord) {
1462
+ var axis, from, to, key, axes = allAxes();
1463
+
1464
+ for (var i = 0; i < axes.length; ++i) {
1465
+ axis = axes[i];
1466
+ if (axis.direction == coord) {
1467
+ key = coord + axis.n + "axis";
1468
+ if (!ranges[key] && axis.n == 1)
1469
+ key = coord + "axis"; // support x1axis as xaxis
1470
+ if (ranges[key]) {
1471
+ from = ranges[key].from;
1472
+ to = ranges[key].to;
1473
+ break;
1474
+ }
1475
+ }
1476
+ }
1477
+
1478
+ // backwards-compat stuff - to be removed in future
1479
+ if (!ranges[key]) {
1480
+ axis = coord == "x" ? xaxes[0] : yaxes[0];
1481
+ from = ranges[coord + "1"];
1482
+ to = ranges[coord + "2"];
1483
+ }
1484
+
1485
+ // auto-reverse as an added bonus
1486
+ if (from != null && to != null && from > to) {
1487
+ var tmp = from;
1488
+ from = to;
1489
+ to = tmp;
1490
+ }
1491
+
1492
+ return { from: from, to: to, axis: axis };
1493
+ }
1494
+
1495
+ function drawBackground() {
1496
+ ctx.save();
1497
+ ctx.translate(plotOffset.left, plotOffset.top);
1498
+
1499
+ ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)");
1500
+ ctx.fillRect(0, 0, plotWidth, plotHeight);
1501
+ ctx.restore();
1502
+ }
1503
+
1504
+ function drawGrid() {
1505
+ var i, axes, bw, bc;
1506
+
1507
+ ctx.save();
1508
+ ctx.translate(plotOffset.left, plotOffset.top);
1509
+
1510
+ // draw markings
1511
+ var markings = options.grid.markings;
1512
+ if (markings) {
1513
+ if ($.isFunction(markings)) {
1514
+ axes = plot.getAxes();
1515
+ // xmin etc. is backwards compatibility, to be
1516
+ // removed in the future
1517
+ axes.xmin = axes.xaxis.min;
1518
+ axes.xmax = axes.xaxis.max;
1519
+ axes.ymin = axes.yaxis.min;
1520
+ axes.ymax = axes.yaxis.max;
1521
+
1522
+ markings = markings(axes);
1523
+ }
1524
+
1525
+ for (i = 0; i < markings.length; ++i) {
1526
+ var m = markings[i],
1527
+ xrange = extractRange(m, "x"),
1528
+ yrange = extractRange(m, "y");
1529
+
1530
+ // fill in missing
1531
+ if (xrange.from == null)
1532
+ xrange.from = xrange.axis.min;
1533
+ if (xrange.to == null)
1534
+ xrange.to = xrange.axis.max;
1535
+ if (yrange.from == null)
1536
+ yrange.from = yrange.axis.min;
1537
+ if (yrange.to == null)
1538
+ yrange.to = yrange.axis.max;
1539
+
1540
+ // clip
1541
+ if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max ||
1542
+ yrange.to < yrange.axis.min || yrange.from > yrange.axis.max)
1543
+ continue;
1544
+
1545
+ xrange.from = Math.max(xrange.from, xrange.axis.min);
1546
+ xrange.to = Math.min(xrange.to, xrange.axis.max);
1547
+ yrange.from = Math.max(yrange.from, yrange.axis.min);
1548
+ yrange.to = Math.min(yrange.to, yrange.axis.max);
1549
+
1550
+ if (xrange.from == xrange.to && yrange.from == yrange.to)
1551
+ continue;
1552
+
1553
+ // then draw
1554
+ xrange.from = xrange.axis.p2c(xrange.from);
1555
+ xrange.to = xrange.axis.p2c(xrange.to);
1556
+ yrange.from = yrange.axis.p2c(yrange.from);
1557
+ yrange.to = yrange.axis.p2c(yrange.to);
1558
+
1559
+ if (xrange.from == xrange.to || yrange.from == yrange.to) {
1560
+ // draw line
1561
+ ctx.beginPath();
1562
+ ctx.strokeStyle = m.color || options.grid.markingsColor;
1563
+ ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth;
1564
+ ctx.moveTo(xrange.from, yrange.from);
1565
+ ctx.lineTo(xrange.to, yrange.to);
1566
+ ctx.stroke();
1567
+ }
1568
+ else {
1569
+ // fill area
1570
+ ctx.fillStyle = m.color || options.grid.markingsColor;
1571
+ ctx.fillRect(xrange.from, yrange.to,
1572
+ xrange.to - xrange.from,
1573
+ yrange.from - yrange.to);
1574
+ }
1575
+ }
1576
+ }
1577
+
1578
+ // draw the ticks
1579
+ axes = allAxes();
1580
+ bw = options.grid.borderWidth;
1581
+
1582
+ for (var j = 0; j < axes.length; ++j) {
1583
+ var axis = axes[j], box = axis.box,
1584
+ t = axis.tickLength, x, y, xoff, yoff;
1585
+ if (!axis.show || axis.ticks.length == 0)
1586
+ continue;
1587
+
1588
+ ctx.strokeStyle = axis.options.tickColor || $.color.parse(axis.options.color).scale('a', 0.22).toString();
1589
+ ctx.lineWidth = 1;
1590
+
1591
+ // find the edges
1592
+ if (axis.direction == "x") {
1593
+ x = 0;
1594
+ if (t == "full")
1595
+ y = (axis.position == "top" ? 0 : plotHeight);
1596
+ else
1597
+ y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0);
1598
+ }
1599
+ else {
1600
+ y = 0;
1601
+ if (t == "full")
1602
+ x = (axis.position == "left" ? 0 : plotWidth);
1603
+ else
1604
+ x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0);
1605
+ }
1606
+
1607
+ // draw tick bar
1608
+ if (!axis.innermost) {
1609
+ ctx.beginPath();
1610
+ xoff = yoff = 0;
1611
+ if (axis.direction == "x")
1612
+ xoff = plotWidth;
1613
+ else
1614
+ yoff = plotHeight;
1615
+
1616
+ if (ctx.lineWidth == 1) {
1617
+ x = Math.floor(x) + 0.5;
1618
+ y = Math.floor(y) + 0.5;
1619
+ }
1620
+
1621
+ ctx.moveTo(x, y);
1622
+ ctx.lineTo(x + xoff, y + yoff);
1623
+ ctx.stroke();
1624
+ }
1625
+
1626
+ // draw ticks
1627
+ ctx.beginPath();
1628
+ for (i = 0; i < axis.ticks.length; ++i) {
1629
+ var v = axis.ticks[i].v;
1630
+
1631
+ xoff = yoff = 0;
1632
+
1633
+ if (v < axis.min || v > axis.max
1634
+ // skip those lying on the axes if we got a border
1635
+ || (t == "full"
1636
+ && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0)
1637
+ && (v == axis.min || v == axis.max)))
1638
+ continue;
1639
+
1640
+ if (axis.direction == "x") {
1641
+ x = axis.p2c(v);
1642
+ yoff = t == "full" ? -plotHeight : t;
1643
+
1644
+ if (axis.position == "top")
1645
+ yoff = -yoff;
1646
+ }
1647
+ else {
1648
+ y = axis.p2c(v);
1649
+ xoff = t == "full" ? -plotWidth : t;
1650
+
1651
+ if (axis.position == "left")
1652
+ xoff = -xoff;
1653
+ }
1654
+
1655
+ if (ctx.lineWidth == 1) {
1656
+ if (axis.direction == "x")
1657
+ x = Math.floor(x) + 0.5;
1658
+ else
1659
+ y = Math.floor(y) + 0.5;
1660
+ }
1661
+
1662
+ ctx.moveTo(x, y);
1663
+ ctx.lineTo(x + xoff, y + yoff);
1664
+ }
1665
+
1666
+ ctx.stroke();
1667
+ }
1668
+
1669
+
1670
+ // draw border
1671
+ if (bw) {
1672
+ // If either borderWidth or borderColor is an object, then draw the border
1673
+ // line by line instead of as one rectangle
1674
+ bc = options.grid.borderColor;
1675
+ if(typeof bw == "object" || typeof bc == "object") {
1676
+ if (typeof bw !== "object") {
1677
+ bw = {top: bw, right: bw, bottom: bw, left: bw};
1678
+ }
1679
+ if (typeof bc !== "object") {
1680
+ bc = {top: bc, right: bc, bottom: bc, left: bc};
1681
+ }
1682
+
1683
+ if (bw.top > 0) {
1684
+ ctx.strokeStyle = bc.top;
1685
+ ctx.lineWidth = bw.top;
1686
+ ctx.beginPath();
1687
+ ctx.moveTo(0 - bw.left, 0 - bw.top/2);
1688
+ ctx.lineTo(plotWidth, 0 - bw.top/2);
1689
+ ctx.stroke();
1690
+ }
1691
+
1692
+ if (bw.right > 0) {
1693
+ ctx.strokeStyle = bc.right;
1694
+ ctx.lineWidth = bw.right;
1695
+ ctx.beginPath();
1696
+ ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top);
1697
+ ctx.lineTo(plotWidth + bw.right / 2, plotHeight);
1698
+ ctx.stroke();
1699
+ }
1700
+
1701
+ if (bw.bottom > 0) {
1702
+ ctx.strokeStyle = bc.bottom;
1703
+ ctx.lineWidth = bw.bottom;
1704
+ ctx.beginPath();
1705
+ ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2);
1706
+ ctx.lineTo(0, plotHeight + bw.bottom / 2);
1707
+ ctx.stroke();
1708
+ }
1709
+
1710
+ if (bw.left > 0) {
1711
+ ctx.strokeStyle = bc.left;
1712
+ ctx.lineWidth = bw.left;
1713
+ ctx.beginPath();
1714
+ ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom);
1715
+ ctx.lineTo(0- bw.left/2, 0);
1716
+ ctx.stroke();
1717
+ }
1718
+ }
1719
+ else {
1720
+ ctx.lineWidth = bw;
1721
+ ctx.strokeStyle = options.grid.borderColor;
1722
+ ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw);
1723
+ }
1724
+ }
1725
+
1726
+ ctx.restore();
1727
+ }
1728
+
1729
+ function drawAxisLabels() {
1730
+ ctx.save();
1731
+
1732
+ $.each(allAxes(), function (_, axis) {
1733
+ if (!axis.show || axis.ticks.length == 0)
1734
+ return;
1735
+
1736
+ var box = axis.box, f = axis.font;
1737
+ // placeholder.append('<div style="position:absolute;opacity:0.10;background-color:red;left:' + box.left + 'px;top:' + box.top + 'px;width:' + box.width + 'px;height:' + box.height + 'px"></div>') // debug
1738
+
1739
+ ctx.fillStyle = axis.options.color;
1740
+ // Important: Don't use quotes around axis.font.family! Just around single
1741
+ // font names like 'Times New Roman' that have a space or special character in it.
1742
+ ctx.font = f.style + " " + f.variant + " " + f.weight + " " + f.size + "px " + f.family;
1743
+ ctx.textAlign = "start";
1744
+ // middle align the labels - top would be more
1745
+ // natural, but browsers can differ a pixel or two in
1746
+ // where they consider the top to be, so instead we
1747
+ // middle align to minimize variation between browsers
1748
+ // and compensate when calculating the coordinates
1749
+ ctx.textBaseline = "middle";
1750
+
1751
+ for (var i = 0; i < axis.ticks.length; ++i) {
1752
+ var tick = axis.ticks[i];
1753
+ if (!tick.label || tick.v < axis.min || tick.v > axis.max)
1754
+ continue;
1755
+
1756
+ var x, y, offset = 0, line;
1757
+ for (var k = 0; k < tick.lines.length; ++k) {
1758
+ line = tick.lines[k];
1759
+
1760
+ if (axis.direction == "x") {
1761
+ x = plotOffset.left + axis.p2c(tick.v) - line.width/2;
1762
+ if (axis.position == "bottom")
1763
+ y = box.top + box.padding;
1764
+ else
1765
+ y = box.top + box.height - box.padding - tick.height;
1766
+ }
1767
+ else {
1768
+ y = plotOffset.top + axis.p2c(tick.v) - tick.height/2;
1769
+ if (axis.position == "left")
1770
+ x = box.left + box.width - box.padding - line.width;
1771
+ else
1772
+ x = box.left + box.padding;
1773
+ }
1774
+
1775
+ // account for middle aligning and line number
1776
+ y += line.height/2 + offset;
1777
+ offset += line.height;
1778
+
1779
+ if (!!(window.opera && window.opera.version().split('.')[0] < 12)) {
1780
+ // FIXME: LEGACY BROWSER FIX
1781
+ // AFFECTS: Opera < 12.00
1782
+
1783
+ // round the coordinates since Opera
1784
+ // otherwise switches to more ugly
1785
+ // rendering (probably non-hinted) and
1786
+ // offset the y coordinates since it seems
1787
+ // to be off pretty consistently compared
1788
+ // to the other browsers
1789
+ x = Math.floor(x);
1790
+ y = Math.ceil(y - 2);
1791
+ }
1792
+ ctx.fillText(line.text, x, y);
1793
+ }
1794
+ }
1795
+ });
1796
+
1797
+ ctx.restore();
1798
+ }
1799
+
1800
+ function drawSeries(series) {
1801
+ if (series.lines.show)
1802
+ drawSeriesLines(series);
1803
+ if (series.bars.show)
1804
+ drawSeriesBars(series);
1805
+ if (series.points.show)
1806
+ drawSeriesPoints(series);
1807
+ }
1808
+
1809
+ function drawSeriesLines(series) {
1810
+ function plotLine(datapoints, xoffset, yoffset, axisx, axisy) {
1811
+ var points = datapoints.points,
1812
+ ps = datapoints.pointsize,
1813
+ prevx = null, prevy = null;
1814
+
1815
+ ctx.beginPath();
1816
+ for (var i = ps; i < points.length; i += ps) {
1817
+ var x1 = points[i - ps], y1 = points[i - ps + 1],
1818
+ x2 = points[i], y2 = points[i + 1];
1819
+
1820
+ if (x1 == null || x2 == null)
1821
+ continue;
1822
+
1823
+ // clip with ymin
1824
+ if (y1 <= y2 && y1 < axisy.min) {
1825
+ if (y2 < axisy.min)
1826
+ continue; // line segment is outside
1827
+ // compute new intersection point
1828
+ x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
1829
+ y1 = axisy.min;
1830
+ }
1831
+ else if (y2 <= y1 && y2 < axisy.min) {
1832
+ if (y1 < axisy.min)
1833
+ continue;
1834
+ x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
1835
+ y2 = axisy.min;
1836
+ }
1837
+
1838
+ // clip with ymax
1839
+ if (y1 >= y2 && y1 > axisy.max) {
1840
+ if (y2 > axisy.max)
1841
+ continue;
1842
+ x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
1843
+ y1 = axisy.max;
1844
+ }
1845
+ else if (y2 >= y1 && y2 > axisy.max) {
1846
+ if (y1 > axisy.max)
1847
+ continue;
1848
+ x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
1849
+ y2 = axisy.max;
1850
+ }
1851
+
1852
+ // clip with xmin
1853
+ if (x1 <= x2 && x1 < axisx.min) {
1854
+ if (x2 < axisx.min)
1855
+ continue;
1856
+ y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
1857
+ x1 = axisx.min;
1858
+ }
1859
+ else if (x2 <= x1 && x2 < axisx.min) {
1860
+ if (x1 < axisx.min)
1861
+ continue;
1862
+ y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
1863
+ x2 = axisx.min;
1864
+ }
1865
+
1866
+ // clip with xmax
1867
+ if (x1 >= x2 && x1 > axisx.max) {
1868
+ if (x2 > axisx.max)
1869
+ continue;
1870
+ y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
1871
+ x1 = axisx.max;
1872
+ }
1873
+ else if (x2 >= x1 && x2 > axisx.max) {
1874
+ if (x1 > axisx.max)
1875
+ continue;
1876
+ y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
1877
+ x2 = axisx.max;
1878
+ }
1879
+
1880
+ if (x1 != prevx || y1 != prevy)
1881
+ ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
1882
+
1883
+ prevx = x2;
1884
+ prevy = y2;
1885
+ ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
1886
+ }
1887
+ ctx.stroke();
1888
+ }
1889
+
1890
+ function plotLineArea(datapoints, axisx, axisy) {
1891
+ var points = datapoints.points,
1892
+ ps = datapoints.pointsize,
1893
+ bottom = Math.min(Math.max(0, axisy.min), axisy.max),
1894
+ i = 0, top, areaOpen = false,
1895
+ ypos = 1, segmentStart = 0, segmentEnd = 0;
1896
+
1897
+ // we process each segment in two turns, first forward
1898
+ // direction to sketch out top, then once we hit the
1899
+ // end we go backwards to sketch the bottom
1900
+ while (true) {
1901
+ if (ps > 0 && i > points.length + ps)
1902
+ break;
1903
+
1904
+ i += ps; // ps is negative if going backwards
1905
+
1906
+ var x1 = points[i - ps],
1907
+ y1 = points[i - ps + ypos],
1908
+ x2 = points[i], y2 = points[i + ypos];
1909
+
1910
+ if (areaOpen) {
1911
+ if (ps > 0 && x1 != null && x2 == null) {
1912
+ // at turning point
1913
+ segmentEnd = i;
1914
+ ps = -ps;
1915
+ ypos = 2;
1916
+ continue;
1917
+ }
1918
+
1919
+ if (ps < 0 && i == segmentStart + ps) {
1920
+ // done with the reverse sweep
1921
+ ctx.fill();
1922
+ areaOpen = false;
1923
+ ps = -ps;
1924
+ ypos = 1;
1925
+ i = segmentStart = segmentEnd + ps;
1926
+ continue;
1927
+ }
1928
+ }
1929
+
1930
+ if (x1 == null || x2 == null)
1931
+ continue;
1932
+
1933
+ // clip x values
1934
+
1935
+ // clip with xmin
1936
+ if (x1 <= x2 && x1 < axisx.min) {
1937
+ if (x2 < axisx.min)
1938
+ continue;
1939
+ y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
1940
+ x1 = axisx.min;
1941
+ }
1942
+ else if (x2 <= x1 && x2 < axisx.min) {
1943
+ if (x1 < axisx.min)
1944
+ continue;
1945
+ y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
1946
+ x2 = axisx.min;
1947
+ }
1948
+
1949
+ // clip with xmax
1950
+ if (x1 >= x2 && x1 > axisx.max) {
1951
+ if (x2 > axisx.max)
1952
+ continue;
1953
+ y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
1954
+ x1 = axisx.max;
1955
+ }
1956
+ else if (x2 >= x1 && x2 > axisx.max) {
1957
+ if (x1 > axisx.max)
1958
+ continue;
1959
+ y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
1960
+ x2 = axisx.max;
1961
+ }
1962
+
1963
+ if (!areaOpen) {
1964
+ // open area
1965
+ ctx.beginPath();
1966
+ ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
1967
+ areaOpen = true;
1968
+ }
1969
+
1970
+ // now first check the case where both is outside
1971
+ if (y1 >= axisy.max && y2 >= axisy.max) {
1972
+ ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));
1973
+ ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));
1974
+ continue;
1975
+ }
1976
+ else if (y1 <= axisy.min && y2 <= axisy.min) {
1977
+ ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
1978
+ ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
1979
+ continue;
1980
+ }
1981
+
1982
+ // else it's a bit more complicated, there might
1983
+ // be a flat maxed out rectangle first, then a
1984
+ // triangular cutout or reverse; to find these
1985
+ // keep track of the current x values
1986
+ var x1old = x1, x2old = x2;
1987
+
1988
+ // clip the y values, without shortcutting, we
1989
+ // go through all cases in turn
1990
+
1991
+ // clip with ymin
1992
+ if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {
1993
+ x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
1994
+ y1 = axisy.min;
1995
+ }
1996
+ else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {
1997
+ x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
1998
+ y2 = axisy.min;
1999
+ }
2000
+
2001
+ // clip with ymax
2002
+ if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {
2003
+ x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
2004
+ y1 = axisy.max;
2005
+ }
2006
+ else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {
2007
+ x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
2008
+ y2 = axisy.max;
2009
+ }
2010
+
2011
+ // if the x value was changed we got a rectangle
2012
+ // to fill
2013
+ if (x1 != x1old) {
2014
+ ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1));
2015
+ // it goes to (x1, y1), but we fill that below
2016
+ }
2017
+
2018
+ // fill triangular section, this sometimes result
2019
+ // in redundant points if (x1, y1) hasn't changed
2020
+ // from previous line to, but we just ignore that
2021
+ ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));
2022
+ ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
2023
+
2024
+ // fill the other rectangle if it's there
2025
+ if (x2 != x2old) {
2026
+ ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
2027
+ ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2));
2028
+ }
2029
+ }
2030
+ }
2031
+
2032
+ ctx.save();
2033
+ ctx.translate(plotOffset.left, plotOffset.top);
2034
+ ctx.lineJoin = "round";
2035
+
2036
+ var lw = series.lines.lineWidth,
2037
+ sw = series.shadowSize;
2038
+ // FIXME: consider another form of shadow when filling is turned on
2039
+ if (lw > 0 && sw > 0) {
2040
+ // draw shadow as a thick and thin line with transparency
2041
+ ctx.lineWidth = sw;
2042
+ ctx.strokeStyle = "rgba(0,0,0,0.1)";
2043
+ // position shadow at angle from the mid of line
2044
+ var angle = Math.PI/18;
2045
+ plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis);
2046
+ ctx.lineWidth = sw/2;
2047
+ plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis);
2048
+ }
2049
+
2050
+ ctx.lineWidth = lw;
2051
+ ctx.strokeStyle = series.color;
2052
+ var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight);
2053
+ if (fillStyle) {
2054
+ ctx.fillStyle = fillStyle;
2055
+ plotLineArea(series.datapoints, series.xaxis, series.yaxis);
2056
+ }
2057
+
2058
+ if (lw > 0)
2059
+ plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis);
2060
+ ctx.restore();
2061
+ }
2062
+
2063
+ function drawSeriesPoints(series) {
2064
+ function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) {
2065
+ var points = datapoints.points, ps = datapoints.pointsize;
2066
+
2067
+ for (var i = 0; i < points.length; i += ps) {
2068
+ var x = points[i], y = points[i + 1];
2069
+ if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
2070
+ continue;
2071
+
2072
+ ctx.beginPath();
2073
+ x = axisx.p2c(x);
2074
+ y = axisy.p2c(y) + offset;
2075
+ if (symbol == "circle")
2076
+ ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);
2077
+ else
2078
+ symbol(ctx, x, y, radius, shadow);
2079
+ ctx.closePath();
2080
+
2081
+ if (fillStyle) {
2082
+ ctx.fillStyle = fillStyle;
2083
+ ctx.fill();
2084
+ }
2085
+ ctx.stroke();
2086
+ }
2087
+ }
2088
+
2089
+ ctx.save();
2090
+ ctx.translate(plotOffset.left, plotOffset.top);
2091
+
2092
+ var lw = series.points.lineWidth,
2093
+ sw = series.shadowSize,
2094
+ radius = series.points.radius,
2095
+ symbol = series.points.symbol;
2096
+ if (lw > 0 && sw > 0) {
2097
+ // draw shadow in two steps
2098
+ var w = sw / 2;
2099
+ ctx.lineWidth = w;
2100
+ ctx.strokeStyle = "rgba(0,0,0,0.1)";
2101
+ plotPoints(series.datapoints, radius, null, w + w/2, true,
2102
+ series.xaxis, series.yaxis, symbol);
2103
+
2104
+ ctx.strokeStyle = "rgba(0,0,0,0.2)";
2105
+ plotPoints(series.datapoints, radius, null, w/2, true,
2106
+ series.xaxis, series.yaxis, symbol);
2107
+ }
2108
+
2109
+ ctx.lineWidth = lw;
2110
+ ctx.strokeStyle = series.color;
2111
+ plotPoints(series.datapoints, radius,
2112
+ getFillStyle(series.points, series.color), 0, false,
2113
+ series.xaxis, series.yaxis, symbol);
2114
+ ctx.restore();
2115
+ }
2116
+
2117
+ function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) {
2118
+ var left, right, bottom, top,
2119
+ drawLeft, drawRight, drawTop, drawBottom,
2120
+ tmp;
2121
+
2122
+ // in horizontal mode, we start the bar from the left
2123
+ // instead of from the bottom so it appears to be
2124
+ // horizontal rather than vertical
2125
+ if (horizontal) {
2126
+ drawBottom = drawRight = drawTop = true;
2127
+ drawLeft = false;
2128
+ left = b;
2129
+ right = x;
2130
+ top = y + barLeft;
2131
+ bottom = y + barRight;
2132
+
2133
+ // account for negative bars
2134
+ if (right < left) {
2135
+ tmp = right;
2136
+ right = left;
2137
+ left = tmp;
2138
+ drawLeft = true;
2139
+ drawRight = false;
2140
+ }
2141
+ }
2142
+ else {
2143
+ drawLeft = drawRight = drawTop = true;
2144
+ drawBottom = false;
2145
+ left = x + barLeft;
2146
+ right = x + barRight;
2147
+ bottom = b;
2148
+ top = y;
2149
+
2150
+ // account for negative bars
2151
+ if (top < bottom) {
2152
+ tmp = top;
2153
+ top = bottom;
2154
+ bottom = tmp;
2155
+ drawBottom = true;
2156
+ drawTop = false;
2157
+ }
2158
+ }
2159
+
2160
+ // clip
2161
+ if (right < axisx.min || left > axisx.max ||
2162
+ top < axisy.min || bottom > axisy.max)
2163
+ return;
2164
+
2165
+ if (left < axisx.min) {
2166
+ left = axisx.min;
2167
+ drawLeft = false;
2168
+ }
2169
+
2170
+ if (right > axisx.max) {
2171
+ right = axisx.max;
2172
+ drawRight = false;
2173
+ }
2174
+
2175
+ if (bottom < axisy.min) {
2176
+ bottom = axisy.min;
2177
+ drawBottom = false;
2178
+ }
2179
+
2180
+ if (top > axisy.max) {
2181
+ top = axisy.max;
2182
+ drawTop = false;
2183
+ }
2184
+
2185
+ left = axisx.p2c(left);
2186
+ bottom = axisy.p2c(bottom);
2187
+ right = axisx.p2c(right);
2188
+ top = axisy.p2c(top);
2189
+
2190
+ // fill the bar
2191
+ if (fillStyleCallback) {
2192
+ c.beginPath();
2193
+ c.moveTo(left, bottom);
2194
+ c.lineTo(left, top);
2195
+ c.lineTo(right, top);
2196
+ c.lineTo(right, bottom);
2197
+ c.fillStyle = fillStyleCallback(bottom, top);
2198
+ c.fill();
2199
+ }
2200
+
2201
+ // draw outline
2202
+ if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) {
2203
+ c.beginPath();
2204
+
2205
+ // FIXME: inline moveTo is buggy with excanvas
2206
+ c.moveTo(left, bottom + offset);
2207
+ if (drawLeft)
2208
+ c.lineTo(left, top + offset);
2209
+ else
2210
+ c.moveTo(left, top + offset);
2211
+ if (drawTop)
2212
+ c.lineTo(right, top + offset);
2213
+ else
2214
+ c.moveTo(right, top + offset);
2215
+ if (drawRight)
2216
+ c.lineTo(right, bottom + offset);
2217
+ else
2218
+ c.moveTo(right, bottom + offset);
2219
+ if (drawBottom)
2220
+ c.lineTo(left, bottom + offset);
2221
+ else
2222
+ c.moveTo(left, bottom + offset);
2223
+ c.stroke();
2224
+ }
2225
+ }
2226
+
2227
+ function drawSeriesBars(series) {
2228
+ function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) {
2229
+ var points = datapoints.points, ps = datapoints.pointsize;
2230
+
2231
+ for (var i = 0; i < points.length; i += ps) {
2232
+ if (points[i] == null)
2233
+ continue;
2234
+ drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth);
2235
+ }
2236
+ }
2237
+
2238
+ ctx.save();
2239
+ ctx.translate(plotOffset.left, plotOffset.top);
2240
+
2241
+ // FIXME: figure out a way to add shadows (for instance along the right edge)
2242
+ ctx.lineWidth = series.bars.lineWidth;
2243
+ ctx.strokeStyle = series.color;
2244
+
2245
+ var barLeft;
2246
+
2247
+ switch (series.bars.align) {
2248
+ case "left":
2249
+ barLeft = 0;
2250
+ break;
2251
+ case "right":
2252
+ barLeft = -series.bars.barWidth;
2253
+ break;
2254
+ case "center":
2255
+ barLeft = -series.bars.barWidth / 2;
2256
+ break;
2257
+ default:
2258
+ throw new Error("Invalid bar alignment: " + series.bars.align);
2259
+ }
2260
+
2261
+ var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null;
2262
+ plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis);
2263
+ ctx.restore();
2264
+ }
2265
+
2266
+ function getFillStyle(filloptions, seriesColor, bottom, top) {
2267
+ var fill = filloptions.fill;
2268
+ if (!fill)
2269
+ return null;
2270
+
2271
+ if (filloptions.fillColor)
2272
+ return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
2273
+
2274
+ var c = $.color.parse(seriesColor);
2275
+ c.a = typeof fill == "number" ? fill : 0.4;
2276
+ c.normalize();
2277
+ return c.toString();
2278
+ }
2279
+
2280
+ function insertLegend() {
2281
+
2282
+ placeholder.find(".legend").remove();
2283
+
2284
+ if (!options.legend.show)
2285
+ return;
2286
+
2287
+ var fragments = [], entries = [], rowStarted = false,
2288
+ lf = options.legend.labelFormatter, s, label;
2289
+
2290
+ // Build a list of legend entries, with each having a label and a color
2291
+
2292
+ for (var i = 0; i < series.length; ++i) {
2293
+ s = series[i];
2294
+ if (s.label) {
2295
+ label = lf ? lf(s.label, s) : s.label;
2296
+ if (label) {
2297
+ entries.push({
2298
+ label: label,
2299
+ color: s.color
2300
+ });
2301
+ }
2302
+ }
2303
+ }
2304
+
2305
+ // Sort the legend using either the default or a custom comparator
2306
+
2307
+ if (options.legend.sorted) {
2308
+ if ($.isFunction(options.legend.sorted)) {
2309
+ entries.sort(options.legend.sorted);
2310
+ } else {
2311
+ var ascending = options.legend.sorted != "descending";
2312
+ entries.sort(function(a, b) {
2313
+ return a.label == b.label ? 0 : (
2314
+ (a.label < b.label) != ascending ? 1 : -1 // Logical XOR
2315
+ );
2316
+ });
2317
+ }
2318
+ }
2319
+
2320
+ // Generate markup for the list of entries, in their final order
2321
+
2322
+ for (var i = 0; i < entries.length; ++i) {
2323
+
2324
+ var entry = entries[i];
2325
+
2326
+ if (i % options.legend.noColumns == 0) {
2327
+ if (rowStarted)
2328
+ fragments.push('</tr>');
2329
+ fragments.push('<tr>');
2330
+ rowStarted = true;
2331
+ }
2332
+
2333
+ fragments.push(
2334
+ '<td class="legendColorBox"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:4px;height:0;border:5px solid ' + entry.color + ';overflow:hidden"></div></div></td>' +
2335
+ '<td class="legendLabel">' + entry.label + '</td>'
2336
+ );
2337
+ }
2338
+
2339
+ if (rowStarted)
2340
+ fragments.push('</tr>');
2341
+
2342
+ if (fragments.length == 0)
2343
+ return;
2344
+
2345
+ var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>';
2346
+ if (options.legend.container != null)
2347
+ var legend = $(options.legend.container).html(table);
2348
+ else {
2349
+ var pos = "",
2350
+ p = options.legend.position,
2351
+ m = options.legend.margin;
2352
+ if (m[0] == null)
2353
+ m = [m, m];
2354
+ if (p.charAt(0) == "n")
2355
+ pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
2356
+ else if (p.charAt(0) == "s")
2357
+ pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
2358
+ if (p.charAt(1) == "e")
2359
+ pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
2360
+ else if (p.charAt(1) == "w")
2361
+ pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
2362
+ var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder);
2363
+ if (options.legend.backgroundOpacity != 0.0) {
2364
+ // put in the transparent background
2365
+ // separately to avoid blended labels and
2366
+ // label boxes
2367
+ var c = options.legend.backgroundColor;
2368
+ if (c == null) {
2369
+ c = options.grid.backgroundColor;
2370
+ if (c && typeof c == "string")
2371
+ c = $.color.parse(c);
2372
+ else
2373
+ c = $.color.extract(legend, 'background-color');
2374
+ c.a = 1;
2375
+ c = c.toString();
2376
+ }
2377
+ var div = legend.children();
2378
+ $('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity);
2379
+ }
2380
+
2381
+ executeHooks(hooks.legendInserted, [legend]);
2382
+ }
2383
+
2384
+ executeHooks(hooks.legendInserted, [legend]);
2385
+ }
2386
+
2387
+
2388
+ // interactive features
2389
+
2390
+ var highlights = [],
2391
+ redrawTimeout = null;
2392
+
2393
+ // returns the data item the mouse is over, or null if none is found
2394
+ function findNearbyItem(mouseX, mouseY, seriesFilter) {
2395
+ var maxDistance = options.grid.mouseActiveRadius,
2396
+ smallestDistance = maxDistance * maxDistance + 1,
2397
+ item = null, foundPoint = false, i, j, ps;
2398
+
2399
+ for (i = series.length - 1; i >= 0; --i) {
2400
+ if (!seriesFilter(series[i]))
2401
+ continue;
2402
+
2403
+ var s = series[i],
2404
+ axisx = s.xaxis,
2405
+ axisy = s.yaxis,
2406
+ points = s.datapoints.points,
2407
+ mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster
2408
+ my = axisy.c2p(mouseY),
2409
+ maxx = maxDistance / axisx.scale,
2410
+ maxy = maxDistance / axisy.scale;
2411
+
2412
+ ps = s.datapoints.pointsize;
2413
+ // with inverse transforms, we can't use the maxx/maxy
2414
+ // optimization, sadly
2415
+ if (axisx.options.inverseTransform)
2416
+ maxx = Number.MAX_VALUE;
2417
+ if (axisy.options.inverseTransform)
2418
+ maxy = Number.MAX_VALUE;
2419
+
2420
+ if (s.lines.show || s.points.show) {
2421
+ for (j = 0; j < points.length; j += ps) {
2422
+ var x = points[j], y = points[j + 1];
2423
+ if (x == null)
2424
+ continue;
2425
+
2426
+ // For points and lines, the cursor must be within a
2427
+ // certain distance to the data point
2428
+ if (x - mx > maxx || x - mx < -maxx ||
2429
+ y - my > maxy || y - my < -maxy)
2430
+ continue;
2431
+
2432
+ // We have to calculate distances in pixels, not in
2433
+ // data units, because the scales of the axes may be different
2434
+ var dx = Math.abs(axisx.p2c(x) - mouseX),
2435
+ dy = Math.abs(axisy.p2c(y) - mouseY),
2436
+ dist = dx * dx + dy * dy; // we save the sqrt
2437
+
2438
+ // use <= to ensure last point takes precedence
2439
+ // (last generally means on top of)
2440
+ if (dist < smallestDistance) {
2441
+ smallestDistance = dist;
2442
+ item = [i, j / ps];
2443
+ }
2444
+ }
2445
+ }
2446
+
2447
+ if (s.bars.show && !item) { // no other point can be nearby
2448
+ var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2,
2449
+ barRight = barLeft + s.bars.barWidth;
2450
+
2451
+ for (j = 0; j < points.length; j += ps) {
2452
+ var x = points[j], y = points[j + 1], b = points[j + 2];
2453
+ if (x == null)
2454
+ continue;
2455
+
2456
+ // for a bar graph, the cursor must be inside the bar
2457
+ if (series[i].bars.horizontal ?
2458
+ (mx <= Math.max(b, x) && mx >= Math.min(b, x) &&
2459
+ my >= y + barLeft && my <= y + barRight) :
2460
+ (mx >= x + barLeft && mx <= x + barRight &&
2461
+ my >= Math.min(b, y) && my <= Math.max(b, y)))
2462
+ item = [i, j / ps];
2463
+ }
2464
+ }
2465
+ }
2466
+
2467
+ if (item) {
2468
+ i = item[0];
2469
+ j = item[1];
2470
+ ps = series[i].datapoints.pointsize;
2471
+
2472
+ return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),
2473
+ dataIndex: j,
2474
+ series: series[i],
2475
+ seriesIndex: i };
2476
+ }
2477
+
2478
+ return null;
2479
+ }
2480
+
2481
+ function onMouseMove(e) {
2482
+ if (options.grid.hoverable)
2483
+ triggerClickHoverEvent("plothover", e,
2484
+ function (s) { return s["hoverable"] != false; });
2485
+ }
2486
+
2487
+ function onMouseLeave(e) {
2488
+ if (options.grid.hoverable)
2489
+ triggerClickHoverEvent("plothover", e,
2490
+ function (s) { return false; });
2491
+ }
2492
+
2493
+ function onClick(e) {
2494
+ triggerClickHoverEvent("plotclick", e,
2495
+ function (s) { return s["clickable"] != false; });
2496
+ }
2497
+
2498
+ // trigger click or hover event (they send the same parameters
2499
+ // so we share their code)
2500
+ function triggerClickHoverEvent(eventname, event, seriesFilter) {
2501
+ var offset = eventHolder.offset(),
2502
+ canvasX = event.pageX - offset.left - plotOffset.left,
2503
+ canvasY = event.pageY - offset.top - plotOffset.top,
2504
+ pos = canvasToAxisCoords({ left: canvasX, top: canvasY });
2505
+
2506
+ pos.pageX = event.pageX;
2507
+ pos.pageY = event.pageY;
2508
+
2509
+ var item = findNearbyItem(canvasX, canvasY, seriesFilter);
2510
+
2511
+ if (item) {
2512
+ // fill in mouse pos for any listeners out there
2513
+ item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10);
2514
+ item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10);
2515
+ }
2516
+
2517
+ if (options.grid.autoHighlight) {
2518
+ // clear auto-highlights
2519
+ for (var i = 0; i < highlights.length; ++i) {
2520
+ var h = highlights[i];
2521
+ if (h.auto == eventname &&
2522
+ !(item && h.series == item.series &&
2523
+ h.point[0] == item.datapoint[0] &&
2524
+ h.point[1] == item.datapoint[1]))
2525
+ unhighlight(h.series, h.point);
2526
+ }
2527
+
2528
+ if (item)
2529
+ highlight(item.series, item.datapoint, eventname);
2530
+ }
2531
+
2532
+ placeholder.trigger(eventname, [ pos, item ]);
2533
+ }
2534
+
2535
+ function triggerRedrawOverlay() {
2536
+ var t = options.interaction.redrawOverlayInterval;
2537
+ if (t == -1) { // skip event queue
2538
+ drawOverlay();
2539
+ return;
2540
+ }
2541
+
2542
+ if (!redrawTimeout)
2543
+ redrawTimeout = setTimeout(drawOverlay, t);
2544
+ }
2545
+
2546
+ function drawOverlay() {
2547
+ redrawTimeout = null;
2548
+
2549
+ // draw highlights
2550
+ octx.save();
2551
+ octx.clearRect(0, 0, canvasWidth, canvasHeight);
2552
+ octx.translate(plotOffset.left, plotOffset.top);
2553
+
2554
+ var i, hi;
2555
+ for (i = 0; i < highlights.length; ++i) {
2556
+ hi = highlights[i];
2557
+
2558
+ if (hi.series.bars.show)
2559
+ drawBarHighlight(hi.series, hi.point);
2560
+ else
2561
+ drawPointHighlight(hi.series, hi.point);
2562
+ }
2563
+ octx.restore();
2564
+
2565
+ executeHooks(hooks.drawOverlay, [octx]);
2566
+ }
2567
+
2568
+ function highlight(s, point, auto) {
2569
+ if (typeof s == "number")
2570
+ s = series[s];
2571
+
2572
+ if (typeof point == "number") {
2573
+ var ps = s.datapoints.pointsize;
2574
+ point = s.datapoints.points.slice(ps * point, ps * (point + 1));
2575
+ }
2576
+
2577
+ var i = indexOfHighlight(s, point);
2578
+ if (i == -1) {
2579
+ highlights.push({ series: s, point: point, auto: auto });
2580
+
2581
+ triggerRedrawOverlay();
2582
+ }
2583
+ else if (!auto)
2584
+ highlights[i].auto = false;
2585
+ }
2586
+
2587
+ function unhighlight(s, point) {
2588
+ if (s == null && point == null) {
2589
+ highlights = [];
2590
+ triggerRedrawOverlay();
2591
+ }
2592
+
2593
+ if (typeof s == "number")
2594
+ s = series[s];
2595
+
2596
+ if (typeof point == "number")
2597
+ point = s.data[point];
2598
+
2599
+ var i = indexOfHighlight(s, point);
2600
+ if (i != -1) {
2601
+ highlights.splice(i, 1);
2602
+
2603
+ triggerRedrawOverlay();
2604
+ }
2605
+ }
2606
+
2607
+ function indexOfHighlight(s, p) {
2608
+ for (var i = 0; i < highlights.length; ++i) {
2609
+ var h = highlights[i];
2610
+ if (h.series == s && h.point[0] == p[0]
2611
+ && h.point[1] == p[1])
2612
+ return i;
2613
+ }
2614
+ return -1;
2615
+ }
2616
+
2617
+ function drawPointHighlight(series, point) {
2618
+ var x = point[0], y = point[1],
2619
+ axisx = series.xaxis, axisy = series.yaxis,
2620
+ highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString();
2621
+
2622
+ if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
2623
+ return;
2624
+
2625
+ var pointRadius = series.points.radius + series.points.lineWidth / 2;
2626
+ octx.lineWidth = pointRadius;
2627
+ octx.strokeStyle = highlightColor;
2628
+ var radius = 1.5 * pointRadius;
2629
+ x = axisx.p2c(x);
2630
+ y = axisy.p2c(y);
2631
+
2632
+ octx.beginPath();
2633
+ if (series.points.symbol == "circle")
2634
+ octx.arc(x, y, radius, 0, 2 * Math.PI, false);
2635
+ else
2636
+ series.points.symbol(octx, x, y, radius, false);
2637
+ octx.closePath();
2638
+ octx.stroke();
2639
+ }
2640
+
2641
+ function drawBarHighlight(series, point) {
2642
+ var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(),
2643
+ fillStyle = highlightColor,
2644
+ barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
2645
+
2646
+ octx.lineWidth = series.bars.lineWidth;
2647
+ octx.strokeStyle = highlightColor;
2648
+
2649
+ drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth,
2650
+ 0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth);
2651
+ }
2652
+
2653
+ function getColorOrGradient(spec, bottom, top, defaultColor) {
2654
+ if (typeof spec == "string")
2655
+ return spec;
2656
+ else {
2657
+ // assume this is a gradient spec; IE currently only
2658
+ // supports a simple vertical gradient properly, so that's
2659
+ // what we support too
2660
+ var gradient = ctx.createLinearGradient(0, top, 0, bottom);
2661
+
2662
+ for (var i = 0, l = spec.colors.length; i < l; ++i) {
2663
+ var c = spec.colors[i];
2664
+ if (typeof c != "string") {
2665
+ var co = $.color.parse(defaultColor);
2666
+ if (c.brightness != null)
2667
+ co = co.scale('rgb', c.brightness);
2668
+ if (c.opacity != null)
2669
+ co.a *= c.opacity;
2670
+ c = co.toString();
2671
+ }
2672
+ gradient.addColorStop(i / (l - 1), c);
2673
+ }
2674
+
2675
+ return gradient;
2676
+ }
2677
+ }
2678
+ }
2679
+
2680
+ $.plot = function(placeholder, data, options) {
2681
+ //var t0 = new Date();
2682
+ var plot = new Plot($(placeholder), data, options, $.plot.plugins);
2683
+ //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime()));
2684
+ return plot;
2685
+ };
2686
+
2687
+ $.plot.version = "0.8-alpha";
2688
+
2689
+ $.plot.plugins = [];
2690
+
2691
+ // round to nearby lower multiple of base
2692
+ function floorInBase(n, base) {
2693
+ return base * Math.floor(n / base);
2694
+ }
2695
+
2696
+ })(jQuery);
view/js/jquery.flot.symbol.js ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Flot plugin that adds some extra symbols for plotting points.
2
+
3
+ Copyright (c) 2007-2013 IOLA and Ole Laursen.
4
+ Licensed under the MIT license.
5
+
6
+ The symbols are accessed as strings through the standard symbol options:
7
+
8
+ series: {
9
+ points: {
10
+ symbol: "square" // or "diamond", "triangle", "cross"
11
+ }
12
+ }
13
+
14
+ */
15
+
16
+ (function ($) {
17
+ function processRawData(plot, series, datapoints) {
18
+ // we normalize the area of each symbol so it is approximately the
19
+ // same as a circle of the given radius
20
+
21
+ var handlers = {
22
+ square: function (ctx, x, y, radius, shadow) {
23
+ // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2
24
+ var size = radius * Math.sqrt(Math.PI) / 2;
25
+ ctx.rect(x - size, y - size, size + size, size + size);
26
+ },
27
+ diamond: function (ctx, x, y, radius, shadow) {
28
+ // pi * r^2 = 2s^2 => s = r * sqrt(pi/2)
29
+ var size = radius * Math.sqrt(Math.PI / 2);
30
+ ctx.moveTo(x - size, y);
31
+ ctx.lineTo(x, y - size);
32
+ ctx.lineTo(x + size, y);
33
+ ctx.lineTo(x, y + size);
34
+ ctx.lineTo(x - size, y);
35
+ },
36
+ triangle: function (ctx, x, y, radius, shadow) {
37
+ // pi * r^2 = 1/2 * s^2 * sin (pi / 3) => s = r * sqrt(2 * pi / sin(pi / 3))
38
+ var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3));
39
+ var height = size * Math.sin(Math.PI / 3);
40
+ ctx.moveTo(x - size/2, y + height/2);
41
+ ctx.lineTo(x + size/2, y + height/2);
42
+ if (!shadow) {
43
+ ctx.lineTo(x, y - height/2);
44
+ ctx.lineTo(x - size/2, y + height/2);
45
+ }
46
+ },
47
+ cross: function (ctx, x, y, radius, shadow) {
48
+ // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2
49
+ var size = radius * Math.sqrt(Math.PI) / 2;
50
+ ctx.moveTo(x - size, y - size);
51
+ ctx.lineTo(x + size, y + size);
52
+ ctx.moveTo(x - size, y + size);
53
+ ctx.lineTo(x + size, y - size);
54
+ }
55
+ };
56
+
57
+ var s = series.points.symbol;
58
+ if (handlers[s])
59
+ series.points.symbol = handlers[s];
60
+ }
61
+
62
+ function init(plot) {
63
+ plot.hooks.processDatapoints.push(processRawData);
64
+ }
65
+
66
+ $.plot.plugins.push({
67
+ init: init,
68
+ name: 'symbols',
69
+ version: '1.0'
70
+ });
71
+ })(jQuery);
view/js/jquery.flot.time.js ADDED
@@ -0,0 +1,431 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Pretty handling of time axes.
2
+
3
+ Copyright (c) 2007-2013 IOLA and Ole Laursen.
4
+ Licensed under the MIT license.
5
+
6
+ Set axis.mode to "time" to enable. See the section "Time series data" in
7
+ API.txt for details.
8
+
9
+ */
10
+
11
+ (function($) {
12
+
13
+ var options = {
14
+ xaxis: {
15
+ timezone: null, // "browser" for local to the client or timezone for timezone-js
16
+ timeformat: null, // format string to use
17
+ twelveHourClock: false, // 12 or 24 time in time mode
18
+ monthNames: null // list of names of months
19
+ }
20
+ };
21
+
22
+ // round to nearby lower multiple of base
23
+
24
+ function floorInBase(n, base) {
25
+ return base * Math.floor(n / base);
26
+ }
27
+
28
+ // Returns a string with the date d formatted according to fmt.
29
+ // A subset of the Open Group's strftime format is supported.
30
+
31
+ function formatDate(d, fmt, monthNames, dayNames) {
32
+
33
+ if (typeof d.strftime == "function") {
34
+ return d.strftime(fmt);
35
+ }
36
+
37
+ var leftPad = function(n, pad) {
38
+ n = "" + n;
39
+ pad = "" + (pad == null ? "0" : pad);
40
+ return n.length == 1 ? pad + n : n;
41
+ };
42
+
43
+ var r = [];
44
+ var escape = false;
45
+ var hours = d.getHours();
46
+ var isAM = hours < 12;
47
+
48
+ if (monthNames == null) {
49
+ monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
50
+ }
51
+
52
+ if (dayNames == null) {
53
+ dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
54
+ }
55
+
56
+ var hours12;
57
+
58
+ if (hours > 12) {
59
+ hours12 = hours - 12;
60
+ } else if (hours == 0) {
61
+ hours12 = 12;
62
+ } else {
63
+ hours12 = hours;
64
+ }
65
+
66
+ for (var i = 0; i < fmt.length; ++i) {
67
+
68
+ var c = fmt.charAt(i);
69
+
70
+ if (escape) {
71
+ switch (c) {
72
+ case 'a': c = "" + dayNames[d.getDay()]; break;
73
+ case 'b': c = "" + monthNames[d.getMonth()]; break;
74
+ case 'd': c = leftPad(d.getDate()); break;
75
+ case 'e': c = leftPad(d.getDate(), " "); break;
76
+ case 'h': // For back-compat with 0.7; remove in 1.0
77
+ case 'H': c = leftPad(hours); break;
78
+ case 'I': c = leftPad(hours12); break;
79
+ case 'l': c = leftPad(hours12, " "); break;
80
+ case 'm': c = leftPad(d.getMonth() + 1); break;
81
+ case 'M': c = leftPad(d.getMinutes()); break;
82
+ // quarters not in Open Group's strftime specification
83
+ case 'q':
84
+ c = "" + (Math.floor(d.getMonth() / 3) + 1); break;
85
+ case 'S': c = leftPad(d.getSeconds()); break;
86
+ case 'y': c = leftPad(d.getFullYear() % 100); break;
87
+ case 'Y': c = "" + d.getFullYear(); break;
88
+ case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
89
+ case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
90
+ case 'w': c = "" + d.getDay(); break;
91
+ }
92
+ r.push(c);
93
+ escape = false;
94
+ } else {
95
+ if (c == "%") {
96
+ escape = true;
97
+ } else {
98
+ r.push(c);
99
+ }
100
+ }
101
+ }
102
+
103
+ return r.join("");
104
+ }
105
+
106
+ // To have a consistent view of time-based data independent of which time
107
+ // zone the client happens to be in we need a date-like object independent
108
+ // of time zones. This is done through a wrapper that only calls the UTC
109
+ // versions of the accessor methods.
110
+
111
+ function makeUtcWrapper(d) {
112
+
113
+ function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) {
114
+ sourceObj[sourceMethod] = function() {
115
+ return targetObj[targetMethod].apply(targetObj, arguments);
116
+ };
117
+ };
118
+
119
+ var utc = {
120
+ date: d
121
+ };
122
+
123
+ // support strftime, if found
124
+
125
+ if (d.strftime != undefined) {
126
+ addProxyMethod(utc, "strftime", d, "strftime");
127
+ }
128
+
129
+ addProxyMethod(utc, "getTime", d, "getTime");
130
+ addProxyMethod(utc, "setTime", d, "setTime");
131
+
132
+ var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"];
133
+
134
+ for (var p = 0; p < props.length; p++) {
135
+ addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]);
136
+ addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]);
137
+ }
138
+
139
+ return utc;
140
+ };
141
+
142
+ // select time zone strategy. This returns a date-like object tied to the
143
+ // desired timezone
144
+
145
+ function dateGenerator(ts, opts) {
146
+ if (opts.timezone == "browser") {
147
+ return new Date(ts);
148
+ } else if (!opts.timezone || opts.timezone == "utc") {
149
+ return makeUtcWrapper(new Date(ts));
150
+ } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") {
151
+ var d = new timezoneJS.Date();
152
+ // timezone-js is fickle, so be sure to set the time zone before
153
+ // setting the time.
154
+ d.setTimezone(opts.timezone);
155
+ d.setTime(ts);
156
+ return d;
157
+ } else {
158
+ return makeUtcWrapper(new Date(ts));
159
+ }
160
+ }
161
+
162
+ // map of app. size of time units in milliseconds
163
+
164
+ var timeUnitSize = {
165
+ "second": 1000,
166
+ "minute": 60 * 1000,
167
+ "hour": 60 * 60 * 1000,
168
+ "day": 24 * 60 * 60 * 1000,
169
+ "month": 30 * 24 * 60 * 60 * 1000,
170
+ "quarter": 3 * 30 * 24 * 60 * 60 * 1000,
171
+ "year": 365.2425 * 24 * 60 * 60 * 1000
172
+ };
173
+
174
+ // the allowed tick sizes, after 1 year we use
175
+ // an integer algorithm
176
+
177
+ var baseSpec = [
178
+ [1, "second"], [2, "second"], [5, "second"], [10, "second"],
179
+ [30, "second"],
180
+ [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
181
+ [30, "minute"],
182
+ [1, "hour"], [2, "hour"], [4, "hour"],
183
+ [8, "hour"], [12, "hour"],
184
+ [1, "day"], [2, "day"], [3, "day"],
185
+ [0.25, "month"], [0.5, "month"], [1, "month"],
186
+ [2, "month"]
187
+ ];
188
+
189
+ // we don't know which variant(s) we'll need yet, but generating both is
190
+ // cheap
191
+
192
+ var specMonths = baseSpec.concat([[3, "month"], [6, "month"],
193
+ [1, "year"]]);
194
+ var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"],
195
+ [1, "year"]]);
196
+
197
+ function init(plot) {
198
+ plot.hooks.processOptions.push(function (plot, options) {
199
+ $.each(plot.getAxes(), function(axisName, axis) {
200
+
201
+ var opts = axis.options;
202
+
203
+ if (opts.mode == "time") {
204
+ axis.tickGenerator = function(axis) {
205
+
206
+ var ticks = [];
207
+ var d = dateGenerator(axis.min, opts);
208
+ var minSize = 0;
209
+
210
+ // make quarter use a possibility if quarters are
211
+ // mentioned in either of these options
212
+
213
+ var spec = (opts.tickSize && opts.tickSize[1] ===
214
+ "quarter") ||
215
+ (opts.minTickSize && opts.minTickSize[1] ===
216
+ "quarter") ? specQuarters : specMonths;
217
+
218
+ if (opts.minTickSize != null) {
219
+ if (typeof opts.tickSize == "number") {
220
+ minSize = opts.tickSize;
221
+ } else {
222
+ minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]];
223
+ }
224
+ }
225
+
226
+ for (var i = 0; i < spec.length - 1; ++i) {
227
+ if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]]
228
+ + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
229
+ && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) {
230
+ break;
231
+ }
232
+ }
233
+
234
+ var size = spec[i][0];
235
+ var unit = spec[i][1];
236
+
237
+ // special-case the possibility of several years
238
+
239
+ if (unit == "year") {
240
+
241
+ // if given a minTickSize in years, just use it,
242
+ // ensuring that it's an integer
243
+
244
+ if (opts.minTickSize != null && opts.minTickSize[1] == "year") {
245
+ size = Math.floor(opts.minTickSize[0]);
246
+ } else {
247
+
248
+ var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10));
249
+ var norm = (axis.delta / timeUnitSize.year) / magn;
250
+
251
+ if (norm < 1.5) {
252
+ size = 1;
253
+ } else if (norm < 3) {
254
+ size = 2;
255
+ } else if (norm < 7.5) {
256
+ size = 5;
257
+ } else {
258
+ size = 10;
259
+ }
260
+
261
+ size *= magn;
262
+ }
263
+
264
+ // minimum size for years is 1
265
+
266
+ if (size < 1) {
267
+ size = 1;
268
+ }
269
+ }
270
+
271
+ axis.tickSize = opts.tickSize || [size, unit];
272
+ var tickSize = axis.tickSize[0];
273
+ unit = axis.tickSize[1];
274
+
275
+ var step = tickSize * timeUnitSize[unit];
276
+
277
+ if (unit == "second") {
278
+ d.setSeconds(floorInBase(d.getSeconds(), tickSize));
279
+ } else if (unit == "minute") {
280
+ d.setMinutes(floorInBase(d.getMinutes(), tickSize));
281
+ } else if (unit == "hour") {
282
+ d.setHours(floorInBase(d.getHours(), tickSize));
283
+ } else if (unit == "month") {
284
+ d.setMonth(floorInBase(d.getMonth(), tickSize));
285
+ } else if (unit == "quarter") {
286
+ d.setMonth(3 * floorInBase(d.getMonth() / 3,
287
+ tickSize));
288
+ } else if (unit == "year") {
289
+ d.setFullYear(floorInBase(d.getFullYear(), tickSize));
290
+ }
291
+
292
+ // reset smaller components
293
+
294
+ d.setMilliseconds(0);
295
+
296
+ if (step >= timeUnitSize.minute) {
297
+ d.setSeconds(0);
298
+ }
299
+ if (step >= timeUnitSize.hour) {
300
+ d.setMinutes(0);
301
+ }
302
+ if (step >= timeUnitSize.day) {
303
+ d.setHours(0);
304
+ }
305
+ if (step >= timeUnitSize.day * 4) {
306
+ d.setDate(1);
307
+ }
308
+ if (step >= timeUnitSize.month * 2) {
309
+ d.setMonth(floorInBase(d.getMonth(), 3));
310
+ }
311
+ if (step >= timeUnitSize.quarter * 2) {
312
+ d.setMonth(floorInBase(d.getMonth(), 6));
313
+ }
314
+ if (step >= timeUnitSize.year) {
315
+ d.setMonth(0);
316
+ }
317
+
318
+ var carry = 0;
319
+ var v = Number.NaN;
320
+ var prev;
321
+
322
+ do {
323
+
324
+ prev = v;
325
+ v = d.getTime();
326
+ ticks.push(v);
327
+
328
+ if (unit == "month" || unit == "quarter") {
329
+ if (tickSize < 1) {
330
+
331
+ // a bit complicated - we'll divide the
332
+ // month/quarter up but we need to take
333
+ // care of fractions so we don't end up in
334
+ // the middle of a day
335
+
336
+ d.setDate(1);
337
+ var start = d.getTime();
338
+ d.setMonth(d.getMonth() +
339
+ (unit == "quarter" ? 3 : 1));
340
+ var end = d.getTime();
341
+ d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
342
+ carry = d.getHours();
343
+ d.setHours(0);
344
+ } else {
345
+ d.setMonth(d.getMonth() +
346
+ tickSize * (unit == "quarter" ? 3 : 1));
347
+ }
348
+ } else if (unit == "year") {
349
+ d.setFullYear(d.getFullYear() + tickSize);
350
+ } else {
351
+ d.setTime(v + step);
352
+ }
353
+ } while (v < axis.max && v != prev);
354
+
355
+ return ticks;
356
+ };
357
+
358
+ axis.tickFormatter = function (v, axis) {
359
+
360
+ var d = dateGenerator(v, axis.options);
361
+
362
+ // first check global format
363
+
364
+ if (opts.timeformat != null) {
365
+ return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames);
366
+ }
367
+
368
+ // possibly use quarters if quarters are mentioned in
369
+ // any of these places
370
+
371
+ var useQuarters = (axis.options.tickSize &&
372
+ axis.options.tickSize[1] == "quarter") ||
373
+ (axis.options.minTickSize &&
374
+ axis.options.minTickSize[1] == "quarter");
375
+
376
+ var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
377
+ var span = axis.max - axis.min;
378
+ var suffix = (opts.twelveHourClock) ? " %p" : "";
379
+ var hourCode = (opts.twelveHourClock) ? "%I" : "%H";
380
+ var fmt;
381
+
382
+ if (t < timeUnitSize.minute) {
383
+ fmt = hourCode + ":%M:%S" + suffix;
384
+ } else if (t < timeUnitSize.day) {
385
+ if (span < 2 * timeUnitSize.day) {
386
+ fmt = hourCode + ":%M" + suffix;
387
+ } else {
388
+ fmt = "%b %d " + hourCode + ":%M" + suffix;
389
+ }
390
+ } else if (t < timeUnitSize.month) {
391
+ fmt = "%b %d";
392
+ } else if ((useQuarters && t < timeUnitSize.quarter) ||
393
+ (!useQuarters && t < timeUnitSize.year)) {
394
+ if (span < timeUnitSize.year) {
395
+ fmt = "%b";
396
+ } else {
397
+ fmt = "%b %Y";
398
+ }
399
+ } else if (useQuarters && t < timeUnitSize.year) {
400
+ if (span < timeUnitSize.year) {
401
+ fmt = "Q%q";
402
+ } else {
403
+ fmt = "Q%q %Y";
404
+ }
405
+ } else {
406
+ fmt = "%Y";
407
+ }
408
+
409
+ var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames);
410
+
411
+ return rt;
412
+ };
413
+ }
414
+ });
415
+ });
416
+ }
417
+
418
+ $.plot.plugins.push({
419
+ init: init,
420
+ options: options,
421
+ name: 'time',
422
+ version: '1.0'
423
+ });
424
+
425
+ // Time-axis support used to be in Flot core, which exposed the
426
+ // formatDate function on the plot object. Various plugins depend
427
+ // on the function, so we need to re-expose it here.
428
+
429
+ $.plot.formatDate = formatDate;
430
+
431
+ })(jQuery);
view/js/jquery.flot.togglelegend.js ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+
3
+ Allows series to be toggled using their entries in the chart legend.
4
+ Supports series groups.
5
+
6
+ TODO:
7
+ * Allow toggling to be disabled for individual series
8
+ * Disable visual feedback (usually so dev can implement their own)
9
+
10
+
11
+ */
12
+
13
+ (function ( $ ) {
14
+
15
+ var options = {
16
+ series: {
17
+ toggle: {
18
+ enabled: true
19
+ }
20
+ }
21
+ },
22
+ state = {
23
+ add: function ( plot, label ) {
24
+
25
+ var placeholder = $(plot.getPlaceholder()),
26
+ data = placeholder.data("togglestates");
27
+
28
+ if ( !$.isArray(data) ) {
29
+
30
+ data = [ ];
31
+
32
+ }
33
+
34
+ if ( $.inArray(label, data) === -1 ) {
35
+
36
+ data.push(label);
37
+
38
+ }
39
+
40
+ placeholder.data("togglestates", data);
41
+
42
+ },
43
+ remove: function ( plot, label ) {
44
+
45
+ var placeholder = $(plot.getPlaceholder()),
46
+ data = placeholder.data("togglestates");
47
+
48
+ if ( $.isArray(data) ) {
49
+
50
+ if ( $.inArray(label, data) > -1 ) {
51
+
52
+ data.splice($.inArray(label, data), 1);
53
+ placeholder.data("togglestates", data);
54
+
55
+ }
56
+
57
+ }
58
+
59
+ }
60
+ },
61
+ toggle = function ( el, plot, datasets ) {
62
+
63
+ var cell,
64
+ label,
65
+ swatch,
66
+ isCell = el.is("td");
67
+
68
+ if ( isCell || (el.parents("td").length) ) {
69
+
70
+ cell = ( isCell ? el : el.parents("td") );
71
+
72
+ // Acquire the label and colour swatch of whatever
73
+ // legend item the user just clicked.
74
+ if ( cell.hasClass("legendLabel") ) {
75
+
76
+ label = cell;
77
+ swatch = cell.prev(".legendColorBox");
78
+
79
+ } else {
80
+
81
+ label = cell.next(".legendLabel");
82
+ swatch = cell;
83
+
84
+ }
85
+
86
+ var series = getSeries(label.text(), datasets);
87
+
88
+ if ( series.toggle.enabled ) {
89
+
90
+ if ( label.hasClass("flotSeriesHidden") ) {
91
+
92
+ label.removeClass("flotSeriesHidden");
93
+ toggleSwatch(swatch, true);
94
+ showSeries(label.text(), plot, datasets);
95
+
96
+ } else {
97
+
98
+ label.addClass("flotSeriesHidden");
99
+ toggleSwatch(swatch);
100
+ hideSeries(label.text(), plot, datasets);
101
+
102
+ }
103
+
104
+ }
105
+
106
+ }
107
+
108
+ },
109
+ setupSwatch = function ( swatch ) {
110
+
111
+ swatch.data("flotcolor", swatch.find("div div").css("border-top-color"));
112
+
113
+ },
114
+ toggleSwatch = function ( swatch, show ) {
115
+
116
+ if ( show ) {
117
+
118
+ swatch.find("div div").css("border-color", swatch.data("flotcolor"));
119
+
120
+ } else {
121
+
122
+ swatch.find("div div").css("border-color", "transparent");
123
+
124
+ }
125
+
126
+ },
127
+ redraw = function ( plot, datasets ) {
128
+
129
+ plot.setData(datasets.visible);
130
+ plot.draw();
131
+
132
+ },
133
+ getSeries = function ( label, datasets ) {
134
+
135
+ for ( var i = 0; i < datasets.all.length; i++ ) {
136
+
137
+ if ( datasets.all[i].label === label ) {
138
+
139
+ return datasets.all[i];
140
+
141
+ }
142
+
143
+ }
144
+
145
+ },
146
+ hideSeries = function ( label, plot, datasets ) {
147
+
148
+ for ( var i = 0; i < datasets.visible.length; i++ ) {
149
+
150
+ if ( datasets.visible[i].label === label ) {
151
+
152
+ // Hide this series
153
+ datasets.visible.splice(i, 1);
154
+ state.add(plot, label);
155
+ break;
156
+
157
+ }
158
+
159
+ }
160
+
161
+ redraw(plot, datasets);
162
+
163
+ },
164
+ showAll = function ( ) {
165
+ plot.setData(datasets.all);
166
+ },
167
+ showSeries = function ( label, plot, datasets ) {
168
+
169
+ var i, j,
170
+ outDataset = [];
171
+
172
+ // Find the series we want to show
173
+ for ( var i = 0; i < datasets.all.length; i++ ) {
174
+
175
+ if ( datasets.all[i].label === label ) {
176
+
177
+ datasets.visible.push(datasets.all[i]);
178
+
179
+ }
180
+
181
+ }
182
+
183
+ // Sometimes the order of items in the datasets array is important
184
+ // (especially when lines or areas overlap one another)
185
+ for ( i = 0; i < datasets.all.length; i++ ) {
186
+
187
+ for ( j = 0; j < datasets.visible.length; j++ ) {
188
+
189
+ if ( datasets.all[i].label === datasets.visible[j].label ) {
190
+
191
+ outDataset.push(datasets.all[i]);
192
+ state.remove(plot, label);
193
+ break;
194
+
195
+ }
196
+
197
+ }
198
+
199
+ }
200
+
201
+ datasets.visible = outDataset;
202
+
203
+ redraw(plot, datasets);
204
+
205
+ },
206
+ init = function ( _plot ) {
207
+
208
+ var datasets = { },
209
+ initDraw = false,
210
+ legend;
211
+
212
+ _plot.hooks.draw.push(function ( _plot ) {
213
+
214
+ var placeholder, toggleStates, lenToggleStates, i;
215
+
216
+ if ( !initDraw ) {
217
+
218
+ placeholder = $(_plot.getPlaceholder());
219
+ toggleStates = [ ];
220
+
221
+ // This stops the calls to draw from creating an infinite loop
222
+ initDraw = true;
223
+
224
+
225
+ // Look for an existing toggleLegend config
226
+ if ( $.isArray(placeholder.data("togglestates")) ) {
227
+
228
+ toggleStates = placeholder.data("togglestates");
229
+
230
+ lenToggleStates = toggleStates.length;
231
+
232
+ // Initialise the line states
233
+ for ( i = 0; i < lenToggleStates; i++ ) {
234
+
235
+ //hideSeries(toggleStates[i], _plot, datasets);
236
+ // Find the corresponding legend entry and click it!
237
+ // (Yucky!)
238
+ toggle(legend.find("td").filter(function ( ) {
239
+
240
+ return $(this).text() === toggleStates[i];
241
+
242
+ }), _plot, datasets);
243
+
244
+ }
245
+
246
+ } else {
247
+
248
+ placeholder.data("togglestates", toggleStates);
249
+
250
+ }
251
+
252
+ }
253
+
254
+ });
255
+
256
+ _plot.hooks.legendInserted.push(function ( _plot, _legend ) {
257
+
258
+ var plot = _plot,
259
+ toggleStates = [ ],
260
+ cells = _legend.find("td"),
261
+ entries = [ ];
262
+
263
+ datasets = {
264
+ visible: plot.getData(),
265
+ toggle: [ ],
266
+ all: plot.getData().slice()
267
+ };
268
+
269
+ legend = _legend;
270
+
271
+ // Split into objects containing each legend item's
272
+ // colour box and label.
273
+ for ( var i = 0; i < cells.length; i += 2 ) {
274
+
275
+ entries.push({
276
+ swatch: $(cells[i]),
277
+ label: $(cells[i + 1])
278
+ });
279
+
280
+ }
281
+
282
+ for ( var e in entries ) {
283
+
284
+ if ( entries.hasOwnProperty(e) ) {
285
+
286
+ setupSwatch(entries[e].swatch);
287
+
288
+ }
289
+
290
+ }
291
+
292
+ legend
293
+ .unbind("click.flot")
294
+ .bind("selectstart", function ( e ) {
295
+
296
+ e.preventDefault();
297
+
298
+ return false;
299
+
300
+ })
301
+ .bind("click.flot", function ( e ) {
302
+
303
+ toggle($(e.target), plot, datasets);
304
+
305
+ })
306
+ .find("td").css("cursor", "pointer");
307
+
308
+ });
309
+
310
+ };
311
+
312
+ $.plot.plugins.push({
313
+ init: init,
314
+ options: options,
315
+ name: 'toggleLegend',
316
+ version: '0.3'
317
+ });
318
+
319
+ }(jQuery));
view/js/jquery.ui.datepicker.js ADDED
@@ -0,0 +1,2038 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*!
2
+ * jQuery UI Datepicker 1.10.3
3
+ * http://jqueryui.com
4
+ *
5
+ * Copyright 2013 jQuery Foundation and other contributors
6
+ * Released under the MIT license.
7
+ * http://jquery.org/license
8
+ *
9
+ * http://api.jqueryui.com/datepicker/
10
+ *
11
+ * Depends:
12
+ * jquery.ui.core.js
13
+ */
14
+ (function( $, undefined ) {
15
+
16
+ $.extend($.ui, { datepicker: { version: "1.10.3" } });
17
+
18
+ var PROP_NAME = "datepicker",
19
+ instActive;
20
+
21
+ /* Date picker manager.
22
+ Use the singleton instance of this class, $.datepicker, to interact with the date picker.
23
+ Settings for (groups of) date pickers are maintained in an instance object,
24
+ allowing multiple different settings on the same page. */
25
+
26
+ function Datepicker() {
27
+ this._curInst = null; // The current instance in use
28
+ this._keyEvent = false; // If the last event was a key event
29
+ this._disabledInputs = []; // List of date picker inputs that have been disabled
30
+ this._datepickerShowing = false; // True if the popup picker is showing , false if not
31
+ this._inDialog = false; // True if showing within a "dialog", false if not
32
+ this._mainDivId = "ui-datepicker-div"; // The ID of the main datepicker division
33
+ this._inlineClass = "ui-datepicker-inline"; // The name of the inline marker class
34
+ this._appendClass = "ui-datepicker-append"; // The name of the append marker class
35
+ this._triggerClass = "ui-datepicker-trigger"; // The name of the trigger marker class
36
+ this._dialogClass = "ui-datepicker-dialog"; // The name of the dialog marker class
37
+ this._disableClass = "ui-datepicker-disabled"; // The name of the disabled covering marker class
38
+ this._unselectableClass = "ui-datepicker-unselectable"; // The name of the unselectable cell marker class
39
+ this._currentClass = "ui-datepicker-current-day"; // The name of the current day marker class
40
+ this._dayOverClass = "ui-datepicker-days-cell-over"; // The name of the day hover marker class
41
+ this.regional = []; // Available regional settings, indexed by language code
42
+ this.regional[""] = { // Default regional settings
43
+ closeText: "Done", // Display text for close link
44
+ prevText: "Prev", // Display text for previous month link
45
+ nextText: "Next", // Display text for next month link
46
+ currentText: "Today", // Display text for current month link
47
+ monthNames: ["January","February","March","April","May","June",
48
+ "July","August","September","October","November","December"], // Names of months for drop-down and formatting
49
+ monthNamesShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], // For formatting
50
+ dayNames: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], // For formatting
51
+ dayNamesShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], // For formatting
52
+ dayNamesMin: ["Su","Mo","Tu","We","Th","Fr","Sa"], // Column headings for days starting at Sunday
53
+ weekHeader: "Wk", // Column header for week of the year
54
+ dateFormat: "mm/dd/yy", // See format options on parseDate
55
+ firstDay: 0, // The first day of the week, Sun = 0, Mon = 1, ...
56
+ isRTL: false, // True if right-to-left language, false if left-to-right
57
+ showMonthAfterYear: false, // True if the year select precedes month, false for month then year
58
+ yearSuffix: "" // Additional text to append to the year in the month headers
59
+ };
60
+ this._defaults = { // Global defaults for all the date picker instances
61
+ showOn: "focus", // "focus" for popup on focus,
62
+ // "button" for trigger button, or "both" for either
63
+ showAnim: "fadeIn", // Name of jQuery animation for popup
64
+ showOptions: {}, // Options for enhanced animations
65
+ defaultDate: null, // Used when field is blank: actual date,
66
+ // +/-number for offset from today, null for today
67
+ appendText: "", // Display text following the input box, e.g. showing the format
68
+ buttonText: "...", // Text for trigger button
69
+ buttonImage: "", // URL for trigger button image
70
+ buttonImageOnly: false, // True if the image appears alone, false if it appears on a button
71
+ hideIfNoPrevNext: false, // True to hide next/previous month links
72
+ // if not applicable, false to just disable them
73
+ navigationAsDateFormat: false, // True if date formatting applied to prev/today/next links
74
+ gotoCurrent: false, // True if today link goes back to current selection instead
75
+ changeMonth: false, // True if month can be selected directly, false if only prev/next
76
+ changeYear: false, // True if year can be selected directly, false if only prev/next
77
+ yearRange: "c-10:c+10", // Range of years to display in drop-down,
78
+ // either relative to today's year (-nn:+nn), relative to currently displayed year
79
+ // (c-nn:c+nn), absolute (nnnn:nnnn), or a combination of the above (nnnn:-n)
80
+ showOtherMonths: false, // True to show dates in other months, false to leave blank
81
+ selectOtherMonths: false, // True to allow selection of dates in other months, false for unselectable
82
+ showWeek: false, // True to show week of the year, false to not show it
83
+ calculateWeek: this.iso8601Week, // How to calculate the week of the year,
84
+ // takes a Date and returns the number of the week for it
85
+ shortYearCutoff: "+10", // Short year values < this are in the current century,
86
+ // > this are in the previous century,
87
+ // string value starting with "+" for current year + value
88
+ minDate: null, // The earliest selectable date, or null for no limit
89
+ maxDate: null, // The latest selectable date, or null for no limit
90
+ duration: "fast", // Duration of display/closure
91
+ beforeShowDay: null, // Function that takes a date and returns an array with
92
+ // [0] = true if selectable, false if not, [1] = custom CSS class name(s) or "",
93
+ // [2] = cell title (optional), e.g. $.datepicker.noWeekends
94
+ beforeShow: null, // Function that takes an input field and
95
+ // returns a set of custom settings for the date picker
96
+ onSelect: null, // Define a callback function when a date is selected
97
+ onChangeMonthYear: null, // Define a callback function when the month or year is changed
98
+ onClose: null, // Define a callback function when the datepicker is closed
99
+ numberOfMonths: 1, // Number of months to show at a time
100
+ showCurrentAtPos: 0, // The position in multipe months at which to show the current month (starting at 0)
101
+ stepMonths: 1, // Number of months to step back/forward
102
+ stepBigMonths: 12, // Number of months to step back/forward for the big links
103
+ altField: "", // Selector for an alternate field to store selected dates into
104
+ altFormat: "", // The date format to use for the alternate field
105
+ constrainInput: true, // The input is constrained by the current date format
106
+ showButtonPanel: false, // True to show button panel, false to not show it
107
+ autoSize: false, // True to size the input for the date format, false to leave as is
108
+ disabled: false // The initial disabled state
109
+ };
110
+ $.extend(this._defaults, this.regional[""]);
111
+ this.dpDiv = bindHover($("<div id='" + this._mainDivId + "' class='ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>"));
112
+ }
113
+
114
+ $.extend(Datepicker.prototype, {
115
+ /* Class name added to elements to indicate already configured with a date picker. */
116
+ markerClassName: "hasDatepicker",
117
+
118
+ //Keep track of the maximum number of rows displayed (see #7043)
119
+ maxRows: 4,
120
+
121
+ // TODO rename to "widget" when switching to widget factory
122
+ _widgetDatepicker: function() {
123
+ return this.dpDiv;
124
+ },
125
+
126
+ /* Override the default settings for all instances of the date picker.
127
+ * @param settings object - the new settings to use as defaults (anonymous object)
128
+ * @return the manager object
129
+ */
130
+ setDefaults: function(settings) {
131
+ extendRemove(this._defaults, settings || {});
132
+ return this;
133
+ },
134
+
135
+ /* Attach the date picker to a jQuery selection.
136
+ * @param target element - the target input field or division or span
137
+ * @param settings object - the new settings to use for this date picker instance (anonymous)
138
+ */
139
+ _attachDatepicker: function(target, settings) {
140
+ var nodeName, inline, inst;
141
+ nodeName = target.nodeName.toLowerCase();
142
+ inline = (nodeName === "div" || nodeName === "span");
143
+ if (!target.id) {
144
+ this.uuid += 1;
145
+ target.id = "dp" + this.uuid;
146
+ }
147
+ inst = this._newInst($(target), inline);
148
+ inst.settings = $.extend({}, settings || {});
149
+ if (nodeName === "input") {
150
+ this._connectDatepicker(target, inst);
151
+ } else if (inline) {
152
+ this._inlineDatepicker(target, inst);
153
+ }
154
+ },
155
+
156
+ /* Create a new instance object. */
157
+ _newInst: function(target, inline) {
158
+ var id = target[0].id.replace(/([^A-Za-z0-9_\-])/g, "\\\\$1"); // escape jQuery meta chars
159
+ return {id: id, input: target, // associated target
160
+ selectedDay: 0, selectedMonth: 0, selectedYear: 0, // current selection
161
+ drawMonth: 0, drawYear: 0, // month being drawn
162
+ inline: inline, // is datepicker inline or not
163
+ dpDiv: (!inline ? this.dpDiv : // presentation div
164
+ bindHover($("<div class='" + this._inlineClass + " ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>")))};
165
+ },
166
+
167
+ /* Attach the date picker to an input field. */
168
+ _connectDatepicker: function(target, inst) {
169
+ var input = $(target);
170
+ inst.append = $([]);
171
+ inst.trigger = $([]);
172
+ if (input.hasClass(this.markerClassName)) {
173
+ return;
174
+ }
175
+ this._attachments(input, inst);
176
+ input.addClass(this.markerClassName).keydown(this._doKeyDown).
177
+ keypress(this._doKeyPress).keyup(this._doKeyUp);
178
+ this._autoSize(inst);
179
+ $.data(target, PROP_NAME, inst);
180
+ //If disabled option is true, disable the datepicker once it has been attached to the input (see ticket #5665)
181
+ if( inst.settings.disabled ) {
182
+ this._disableDatepicker( target );
183
+ }
184
+ },
185
+
186
+ /* Make attachments based on settings. */
187
+ _attachments: function(input, inst) {
188
+ var showOn, buttonText, buttonImage,
189
+ appendText = this._get(inst, "appendText"),
190
+ isRTL = this._get(inst, "isRTL");
191
+
192
+ if (inst.append) {
193
+ inst.append.remove();
194
+ }
195
+ if (appendText) {
196
+ inst.append = $("<span class='" + this._appendClass + "'>" + appendText + "</span>");
197
+ input[isRTL ? "before" : "after"](inst.append);
198
+ }
199
+
200
+ input.unbind("focus", this._showDatepicker);
201
+
202
+ if (inst.trigger) {
203
+ inst.trigger.remove();
204
+ }
205
+
206
+ showOn = this._get(inst, "showOn");
207
+ if (showOn === "focus" || showOn === "both") { // pop-up date picker when in the marked field
208
+ input.focus(this._showDatepicker);
209
+ }
210
+ if (showOn === "button" || showOn === "both") { // pop-up date picker when button clicked
211
+ buttonText = this._get(inst, "buttonText");
212
+ buttonImage = this._get(inst, "buttonImage");
213
+ inst.trigger = $(this._get(inst, "buttonImageOnly") ?
214
+ $("<img/>").addClass(this._triggerClass).
215
+ attr({ src: buttonImage, alt: buttonText, title: buttonText }) :
216
+ $("<button type='button'></button>").addClass(this._triggerClass).
217
+ html(!buttonImage ? buttonText : $("<img/>").attr(
218
+ { src:buttonImage, alt:buttonText, title:buttonText })));
219
+ input[isRTL ? "before" : "after"](inst.trigger);
220
+ inst.trigger.click(function() {
221
+ if ($.datepicker._datepickerShowing && $.datepicker._lastInput === input[0]) {
222
+ $.datepicker._hideDatepicker();
223
+ } else if ($.datepicker._datepickerShowing && $.datepicker._lastInput !== input[0]) {
224
+ $.datepicker._hideDatepicker();
225
+ $.datepicker._showDatepicker(input[0]);
226
+ } else {
227
+ $.datepicker._showDatepicker(input[0]);
228
+ }
229
+ return false;
230
+ });
231
+ }
232
+ },
233
+
234
+ /* Apply the maximum length for the date format. */
235
+ _autoSize: function(inst) {
236
+ if (this._get(inst, "autoSize") && !inst.inline) {
237
+ var findMax, max, maxI, i,
238
+ date = new Date(2009, 12 - 1, 20), // Ensure double digits
239
+ dateFormat = this._get(inst, "dateFormat");
240
+
241
+ if (dateFormat.match(/[DM]/)) {
242
+ findMax = function(names) {
243
+ max = 0;
244
+ maxI = 0;
245
+ for (i = 0; i < names.length; i++) {
246
+ if (names[i].length > max) {
247
+ max = names[i].length;
248
+ maxI = i;
249
+ }
250
+ }
251
+ return maxI;
252
+ };
253
+ date.setMonth(findMax(this._get(inst, (dateFormat.match(/MM/) ?
254
+ "monthNames" : "monthNamesShort"))));
255
+ date.setDate(findMax(this._get(inst, (dateFormat.match(/DD/) ?
256
+ "dayNames" : "dayNamesShort"))) + 20 - date.getDay());
257
+ }
258
+ inst.input.attr("size", this._formatDate(inst, date).length);
259
+ }
260
+ },
261
+
262
+ /* Attach an inline date picker to a div. */
263
+ _inlineDatepicker: function(target, inst) {
264
+ var divSpan = $(target);
265
+ if (divSpan.hasClass(this.markerClassName)) {
266
+ return;
267
+ }
268
+ divSpan.addClass(this.markerClassName).append(inst.dpDiv);
269
+ $.data(target, PROP_NAME, inst);
270
+ this._setDate(inst, this._getDefaultDate(inst), true);
271
+ this._updateDatepicker(inst);
272
+ this._updateAlternate(inst);
273
+ //If disabled option is true, disable the datepicker before showing it (see ticket #5665)
274
+ if( inst.settings.disabled ) {
275
+ this._disableDatepicker( target );
276
+ }
277
+ // Set display:block in place of inst.dpDiv.show() which won't work on disconnected elements
278
+ // http://bugs.jqueryui.com/ticket/7552 - A Datepicker created on a detached div has zero height
279
+ inst.dpDiv.css( "display", "block" );
280
+ },
281
+
282
+ /* Pop-up the date picker in a "dialog" box.
283
+ * @param input element - ignored
284
+ * @param date string or Date - the initial date to display
285
+ * @param onSelect function - the function to call when a date is selected
286
+ * @param settings object - update the dialog date picker instance's settings (anonymous object)
287
+ * @param pos int[2] - coordinates for the dialog's position within the screen or
288
+ * event - with x/y coordinates or
289
+ * leave empty for default (screen centre)
290
+ * @return the manager object
291
+ */
292
+ _dialogDatepicker: function(input, date, onSelect, settings, pos) {
293
+ var id, browserWidth, browserHeight, scrollX, scrollY,
294
+ inst = this._dialogInst; // internal instance
295
+
296
+ if (!inst) {
297
+ this.uuid += 1;
298
+ id = "dp" + this.uuid;
299
+ this._dialogInput = $("<input type='text' id='" + id +
300
+ "' style='position: absolute; top: -100px; width: 0px;'/>");
301
+ this._dialogInput.keydown(this._doKeyDown);
302
+ $("body").append(this._dialogInput);
303
+ inst = this._dialogInst = this._newInst(this._dialogInput, false);
304
+ inst.settings = {};
305
+ $.data(this._dialogInput[0], PROP_NAME, inst);
306
+ }
307
+ extendRemove(inst.settings, settings || {});
308
+ date = (date && date.constructor === Date ? this._formatDate(inst, date) : date);
309
+ this._dialogInput.val(date);
310
+
311
+ this._pos = (pos ? (pos.length ? pos : [pos.pageX, pos.pageY]) : null);
312
+ if (!this._pos) {
313
+ browserWidth = document.documentElement.clientWidth;
314
+ browserHeight = document.documentElement.clientHeight;
315
+ scrollX = document.documentElement.scrollLeft || document.body.scrollLeft;
316
+ scrollY = document.documentElement.scrollTop || document.body.scrollTop;
317
+ this._pos = // should use actual width/height below
318
+ [(browserWidth / 2) - 100 + scrollX, (browserHeight / 2) - 150 + scrollY];
319
+ }
320
+
321
+ // move input on screen for focus, but hidden behind dialog
322
+ this._dialogInput.css("left", (this._pos[0] + 20) + "px").css("top", this._pos[1] + "px");
323
+ inst.settings.onSelect = onSelect;
324
+ this._inDialog = true;
325
+ this.dpDiv.addClass(this._dialogClass);
326
+ this._showDatepicker(this._dialogInput[0]);
327
+ if ($.blockUI) {
328
+ $.blockUI(this.dpDiv);
329
+ }
330
+ $.data(this._dialogInput[0], PROP_NAME, inst);
331
+ return this;
332
+ },
333
+
334
+ /* Detach a datepicker from its control.
335
+ * @param target element - the target input field or division or span
336
+ */
337
+ _destroyDatepicker: function(target) {
338
+ var nodeName,
339
+ $target = $(target),
340
+ inst = $.data(target, PROP_NAME);
341
+
342
+ if (!$target.hasClass(this.markerClassName)) {
343
+ return;
344
+ }
345
+
346
+ nodeName = target.nodeName.toLowerCase();
347
+ $.removeData(target, PROP_NAME);
348
+ if (nodeName === "input") {
349
+ inst.append.remove();
350
+ inst.trigger.remove();
351
+ $target.removeClass(this.markerClassName).
352
+ unbind("focus", this._showDatepicker).
353
+ unbind("keydown", this._doKeyDown).
354
+ unbind("keypress", this._doKeyPress).
355
+ unbind("keyup", this._doKeyUp);
356
+ } else if (nodeName === "div" || nodeName === "span") {
357
+ $target.removeClass(this.markerClassName).empty();
358
+ }
359
+ },
360
+
361
+ /* Enable the date picker to a jQuery selection.
362
+ * @param target element - the target input field or division or span
363
+ */
364
+ _enableDatepicker: function(target) {
365
+ var nodeName, inline,
366
+ $target = $(target),
367
+ inst = $.data(target, PROP_NAME);
368
+
369
+ if (!$target.hasClass(this.markerClassName)) {
370
+ return;
371
+ }
372
+
373
+ nodeName = target.nodeName.toLowerCase();
374
+ if (nodeName === "input") {
375
+ target.disabled = false;
376
+ inst.trigger.filter("button").
377
+ each(function() { this.disabled = false; }).end().
378
+ filter("img").css({opacity: "1.0", cursor: ""});
379
+ } else if (nodeName === "div" || nodeName === "span") {
380
+ inline = $target.children("." + this._inlineClass);
381
+ inline.children().removeClass("ui-state-disabled");
382
+ inline.find("select.ui-datepicker-month, select.ui-datepicker-year").
383
+ prop("disabled", false);
384
+ }
385
+ this._disabledInputs = $.map(this._disabledInputs,
386
+ function(value) { return (value === target ? null : value); }); // delete entry
387
+ },
388
+
389
+ /* Disable the date picker to a jQuery selection.
390
+ * @param target element - the target input field or division or span
391
+ */
392
+ _disableDatepicker: function(target) {
393
+ var nodeName, inline,
394
+ $target = $(target),
395
+ inst = $.data(target, PROP_NAME);
396
+
397
+ if (!$target.hasClass(this.markerClassName)) {
398
+ return;
399
+ }
400
+
401
+ nodeName = target.nodeName.toLowerCase();
402
+ if (nodeName === "input") {
403
+ target.disabled = true;
404
+ inst.trigger.filter("button").
405
+ each(function() { this.disabled = true; }).end().
406
+ filter("img").css({opacity: "0.5", cursor: "default"});
407
+ } else if (nodeName === "div" || nodeName === "span") {
408
+ inline = $target.children("." + this._inlineClass);
409
+ inline.children().addClass("ui-state-disabled");
410
+ inline.find("select.ui-datepicker-month, select.ui-datepicker-year").
411
+ prop("disabled", true);
412
+ }
413
+ this._disabledInputs = $.map(this._disabledInputs,
414
+ function(value) { return (value === target ? null : value); }); // delete entry
415
+ this._disabledInputs[this._disabledInputs.length] = target;
416
+ },
417
+
418
+ /* Is the first field in a jQuery collection disabled as a datepicker?
419
+ * @param target element - the target input field or division or span
420
+ * @return boolean - true if disabled, false if enabled
421
+ */
422
+ _isDisabledDatepicker: function(target) {
423
+ if (!target) {
424
+ return false;
425
+ }
426
+ for (var i = 0; i < this._disabledInputs.length; i++) {
427
+ if (this._disabledInputs[i] === target) {
428
+ return true;
429
+ }
430
+ }
431
+ return false;
432
+ },
433
+
434
+ /* Retrieve the instance data for the target control.
435
+ * @param target element - the target input field or division or span
436
+ * @return object - the associated instance data
437
+ * @throws error if a jQuery problem getting data
438
+ */
439
+ _getInst: function(target) {
440
+ try {
441
+ return $.data(target, PROP_NAME);
442
+ }
443
+ catch (err) {
444
+ throw "Missing instance data for this datepicker";
445
+ }
446
+ },
447
+
448
+ /* Update or retrieve the settings for a date picker attached to an input field or division.
449
+ * @param target element - the target input field or division or span
450
+ * @param name object - the new settings to update or
451
+ * string - the name of the setting to change or retrieve,
452
+ * when retrieving also "all" for all instance settings or
453
+ * "defaults" for all global defaults
454
+ * @param value any - the new value for the setting
455
+ * (omit if above is an object or to retrieve a value)
456
+ */
457
+ _optionDatepicker: function(target, name, value) {
458
+ var settings, date, minDate, maxDate,
459
+ inst = this._getInst(target);
460
+
461
+ if (arguments.length === 2 && typeof name === "string") {
462
+ return (name === "defaults" ? $.extend({}, $.datepicker._defaults) :
463
+ (inst ? (name === "all" ? $.extend({}, inst.settings) :
464
+ this._get(inst, name)) : null));
465
+ }
466
+
467
+ settings = name || {};
468
+ if (typeof name === "string") {
469
+ settings = {};
470
+ settings[name] = value;
471
+ }
472
+
473
+ if (inst) {
474
+ if (this._curInst === inst) {
475
+ this._hideDatepicker();
476
+ }
477
+
478
+ date = this._getDateDatepicker(target, true);
479
+ minDate = this._getMinMaxDate(inst, "min");
480
+ maxDate = this._getMinMaxDate(inst, "max");
481
+ extendRemove(inst.settings, settings);
482
+ // reformat the old minDate/maxDate values if dateFormat changes and a new minDate/maxDate isn't provided
483
+ if (minDate !== null && settings.dateFormat !== undefined && settings.minDate === undefined) {
484
+ inst.settings.minDate = this._formatDate(inst, minDate);
485
+ }
486
+ if (maxDate !== null && settings.dateFormat !== undefined && settings.maxDate === undefined) {
487
+ inst.settings.maxDate = this._formatDate(inst, maxDate);
488
+ }
489
+ if ( "disabled" in settings ) {
490
+ if ( settings.disabled ) {
491
+ this._disableDatepicker(target);
492
+ } else {
493
+ this._enableDatepicker(target);
494
+ }
495
+ }
496
+ this._attachments($(target), inst);
497
+ this._autoSize(inst);
498
+ this._setDate(inst, date);
499
+ this._updateAlternate(inst);
500
+ this._updateDatepicker(inst);
501
+ }
502
+ },
503
+
504
+ // change method deprecated
505
+ _changeDatepicker: function(target, name, value) {
506
+ this._optionDatepicker(target, name, value);
507
+ },
508
+
509
+ /* Redraw the date picker attached to an input field or division.
510
+ * @param target element - the target input field or division or span
511
+ */
512
+ _refreshDatepicker: function(target) {
513
+ var inst = this._getInst(target);
514
+ if (inst) {
515
+ this._updateDatepicker(inst);
516
+ }
517
+ },
518
+
519
+ /* Set the dates for a jQuery selection.
520
+ * @param target element - the target input field or division or span
521
+ * @param date Date - the new date
522
+ */
523
+ _setDateDatepicker: function(target, date) {
524
+ var inst = this._getInst(target);
525
+ if (inst) {
526
+ this._setDate(inst, date);
527
+ this._updateDatepicker(inst);
528
+ this._updateAlternate(inst);
529
+ }
530
+ },
531
+
532
+ /* Get the date(s) for the first entry in a jQuery selection.
533
+ * @param target element - the target input field or division or span
534
+ * @param noDefault boolean - true if no default date is to be used
535
+ * @return Date - the current date
536
+ */
537
+ _getDateDatepicker: function(target, noDefault) {
538
+ var inst = this._getInst(target);
539
+ if (inst && !inst.inline) {
540
+ this._setDateFromField(inst, noDefault);
541
+ }
542
+ return (inst ? this._getDate(inst) : null);
543
+ },
544
+
545
+ /* Handle keystrokes. */
546
+ _doKeyDown: function(event) {
547
+ var onSelect, dateStr, sel,
548
+ inst = $.datepicker._getInst(event.target),
549
+ handled = true,
550
+ isRTL = inst.dpDiv.is(".ui-datepicker-rtl");
551
+
552
+ inst._keyEvent = true;
553
+ if ($.datepicker._datepickerShowing) {
554
+ switch (event.keyCode) {
555
+ case 9: $.datepicker._hideDatepicker();
556
+ handled = false;
557
+ break; // hide on tab out
558
+ case 13: sel = $("td." + $.datepicker._dayOverClass + ":not(." +
559
+ $.datepicker._currentClass + ")", inst.dpDiv);
560
+ if (sel[0]) {
561
+ $.datepicker._selectDay(event.target, inst.selectedMonth, inst.selectedYear, sel[0]);
562
+ }
563
+
564
+ onSelect = $.datepicker._get(inst, "onSelect");
565
+ if (onSelect) {
566
+ dateStr = $.datepicker._formatDate(inst);
567
+
568
+ // trigger custom callback
569
+ onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]);
570
+ } else {
571
+ $.datepicker._hideDatepicker();
572
+ }
573
+
574
+ return false; // don't submit the form
575
+ case 27: $.datepicker._hideDatepicker();
576
+ break; // hide on escape
577
+ case 33: $.datepicker._adjustDate(event.target, (event.ctrlKey ?
578
+ -$.datepicker._get(inst, "stepBigMonths") :
579
+ -$.datepicker._get(inst, "stepMonths")), "M");
580
+ break; // previous month/year on page up/+ ctrl
581
+ case 34: $.datepicker._adjustDate(event.target, (event.ctrlKey ?
582
+ +$.datepicker._get(inst, "stepBigMonths") :
583
+ +$.datepicker._get(inst, "stepMonths")), "M");
584
+ break; // next month/year on page down/+ ctrl
585
+ case 35: if (event.ctrlKey || event.metaKey) {
586
+ $.datepicker._clearDate(event.target);
587
+ }
588
+ handled = event.ctrlKey || event.metaKey;
589
+ break; // clear on ctrl or command +end
590
+ case 36: if (event.ctrlKey || event.metaKey) {
591
+ $.datepicker._gotoToday(event.target);
592
+ }
593
+ handled = event.ctrlKey || event.metaKey;
594
+ break; // current on ctrl or command +home
595
+ case 37: if (event.ctrlKey || event.metaKey) {
596
+ $.datepicker._adjustDate(event.target, (isRTL ? +1 : -1), "D");
597
+ }
598
+ handled = event.ctrlKey || event.metaKey;
599
+ // -1 day on ctrl or command +left
600
+ if (event.originalEvent.altKey) {
601
+ $.datepicker._adjustDate(event.target, (event.ctrlKey ?
602
+ -$.datepicker._get(inst, "stepBigMonths") :
603
+ -$.datepicker._get(inst, "stepMonths")), "M");
604
+ }
605
+ // next month/year on alt +left on Mac
606
+ break;
607
+ case 38: if (event.ctrlKey || event.metaKey) {
608
+ $.datepicker._adjustDate(event.target, -7, "D");
609
+ }
610
+ handled = event.ctrlKey || event.metaKey;
611
+ break; // -1 week on ctrl or command +up
612
+ case 39: if (event.ctrlKey || event.metaKey) {
613
+ $.datepicker._adjustDate(event.target, (isRTL ? -1 : +1), "D");
614
+ }
615
+ handled = event.ctrlKey || event.metaKey;
616
+ // +1 day on ctrl or command +right
617
+ if (event.originalEvent.altKey) {
618
+ $.datepicker._adjustDate(event.target, (event.ctrlKey ?
619
+ +$.datepicker._get(inst, "stepBigMonths") :
620
+ +$.datepicker._get(inst, "stepMonths")), "M");
621
+ }
622
+ // next month/year on alt +right
623
+ break;
624
+ case 40: if (event.ctrlKey || event.metaKey) {
625
+ $.datepicker._adjustDate(event.target, +7, "D");
626
+ }
627
+ handled = event.ctrlKey || event.metaKey;
628
+ break; // +1 week on ctrl or command +down
629
+ default: handled = false;
630
+ }
631
+ } else if (event.keyCode === 36 && event.ctrlKey) { // display the date picker on ctrl+home
632
+ $.datepicker._showDatepicker(this);
633
+ } else {
634
+ handled = false;
635
+ }
636
+
637
+ if (handled) {
638
+ event.preventDefault();
639
+ event.stopPropagation();
640
+ }
641
+ },
642
+
643
+ /* Filter entered characters - based on date format. */
644
+ _doKeyPress: function(event) {
645
+ var chars, chr,
646
+ inst = $.datepicker._getInst(event.target);
647
+
648
+ if ($.datepicker._get(inst, "constrainInput")) {
649
+ chars = $.datepicker._possibleChars($.datepicker._get(inst, "dateFormat"));
650
+ chr = String.fromCharCode(event.charCode == null ? event.keyCode : event.charCode);
651
+ return event.ctrlKey || event.metaKey || (chr < " " || !chars || chars.indexOf(chr) > -1);
652
+ }
653
+ },
654
+
655
+ /* Synchronise manual entry and field/alternate field. */
656
+ _doKeyUp: function(event) {
657
+ var date,
658
+ inst = $.datepicker._getInst(event.target);
659
+
660
+ if (inst.input.val() !== inst.lastVal) {
661
+ try {
662
+ date = $.datepicker.parseDate($.datepicker._get(inst, "dateFormat"),
663
+ (inst.input ? inst.input.val() : null),
664
+ $.datepicker._getFormatConfig(inst));
665
+
666
+ if (date) { // only if valid
667
+ $.datepicker._setDateFromField(inst);
668
+ $.datepicker._updateAlternate(inst);
669
+ $.datepicker._updateDatepicker(inst);
670
+ }
671
+ }
672
+ catch (err) {
673
+ }
674
+ }
675
+ return true;
676
+ },
677
+
678
+ /* Pop-up the date picker for a given input field.
679
+ * If false returned from beforeShow event handler do not show.
680
+ * @param input element - the input field attached to the date picker or
681
+ * event - if triggered by focus
682
+ */
683
+ _showDatepicker: function(input) {
684
+ input = input.target || input;
685
+ if (input.nodeName.toLowerCase() !== "input") { // find from button/image trigger
686
+ input = $("input", input.parentNode)[0];
687
+ }
688
+
689
+ if ($.datepicker._isDisabledDatepicker(input) || $.datepicker._lastInput === input) { // already here
690
+ return;
691
+ }
692
+
693
+ var inst, beforeShow, beforeShowSettings, isFixed,
694
+ offset, showAnim, duration;
695
+
696
+ inst = $.datepicker._getInst(input);
697
+ if ($.datepicker._curInst && $.datepicker._curInst !== inst) {
698
+ $.datepicker._curInst.dpDiv.stop(true, true);
699
+ if ( inst && $.datepicker._datepickerShowing ) {
700
+ $.datepicker._hideDatepicker( $.datepicker._curInst.input[0] );
701
+ }
702
+ }
703
+
704
+ beforeShow = $.datepicker._get(inst, "beforeShow");
705
+ beforeShowSettings = beforeShow ? beforeShow.apply(input, [input, inst]) : {};
706
+ if(beforeShowSettings === false){
707
+ return;
708
+ }
709
+ extendRemove(inst.settings, beforeShowSettings);
710
+
711
+ inst.lastVal = null;
712
+ $.datepicker._lastInput = input;
713
+ $.datepicker._setDateFromField(inst);
714
+
715
+ if ($.datepicker._inDialog) { // hide cursor
716
+ input.value = "";
717
+ }
718
+ if (!$.datepicker._pos) { // position below input
719
+ $.datepicker._pos = $.datepicker._findPos(input);
720
+ $.datepicker._pos[1] += input.offsetHeight; // add the height
721
+ }
722
+
723
+ isFixed = false;
724
+ $(input).parents().each(function() {
725
+ isFixed |= $(this).css("position") === "fixed";
726
+ return !isFixed;
727
+ });
728
+
729
+ offset = {left: $.datepicker._pos[0], top: $.datepicker._pos[1]};
730
+ $.datepicker._pos = null;
731
+ //to avoid flashes on Firefox
732
+ inst.dpDiv.empty();
733
+ // determine sizing offscreen
734
+ inst.dpDiv.css({position: "absolute", display: "block", top: "-1000px"});
735
+ $.datepicker._updateDatepicker(inst);
736
+ // fix width for dynamic number of date pickers
737
+ // and adjust position before showing
738
+ offset = $.datepicker._checkOffset(inst, offset, isFixed);
739
+ inst.dpDiv.css({position: ($.datepicker._inDialog && $.blockUI ?
740
+ "static" : (isFixed ? "fixed" : "absolute")), display: "none",
741
+ left: offset.left + "px", top: offset.top + "px"});
742
+
743
+ if (!inst.inline) {
744
+ showAnim = $.datepicker._get(inst, "showAnim");
745
+ duration = $.datepicker._get(inst, "duration");
746
+ inst.dpDiv.zIndex($(input).zIndex()+1);
747
+ $.datepicker._datepickerShowing = true;
748
+
749
+ if ( $.effects && $.effects.effect[ showAnim ] ) {
750
+ inst.dpDiv.show(showAnim, $.datepicker._get(inst, "showOptions"), duration);
751
+ } else {
752
+ inst.dpDiv[showAnim || "show"](showAnim ? duration : null);
753
+ }
754
+
755
+ if ( $.datepicker._shouldFocusInput( inst ) ) {
756
+ inst.input.focus();
757
+ }
758
+
759
+ $.datepicker._curInst = inst;
760
+ }
761
+ },
762
+
763
+ /* Generate the date picker content. */
764
+ _updateDatepicker: function(inst) {
765
+ this.maxRows = 4; //Reset the max number of rows being displayed (see #7043)
766
+ instActive = inst; // for delegate hover events
767
+ inst.dpDiv.empty().append(this._generateHTML(inst));
768
+ this._attachHandlers(inst);
769
+ inst.dpDiv.find("." + this._dayOverClass + " a").mouseover();
770
+
771
+ var origyearshtml,
772
+ numMonths = this._getNumberOfMonths(inst),
773
+ cols = numMonths[1],
774
+ width = 17;
775
+
776
+ inst.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width("");
777
+ if (cols > 1) {
778
+ inst.dpDiv.addClass("ui-datepicker-multi-" + cols).css("width", (width * cols) + "em");
779
+ }
780
+ inst.dpDiv[(numMonths[0] !== 1 || numMonths[1] !== 1 ? "add" : "remove") +
781
+ "Class"]("ui-datepicker-multi");
782
+ inst.dpDiv[(this._get(inst, "isRTL") ? "add" : "remove") +
783
+ "Class"]("ui-datepicker-rtl");
784
+
785
+ if (inst === $.datepicker._curInst && $.datepicker._datepickerShowing && $.datepicker._shouldFocusInput( inst ) ) {
786
+ inst.input.focus();
787
+ }
788
+
789
+ // deffered render of the years select (to avoid flashes on Firefox)
790
+ if( inst.yearshtml ){
791
+ origyearshtml = inst.yearshtml;
792
+ setTimeout(function(){
793
+ //assure that inst.yearshtml didn't change.
794
+ if( origyearshtml === inst.yearshtml && inst.yearshtml ){
795
+ inst.dpDiv.find("select.ui-datepicker-year:first").replaceWith(inst.yearshtml);
796
+ }
797
+ origyearshtml = inst.yearshtml = null;
798
+ }, 0);
799
+ }
800
+ },
801
+
802
+ // #6694 - don't focus the input if it's already focused
803
+ // this breaks the change event in IE
804
+ // Support: IE and jQuery <1.9
805
+ _shouldFocusInput: function( inst ) {
806
+ return inst.input && inst.input.is( ":visible" ) && !inst.input.is( ":disabled" ) && !inst.input.is( ":focus" );
807
+ },
808
+
809
+ /* Check positioning to remain on screen. */
810
+ _checkOffset: function(inst, offset, isFixed) {
811
+ var dpWidth = inst.dpDiv.outerWidth(),
812
+ dpHeight = inst.dpDiv.outerHeight(),
813
+ inputWidth = inst.input ? inst.input.outerWidth() : 0,
814
+ inputHeight = inst.input ? inst.input.outerHeight() : 0,
815
+ viewWidth = document.documentElement.clientWidth + (isFixed ? 0 : $(document).scrollLeft()),
816
+ viewHeight = document.documentElement.clientHeight + (isFixed ? 0 : $(document).scrollTop());
817
+
818
+ offset.left -= (this._get(inst, "isRTL") ? (dpWidth - inputWidth) : 0);
819
+ offset.left -= (isFixed && offset.left === inst.input.offset().left) ? $(document).scrollLeft() : 0;
820
+ offset.top -= (isFixed && offset.top === (inst.input.offset().top + inputHeight)) ? $(document).scrollTop() : 0;
821
+
822
+ // now check if datepicker is showing outside window viewport - move to a better place if so.
823
+ offset.left -= Math.min(offset.left, (offset.left + dpWidth > viewWidth && viewWidth > dpWidth) ?
824
+ Math.abs(offset.left + dpWidth - viewWidth) : 0);
825
+ offset.top -= Math.min(offset.top, (offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ?
826
+ Math.abs(dpHeight + inputHeight) : 0);
827
+
828
+ return offset;
829
+ },
830
+
831
+ /* Find an object's position on the screen. */
832
+ _findPos: function(obj) {
833
+ var position,
834
+ inst = this._getInst(obj),
835
+ isRTL = this._get(inst, "isRTL");
836
+
837
+ while (obj && (obj.type === "hidden" || obj.nodeType !== 1 || $.expr.filters.hidden(obj))) {
838
+ obj = obj[isRTL ? "previousSibling" : "nextSibling"];
839
+ }
840
+
841
+ position = $(obj).offset();
842
+ return [position.left, position.top];
843
+ },
844
+
845
+ /* Hide the date picker from view.
846
+ * @param input element - the input field attached to the date picker
847
+ */
848
+ _hideDatepicker: function(input) {
849
+ var showAnim, duration, postProcess, onClose,
850
+ inst = this._curInst;
851
+
852
+ if (!inst || (input && inst !== $.data(input, PROP_NAME))) {
853
+ return;
854
+ }
855
+
856
+ if (this._datepickerShowing) {
857
+ showAnim = this._get(inst, "showAnim");
858
+ duration = this._get(inst, "duration");
859
+ postProcess = function() {
860
+ $.datepicker._tidyDialog(inst);
861
+ };
862
+
863
+ // DEPRECATED: after BC for 1.8.x $.effects[ showAnim ] is not needed
864
+ if ( $.effects && ( $.effects.effect[ showAnim ] || $.effects[ showAnim ] ) ) {
865
+ inst.dpDiv.hide(showAnim, $.datepicker._get(inst, "showOptions"), duration, postProcess);
866
+ } else {
867
+ inst.dpDiv[(showAnim === "slideDown" ? "slideUp" :
868
+ (showAnim === "fadeIn" ? "fadeOut" : "hide"))]((showAnim ? duration : null), postProcess);
869
+ }
870
+
871
+ if (!showAnim) {
872
+ postProcess();
873
+ }
874
+ this._datepickerShowing = false;
875
+
876
+ onClose = this._get(inst, "onClose");
877
+ if (onClose) {
878
+ onClose.apply((inst.input ? inst.input[0] : null), [(inst.input ? inst.input.val() : ""), inst]);
879
+ }
880
+
881
+ this._lastInput = null;
882
+ if (this._inDialog) {
883
+ this._dialogInput.css({ position: "absolute", left: "0", top: "-100px" });
884
+ if ($.blockUI) {
885
+ $.unblockUI();
886
+ $("body").append(this.dpDiv);
887
+ }
888
+ }
889
+ this._inDialog = false;
890
+ }
891
+ },
892
+
893
+ /* Tidy up after a dialog display. */
894
+ _tidyDialog: function(inst) {
895
+ inst.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar");
896
+ },
897
+
898
+ /* Close date picker if clicked elsewhere. */
899
+ _checkExternalClick: function(event) {
900
+ if (!$.datepicker._curInst) {
901
+ return;
902
+ }
903
+
904
+ var $target = $(event.target),
905
+ inst = $.datepicker._getInst($target[0]);
906
+
907
+ if ( ( ( $target[0].id !== $.datepicker._mainDivId &&
908
+ $target.parents("#" + $.datepicker._mainDivId).length === 0 &&
909
+ !$target.hasClass($.datepicker.markerClassName) &&
910
+ !$target.closest("." + $.datepicker._triggerClass).length &&
911
+ $.datepicker._datepickerShowing && !($.datepicker._inDialog && $.blockUI) ) ) ||
912
+ ( $target.hasClass($.datepicker.markerClassName) && $.datepicker._curInst !== inst ) ) {
913
+ $.datepicker._hideDatepicker();
914
+ }
915
+ },
916
+
917
+ /* Adjust one of the date sub-fields. */
918
+ _adjustDate: function(id, offset, period) {
919
+ var target = $(id),
920
+ inst = this._getInst(target[0]);
921
+
922
+ if (this._isDisabledDatepicker(target[0])) {
923
+ return;
924
+ }
925
+ this._adjustInstDate(inst, offset +
926
+ (period === "M" ? this._get(inst, "showCurrentAtPos") : 0), // undo positioning
927
+ period);
928
+ this._updateDatepicker(inst);
929
+ },
930
+
931
+ /* Action for current link. */
932
+ _gotoToday: function(id) {
933
+ var date,
934
+ target = $(id),
935
+ inst = this._getInst(target[0]);
936
+
937
+ if (this._get(inst, "gotoCurrent") && inst.currentDay) {
938
+ inst.selectedDay = inst.currentDay;
939
+ inst.drawMonth = inst.selectedMonth = inst.currentMonth;
940
+ inst.drawYear = inst.selectedYear = inst.currentYear;
941
+ } else {
942
+ date = new Date();
943
+ inst.selectedDay = date.getDate();
944
+ inst.drawMonth = inst.selectedMonth = date.getMonth();
945
+ inst.drawYear = inst.selectedYear = date.getFullYear();
946
+ }
947
+ this._notifyChange(inst);
948
+ this._adjustDate(target);
949
+ },
950
+
951
+ /* Action for selecting a new month/year. */
952
+ _selectMonthYear: function(id, select, period) {
953
+ var target = $(id),
954
+ inst = this._getInst(target[0]);
955
+
956
+ inst["selected" + (period === "M" ? "Month" : "Year")] =
957
+ inst["draw" + (period === "M" ? "Month" : "Year")] =
958
+ parseInt(select.options[select.selectedIndex].value,10);
959
+
960
+ this._notifyChange(inst);
961
+ this._adjustDate(target);
962
+ },
963
+
964
+ /* Action for selecting a day. */
965
+ _selectDay: function(id, month, year, td) {
966
+ var inst,
967
+ target = $(id);
968
+
969
+ if ($(td).hasClass(this._unselectableClass) || this._isDisabledDatepicker(target[0])) {
970
+ return;
971
+ }
972
+
973
+ inst = this._getInst(target[0]);
974
+ inst.selectedDay = inst.currentDay = $("a", td).html();
975
+ inst.selectedMonth = inst.currentMonth = month;
976
+ inst.selectedYear = inst.currentYear = year;
977
+ this._selectDate(id, this._formatDate(inst,
978
+ inst.currentDay, inst.currentMonth, inst.currentYear));
979
+ },
980
+
981
+ /* Erase the input field and hide the date picker. */
982
+ _clearDate: function(id) {
983
+ var target = $(id);
984
+ this._selectDate(target, "");
985
+ },
986
+
987
+ /* Update the input field with the selected date. */
988
+ _selectDate: function(id, dateStr) {
989
+ var onSelect,
990
+ target = $(id),
991
+ inst = this._getInst(target[0]);
992
+
993
+ dateStr = (dateStr != null ? dateStr : this._formatDate(inst));
994
+ if (inst.input) {
995
+ inst.input.val(dateStr);
996
+ }
997
+ this._updateAlternate(inst);
998
+
999
+ onSelect = this._get(inst, "onSelect");
1000
+ if (onSelect) {
1001
+ onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]); // trigger custom callback
1002
+ } else if (inst.input) {
1003
+ inst.input.trigger("change"); // fire the change event
1004
+ }
1005
+
1006
+ if (inst.inline){
1007
+ this._updateDatepicker(inst);
1008
+ } else {
1009
+ this._hideDatepicker();
1010
+ this._lastInput = inst.input[0];
1011
+ if (typeof(inst.input[0]) !== "object") {
1012
+ inst.input.focus(); // restore focus
1013
+ }
1014
+ this._lastInput = null;
1015
+ }
1016
+ },
1017
+
1018
+ /* Update any alternate field to synchronise with the main field. */
1019
+ _updateAlternate: function(inst) {
1020
+ var altFormat, date, dateStr,
1021
+ altField = this._get(inst, "altField");
1022
+
1023
+ if (altField) { // update alternate field too
1024
+ altFormat = this._get(inst, "altFormat") || this._get(inst, "dateFormat");
1025
+ date = this._getDate(inst);
1026
+ dateStr = this.formatDate(altFormat, date, this._getFormatConfig(inst));
1027
+ $(altField).each(function() { $(this).val(dateStr); });
1028
+ }
1029
+ },
1030
+
1031
+ /* Set as beforeShowDay function to prevent selection of weekends.
1032
+ * @param date Date - the date to customise
1033
+ * @return [boolean, string] - is this date selectable?, what is its CSS class?
1034
+ */
1035
+ noWeekends: function(date) {
1036
+ var day = date.getDay();
1037
+ return [(day > 0 && day < 6), ""];
1038
+ },
1039
+
1040
+ /* Set as calculateWeek to determine the week of the year based on the ISO 8601 definition.
1041
+ * @param date Date - the date to get the week for
1042
+ * @return number - the number of the week within the year that contains this date
1043
+ */
1044
+ iso8601Week: function(date) {
1045
+ var time,
1046
+ checkDate = new Date(date.getTime());
1047
+
1048
+ // Find Thursday of this week starting on Monday
1049
+ checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7));
1050
+
1051
+ time = checkDate.getTime();
1052
+ checkDate.setMonth(0); // Compare with Jan 1
1053
+ checkDate.setDate(1);
1054
+ return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
1055
+ },
1056
+
1057
+ /* Parse a string value into a date object.
1058
+ * See formatDate below for the possible formats.
1059
+ *
1060
+ * @param format string - the expected format of the date
1061
+ * @param value string - the date in the above format
1062
+ * @param settings Object - attributes include:
1063
+ * shortYearCutoff number - the cutoff year for determining the century (optional)
1064
+ * dayNamesShort string[7] - abbreviated names of the days from Sunday (optional)
1065
+ * dayNames string[7] - names of the days from Sunday (optional)
1066
+ * monthNamesShort string[12] - abbreviated names of the months (optional)
1067
+ * monthNames string[12] - names of the months (optional)
1068
+ * @return Date - the extracted date value or null if value is blank
1069
+ */
1070
+ parseDate: function (format, value, settings) {
1071
+ if (format == null || value == null) {
1072
+ throw "Invalid arguments";
1073
+ }
1074
+
1075
+ value = (typeof value === "object" ? value.toString() : value + "");
1076
+ if (value === "") {
1077
+ return null;
1078
+ }
1079
+
1080
+ var iFormat, dim, extra,
1081
+ iValue = 0,
1082
+ shortYearCutoffTemp = (settings ? settings.shortYearCutoff : null) || this._defaults.shortYearCutoff,
1083
+ shortYearCutoff = (typeof shortYearCutoffTemp !== "string" ? shortYearCutoffTemp :
1084
+ new Date().getFullYear() % 100 + parseInt(shortYearCutoffTemp, 10)),
1085
+ dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort,
1086
+ dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames,
1087
+ monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort,
1088
+ monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames,
1089
+ year = -1,
1090
+ month = -1,
1091
+ day = -1,
1092
+ doy = -1,
1093
+ literal = false,
1094
+ date,
1095
+ // Check whether a format character is doubled
1096
+ lookAhead = function(match) {
1097
+ var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match);
1098
+ if (matches) {
1099
+ iFormat++;
1100
+ }
1101
+ return matches;
1102
+ },
1103
+ // Extract a number from the string value
1104
+ getNumber = function(match) {
1105
+ var isDoubled = lookAhead(match),
1106
+ size = (match === "@" ? 14 : (match === "!" ? 20 :
1107
+ (match === "y" && isDoubled ? 4 : (match === "o" ? 3 : 2)))),
1108
+ digits = new RegExp("^\\d{1," + size + "}"),
1109
+ num = value.substring(iValue).match(digits);
1110
+ if (!num) {
1111
+ throw "Missing number at position " + iValue;
1112
+ }
1113
+ iValue += num[0].length;
1114
+ return parseInt(num[0], 10);
1115
+ },
1116
+ // Extract a name from the string value and convert to an index
1117
+ getName = function(match, shortNames, longNames) {
1118
+ var index = -1,
1119
+ names = $.map(lookAhead(match) ? longNames : shortNames, function (v, k) {
1120
+ return [ [k, v] ];
1121
+ }).sort(function (a, b) {
1122
+ return -(a[1].length - b[1].length);
1123
+ });
1124
+
1125
+ $.each(names, function (i, pair) {
1126
+ var name = pair[1];
1127
+ if (value.substr(iValue, name.length).toLowerCase() === name.toLowerCase()) {
1128
+ index = pair[0];
1129
+ iValue += name.length;
1130
+ return false;
1131
+ }
1132
+ });
1133
+ if (index !== -1) {
1134
+ return index + 1;
1135
+ } else {
1136
+ throw "Unknown name at position " + iValue;
1137
+ }
1138
+ },
1139
+ // Confirm that a literal character matches the string value
1140
+ checkLiteral = function() {
1141
+ if (value.charAt(iValue) !== format.charAt(iFormat)) {
1142
+ throw "Unexpected literal at position " + iValue;
1143
+ }
1144
+ iValue++;
1145
+ };
1146
+
1147
+ for (iFormat = 0; iFormat < format.length; iFormat++) {
1148
+ if (literal) {
1149
+ if (format.charAt(iFormat) === "'" && !lookAhead("'")) {
1150
+ literal = false;
1151
+ } else {
1152
+ checkLiteral();
1153
+ }
1154
+ } else {
1155
+ switch (format.charAt(iFormat)) {
1156
+ case "d":
1157
+ day = getNumber("d");
1158
+ break;
1159
+ case "D":
1160
+ getName("D", dayNamesShort, dayNames);
1161
+ break;
1162
+ case "o":
1163
+ doy = getNumber("o");
1164
+ break;
1165
+ case "m":
1166
+ month = getNumber("m");
1167
+ break;
1168
+ case "M":
1169
+ month = getName("M", monthNamesShort, monthNames);
1170
+ break;
1171
+ case "y":
1172
+ year = getNumber("y");
1173
+ break;
1174
+ case "@":
1175
+ date = new Date(getNumber("@"));
1176
+ year = date.getFullYear();
1177
+ month = date.getMonth() + 1;
1178
+ day = date.getDate();
1179
+ break;
1180
+ case "!":
1181
+ date = new Date((getNumber("!") - this._ticksTo1970) / 10000);
1182
+ year = date.getFullYear();
1183
+ month = date.getMonth() + 1;
1184
+ day = date.getDate();
1185
+ break;
1186
+ case "'":
1187
+ if (lookAhead("'")){
1188
+ checkLiteral();
1189
+ } else {
1190
+ literal = true;
1191
+ }
1192
+ break;
1193
+ default:
1194
+ checkLiteral();
1195
+ }
1196
+ }
1197
+ }
1198
+
1199
+ if (iValue < value.length){
1200
+ extra = value.substr(iValue);
1201
+ if (!/^\s+/.test(extra)) {
1202
+ throw "Extra/unparsed characters found in date: " + extra;
1203
+ }
1204
+ }
1205
+
1206
+ if (year === -1) {
1207
+ year = new Date().getFullYear();
1208
+ } else if (year < 100) {
1209
+ year += new Date().getFullYear() - new Date().getFullYear() % 100 +
1210
+ (year <= shortYearCutoff ? 0 : -100);
1211
+ }
1212
+
1213
+ if (doy > -1) {
1214
+ month = 1;
1215
+ day = doy;
1216
+ do {
1217
+ dim = this._getDaysInMonth(year, month - 1);
1218
+ if (day <= dim) {
1219
+ break;
1220
+ }
1221
+ month++;
1222
+ day -= dim;
1223
+ } while (true);
1224
+ }
1225
+
1226
+ date = this._daylightSavingAdjust(new Date(year, month - 1, day));
1227
+ if (date.getFullYear() !== year || date.getMonth() + 1 !== month || date.getDate() !== day) {
1228
+ throw "Invalid date"; // E.g. 31/02/00
1229
+ }
1230
+ return date;
1231
+ },
1232
+
1233
+ /* Standard date formats. */
1234
+ ATOM: "yy-mm-dd", // RFC 3339 (ISO 8601)
1235
+ COOKIE: "D, dd M yy",
1236
+ ISO_8601: "yy-mm-dd",
1237
+ RFC_822: "D, d M y",
1238
+ RFC_850: "DD, dd-M-y",
1239
+ RFC_1036: "D, d M y",
1240
+ RFC_1123: "D, d M yy",
1241
+ RFC_2822: "D, d M yy",
1242
+ RSS: "D, d M y", // RFC 822
1243
+ TICKS: "!",
1244
+ TIMESTAMP: "@",
1245
+ W3C: "yy-mm-dd", // ISO 8601
1246
+
1247
+ _ticksTo1970: (((1970 - 1) * 365 + Math.floor(1970 / 4) - Math.floor(1970 / 100) +
1248
+ Math.floor(1970 / 400)) * 24 * 60 * 60 * 10000000),
1249
+
1250
+ /* Format a date object into a string value.
1251
+ * The format can be combinations of the following:
1252
+ * d - day of month (no leading zero)
1253
+ * dd - day of month (two digit)
1254
+ * o - day of year (no leading zeros)
1255
+ * oo - day of year (three digit)
1256
+ * D - day name short
1257
+ * DD - day name long
1258
+ * m - month of year (no leading zero)
1259
+ * mm - month of year (two digit)
1260
+ * M - month name short
1261
+ * MM - month name long
1262
+ * y - year (two digit)
1263
+ * yy - year (four digit)
1264
+ * @ - Unix timestamp (ms since 01/01/1970)
1265
+ * ! - Windows ticks (100ns since 01/01/0001)
1266
+ * "..." - literal text
1267
+ * '' - single quote
1268
+ *
1269
+ * @param format string - the desired format of the date
1270
+ * @param date Date - the date value to format
1271
+ * @param settings Object - attributes include:
1272
+ * dayNamesShort string[7] - abbreviated names of the days from Sunday (optional)
1273
+ * dayNames string[7] - names of the days from Sunday (optional)
1274
+ * monthNamesShort string[12] - abbreviated names of the months (optional)
1275
+ * monthNames string[12] - names of the months (optional)
1276
+ * @return string - the date in the above format
1277
+ */
1278
+ formatDate: function (format, date, settings) {
1279
+ if (!date) {
1280
+ return "";
1281
+ }
1282
+
1283
+ var iFormat,
1284
+ dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort,
1285
+ dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames,
1286
+ monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort,
1287
+ monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames,
1288
+ // Check whether a format character is doubled
1289
+ lookAhead = function(match) {
1290
+ var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match);
1291
+ if (matches) {
1292
+ iFormat++;
1293
+ }
1294
+ return matches;
1295
+ },
1296
+ // Format a number, with leading zero if necessary
1297
+ formatNumber = function(match, value, len) {
1298
+ var num = "" + value;
1299
+ if (lookAhead(match)) {
1300
+ while (num.length < len) {
1301
+ num = "0" + num;
1302
+ }
1303
+ }
1304
+ return num;
1305
+ },
1306
+ // Format a name, short or long as requested
1307
+ formatName = function(match, value, shortNames, longNames) {
1308
+ return (lookAhead(match) ? longNames[value] : shortNames[value]);
1309
+ },
1310
+ output = "",
1311
+ literal = false;
1312
+
1313
+ if (date) {
1314
+ for (iFormat = 0; iFormat < format.length; iFormat++) {
1315
+ if (literal) {
1316
+ if (format.charAt(iFormat) === "'" && !lookAhead("'")) {
1317
+ literal = false;
1318
+ } else {
1319
+ output += format.charAt(iFormat);
1320
+ }
1321
+ } else {
1322
+ switch (format.charAt(iFormat)) {
1323
+ case "d":
1324
+ output += formatNumber("d", date.getDate(), 2);
1325
+ break;
1326
+ case "D":
1327
+ output += formatName("D", date.getDay(), dayNamesShort, dayNames);
1328
+ break;
1329
+ case "o":
1330
+ output += formatNumber("o",
1331
+ Math.round((new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000), 3);
1332
+ break;
1333
+ case "m":
1334
+ output += formatNumber("m", date.getMonth() + 1, 2);
1335
+ break;
1336
+ case "M":
1337
+ output += formatName("M", date.getMonth(), monthNamesShort, monthNames);
1338
+ break;
1339
+ case "y":
1340
+ output += (lookAhead("y") ? date.getFullYear() :
1341
+ (date.getYear() % 100 < 10 ? "0" : "") + date.getYear() % 100);
1342
+ break;
1343
+ case "@":
1344
+ output += date.getTime();
1345
+ break;
1346
+ case "!":
1347
+ output += date.getTime() * 10000 + this._ticksTo1970;
1348
+ break;
1349
+ case "'":
1350
+ if (lookAhead("'")) {
1351
+ output += "'";
1352
+ } else {
1353
+ literal = true;
1354
+ }
1355
+ break;
1356
+ default:
1357
+ output += format.charAt(iFormat);
1358
+ }
1359
+ }
1360
+ }
1361
+ }
1362
+ return output;
1363
+ },
1364
+
1365
+ /* Extract all possible characters from the date format. */
1366
+ _possibleChars: function (format) {
1367
+ var iFormat,
1368
+ chars = "",
1369
+ literal = false,
1370
+ // Check whether a format character is doubled
1371
+ lookAhead = function(match) {
1372
+ var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match);
1373
+ if (matches) {
1374
+ iFormat++;
1375
+ }
1376
+ return matches;
1377
+ };
1378
+
1379
+ for (iFormat = 0; iFormat < format.length; iFormat++) {
1380
+ if (literal) {
1381
+ if (format.charAt(iFormat) === "'" && !lookAhead("'")) {
1382
+ literal = false;
1383
+ } else {
1384
+ chars += format.charAt(iFormat);
1385
+ }
1386
+ } else {
1387
+ switch (format.charAt(iFormat)) {
1388
+ case "d": case "m": case "y": case "@":
1389
+ chars += "0123456789";
1390
+ break;
1391
+ case "D": case "M":
1392
+ return null; // Accept anything
1393
+ case "'":
1394
+ if (lookAhead("'")) {
1395
+ chars += "'";
1396
+ } else {
1397
+ literal = true;
1398
+ }
1399
+ break;
1400
+ default:
1401
+ chars += format.charAt(iFormat);
1402
+ }
1403
+ }
1404
+ }
1405
+ return chars;
1406
+ },
1407
+
1408
+ /* Get a setting value, defaulting if necessary. */
1409
+ _get: function(inst, name) {
1410
+ return inst.settings[name] !== undefined ?
1411
+ inst.settings[name] : this._defaults[name];
1412
+ },
1413
+
1414
+ /* Parse existing date and initialise date picker. */
1415
+ _setDateFromField: function(inst, noDefault) {
1416
+ if (inst.input.val() === inst.lastVal) {
1417
+ return;
1418
+ }
1419
+
1420
+ var dateFormat = this._get(inst, "dateFormat"),
1421
+ dates = inst.lastVal = inst.input ? inst.input.val() : null,
1422
+ defaultDate = this._getDefaultDate(inst),
1423
+ date = defaultDate,
1424
+ settings = this._getFormatConfig(inst);
1425
+
1426
+ try {
1427
+ date = this.parseDate(dateFormat, dates, settings) || defaultDate;
1428
+ } catch (event) {
1429
+ dates = (noDefault ? "" : dates);
1430
+ }
1431
+ inst.selectedDay = date.getDate();
1432
+ inst.drawMonth = inst.selectedMonth = date.getMonth();
1433
+ inst.drawYear = inst.selectedYear = date.getFullYear();
1434
+ inst.currentDay = (dates ? date.getDate() : 0);
1435
+ inst.currentMonth = (dates ? date.getMonth() : 0);
1436
+ inst.currentYear = (dates ? date.getFullYear() : 0);
1437
+ this._adjustInstDate(inst);
1438
+ },
1439
+
1440
+ /* Retrieve the default date shown on opening. */
1441
+ _getDefaultDate: function(inst) {
1442
+ return this._restrictMinMax(inst,
1443
+ this._determineDate(inst, this._get(inst, "defaultDate"), new Date()));
1444
+ },
1445
+
1446
+ /* A date may be specified as an exact value or a relative one. */
1447
+ _determineDate: function(inst, date, defaultDate) {
1448
+ var offsetNumeric = function(offset) {
1449
+ var date = new Date();
1450
+ date.setDate(date.getDate() + offset);
1451
+ return date;
1452
+ },
1453
+ offsetString = function(offset) {
1454
+ try {
1455
+ return $.datepicker.parseDate($.datepicker._get(inst, "dateFormat"),
1456
+ offset, $.datepicker._getFormatConfig(inst));
1457
+ }
1458
+ catch (e) {
1459
+ // Ignore
1460
+ }
1461
+
1462
+ var date = (offset.toLowerCase().match(/^c/) ?
1463
+ $.datepicker._getDate(inst) : null) || new Date(),
1464
+ year = date.getFullYear(),
1465
+ month = date.getMonth(),
1466
+ day = date.getDate(),
1467
+ pattern = /([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g,
1468
+ matches = pattern.exec(offset);
1469
+
1470
+ while (matches) {
1471
+ switch (matches[2] || "d") {
1472
+ case "d" : case "D" :
1473
+ day += parseInt(matches[1],10); break;
1474
+ case "w" : case "W" :
1475
+ day += parseInt(matches[1],10) * 7; break;
1476
+ case "m" : case "M" :
1477
+ month += parseInt(matches[1],10);
1478
+ day = Math.min(day, $.datepicker._getDaysInMonth(year, month));
1479
+ break;
1480
+ case "y": case "Y" :
1481
+ year += parseInt(matches[1],10);
1482
+ day = Math.min(day, $.datepicker._getDaysInMonth(year, month));
1483
+ break;
1484
+ }
1485
+ matches = pattern.exec(offset);
1486
+ }
1487
+ return new Date(year, month, day);
1488
+ },
1489
+ newDate = (date == null || date === "" ? defaultDate : (typeof date === "string" ? offsetString(date) :
1490
+ (typeof date === "number" ? (isNaN(date) ? defaultDate : offsetNumeric(date)) : new Date(date.getTime()))));
1491
+
1492
+ newDate = (newDate && newDate.toString() === "Invalid Date" ? defaultDate : newDate);
1493
+ if (newDate) {
1494
+ newDate.setHours(0);
1495
+ newDate.setMinutes(0);
1496
+ newDate.setSeconds(0);
1497
+ newDate.setMilliseconds(0);
1498
+ }
1499
+ return this._daylightSavingAdjust(newDate);
1500
+ },
1501
+
1502
+ /* Handle switch to/from daylight saving.
1503
+ * Hours may be non-zero on daylight saving cut-over:
1504
+ * > 12 when midnight changeover, but then cannot generate
1505
+ * midnight datetime, so jump to 1AM, otherwise reset.
1506
+ * @param date (Date) the date to check
1507
+ * @return (Date) the corrected date
1508
+ */
1509
+ _daylightSavingAdjust: function(date) {
1510
+ if (!date) {
1511
+ return null;
1512
+ }
1513
+ date.setHours(date.getHours() > 12 ? date.getHours() + 2 : 0);
1514
+ return date;
1515
+ },
1516
+
1517
+ /* Set the date(s) directly. */
1518
+ _setDate: function(inst, date, noChange) {
1519
+ var clear = !date,
1520
+ origMonth = inst.selectedMonth,
1521
+ origYear = inst.selectedYear,
1522
+ newDate = this._restrictMinMax(inst, this._determineDate(inst, date, new Date()));
1523
+
1524
+ inst.selectedDay = inst.currentDay = newDate.getDate();
1525
+ inst.drawMonth = inst.selectedMonth = inst.currentMonth = newDate.getMonth();
1526
+ inst.drawYear = inst.selectedYear = inst.currentYear = newDate.getFullYear();
1527
+ if ((origMonth !== inst.selectedMonth || origYear !== inst.selectedYear) && !noChange) {
1528
+ this._notifyChange(inst);
1529
+ }
1530
+ this._adjustInstDate(inst);
1531
+ if (inst.input) {
1532
+ inst.input.val(clear ? "" : this._formatDate(inst));
1533
+ }
1534
+ },
1535
+
1536
+ /* Retrieve the date(s) directly. */
1537
+ _getDate: function(inst) {
1538
+ var startDate = (!inst.currentYear || (inst.input && inst.input.val() === "") ? null :
1539
+ this._daylightSavingAdjust(new Date(
1540
+ inst.currentYear, inst.currentMonth, inst.currentDay)));
1541
+ return startDate;
1542
+ },
1543
+
1544
+ /* Attach the onxxx handlers. These are declared statically so
1545
+ * they work with static code transformers like Caja.
1546
+ */
1547
+ _attachHandlers: function(inst) {
1548
+ var stepMonths = this._get(inst, "stepMonths"),
1549
+ id = "#" + inst.id.replace( /\\\\/g, "\\" );
1550
+ inst.dpDiv.find("[data-handler]").map(function () {
1551
+ var handler = {
1552
+ prev: function () {
1553
+ $.datepicker._adjustDate(id, -stepMonths, "M");
1554
+ },
1555
+ next: function () {
1556
+ $.datepicker._adjustDate(id, +stepMonths, "M");
1557
+ },
1558
+ hide: function () {
1559
+ $.datepicker._hideDatepicker();
1560
+ },
1561
+ today: function () {
1562
+ $.datepicker._gotoToday(id);
1563
+ },
1564
+ selectDay: function () {
1565
+ $.datepicker._selectDay(id, +this.getAttribute("data-month"), +this.getAttribute("data-year"), this);
1566
+ return false;
1567
+ },
1568
+ selectMonth: function () {
1569
+ $.datepicker._selectMonthYear(id, this, "M");
1570
+ return false;
1571
+ },
1572
+ selectYear: function () {
1573
+ $.datepicker._selectMonthYear(id, this, "Y");
1574
+ return false;
1575
+ }
1576
+ };
1577
+ $(this).bind(this.getAttribute("data-event"), handler[this.getAttribute("data-handler")]);
1578
+ });
1579
+ },
1580
+
1581
+ /* Generate the HTML for the current state of the date picker. */
1582
+ _generateHTML: function(inst) {
1583
+ var maxDraw, prevText, prev, nextText, next, currentText, gotoDate,
1584
+ controls, buttonPanel, firstDay, showWeek, dayNames, dayNamesMin,
1585
+ monthNames, monthNamesShort, beforeShowDay, showOtherMonths,
1586
+ selectOtherMonths, defaultDate, html, dow, row, group, col, selectedDate,
1587
+ cornerClass, calender, thead, day, daysInMonth, leadDays, curRows, numRows,
1588
+ printDate, dRow, tbody, daySettings, otherMonth, unselectable,
1589
+ tempDate = new Date(),
1590
+ today = this._daylightSavingAdjust(
1591
+ new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate())), // clear time
1592
+ isRTL = this._get(inst, "isRTL"),
1593
+ showButtonPanel = this._get(inst, "showButtonPanel"),
1594
+ hideIfNoPrevNext = this._get(inst, "hideIfNoPrevNext"),
1595
+ navigationAsDateFormat = this._get(inst, "navigationAsDateFormat"),
1596
+ numMonths = this._getNumberOfMonths(inst),
1597
+ showCurrentAtPos = this._get(inst, "showCurrentAtPos"),
1598
+ stepMonths = this._get(inst, "stepMonths"),
1599
+ isMultiMonth = (numMonths[0] !== 1 || numMonths[1] !== 1),
1600
+ currentDate = this._daylightSavingAdjust((!inst.currentDay ? new Date(9999, 9, 9) :
1601
+ new Date(inst.currentYear, inst.currentMonth, inst.currentDay))),
1602
+ minDate = this._getMinMaxDate(inst, "min"),
1603
+ maxDate = this._getMinMaxDate(inst, "max"),
1604
+ drawMonth = inst.drawMonth - showCurrentAtPos,
1605
+ drawYear = inst.drawYear;
1606
+
1607
+ if (drawMonth < 0) {
1608
+ drawMonth += 12;
1609
+ drawYear--;
1610
+ }
1611
+ if (maxDate) {
1612
+ maxDraw = this._daylightSavingAdjust(new Date(maxDate.getFullYear(),
1613
+ maxDate.getMonth() - (numMonths[0] * numMonths[1]) + 1, maxDate.getDate()));
1614
+ maxDraw = (minDate && maxDraw < minDate ? minDate : maxDraw);
1615
+ while (this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1)) > maxDraw) {
1616
+ drawMonth--;
1617
+ if (drawMonth < 0) {
1618
+ drawMonth = 11;
1619
+ drawYear--;
1620
+ }
1621
+ }
1622
+ }
1623
+ inst.drawMonth = drawMonth;
1624
+ inst.drawYear = drawYear;
1625
+
1626
+ prevText = this._get(inst, "prevText");
1627
+ prevText = (!navigationAsDateFormat ? prevText : this.formatDate(prevText,
1628
+ this._daylightSavingAdjust(new Date(drawYear, drawMonth - stepMonths, 1)),
1629
+ this._getFormatConfig(inst)));
1630
+
1631
+ prev = (this._canAdjustMonth(inst, -1, drawYear, drawMonth) ?
1632
+ "<a class='ui-datepicker-prev ui-corner-all' data-handler='prev' data-event='click'" +
1633
+ " title='" + prevText + "'><span class='ui-icon ui-icon-circle-triangle-" + ( isRTL ? "e" : "w") + "'>" + prevText + "</span></a>" :
1634
+ (hideIfNoPrevNext ? "" : "<a class='ui-datepicker-prev ui-corner-all ui-state-disabled' title='"+ prevText +"'><span class='ui-icon ui-icon-circle-triangle-" + ( isRTL ? "e" : "w") + "'>" + prevText + "</span></a>"));
1635
+
1636
+ nextText = this._get(inst, "nextText");
1637
+ nextText = (!navigationAsDateFormat ? nextText : this.formatDate(nextText,
1638
+ this._daylightSavingAdjust(new Date(drawYear, drawMonth + stepMonths, 1)),
1639
+ this._getFormatConfig(inst)));
1640
+
1641
+ next = (this._canAdjustMonth(inst, +1, drawYear, drawMonth) ?
1642
+ "<a class='ui-datepicker-next ui-corner-all' data-handler='next' data-event='click'" +
1643
+ " title='" + nextText + "'><span class='ui-icon ui-icon-circle-triangle-" + ( isRTL ? "w" : "e") + "'>" + nextText + "</span></a>" :
1644
+ (hideIfNoPrevNext ? "" : "<a class='ui-datepicker-next ui-corner-all ui-state-disabled' title='"+ nextText + "'><span class='ui-icon ui-icon-circle-triangle-" + ( isRTL ? "w" : "e") + "'>" + nextText + "</span></a>"));
1645
+
1646
+ currentText = this._get(inst, "currentText");
1647
+ gotoDate = (this._get(inst, "gotoCurrent") && inst.currentDay ? currentDate : today);
1648
+ currentText = (!navigationAsDateFormat ? currentText :
1649
+ this.formatDate(currentText, gotoDate, this._getFormatConfig(inst)));
1650
+
1651
+ controls = (!inst.inline ? "<button type='button' class='ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all' data-handler='hide' data-event='click'>" +
1652
+ this._get(inst, "closeText") + "</button>" : "");
1653
+
1654
+ buttonPanel = (showButtonPanel) ? "<div class='ui-datepicker-buttonpane ui-widget-content'>" + (isRTL ? controls : "") +
1655
+ (this._isInRange(inst, gotoDate) ? "<button type='button' class='ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all' data-handler='today' data-event='click'" +
1656
+ ">" + currentText + "</button>" : "") + (isRTL ? "" : controls) + "</div>" : "";
1657
+
1658
+ firstDay = parseInt(this._get(inst, "firstDay"),10);
1659
+ firstDay = (isNaN(firstDay) ? 0 : firstDay);
1660
+
1661
+ showWeek = this._get(inst, "showWeek");
1662
+ dayNames = this._get(inst, "dayNames");
1663
+ dayNamesMin = this._get(inst, "dayNamesMin");
1664
+ monthNames = this._get(inst, "monthNames");
1665
+ monthNamesShort = this._get(inst, "monthNamesShort");
1666
+ beforeShowDay = this._get(inst, "beforeShowDay");
1667
+ showOtherMonths = this._get(inst, "showOtherMonths");
1668
+ selectOtherMonths = this._get(inst, "selectOtherMonths");
1669
+ defaultDate = this._getDefaultDate(inst);
1670
+ html = "";
1671
+ dow;
1672
+ for (row = 0; row < numMonths[0]; row++) {
1673
+ group = "";
1674
+ this.maxRows = 4;
1675
+ for (col = 0; col < numMonths[1]; col++) {
1676
+ selectedDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, inst.selectedDay));
1677
+ cornerClass = " ui-corner-all";
1678
+ calender = "";
1679
+ if (isMultiMonth) {
1680
+ calender += "<div class='ui-datepicker-group";
1681
+ if (numMonths[1] > 1) {
1682
+ switch (col) {
1683
+ case 0: calender += " ui-datepicker-group-first";
1684
+ cornerClass = " ui-corner-" + (isRTL ? "right" : "left"); break;
1685
+ case numMonths[1]-1: calender += " ui-datepicker-group-last";
1686
+ cornerClass = " ui-corner-" + (isRTL ? "left" : "right"); break;
1687
+ default: calender += " ui-datepicker-group-middle"; cornerClass = ""; break;
1688
+ }
1689
+ }
1690
+ calender += "'>";
1691
+ }
1692
+ calender += "<div class='ui-datepicker-header ui-widget-header ui-helper-clearfix" + cornerClass + "'>" +
1693
+ (/all|left/.test(cornerClass) && row === 0 ? (isRTL ? next : prev) : "") +
1694
+ (/all|right/.test(cornerClass) && row === 0 ? (isRTL ? prev : next) : "") +
1695
+ this._generateMonthYearHeader(inst, drawMonth, drawYear, minDate, maxDate,
1696
+ row > 0 || col > 0, monthNames, monthNamesShort) + // draw month headers
1697
+ "</div><table class='ui-datepicker-calendar'><thead>" +
1698
+ "<tr>";
1699
+ thead = (showWeek ? "<th class='ui-datepicker-week-col'>" + this._get(inst, "weekHeader") + "</th>" : "");
1700
+ for (dow = 0; dow < 7; dow++) { // days of the week
1701
+ day = (dow + firstDay) % 7;
1702
+ thead += "<th" + ((dow + firstDay + 6) % 7 >= 5 ? " class='ui-datepicker-week-end'" : "") + ">" +
1703
+ "<span title='" + dayNames[day] + "'>" + dayNamesMin[day] + "</span></th>";
1704
+ }
1705
+ calender += thead + "</tr></thead><tbody>";
1706
+ daysInMonth = this._getDaysInMonth(drawYear, drawMonth);
1707
+ if (drawYear === inst.selectedYear && drawMonth === inst.selectedMonth) {
1708
+ inst.selectedDay = Math.min(inst.selectedDay, daysInMonth);
1709
+ }
1710
+ leadDays = (this._getFirstDayOfMonth(drawYear, drawMonth) - firstDay + 7) % 7;
1711
+ curRows = Math.ceil((leadDays + daysInMonth) / 7); // calculate the number of rows to generate
1712
+ numRows = (isMultiMonth ? this.maxRows > curRows ? this.maxRows : curRows : curRows); //If multiple months, use the higher number of rows (see #7043)
1713
+ this.maxRows = numRows;
1714
+ printDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1 - leadDays));
1715
+ for (dRow = 0; dRow < numRows; dRow++) { // create date picker rows
1716
+ calender += "<tr>";
1717
+ tbody = (!showWeek ? "" : "<td class='ui-datepicker-week-col'>" +
1718
+ this._get(inst, "calculateWeek")(printDate) + "</td>");
1719
+ for (dow = 0; dow < 7; dow++) { // create date picker days
1720
+ daySettings = (beforeShowDay ?
1721
+ beforeShowDay.apply((inst.input ? inst.input[0] : null), [printDate]) : [true, ""]);
1722
+ otherMonth = (printDate.getMonth() !== drawMonth);
1723
+ unselectable = (otherMonth && !selectOtherMonths) || !daySettings[0] ||
1724
+ (minDate && printDate < minDate) || (maxDate && printDate > maxDate);
1725
+ tbody += "<td class='" +
1726
+ ((dow + firstDay + 6) % 7 >= 5 ? " ui-datepicker-week-end" : "") + // highlight weekends
1727
+ (otherMonth ? " ui-datepicker-other-month" : "") + // highlight days from other months
1728
+ ((printDate.getTime() === selectedDate.getTime() && drawMonth === inst.selectedMonth && inst._keyEvent) || // user pressed key
1729
+ (defaultDate.getTime() === printDate.getTime() && defaultDate.getTime() === selectedDate.getTime()) ?
1730
+ // or defaultDate is current printedDate and defaultDate is selectedDate
1731
+ " " + this._dayOverClass : "") + // highlight selected day
1732
+ (unselectable ? " " + this._unselectableClass + " ui-state-disabled": "") + // highlight unselectable days
1733
+ (otherMonth && !showOtherMonths ? "" : " " + daySettings[1] + // highlight custom dates
1734
+ (printDate.getTime() === currentDate.getTime() ? " " + this._currentClass : "") + // highlight selected day
1735
+ (printDate.getTime() === today.getTime() ? " ui-datepicker-today" : "")) + "'" + // highlight today (if different)
1736
+ ((!otherMonth || showOtherMonths) && daySettings[2] ? " title='" + daySettings[2].replace(/'/g, "&#39;") + "'" : "") + // cell title
1737
+ (unselectable ? "" : " data-handler='selectDay' data-event='click' data-month='" + printDate.getMonth() + "' data-year='" + printDate.getFullYear() + "'") + ">" + // actions
1738
+ (otherMonth && !showOtherMonths ? "&#xa0;" : // display for other months
1739
+ (unselectable ? "<span class='ui-state-default'>" + printDate.getDate() + "</span>" : "<a class='ui-state-default" +
1740
+ (printDate.getTime() === today.getTime() ? " ui-state-highlight" : "") +
1741
+ (printDate.getTime() === currentDate.getTime() ? " ui-state-active" : "") + // highlight selected day
1742
+ (otherMonth ? " ui-priority-secondary" : "") + // distinguish dates from other months
1743
+ "' href='#'>" + printDate.getDate() + "</a>")) + "</td>"; // display selectable date
1744
+ printDate.setDate(printDate.getDate() + 1);
1745
+ printDate = this._daylightSavingAdjust(printDate);
1746
+ }
1747
+ calender += tbody + "</tr>";
1748
+ }
1749
+ drawMonth++;
1750
+ if (drawMonth > 11) {
1751
+ drawMonth = 0;
1752
+ drawYear++;
1753
+ }
1754
+ calender += "</tbody></table>" + (isMultiMonth ? "</div>" +
1755
+ ((numMonths[0] > 0 && col === numMonths[1]-1) ? "<div class='ui-datepicker-row-break'></div>" : "") : "");
1756
+ group += calender;
1757
+ }
1758
+ html += group;
1759
+ }
1760
+ html += buttonPanel;
1761
+ inst._keyEvent = false;
1762
+ return html;
1763
+ },
1764
+
1765
+ /* Generate the month and year header. */
1766
+ _generateMonthYearHeader: function(inst, drawMonth, drawYear, minDate, maxDate,
1767
+ secondary, monthNames, monthNamesShort) {
1768
+
1769
+ var inMinYear, inMaxYear, month, years, thisYear, determineYear, year, endYear,
1770
+ changeMonth = this._get(inst, "changeMonth"),
1771
+ changeYear = this._get(inst, "changeYear"),
1772
+ showMonthAfterYear = this._get(inst, "showMonthAfterYear"),
1773
+ html = "<div class='ui-datepicker-title'>",
1774
+ monthHtml = "";
1775
+
1776
+ // month selection
1777
+ if (secondary || !changeMonth) {
1778
+ monthHtml += "<span class='ui-datepicker-month'>" + monthNames[drawMonth] + "</span>";
1779
+ } else {
1780
+ inMinYear = (minDate && minDate.getFullYear() === drawYear);
1781
+ inMaxYear = (maxDate && maxDate.getFullYear() === drawYear);
1782
+ monthHtml += "<select class='ui-datepicker-month' data-handler='selectMonth' data-event='change'>";
1783
+ for ( month = 0; month < 12; month++) {
1784
+ if ((!inMinYear || month >= minDate.getMonth()) && (!inMaxYear || month <= maxDate.getMonth())) {
1785
+ monthHtml += "<option value='" + month + "'" +
1786
+ (month === drawMonth ? " selected='selected'" : "") +
1787
+ ">" + monthNamesShort[month] + "</option>";
1788
+ }
1789
+ }
1790
+ monthHtml += "</select>";
1791
+ }
1792
+
1793
+ if (!showMonthAfterYear) {
1794
+ html += monthHtml + (secondary || !(changeMonth && changeYear) ? "&#xa0;" : "");
1795
+ }
1796
+
1797
+ // year selection
1798
+ if ( !inst.yearshtml ) {
1799
+ inst.yearshtml = "";
1800
+ if (secondary || !changeYear) {
1801
+ html += "<span class='ui-datepicker-year'>" + drawYear + "</span>";
1802
+ } else {
1803
+ // determine range of years to display
1804
+ years = this._get(inst, "yearRange").split(":");
1805
+ thisYear = new Date().getFullYear();
1806
+ determineYear = function(value) {
1807
+ var year = (value.match(/c[+\-].*/) ? drawYear + parseInt(value.substring(1), 10) :
1808
+ (value.match(/[+\-].*/) ? thisYear + parseInt(value, 10) :
1809
+ parseInt(value, 10)));
1810
+ return (isNaN(year) ? thisYear : year);
1811
+ };
1812
+ year = determineYear(years[0]);
1813
+ endYear = Math.max(year, determineYear(years[1] || ""));
1814
+ year = (minDate ? Math.max(year, minDate.getFullYear()) : year);
1815
+ endYear = (maxDate ? Math.min(endYear, maxDate.getFullYear()) : endYear);
1816
+ inst.yearshtml += "<select class='ui-datepicker-year' data-handler='selectYear' data-event='change'>";
1817
+ for (; year <= endYear; year++) {
1818
+ inst.yearshtml += "<option value='" + year + "'" +
1819
+ (year === drawYear ? " selected='selected'" : "") +
1820
+ ">" + year + "</option>";
1821
+ }
1822
+ inst.yearshtml += "</select>";
1823
+
1824
+ html += inst.yearshtml;
1825
+ inst.yearshtml = null;
1826
+ }
1827
+ }
1828
+
1829
+ html += this._get(inst, "yearSuffix");
1830
+ if (showMonthAfterYear) {
1831
+ html += (secondary || !(changeMonth && changeYear) ? "&#xa0;" : "") + monthHtml;
1832
+ }
1833
+ html += "</div>"; // Close datepicker_header
1834
+ return html;
1835
+ },
1836
+
1837
+ /* Adjust one of the date sub-fields. */
1838
+ _adjustInstDate: function(inst, offset, period) {
1839
+ var year = inst.drawYear + (period === "Y" ? offset : 0),
1840
+ month = inst.drawMonth + (period === "M" ? offset : 0),
1841
+ day = Math.min(inst.selectedDay, this._getDaysInMonth(year, month)) + (period === "D" ? offset : 0),
1842
+ date = this._restrictMinMax(inst, this._daylightSavingAdjust(new Date(year, month, day)));
1843
+
1844
+ inst.selectedDay = date.getDate();
1845
+ inst.drawMonth = inst.selectedMonth = date.getMonth();
1846
+ inst.drawYear = inst.selectedYear = date.getFullYear();
1847
+ if (period === "M" || period === "Y") {
1848
+ this._notifyChange(inst);
1849
+ }
1850
+ },
1851
+
1852
+ /* Ensure a date is within any min/max bounds. */
1853
+ _restrictMinMax: function(inst, date) {
1854
+ var minDate = this._getMinMaxDate(inst, "min"),
1855
+ maxDate = this._getMinMaxDate(inst, "max"),
1856
+ newDate = (minDate && date < minDate ? minDate : date);
1857
+ return (maxDate && newDate > maxDate ? maxDate : newDate);
1858
+ },
1859
+
1860
+ /* Notify change of month/year. */
1861
+ _notifyChange: function(inst) {
1862
+ var onChange = this._get(inst, "onChangeMonthYear");
1863
+ if (onChange) {
1864
+ onChange.apply((inst.input ? inst.input[0] : null),
1865
+ [inst.selectedYear, inst.selectedMonth + 1, inst]);
1866
+ }
1867
+ },
1868
+
1869
+ /* Determine the number of months to show. */
1870
+ _getNumberOfMonths: function(inst) {
1871
+ var numMonths = this._get(inst, "numberOfMonths");
1872
+ return (numMonths == null ? [1, 1] : (typeof numMonths === "number" ? [1, numMonths] : numMonths));
1873
+ },
1874
+
1875
+ /* Determine the current maximum date - ensure no time components are set. */
1876
+ _getMinMaxDate: function(inst, minMax) {
1877
+ return this._determineDate(inst, this._get(inst, minMax + "Date"), null);
1878
+ },
1879
+
1880
+ /* Find the number of days in a given month. */
1881
+ _getDaysInMonth: function(year, month) {
1882
+ return 32 - this._daylightSavingAdjust(new Date(year, month, 32)).getDate();
1883
+ },
1884
+
1885
+ /* Find the day of the week of the first of a month. */
1886
+ _getFirstDayOfMonth: function(year, month) {
1887
+ return new Date(year, month, 1).getDay();
1888
+ },
1889
+
1890
+ /* Determines if we should allow a "next/prev" month display change. */
1891
+ _canAdjustMonth: function(inst, offset, curYear, curMonth) {
1892
+ var numMonths = this._getNumberOfMonths(inst),
1893
+ date = this._daylightSavingAdjust(new Date(curYear,
1894
+ curMonth + (offset < 0 ? offset : numMonths[0] * numMonths[1]), 1));
1895
+
1896
+ if (offset < 0) {
1897
+ date.setDate(this._getDaysInMonth(date.getFullYear(), date.getMonth()));
1898
+ }
1899
+ return this._isInRange(inst, date);
1900
+ },
1901
+
1902
+ /* Is the given date in the accepted range? */
1903
+ _isInRange: function(inst, date) {
1904
+ var yearSplit, currentYear,
1905
+ minDate = this._getMinMaxDate(inst, "min"),
1906
+ maxDate = this._getMinMaxDate(inst, "max"),
1907
+ minYear = null,
1908
+ maxYear = null,
1909
+ years = this._get(inst, "yearRange");
1910
+ if (years){
1911
+ yearSplit = years.split(":");
1912
+ currentYear = new Date().getFullYear();
1913
+ minYear = parseInt(yearSplit[0], 10);
1914
+ maxYear = parseInt(yearSplit[1], 10);
1915
+ if ( yearSplit[0].match(/[+\-].*/) ) {
1916
+ minYear += currentYear;
1917
+ }
1918
+ if ( yearSplit[1].match(/[+\-].*/) ) {
1919
+ maxYear += currentYear;
1920
+ }
1921
+ }
1922
+
1923
+ return ((!minDate || date.getTime() >= minDate.getTime()) &&
1924
+ (!maxDate || date.getTime() <= maxDate.getTime()) &&
1925
+ (!minYear || date.getFullYear() >= minYear) &&
1926
+ (!maxYear || date.getFullYear() <= maxYear));
1927
+ },
1928
+
1929
+ /* Provide the configuration settings for formatting/parsing. */
1930
+ _getFormatConfig: function(inst) {
1931
+ var shortYearCutoff = this._get(inst, "shortYearCutoff");
1932
+ shortYearCutoff = (typeof shortYearCutoff !== "string" ? shortYearCutoff :
1933
+ new Date().getFullYear() % 100 + parseInt(shortYearCutoff, 10));
1934
+ return {shortYearCutoff: shortYearCutoff,
1935
+ dayNamesShort: this._get(inst, "dayNamesShort"), dayNames: this._get(inst, "dayNames"),
1936
+ monthNamesShort: this._get(inst, "monthNamesShort"), monthNames: this._get(inst, "monthNames")};
1937
+ },
1938
+
1939
+ /* Format the given date for display. */
1940
+ _formatDate: function(inst, day, month, year) {
1941
+ if (!day) {
1942
+ inst.currentDay = inst.selectedDay;
1943
+ inst.currentMonth = inst.selectedMonth;
1944
+ inst.currentYear = inst.selectedYear;
1945
+ }
1946
+ var date = (day ? (typeof day === "object" ? day :
1947
+ this._daylightSavingAdjust(new Date(year, month, day))) :
1948
+ this._daylightSavingAdjust(new Date(inst.currentYear, inst.currentMonth, inst.currentDay)));
1949
+ return this.formatDate(this._get(inst, "dateFormat"), date, this._getFormatConfig(inst));
1950
+ }
1951
+ });
1952
+
1953
+ /*
1954
+ * Bind hover events for datepicker elements.
1955
+ * Done via delegate so the binding only occurs once in the lifetime of the parent div.
1956
+ * Global instActive, set by _updateDatepicker allows the handlers to find their way back to the active picker.
1957
+ */
1958
+ function bindHover(dpDiv) {
1959
+ var selector = "button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";
1960
+ return dpDiv.delegate(selector, "mouseout", function() {
1961
+ $(this).removeClass("ui-state-hover");
1962
+ if (this.className.indexOf("ui-datepicker-prev") !== -1) {
1963
+ $(this).removeClass("ui-datepicker-prev-hover");
1964
+ }
1965
+ if (this.className.indexOf("ui-datepicker-next") !== -1) {
1966
+ $(this).removeClass("ui-datepicker-next-hover");
1967
+ }
1968
+ })
1969
+ .delegate(selector, "mouseover", function(){
1970
+ if (!$.datepicker._isDisabledDatepicker( instActive.inline ? dpDiv.parent()[0] : instActive.input[0])) {
1971
+ $(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover");
1972
+ $(this).addClass("ui-state-hover");
1973
+ if (this.className.indexOf("ui-datepicker-prev") !== -1) {
1974
+ $(this).addClass("ui-datepicker-prev-hover");
1975
+ }
1976
+ if (this.className.indexOf("ui-datepicker-next") !== -1) {
1977
+ $(this).addClass("ui-datepicker-next-hover");
1978
+ }
1979
+ }
1980
+ });
1981
+ }
1982
+
1983
+ /* jQuery extend now ignores nulls! */
1984
+ function extendRemove(target, props) {
1985
+ $.extend(target, props);
1986
+ for (var name in props) {
1987
+ if (props[name] == null) {
1988
+ target[name] = props[name];
1989
+ }
1990
+ }
1991
+ return target;
1992
+ }
1993
+
1994
+ /* Invoke the datepicker functionality.
1995
+ @param options string - a command, optionally followed by additional parameters or
1996
+ Object - settings for attaching new datepicker functionality
1997
+ @return jQuery object */
1998
+ $.fn.datepicker = function(options){
1999
+
2000
+ /* Verify an empty collection wasn't passed - Fixes #6976 */
2001
+ if ( !this.length ) {
2002
+ return this;
2003
+ }
2004
+
2005
+ /* Initialise the date picker. */
2006
+ if (!$.datepicker.initialized) {
2007
+ $(document).mousedown($.datepicker._checkExternalClick);
2008
+ $.datepicker.initialized = true;
2009
+ }
2010
+
2011
+ /* Append datepicker main container to body if not exist. */
2012
+ if ($("#"+$.datepicker._mainDivId).length === 0) {
2013
+ $("body").append($.datepicker.dpDiv);
2014
+ }
2015
+
2016
+ var otherArgs = Array.prototype.slice.call(arguments, 1);
2017
+ if (typeof options === "string" && (options === "isDisabled" || options === "getDate" || options === "widget")) {
2018
+ return $.datepicker["_" + options + "Datepicker"].
2019
+ apply($.datepicker, [this[0]].concat(otherArgs));
2020
+ }
2021
+ if (options === "option" && arguments.length === 2 && typeof arguments[1] === "string") {
2022
+ return $.datepicker["_" + options + "Datepicker"].
2023
+ apply($.datepicker, [this[0]].concat(otherArgs));
2024
+ }
2025
+ return this.each(function() {
2026
+ typeof options === "string" ?
2027
+ $.datepicker["_" + options + "Datepicker"].
2028
+ apply($.datepicker, [this].concat(otherArgs)) :
2029
+ $.datepicker._attachDatepicker(this, options);
2030
+ });
2031
+ };
2032
+
2033
+ $.datepicker = new Datepicker(); // singleton instance
2034
+ $.datepicker.initialized = false;
2035
+ $.datepicker.uuid = new Date().getTime();
2036
+ $.datepicker.version = "1.10.3";
2037
+
2038
+ })(jQuery);
view/js/sendgrid-stats.js ADDED
@@ -0,0 +1,434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ jQuery(document).ready(function($){
2
+ var defaultDaysBefore = 7;
3
+
4
+ /* Initialize datepicker */
5
+ var date = new Date();
6
+ jQuery( "#sendgrid-start-date" ).datepicker({
7
+ dateFormat: "yy-mm-dd",
8
+ changeMonth: true,
9
+ maxDate: _dateToYMD(new Date()),
10
+ onClose: function( selectedDate ) {
11
+ $( "#sendgrid-end-date" ).datepicker( "option", "minDate", selectedDate );
12
+ }
13
+ });
14
+ var startDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() - defaultDaysBefore);
15
+ $('#sendgrid-start-date').datepicker("setDate", startDate);
16
+ jQuery( "#sendgrid-end-date" ).datepicker({
17
+ dateFormat: "yy-mm-dd",
18
+ changeMonth: true,
19
+ maxDate: _dateToYMD(new Date()),
20
+ onClose: function( selectedDate ) {
21
+ $( "#sendgrid-start-date" ).datepicker( "option", "maxDate", selectedDate );
22
+ }
23
+ });
24
+ var endDate = new Date(date.getFullYear(),date.getMonth(),date.getDate());
25
+ $('#sendgrid-end-date').datepicker("setDate", endDate);
26
+
27
+ /* Apply filter */
28
+ var filterType = $("#sendgrid-apply-filter").attr("data-filter");
29
+ jQuery("#sendgrid-apply-filter").click(function(event) {
30
+ event.preventDefault();
31
+
32
+ getStats(jQuery("#sendgrid-start-date").val(), jQuery("#sendgrid-end-date").val(), 'sendgrid_get_stats');
33
+ });
34
+
35
+ /* Make charts responsive in statistics page, reload charts when window is resized */
36
+ if (filterType === "sendgrid-statistics")
37
+ {
38
+ jQuery("#collapse-menu, input[name='screen_columns']").click(function(event) {
39
+ getStats(jQuery("#sendgrid-start-date").val(), jQuery("#sendgrid-end-date").val(), 'sendgrid_get_stats');
40
+ });
41
+ window.onresize = function(event) {
42
+ getStats(jQuery("#sendgrid-start-date").val(), jQuery("#sendgrid-end-date").val(), 'sendgrid_get_stats');
43
+ };
44
+ }
45
+
46
+ /* Get Statistics and show chart */
47
+
48
+ /**
49
+ * Show laoder, make ajax request and get statistics, prepare data for charts
50
+ *
51
+ * @param {String} startDate
52
+ * @param {String} endDate
53
+ * @param {String} action
54
+ * @returns {Void}
55
+ */
56
+ getStats(_dateToYMD(startDate), _dateToYMD(endDate), 'sendgrid_get_stats');
57
+ function getStats(startDate, endDate, action)
58
+ {
59
+ $(".sendgrid-container .sendgrid-stats").html("");
60
+
61
+ /* Show laoders */
62
+ $(".sendgrid-container .loading, .sendgrid-filters-container .loading").show();
63
+
64
+ data = {
65
+ action: action,
66
+ start_date: startDate,
67
+ end_date: endDate,
68
+ sendgrid_nonce: sendgrid_vars.sendgrid_nonce
69
+ };
70
+
71
+ /* Make request and prepare data */
72
+ $.post(ajaxurl, data, function(response) {
73
+ var requestStats = [];
74
+ var deliveredStats = [];
75
+ var openStats = [];
76
+ var uniqueOpenStats = [];
77
+ var clickStats = [];
78
+ var uniqueClickStats = [];
79
+ var unsubscribeStats = [];
80
+ var bounceStats = [];
81
+ var spamreportStats = [];
82
+ var dropStats = [];
83
+ var blockStats = [];
84
+
85
+ var requests = 0;
86
+ var opens = 0;
87
+ var clicks = 0;
88
+ var deliveres = 0;
89
+ var bounces = 0;
90
+ var unsubscribes = 0;
91
+ var spamReports = 0;
92
+ var spamDrop = 0;
93
+ var repeatBounces = 0;
94
+ var repeatSpamreports = 0;
95
+ var repeatUnsubscribes = 0;
96
+ var drops = 0;
97
+ var blocks = 0;
98
+ var uniqueOpens = 0;
99
+
100
+ /* Get stats from request */
101
+ response = jQuery.parseJSON(response);
102
+ jQuery.each(response, function(key, value) {
103
+ var dateString = _splitDate(value.date);
104
+ var date = Date.UTC(dateString[0], convertMonthToUTC(dateString[1]), dateString[2]);
105
+ var requestsThisDay = value.requests ? value.requests : 0;
106
+ var opensThisDay = value.opens ? value.opens : 0;
107
+ var clicksThisDay = value.clicks ? value.clicks : 0;
108
+ var deliveresThisDay = value.delivered ? value.delivered : 0;
109
+ var uniqueOpensThisDay = value.unique_opens ? value.unique_opens : 0;
110
+ var uniqueClicksThisDay = value.unique_clicks ? value.unique_clicks : 0;
111
+ var unsubscribersThisDay = value.unsubscribes ? value.unsubscribes : 0;
112
+ var bouncesThisDay = value.bounces ? value.bounces : 0;
113
+ var spamReportsThisDay = value.spamreports ? value.spamreports : 0;
114
+ var spamDropThisDay = value.spam_drop ? value.spam_drop : 0;
115
+ var repeatBouncesThisDay = value.repeat_bounces ? value.repeat_bounces : 0;
116
+ var repeatSpamreportsThisDay = value.repeat_spamreports ? value.repeat_spamreports : 0;
117
+ var repeatUnsubscribesThisDay = value.repeat_unsubscribes ? value.repeat_unsubscribes : 0;
118
+ var dropsThisDay = spamDropThisDay + repeatBouncesThisDay + repeatSpamreportsThisDay + repeatUnsubscribesThisDay;
119
+ var blocksThisDay = value.blocked ? value.blocked : 0;
120
+
121
+ requests += requestsThisDay;
122
+ deliveres += deliveresThisDay;
123
+ opens += opensThisDay;
124
+ clicks += clicksThisDay;
125
+ bounces += bouncesThisDay;
126
+ unsubscribes += unsubscribersThisDay;
127
+ spamReports += spamReportsThisDay;
128
+ spamDrop += spamDropThisDay;
129
+ repeatBounces += repeatBouncesThisDay;
130
+ repeatSpamreports += repeatSpamreportsThisDay;
131
+ repeatUnsubscribes += repeatUnsubscribesThisDay;
132
+ drops += dropsThisDay;
133
+ blocks += blocksThisDay;
134
+ uniqueOpens += uniqueOpensThisDay;
135
+
136
+ requestStats.push([date, requestsThisDay]);
137
+ deliveredStats.push([date, deliveresThisDay]);
138
+ openStats.push([date, opensThisDay]);
139
+ uniqueOpenStats.push([date, uniqueOpensThisDay]);
140
+ clickStats.push([date, clicksThisDay]);
141
+ uniqueClickStats.push([date, uniqueClicksThisDay]);
142
+ unsubscribeStats.push([date, unsubscribersThisDay]);
143
+ bounceStats.push([date, bouncesThisDay]);
144
+ spamreportStats.push([date, spamReportsThisDay]);
145
+ dropStats.push([date, dropsThisDay]);
146
+ blockStats.push([date, blocksThisDay]);
147
+ });
148
+
149
+ /* Prepare data for charts */
150
+ var dataDeliveries = [
151
+ {
152
+ label : 'Requests',
153
+ data : requestStats,
154
+ points: { symbol: "circle" }
155
+ },
156
+ {
157
+ label : 'Drops',
158
+ data : dropStats,
159
+ points: { symbol: "square" }
160
+ },
161
+ {
162
+ label : 'Delivered',
163
+ data : deliveredStats,
164
+ points: { symbol: "diamond" }
165
+ }];
166
+
167
+ var dataCompliance = [
168
+ {
169
+ label : 'Spam reports',
170
+ data : spamreportStats,
171
+ points: { symbol: "circle" }
172
+ },
173
+ {
174
+ label : 'Bounces',
175
+ data : bounceStats,
176
+ points: { symbol: "square" }
177
+ },
178
+ {
179
+ label : 'Blocked',
180
+ data : blockStats,
181
+ points: { symbol: "diamond" }
182
+ }
183
+ ];
184
+
185
+ var dataEngagement = [
186
+ {
187
+ label : 'Unsubscribes',
188
+ data : unsubscribeStats,
189
+ points: { symbol: "diamond" }
190
+ },
191
+ {
192
+ label : 'Unique Opens',
193
+ data : uniqueOpenStats,
194
+ points: { symbol: "triangle" }
195
+ },
196
+ {
197
+ label : 'Opens',
198
+ data : openStats,
199
+ points: { symbol: "square" }
200
+ },
201
+ {
202
+ label : 'Clicks',
203
+ data : clickStats,
204
+ points: { symbol: "cross" }
205
+ }
206
+ ];
207
+
208
+ /* Show charts only on SendGrid Statistics page */
209
+ if (filterType === "sendgrid-statistics")
210
+ {
211
+ showChart("#deliveries-container", "#deliveries-container-legend", startDate,
212
+ endDate, dataDeliveries, ["#328701", "#bcd516", "#fba617"]);
213
+ showChart("#compliance-container", "#compliance-container-legend", startDate,
214
+ endDate, dataCompliance, ["#fbe500", "#1185c1", "#bcd0d1"]);
215
+ showChart("#engagement-container", "#engagement-container-legend", startDate,
216
+ endDate, dataEngagement, ["#3e44c0", "#ff00e0", "#e04428", "#328701"]);
217
+ }
218
+
219
+ /* Show info in widgets */
220
+ /* Deliveries */
221
+ var dropsRate = _round(((drops * 100) / requests), 2) + "%";
222
+ var deliveresRate = _round(((deliveres * 100) / requests), 2) + "%";
223
+ $(".sendgrid-container #deliveries #requests").html(requests);
224
+ $(".sendgrid-container #deliveries #drop").html((dropsRate === "NaN%") ? "0%" : dropsRate);
225
+ $(".sendgrid-container #deliveries #delivered").html((deliveresRate === "NaN%") ? "0%" : deliveresRate);
226
+
227
+ /* Compliance */
228
+ var spamReportsRate = _round(((spamReports * 100) / deliveres), 2) + "%";
229
+ var bouncesRate = _round(((bounces * 100) / deliveres), 2) + "%";
230
+ var blocksRate = _round(((blocks * 100) / requests), 2) + "%";
231
+ $(".sendgrid-container #compliance #spam-reports").html((spamReportsRate === "NaN%") ? "0%" : spamReportsRate);
232
+ $(".sendgrid-container #compliance #bounces").html((bouncesRate === "NaN%") ? "0%" : bouncesRate);
233
+ $(".sendgrid-container #compliance #blocks").html((blocksRate === "NaN%") ? "0%" : blocksRate);
234
+
235
+ /* Engagement */
236
+ var unsubscribesRate = _round(((unsubscribes * 100) / deliveres), 2) + "%";
237
+ var uniqueOpensRate = _round(((uniqueOpens * 100) / deliveres), 2) + "%";
238
+ var opensRate = _round(((opens * 100) / deliveres), 2) + "%";
239
+ var clicksRate = _round(((clicks * 100) / deliveres), 2) + "%";
240
+ $(".sendgrid-container #engagement #unsubscribes").html((unsubscribesRate === "NaN%") ? "0%" : unsubscribesRate);
241
+ $(".sendgrid-container #engagement #unique-opens").html((uniqueOpensRate === "NaN%") ? "0%" : uniqueOpensRate);
242
+ $(".sendgrid-container #engagement #opens").html((opensRate === "NaN%") ? "0%" : opensRate);
243
+ $(".sendgrid-container #engagement #clicks").html((clicksRate === "NaN%") ? "0%" : clicksRate);
244
+
245
+ /* Hide loaders */
246
+ $(".sendgrid-container .loading, .sendgrid-filters-container .loading").hide();
247
+ });
248
+ }
249
+
250
+ /**
251
+ * Generate flot chart with submited parameters
252
+ *
253
+ * @param {String} cssSelector
254
+ * @param {String} legendSelector
255
+ * @param {String} startDate
256
+ * @param {String} endDate
257
+ * @param {Array} data
258
+ * @param {Array} colors
259
+ * @returns {Void}
260
+ */
261
+ function showChart(cssSelector, legendSelector, startDate, endDate, data, colors)
262
+ {
263
+ var startDateArray = _splitDate(startDate);
264
+ var endDateArray = _splitDate(endDate);
265
+
266
+ $.plot(cssSelector, data, {
267
+ xaxis: {
268
+ mode: "time",
269
+ minTickSize: [1, "day"],
270
+ tickLength: 0,
271
+ min: Date.UTC(startDateArray[0], convertMonthToUTC(startDateArray[1]), startDateArray[2]),
272
+ max: Date.UTC(endDateArray[0], convertMonthToUTC(endDateArray[1]), endDateArray[2]),
273
+ timeformat: "%b %d",
274
+ reserveSpace: true,
275
+ labelWidth: 50
276
+ },
277
+ yaxis: {
278
+ min: 0
279
+ },
280
+ series: {
281
+ lines: { show: true },
282
+ points: {
283
+ radius: 4,
284
+ show: true
285
+ }
286
+ },
287
+ grid: {
288
+ hoverable: true,
289
+ borderWidth: 0
290
+ },
291
+ legend: {
292
+ noColumns: 0,
293
+ container: $(legendSelector)
294
+ },
295
+ colors: colors
296
+ });
297
+ showInfo(cssSelector);
298
+ }
299
+
300
+ /* Flop chart tooltop */
301
+
302
+ /**
303
+ * Bind plothover and hide another tooltip if is already diplayed
304
+ *
305
+ * @param {String} cssSelector
306
+ * @returns {Void}
307
+ */
308
+ function showInfo(cssSelector)
309
+ {
310
+ var previousPoint = null;
311
+ var previousLabel = null;
312
+
313
+ $(cssSelector).bind("plothover", function (event, pos, item) {
314
+ if (item) {
315
+ if ((previousPoint !== item.dataIndex) || (previousLabel !== item.series.label)) {
316
+ previousPoint = item.dataIndex;
317
+ previousLabel = item.series.label;
318
+
319
+ $("#flot-tooltip").remove();
320
+ var date = _convertMonthToString(item.datapoint[0]);
321
+ var value = item.datapoint[1];
322
+ var color = item.series.color;
323
+
324
+ showTooltip(item.pageX, item.pageY,
325
+ "<b>" + date + "</b><br />" + item.series.label + ": " + value ,
326
+ color);
327
+ }
328
+ } else {
329
+ $("#flot-tooltip").remove();
330
+ previousPoint = null;
331
+ }
332
+ });
333
+ }
334
+
335
+ /**
336
+ * Generate content for flot tooltip and show this
337
+ *
338
+ * @param {Number} x
339
+ * @param {Number} y
340
+ * @param {String} contents
341
+ * @param {Number} z
342
+ * @returns {Void}
343
+ */
344
+ function showTooltip(x, y, contents, z)
345
+ {
346
+ $('<div id="flot-tooltip">' + contents + '</div>').css({
347
+ position: 'absolute',
348
+ display: 'none',
349
+ top: y - 30,
350
+ left: x + 30,
351
+ border: '2px solid',
352
+ padding: '2px',
353
+ 'background-color': '#FFF',
354
+ opacity: 0.80,
355
+ 'border-color': z,
356
+ '-moz-border-radius': '5px',
357
+ '-webkit-border-radius': '5px',
358
+ '-khtml-border-radius': '5px',
359
+ 'border-radius': '5px'
360
+ }).appendTo("body").fadeIn(200);
361
+ }
362
+
363
+ /**** Helpers ****/
364
+
365
+ /**
366
+ * Round number with specific number of decimals
367
+ *
368
+ * @param {Number} value
369
+ * @param {Int} places
370
+ * @returns {Number}
371
+ */
372
+ function _round(value, places)
373
+ {
374
+ var multiplier = Math.pow(10, places);
375
+
376
+ return (Math.round(value * multiplier) / multiplier);
377
+ }
378
+
379
+ /**
380
+ * Return datestring yyyy-mm-dd
381
+ *
382
+ * @param {Date} date
383
+ * @returns {String}
384
+ */
385
+ function _dateToYMD(date)
386
+ {
387
+ var d = date.getDate();
388
+ var m = date.getMonth() + 1;
389
+ var y = date.getFullYear();
390
+
391
+ return '' + y + '-' + (m<=9 ? '0' + m : m) + '-' + (d <= 9 ? '0' + d : d);
392
+ }
393
+
394
+ /**
395
+ * Return month for specific timestamp
396
+ *
397
+ * @param {Timestamp} timestamp
398
+ * @returns {String}
399
+ */
400
+ function _convertMonthToString(timestamp)
401
+ {
402
+ var month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
403
+ var newDate = new Date(timestamp);
404
+
405
+ var dateString = month_names[newDate.getMonth()] + " " + newDate.getDate();
406
+
407
+ return dateString;
408
+ }
409
+
410
+ /**
411
+ * Split date by - and return array
412
+ *
413
+ * @param {String} date
414
+ * @returns {Array}
415
+ */
416
+ function _splitDate(date)
417
+ {
418
+ return date.split("-");
419
+ }
420
+
421
+ /**
422
+ * Convert month from GMT to UTC, in GMT month number start from 1 and in UTC start from 0,
423
+ * decrease month by 1
424
+ *
425
+ * @param {String} month
426
+ * @returns {String}
427
+ */
428
+ function convertMonthToUTC(month)
429
+ {
430
+ month = parseInt(month.replace("0","")) - 1;
431
+
432
+ return (month<=9 ? '0' + month : month);
433
+ }
434
+ });
view/partials/sendgrid_stats_compliance.php ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div id="sendgrid_statistics_compliance_widget" class="postbox ">
2
+ <h3 class="hndle"><span>SendGrid Compliance</span></h3>
3
+ <div class="inside">
4
+
5
+ <div class="sendgrid-container" style="position:relative;">
6
+ <img src="<?= plugin_dir_url(__FILE__); ?>../images/loader.gif" class="loading" style="position:absolute;" />
7
+ <div id="compliance-container" class="sendgrid-stats"></div>
8
+ </div>
9
+ <div id="compliance-container-legend" class="sendgrid-stats-legend"></div>
10
+
11
+ </div>
12
+ </div>
view/partials/sendgrid_stats_deliveries.php ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div id="sendgrid_statistics_deliveries_widget" class="postbox ">
2
+ <h3 class="hndle"><span>SendGrid Deliveries</span></h3>
3
+ <div class="inside">
4
+
5
+ <div class="sendgrid-container" style="position:relative;">
6
+ <img src="<?= plugin_dir_url(__FILE__); ?>../images/loader.gif" class="loading" style="position:absolute;" />
7
+ <div id="deliveries-container" class="sendgrid-stats"></div>
8
+ </div>
9
+ <div id="deliveries-container-legend" class="sendgrid-stats-legend"></div>
10
+
11
+ </div>
12
+ </div>
view/partials/sendgrid_stats_engagement.php ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div id="sendgrid_statistics_engagement_widget" class="postbox ">
2
+ <h3 class="hndle"><span>SendGrid Engagement</span></h3>
3
+ <div class="inside">
4
+
5
+ <div class="sendgrid-container" style="position:relative;">
6
+ <img src="<?= plugin_dir_url(__FILE__); ?>../images/loader.gif" class="loading" style="position:absolute;" />
7
+ <div id="engagement-container" class="sendgrid-stats"></div>
8
+ </div>
9
+ <div id="engagement-container-legend" class="sendgrid-stats-legend"></div>
10
+
11
+ </div>
12
+ </div>
view/partials/sendgrid_stats_widget.php ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="sendgrid-filters-container">
2
+ <div id="sendgrid-filters">
3
+ <label for="sendgrid-start-date">Start date</label><input type="text" id="sendgrid-start-date" name="sendgrid-start-date" />
4
+ <label for="sendgrid-end-date">End date</label><input type="text" id="sendgrid-end-date" name="sendgrid-end-date" />
5
+ <a href="#" id="sendgrid-apply-filter" data-filter="<?php if (mysql_real_escape_string($_GET['page']) == "sendgrid-statistics") { ?>sendgrid-statistics<?php } else { ?>dashboard<?php } ?>" class="button">Apply</a>
6
+ </div>
7
+ <div class="loading"><img src="<?= plugin_dir_url(__FILE__); ?>../images/loader.gif" style="width: 15px; height: 15px;" /></div>
8
+ </div>
9
+ <br style="clear:both;"/>
10
+ <div class="sendgrid-container" style="position:relative;">
11
+
12
+ <div class="widget others" id="deliveries">
13
+ <div class="widget-top">
14
+ <div class="widget-title"><h4>Deliveries</h4></div>
15
+ </div>
16
+ <div class="widget-inside">
17
+ <div class="row clearfix">
18
+ <div class="pull-left">
19
+ <span class="square" style="background-color: rgb(50,135,1);"></span><span>Requests</span>
20
+ </div>
21
+ <div id="requests" class="pull-right">0%</div>
22
+ </div>
23
+ <div class="row clearfix">
24
+ <div class="pull-left">
25
+ <span class="square" style="background-color: rgb(188,213,22);"></span><span>Drop</span>
26
+ </div>
27
+ <div id="drop" class="pull-right">0%</div>
28
+ </div>
29
+ <div class="row clearfix">
30
+ <div class="pull-left">
31
+ <span class="square" style="background-color: rgb(251,166,23);"></span><span>Delivered</span>
32
+ </div>
33
+ <div id="delivered" class="pull-right">0%</div>
34
+ </div>
35
+ </div>
36
+ </div>
37
+
38
+ <div class="widget others" id="compliance">
39
+ <div class="widget-top">
40
+ <div class="widget-title"><h4>Compliance</h4></div>
41
+ </div>
42
+ <div class="widget-inside">
43
+ <div class="row clearfix">
44
+ <div class="pull-left">
45
+ <span class="square" style="background-color: rgb(251,229,0);"></span><span>Spam Reports</span>
46
+ </div>
47
+ <div id="spam-reports" class="pull-right">0%</div>
48
+ </div>
49
+ <div class="row clearfix">
50
+ <div class="pull-left">
51
+ <span class="square" style="background-color: rgb(17,133,193);"></span><span>Bounces</span>
52
+ </div>
53
+ <div id="bounces" class="pull-right">0%</div>
54
+ </div>
55
+ <div class="row clearfix">
56
+ <div class="pull-left">
57
+ <span class="square" style="background-color: rgb(188,208,209);"></span><span>Blocks</span>
58
+ </div>
59
+ <div id="blocks" class="pull-right">0%</div>
60
+ </div>
61
+ </div>
62
+ </div>
63
+
64
+ <div class="widget others" id="engagement">
65
+ <div class="widget-top">
66
+ <div class="widget-title"><h4>Engagement</h4></div>
67
+ </div>
68
+ <div class="widget-inside">
69
+ <div class="row clearfix">
70
+ <div class="pull-left">
71
+ <span class="square" style="background-color: rgb(62,68,192);"></span><span>Unsubscribes</span>
72
+ </div>
73
+ <div id="unsubscribes" class="pull-right">0%</div>
74
+ </div>
75
+ <div class="row clearfix">
76
+ <div class="pull-left">
77
+ <span class="square" style="background-color: rgb(255,0,224);"></span><span>Unique Opens</span>
78
+ </div>
79
+ <div id="unique-opens" class="pull-right">0%</div>
80
+ </div>
81
+ <div class="row clearfix">
82
+ <div class="pull-left">
83
+ <span class="square" style="background-color: rgb(224,68,40);"></span><span>Opens</span>
84
+ </div>
85
+ <div id="opens" class="pull-right">0%</div>
86
+ </div>
87
+ <div class="row clearfix">
88
+ <div class="pull-left">
89
+ <span class="square" style="background-color: rgb(50,135,1);"></span><span>Clicks</span>
90
+ </div>
91
+ <div id="clicks" class="pull-right">0%</div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ <br style="clear:both;"/>
96
+
97
+ <?php if (mysql_real_escape_string($_GET['page']) != "sendgrid-statistics") { ?>
98
+ <a href="index.php?page=sendgrid-statistics" class="more-statistics">See charts</a>
99
+ <br style="clear:both;"/>
100
+ <?php } ?>
101
+ </div>
view/sendgrid_settings.php ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <link rel="stylesheet" href="<?php echo plugin_dir_url(__FILE__) . 'css/sendgrid.css'; ?>" type="text/css">
2
+
3
+ <div class="wrap">
4
+ <div class="stuffbox">
5
+ <a href="http://sendgrid.com" target="_blank">
6
+ <img src="<?php echo plugins_url('/images/logo.png', __FILE__) ?>" width="100" alt="" />
7
+ </a>
8
+ <h2 class="title"><?php echo _e('SendGrid Options') ?></h2>
9
+ <?php if ($status == 'save-error' or $status == 'save-success'): ?>
10
+ <div id="message" class="<?php echo $status ?>">
11
+ <strong><?php echo $message ?></strong>
12
+ </div>
13
+ <?php endif; ?>
14
+ <h3><?php echo _e('SendGrid credentials') ?></h3>
15
+ <form class="form-table" name="sendgrid_form" method="POST" action="<?php echo str_replace('%7E', '~', $_SERVER['REQUEST_URI']); ?>">
16
+ <table class="form-table">
17
+ <tr class="top">
18
+ <th scope="row"><?php _e("Username: "); ?></th>
19
+ <td>
20
+ <div class="inside">
21
+ <input type="text" required="true" name="sendgrid_user" value="<?php echo $user; ?>" size="20">
22
+ </div>
23
+ </td>
24
+ </tr>
25
+ <tr class="top">
26
+ <th scope="row"><?php _e("Password: "); ?></th>
27
+ <td>
28
+ <div class="inside">
29
+ <input type="password" required="true" name="sendgrid_pwd" value="<?php echo $password; ?>" size="20">
30
+ </div>
31
+ </td>
32
+ </tr>
33
+ <tr class="top">
34
+ <th scope="row"><?php _e("Send Mail with: "); ?></th>
35
+ <td>
36
+ <div class="inside">
37
+ <select name="sendgrid_api">
38
+ <option value="api" id="api" <?php echo ($method == 'api') ? 'selected' : '' ?>><?php _e('API') ?></option>
39
+ <option value="smtp" id="smtp" <?php echo ($method == 'smtp') ? 'selected' : '' ?>><?php _e('SMTP') ?></option>
40
+ </select>
41
+ </div>
42
+ </td>
43
+ </tr>
44
+ </table>
45
+ <br />
46
+ <h3><?php _e('Mail settings') ?></h3>
47
+ <table class="form-table">
48
+ <tr class="top">
49
+ <th scope="row"><?php _e("Name: "); ?></th>
50
+ <td>
51
+ <div class="inside">
52
+ <?php _e('Name as it will appear in recipient clients.') ?>
53
+ <br />
54
+ <input type="text" name="sendgrid_name" value="<?php echo $name; ?>" size="20">
55
+ </div>
56
+ </td>
57
+ </tr>
58
+ <tr class="top">
59
+ <th scope="row"><?php _e("Sending Address: "); ?></th>
60
+ <td>
61
+ <div class="inside">
62
+ <?php _e('Email address from which message will be sent,') ?>
63
+ <br />
64
+ <input type="email" name="sendgrid_email" value="<?php echo $email; ?>" size="20">
65
+ </div>
66
+ </td>
67
+ </tr>
68
+ <tr class="top">
69
+ <th scope="row"><?php _e("Reply Address: "); ?></th>
70
+ <td>
71
+ <div class="inside">
72
+ <?php _e('Email address where replies will be returned.') ?>
73
+ <br />
74
+ <input type="email" name="sendgrid_reply_to" value="<?php echo $reply_to; ?>" size="20">
75
+ <br />
76
+ <span>
77
+ <small>
78
+ <em>
79
+ <?php _e('Leave blank to use Sending Address.') ?>
80
+ </em>
81
+ </small>
82
+ </span>
83
+ </div>
84
+ </td>
85
+ </tr>
86
+ </table>
87
+ <div class="submit-button">
88
+ <p class="submit">
89
+ <input class="button button-primary" type="submit" name="Submit" value="<?php _e('Update Settings') ?>" />
90
+ </p>
91
+ </div>
92
+ </form>
93
+ </div>
94
+ <br />
95
+ <?php if ($valid_credentials): ?>
96
+ <div class="stuffbox">
97
+ <h2 class="title"><?php _e('SendGrid Test') ?></h2>
98
+ <?php if ($status == 'send-failed' or $status == 'send-success'): ?>
99
+ <div id="message" class="<?php echo $status ?>">
100
+ <strong><?php echo $message ?></strong>
101
+ </div>
102
+ <?php endif; ?>
103
+ <h3><?php _e('Send a test email with these settings') ?></h3>
104
+ <form name="sendgrid_test" method="POST" action="<?php echo str_replace('%7E', '~', $_SERVER['REQUEST_URI']); ?>">
105
+ <table class="form-table">
106
+ <tr class="top">
107
+ <th scope="row"><?php _e("To: "); ?></th>
108
+ <td>
109
+ <div class="inside">
110
+ <input type="email" name="sendgrid_to" required="true" value="<?php echo $success ? '' : $to; ?>" size="20">
111
+ </div>
112
+ </td>
113
+ </tr>
114
+ <tr class="top">
115
+ <th scope="row"><?php _e("Subject: "); ?></th>
116
+ <td>
117
+ <div class="inside">
118
+ <input type="text" name="sendgrid_subj" required="true" value="<?php echo $success ? '' : $subject; ?>" size="20">
119
+ </div>
120
+ </td>
121
+ </tr>
122
+ <tr class="top">
123
+ <th scope="row"><?php _e("Body: "); ?></th>
124
+ <td>
125
+ <div class="inside">
126
+ <textarea name="sendgrid_body" rows="5"><?php echo $success ? '' : $body; ?></textarea>
127
+ </div>
128
+ </td>
129
+ </tr>
130
+ <tr class="top">
131
+ <th scope="row"><?php _e("Headers: "); ?></th>
132
+ <td>
133
+ <div class="inside">
134
+ <textarea name="sendgrid_headers" rows="3"><?php echo $success ? '' : $headers; ?></textarea>
135
+ </div>
136
+ </td>
137
+ </tr>
138
+ </table>
139
+ <input type="hidden" name="email_test" value="true"/>
140
+ <div class="submit-button">
141
+ <p class="submit">
142
+ <input class="button button-primary" type="submit" name="Submit" value="<?php _e('Send') ?>" />
143
+ </p>
144
+ </div>
145
+ </form>
146
+ </div>
147
+ <?php endif; ?>
148
+ </div>
view/sendgrid_stats.php ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="wrap" id="sendgrid-statistics-page">
2
+ <div id="icon-sendgrid" class="icon32"><br></div>
3
+ <h2>SendGrid Statistics</h2>
4
+
5
+ <div id="dashboard-widgets-wrap">
6
+ <div id="dashboard-widgets" class="metabox-holder columns-1">
7
+ <div id="postbox-container-1" class="postbox-container">
8
+ <div id="normal-sortables" class="meta-box-sortables">
9
+
10
+ <div id="sendgrid_statistics_widget" class="postbox ">
11
+ <h3 class="hndle"><span>SendGrid Statistics</span></h3>
12
+ <div class="inside">
13
+ <?php require plugin_dir_path( __FILE__ ) . '../view/partials/sendgrid_stats_widget.php'; ?>
14
+ </div>
15
+ </div>
16
+
17
+ <?php
18
+ require plugin_dir_path( __FILE__ ) . '../view/partials/sendgrid_stats_deliveries.php';
19
+ require plugin_dir_path( __FILE__ ) . '../view/partials/sendgrid_stats_compliance.php';
20
+ require plugin_dir_path( __FILE__ ) . '../view/partials/sendgrid_stats_engagement.php';
21
+ ?>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </div>
wpsendgrid.php ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /*
3
+ Plugin Name: SendGrid
4
+ Plugin URI: http://sendgrid.com
5
+ Description: Email Delivery. Simplified. SendGrid's cloud-based email infrastructure relieves businesses of the cost and complexity of maintaining custom email systems. SendGrid provides reliable delivery, scalability and real-time analytics along with flexible APIs that make custom integration a breeze.
6
+ Version: 1.1
7
+ Author: SendGrid
8
+ Author URI: http://sendgrid.com
9
+ License: GPLv2
10
+ */
11
+
12
+ require_once plugin_dir_path( __FILE__ ) . '/lib/SendGridSettings.php';
13
+ require_once plugin_dir_path( __FILE__ ) . '/lib/SendGridStats.php';
14
+ require_once plugin_dir_path( __FILE__ ) . '/lib/sendgrid-php/SendGrid_loader.php';
15
+
16
+ $sendgridSettings = new wpSendGridSettings();
17
+ $plugin = plugin_basename(__FILE__);
18
+
19
+ if (!function_exists('wp_mail'))
20
+ {
21
+ /**
22
+ * Send mail, similar to PHP's mail
23
+ *
24
+ * A true return value does not automatically mean that the user received the
25
+ * email successfully. It just only means that the method used was able to
26
+ * process the request without any errors.
27
+ *
28
+ * Using the two 'wp_mail_from' and 'wp_mail_from_name' hooks allow from
29
+ * creating a from address like 'Name <email@address.com>' when both are set. If
30
+ * just 'wp_mail_from' is set, then just the email address will be used with no
31
+ * name.
32
+ *
33
+ * The default content type is 'text/plain' which does not allow using HTML.
34
+ * However, you can set the content type of the email by using the
35
+ * 'wp_mail_content_type' filter.
36
+ *
37
+ * The default charset is based on the charset used on the blog. The charset can
38
+ * be set using the 'wp_mail_charset' filter.
39
+ *
40
+ * @since 1.2.1
41
+ * @uses apply_filters() Calls 'wp_mail' hook on an array of all of the parameters.
42
+ * @uses apply_filters() Calls 'wp_mail_from' hook to get the from email address.
43
+ * @uses apply_filters() Calls 'wp_mail_from_name' hook to get the from address name.
44
+ * @uses apply_filters() Calls 'wp_mail_content_type' hook to get the email content type.
45
+ * @uses apply_filters() Calls 'wp_mail_charset' hook to get the email charset
46
+ *
47
+ * @param string|array $to Array or comma-separated list of email addresses to send message.
48
+ * @param string $subject Email subject
49
+ * @param string $message Message contents
50
+ * @param string|array $headers Optional. Additional headers.
51
+ * @param string|array $attachments Optional. Files to attach.
52
+ * @return bool Whether the email contents were sent successfully.
53
+ */
54
+ function wp_mail($to, $subject, $message, $headers = '', $attachments = array())
55
+ {
56
+ $sendgrid = new SendGrid(get_option('sendgrid_user'), get_option('sendgrid_pwd'));
57
+ $mail = new SendGrid\Mail();
58
+ $method = get_option('sendgrid_api');
59
+ // Compact the input, apply the filters, and extract them back out
60
+ extract(apply_filters('wp_mail', compact('to', 'subject', 'message', 'headers', 'attachments')));
61
+
62
+ // prepare attachments
63
+ $attached_files = array();
64
+ if (!empty($attachments))
65
+ {
66
+ if (!is_array($attachments))
67
+ {
68
+ $pos = strpos(',', $attachments);
69
+ if ($pos !== false)
70
+ {
71
+ $attachments = preg_split('/,\s*/', $attachments);
72
+ }
73
+ else
74
+ {
75
+ $attachments = explode("\n", str_replace("\r\n", "\n", $attachments));
76
+ }
77
+ }
78
+
79
+ if (is_array($attachments)) {
80
+ foreach ($attachments as $attachment) {
81
+ if (file_exists($attachment)) {
82
+ $attached_files[] = $attachment;
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ // Headers
89
+ if (empty($headers)) {
90
+ $headers = array();
91
+ } else {
92
+ if (!is_array($headers)) {
93
+ // Explode the headers out, so this function can take both
94
+ // string headers and an array of headers.
95
+ $tempheaders = explode( "\n", str_replace( "\r\n", "\n", $headers ) );
96
+ } else {
97
+ $tempheaders = $headers;
98
+ }
99
+ $headers = array();
100
+ $cc = array();
101
+ $bcc = array();
102
+
103
+ // If it's actually got contents
104
+ if ( !empty( $tempheaders ) ) {
105
+ // Iterate through the raw headers
106
+ foreach ( (array) $tempheaders as $header ) {
107
+ if ( strpos($header, ':') === false ) {
108
+ if ( false !== stripos( $header, 'boundary=' ) ) {
109
+ $parts = preg_split('/boundary=/i', trim( $header ) );
110
+ $boundary = trim( str_replace( array( "'", '"' ), '', $parts[1] ) );
111
+ }
112
+ continue;
113
+ }
114
+ // Explode them out
115
+ list( $name, $content ) = explode( ':', trim( $header ), 2 );
116
+
117
+ // Cleanup crew
118
+ $name = trim( $name );
119
+ $content = trim( $content );
120
+
121
+ switch ( strtolower( $name ) ) {
122
+ // Mainly for legacy -- process a From: header if it's there
123
+ case 'from':
124
+ if ( strpos($content, '<' ) !== false ) {
125
+ // So... making my life hard again?
126
+ $from_name = substr( $content, 0, strpos( $content, '<' ) - 1 );
127
+ $from_name = str_replace( '"', '', $from_name );
128
+ $from_name = trim( $from_name );
129
+
130
+ $from_email = substr( $content, strpos( $content, '<' ) + 1 );
131
+ $from_email = str_replace( '>', '', $from_email );
132
+ $from_email = trim( $from_email );
133
+ } else {
134
+ $from_email = trim( $content );
135
+ }
136
+ break;
137
+ case 'content-type':
138
+ if ( strpos( $content, ';' ) !== false ) {
139
+ list( $type, $charset ) = explode( ';', $content );
140
+ $content_type = trim( $type );
141
+ if ( false !== stripos( $charset, 'charset=' ) ) {
142
+ $charset = trim( str_replace( array( 'charset=', '"' ), '', $charset ) );
143
+ } elseif ( false !== stripos( $charset, 'boundary=' ) ) {
144
+ $boundary = trim( str_replace( array( 'BOUNDARY=', 'boundary=', '"' ), '', $charset ) );
145
+ $charset = '';
146
+ }
147
+ } else {
148
+ $content_type = trim( $content );
149
+ }
150
+ break;
151
+ case 'cc':
152
+ $cc = array_merge( (array) $cc, explode( ',', $content ) );
153
+ foreach ($cc as $key => $recipient)
154
+ {
155
+ $cc[$key] = trim($recipient);
156
+ }
157
+ break;
158
+ case 'bcc':
159
+ $bcc = array_merge( (array) $bcc, explode( ',', $content ) );
160
+ foreach ($bcc as $key => $recipient)
161
+ {
162
+ $bcc[$key] = trim($recipient);
163
+ }
164
+ break;
165
+ default:
166
+ // Add it to our grand headers array
167
+ $headers[trim( $name )] = trim( $content );
168
+ break;
169
+ }
170
+ }
171
+ }
172
+ }
173
+
174
+ // From email and name
175
+ // If we don't have a name from the input headers
176
+ if ( !isset( $from_name ) )
177
+ $from_name = get_option('sendgrid_from_name');
178
+
179
+ /* If we don't have an email from the input headers default to wordpress@$sitename
180
+ * Some hosts will block outgoing mail from this address if it doesn't exist but
181
+ * there's no easy alternative. Defaulting to admin_email might appear to be another
182
+ * option but some hosts may refuse to relay mail from an unknown domain. See
183
+ * http://trac.wordpress.org/ticket/5007.
184
+ */
185
+
186
+ if ( !isset( $from_email ) ) {
187
+ $from_email = trim(get_option('sendgrid_from_email'));
188
+ if (!$from_email)
189
+ {
190
+ // Get the site domain and get rid of www.
191
+ $sitename = strtolower( $_SERVER['SERVER_NAME'] );
192
+ if ( substr( $sitename, 0, 4 ) == 'www.' ) {
193
+ $sitename = substr( $sitename, 4 );
194
+ }
195
+
196
+ $from_email = 'wordpress@' . $sitename;
197
+ }
198
+ }
199
+
200
+ // Plugin authors can override the potentially troublesome default
201
+ $from_email = apply_filters( 'wp_mail_from' , $from_email );
202
+ $from_name = apply_filters( 'wp_mail_from_name', $from_name );
203
+
204
+ // Set destination addresses
205
+ if ( !is_array( $to ) )
206
+ $to = explode( ',', $to );
207
+
208
+ // Add any CC and BCC recipients
209
+ if (!empty( $cc ))
210
+ {
211
+ foreach ((array) $cc as $key => $recipient)
212
+ {
213
+ // Break $recipient into name and address parts if in the format "Foo <bar@baz.com>"
214
+ if (preg_match('/(.*)<(.+)>/', $recipient, $matches))
215
+ {
216
+ if ( count( $matches ) == 3 )
217
+ {
218
+ $cc[$key] = trim($matches[2]);
219
+ }
220
+ }
221
+ }
222
+ }
223
+
224
+ if ( !empty( $bcc ) ) {
225
+ foreach ( (array) $bcc as $key => $recipient) {
226
+ // Break $recipient into name and address parts if in the format "Foo <bar@baz.com>"
227
+ if( preg_match( '/(.*)<(.+)>/', $recipient, $matches ) ) {
228
+ if ( count( $matches ) == 3 )
229
+ {
230
+ $bcc[$key] = trim($matches[2]);
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+ if (($method == 'api') and (count($cc) or count($bcc)))
237
+ {
238
+ foreach ((array) $to as $key => $recipient)
239
+ {
240
+ // Break $recipient into name and address parts if in the format "Foo <bar@baz.com>"
241
+ if (preg_match( '/(.*)<(.+)>/', $recipient, $matches ) )
242
+ {
243
+ if ( count( $matches ) == 3 ) {
244
+ $to[$key] = trim($matches[2]);
245
+ }
246
+ }
247
+ }
248
+ }
249
+ // Set Content-Type and charset
250
+ // If we don't have a content-type from the input headers
251
+ if ( !isset( $content_type ) )
252
+ $content_type = 'text/plain';
253
+
254
+ $content_type = apply_filters( 'wp_mail_content_type', $content_type );
255
+
256
+ $mail->setTos($to)
257
+ ->setSubject($subject)
258
+ ->setText($message)
259
+ ->setFrom($from_email);
260
+
261
+ // send HTML content
262
+ if ($content_type !== 'text/plain')
263
+ {
264
+ $mail->setHtml($message);
265
+ }
266
+ // set from name
267
+ if ($from_email)
268
+ {
269
+ $mail->setFromName($from_name);
270
+ }
271
+ // set from cc
272
+ if (count($cc))
273
+ {
274
+ $mail->setCcs($cc);
275
+ }
276
+ // set from bcc
277
+ if (count($bcc))
278
+ {
279
+ $mail->setBccs($bcc);
280
+ }
281
+ $reply_to = trim(get_option('sendgrid_reply_to'));
282
+ if ($reply_to)
283
+ {
284
+ $mail->setReplyTo($reply_to);
285
+ }
286
+ // add attachemnts
287
+ if (count($attached_files))
288
+ {
289
+ $mail->setAttachments($attached_files);
290
+ }
291
+
292
+ // Send!
293
+ try
294
+ {
295
+ if ($method == 'api')
296
+ {
297
+ return $sendgrid->web->send($mail);
298
+ }
299
+ elseif ($method == 'smtp')
300
+ {
301
+ if (class_exists('Swift'))
302
+ {
303
+ return $sendgrid->smtp->send($mail);
304
+ }
305
+ else
306
+ {
307
+ return 'Error: Swift Class not loaded. Please activate Swift plugin or use API.';
308
+ }
309
+ }
310
+ }
311
+ catch (Exception $e)
312
+ {
313
+ return $e->getMessage();
314
+ }
315
+
316
+ return false;
317
+ }
318
+ }
319
+ else
320
+ {
321
+ /**
322
+ * wp_mail has been declared by another process or plugin, so you won't be able to use SENDGRID until the problem is solved.
323
+ */
324
+ add_action('admin_notices', 'adminNotices');
325
+
326
+ /**
327
+ * Display the notice that wp_mail function was declared by another plugin
328
+ *
329
+ * return void
330
+ */
331
+ function adminNotices()
332
+ {
333
+ echo '<div class="error"><p>'.__('SendGrid: wp_mail has been declared by another process or plugin, so you won\'t be able to use SendGrid until the conflict is solved.') . '</p></div>';
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Add settings link on the plugin page
339
+ *
340
+ * @param mixed $links links
341
+ * @return mixed links
342
+ */
343
+ function sendgrid_settings_link($links)
344
+ {
345
+ $settings_link = '<a href="options-general.php?page=sendgrid-settings.php">Settings</a>';
346
+ array_unshift($links, $settings_link);
347
+
348
+ return $links;
349
+ }
350
+ add_filter("plugin_action_links_$plugin", 'sendgrid_settings_link' );
351
+
352
+ /**
353
+ * Generates source of contextual help panel.
354
+ *
355
+ * @param mixed $contextual_help contextual help
356
+ * @param integer $screen_id screen id
357
+ * @param integer $screen screen
358
+ * @return string
359
+ */
360
+ function showContextualHelp($contextual_help, $screen_id, $screen)
361
+ {
362
+ $text = '<p>' . __('Email Delivery. Simplified.') . '</p>' .
363
+ '<p>' . __("SendGrid's cloud-based email infrastructure relieves businesses of the cost and complexity " .
364
+ "of maintaining custom email systems. SendGrid provides reliable delivery, scalability and real-time " .
365
+ "analytics along with flexible APIs that make custom integration a breeze.") . '</p>' .
366
+ '<p><br />' . __('To have the SendGrid plugin running after you activated it, please go to plugin\'s ' .
367
+ 'settings page and set the SendGrid credentials, and the way your email will be sent through SMTP or API.') .
368
+ '<br />' . __('You can also set default values for the \'Name\', \'Sending Address\' and the \'Reply Address\' ' .
369
+ ' in this page, so that you don\'t need to set these headers every time you want to send an email from your ' .
370
+ 'application.') . '</p>' .
371
+ '<p>' . __('After you have done these configurations, all your emails sent from your WordPress installation will ' .
372
+ 'go through SendGrid.') . '</p><p>' . __('Now let see how simple is to send a text email:') . '<br />' .
373
+ '<div class="code">' . __('&lt;?php wp_mail(\'to@address.com\', \'Email Subject\', \'Email Body\'); ?&gt;') . '</div><br />' .
374
+
375
+ __('If you want to use additional headers, here you have a more complex example:') . '<br />' .
376
+
377
+ '<div class="code">$subject = \'test plugin\'<br />' .
378
+ '$message = \'testing wordpress plugin\'<br />' .
379
+ '$to = array(\'address1@sendgrid.com\', \'Address2 <address2@sendgrid.com>\', \'address3@sendgrid.com\');<br /><br />' .
380
+
381
+ '$headers = array();<br />' .
382
+ '$headers[] = \'From: Me Myself <me@example.net>\';<br />' .
383
+ '$headers[] = \'Cc: address4@sendgrid.com\';<br />' .
384
+ '$headers[] = \'Bcc: address5@sendgrid.com\';<br /><br />' .
385
+
386
+ '$attachments = array(\'/tmp/img1.jpg\', \'/tmp/img2.jpg\');<br /><br />' .
387
+
388
+ 'add_filter(\'wp_mail_content_type\', \'set_html_content_type\');<br />' .
389
+ '$mail = wp_mail($to, $subject, $message, $headers, $attachments);<br />' .
390
+
391
+ 'remove_filter(\'wp_mail_content_type\', \'set_html_content_type\');</div><br /><br />' .
392
+
393
+ 'Where:<br />' .
394
+ '<ul>' .
395
+ '<li>$to - ' . __('Array or comma-separated list of email addresses to send message.') . '</li>' .
396
+ '<li>$subject - ' . __('Email subject') . '</li>' .
397
+ '<li>$message - ' . __('Message contents') . '</li>' .
398
+ '<li>$headers - ' . __('Array or "\n" separated list of additional headers. Optional.') . '</li>' .
399
+ '<li>$attachments - ' . __('Array or "\n"/"," separated list of files to attach. Optional.') . '</li>' .
400
+ '</ul>' .
401
+ __('The wp_mail function is sending text emails as default. If you want to send an email with HTML content you have ' .
402
+ 'to set the content type to \'text/html\' running') . ' <span class="code">add_filter(\'wp_mail_content_type\', ' .
403
+ '\'set_html_content_type\');</span> ' . __('function before to wp_mail() one') . '.<br /><br />' .
404
+ __('After wp_mail function you need to run the ') . '<span class="code">remove_filter(\'wp_mail_content_type\', ' .
405
+ '\'set_html_content_type\');</span>' . __(' to remove the \'text/html\' filter to avoid conflicts') .
406
+ ' -- http://core.trac.wordpress.org/ticket/23578';
407
+
408
+ return $text;
409
+ }
410
+ add_filter('contextual_help', 'showContextualHelp', 10, 2);
411
+
412
+ /**
413
+ * Return the content type used to send html emails
414
+ *
415
+ * return string Conteny-type needed to send HTML emails
416
+ */
417
+ function set_html_content_type()
418
+ {
419
+ return 'text/html';
420
+ }