diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index de3de84e7e492b626cd2af104047286cf76fb6d1..386a5695f4f8802a2d0d1f8710b35d6863f85677 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -14,7 +14,6 @@ // /*= require jquery2 */ /*= require jquery-ui/autocomplete */ -/*= require jquery-ui/datepicker */ /*= require jquery-ui/draggable */ /*= require jquery-ui/effect-highlight */ /*= require jquery-ui/sortable */ @@ -26,6 +25,7 @@ /*= require jquery.scrollTo */ /*= require jquery.turbolinks */ /*= require jquery.tablesorter */ +/*= require pikaday */ /*= require js.cookie */ /*= require turbolinks */ /*= require autosave */ diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js.es6 index 7e192e90fe64ab430662cf6b3915060a0067f96c..ac2966cef5d69a2d19eb1917e4ec2819ab9f8110 100644 --- a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 +++ b/app/assets/javascripts/boards/filters/due_date_filters.js.es6 @@ -1,6 +1,7 @@ /* global Vue */ +/* global dateFormat */ Vue.filter('due-date', (value) => { const date = new Date(value); - return $.datepicker.formatDate('M d, yy', date); + return dateFormat(date, 'mmm d, yyyy'); }); diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index f8b22a2ce46fe069c584ac080e32ee875e5d4697..a67e67b367fc60ebf166ae0e751501cee98d05c2 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -110,6 +110,7 @@ break; case 'projects:milestones:new': case 'projects:milestones:edit': + case 'projects:milestones:update': new ZenMode(); new gl.DueDateSelectors(); new GLForm($('.milestone-form')); diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6 index d81d4cf84255d1c7e5f1e0c7a9532b8360674c7d..5ce76901734b9c39259a9679e9b4e5436233a880 100644 --- a/app/assets/javascripts/due_date_select.js.es6 +++ b/app/assets/javascripts/due_date_select.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */ +/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, prefer-const, padded-blocks, no-unused-vars, no-underscore-dangle, no-new, max-len, semi, no-sequences, no-unused-expressions, no-param-reassign */ +/* global dateFormat */ +/* global Pikaday */ (function(global) { class DueDateSelect { @@ -25,11 +27,14 @@ this.initGlDropdown(); this.initRemoveDueDate(); this.initDatePicker(); - this.initStopPropagation(); } initGlDropdown() { this.$dropdown.glDropdown({ + opened: () => { + const calendar = this.$datePicker.data('pikaday'); + calendar.show(); + }, hidden: () => { this.$selectbox.hide(); this.$value.css('display', ''); @@ -38,25 +43,39 @@ } initDatePicker() { - this.$datePicker.datepicker({ - dateFormat: 'yy-mm-dd', - defaultDate: $("input[name='" + this.fieldName + "']").val(), - altField: "input[name='" + this.fieldName + "']", - onSelect: () => { + const $dueDateInput = $(`input[name='${this.fieldName}']`); + + const calendar = new Pikaday({ + field: $dueDateInput.get(0), + theme: 'gitlab-theme', + format: 'yyyy-mm-dd', + onSelect: (dateText) => { + const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd'); + + $dueDateInput.val(formattedDate); + if (this.$dropdown.hasClass('js-issue-boards-due-date')) { - gl.issueBoards.BoardsStore.detail.issue.dueDate = $(`input[name='${this.fieldName}']`).val(); + gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val(); this.updateIssueBoardIssue(); } else { - return this.saveDueDate(true); + this.saveDueDate(true); } } }); + + calendar.setDate(new Date($dueDateInput.val())); + + this.$datePicker.append(calendar.el); + this.$datePicker.data('pikaday', calendar); } initRemoveDueDate() { this.$block.on('click', '.js-remove-due-date', (e) => { + const calendar = this.$datePicker.data('pikaday'); e.preventDefault(); + calendar.setDate(null); + if (this.$dropdown.hasClass('js-issue-boards-due-date')) { gl.issueBoards.BoardsStore.detail.issue.dueDate = ''; this.updateIssueBoardIssue(); @@ -67,12 +86,6 @@ }); } - initStopPropagation() { - $(document).off('click', '.ui-datepicker-header a').on('click', '.ui-datepicker-header a', (e) => { - return e.stopImmediatePropagation(); - }); - } - saveDueDate(isDropdown) { this.parseSelectedDate(); this.prepSelectedDate(); @@ -86,7 +99,7 @@ // Construct Date object manually to avoid buggy dateString support within Date constructor const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10)); const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]); - this.displayedDate = $.datepicker.formatDate('M d, yy', dateObj); + this.displayedDate = dateFormat(dateObj, 'mmm d, yyyy'); } else { this.displayedDate = 'No due date'; } @@ -153,14 +166,25 @@ } initMilestoneDatePicker() { - $('.datepicker').datepicker({ - dateFormat: 'yy-mm-dd' + $('.datepicker').each(function() { + const $datePicker = $(this); + const calendar = new Pikaday({ + field: $datePicker.get(0), + theme: 'gitlab-theme', + format: 'yyyy-mm-dd', + onSelect(dateText) { + $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); + } + }); + calendar.setDate(new Date($datePicker.val())); + + $datePicker.data('pikaday', calendar); }); $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => { e.preventDefault(); - const datepicker = $(e.target).siblings('.datepicker'); - $.datepicker._clearDate(datepicker); + const calendar = $(e.target).siblings('.datepicker').data('pikaday'); + calendar.setDate(null); }); } diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index cc1c0877cdf4851f7295e6f4ef98ea142a550ef3..299d6faf64ce8cc0df78e0867bc9732858d40afd 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -438,7 +438,7 @@ } }; - GitLabDropdown.prototype.opened = function() { + GitLabDropdown.prototype.opened = function(e) { var contentHtml; this.resetRows(); this.addArrowKeyEvent(); @@ -458,6 +458,10 @@ this.positionMenuAbove(); } + if (this.options.opened) { + this.options.opened.call(this, e); + } + return this.dropdown.trigger('shown.gl.dropdown'); }; diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 3634bd8666c38c246d22d56fae2755f363cdb03e..0a783dd9c1886e7338b70529612ecade7e724805 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -4,6 +4,8 @@ /* global ZenMode */ /* global Autosave */ /* global GroupsSelect */ +/* global dateFormat */ +/* global Pikaday */ (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; @@ -14,7 +16,7 @@ IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i; function IssuableForm(form) { - var $issuableDueDate; + var $issuableDueDate, calendar; this.form = form; this.toggleWip = bind(this.toggleWip, this); this.renderWipExplanation = bind(this.renderWipExplanation, this); @@ -37,12 +39,15 @@ this.initMoveDropdown(); $issuableDueDate = $('#issuable-due-date'); if ($issuableDueDate.length) { - $('.datepicker').datepicker({ - dateFormat: 'yy-mm-dd', - onSelect: function(dateText, inst) { - return $issuableDueDate.val(dateText); + calendar = new Pikaday({ + field: $issuableDueDate.get(0), + theme: 'gitlab-theme', + format: 'yyyy-mm-dd', + onSelect: function(dateText) { + $issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); } - }).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $issuableDueDate.val())); + }); + calendar.setDate(new Date($issuableDueDate.val())); } } diff --git a/app/assets/javascripts/member_expiration_date.js.es6 b/app/assets/javascripts/member_expiration_date.js.es6 index bf6c0ec27982d962f0ed6a28b61c98a00415d422..8b045dfc1345429644295d1f6b8899c3728805c6 100644 --- a/app/assets/javascripts/member_expiration_date.js.es6 +++ b/app/assets/javascripts/member_expiration_date.js.es6 @@ -1,3 +1,5 @@ +/* global Pikaday */ +/* global dateFormat */ (() => { // Add datepickers to all `js-access-expiration-date` elements. If those elements are // children of an element with the `clearable-input` class, and have a sibling @@ -11,21 +13,36 @@ } const inputs = $(selector); - inputs.datepicker({ - dateFormat: 'yy-mm-dd', - minDate: 1, - onSelect: function onSelect() { - $(this).trigger('change'); - toggleClearInput.call(this); - }, + inputs.each((i, el) => { + const $input = $(el); + + const calendar = new Pikaday({ + field: $input.get(0), + theme: 'gitlab-theme', + format: 'yyyy-mm-dd', + minDate: new Date(), + onSelect(dateText) { + $input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); + + $input.trigger('change'); + + toggleClearInput.call($input); + }, + }); + + calendar.setDate(new Date($input.val())); + + $input.data('pikaday', calendar); }); inputs.next('.js-clear-input').on('click', function clicked(event) { event.preventDefault(); const input = $(this).closest('.clearable-input').find(selector); - input.datepicker('setDate', null) - .trigger('change'); + const calendar = input.data('pikaday'); + + calendar.setDate(null); + input.trigger('change'); toggleClearInput.call(input); }); diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 8b93665d085eb8a5e7360588fe633d9b21d659eb..2eb7ff915a99fb884669149efe44cedd3397a6c7 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -2,13 +2,13 @@ * This is a manifest file that'll automatically include all the stylesheets available in this directory * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at * the top of the compiled file, but it's generally better to create a new file per style scope. - *= require jquery-ui/datepicker *= require jquery-ui/autocomplete *= require jquery.atwho *= require select2 *= require_self *= require dropzone/basic *= require cropper.css + *= require pikaday */ /* diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index ef921a8c6a94953c5b01eb65d323b56993bf6443..12f03953ca3f20ea97f465335b1779ddc48a3647 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -42,3 +42,56 @@ float: right; font-size: 12px; } + +.pika-single.gitlab-theme { + .pika-label { + color: $gl-text-color-secondary; + font-size: 14px; + font-weight: normal; + } + + th { + padding: 2px 0; + color: $note-disabled-comment-color; + font-weight: normal; + text-transform: lowercase; + border-top: 1px solid $calendar-border-color; + } + + abbr { + cursor: default; + } + + td { + border: 1px solid $calendar-border-color; + + &:first-child { + border-left: 0; + } + + &:last-child { + border-right: 0; + } + } + + .pika-day { + border-radius: 0; + background-color: $white-light; + text-align: center; + } + + .is-today { + .pika-day { + color: inherit; + font-weight: normal; + } + } + + .is-selected .pika-day, + .pika-day:hover, + .is-today .pika-day:hover { + background: $gl-primary; + color: $white-light; + box-shadow: none; + } +} diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 6ed5f8b2ddaac6c847d7c75f5a376715155a13e3..30b59fdf3cd5030bcd4dc0feb3b944ccf2d17784 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -501,119 +501,16 @@ max-height: 230px; } - .ui-widget { - table { - margin: 0; - } - - &.ui-datepicker-inline { - padding: 0 10px; - border: 0; - width: 100%; - } - - .ui-datepicker-header { - padding: 0 8px 10px; - border: 0; - - .ui-icon { - background: none; - font-size: 20px; - text-indent: 0; - - &::before { - display: block; - position: relative; - top: -2px; - color: $dropdown-title-btn-color; - font: normal normal normal 14px/1 FontAwesome; - font-size: inherit; - text-rendering: auto; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } - } - } - - .ui-datepicker-calendar { - .ui-state-hover, - .ui-state-active { - color: $white-light; - border: 0; - } - } - - .ui-datepicker-prev, - .ui-datepicker-next { - top: 0; - height: 15px; - cursor: pointer; - - &:hover { - background-color: transparent; - border: 0; - - .ui-icon::before { - color: $md-link-color; - } - } - } - - .ui-datepicker-prev { - left: 0; - - .ui-icon::before { - content: '\f104'; - text-align: left; - } - } - - .ui-datepicker-next { - right: 0; - - .ui-icon::before { - content: '\f105'; - text-align: right; - } - } - - td { - padding: 0; - border: 1px solid $calendar-border-color; - - &:first-child { - border-left: 0; - } - - &:last-child { - border-right: 0; - } - - a { - line-height: 17px; - border: 0; - border-radius: 0; - } - } - - .ui-datepicker-title { - color: $gl-text-color; - font-size: 14px; - line-height: 1; - font-weight: normal; - } - } - - th { - padding: 2px 0; - color: $note-disabled-comment-color; - font-weight: normal; - text-transform: lowercase; - border-top: 1px solid $calendar-border-color; + .pika-single { + position: relative!important; + top: 0!important; + border: 0; + box-shadow: none; } - .ui-datepicker-unselectable { - background-color: $gray-light; + .pika-lendar { + margin-top: -5px; + margin-bottom: 0; } } diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss index 18f2f316f02124b3eb2a95466cf6316943151605..317f9ff3c188bb0ce58e17d261abaa3507cfdbc7 100644 --- a/app/assets/stylesheets/framework/jquery.scss +++ b/app/assets/stylesheets/framework/jquery.scss @@ -2,42 +2,6 @@ font-family: $regular_font; font-size: $font-size-base; - &.ui-datepicker, - &.ui-datepicker-inline { - border: 1px solid $jq-ui-border; - padding: 10px; - width: 270px; - - .ui-datepicker-header { - background: $white-light; - border-color: $jq-ui-border; - - .ui-datepicker-prev, - .ui-datepicker-next { - top: 4px; - } - - .ui-datepicker-prev { - left: 2px; - } - - .ui-datepicker-next { - right: 2px; - } - - .ui-state-hover { - background: transparent; - border: 0; - cursor: pointer; - } - } - - .ui-datepicker-calendar td a { - padding: 5px; - text-align: center; - } - } - &.ui-autocomplete { border-color: $jq-ui-border; padding: 0; @@ -59,16 +23,6 @@ border: 0; background: transparent; } - - .ui-datepicker-calendar { - .ui-state-active, - .ui-state-hover, - .ui-state-focus { - border: 1px solid $gl-primary; - background: $gl-primary; - color: $white-light; - } - } } .ui-sortable-handle { diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 722b3006f7c870b12e62e32228aa766dd02b4c42..8031c4467a47db0088728c4322f97e4e4ea7a370 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -201,10 +201,6 @@ color: $note-disabled-comment-color; } -.datepicker.personal-access-tokens-expires-at .ui-state-disabled span { - text-align: center; -} - .created-personal-access-token-container { #created-personal-access-token { width: 90%; diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index 60a561c9f9c1483d48414087780d01d80b9626e3..b10f5fc08e2bdda5cc0e5c0631a310815e78b62e 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -85,11 +85,17 @@ :javascript - var date = $('#personal_access_token_expires_at').val(); - - var datepicker = $(".datepicker").datepicker({ - dateFormat: "yy-mm-dd", - minDate: 0 + var $dateField = $('#personal_access_token_expires_at'); + var date = $dateField.val(); + + new Pikaday({ + field: $dateField.get(0), + theme: 'gitlab-theme', + format: 'yyyy-mm-dd', + minDate: new Date(), + onSelect: function(dateText) { + $dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); + } }); $("#created-personal-access-token").click(function() { diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml index 748b10a1298742905ec7989ca3240ce413d4d942..ed94773ef89f4978cb32f913656c5cdd538f65c5 100644 --- a/app/views/shared/milestones/_form_dates.html.haml +++ b/app/views/shared/milestones/_form_dates.html.haml @@ -10,6 +10,3 @@ .col-sm-10 = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date" %a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date - -:javascript - new gl.DueDateSelectors(); diff --git a/changelogs/unreleased/remove-jquery-ui-datepicker.yml b/changelogs/unreleased/remove-jquery-ui-datepicker.yml new file mode 100644 index 0000000000000000000000000000000000000000..cd00690d774275caa22a4de363204718b0700a28 --- /dev/null +++ b/changelogs/unreleased/remove-jquery-ui-datepicker.yml @@ -0,0 +1,4 @@ +--- +title: Replaced jQuery UI datepicker +merge_request: +author: diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 188d33e8ef4338e1127cb6df1f627dd6553af5c0..bf1046620dca5c1ab4a4def0197fe2c731c2f37a 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -194,7 +194,7 @@ page.within('.due_date') do click_link 'Edit' - click_link Date.today.day + click_button Date.today.day wait_for_vue_resource diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 21e70b3141af3a24888057e67eac8a66b1e44732..3da68b723bfffa73c8a86dcbdaf22c373d19f0d4 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -78,8 +78,8 @@ fill_in 'issue_description', with: 'bug description' find('#issuable-due-date').click - page.within '.ui-datepicker' do - click_link date.day + page.within '.pika-single' do + click_button date.day end expect(find('#issuable-due-date').value).to eq date.to_s @@ -110,8 +110,8 @@ fill_in 'issue_description', with: 'bug description' find('#issuable-due-date').click - page.within '.ui-datepicker' do - click_link date.day + page.within '.pika-single' do + click_button date.day end expect(find('#issuable-due-date').value).to eq date.to_s @@ -645,8 +645,8 @@ page.within '.due_date' do click_link 'Edit' - page.within '.ui-datepicker-calendar' do - click_link date.day + page.within '.pika-single' do + click_button date.day end wait_for_ajax @@ -656,11 +656,13 @@ end it 'removes due date from issue' do + date = Date.today.at_beginning_of_month + 2.days + page.within '.due_date' do click_link 'Edit' - page.within '.ui-datepicker-calendar' do - first('.ui-state-default').click + page.within '.pika-single' do + click_button date.day end wait_for_ajax diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb index 55a01057c83b25b1d7acea05b2033e3d3563f955..eb7b8a246695dc03d942a7bbeaeef5628894556e 100644 --- a/spec/features/profiles/personal_access_tokens_spec.rb +++ b/spec/features/profiles/personal_access_tokens_spec.rb @@ -34,7 +34,7 @@ def disallow_personal_access_token_saves! # Set date to 1st of next month find_field("Expires at").trigger('focus') - find("a[title='Next']").click + find(".pika-next").click click_on "1" # Scopes diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb index f136d9ce0fa20f60973c085a0699b4b3387d385d..c3f45be6e4b9f70664cf841f6fa851237f295d2a 100644 --- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb +++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb @@ -14,15 +14,16 @@ login_as(master) end - scenario 'expiration date is displayed in the members list', js: true do + scenario 'expiration date is displayed in the members list' do travel_to Time.zone.parse('2016-08-06 08:00') do - visit namespace_project_settings_members_path(project.namespace, project) + date = 4.days.from_now + visit namespace_project_project_members_path(project.namespace, project) + page.within '.users-project-form' do select2(new_member.id, from: '#user_ids', multiple: true) - fill_in 'expires_at', with: '2016-08-10' + fill_in 'expires_at', with: date.to_s(:medium) + click_on 'Add to project' end - find('.users-project-form').click - click_on 'Add to project' page.within "#project_member_#{new_member.project_members.first.id}" do expect(page).to have_content('Expires in 4 days') @@ -32,11 +33,12 @@ scenario 'change expiration date' do travel_to Time.zone.parse('2016-08-06 08:00') do - project.team.add_users([new_member.id], :developer, expires_at: '2016-09-06') + date = 3.days.from_now + project.team.add_users([new_member.id], :developer, expires_at: Date.today.to_s(:medium)) visit namespace_project_project_members_path(project.namespace, project) page.within "#project_member_#{new_member.project_members.first.id}" do - find('.js-access-expiration-date').set '2016-08-09' + find('.js-access-expiration-date').set date.to_s(:medium) wait_for_ajax expect(page).to have_content('Expires in 3 days') end diff --git a/vendor/assets/javascripts/pikaday.js b/vendor/assets/javascripts/pikaday.js new file mode 100644 index 0000000000000000000000000000000000000000..225335627fcebcb7260ef9dee654fed27f22674e --- /dev/null +++ b/vendor/assets/javascripts/pikaday.js @@ -0,0 +1,1193 @@ +/*! + * Pikaday + * + * Copyright © 2014 David Bushell | BSD & MIT license | https://github.com/dbushell/Pikaday + */ + +(function (root, factory) +{ + 'use strict'; + + var moment; + if (typeof exports === 'object') { + // CommonJS module + // Load moment.js as an optional dependency + try { moment = require('moment'); } catch (e) {} + module.exports = factory(moment); + } else if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(function (req) + { + // Load moment.js as an optional dependency + var id = 'moment'; + try { moment = req(id); } catch (e) {} + return factory(moment); + }); + } else { + root.Pikaday = factory(root.moment); + } +}(this, function (moment) +{ + 'use strict'; + + /** + * feature detection and helper functions + */ + var hasMoment = typeof moment === 'function', + + hasEventListeners = !!window.addEventListener, + + document = window.document, + + sto = window.setTimeout, + + addEvent = function(el, e, callback, capture) + { + if (hasEventListeners) { + el.addEventListener(e, callback, !!capture); + } else { + el.attachEvent('on' + e, callback); + } + }, + + removeEvent = function(el, e, callback, capture) + { + if (hasEventListeners) { + el.removeEventListener(e, callback, !!capture); + } else { + el.detachEvent('on' + e, callback); + } + }, + + fireEvent = function(el, eventName, data) + { + var ev; + + if (document.createEvent) { + ev = document.createEvent('HTMLEvents'); + ev.initEvent(eventName, true, false); + ev = extend(ev, data); + el.dispatchEvent(ev); + } else if (document.createEventObject) { + ev = document.createEventObject(); + ev = extend(ev, data); + el.fireEvent('on' + eventName, ev); + } + }, + + trim = function(str) + { + return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g,''); + }, + + hasClass = function(el, cn) + { + return (' ' + el.className + ' ').indexOf(' ' + cn + ' ') !== -1; + }, + + addClass = function(el, cn) + { + if (!hasClass(el, cn)) { + el.className = (el.className === '') ? cn : el.className + ' ' + cn; + } + }, + + removeClass = function(el, cn) + { + el.className = trim((' ' + el.className + ' ').replace(' ' + cn + ' ', ' ')); + }, + + isArray = function(obj) + { + return (/Array/).test(Object.prototype.toString.call(obj)); + }, + + isDate = function(obj) + { + return (/Date/).test(Object.prototype.toString.call(obj)) && !isNaN(obj.getTime()); + }, + + isWeekend = function(date) + { + var day = date.getDay(); + return day === 0 || day === 6; + }, + + isLeapYear = function(year) + { + // solution by Matti Virkkunen: http://stackoverflow.com/a/4881951 + return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0; + }, + + getDaysInMonth = function(year, month) + { + return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; + }, + + setToStartOfDay = function(date) + { + if (isDate(date)) date.setHours(0,0,0,0); + }, + + compareDates = function(a,b) + { + // weak date comparison (use setToStartOfDay(date) to ensure correct result) + return a.getTime() === b.getTime(); + }, + + extend = function(to, from, overwrite) + { + var prop, hasProp; + for (prop in from) { + hasProp = to[prop] !== undefined; + if (hasProp && typeof from[prop] === 'object' && from[prop] !== null && from[prop].nodeName === undefined) { + if (isDate(from[prop])) { + if (overwrite) { + to[prop] = new Date(from[prop].getTime()); + } + } + else if (isArray(from[prop])) { + if (overwrite) { + to[prop] = from[prop].slice(0); + } + } else { + to[prop] = extend({}, from[prop], overwrite); + } + } else if (overwrite || !hasProp) { + to[prop] = from[prop]; + } + } + return to; + }, + + adjustCalendar = function(calendar) { + if (calendar.month < 0) { + calendar.year -= Math.ceil(Math.abs(calendar.month)/12); + calendar.month += 12; + } + if (calendar.month > 11) { + calendar.year += Math.floor(Math.abs(calendar.month)/12); + calendar.month -= 12; + } + return calendar; + }, + + /** + * defaults and localisation + */ + defaults = { + + // bind the picker to a form field + field: null, + + // automatically show/hide the picker on `field` focus (default `true` if `field` is set) + bound: undefined, + + // position of the datepicker, relative to the field (default to bottom & left) + // ('bottom' & 'left' keywords are not used, 'top' & 'right' are modifier on the bottom/left position) + position: 'bottom left', + + // automatically fit in the viewport even if it means repositioning from the position option + reposition: true, + + // the default output format for `.toString()` and `field` value + format: 'YYYY-MM-DD', + + // the initial date to view when first opened + defaultDate: null, + + // make the `defaultDate` the initial selected value + setDefaultDate: false, + + // first day of week (0: Sunday, 1: Monday etc) + firstDay: 0, + + // the default flag for moment's strict date parsing + formatStrict: false, + + // the minimum/earliest date that can be selected + minDate: null, + // the maximum/latest date that can be selected + maxDate: null, + + // number of years either side, or array of upper/lower range + yearRange: 10, + + // show week numbers at head of row + showWeekNumber: false, + + // used internally (don't config outside) + minYear: 0, + maxYear: 9999, + minMonth: undefined, + maxMonth: undefined, + + startRange: null, + endRange: null, + + isRTL: false, + + // Additional text to append to the year in the calendar title + yearSuffix: '', + + // Render the month after year in the calendar title + showMonthAfterYear: false, + + // Render days of the calendar grid that fall in the next or previous month + showDaysInNextAndPreviousMonths: false, + + // how many months are visible + numberOfMonths: 1, + + // when numberOfMonths is used, this will help you to choose where the main calendar will be (default `left`, can be set to `right`) + // only used for the first display or when a selected date is not visible + mainCalendar: 'left', + + // Specify a DOM element to render the calendar in + container: undefined, + + // internationalization + i18n: { + previousMonth : 'Previous Month', + nextMonth : 'Next Month', + months : ['January','February','March','April','May','June','July','August','September','October','November','December'], + weekdays : ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'], + weekdaysShort : ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'] + }, + + // Theme Classname + theme: null, + + // callback function + onSelect: null, + onOpen: null, + onClose: null, + onDraw: null + }, + + + /** + * templating functions to abstract HTML rendering + */ + renderDayName = function(opts, day, abbr) + { + day += opts.firstDay; + while (day >= 7) { + day -= 7; + } + return abbr ? opts.i18n.weekdaysShort[day] : opts.i18n.weekdays[day]; + }, + + renderDay = function(opts) + { + var arr = []; + var ariaSelected = 'false'; + if (opts.isEmpty) { + if (opts.showDaysInNextAndPreviousMonths) { + arr.push('is-outside-current-month'); + } else { + return '