Version Description
- Added SendGrid Statistics
=
Download this release
Release Info
Developer | team-rs |
Plugin | SendGrid |
Version | 1.1 |
Comparing to | |
See all releases |
Version 1.1
- assets/screenshot-1.png +0 -0
- assets/screenshot-2.png +0 -0
- assets/screenshot-3.png +0 -0
- assets/screenshot-4.png +0 -0
- assets/screenshot-5.png +0 -0
- assets/screenshot-6.png +0 -0
- assets/screenshot-7.png +0 -0
- lib/SendGridSettings.php +154 -0
- lib/SendGridStats.php +131 -0
- lib/sendgrid-php/.gitignore +4 -0
- lib/sendgrid-php/.travis.yml +6 -0
- lib/sendgrid-php/MIT.LICENSE +15 -0
- lib/sendgrid-php/Makefile +37 -0
- lib/sendgrid-php/README.md +234 -0
- lib/sendgrid-php/SendGrid.php +44 -0
- lib/sendgrid-php/SendGrid/Api.php +17 -0
- lib/sendgrid-php/SendGrid/Mail.php +721 -0
- lib/sendgrid-php/SendGrid/MailInterface.php +10 -0
- lib/sendgrid-php/SendGrid/Smtp.php +159 -0
- lib/sendgrid-php/SendGrid/Web.php +147 -0
- lib/sendgrid-php/SendGrid_loader.php +14 -0
- lib/sendgrid-php/Test/Mock/Mock_loader.php +14 -0
- lib/sendgrid-php/Test/Mock/SmtpMock.php +14 -0
- lib/sendgrid-php/Test/Mock/WebMock.php +19 -0
- lib/sendgrid-php/Test/SendGrid/ApiTest.php +0 -0
- lib/sendgrid-php/Test/SendGrid/MailTest.php +549 -0
- lib/sendgrid-php/Test/SendGrid/SmtpTest.php +91 -0
- lib/sendgrid-php/Test/SendGrid/WebTest.php +101 -0
- lib/sendgrid-php/Test/SendGridTest.php +36 -0
- lib/sendgrid-php/Test/a_loaderTest.php +7 -0
- lib/sendgrid-php/Test/phpunit.xml +7 -0
- lib/sendgrid-php/composer.json +16 -0
- lib/sendgrid-php/composer.lock +70 -0
- readme.txt +111 -0
- view/css/sendgrid.css +239 -0
- view/css/smoothness/images/animated-overlay.gif +0 -0
- view/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- view/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- view/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- view/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- view/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- view/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- view/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- view/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- view/css/smoothness/images/ui-icons_222222_256x240.png +0 -0
- view/css/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
- view/css/smoothness/images/ui-icons_454545_256x240.png +0 -0
- view/css/smoothness/images/ui-icons_888888_256x240.png +0 -0
- view/css/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
- view/css/smoothness/jquery-ui-1.10.3.custom.css +677 -0
- view/images/loader.gif +0 -0
- view/images/logo.png +0 -0
- view/images/logo32.png +0 -0
- view/js/jquery.flot.js +2696 -0
- view/js/jquery.flot.symbol.js +71 -0
- view/js/jquery.flot.time.js +431 -0
- view/js/jquery.flot.togglelegend.js +319 -0
- view/js/jquery.ui.datepicker.js +2038 -0
- view/js/sendgrid-stats.js +434 -0
- view/partials/sendgrid_stats_compliance.php +12 -0
- view/partials/sendgrid_stats_deliveries.php +12 -0
- view/partials/sendgrid_stats_engagement.php +12 -0
- view/partials/sendgrid_stats_widget.php +101 -0
- view/sendgrid_settings.php +148 -0
- view/sendgrid_stats.php +26 -0
- 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("¶m[]=foo¶m[]=bar¶m[]=car¶m[]=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, "'") + "'" : "") + // cell title
|
1737 |
+
(unselectable ? "" : " data-handler='selectDay' data-event='click' data-month='" + printDate.getMonth() + "' data-year='" + printDate.getFullYear() + "'") + ">" + // actions
|
1738 |
+
(otherMonth && !showOtherMonths ? " " : // 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) ? " " : "");
|
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) ? " " : "") + 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">' . __('<?php wp_mail(\'to@address.com\', \'Email Subject\', \'Email Body\'); ?>') . '</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 |
+
}
|