diff --git a/PHPCI/Application.php b/PHPCI/Application.php index 88380c87..a8790804 100644 --- a/PHPCI/Application.php +++ b/PHPCI/Application.php @@ -22,6 +22,11 @@ use PHPCI\Model\Build; */ class Application extends b8\Application { + /** + * @var \PHPCI\Controller + */ + protected $controller; + /** * Initialise PHPCI - Handles session verification, routing, etc. */ @@ -100,7 +105,7 @@ class Application extends b8\Application $this->response->setContent($view->render()); } - if ($this->response->hasLayout()) { + if ($this->response->hasLayout() && $this->controller->layout) { $this->setLayoutVariables($this->controller->layout); $this->controller->layout->content = $this->response->getContent(); diff --git a/PHPCI/Controller.php b/PHPCI/Controller.php index b065e857..5fd22594 100644 --- a/PHPCI/Controller.php +++ b/PHPCI/Controller.php @@ -30,6 +30,11 @@ class Controller extends \b8\Controller */ protected $view; + /** + * @var \b8\View + */ + public $layout; + /** * Initialise the controller. */ diff --git a/PHPCI/Helper/Lang.php b/PHPCI/Helper/Lang.php index 1877426c..c86de1aa 100644 --- a/PHPCI/Helper/Lang.php +++ b/PHPCI/Helper/Lang.php @@ -13,6 +13,7 @@ use b8\Config; /** * Languages Helper Class - Handles loading strings files and the strings within them. + * * @package PHPCI\Helper */ class Lang @@ -23,6 +24,7 @@ class Lang /** * Get a specific string from the language file. + * * @param $string * @return mixed|string */ @@ -48,6 +50,7 @@ class Lang /** * Get the currently active language. + * * @return string|null */ public static function getLanguage() @@ -57,7 +60,9 @@ class Lang /** * Try and load a language, and if successful, set it for use throughout the system. + * * @param $language + * * @return bool */ public static function setLanguage($language) @@ -73,6 +78,7 @@ class Lang /** * Return a list of available languages and their names. + * * @return array */ public static function getLanguageOptions() @@ -90,6 +96,7 @@ class Lang /** * Get the strings for the currently active language. + * * @return string[] */ public static function getStrings() @@ -99,6 +106,7 @@ class Lang /** * Initialise the Language helper, try load the language file for the user's browser or the configured default. + * * @param Config $config */ public static function init(Config $config) @@ -137,6 +145,7 @@ class Lang /** * Load a specific language file. + * * @return string[]|null */ protected static function loadLanguage() @@ -168,4 +177,24 @@ class Lang } } } + + /** + * Create a time tag for localization. + * + * See http://momentjs.com/docs/#/displaying/format/ for a list of supported formats. + * + * @param \DateTime $dateTime The dateTime to represent. + * @param string $format The moment.js format to use. + * + * @return string The formatted tag. + */ + public static function formatDateTime(\DateTime $dateTime, $format = 'lll') + { + return sprintf( + '', + $dateTime->format(\DateTime::ISO8601), + $format, + $dateTime->format(\DateTime::RFC2822) + ); + } } diff --git a/PHPCI/Helper/SshKey.php b/PHPCI/Helper/SshKey.php index ca12a815..47bd85bd 100644 --- a/PHPCI/Helper/SshKey.php +++ b/PHPCI/Helper/SshKey.php @@ -63,7 +63,7 @@ class SshKey */ public function canGenerateKeys() { - $keygen = @shell_exec('ssh-keygen -h'); + $keygen = @shell_exec('ssh-keygen --help'); $canGenerateKeys = !empty($keygen); return $canGenerateKeys; diff --git a/PHPCI/Languages/lang.fr.php b/PHPCI/Languages/lang.fr.php index bd85bccd..9e7cd339 100644 --- a/PHPCI/Languages/lang.fr.php +++ b/PHPCI/Languages/lang.fr.php @@ -14,21 +14,21 @@ $strings = array( // Log in: 'log_in_to_phpci' => 'Connectez-vous à PHPCI', 'login_error' => 'Adresse email ou mot de passe invalide', - 'forgotten_password_link' => 'Mot de passe oublié ?', + 'forgotten_password_link' => 'Mot de passe oublié ?', 'reset_emailed' => 'Nous vous avons envoyé un email avec un lien pour réinitialiser votre mot de passe.', 'reset_header' => 'Pas d\'inquiétude
Entrez simplement votre adresse email ci-dessous et nous vous enverrons un message avec un lien pour réinitialiser votre mot de passe.', 'reset_email_address' => 'Entrez votre adresse email:', 'reset_send_email' => 'Envoyer le mail', 'reset_enter_password' => 'Veuillez entrer un nouveau mot de passe', - 'reset_new_password' => 'Nouveau mot de passe :', + 'reset_new_password' => 'Nouveau mot de passe :', 'reset_change_password' => 'Modifier le mot de passe', 'reset_no_user_exists' => 'Il n\'existe aucun utilisateur avec cette adresse email, merci de réessayer.', 'reset_email_body' => 'Bonjour %s, Vous avez reçu cet email parce qu\'une demande de réinitialisation de mot de passe a été faite pour votre compte PHPCI. -Si c\'est bien vous, merci de cliquer sur le lien suivant pour réinitialiser votre mot de passe : %ssession/reset-password/%d/%s +Si c\'est bien vous, merci de cliquer sur le lien suivant pour réinitialiser votre mot de passe : %ssession/reset-password/%d/%s Sinon, merci d\'ignorer ce message. @@ -49,9 +49,9 @@ PHPCI', 'n_builds_running' => '%d builds en cours d\'exécution', 'edit_profile' => 'Éditer le profil', 'sign_out' => 'Déconnexion', - 'branch_x' => 'Branche : %s', - 'created_x' => 'Créé à : %s', - 'started_x' => 'Démarré à : %s', + 'branch_x' => 'Branche : %s', + 'created_x' => 'Créé à : %s', + 'started_x' => 'Démarré à : %s', // Sidebar 'hello_name' => 'Salut %s', @@ -67,19 +67,19 @@ PHPCI', 'delete_project' => 'Supprimer le projet', // Project Summary: - 'no_builds_yet' => 'Aucun build pour le moment!', - 'x_of_x_failed' => '%d parmis les derniers %d builds ont échoué.', - 'x_of_x_failed_short' => '%d / %d ont échoué.', - 'last_successful_build' => ' Le dernier build qui a réussi est %s.', - 'never_built_successfully' => ' Aucun build n\'a été exécuté avec succès sur ce projet.', - 'all_builds_passed' => 'Les derniers %d builds ont réussis.', - 'all_builds_passed_short' => '%d / %d ont réussis.', - 'last_failed_build' => ' Le dernier build en échec est %s.', - 'never_failed_build' => ' Ce projet n\'a jamais eu un build en échec.', + 'no_builds_yet' => 'Aucun build pour le moment !', + 'x_of_x_failed' => '%d des %d derniers builds ont échoué.', + 'x_of_x_failed_short' => '%d échecs / %d.', + 'last_successful_build' => ' Le dernier build réussi date du %s.', + 'never_built_successfully' => ' Aucun build de ce projet n\'a réussi.', + 'all_builds_passed' => 'Les %d derniers builds ont réussi.', + 'all_builds_passed_short' => '%d réussites / %d.', + 'last_failed_build' => ' Le dernier build en échec date du %s.', + 'never_failed_build' => ' Aucun build de ce projet n\'a échoué.', 'view_project' => 'Voir le projet', // Timeline: - 'latest_builds' => 'Les derniers Builds', + 'latest_builds' => 'Derniers builds', 'pending' => 'En attente', 'running' => 'En cours', 'success' => 'Terminé', @@ -102,8 +102,8 @@ PHPCI', 'local' => 'Chemin local', 'hg' => 'Mercurial', - 'where_hosted' => 'Où est hébergé votre projet ?', - 'choose_github' => 'Choisissez un dépôt GitHub :', + 'where_hosted' => 'Où est hébergé votre projet ?', + 'choose_github' => 'Choisissez un dépôt GitHub :', 'repo_name' => 'Nom du dépôt / URL (distance) ou chemin (local)', 'project_title' => 'Titre du projet', @@ -112,7 +112,7 @@ PHPCI', 'build_config' => 'Configuration PHPCI spécifique pour ce projet (si vous ne pouvez pas ajouter de fichier phpci.yml à la racine du dépôt)', 'default_branch' => 'Nom de la branche par défaut', - 'allow_public_status' => 'Activer la page de statut publique et l\'image pour ce projet ?', + 'allow_public_status' => 'Activer la page de statut publique et l\'image pour ce projet ?', 'save_project' => 'Enregistrer le projet', 'error_mercurial' => 'Les URLs de dépôt Mercurial doivent commencer par http:// ou https://', @@ -155,7 +155,7 @@ PHPCI', 'committed_by_x' => 'Committé par %s', - 'commit_id_x' => 'Commit: %s', + 'commit_id_x' => 'Commit : %s', 'chart_display' => 'Ce graphique s\'affichera une fois que le build sera terminé.', @@ -207,7 +207,7 @@ PHPCI', 'update_your_details' => 'Mettre à jour vos préférences', 'your_details_updated' => 'Vos préférences ont été bien mises à jour.', 'add_user' => 'Ajouter un utilisateur', - 'is_admin' => 'Est-il administrateur ?', + 'is_admin' => 'Est-il administrateur ?', 'yes' => 'Oui', 'no' => 'Non', 'edit' => 'Éditer', @@ -287,17 +287,17 @@ PHPCI', 'config_path' => 'Chemin vers le fichier de configuration', 'install_phpci' => 'Installer PHPCI', 'welcome_to_phpci' => 'Bienvenue sur PHPCI', - 'please_answer' => 'Merci de répondre aux questions suivantes:', + 'please_answer' => 'Merci de répondre aux questions suivantes :', 'phpci_php_req' => 'PHPCI requiert au moins PHP 5.3.8 pour fonctionner.', - 'extension_required' => 'Extensions requises: %s', + 'extension_required' => 'Extensions requises : %s', 'function_required' => 'PHPCI doit être capable d\'appeler la fonction %s(). Est-ce qu\'elle est désactivée dans votre php.ini?', 'requirements_not_met' => 'PHPCI ne peut pas être installé parce que toutes les conditions requises ne sont pas respectées. Merci de corriger les erreurs ci-dessus avant de continuer.', 'must_be_valid_email' => 'Doit être une adresse email valide.', 'must_be_valid_url' => 'Doit être une URL valide.', - 'enter_name' => 'Nom de l\'admin:', - 'enter_email' => 'Email de l\'admin:', - 'enter_password' => 'Mot de passe de l\'admin:', + 'enter_name' => 'Nom de l\'admin :', + 'enter_email' => 'Email de l\'admin :', + 'enter_password' => 'Mot de passe de l\'admin :', 'enter_phpci_url' => 'Votre URL vers PHPCI (par exemple "http://phpci.local"): ', 'enter_db_host' => 'Merci d\'entrer le nom d\'hôte MySQL [localhost]: ', @@ -306,7 +306,7 @@ PHPCI', 'enter_db_pass' => 'Merci d\'entrer le mot de passe MySQL: ', 'could_not_connect' => 'PHPCI ne peut pas se connecter à MySQL à partir des informations fournies. Veuillez réessayer..', 'setting_up_db' => 'Paramétrage de la base de données... ', - 'user_created' => 'Le compte utilisateur a été créé!', + 'user_created' => 'Le compte utilisateur a été créé !', 'failed_to_create' => 'PHPCI n\'a pas réussi à créer votre compte admin.', 'config_exists' => 'Le fichier de configuration PHPCI existe et n\'est pas vide.', 'update_instead' => 'Si vous essayez de mettre à jour PHPCI, merci d\'utiliser la commande phpci:update.', @@ -365,7 +365,7 @@ PHPCI', 'n_emails_sent' => '%d emails envoyés.', 'n_emails_failed' => '%d emails dont l\'envoi a échoué.', 'unable_to_set_env' => 'Impossible d\'initialiser la variable d\'environnement', - 'tag_created' => 'Tag créé par PHPCI: %s', + 'tag_created' => 'Tag créé par PHPCI : %s', 'x_built_at_x' => '%PROJECT_TITLE% construit à %BUILD_URI%', 'hipchat_settings' => 'Merci de définir une "room" et un "authToken" pour le plugin hipchat_notify', 'irc_settings' => 'Vous devez configurer un serveur, une "room" et un "nick".', diff --git a/PHPCI/Languages/lang.ru.php b/PHPCI/Languages/lang.ru.php index 4abc9721..dfef84d9 100644 --- a/PHPCI/Languages/lang.ru.php +++ b/PHPCI/Languages/lang.ru.php @@ -84,7 +84,7 @@ PHPCI', 'success' => 'Успешно', 'successful' => 'Успешна', 'failed' => 'Провалена', - 'manual_build' => 'Ручной сборки', + 'manual_build' => 'Запущена вручную', // Add/Edit Project: 'new_project' => 'Новый проект', diff --git a/PHPCI/Model/Build.php b/PHPCI/Model/Build.php index b5928cfe..5561d51e 100644 --- a/PHPCI/Model/Build.php +++ b/PHPCI/Model/Build.php @@ -194,6 +194,10 @@ class Build extends BuildBase return $rtn; } + /** + * Returns the commit message for this build. + * @return string + */ public function getCommitMessage() { $rtn = htmlspecialchars($this->data['commit_message']); diff --git a/PHPCI/Plugin/Email.php b/PHPCI/Plugin/Email.php index 98b226e3..7c5171d6 100644 --- a/PHPCI/Plugin/Email.php +++ b/PHPCI/Plugin/Email.php @@ -182,7 +182,7 @@ class Email implements \PHPCI\Plugin $addresses[] = $this->options['default_mailto_address']; return $addresses; } - return $addresses; + return array_unique($addresses); } /** diff --git a/PHPCI/Plugin/Pgsql.php b/PHPCI/Plugin/Pgsql.php index 9a5f630c..9d0f924d 100644 --- a/PHPCI/Plugin/Pgsql.php +++ b/PHPCI/Plugin/Pgsql.php @@ -83,7 +83,7 @@ class Pgsql implements \PHPCI\Plugin $pdo = new PDO('pgsql:host=' . $this->host, $this->user, $this->pass, $opts); foreach ($this->queries as $query) { - $pdo->query($query); + $pdo->query($this->phpci->interpolate($query)); } } catch (\Exception $ex) { $this->phpci->logFailure($ex->getMessage()); diff --git a/PHPCI/Plugin/Sqlite.php b/PHPCI/Plugin/Sqlite.php index 1ffdcc88..f80ece3d 100644 --- a/PHPCI/Plugin/Sqlite.php +++ b/PHPCI/Plugin/Sqlite.php @@ -70,7 +70,7 @@ class Sqlite implements \PHPCI\Plugin $pdo = new PDO('sqlite:' . $this->path, $opts); foreach ($this->queries as $query) { - $pdo->query($query); + $pdo->query($this->phpci->interpolate($query)); } } catch (\Exception $ex) { $this->phpci->logFailure($ex->getMessage()); diff --git a/PHPCI/Service/BuildService.php b/PHPCI/Service/BuildService.php index b4721779..ca2336c8 100644 --- a/PHPCI/Service/BuildService.php +++ b/PHPCI/Service/BuildService.php @@ -100,6 +100,7 @@ class BuildService $build = new Build(); $build->setValues($data); $build->setCreated(new \DateTime()); + $build->setStatus(0); return $this->buildStore->save($build); } diff --git a/PHPCI/View/Home/index.phtml b/PHPCI/View/Home/index.phtml index 11db43cf..6e27bb79 100644 --- a/PHPCI/View/Home/index.phtml +++ b/PHPCI/View/Home/index.phtml @@ -48,7 +48,7 @@ ?>
  • - format('M j Y'); ?> +
  • @@ -58,7 +58,7 @@
  • - format('H:i'); ?> +

    getProject()->getTitle(); ?> diff --git a/PHPCI/View/SummaryTable.phtml b/PHPCI/View/SummaryTable.phtml index d8ee8a56..10782a5b 100644 --- a/PHPCI/View/SummaryTable.phtml +++ b/PHPCI/View/SummaryTable.phtml @@ -27,12 +27,12 @@ foreach($projects as $project): case 2: $successes++; $statuses[] = 'ok'; - $success = is_null($success) && !is_null($build->getFinished()) ? $build->getFinished()->format('M j Y g:ia') : $success; + $success = is_null($success) && !is_null($build->getFinished()) ? Lang::formatDateTime($build->getFinished()) : $success; break; case 3: $failures++; $statuses[] = 'failed'; - $failure = is_null($failure) && !is_null($build->getFinished()) ? $build->getFinished()->format('M j Y g:ia') : $failure; + $failure = is_null($failure) && !is_null($build->getFinished()) ? Lang::formatDateTime($build->getFinished()) : $failure; break; } } @@ -59,7 +59,7 @@ foreach($projects as $project): $message = Lang::get('x_of_x_failed', $failures, $buildCount); if (!is_null($lastSuccess) && !is_null($lastSuccess->getFinished())) { - $message .= Lang::get('last_successful_build', $lastSuccess->getFinished()->format('M j Y')); + $message .= Lang::get('last_successful_build', Lang::formatDateTime($lastSuccess->getFinished())); } else { $message .= Lang::get('never_built_successfully'); } @@ -68,7 +68,7 @@ foreach($projects as $project): $shortMessage = Lang::get('all_builds_passed_short', $buildCount, $buildCount); if (!is_null($lastFailure) && !is_null($lastFailure->getFinished())) { - $message .= Lang::get('last_failed_build', $lastFailure->getFinished()->format('M j Y')); + $message .= Lang::get('last_failed_build', Lang::formatDateTime($lastFailure->getFinished())); } else { $message .= Lang::get('never_failed_build'); } diff --git a/PHPCI/View/layout.phtml b/PHPCI/View/layout.phtml index 1aca09c1..64ec0834 100644 --- a/PHPCI/View/layout.phtml +++ b/PHPCI/View/layout.phtml @@ -16,6 +16,7 @@ + + diff --git a/Tests/PHPCI/Helper/LangTest.php b/Tests/PHPCI/Helper/LangTest.php new file mode 100644 index 00000000..492cca52 --- /dev/null +++ b/Tests/PHPCI/Helper/LangTest.php @@ -0,0 +1,28 @@ +prophesize('DateTime'); + $dateTime->format(DateTime::ISO8601)->willReturn("ISODATE"); + $dateTime->format(DateTime::RFC2822)->willReturn("RFCDATE"); + + $this->assertEquals('', Lang::formatDateTime($dateTime->reveal(), 'FORMAT')); + } + + public function testLang_UseDefaultFormat() + { + $dateTime = $this->prophesize('DateTime'); + $dateTime->format(DateTime::ISO8601)->willReturn("ISODATE"); + $dateTime->format(DateTime::RFC2822)->willReturn("RFCDATE"); + + $this->assertEquals('', Lang::formatDateTime($dateTime->reveal())); + } +} diff --git a/Tests/PHPCI/Plugin/EmailTest.php b/Tests/PHPCI/Plugin/EmailTest.php index 87225772..0fa2d8f0 100644 --- a/Tests/PHPCI/Plugin/EmailTest.php +++ b/Tests/PHPCI/Plugin/EmailTest.php @@ -57,6 +57,10 @@ class EmailTest extends \PHPUnit_Framework_TestCase ->method('getStatus') ->will($this->returnValue(\PHPCI\Model\Build::STATUS_SUCCESS)); + $this->mockBuild->expects($this->any()) + ->method('getCommitterEmail') + ->will($this->returnValue("committer@test.com")); + $this->mockCiBuilder = $this->getMock( '\PHPCI\Builder', array( @@ -151,8 +155,53 @@ class EmailTest extends \PHPUnit_Framework_TestCase ); $this->assertEquals($expectedReturn, $returnValue); + } + /** + * @covers PHPUnit::execute + */ + public function testExecute_UniqueRecipientsFromWithCommitter() + { + $this->loadEmailPluginWithOptions( + array( + 'addresses' => array('test-receiver@example.com', 'test-receiver2@example.com') + ) + ); + $actualMails = []; + $this->catchMailPassedToSend($actualMails); + + $returnValue = $this->testedEmailPlugin->execute(); + $this->assertTrue($returnValue); + + $this->assertCount(2, $actualMails); + + $actualTos = array(key($actualMails[0]->getTo()), key($actualMails[1]->getTo())); + $this->assertContains('test-receiver@example.com', $actualTos); + $this->assertContains('test-receiver2@example.com', $actualTos); + } + + /** + * @covers PHPUnit::execute + */ + public function testExecute_UniqueRecipientsWithCommiter() + { + $this->loadEmailPluginWithOptions( + array( + 'commiter' => true, + 'addresses' => array('test-receiver@example.com', 'committer@test.com') + ) + ); + + $actualMails = []; + $this->catchMailPassedToSend($actualMails); + + $returnValue = $this->testedEmailPlugin->execute(); + $this->assertTrue($returnValue); + + $actualTos = array(key($actualMails[0]->getTo()), key($actualMails[1]->getTo())); + $this->assertContains('test-receiver@example.com', $actualTos); + $this->assertContains('committer@test.com', $actualTos); } /** @@ -215,12 +264,16 @@ class EmailTest extends \PHPUnit_Framework_TestCase */ protected function catchMailPassedToSend(&$actualMail) { - $this->mockMailer->expects($this->once()) + $this->mockMailer->expects(is_array($actualMail) ? $this->atLeast(1) : $this->once()) ->method('send') ->will( $this->returnCallback( function ($passedMail) use (&$actualMail) { - $actualMail = $passedMail; + if(is_array($actualMail)) { + $actualMail[] = $passedMail; + } else { + $actualMail = $passedMail; + } return array(); } ) diff --git a/Tests/PHPCI/Service/BuildServiceTest.php b/Tests/PHPCI/Service/BuildServiceTest.php index 9b1097a3..6dff6385 100644 --- a/Tests/PHPCI/Service/BuildServiceTest.php +++ b/Tests/PHPCI/Service/BuildServiceTest.php @@ -56,7 +56,7 @@ class BuildServiceTest extends \PHPUnit_Framework_TestCase $this->assertNull($returnValue->getStarted()); $this->assertNull($returnValue->getFinished()); $this->assertNull($returnValue->getLog()); - $this->assertNull($returnValue->getCommitMessage()); + $this->assertEmpty($returnValue->getCommitMessage()); $this->assertNull($returnValue->getCommitterEmail()); $this->assertNull($returnValue->getExtra()); $this->assertEquals('master', $returnValue->getBranch()); diff --git a/public/assets/css/AdminLTE-custom.css b/public/assets/css/AdminLTE-custom.css new file mode 100644 index 00000000..009a360b --- /dev/null +++ b/public/assets/css/AdminLTE-custom.css @@ -0,0 +1,53 @@ +.skin-blue .logo, .skin-blue .logo:hover { + background-image: url('/assets/img/logo-large.png'); + background-repeat: no-repeat; + background-size: 40%; + background-position: 65px; + text-indent: -5000px; +} + +.build-info-panel { + +} + + .build-info-panel .box-header h1.box-title { + border: 0; + font-size: 1.5em; + font-weight: bold; + margin-left: 110px; + } + + .build-info-panel h1.box-title span { + font-weight: normal; + } + + .build-info-panel img { + border: 2px solid #fff; + border-radius: 50%; + margin-top: -40px; + } + + .build-info-panel #build-info { + margin-left: 110px; + min-height: 50px; + } + + .build-info-panel .commit-message { + margin-bottom: 20px; + } + +.small-box h3 a, .small-box h4 a { + color: #fff; +} + +.pagination>li>span { + font-weight: bold; + background: #337ab7; + color: #fff; +} + +#plugins table td { + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/public/assets/css/daterangepicker/daterangepicker-bs3.css b/public/assets/css/daterangepicker/daterangepicker-bs3.css index eed1e9f4..9ddabb1f 100755 --- a/public/assets/css/daterangepicker/daterangepicker-bs3.css +++ b/public/assets/css/daterangepicker/daterangepicker-bs3.css @@ -18,11 +18,16 @@ margin: 4px; } -.daterangepicker.opensright .ranges, .daterangepicker.opensright .calendar { +.daterangepicker.opensright .ranges, .daterangepicker.opensright .calendar, +.daterangepicker.openscenter .ranges, .daterangepicker.openscenter .calendar { float: right; margin: 4px; } +.daterangepicker.single .ranges, .daterangepicker.single .calendar { + float: none; +} + .daterangepicker .ranges { width: 160px; text-align: left; @@ -41,6 +46,14 @@ max-width: 270px; } +.daterangepicker.show-calendar .calendar { + display: block; +} + +.daterangepicker .calendar.single .calendar-date { + border: none; +} + .daterangepicker .calendar th, .daterangepicker .calendar td { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; white-space: nowrap; @@ -48,7 +61,8 @@ min-width: 32px; } -.daterangepicker .ranges label { +.daterangepicker .daterangepicker_start_input label, +.daterangepicker .daterangepicker_end_input label { color: #333; display: block; font-size: 11px; @@ -66,7 +80,6 @@ } .daterangepicker .ranges .input-mini { - background-color: #eee; border: 1px solid #ccc; border-radius: 4px; color: #555; @@ -153,6 +166,37 @@ content: ''; } +.daterangepicker.openscenter:before { + position: absolute; + top: -7px; + left: 0; + right: 0; + width: 0; + margin-left: auto; + margin-right: auto; + display: inline-block; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-left: 7px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; +} + +.daterangepicker.openscenter:after { + position: absolute; + top: -6px; + left: 0; + right: 0; + width: 0; + margin-left: auto; + margin-right: auto; + display: inline-block; + border-right: 6px solid transparent; + border-bottom: 6px solid #fff; + border-left: 6px solid transparent; + content: ''; +} + .daterangepicker.opensright:before { position: absolute; top: -7px; @@ -196,7 +240,7 @@ color: #999; } -.daterangepicker td.disabled { +.daterangepicker td.disabled, .daterangepicker option.disabled { color: #999; } @@ -211,6 +255,24 @@ border-radius: 0; } +.daterangepicker td.start-date { + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.daterangepicker td.end-date { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.daterangepicker td.start-date.end-date { + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + .daterangepicker td.active, .daterangepicker td.active:hover { background-color: #357ebd; border-color: #3071a9; @@ -239,7 +301,20 @@ width: 40%; } -.daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.ampmselect { +.daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect { width: 50px; margin-bottom: 0; } + +.daterangepicker_start_input { + float: left; +} + +.daterangepicker_end_input { + float: left; + padding-left: 11px +} + +.daterangepicker th.month { + width: auto; +} diff --git a/public/assets/js/build-plugins/phpcpd.js b/public/assets/js/build-plugins/phpcpd.js index c9733678..2a0f3e16 100644 --- a/public/assets/js/build-plugins/phpcpd.js +++ b/public/assets/js/build-plugins/phpcpd.js @@ -23,14 +23,14 @@ var phpcpdPlugin = ActiveBuild.UiPlugin.extend({ render: function() { - return $('' + + return $('
    ' + '' + '' + ' ' + ' ' + ' ' + '' + - '
    '+Lang.get('file')+''+Lang.get('start')+''+Lang.get('end')+'
    '); + '

    '); }, diff --git a/public/assets/js/build-plugins/phpcs.js b/public/assets/js/build-plugins/phpcs.js index f3b1fe67..3e71ba72 100644 --- a/public/assets/js/build-plugins/phpcs.js +++ b/public/assets/js/build-plugins/phpcs.js @@ -22,14 +22,14 @@ var phpcsPlugin = ActiveBuild.UiPlugin.extend({ }, render: function() { - return $('' + + return $('
    ' + '' + '' + ' ' + ' ' + ' ' + '' + - '
    '+Lang.get('file')+''+Lang.get('line')+''+Lang.get('message')+'
    '); + ''); }, onUpdate: function(e) { diff --git a/public/assets/js/build-plugins/phpdoccheck.js b/public/assets/js/build-plugins/phpdoccheck.js index 7d284617..04a6a0ad 100644 --- a/public/assets/js/build-plugins/phpdoccheck.js +++ b/public/assets/js/build-plugins/phpdoccheck.js @@ -24,7 +24,7 @@ var phpdoccheckPlugin = ActiveBuild.UiPlugin.extend({ }, render: function() { - return $('' + + return $('
    ' + '' + '' + ' ' + @@ -32,7 +32,7 @@ var phpdoccheckPlugin = ActiveBuild.UiPlugin.extend({ ' ' + ' ' + '' + - '
    '+Lang.get('file')+''+Lang.get('class')+''+Lang.get('method')+'
    '); + ''); }, onUpdate: function(e) { diff --git a/public/assets/js/build-plugins/phpmd.js b/public/assets/js/build-plugins/phpmd.js index 57723ba9..d6d5f867 100644 --- a/public/assets/js/build-plugins/phpmd.js +++ b/public/assets/js/build-plugins/phpmd.js @@ -25,7 +25,7 @@ var phpmdPlugin = ActiveBuild.UiPlugin.extend({ render: function() { - return $('' + + return $('
    ' + '' + '' + ' ' + @@ -33,7 +33,7 @@ var phpmdPlugin = ActiveBuild.UiPlugin.extend({ ' ' + ' ' + '' + - '
    '+Lang.get('file')+''+Lang.get('end')+''+Lang.get('message')+'
    '); + ''); }, onUpdate: function(e) { diff --git a/public/assets/js/build-plugins/phptallint.js b/public/assets/js/build-plugins/phptallint.js index 9d981c53..fe549b99 100644 --- a/public/assets/js/build-plugins/phptallint.js +++ b/public/assets/js/build-plugins/phptallint.js @@ -22,14 +22,14 @@ var phptalPlugin = ActiveBuild.UiPlugin.extend({ }, render: function() { - return $('' + + return $('
    ' + '' + '' + ' ' + ' ' + ' ' + '' + - '
    FileLineMessage
    '); + ''); }, onUpdate: function(e) { diff --git a/public/assets/js/build-plugins/phpunit.js b/public/assets/js/build-plugins/phpunit.js index 0e648e9d..4a55d905 100644 --- a/public/assets/js/build-plugins/phpunit.js +++ b/public/assets/js/build-plugins/phpunit.js @@ -25,12 +25,12 @@ var phpunitPlugin = ActiveBuild.UiPlugin.extend({ render: function() { - return $('' + + return $('
    ' + '' + '' + ' ' + '' + - '
    '+Lang.get('test')+'
    '); + ''); }, onUpdate: function(e) { diff --git a/public/assets/js/phpci.js b/public/assets/js/phpci.js index f98ea959..7e0b0f8b 100644 --- a/public/assets/js/phpci.js +++ b/public/assets/js/phpci.js @@ -3,7 +3,16 @@ var PHPCI = { intervals: {}, init: function () { + // Setup the date locale + moment.locale(PHPCI_LANGUAGE); + $(document).ready(function () { + // Format datetimes + $('time[datetime]').each(function() { + var $this = $(this); + $this.text(moment(this.dateTime).format($this.data('format') || 'lll')); + }); + // Update latest builds every 5 seconds: PHPCI.getBuilds(); PHPCI.intervals.getBuilds = setInterval(PHPCI.getBuilds, 5000); diff --git a/public/assets/js/plugins/daterangepicker/daterangepicker.js b/public/assets/js/plugins/daterangepicker/daterangepicker.js index 11a00fa3..891a1bf4 100755 --- a/public/assets/js/plugins/daterangepicker/daterangepicker.js +++ b/public/assets/js/plugins/daterangepicker/daterangepicker.js @@ -1,60 +1,39 @@ -// moment.js -// version : 2.1.0 -// author : Tim Wood -// license : MIT -// momentjs.com -!function(t){function e(t,e){return function(n){return u(t.call(this,n),e)}}function n(t,e){return function(n){return this.lang().ordinal(t.call(this,n),e)}}function s(){}function i(t){a(this,t)}function r(t){var e=t.years||t.year||t.y||0,n=t.months||t.month||t.M||0,s=t.weeks||t.week||t.w||0,i=t.days||t.day||t.d||0,r=t.hours||t.hour||t.h||0,a=t.minutes||t.minute||t.m||0,o=t.seconds||t.second||t.s||0,u=t.milliseconds||t.millisecond||t.ms||0;this._input=t,this._milliseconds=u+1e3*o+6e4*a+36e5*r,this._days=i+7*s,this._months=n+12*e,this._data={},this._bubble()}function a(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}function o(t){return 0>t?Math.ceil(t):Math.floor(t)}function u(t,e){for(var n=t+"";n.lengthn;n++)~~t[n]!==~~e[n]&&r++;return r+i}function f(t){return t?ie[t]||t.toLowerCase().replace(/(.)s$/,"$1"):t}function l(t,e){return e.abbr=t,x[t]||(x[t]=new s),x[t].set(e),x[t]}function _(t){if(!t)return H.fn._lang;if(!x[t]&&A)try{require("./lang/"+t)}catch(e){return H.fn._lang}return x[t]}function m(t){return t.match(/\[.*\]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function y(t){var e,n,s=t.match(E);for(e=0,n=s.length;n>e;e++)s[e]=ue[s[e]]?ue[s[e]]:m(s[e]);return function(i){var r="";for(e=0;n>e;e++)r+=s[e]instanceof Function?s[e].call(i,t):s[e];return r}}function M(t,e){function n(e){return t.lang().longDateFormat(e)||e}for(var s=5;s--&&N.test(e);)e=e.replace(N,n);return re[e]||(re[e]=y(e)),re[e](t)}function g(t,e){switch(t){case"DDDD":return V;case"YYYY":return X;case"YYYYY":return $;case"S":case"SS":case"SSS":case"DDD":return I;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return R;case"a":case"A":return _(e._l)._meridiemParse;case"X":return B;case"Z":case"ZZ":return j;case"T":return q;case"MM":case"DD":case"YY":case"HH":case"hh":case"mm":case"ss":case"M":case"D":case"d":case"H":case"h":case"m":case"s":return J;default:return new RegExp(t.replace("\\",""))}}function p(t){var e=(j.exec(t)||[])[0],n=(e+"").match(ee)||["-",0,0],s=+(60*n[1])+~~n[2];return"+"===n[0]?-s:s}function D(t,e,n){var s,i=n._a;switch(t){case"M":case"MM":i[1]=null==e?0:~~e-1;break;case"MMM":case"MMMM":s=_(n._l).monthsParse(e),null!=s?i[1]=s:n._isValid=!1;break;case"D":case"DD":case"DDD":case"DDDD":null!=e&&(i[2]=~~e);break;case"YY":i[0]=~~e+(~~e>68?1900:2e3);break;case"YYYY":case"YYYYY":i[0]=~~e;break;case"a":case"A":n._isPm=_(n._l).isPM(e);break;case"H":case"HH":case"h":case"hh":i[3]=~~e;break;case"m":case"mm":i[4]=~~e;break;case"s":case"ss":i[5]=~~e;break;case"S":case"SS":case"SSS":i[6]=~~(1e3*("0."+e));break;case"X":n._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":n._useUTC=!0,n._tzm=p(e)}null==e&&(n._isValid=!1)}function Y(t){var e,n,s=[];if(!t._d){for(e=0;7>e;e++)t._a[e]=s[e]=null==t._a[e]?2===e?1:0:t._a[e];s[3]+=~~((t._tzm||0)/60),s[4]+=~~((t._tzm||0)%60),n=new Date(0),t._useUTC?(n.setUTCFullYear(s[0],s[1],s[2]),n.setUTCHours(s[3],s[4],s[5],s[6])):(n.setFullYear(s[0],s[1],s[2]),n.setHours(s[3],s[4],s[5],s[6])),t._d=n}}function w(t){var e,n,s=t._f.match(E),i=t._i;for(t._a=[],e=0;eo&&(u=o,s=n);a(t,s)}function v(t){var e,n=t._i,s=K.exec(n);if(s){for(t._f="YYYY-MM-DD"+(s[2]||" "),e=0;4>e;e++)if(te[e][1].exec(n)){t._f+=te[e][0];break}j.exec(n)&&(t._f+=" Z"),w(t)}else t._d=new Date(n)}function T(e){var n=e._i,s=G.exec(n);n===t?e._d=new Date:s?e._d=new Date(+s[1]):"string"==typeof n?v(e):d(n)?(e._a=n.slice(0),Y(e)):e._d=n instanceof Date?new Date(+n):new Date(n)}function b(t,e,n,s,i){return i.relativeTime(e||1,!!n,t,s)}function S(t,e,n){var s=W(Math.abs(t)/1e3),i=W(s/60),r=W(i/60),a=W(r/24),o=W(a/365),u=45>s&&["s",s]||1===i&&["m"]||45>i&&["mm",i]||1===r&&["h"]||22>r&&["hh",r]||1===a&&["d"]||25>=a&&["dd",a]||45>=a&&["M"]||345>a&&["MM",W(a/30)]||1===o&&["y"]||["yy",o];return u[2]=e,u[3]=t>0,u[4]=n,b.apply({},u)}function F(t,e,n){var s,i=n-e,r=n-t.day();return r>i&&(r-=7),i-7>r&&(r+=7),s=H(t).add("d",r),{week:Math.ceil(s.dayOfYear()/7),year:s.year()}}function O(t){var e=t._i,n=t._f;return null===e||""===e?null:("string"==typeof e&&(t._i=e=_().preparse(e)),H.isMoment(e)?(t=a({},e),t._d=new Date(+e._d)):n?d(n)?k(t):w(t):T(t),new i(t))}function z(t,e){H.fn[t]=H.fn[t+"s"]=function(t){var n=this._isUTC?"UTC":"";return null!=t?(this._d["set"+n+e](t),H.updateOffset(this),this):this._d["get"+n+e]()}}function C(t){H.duration.fn[t]=function(){return this._data[t]}}function L(t,e){H.duration.fn["as"+t]=function(){return+this/e}}for(var H,P,U="2.1.0",W=Math.round,x={},A="undefined"!=typeof module&&module.exports,G=/^\/?Date\((\-?\d+)/i,Z=/(\-)?(\d*)?\.?(\d+)\:(\d+)\:(\d+)\.?(\d{3})?/,E=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,N=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,J=/\d\d?/,I=/\d{1,3}/,V=/\d{3}/,X=/\d{1,4}/,$=/[+\-]?\d{1,6}/,R=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,j=/Z|[\+\-]\d\d:?\d\d/i,q=/T/i,B=/[\+\-]?\d+(\.\d{1,3})?/,K=/^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,Q="YYYY-MM-DDTHH:mm:ssZ",te=[["HH:mm:ss.S",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],ee=/([\+\-]|\d\d)/gi,ne="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),se={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},ie={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",w:"week",M:"month",y:"year"},re={},ae="DDD w W M D d".split(" "),oe="M D H h m s w W".split(" "),ue={M:function(){return this.month()+1},MMM:function(t){return this.lang().monthsShort(this,t)},MMMM:function(t){return this.lang().months(this,t)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(t){return this.lang().weekdaysMin(this,t)},ddd:function(t){return this.lang().weekdaysShort(this,t)},dddd:function(t){return this.lang().weekdays(this,t)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return u(this.year()%100,2)},YYYY:function(){return u(this.year(),4)},YYYYY:function(){return u(this.year(),5)},gg:function(){return u(this.weekYear()%100,2)},gggg:function(){return this.weekYear()},ggggg:function(){return u(this.weekYear(),5)},GG:function(){return u(this.isoWeekYear()%100,2)},GGGG:function(){return this.isoWeekYear()},GGGGG:function(){return u(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return~~(this.milliseconds()/100)},SS:function(){return u(~~(this.milliseconds()/10),2)},SSS:function(){return u(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+u(~~(t/60),2)+":"+u(~~t%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+u(~~(10*t/6),4)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()}};ae.length;)P=ae.pop(),ue[P+"o"]=n(ue[P],P);for(;oe.length;)P=oe.pop(),ue[P+P]=e(ue[P],2);for(ue.DDDD=e(ue.DDD,3),s.prototype={set:function(t){var e,n;for(n in t)e=t[n],"function"==typeof e?this[n]=e:this["_"+n]=e},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,n,s;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(n=H([2e3,e]),s="^"+this.months(n,"")+"|^"+this.monthsShort(n,""),this._monthsParse[e]=new RegExp(s.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},weekdaysParse:function(t){var e,n,s;for(this._weekdaysParse||(this._weekdaysParse=[]),e=0;7>e;e++)if(this._weekdaysParse[e]||(n=H([2e3,1]).day(e),s="^"+this.weekdays(n,"")+"|^"+this.weekdaysShort(n,"")+"|^"+this.weekdaysMin(n,""),this._weekdaysParse[e]=new RegExp(s.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(t){var e=this._longDateFormat[t];return!e&&this._longDateFormat[t.toUpperCase()]&&(e=this._longDateFormat[t.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t]=e),e},isPM:function(t){return"p"===(t+"").toLowerCase()[0]},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var n=this._calendar[t];return"function"==typeof n?n.apply(e):n},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,n,s){var i=this._relativeTime[n];return"function"==typeof i?i(t,e,n,s):i.replace(/%d/i,t)},pastFuture:function(t,e){var n=this._relativeTime[t>0?"future":"past"];return"function"==typeof n?n(e):n.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return F(t,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6}},H=function(t,e,n){return O({_i:t,_f:e,_l:n,_isUTC:!1})},H.utc=function(t,e,n){return O({_useUTC:!0,_isUTC:!0,_l:n,_i:t,_f:e})},H.unix=function(t){return H(1e3*t)},H.duration=function(t,e){var n,s,i=H.isDuration(t),a="number"==typeof t,o=i?t._input:a?{}:t,u=Z.exec(t);return a?e?o[e]=t:o.milliseconds=t:u&&(n="-"===u[1]?-1:1,o={y:0,d:~~u[2]*n,h:~~u[3]*n,m:~~u[4]*n,s:~~u[5]*n,ms:~~u[6]*n}),s=new r(o),i&&t.hasOwnProperty("_lang")&&(s._lang=t._lang),s},H.version=U,H.defaultFormat=Q,H.updateOffset=function(){},H.lang=function(t,e){return t?(e?l(t,e):x[t]||_(t),H.duration.fn._lang=H.fn._lang=_(t),void 0):H.fn._lang._abbr},H.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),_(t)},H.isMoment=function(t){return t instanceof i},H.isDuration=function(t){return t instanceof r},H.fn=i.prototype={clone:function(){return H(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){return M(H(this).utc(),"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var t=this;return[t.year(),t.month(),t.date(),t.hours(),t.minutes(),t.seconds(),t.milliseconds()]},isValid:function(){return null==this._isValid&&(this._isValid=this._a?!c(this._a,(this._isUTC?H.utc(this._a):H(this._a)).toArray()):!isNaN(this._d.getTime())),!!this._isValid},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(t){var e=M(this,t||H.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var n;return n="string"==typeof t?H.duration(+e,t):H.duration(t,e),h(this,n,1),this},subtract:function(t,e){var n;return n="string"==typeof t?H.duration(+e,t):H.duration(t,e),h(this,n,-1),this},diff:function(t,e,n){var s,i,r=this._isUTC?H(t).zone(this._offset||0):H(t).local(),a=6e4*(this.zone()-r.zone());return e=f(e),"year"===e||"month"===e?(s=432e5*(this.daysInMonth()+r.daysInMonth()),i=12*(this.year()-r.year())+(this.month()-r.month()),i+=(this-H(this).startOf("month")-(r-H(r).startOf("month")))/s,i-=6e4*(this.zone()-H(this).startOf("month").zone()-(r.zone()-H(r).startOf("month").zone()))/s,"year"===e&&(i/=12)):(s=this-r,i="second"===e?s/1e3:"minute"===e?s/6e4:"hour"===e?s/36e5:"day"===e?(s-a)/864e5:"week"===e?(s-a)/6048e5:s),n?i:o(i)},from:function(t,e){return H.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(H(),t)},calendar:function(){var t=this.diff(H().startOf("day"),"days",!0),e=-6>t?"sameElse":-1>t?"lastWeek":0>t?"lastDay":1>t?"sameDay":2>t?"nextDay":7>t?"nextWeek":"sameElse";return this.format(this.lang().calendar(e,this))},isLeapYear:function(){var t=this.year();return 0===t%4&&0!==t%100||0===t%400},isDST:function(){return this.zone()+H(t).startOf(e)},isBefore:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)<+H(t).startOf(e)},isSame:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)===+H(t).startOf(e)},min:function(t){return t=H.apply(null,arguments),this>t?this:t},max:function(t){return t=H.apply(null,arguments),t>this?this:t},zone:function(t){var e=this._offset||0;return null==t?this._isUTC?e:this._d.getTimezoneOffset():("string"==typeof t&&(t=p(t)),Math.abs(t)<16&&(t=60*t),this._offset=t,this._isUTC=!0,e!==t&&h(this,H.duration(e-t,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},daysInMonth:function(){return H.utc([this.year(),this.month()+1,0]).date()},dayOfYear:function(t){var e=W((H(this).startOf("day")-H(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},weekYear:function(t){var e=F(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==t?e:this.add("y",t-e)},isoWeekYear:function(t){var e=F(this,1,4).year;return null==t?e:this.add("y",t-e)},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},isoWeek:function(t){var e=F(this,1,4).week;return null==t?e:this.add("d",7*(t-e))},weekday:function(t){var e=(this._d.getDay()+7-this.lang()._week.dow)%7;return null==t?e:this.add("d",t-e)},isoWeekday:function(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)},lang:function(e){return e===t?this._lang:(this._lang=_(e),this)}},P=0;P' + - '
    ' + + '
    ' + + '
    ' + '
    ' + '
    ' + - '
    ' + - '' + - '' + + '
    ' + + '' + + '' + '
    ' + - '
    ' + - '' + - '' + + '
    ' + + '' + + '' + '
    ' + - ' ' + - '' + + ' ' + + '' + '
    ' + '
    ' + '
    '; - this.parentEl = (hasOptions && options.parentEl && $(options.parentEl)) || $(this.parentEl); - //the date range picker + //custom options + if (typeof options !== 'object' || options === null) + options = {}; + + this.parentEl = (typeof options === 'object' && options.parentEl && $(options.parentEl).length) ? $(options.parentEl) : $(this.parentEl); this.container = $(DRPTemplate).appendTo(this.parentEl); - if (hasOptions) { + this.setOptions(options, cb); - if (typeof options.format == 'string') + //apply CSS classes and labels to buttons + var c = this.container; + $.each(this.buttonClasses, function (idx, val) { + c.find('button').addClass(val); + }); + this.container.find('.daterangepicker_start_input label').html(this.locale.fromLabel); + this.container.find('.daterangepicker_end_input label').html(this.locale.toLabel); + if (this.applyClass.length) + this.container.find('.applyBtn').addClass(this.applyClass); + if (this.cancelClass.length) + this.container.find('.cancelBtn').addClass(this.cancelClass); + this.container.find('.applyBtn').html(this.locale.applyLabel); + this.container.find('.cancelBtn').html(this.locale.cancelLabel); + + //event listeners + + this.container.find('.calendar') + .on('click.daterangepicker', '.prev', $.proxy(this.clickPrev, this)) + .on('click.daterangepicker', '.next', $.proxy(this.clickNext, this)) + .on('click.daterangepicker', 'td.available', $.proxy(this.clickDate, this)) + .on('mouseenter.daterangepicker', 'td.available', $.proxy(this.hoverDate, this)) + .on('mouseleave.daterangepicker', 'td.available', $.proxy(this.updateFormInputs, this)) + .on('change.daterangepicker', 'select.yearselect', $.proxy(this.updateMonthYear, this)) + .on('change.daterangepicker', 'select.monthselect', $.proxy(this.updateMonthYear, this)) + .on('change.daterangepicker', 'select.hourselect,select.minuteselect,select.secondselect,select.ampmselect', $.proxy(this.updateTime, this)); + + this.container.find('.ranges') + .on('click.daterangepicker', 'button.applyBtn', $.proxy(this.clickApply, this)) + .on('click.daterangepicker', 'button.cancelBtn', $.proxy(this.clickCancel, this)) + .on('click.daterangepicker', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.showCalendars, this)) + .on('change.daterangepicker', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.inputsChanged, this)) + .on('keydown.daterangepicker', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.inputsKeydown, this)) + .on('click.daterangepicker', 'li', $.proxy(this.clickRange, this)) + .on('mouseenter.daterangepicker', 'li', $.proxy(this.enterRange, this)) + .on('mouseleave.daterangepicker', 'li', $.proxy(this.updateFormInputs, this)); + + if (this.element.is('input')) { + this.element.on({ + 'click.daterangepicker': $.proxy(this.show, this), + 'focus.daterangepicker': $.proxy(this.show, this), + 'keyup.daterangepicker': $.proxy(this.updateFromControl, this) + }); + } else { + this.element.on('click.daterangepicker', $.proxy(this.toggle, this)); + } + + }; + + DateRangePicker.prototype = { + + constructor: DateRangePicker, + + setOptions: function(options, callback) { + + this.startDate = moment().startOf('day'); + this.endDate = moment().endOf('day'); + this.timeZone = moment().zone(); + this.minDate = false; + this.maxDate = false; + this.dateLimit = false; + + this.showDropdowns = false; + this.showWeekNumbers = false; + this.timePicker = false; + this.timePickerSeconds = false; + this.timePickerIncrement = 30; + this.timePicker12Hour = true; + this.singleDatePicker = false; + this.ranges = {}; + + this.opens = 'right'; + if (this.element.hasClass('pull-right')) + this.opens = 'left'; + + this.buttonClasses = ['btn', 'btn-small btn-sm']; + this.applyClass = 'btn-success'; + this.cancelClass = 'btn-default'; + + this.format = 'MM/DD/YYYY'; + this.separator = ' - '; + + this.locale = { + applyLabel: 'Apply', + cancelLabel: 'Cancel', + fromLabel: 'From', + toLabel: 'To', + weekLabel: 'W', + customRangeLabel: 'Custom Range', + daysOfWeek: moment.weekdaysMin(), + monthNames: moment.monthsShort(), + firstDay: moment.localeData()._week.dow + }; + + this.cb = function () { }; + + if (typeof options.format === 'string') this.format = options.format; - if (typeof options.separator == 'string') + if (typeof options.separator === 'string') this.separator = options.separator; - if (typeof options.startDate == 'string') + if (typeof options.startDate === 'string') this.startDate = moment(options.startDate, this.format); - if (typeof options.endDate == 'string') + if (typeof options.endDate === 'string') this.endDate = moment(options.endDate, this.format); - if (typeof options.minDate == 'string') + if (typeof options.minDate === 'string') this.minDate = moment(options.minDate, this.format); - if (typeof options.maxDate == 'string') + if (typeof options.maxDate === 'string') this.maxDate = moment(options.maxDate, this.format); - if (typeof options.startDate == 'object') + if (typeof options.startDate === 'object') this.startDate = moment(options.startDate); - if (typeof options.endDate == 'object') + if (typeof options.endDate === 'object') this.endDate = moment(options.endDate); - if (typeof options.minDate == 'object') + if (typeof options.minDate === 'object') this.minDate = moment(options.minDate); - if (typeof options.maxDate == 'object') + if (typeof options.maxDate === 'object') this.maxDate = moment(options.maxDate); - if (typeof options.ranges == 'object') { - for (var range in options.ranges) { + if (typeof options.applyClass === 'string') + this.applyClass = options.applyClass; - var start = moment(options.ranges[range][0]); - var end = moment(options.ranges[range][1]); + if (typeof options.cancelClass === 'string') + this.cancelClass = options.cancelClass; + + if (typeof options.dateLimit === 'object') + this.dateLimit = options.dateLimit; + + if (typeof options.locale === 'object') { + + if (typeof options.locale.daysOfWeek === 'object') { + // Create a copy of daysOfWeek to avoid modification of original + // options object for reusability in multiple daterangepicker instances + this.locale.daysOfWeek = options.locale.daysOfWeek.slice(); + } + + if (typeof options.locale.monthNames === 'object') { + this.locale.monthNames = options.locale.monthNames.slice(); + } + + if (typeof options.locale.firstDay === 'number') { + this.locale.firstDay = options.locale.firstDay; + } + + if (typeof options.locale.applyLabel === 'string') { + this.locale.applyLabel = options.locale.applyLabel; + } + + if (typeof options.locale.cancelLabel === 'string') { + this.locale.cancelLabel = options.locale.cancelLabel; + } + + if (typeof options.locale.fromLabel === 'string') { + this.locale.fromLabel = options.locale.fromLabel; + } + + if (typeof options.locale.toLabel === 'string') { + this.locale.toLabel = options.locale.toLabel; + } + + if (typeof options.locale.weekLabel === 'string') { + this.locale.weekLabel = options.locale.weekLabel; + } + + if (typeof options.locale.customRangeLabel === 'string') { + this.locale.customRangeLabel = options.locale.customRangeLabel; + } + } + + if (typeof options.opens === 'string') + this.opens = options.opens; + + if (typeof options.showWeekNumbers === 'boolean') { + this.showWeekNumbers = options.showWeekNumbers; + } + + if (typeof options.buttonClasses === 'string') { + this.buttonClasses = [options.buttonClasses]; + } + + if (typeof options.buttonClasses === 'object') { + this.buttonClasses = options.buttonClasses; + } + + if (typeof options.showDropdowns === 'boolean') { + this.showDropdowns = options.showDropdowns; + } + + if (typeof options.singleDatePicker === 'boolean') { + this.singleDatePicker = options.singleDatePicker; + if (this.singleDatePicker) { + this.endDate = this.startDate.clone(); + } + } + + if (typeof options.timePicker === 'boolean') { + this.timePicker = options.timePicker; + } + + if (typeof options.timePickerSeconds === 'boolean') { + this.timePickerSeconds = options.timePickerSeconds; + } + + if (typeof options.timePickerIncrement === 'number') { + this.timePickerIncrement = options.timePickerIncrement; + } + + if (typeof options.timePicker12Hour === 'boolean') { + this.timePicker12Hour = options.timePicker12Hour; + } + + // update day names order to firstDay + if (this.locale.firstDay != 0) { + var iterator = this.locale.firstDay; + while (iterator > 0) { + this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift()); + iterator--; + } + } + + var start, end, range; + + //if no start/end dates set, check if an input element contains initial values + if (typeof options.startDate === 'undefined' && typeof options.endDate === 'undefined') { + if ($(this.element).is('input[type=text]')) { + var val = $(this.element).val(), + split = val.split(this.separator); + + start = end = null; + + if (split.length == 2) { + start = moment(split[0], this.format); + end = moment(split[1], this.format); + } else if (this.singleDatePicker && val !== "") { + start = moment(val, this.format); + end = moment(val, this.format); + } + if (start !== null && end !== null) { + this.startDate = start; + this.endDate = end; + } + } + } + + // bind the time zone used to build the calendar to either the timeZone passed in through the options or the zone of the startDate (which will be the local time zone by default) + if (typeof options.timeZone === 'string' || typeof options.timeZone === 'number') { + this.timeZone = options.timeZone; + this.startDate.zone(this.timeZone); + this.endDate.zone(this.timeZone); + } else { + this.timeZone = moment(this.startDate).zone(); + } + + if (typeof options.ranges === 'object') { + for (range in options.ranges) { + + if (typeof options.ranges[range][0] === 'string') + start = moment(options.ranges[range][0], this.format); + else + start = moment(options.ranges[range][0]); + + if (typeof options.ranges[range][1] === 'string') + end = moment(options.ranges[range][1], this.format); + else + end = moment(options.ranges[range][1]); // If we have a min/max date set, bound this range // to it, but only if it would otherwise fall @@ -173,169 +365,123 @@ } var list = '
      '; - for (var range in this.ranges) { + for (range in this.ranges) { list += '
    • ' + range + '
    • '; } list += '
    • ' + this.locale.customRangeLabel + '
    • '; list += '
    '; + this.container.find('.ranges ul').remove(); this.container.find('.ranges').prepend(list); } - if (typeof options.dateLimit == 'object') - this.dateLimit = options.dateLimit; + if (typeof callback === 'function') { + this.cb = callback; + } - // update day names order to firstDay - if (typeof options.locale == 'object') { + if (!this.timePicker) { + this.startDate = this.startDate.startOf('day'); + this.endDate = this.endDate.endOf('day'); + } - if (typeof options.locale.daysOfWeek == 'object') { + if (this.singleDatePicker) { + this.opens = 'right'; + this.container.addClass('single'); + this.container.find('.calendar.right').show(); + this.container.find('.calendar.left').hide(); + if (!this.timePicker) { + this.container.find('.ranges').hide(); + } else { + this.container.find('.ranges .daterangepicker_start_input, .ranges .daterangepicker_end_input').hide(); + } + if (!this.container.find('.calendar.right').hasClass('single')) + this.container.find('.calendar.right').addClass('single'); + } else { + this.container.removeClass('single'); + this.container.find('.calendar.right').removeClass('single'); + this.container.find('.ranges').show(); + } - // Create a copy of daysOfWeek to avoid modification of original - // options object for reusability in multiple daterangepicker instances - this.locale.daysOfWeek = options.locale.daysOfWeek.slice(); + this.oldStartDate = this.startDate.clone(); + this.oldEndDate = this.endDate.clone(); + this.oldChosenLabel = this.chosenLabel; + + this.leftCalendar = { + month: moment([this.startDate.year(), this.startDate.month(), 1, this.startDate.hour(), this.startDate.minute(), this.startDate.second()]), + calendar: [] + }; + + this.rightCalendar = { + month: moment([this.endDate.year(), this.endDate.month(), 1, this.endDate.hour(), this.endDate.minute(), this.endDate.second()]), + calendar: [] + }; + + if (this.opens == 'right' || this.opens == 'center') { + //swap calendar positions + var first = this.container.find('.calendar.first'); + var second = this.container.find('.calendar.second'); + + if (second.hasClass('single')) { + second.removeClass('single'); + first.addClass('single'); } - if (typeof options.locale.firstDay == 'number') { - this.locale.firstDay = options.locale.firstDay; - var iterator = options.locale.firstDay; - while (iterator > 0) { - this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift()); - iterator--; - } + first.removeClass('left').addClass('right'); + second.removeClass('right').addClass('left'); + + if (this.singleDatePicker) { + first.show(); + second.hide(); } } - if (typeof options.opens == 'string') - this.opens = options.opens; - - if (typeof options.showWeekNumbers == 'boolean') { - this.showWeekNumbers = options.showWeekNumbers; + if (typeof options.ranges === 'undefined' && !this.singleDatePicker) { + this.container.addClass('show-calendar'); } - if (typeof options.buttonClasses == 'string') { - this.buttonClasses = [options.buttonClasses]; - } + this.container.addClass('opens' + this.opens); - if (typeof options.buttonClasses == 'object') { - this.buttonClasses = options.buttonClasses; - } + this.updateView(); + this.updateCalendars(); - if (typeof options.showDropdowns == 'boolean') { - this.showDropdowns = options.showDropdowns; - } + }, - if (typeof options.timePicker == 'boolean') { - this.timePicker = options.timePicker; - } + setStartDate: function(startDate) { + if (typeof startDate === 'string') + this.startDate = moment(startDate, this.format).zone(this.timeZone); - if (typeof options.timePickerIncrement == 'number') { - this.timePickerIncrement = options.timePickerIncrement; - } + if (typeof startDate === 'object') + this.startDate = moment(startDate); - if (typeof options.timePicker12Hour == 'boolean') { - this.timePicker12Hour = options.timePicker12Hour; - } + if (!this.timePicker) + this.startDate = this.startDate.startOf('day'); - } + this.oldStartDate = this.startDate.clone(); - if (!this.timePicker) { - this.startDate = this.startDate.startOf('day'); - this.endDate = this.endDate.startOf('day'); - } + this.updateView(); + this.updateCalendars(); + this.updateInputText(); + }, - //apply CSS classes to buttons - var c = this.container; - $.each(this.buttonClasses, function (idx, val) { - c.find('button').addClass(val); - }); + setEndDate: function(endDate) { + if (typeof endDate === 'string') + this.endDate = moment(endDate, this.format).zone(this.timeZone); - if (this.opens == 'right') { - //swap calendar positions - var left = this.container.find('.calendar.left'); - var right = this.container.find('.calendar.right'); - left.removeClass('left').addClass('right'); - right.removeClass('right').addClass('left'); - } + if (typeof endDate === 'object') + this.endDate = moment(endDate); - if (typeof options == 'undefined' || typeof options.ranges == 'undefined') { - this.container.find('.calendar').show(); - this.move(); - } + if (!this.timePicker) + this.endDate = this.endDate.endOf('day'); - if (typeof cb == 'function') - this.cb = cb; + this.oldEndDate = this.endDate.clone(); - this.container.addClass('opens' + this.opens); - - //try parse date if in text input - if (!hasOptions || (typeof options.startDate == 'undefined' && typeof options.endDate == 'undefined')) { - if ($(this.element).is('input[type=text]')) { - var val = $(this.element).val(); - var split = val.split(this.separator); - var start, end; - if (split.length == 2) { - start = moment(split[0], this.format); - end = moment(split[1], this.format); - } - if (start != null && end != null) { - this.startDate = start; - this.endDate = end; - } - } - } - - //state - this.oldStartDate = this.startDate.clone(); - this.oldEndDate = this.endDate.clone(); - - this.leftCalendar = { - month: moment([this.startDate.year(), this.startDate.month(), 1, this.startDate.hour(), this.startDate.minute()]), - calendar: [] - }; - - this.rightCalendar = { - month: moment([this.endDate.year(), this.endDate.month(), 1, this.endDate.hour(), this.endDate.minute()]), - calendar: [] - }; - - //event listeners - this.container.on('mousedown', $.proxy(this.mousedown, this)); - - this.container.find('.calendar') - .on('click', '.prev', $.proxy(this.clickPrev, this)) - .on('click', '.next', $.proxy(this.clickNext, this)) - .on('click', 'td.available', $.proxy(this.clickDate, this)) - .on('mouseenter', 'td.available', $.proxy(this.enterDate, this)) - .on('mouseleave', 'td.available', $.proxy(this.updateFormInputs, this)) - .on('change', 'select.yearselect', $.proxy(this.updateMonthYear, this)) - .on('change', 'select.monthselect', $.proxy(this.updateMonthYear, this)) - .on('change', 'select.hourselect,select.minuteselect,select.ampmselect', $.proxy(this.updateTime, this)); - - this.container.find('.ranges') - .on('click', 'button.applyBtn', $.proxy(this.clickApply, this)) - .on('click', 'button.cancelBtn', $.proxy(this.clickCancel, this)) - .on('click', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.showCalendars, this)) - .on('click', 'li', $.proxy(this.clickRange, this)) - .on('mouseenter', 'li', $.proxy(this.enterRange, this)) - .on('mouseleave', 'li', $.proxy(this.updateFormInputs, this)); - - this.element.on('keyup', $.proxy(this.updateFromControl, this)); - - this.updateView(); - this.updateCalendars(); - - }; - - DateRangePicker.prototype = { - - constructor: DateRangePicker, - - mousedown: function (e) { - e.stopPropagation(); + this.updateView(); + this.updateCalendars(); + this.updateInputText(); }, updateView: function () { - this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()); - this.rightCalendar.month.month(this.endDate.month()).year(this.endDate.year()); + this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()).hour(this.startDate.hour()).minute(this.startDate.minute()); + this.rightCalendar.month.month(this.endDate.month()).year(this.endDate.year()).hour(this.endDate.hour()).minute(this.endDate.minute()); this.updateFormInputs(); }, @@ -354,11 +500,20 @@ if (!this.element.is('input')) return; if (!this.element.val().length) return; - var dateString = this.element.val().split(this.separator); - var start = moment(dateString[0], this.format); - var end = moment(dateString[1], this.format); + var dateString = this.element.val().split(this.separator), + start = null, + end = null; + + if(dateString.length === 2) { + start = moment(dateString[0], this.format).zone(this.timeZone); + end = moment(dateString[1], this.format).zone(this.timeZone); + } + + if (this.singleDatePicker || start === null || end === null) { + start = moment(this.element.val(), this.format).zone(this.timeZone); + end = start; + } - if (start == null || end == null) return; if (end.isBefore(start)) return; this.oldStartDate = this.startDate.clone(); @@ -375,18 +530,24 @@ notify: function () { this.updateView(); - this.cb(this.startDate, this.endDate); + this.cb(this.startDate, this.endDate, this.chosenLabel); }, move: function () { - var parentOffset = { - top: this.parentEl.offset().top - (this.parentEl.is('body') ? 0 : this.parentEl.scrollTop()), - left: this.parentEl.offset().left - (this.parentEl.is('body') ? 0 : this.parentEl.scrollLeft()) - }; + var parentOffset = { top: 0, left: 0 }; + var parentRightEdge = $(window).width(); + if (!this.parentEl.is('body')) { + parentOffset = { + top: this.parentEl.offset().top - this.parentEl.scrollTop(), + left: this.parentEl.offset().left - this.parentEl.scrollLeft() + }; + parentRightEdge = this.parentEl[0].clientWidth + this.parentEl.offset().left; + } + if (this.opens == 'left') { this.container.css({ top: this.element.offset().top + this.element.outerHeight() - parentOffset.top, - right: $(window).width() - this.element.offset().left - this.element.outerWidth() - parentOffset.left, + right: parentRightEdge - this.element.offset().left - this.element.outerWidth(), left: 'auto' }); if (this.container.offset().left < 0) { @@ -395,6 +556,19 @@ left: 9 }); } + } else if (this.opens == 'center') { + this.container.css({ + top: this.element.offset().top + this.element.outerHeight() - parentOffset.top, + left: this.element.offset().left - parentOffset.left + this.element.outerWidth() / 2 + - this.container.outerWidth() / 2, + right: 'auto' + }); + if (this.container.offset().left < 0) { + this.container.css({ + right: 'auto', + left: 9 + }); + } } else { this.container.css({ top: this.element.offset().top + this.element.outerHeight() - parentOffset.top, @@ -410,20 +584,58 @@ } }, + toggle: function (e) { + if (this.element.hasClass('active')) { + this.hide(); + } else { + this.show(); + } + }, + show: function (e) { + if (this.isShowing) return; + + this.element.addClass('active'); this.container.show(); this.move(); - if (e) { - e.stopPropagation(); - e.preventDefault(); - } + // Create a click proxy that is private to this instance of datepicker, for unbinding + this._outsideClickProxy = $.proxy(function (e) { this.outsideClick(e); }, this); + // Bind global datepicker mousedown for hiding and + $(document) + .on('mousedown.daterangepicker', this._outsideClickProxy) + // also support mobile devices + .on('touchend.daterangepicker', this._outsideClickProxy) + // also explicitly play nice with Bootstrap dropdowns, which stopPropagation when clicking them + .on('click.daterangepicker', '[data-toggle=dropdown]', this._outsideClickProxy) + // and also close when focus changes to outside the picker (eg. tabbing between controls) + .on('focusin.daterangepicker', this._outsideClickProxy); - $(document).on('mousedown', $.proxy(this.hide, this)); - this.element.trigger('shown', {target: e.target, picker: this}); + this.isShowing = true; + this.element.trigger('show.daterangepicker', this); + }, + + outsideClick: function (e) { + var target = $(e.target); + // if the page is clicked anywhere except within the daterangerpicker/button + // itself then call this.hide() + if ( + // ie modal dialog fix + e.type == "focusin" || + target.closest(this.element).length || + target.closest(this.container).length || + target.closest('.calendar-date').length + ) return; + this.hide(); }, hide: function (e) { + if (!this.isShowing) return; + + $(document) + .off('.daterangepicker'); + + this.element.removeClass('active'); this.container.hide(); if (!this.startDate.isSame(this.oldStartDate) || !this.endDate.isSame(this.oldEndDate)) @@ -432,11 +644,12 @@ this.oldStartDate = this.startDate.clone(); this.oldEndDate = this.endDate.clone(); - $(document).off('mousedown', this.hide); - this.element.trigger('hidden', { picker: this }); + this.isShowing = false; + this.element.trigger('hide.daterangepicker', this); }, enterRange: function (e) { + // mouse pointer has entered a range label var label = e.target.innerHTML; if (label == this.locale.customRangeLabel) { this.updateView(); @@ -448,17 +661,51 @@ }, showCalendars: function() { - this.container.find('.calendar').show(); + this.container.addClass('show-calendar'); this.move(); + this.element.trigger('showCalendar.daterangepicker', this); + }, + + hideCalendars: function() { + this.container.removeClass('show-calendar'); + this.element.trigger('hideCalendar.daterangepicker', this); + }, + + // when a date is typed into the start to end date textboxes + inputsChanged: function (e) { + var el = $(e.target); + var date = moment(el.val(), this.format); + if (!date.isValid()) return; + + var startDate, endDate; + if (el.attr('name') === 'daterangepicker_start') { + startDate = date; + endDate = this.endDate; + } else { + startDate = this.startDate; + endDate = date; + } + this.setCustomDates(startDate, endDate); + }, + + inputsKeydown: function(e) { + if (e.keyCode === 13) { + this.inputsChanged(e); + this.notify(); + } }, updateInputText: function() { - if (this.element.is('input')) + if (this.element.is('input') && !this.singleDatePicker) { this.element.val(this.startDate.format(this.format) + this.separator + this.endDate.format(this.format)); + } else if (this.element.is('input')) { + this.element.val(this.endDate.format(this.format)); + } }, clickRange: function (e) { var label = e.target.innerHTML; + this.chosenLabel = label; if (label == this.locale.customRangeLabel) { this.showCalendars(); } else { @@ -469,7 +716,7 @@ if (!this.timePicker) { this.startDate.startOf('day'); - this.endDate.startOf('day'); + this.endDate.endOf('day'); } this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()).hour(this.startDate.hour()).minute(this.startDate.minute()); @@ -478,17 +725,18 @@ this.updateInputText(); - this.container.find('.calendar').hide(); + this.hideCalendars(); this.hide(); + this.element.trigger('apply.daterangepicker', this); } }, clickPrev: function (e) { var cal = $(e.target).parents('.calendar'); if (cal.hasClass('left')) { - this.leftCalendar.month.subtract('month', 1); + this.leftCalendar.month.subtract(1, 'month'); } else { - this.rightCalendar.month.subtract('month', 1); + this.rightCalendar.month.subtract(1, 'month'); } this.updateCalendars(); }, @@ -496,15 +744,14 @@ clickNext: function (e) { var cal = $(e.target).parents('.calendar'); if (cal.hasClass('left')) { - this.leftCalendar.month.add('month', 1); + this.leftCalendar.month.add(1, 'month'); } else { - this.rightCalendar.month.add('month', 1); + this.rightCalendar.month.add(1, 'month'); } this.updateCalendars(); }, - enterDate: function (e) { - + hoverDate: function (e) { var title = $(e.target).attr('data-title'); var row = title.substr(1, 1); var col = title.substr(3, 1); @@ -515,7 +762,19 @@ } else { this.container.find('input[name=daterangepicker_end]').val(this.rightCalendar.calendar[row][col].format(this.format)); } + }, + setCustomDates: function(startDate, endDate) { + this.chosenLabel = this.locale.customRangeLabel; + if (startDate.isAfter(endDate)) { + var difference = this.endDate.diff(this.startDate); + endDate = moment(startDate).add(difference, 'ms'); + } + this.startDate = startDate; + this.endDate = endDate; + + this.updateView(); + this.updateCalendars(); }, clickDate: function (e) { @@ -524,19 +783,20 @@ var col = title.substr(3, 1); var cal = $(e.target).parents('.calendar'); + var startDate, endDate; if (cal.hasClass('left')) { - var startDate = this.leftCalendar.calendar[row][col]; - var endDate = this.endDate; - if (typeof this.dateLimit == 'object') { + startDate = this.leftCalendar.calendar[row][col]; + endDate = this.endDate; + if (typeof this.dateLimit === 'object') { var maxDate = moment(startDate).add(this.dateLimit).startOf('day'); if (endDate.isAfter(maxDate)) { endDate = maxDate; } } } else { - var startDate = this.startDate; - var endDate = this.rightCalendar.calendar[row][col]; - if (typeof this.dateLimit == 'object') { + startDate = this.startDate; + endDate = this.rightCalendar.calendar[row][col]; + if (typeof this.dateLimit === 'object') { var minDate = moment(endDate).subtract(this.dateLimit).startOf('day'); if (startDate.isBefore(minDate)) { startDate = minDate; @@ -544,72 +804,72 @@ } } - cal.find('td').removeClass('active'); - - if (startDate.isSame(endDate) || startDate.isBefore(endDate)) { - $(e.target).addClass('active'); - this.startDate = startDate; - this.endDate = endDate; - } else if (startDate.isAfter(endDate)) { - $(e.target).addClass('active'); - this.startDate = startDate; - this.endDate = moment(startDate).add('day', 1).startOf('day'); + if (this.singleDatePicker && cal.hasClass('left')) { + endDate = startDate.clone(); + } else if (this.singleDatePicker && cal.hasClass('right')) { + startDate = endDate.clone(); } - this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()); - this.rightCalendar.month.month(this.endDate.month()).year(this.endDate.year()); - this.updateCalendars(); + cal.find('td').removeClass('active'); + + $(e.target).addClass('active'); + + this.setCustomDates(startDate, endDate); + + if (!this.timePicker) + endDate.endOf('day'); + + if (this.singleDatePicker && !this.timePicker) + this.clickApply(); }, clickApply: function (e) { this.updateInputText(); this.hide(); + this.element.trigger('apply.daterangepicker', this); }, clickCancel: function (e) { this.startDate = this.oldStartDate; this.endDate = this.oldEndDate; + this.chosenLabel = this.oldChosenLabel; this.updateView(); this.updateCalendars(); this.hide(); + this.element.trigger('cancel.daterangepicker', this); }, updateMonthYear: function (e) { - - var isLeft = $(e.target).closest('.calendar').hasClass('left'); - var cal = this.container.find('.calendar.left'); - if (!isLeft) - cal = this.container.find('.calendar.right'); + var isLeft = $(e.target).closest('.calendar').hasClass('left'), + leftOrRight = isLeft ? 'left' : 'right', + cal = this.container.find('.calendar.'+leftOrRight); // Month must be Number for new moment versions var month = parseInt(cal.find('.monthselect').val(), 10); var year = cal.find('.yearselect').val(); - if (isLeft) { - this.leftCalendar.month.month(month).year(year); - } else { - this.rightCalendar.month.month(month).year(year); - } - + this[leftOrRight+'Calendar'].month.month(month).year(year); this.updateCalendars(); - }, updateTime: function(e) { - var isLeft = $(e.target).closest('.calendar').hasClass('left'); - var cal = this.container.find('.calendar.left'); - if (!isLeft) - cal = this.container.find('.calendar.right'); + var cal = $(e.target).closest('.calendar'), + isLeft = cal.hasClass('left'); - var hour = parseInt(cal.find('.hourselect').val()); - var minute = parseInt(cal.find('.minuteselect').val()); + var hour = parseInt(cal.find('.hourselect').val(), 10); + var minute = parseInt(cal.find('.minuteselect').val(), 10); + var second = 0; + + if (this.timePickerSeconds) { + second = parseInt(cal.find('.secondselect').val(), 10); + } if (this.timePicker12Hour) { var ampm = cal.find('.ampmselect').val(); - if (ampm == 'PM' && hour < 12) + if (ampm === 'PM' && hour < 12) hour += 12; - if (ampm == 'AM' && hour == 12) + if (ampm === 'AM' && hour === 12) hour = 0; } @@ -617,25 +877,31 @@ var start = this.startDate.clone(); start.hour(hour); start.minute(minute); + start.second(second); this.startDate = start; - this.leftCalendar.month.hour(hour).minute(minute); + this.leftCalendar.month.hour(hour).minute(minute).second(second); + if (this.singleDatePicker) + this.endDate = start.clone(); } else { var end = this.endDate.clone(); end.hour(hour); end.minute(minute); + end.second(second); this.endDate = end; - this.rightCalendar.month.hour(hour).minute(minute); + if (this.singleDatePicker) + this.startDate = end.clone(); + this.rightCalendar.month.hour(hour).minute(minute).second(second); } + this.updateView(); this.updateCalendars(); - }, updateCalendars: function () { - this.leftCalendar.calendar = this.buildCalendar(this.leftCalendar.month.month(), this.leftCalendar.month.year(), this.leftCalendar.month.hour(), this.leftCalendar.month.minute(), 'left'); - this.rightCalendar.calendar = this.buildCalendar(this.rightCalendar.month.month(), this.rightCalendar.month.year(), this.rightCalendar.month.hour(), this.rightCalendar.month.minute(), 'right'); - this.container.find('.calendar.left').html(this.renderCalendar(this.leftCalendar.calendar, this.startDate, this.minDate, this.maxDate)); - this.container.find('.calendar.right').html(this.renderCalendar(this.rightCalendar.calendar, this.endDate, this.startDate, this.maxDate)); + this.leftCalendar.calendar = this.buildCalendar(this.leftCalendar.month.month(), this.leftCalendar.month.year(), this.leftCalendar.month.hour(), this.leftCalendar.month.minute(), this.leftCalendar.month.second(), 'left'); + this.rightCalendar.calendar = this.buildCalendar(this.rightCalendar.month.month(), this.rightCalendar.month.year(), this.rightCalendar.month.hour(), this.rightCalendar.month.minute(), this.rightCalendar.month.second(), 'right'); + this.container.find('.calendar.left').empty().html(this.renderCalendar(this.leftCalendar.calendar, this.startDate, this.minDate, this.maxDate, 'left')); + this.container.find('.calendar.right').empty().html(this.renderCalendar(this.rightCalendar.calendar, this.endDate, this.singleDatePicker ? this.minDate : this.startDate, this.maxDate, 'right')); this.container.find('.ranges li').removeClass('active'); var customRange = true; @@ -644,34 +910,44 @@ if (this.timePicker) { if (this.startDate.isSame(this.ranges[range][0]) && this.endDate.isSame(this.ranges[range][1])) { customRange = false; - this.container.find('.ranges li:eq(' + i + ')').addClass('active'); + this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')') + .addClass('active').html(); } } else { //ignore times when comparing dates if time picker is not enabled if (this.startDate.format('YYYY-MM-DD') == this.ranges[range][0].format('YYYY-MM-DD') && this.endDate.format('YYYY-MM-DD') == this.ranges[range][1].format('YYYY-MM-DD')) { customRange = false; - this.container.find('.ranges li:eq(' + i + ')').addClass('active'); + this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')') + .addClass('active').html(); } } i++; } - if (customRange) - this.container.find('.ranges li:last').addClass('active'); + if (customRange) { + this.chosenLabel = this.container.find('.ranges li:last').addClass('active').html(); + this.showCalendars(); + } }, - buildCalendar: function (month, year, hour, minute, side) { - + buildCalendar: function (month, year, hour, minute, second, side) { + var daysInMonth = moment([year, month]).daysInMonth(); var firstDay = moment([year, month, 1]); - var lastMonth = moment(firstDay).subtract('month', 1).month(); - var lastYear = moment(firstDay).subtract('month', 1).year(); + var lastDay = moment([year, month, daysInMonth]); + var lastMonth = moment(firstDay).subtract(1, 'month').month(); + var lastYear = moment(firstDay).subtract(1, 'month').year(); var daysInLastMonth = moment([lastYear, lastMonth]).daysInMonth(); var dayOfWeek = firstDay.day(); + var i; + //initialize a 6 rows x 7 columns array for the calendar var calendar = []; - for (var i = 0; i < 6; i++) { + calendar.firstDay = firstDay; + calendar.lastDay = lastDay; + + for (i = 0; i < 6; i++) { calendar[i] = []; } @@ -683,25 +959,39 @@ if (dayOfWeek == this.locale.firstDay) startDay = daysInLastMonth - 6; - var curDate = moment([lastYear, lastMonth, startDay, 12, minute]); - for (var i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = moment(curDate).add('hour', 24)) { - if (i > 0 && col % 7 == 0) { + var curDate = moment([lastYear, lastMonth, startDay, 12, minute, second]).zone(this.timeZone); + + var col, row; + for (i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = moment(curDate).add(24, 'hour')) { + if (i > 0 && col % 7 === 0) { col = 0; row++; } calendar[row][col] = curDate.clone().hour(hour); curDate.hour(12); + + if (this.minDate && calendar[row][col].format('YYYY-MM-DD') == this.minDate.format('YYYY-MM-DD') && calendar[row][col].isBefore(this.minDate) && side == 'left') { + calendar[row][col] = this.minDate.clone(); + } + + if (this.maxDate && calendar[row][col].format('YYYY-MM-DD') == this.maxDate.format('YYYY-MM-DD') && calendar[row][col].isAfter(this.maxDate) && side == 'right') { + calendar[row][col] = this.maxDate.clone(); + } + } return calendar; - }, renderDropdowns: function (selected, minDate, maxDate) { var currentMonth = selected.month(); + var currentYear = selected.year(); + var maxYear = (maxDate && maxDate.year()) || (currentYear + 5); + var minYear = (minDate && minDate.year()) || (currentYear - 50); + var monthHtml = '"; - var currentYear = selected.year(); - var maxYear = (maxDate && maxDate.year()) || (currentYear + 5); - var minYear = (minDate && minDate.year()) || (currentYear - 50); var yearHtml = ''; + + // Disallow selections before the minDate or after the maxDate + var min_hour = 0; + var max_hour = 23; + + if (minDate && (side == 'left' || this.singleDatePicker) && selected.format('YYYY-MM-DD') == minDate.format('YYYY-MM-DD')) { + min_hour = minDate.hour(); + if (selected.hour() < min_hour) + selected.hour(min_hour); + if (this.timePicker12Hour && min_hour >= 12 && selected.hour() >= 12) + min_hour -= 12; + if (this.timePicker12Hour && min_hour == 12) + min_hour = 1; + } + + if (maxDate && (side == 'right' || this.singleDatePicker) && selected.format('YYYY-MM-DD') == maxDate.format('YYYY-MM-DD')) { + max_hour = maxDate.hour(); + if (selected.hour() > max_hour) + selected.hour(max_hour); + if (this.timePicker12Hour && max_hour >= 12 && selected.hour() >= 12) + max_hour -= 12; + } + var start = 0; var end = 23; var selected_hour = selected.hour(); @@ -822,13 +1133,16 @@ end = 12; if (selected_hour >= 12) selected_hour -= 12; - if (selected_hour == 0) + if (selected_hour === 0) selected_hour = 12; } - for (var i = start; i <= end; i++) { + for (i = start; i <= end; i++) { + if (i == selected_hour) { html += ''; + } else if (i < min_hour || i > max_hour) { + html += ''; } else { html += ''; } @@ -838,12 +1152,30 @@ html += ' '; + if (this.timePickerSeconds) { + html += ': '; + } + if (this.timePicker12Hour) { html += ''; } @@ -867,6 +1229,14 @@ return html; + }, + + remove: function() { + + this.container.remove(); + this.element.off('.daterangepicker'); + this.element.removeData('daterangepicker'); + } }; @@ -874,10 +1244,11 @@ $.fn.daterangepicker = function (options, cb) { this.each(function () { var el = $(this); - if (!el.data('daterangepicker')) - el.data('daterangepicker', new DateRangePicker(el, options, cb)); + if (el.data('daterangepicker')) + el.data('daterangepicker').remove(); + el.data('daterangepicker', new DateRangePicker(el, options, cb)); }); return this; }; -}(window.jQuery); +}));