diff --git a/account.php b/account.php index 2f2914a4a2893f439e16f4a1ad95fa437d16c609..a272c6f3d9e45ec52f5f2ccca1c1f2384dcb4ca1 100644 --- a/account.php +++ b/account.php @@ -57,6 +57,7 @@ elseif ($_POST) { if ($thisclient) { $user_form->getField('email')->configure('disabled', true); $user_form->getField('email')->value = $thisclient->getEmail(); + $_POST['email'] = $thisclient->getEmail(); } if (!$user_form->isValid(function($f) { return !$f->isVisibleToUsers(); })) diff --git a/css/jquery-ui-timepicker-addon.css b/css/jquery-ui-timepicker-addon.css new file mode 100644 index 0000000000000000000000000000000000000000..586a7f04d6e541bfff7c3b1efb8387025096d5cf --- /dev/null +++ b/css/jquery-ui-timepicker-addon.css @@ -0,0 +1,30 @@ +.ui-timepicker-div .ui-widget-header { margin-bottom: 8px; } +.ui-timepicker-div dl { text-align: left; } +.ui-timepicker-div dl dt { float: left; clear:left; padding: 0 0 0 5px; } +.ui-timepicker-div dl dd { margin: 0 10px 10px 40%; } +.ui-timepicker-div td { font-size: 90%; } +.ui-tpicker-grid-label { background: none; border: none; margin: 0; padding: 0; } +.ui-timepicker-div .ui_tpicker_unit_hide{ display: none; } + +.ui-timepicker-div .ui_tpicker_time .ui_tpicker_time_input { background: none; color: inherit; border: none; outline: none; border-bottom: solid 1px #555; width: 95%; } +.ui-timepicker-div .ui_tpicker_time .ui_tpicker_time_input:focus { border-bottom-color: #aaa; } + +.ui-timepicker-rtl{ direction: rtl; } +.ui-timepicker-rtl dl { text-align: right; padding: 0 5px 0 0; } +.ui-timepicker-rtl dl dt{ float: right; clear: right; } +.ui-timepicker-rtl dl dd { margin: 0 40% 10px 10px; } + +/* Shortened version style */ +.ui-timepicker-div.ui-timepicker-oneLine { padding-right: 2px; } +.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_time, +.ui-timepicker-div.ui-timepicker-oneLine dt { display: none; } +.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_time_label { display: block; padding-top: 2px; } +.ui-timepicker-div.ui-timepicker-oneLine dl { text-align: right; } +.ui-timepicker-div.ui-timepicker-oneLine dl dd, +.ui-timepicker-div.ui-timepicker-oneLine dl dd > div { display:inline-block; margin:0; } +.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_minute:before, +.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_second:before { content:':'; display:inline-block; } +.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_millisec:before, +.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_microsec:before { content:'.'; display:inline-block; } +.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_unit_hide, +.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_unit_hide:before{ display: none; } \ No newline at end of file diff --git a/include/ajax.thread.php b/include/ajax.thread.php index 288634bb83c3f4a248f197b10bb4bd82482a6b47..aae1e1d7762c9e7f24fcd9e9875f34863521fb1d 100644 --- a/include/ajax.thread.php +++ b/include/ajax.thread.php @@ -222,20 +222,7 @@ class ThreadAjaxAPI extends AjaxController { Http::response(404, 'No such thread'); $errors = $info = array(); - if ($thread->updateCollaborators($_POST, $errors)) - $users = array(); - foreach ($thread->getCollaborators() as $c) - $users[] = array('id' => $c->getUserId(), - 'name' => $c->getName(), - 'email' => $c->getEmail()); - - Http::response(201, $this->json_encode(array( - 'id' => $thread->getId(), - 'users' => $users, - 'text' => sprintf('(%d)', - $thread->getNumCollaborators()) - ) - )); + $thread->updateCollaborators($_POST, $errors); if($errors && $errors['err']) $info +=array('error' => $errors['err']); diff --git a/include/class.charset.php b/include/class.charset.php index 160c26fadb59a8bc3dc10a4aba7277e2f269af21..e8357b9aa262589b9be30949398b6fb2a85e04cd 100644 --- a/include/class.charset.php +++ b/include/class.charset.php @@ -29,7 +29,8 @@ class Charset { // ks_c_5601-1987: Korean alias for cp949 case preg_match('`^ks_c_5601-1987`i', $charset): return 'cp949'; - case preg_match('`^iso-?(\S+)$`i', $charset, $match): + // Remove trailing junk from ISO charset + case preg_match('`^iso-?(\S+[^i])(-i)?$`i', $charset, $match): return "ISO-".$match[1]; // GBK superceded gb2312 and is backward compatible case preg_match('`^gb2312`i', $charset): diff --git a/include/class.dept.php b/include/class.dept.php index 102e87030e48f0debdbb40e311f29a45938768c4..8654e3e75f8203ba9af68d213c54de0a25d46e2c 100644 --- a/include/class.dept.php +++ b/include/class.dept.php @@ -326,6 +326,10 @@ implements TemplateVariable, Searchable { return $rv; } + function getNumMembersForAlerts() { + return count($this->getMembersForAlerts()); + } + function getSLAId() { return $this->sla_id; } diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php index ca46bed6994d3d4e92a616a639607058bfd1f76c..dfc59d22d9d80e02de9ac7ca9919a80e967904bf 100644 --- a/include/class.dynamic_forms.php +++ b/include/class.dynamic_forms.php @@ -1795,7 +1795,8 @@ class SelectionField extends FormField { // Add in the properties for all selected list items in sub // labeled by their field id foreach ($v as $id=>$L) { - if (!($li = DynamicListItem::lookup($id))) + if (!($li = DynamicListItem::lookup($id)) + || !$li->getListId()) continue; foreach ($li->getFilterData() as $prop=>$value) { if (!isset($data[$prop])) diff --git a/include/class.export.php b/include/class.export.php index 4ee0c0c796ed0e0d9f69349162ff0dac39817e3c..5b49b939eabc3384e5ac4fd19dd2318cfaeee829 100644 --- a/include/class.export.php +++ b/include/class.export.php @@ -424,7 +424,13 @@ class CsvResultsExporter extends ResultSetExporter { fputs($this->output, chr(0xEF) . chr(0xBB) . chr(0xBF)); fputcsv($this->output, $this->getHeaders(), $delimiter); while ($row=$this->next()) - fputcsv($this->output, $row, $delimiter); + fputcsv($this->output, array_map( + function($v){ + if (preg_match('/^[=\-+@].*/', $v)) + return "'".$v; + return $v; + }, $row), + $delimiter); fclose($this->output); } diff --git a/include/class.file.php b/include/class.file.php index 419a2820922f6405f83d236b8cc47934f566dbc5..0334d57e7d11360f7e2526c13d95d985602aa6eb 100644 --- a/include/class.file.php +++ b/include/class.file.php @@ -261,17 +261,15 @@ class AttachmentFile extends VerySimpleModel { } function download($name=false, $disposition=false, $expires=false) { - $disposition = $disposition ?: 'inline'; + $disposition = ($disposition && strcasecmp($disposition, 'inline') == 0 + && strpos($this->getType(), 'image/') !== false) + ? 'inline' : 'attachment'; $bk = $this->open(); if ($bk->sendRedirectUrl($disposition)) return; $ttl = ($expires) ? $expires - Misc::gmtime() : false; $this->makeCacheable($ttl); $type = $this->getType() ?: 'application/octet-stream'; - if (isset($_REQUEST['overridetype'])) - $type = $_REQUEST['overridetype']; - elseif (!strcasecmp($disposition, 'attachment')) - $type = 'application/octet-stream'; Http::download($name ?: $this->getName(), $type, null, $disposition); header('Content-Length: '.$this->getSize()); $this->sendData(false); diff --git a/include/class.format.php b/include/class.format.php index 7d4877b5e9a33778d8362191b33c237154f437b8..51a59b88d211281a8bb06112c2ed29543dc5c52f 100644 --- a/include/class.format.php +++ b/include/class.format.php @@ -138,7 +138,7 @@ class Format { $xpath = new DOMXPath($doc); static $eE = array('area'=>1, 'br'=>1, 'col'=>1, 'embed'=>1, 'iframe' => 1, 'hr'=>1, 'img'=>1, 'input'=>1, - 'isindex'=>1, 'param'=>1); + 'isindex'=>1, 'param'=>1, 'div'=>1); do { $done = true; $nodes = $xpath->query('//*[not(text()) and not(node())]'); @@ -493,7 +493,7 @@ class Format { function viewableImages($html, $options=array()) { $cids = $images = array(); $options +=array( - 'deposition' => 'inline'); + 'disposition' => 'inline'); return preg_replace_callback('/"cid:([\w._-]{32})"/', function($match) use ($options, $images) { if (!($file = AttachmentFile::lookup($match[1]))) @@ -684,6 +684,8 @@ class Format { $tz = $datetime->getTimezone()->getName(); if ($tz && $tz[0] == '+' || $tz[0] == '-') $tz = (int) $datetime->format('Z'); + elseif ($tz == 'Z') + $tz = 'UTC'; $timezone = new DateTimeZone(Format::timezone($tz) ?: 'UTC'); $datetime->setTimezone($timezone); } catch (Exception $ex) { diff --git a/include/class.forms.php b/include/class.forms.php index 45a3b3bf1cc852570a77d61e8a81309933a3e4b5..14bbd05beeebd19ab43205562c14a5afc1324d97 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -1444,7 +1444,8 @@ class TextboxField extends FormField { parent::validateEntry($value); $config = $this->getConfiguration(); $validators = array( - '' => null, + '' => array(array('Validator', 'is_formula'), + __('Content cannot start with the following characters: = - + @')), 'email' => array(array('Validator', 'is_valid_email'), __('Enter a valid email address')), 'phone' => array(array('Validator', 'is_phone'), @@ -1529,9 +1530,12 @@ class TextareaField extends FormField { function validateEntry($value) { parent::validateEntry($value); + if (!$value) + return; $config = $this->getConfiguration(); $validators = array( - '' => null, + '' => array(array('Validator', 'is_formula'), + __('Content cannot start with the following characters: = - + @')), 'choices' => array( function($val) { $val = str_replace('"', '', JsonDataEncoder::encode($val)); @@ -1545,12 +1549,12 @@ class TextareaField extends FormField { ), ); // Support configuration forms, as well as GUI-based form fields - $valid = $this->get('validator'); - if (!$valid) { + if (!($valid = $this->get('validator')) && isset($config['validator'])) $valid = $config['validator']; - } - if (!$value || !isset($validators[$valid])) + + if (!isset($validators[$valid])) return; + $func = $validators[$valid]; $error = $func[1]; if ($config['validator-error']) @@ -2271,21 +2275,19 @@ class DatetimeField extends FormField { if (!$value || !($datetime = Format::parseDateTime($value))) return; - // Parse value to DateTime object - $val = Format::parseDateTime($value); // Get configured min/max (if any) $min = $this->getMinDateTime(); $max = $this->getMaxDateTime(); - if (!$val) { + if (!$datetime) { $this->_errors[] = __('Enter a valid date'); - } elseif ($min and $val < $min) { + } elseif ($min and $datetime < $min) { $this->_errors[] = sprintf('%s (%s)', __('Selected date is earlier than permitted'), Format::date($min->getTimestamp(), false, false, $min->getTimezone()->getName() ?: 'UTC') ); - } elseif ($max and $val > $max) { + } elseif ($max and $datetime > $max) { $this->_errors[] = sprintf('%s (%s)', __('Selected date is later than permitted'), Format::date($max->getTimestamp(), false, false, @@ -3420,6 +3422,9 @@ class FileUploadField extends FormField { $file = array_shift($files); $file['name'] = urldecode($file['name']); + if (!$this->isValidFile($file)) + Http::response(413, 'Invalid File'); + if (!$bypass && !$this->isValidFileType($file['name'], $file['type'])) Http::response(415, 'File type is not allowed'); @@ -3446,6 +3451,9 @@ class FileUploadField extends FormField { if (!$this->isValidFileType($file['name'], $file['type'])) throw new FileUploadError(__('File type is not allowed')); + if (!$this->isValidFile($file)) + throw new FileUploadError(__('Invalid File')); + $config = $this->getConfiguration(); if ($file['size'] > $config['size']) throw new FileUploadError(__('File size is too large')); @@ -3481,6 +3489,18 @@ class FileUploadField extends FormField { return $F; } + function isValidFile($file) { + + // Check invalid image hacks + if ($file['tmp_name'] + && stripos($file['type'], 'image/') === 0 + && function_exists('exif_imagetype') + && !exif_imagetype($file['tmp_name'])) + return false; + + return true; + } + function isValidFileType($name, $type=false) { $config = $this->getConfiguration(); @@ -4401,29 +4421,47 @@ class DatetimePickerWidget extends Widget { if (is_int($this->value)) // Assuming UTC timezone. $datetime = DateTime::createFromFormat('U', $this->value); - else { + else $datetime = Format::parseDateTime($this->value); - } - if ($config['time']) { + + if ($config['time']) // Convert to user's timezone for update. $datetime->setTimezone($timezone); - } + // Get the date $this->value = Format::date($datetime->getTimestamp(), false, - false, $timezone ? $timezone->getName() : 'UTC'); + false, $timezone ? $timezone->getName() : 'UTC'); + + // TODO: Fix timeformat based on config. For now we're forcing + // hh:mm tt + if ($config['time']) { + $this->value .=$datetime->format(' h:i a'); + } + } else { + // For timezone display purposes $datetime = new DateTime('now'); $datetime->setTimezone($timezone); } ?> <input type="text" name="<?php echo $this->name; ?>" id="<?php echo $this->id; ?>" style="display:inline-block;width:auto" - value="<?php echo Format::htmlchars($this->value ?: ''); ?>" size="12" + value="<?php echo $this->value; ?>" + size="<?php $config['time'] ? 20 : 12; ?>" autocomplete="off" class="dp" /> + <?php + // Timezone hint + echo sprintf(' <span class="faded">(<a href="#" + data-placement="top" data-toggle="tooltip" + title="%s">%s</a>)</span>', + $datetime->getTimezone()->getName(), + $datetime->format('T')); + ?> <script type="text/javascript"> $(function() { - $('input[name="<?php echo $this->name; ?>"]').datepicker({ + $('input[name="<?php echo $this->name; ?>"]').<?php echo + $config['time'] ? 'datetimepicker':'datepicker';?>({ <?php if ($dt=$this->field->getMinDateTime()) echo sprintf("minDate: new Date(%s),\n", $dt->format('U')*1000); @@ -4431,6 +4469,18 @@ class DatetimePickerWidget extends Widget { echo sprintf("maxDate: new Date(%s),\n", $dt->format('U')*1000); elseif (!$config['future']) echo "maxDate: new Date().getTime(),\n"; + + // Set time options + if ($config['time']) { + // Set Timezone + echo sprintf("timezone: %s,\n", + ($datetime->getOffset()/60)); + echo sprintf(" + controlType: 'select',\n + timeInput: true,\n + timeFormat: \"%s\",\n", + "hh:mm tt"); + } ?> numberOfMonths: 2, showButtonPanel: true, @@ -4441,18 +4491,6 @@ class DatetimePickerWidget extends Widget { }); </script> <?php - if ($config['time']) { - list($hr, $min) = explode(':', $datetime ? - $datetime->format('H:i') : ''); - // TODO: Add time picker -- requires time picker or selection with - // Misc::timeDropdown - echo ' ' . Misc::timeDropdown($hr, $min, $this->name . ':time'); - echo sprintf(' <span class="faded">(<a href="#" - data-placement="top" data-toggle="tooltip" - title="%s">%s</a>)</span>', - $datetime->getTimezone()->getName(), - $datetime->format('T')); - } } /** @@ -4464,14 +4502,14 @@ class DatetimePickerWidget extends Widget { global $cfg; if ($value = parent::getValue()) { - // Effective timezone for the selection - $timezone = $this->field->getTimezone(); // See if we have time $data = $this->field->getSource(); - if ($value && isset($data[$this->name . ':time'])) - $value .=' '.$data[$this->name . ':time']; - - $dt = new DateTime($value, $timezone); + // Parse value into datetime object + $dt = Format::parseDatetime($value); + // Effective timezone for the selection + if (($timezone = $this->field->getTimezone())) + $dt->setTimezone($timezone); + // Format date time to universal format $value = $dt->format('Y-m-d H:i:s T'); } diff --git a/include/class.list.php b/include/class.list.php index f8055805c2e692eec4fb446db79c8585241260f8..31cebdfdfbbdd4bf688d14a3cf8b9be59830c8b2 100644 --- a/include/class.list.php +++ b/include/class.list.php @@ -807,7 +807,10 @@ class DynamicListItem extends VerySimpleModel implements CustomListItem { 'sort' => 'sort', 'value' => 'value', 'abbrev' => 'extra') as $k => $v) { - if (isset($vars[$k])) + if ($k == 'abbrev' && empty($vars[$k])) { + $vars[$k] = NULL; + $this->set($v, $vars[$k]); + } elseif (isset($vars[$k])) $this->set($v, $vars[$k]); } diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php index d6e0953861a3992419f66f2ba2c3f9f592e2127a..57088768ec9424863aa7fb73b6beed6d2cedd611 100644 --- a/include/class.mailfetch.php +++ b/include/class.mailfetch.php @@ -671,6 +671,18 @@ class MailFetcher { // attachment. Download the body and pass it along to the mail // parsing engine. $info = Mail_Parse::splitHeaders($mailinfo['header']); + + //make sure reply-to headers are correctly formatted + if ($mailinfo['reply-to'] && !Validator::is_email($mailinfo['reply-to']) && $info['Reply-To']) { + $replyto = Mail_Parse::parseAddressList($info['Reply-To']); + if ($replyto[0]) { + $mailinfo['reply-to'] = sprintf('%s@%s', $replyto[0]->mailbox, $replyto[0]->host); + $mailinfo['reply-to-name'] = $replyto[0]->personal; + } else { + $mailinfo['reply-to'] = null; + } + } + if (strtolower($info['Content-Type']) == 'message/rfc822') { if ($wrapped = $this->getPart($mid, 'message/rfc822')) { require_once INCLUDE_DIR.'api.tickets.php'; diff --git a/include/class.misc.php b/include/class.misc.php index 3a1dc0551508986b23ed0a9abbc822df8c39c0fe..12bd7452689eacff5e579749a8ded800f2bbc238 100644 --- a/include/class.misc.php +++ b/include/class.misc.php @@ -245,48 +245,6 @@ class Misc { return $str; } - function timeDropdown($hr=null, $min =null,$name='time') { - global $cfg; - - //normalize; - if ($hr >= 24) - $hr = $hr%24; - elseif ($hr < 0) - $hr = 0; - elseif ($hr) - $hr = (int) $hr; - else // Default to 5pm - $hr = 17; - - if ($min >= 45) - $min = 45; - elseif ($min >= 30) - $min = 30; - elseif ($min >= 15) - $min = 15; - else - $min = 0; - - $time = Misc::user2gmtime(mktime(0,0,0)); - ob_start(); - echo sprintf('<select name="%s" id="%s" style="display:inline-block;width:auto">',$name,$name); - echo '<option value="" selected="selected">—'.__('Time').'—</option>'; - for($i=23; $i>=0; $i--) { - for ($minute=45; $minute>=0; $minute-=15) { - $sel=($hr===$i && $min===$minute) ? 'selected="selected"' : ''; - $_minute=str_pad($minute, 2, '0',STR_PAD_LEFT); - $_hour=str_pad($i, 2, '0',STR_PAD_LEFT); - $disp = Format::time($time + ($i*3600 + $minute*60 + 1), false); - echo sprintf('<option value="%s:%s" %s>%s</option>',$_hour,$_minute,$sel,$disp); - } - } - echo '</select>'; - $output = ob_get_contents(); - ob_end_clean(); - - return $output; - } - function realpath($path) { $rp = realpath($path); return $rp ? $rp : $path; diff --git a/include/class.search.php b/include/class.search.php index 2e05d9445ed1e5195cd96dd04469986579d55e39..3bc1ec35552ea4a7d7ecadb050e7c2aea18ff18e 100755 --- a/include/class.search.php +++ b/include/class.search.php @@ -482,7 +482,7 @@ class MysqlSearchBackend extends SearchBackend { $sql = "SELECT A1.`id`, A1.`title`, A1.`body`, A1.`format` FROM `".THREAD_ENTRY_TABLE."` A1 LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`id` = A2.`object_id` AND A2.`object_type`='H') WHERE A2.`object_id` IS NULL AND (A1.poster <> 'SYSTEM') - AND (LENGTH(A1.`title`) + LENGTH(A1.`body`) > 0) + AND (IFNULL(LENGTH(A1.`title`), 0) + IFNULL(LENGTH(A1.`body`), 0) > 0) ORDER BY A1.`id` DESC LIMIT 500"; if (!($res = db_query_unbuffered($sql, $auto_create))) return false; diff --git a/include/class.task.php b/include/class.task.php index acefedda76d947209305f32f63bd7675f0f1e316..fdacbc4eba1e5d730ff931a2d0d394caf783c76e 100644 --- a/include/class.task.php +++ b/include/class.task.php @@ -204,8 +204,24 @@ class TaskModel extends VerySimpleModel { return $this->setFlag(self::ISOPEN); } - function isAssigned() { - return ($this->isOpen() && ($this->getStaffId() || $this->getTeamId())); + function isAssigned($to=null) { + if (!$this->isOpen()) + return false; + + if (is_null($to)) + return ($this->getStaffId() || $this->getTeamId()); + + switch (true) { + case $to instanceof Staff: + return ($to->getId() == $this->getStaffId() || + $to->isTeamMember($this->getTeamId())); + break; + case $to instanceof Team: + return ($to->getId() == $this->getTeamId()); + break; + } + + return false; } function isOverdue() { diff --git a/include/class.thread.php b/include/class.thread.php index 7aae4f939bc5dec4ca298b7f1e698f314b556c41..94ffe4be037d89e155dcf06f56a1fbc5e260fbf0 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -119,7 +119,7 @@ implements Searchable { // Collaborators function getNumCollaborators() { - return $this->collaborators->count(); + return $this->getCollaborators()->count(); } function getNumActiveCollaborators() { diff --git a/include/class.thread_actions.php b/include/class.thread_actions.php index 54a2821c996b9fa4c3ef64ce8546aea735076825..b529436954730871070a75d1c3d553e3b45b5474 100644 --- a/include/class.thread_actions.php +++ b/include/class.thread_actions.php @@ -138,6 +138,13 @@ class TEA_EditThreadEntry extends ThreadEntryAction { && ($role = $thisstaff->getRole($T->getDeptId(), $T->isAssigned($thisstaff))) && $role->hasPerm(ThreadEntry::PERM_EDIT) ) + || ($T instanceof Task + && $T->getDept()->getManagerId() == $thisstaff->getId() + ) + || ($T instanceof Task + && ($role = $thisstaff->getRole($T->getDeptId(), $T->isAssigned($thisstaff))) + && $role->hasPerm(ThreadEntry::PERM_EDIT) + ) ); } diff --git a/include/class.ticket.php b/include/class.ticket.php index a634799bfafc37787625e3089791ffc311417d09..9227cd6d49afd4f14d9890fba065574e10b56c5c 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -551,15 +551,8 @@ implements RestrictedAccess, Threadable, Searchable { 'topicId' => $this->getTopicId(), 'slaId' => $this->getSLAId(), 'user_id' => $this->getOwnerId(), - 'duedate' => $this->getDueDate() - ? Format::date($this->getDueDate(), true, - $cfg->getDateFormat(true)) - : '', - 'time' => $this->getDueDate() - ? Format::time($this->getDueDate(), true, 'HH:mm') - : '', + 'duedate' => Misc::db2gmtime($this->getDueDate()), ); - } function getLock() { @@ -1505,6 +1498,7 @@ implements RestrictedAccess, Threadable, Searchable { // Send alert to out sleepy & idle staff. if ($alertstaff && $cfg->alertONNewTicket() + && $dept->getNumMembersForAlerts() && ($email=$dept->getAlertEmail()) && ($msg=$tpl->getNewTicketAlertMsgTemplate()) ) { @@ -1784,6 +1778,7 @@ implements RestrictedAccess, Threadable, Searchable { if (!$alert // Check if alert is enabled || !$cfg->alertONNewActivity() || !($dept=$this->getDept()) + || !$dept->getNumMembersForAlerts() || !($email=$cfg->getAlertEmail()) || !($tpl = $dept->getTemplate()) || !($msg=$tpl->getNoteAlertMsgTemplate()) @@ -1882,12 +1877,11 @@ implements RestrictedAccess, Threadable, Searchable { $note = $this->logNote($title, $comments, $assigner, false); } - + $dept = $this->getDept(); // See if we need to send alerts - if (!$alert || !$cfg->alertONAssignment()) + if (!$alert || !$cfg->alertONAssignment() || !$dept->getNumMembersForAlerts()) return true; //No alerts! - $dept = $this->getDept(); if (!$dept || !($tpl = $dept->getTemplate()) || !($email = $dept->getAlertEmail()) @@ -1947,6 +1941,7 @@ implements RestrictedAccess, Threadable, Searchable { if (!$whine || !$cfg->alertONOverdueTicket() || !($dept = $this->getDept()) + || !$dept->getNumMembersForAlerts() ) { return true; } @@ -2328,7 +2323,7 @@ implements RestrictedAccess, Threadable, Searchable { $this->thread->refer($cdept); //Send out alerts if enabled AND requested - if (!$alert || !$cfg->alertONTransfer()) + if (!$alert || !$cfg->alertONTransfer() || !$dept->getNumMembersForAlerts()) return true; //no alerts!! if (($email = $dept->getAlertEmail()) @@ -2769,6 +2764,7 @@ implements RestrictedAccess, Threadable, Searchable { $options = array('thread'=>$message); // If enabled...send alert to staff (New Message Alert) if ($cfg->alertONNewMessage() + && $dept->getNumMembersForAlerts() && ($email = $dept->getAlertEmail()) && ($tpl = $dept->getTemplate()) && ($msg = $tpl->getNewMessageAlertMsgTemplate()) @@ -3204,11 +3200,9 @@ implements RestrictedAccess, Threadable, Searchable { if ($vars['duedate']) { if ($this->isClosed()) $errors['duedate']=__('Due date can NOT be set on a closed ticket'); - elseif (!$vars['time'] || strpos($vars['time'],':') === false) - $errors['time']=__('Select a time from the list'); - elseif (strtotime($vars['duedate'].' '.$vars['time']) === false) + elseif (strtotime($vars['duedate']) === false) $errors['duedate']=__('Invalid due date'); - elseif (Misc::user2gmtime($vars['duedate'].' '.$vars['time']) <= Misc::user2gmtime()) + elseif (Misc::user2gmtime($vars['duedate']) <= Misc::user2gmtime()) $errors['duedate']=__('Due date must be in the future'); } @@ -3246,7 +3240,7 @@ implements RestrictedAccess, Threadable, Searchable { $this->sla_id = $vars['slaId']; $this->source = $vars['source']; $this->duedate = $vars['duedate'] - ? date('Y-m-d G:i',Misc::dbtime($vars['duedate'].' '.$vars['time'])) + ? date('Y-m-d G:i',Misc::dbtime($vars['duedate'])) : null; if ($vars['user_id']) @@ -4247,6 +4241,23 @@ implements RestrictedAccess, Threadable, Searchable { return static::$sources; } + // TODO: Create internal Form for internal fields + static function duedateField($name, $default='', $hint='') { + return DateTimeField::init(array( + 'id' => $name, + 'name' => $name, + 'default' => $default ?: false, + 'label' => __('Due Date'), + 'hint' => $hint, + 'configuration' => array( + 'min' => Misc::gmtime(), + 'time' => true, + 'gmt' => false, + 'future' => true, + ) + )); + } + static function registerCustomData(DynamicForm $form) { if (!isset(static::$meta['joins']['cdata+'.$form->id])) { $cdata_class = <<<EOF diff --git a/include/class.user.php b/include/class.user.php index e70749626b6a565f7d6bb8035ff30ba5a47056c3..412038646a9d655807b710f6a08aa28a1ab183a9 100644 --- a/include/class.user.php +++ b/include/class.user.php @@ -268,7 +268,7 @@ implements TemplateVariable, Searchable { //Validate the form $valid = true; $filter = function($f) use ($thisstaff) { - return !isset($thisstaff) || $f->isRequiredForStaff(); + return !isset($thisstaff) || $f->isRequiredForStaff() || $f->isVisibleToStaff(); }; if (!$form->isValid($filter)) $valid = false; diff --git a/include/class.validator.php b/include/class.validator.php index 2e1928f37882ea18c24e0b1500e3d29c9e0b8bd4..2d4c160f88b6de83b86b5010b4badbddcd2aff23 100644 --- a/include/class.validator.php +++ b/include/class.validator.php @@ -211,6 +211,11 @@ class Validator { return $error == ''; } + static function is_formula($text, &$error='') { + if (!preg_match('/^[^=\+@-].*$/s', $text)) + $error = __('Content cannot start with the following characters: = - + @'); + return $error == ''; + } /* * check_ip diff --git a/include/client/header.inc.php b/include/client/header.inc.php index 8bbff83307e751f11a08d52c242858f5ce43c45d..b48087bd8decbca292c2f990433d693490f26dba 100644 --- a/include/client/header.inc.php +++ b/include/client/header.inc.php @@ -6,7 +6,7 @@ $signin_url = ROOT_PATH . "login.php" $signout_url = ROOT_PATH . "logout.php?auth=".$ost->getLinkToken(); header("Content-Type: text/html; charset=UTF-8"); -header("Content-Security-Policy: frame-ancestors '".$cfg->getAllowIframes()."';"); +header("Content-Security-Policy: frame-ancestors ".$cfg->getAllowIframes().";"); if (($lang = Internationalization::getCurrentLanguage())) { $langs = array_unique(array($lang, $cfg->getPrimaryLanguage())); @@ -38,6 +38,7 @@ if ($lang) { media="screen" /> <link type="text/css" href="<?php echo ROOT_PATH; ?>css/ui-lightness/jquery-ui-1.10.3.custom.min.css" rel="stylesheet" media="screen" /> + <link rel="stylesheet" href="<?php echo ROOT_PATH ?>css/jquery-ui-timepicker-addon.css" media="all"> <link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/thread.css" media="screen"> <link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/redactor.css" media="screen"> <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/font-awesome.min.css"> @@ -49,6 +50,7 @@ if ($lang) { <link rel="icon" type="image/png" href="<?php echo ROOT_PATH ?>images/oscar-favicon-16x16.png" sizes="16x16" /> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-3.4.0.min.js"></script> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-ui-1.12.1.custom.min.js"></script> + <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-ui-timepicker-addon.js"></script> <script src="<?php echo ROOT_PATH; ?>js/osticket.js"></script> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/filedrop.field.js"></script> <script src="<?php echo ROOT_PATH; ?>scp/js/bootstrap-typeahead.js"></script> diff --git a/include/client/templates/thread-export.tmpl.php b/include/client/templates/thread-export.tmpl.php index 2ed2288d85d1344781ad195798946b1234277f29..d7b9d58e7a881ef7be93bd61edf3b2d32bf3c8f5 100644 --- a/include/client/templates/thread-export.tmpl.php +++ b/include/client/templates/thread-export.tmpl.php @@ -12,7 +12,7 @@ AttachmentFile::objects()->filter(array( ))->all(); $entries = $thread->getEntries(); -$entries->filter(array('type__in' => array_keys($entryTypes)))->order_by("{$order}id");; +$entries->filter(array('type__in' => array_keys($entryTypes)))->order_by("{$order}id"); ?> <style type="text/css"> div {font-family: sans-serif;} diff --git a/include/mpdf/vendor/mpdf/mpdf/src/Config/ConfigVariables.php b/include/mpdf/vendor/mpdf/mpdf/src/Config/ConfigVariables.php index 95633de2a24402e48cf4d211fa27f7f0bb1dc758..8bd319382dbf969a35234b5c7e87e09e7032232d 100755 --- a/include/mpdf/vendor/mpdf/mpdf/src/Config/ConfigVariables.php +++ b/include/mpdf/vendor/mpdf/mpdf/src/Config/ConfigVariables.php @@ -93,12 +93,12 @@ class ConfigVariables // AUTOMATIC FONT SELECTION // Based on script and/or language // mPDF 6.0 (similar to previously using function SetAutoFont() ) - 'autoScriptToLang' => false, + 'autoScriptToLang' => true, 'baseScript' => Ucdn::SCRIPT_LATIN, 'autoVietnamese' => true, 'autoArabic' => true, // mPDF 6.0 (similar to old useLang) - 'autoLangToFont' => false, + 'autoLangToFont' => true, // Substitute missing characters in UTF-8(multibyte) documents - from other fonts 'useSubstitutions' => false, diff --git a/include/staff/footer.inc.php b/include/staff/footer.inc.php index 94de13f1a940812525c0c84826aa9d823b5f9cc3..20a25a560c175c2b625fea2696e25bca458ac962 100644 --- a/include/staff/footer.inc.php +++ b/include/staff/footer.inc.php @@ -44,6 +44,8 @@ if(is_object($thisstaff) && $thisstaff->isStaff()) { ?> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery.pjax.js"></script> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>scp/js/bootstrap-typeahead.js"></script> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-ui-1.12.1.custom.min.js"></script> +<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-ui-timepicker-addon.js"></script> +<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-ui-sliderAccess.js"></script> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>scp/js/scp.js"></script> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/filedrop.field.js"></script> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/select2.min.js"></script> diff --git a/include/staff/header.inc.php b/include/staff/header.inc.php index 8ade5d3cde0cff815ebe925b1bce302ea439fc76..c08a84f3b071c470cc905f7e71411dad1ab73714 100644 --- a/include/staff/header.inc.php +++ b/include/staff/header.inc.php @@ -35,7 +35,8 @@ if ($lang) { <link rel="stylesheet" href="<?php echo ROOT_PATH ?>scp/css/typeahead.css" media="screen"> <link type="text/css" href="<?php echo ROOT_PATH; ?>css/ui-lightness/jquery-ui-1.10.3.custom.min.css" rel="stylesheet" media="screen" /> - <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/font-awesome.min.css"> + <link rel="stylesheet" href="<?php echo ROOT_PATH ?>css/jquery-ui-timepicker-addon.css" media="all"> + <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/font-awesome.min.css"> <!--[if IE 7]> <link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/font-awesome-ie7.min.css"> <![endif]--> diff --git a/include/staff/templates/collaborators.tmpl.php b/include/staff/templates/collaborators.tmpl.php index b2545daa315c3f889f1150267c8736eccea54401..a123ed035be52c7b70aa62fa36ac319112e6b70f 100644 --- a/include/staff/templates/collaborators.tmpl.php +++ b/include/staff/templates/collaborators.tmpl.php @@ -14,7 +14,7 @@ if ($thread->object_type == 'A') <?php if(($users=$thread->getCollaborators())) {?> <div id="manage_collaborators"> -<form method="post" class="collaborators" onsubmit="refreshAndClose(<?php echo $thread->object_id; ?>, <?php echo $type; ?>);" action="#thread/<?php echo $thread->getId(); ?>/collaborators"> +<form method="post" class="collaborators" action="#thread/<?php echo $thread->getId(); ?>/collaborators"> <table border="0" cellspacing="1" cellpadding="1" width="100%"> <?php foreach($users as $user) { @@ -24,15 +24,13 @@ if(($users=$thread->getCollaborators())) {?> echo sprintf('<tr> <td> <label class="inline checkbox"> - <input type="checkbox" class="hidden" name="uid[]" id="%d" value="%d" checked="checked"> <input type="checkbox" name="cid[]" id="c%d" value="%d" %s> </label> <a class="collaborator" href="#thread/%d/collaborators/%d/view">%s%s</a> <div align="left"> <span class="faded"><em>%s</em></span> </div> - </td>', $user->getId(), - $user->getId(), + </td>', $user->getId(), $user->getId(), $checked, @@ -65,7 +63,7 @@ if(($users=$thread->getCollaborators())) {?> <p class="full-width"> <span class="buttons pull-left"> <input type="reset" value="<?php echo __('Reset'); ?>"> - <input type="button" value="<?php echo __('Cancel'); ?>" class="close"> + <input type="button" value="<?php echo __('Done'); ?>" class="close"> </span> <span class="buttons pull-right"> <input type="submit" value="<?php echo __('Save Changes'); ?>"> @@ -79,22 +77,24 @@ if(($users=$thread->getCollaborators())) {?> echo __("Bro, not sure how you got here!"); } -if ($_POST && $thread && $thread->getNumCollaborators()) { - - $collaborators = sprintf('Collaborators (%d)', - $thread->getNumCollaborators()); - - $recipients = sprintf(__('Collaborators (%d of %d)'), +if ($_POST && $thread) { + $collabs = $thread->getCollaborators(); + foreach ($collabs as $c) { + $options .= sprintf('<option value="%s" %s class="%s">%s</option>', + $c->getUserId(), + $c->isActive() ? 'selected="selected"' : '', + $c->isActive() ? 'active' : 'disabled', + $c->getName()); + } + $recipients = sprintf(__('(%d of %d)'), $thread->getNumActiveCollaborators(), $thread->getNumCollaborators()); ?> <script type="text/javascript"> $(function() { - $('#emailcollab').show(); - $('#t<?php echo $thread->getId(); ?>-recipients') - .html('<?php echo $recipients; ?>'); - $('#t<?php echo $thread->getId(); ?>-collaborators') - .html('<?php echo $collaborators; ?>'); + $('#t<?php echo $thread->getId(); ?>-recipients').html('<?php echo $recipients; ?>'); + $('#t<?php echo $thread->getId(); ?>-collaborators').html('<?php echo $thread->getNumCollaborators(); ?>'); + $('#collabselection').html('<?php echo $options; ?>'); }); </script> <?php @@ -153,8 +153,4 @@ $(function() { }); }); - -function refreshAndClose(tid, type) { - window.location.href = type + '.php?id=' + tid; -} </script> diff --git a/include/staff/templates/task-view.tmpl.php b/include/staff/templates/task-view.tmpl.php index d56b230c2e96f26cb789b4a954ea3ee41777715a..76d5406ffb6c63bee111f3e1ea78f5a20fa4476c 100644 --- a/include/staff/templates/task-view.tmpl.php +++ b/include/staff/templates/task-view.tmpl.php @@ -486,15 +486,15 @@ else style="display:<?php echo $thread->getNumCollaborators() ? 'inline-block': 'none'; ?>;" > <?php - $recipients = __('Collaborators'); if ($thread->getNumCollaborators()) - $recipients = sprintf(__('Collaborators (%d of %d)'), + $recipients = sprintf(__('(%d of %d)'), $thread->getNumActiveCollaborators(), $thread->getNumCollaborators()); echo sprintf('<span><a class="collaborators preview" - href="#thread/%d/collaborators"><span id="t%d-recipients">%s</span></a></span>', + href="#thread/%d/collaborators"> %s <span id="t%d-recipients">%s</span></a></span>', $thread->getId(), + __('Collaborators'), $thread->getId(), $recipients); ?> diff --git a/include/staff/ticket-edit.inc.php b/include/staff/ticket-edit.inc.php index 3f11fdde2d91cfbd79c4529ddd64b69db94e1b77..80769aa7e846b34b987775a18d7d9198aa620661 100644 --- a/include/staff/ticket-edit.inc.php +++ b/include/staff/ticket-edit.inc.php @@ -140,16 +140,11 @@ if ($_POST) <?php echo __('Due Date');?>: </td> <td> - <input class="dp" id="duedate" name="duedate" value="<?php echo Format::htmlchars($info['duedate']); ?>" size="12" autocomplete=OFF> - <?php - $min=$hr=null; - if($info['time']) - list($hr, $min)=explode(':', $info['time']); - - echo Misc::timeDropdown($hr, $min, 'time'); + $duedateField = Ticket::duedateField('duedate', $info['duedate']); + $duedateField->render(); ?> - <font class="error"> <?php echo $errors['duedate']; ?> <?php echo $errors['time']; ?></font> + <font class="error"> <?php echo $errors['duedate']; ?></font> <em><?php echo __('Time is based on your time zone');?> (<?php echo $cfg->getTimezone($thisstaff); ?>)</em> </td> diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php index a1f23e116d6b6db750b57865b7479ea694c14da6..9f5a50e9f403be3d28d7ea62f368c836005c93a0 100644 --- a/include/staff/ticket-open.inc.php +++ b/include/staff/ticket-open.inc.php @@ -301,17 +301,13 @@ if ($_POST) <?php echo __('Due Date');?>: </td> <td> - <input class="dp" id="duedate" name="duedate" value="<?php echo Format::htmlchars($info['duedate']); ?>" size="12" autocomplete=OFF> - <?php - $min=$hr=null; - if($info['time']) - list($hr, $min)=explode(':', $info['time']); - - echo Misc::timeDropdown($hr, $min, 'time'); + $duedateField = Ticket::duedateField('duedate', $info['duedate']); + $duedateField->render(); ?> <font class="error"> <?php echo $errors['duedate']; ?> <?php echo $errors['time']; ?></font> - <em><?php echo __('Time is based on your time zone');?> (GMT <?php echo Format::date(false, false, 'ZZZ'); ?>)</em> + <em><?php echo __('Time is based on your time + zone');?> (<?php echo $cfg->getTimezone($thisstaff); ?>)</em> </td> </tr> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index b71877bf99d22b64cdf549f2c34ff7f2629ea7a6..47e7728e0b14cd671880b582c1dcc98c91b5872a 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -383,7 +383,7 @@ if($ticket->isOverdue()) $recipients = 0; echo sprintf('<span><a class="manage-collaborators preview" - href="#thread/%d/collaborators"><span id="t%d-recipients"><i class="icon-group"></i> (%s)</span></a></span>', + href="#thread/%d/collaborators"><span><i class="icon-group"></i> (<span id="t%d-collaborators">%s</span>)</span></a></span>', $ticket->getThreadId(), $ticket->getThreadId(), $recipients); @@ -794,26 +794,26 @@ if ($errors['err'] && isset($_POST['a'])) { <td> <div style="margin-bottom:2px;"> <?php - echo sprintf('<span><a id="show_ccs"> + if ($ticket->getThread()->getNumCollaborators()) + $recipients = sprintf(__('(%d of %d)'), + $ticket->getThread()->getNumActiveCollaborators(), + $ticket->getThread()->getNumCollaborators()); + + echo sprintf('<span"><a id="show_ccs"> <i id="arrow-icon" class="icon-caret-right"></i> %s </a> <a class="manage-collaborators collaborators preview noclick %s" href="#thread/%d/collaborators"> - (%s)</a></span>', + %s</a></span>', __('Collaborators'), $ticket->getNumCollaborators() ? '' : 'hidden', $ticket->getThreadId(), - sprintf(__('%s of %s'), - sprintf('<span - class="collabselection__count">%d</span>', - $ticket->getNumActiveCollaborators()), - sprintf('<span - class="collabselection__total">%d</span>', - $ticket->getNumCollaborators()) - ) - ); + sprintf('<span id="t%d-recipients">%s</span></a></span>', + $ticket->getThreadId(), + $recipients) + ); ?> </div> <div id="ccs" class="hidden"> diff --git a/js/jquery-ui-sliderAccess.js b/js/jquery-ui-sliderAccess.js new file mode 100644 index 0000000000000000000000000000000000000000..b075c6698e4e7ce66a7b1016266413d0b35b75dd --- /dev/null +++ b/js/jquery-ui-sliderAccess.js @@ -0,0 +1,91 @@ +/* + * jQuery UI Slider Access + * By: Trent Richardson [http://trentrichardson.com] + * Version 0.3 + * Last Modified: 10/20/2012 + * + * Copyright 2011 Trent Richardson + * Dual licensed under the MIT and GPL licenses. + * http://trentrichardson.com/Impromptu/GPL-LICENSE.txt + * http://trentrichardson.com/Impromptu/MIT-LICENSE.txt + * + */ + (function($){ + + $.fn.extend({ + sliderAccess: function(options){ + options = options || {}; + options.touchonly = options.touchonly !== undefined? options.touchonly : true; // by default only show it if touch device + + if(options.touchonly === true && !("ontouchend" in document)){ + return $(this); + } + + return $(this).each(function(i,obj){ + var $t = $(this), + o = $.extend({},{ + where: 'after', + step: $t.slider('option','step'), + upIcon: 'ui-icon-plus', + downIcon: 'ui-icon-minus', + text: false, + upText: '+', + downText: '-', + buttonset: true, + buttonsetTag: 'span', + isRTL: false + }, options), + $buttons = $('<'+ o.buttonsetTag +' class="ui-slider-access">'+ + '<button data-icon="'+ o.downIcon +'" data-step="'+ (o.isRTL? o.step : o.step*-1) +'">'+ o.downText +'</button>'+ + '<button data-icon="'+ o.upIcon +'" data-step="'+ (o.isRTL? o.step*-1 : o.step) +'">'+ o.upText +'</button>'+ + '</'+ o.buttonsetTag +'>'); + + $buttons.children('button').each(function(j, jobj){ + var $jt = $(this); + $jt.button({ + text: o.text, + icons: { primary: $jt.data('icon') } + }) + .click(function(e){ + var step = $jt.data('step'), + curr = $t.slider('value'), + newval = curr += step*1, + minval = $t.slider('option','min'), + maxval = $t.slider('option','max'), + slidee = $t.slider("option", "slide") || function(){}, + stope = $t.slider("option", "stop") || function(){}; + + e.preventDefault(); + + if(newval < minval || newval > maxval){ + return; + } + + $t.slider('value', newval); + + slidee.call($t, null, { value: newval }); + stope.call($t, null, { value: newval }); + }); + }); + + // before or after + $t[o.where]($buttons); + + if(o.buttonset){ + $buttons.removeClass('ui-corner-right').removeClass('ui-corner-left').buttonset(); + $buttons.eq(0).addClass('ui-corner-left'); + $buttons.eq(1).addClass('ui-corner-right'); + } + + // adjust the width so we don't break the original layout + var bOuterWidth = $buttons.css({ + marginLeft: ((o.where === 'after' && !o.isRTL) || (o.where === 'before' && o.isRTL)? 10:0), + marginRight: ((o.where === 'before' && !o.isRTL) || (o.where === 'after' && o.isRTL)? 10:0) + }).outerWidth(true) + 5; + var tOuterWidth = $t.outerWidth(true); + $t.css('display','inline-block').width(tOuterWidth-bOuterWidth); + }); + } + }); + +})(jQuery); \ No newline at end of file diff --git a/js/jquery-ui-timepicker-addon.js b/js/jquery-ui-timepicker-addon.js new file mode 100644 index 0000000000000000000000000000000000000000..9bbb21f2939c8832434b878119bc20d2e512176e --- /dev/null +++ b/js/jquery-ui-timepicker-addon.js @@ -0,0 +1,2297 @@ +/* + * jQuery Timepicker Addon + * By: Trent Richardson [http://trentrichardson.com] + * + * Copyright 2013 Trent Richardson + * You may use this project under MIT license. + * http://trentrichardson.com/Impromptu/MIT-LICENSE.txt + */ + +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery', 'jquery-ui'], factory); + } else { + factory(jQuery); + } +}(function ($) { + + /* + * Lets not redefine timepicker, Prevent "Uncaught RangeError: Maximum call stack size exceeded" + */ + $.ui.timepicker = $.ui.timepicker || {}; + if ($.ui.timepicker.version) { + return; + } + + /* + * Extend jQueryUI, get it started with our version number + */ + $.extend($.ui, { + timepicker: { + version: "@@version" + } + }); + + /* + * Timepicker manager. + * Use the singleton instance of this class, $.timepicker, to interact with the time picker. + * Settings for (groups of) time pickers are maintained in an instance object, + * allowing multiple different settings on the same page. + */ + var Timepicker = function () { + this.regional = []; // Available regional settings, indexed by language code + this.regional[''] = { // Default regional settings + currentText: 'Now', + closeText: 'Done', + amNames: ['AM', 'A'], + pmNames: ['PM', 'P'], + timeFormat: 'HH:mm', + timeSuffix: '', + timeOnlyTitle: 'Choose Time', + timeText: 'Time', + hourText: 'Hour', + minuteText: 'Minute', + secondText: 'Second', + millisecText: 'Millisecond', + microsecText: 'Microsecond', + timezoneText: 'Time Zone', + isRTL: false + }; + this._defaults = { // Global defaults for all the datetime picker instances + showButtonPanel: true, + timeOnly: false, + timeOnlyShowDate: false, + showHour: null, + showMinute: null, + showSecond: null, + showMillisec: null, + showMicrosec: null, + showTimezone: null, + showTime: true, + stepHour: 1, + stepMinute: 1, + stepSecond: 1, + stepMillisec: 1, + stepMicrosec: 1, + hour: 0, + minute: 0, + second: 0, + millisec: 0, + microsec: 0, + timezone: null, + hourMin: 0, + minuteMin: 0, + secondMin: 0, + millisecMin: 0, + microsecMin: 0, + hourMax: 23, + minuteMax: 59, + secondMax: 59, + millisecMax: 999, + microsecMax: 999, + minDateTime: null, + maxDateTime: null, + maxTime: null, + minTime: null, + onSelect: null, + hourGrid: 0, + minuteGrid: 0, + secondGrid: 0, + millisecGrid: 0, + microsecGrid: 0, + alwaysSetTime: true, + separator: ' ', + altFieldTimeOnly: true, + altTimeFormat: null, + altSeparator: null, + altTimeSuffix: null, + altRedirectFocus: true, + pickerTimeFormat: null, + pickerTimeSuffix: null, + showTimepicker: true, + timezoneList: null, + addSliderAccess: false, + sliderAccessArgs: null, + controlType: 'slider', + oneLine: false, + defaultValue: null, + parse: 'strict', + afterInject: null + }; + $.extend(this._defaults, this.regional['']); + }; + + $.extend(Timepicker.prototype, { + $input: null, + $altInput: null, + $timeObj: null, + inst: null, + hour_slider: null, + minute_slider: null, + second_slider: null, + millisec_slider: null, + microsec_slider: null, + timezone_select: null, + maxTime: null, + minTime: null, + hour: 0, + minute: 0, + second: 0, + millisec: 0, + microsec: 0, + timezone: null, + hourMinOriginal: null, + minuteMinOriginal: null, + secondMinOriginal: null, + millisecMinOriginal: null, + microsecMinOriginal: null, + hourMaxOriginal: null, + minuteMaxOriginal: null, + secondMaxOriginal: null, + millisecMaxOriginal: null, + microsecMaxOriginal: null, + ampm: '', + formattedDate: '', + formattedTime: '', + formattedDateTime: '', + timezoneList: null, + units: ['hour', 'minute', 'second', 'millisec', 'microsec'], + support: {}, + control: null, + + /* + * Override the default settings for all instances of the time picker. + * @param {Object} settings object - the new settings to use as defaults (anonymous object) + * @return {Object} the manager object + */ + setDefaults: function (settings) { + extendRemove(this._defaults, settings || {}); + return this; + }, + + /* + * Create a new Timepicker instance + */ + _newInst: function ($input, opts) { + var tp_inst = new Timepicker(), + inlineSettings = {}, + fns = {}, + overrides, i; + + for (var attrName in this._defaults) { + if (this._defaults.hasOwnProperty(attrName)) { + var attrValue = $input.attr('time:' + attrName); + if (attrValue) { + try { + inlineSettings[attrName] = eval(attrValue); + } catch (err) { + inlineSettings[attrName] = attrValue; + } + } + } + } + + overrides = { + beforeShow: function (input, dp_inst) { + if ($.isFunction(tp_inst._defaults.evnts.beforeShow)) { + return tp_inst._defaults.evnts.beforeShow.call($input[0], input, dp_inst, tp_inst); + } + }, + onChangeMonthYear: function (year, month, dp_inst) { + // Update the time as well : this prevents the time from disappearing from the $input field. + // tp_inst._updateDateTime(dp_inst); + if ($.isFunction(tp_inst._defaults.evnts.onChangeMonthYear)) { + tp_inst._defaults.evnts.onChangeMonthYear.call($input[0], year, month, dp_inst, tp_inst); + } + }, + onClose: function (dateText, dp_inst) { + if (tp_inst.timeDefined === true && $input.val() !== '') { + tp_inst._updateDateTime(dp_inst); + } + if ($.isFunction(tp_inst._defaults.evnts.onClose)) { + tp_inst._defaults.evnts.onClose.call($input[0], dateText, dp_inst, tp_inst); + } + } + }; + for (i in overrides) { + if (overrides.hasOwnProperty(i)) { + fns[i] = opts[i] || this._defaults[i] || null; + } + } + + tp_inst._defaults = $.extend({}, this._defaults, inlineSettings, opts, overrides, { + evnts: fns, + timepicker: tp_inst // add timepicker as a property of datepicker: $.datepicker._get(dp_inst, 'timepicker'); + }); + tp_inst.amNames = $.map(tp_inst._defaults.amNames, function (val) { + return val.toUpperCase(); + }); + tp_inst.pmNames = $.map(tp_inst._defaults.pmNames, function (val) { + return val.toUpperCase(); + }); + + // detect which units are supported + tp_inst.support = detectSupport( + tp_inst._defaults.timeFormat + + (tp_inst._defaults.pickerTimeFormat ? tp_inst._defaults.pickerTimeFormat : '') + + (tp_inst._defaults.altTimeFormat ? tp_inst._defaults.altTimeFormat : '')); + + // controlType is string - key to our this._controls + if (typeof(tp_inst._defaults.controlType) === 'string') { + if (tp_inst._defaults.controlType === 'slider' && typeof($.ui.slider) === 'undefined') { + tp_inst._defaults.controlType = 'select'; + } + tp_inst.control = tp_inst._controls[tp_inst._defaults.controlType]; + } + // controlType is an object and must implement create, options, value methods + else { + tp_inst.control = tp_inst._defaults.controlType; + } + + // prep the timezone options + var timezoneList = [-720, -660, -600, -570, -540, -480, -420, -360, -300, -270, -240, -210, -180, -120, -60, + 0, 60, 120, 180, 210, 240, 270, 300, 330, 345, 360, 390, 420, 480, 525, 540, 570, 600, 630, 660, 690, 720, 765, 780, 840]; + if (tp_inst._defaults.timezoneList !== null) { + timezoneList = tp_inst._defaults.timezoneList; + } + var tzl = timezoneList.length, tzi = 0, tzv = null; + if (tzl > 0 && typeof timezoneList[0] !== 'object') { + for (; tzi < tzl; tzi++) { + tzv = timezoneList[tzi]; + timezoneList[tzi] = { value: tzv, label: $.timepicker.timezoneOffsetString(tzv, tp_inst.support.iso8601) }; + } + } + tp_inst._defaults.timezoneList = timezoneList; + + // set the default units + tp_inst.timezone = tp_inst._defaults.timezone !== null ? $.timepicker.timezoneOffsetNumber(tp_inst._defaults.timezone) : + ((new Date()).getTimezoneOffset() * -1); + tp_inst.hour = tp_inst._defaults.hour < tp_inst._defaults.hourMin ? tp_inst._defaults.hourMin : + tp_inst._defaults.hour > tp_inst._defaults.hourMax ? tp_inst._defaults.hourMax : tp_inst._defaults.hour; + tp_inst.minute = tp_inst._defaults.minute < tp_inst._defaults.minuteMin ? tp_inst._defaults.minuteMin : + tp_inst._defaults.minute > tp_inst._defaults.minuteMax ? tp_inst._defaults.minuteMax : tp_inst._defaults.minute; + tp_inst.second = tp_inst._defaults.second < tp_inst._defaults.secondMin ? tp_inst._defaults.secondMin : + tp_inst._defaults.second > tp_inst._defaults.secondMax ? tp_inst._defaults.secondMax : tp_inst._defaults.second; + tp_inst.millisec = tp_inst._defaults.millisec < tp_inst._defaults.millisecMin ? tp_inst._defaults.millisecMin : + tp_inst._defaults.millisec > tp_inst._defaults.millisecMax ? tp_inst._defaults.millisecMax : tp_inst._defaults.millisec; + tp_inst.microsec = tp_inst._defaults.microsec < tp_inst._defaults.microsecMin ? tp_inst._defaults.microsecMin : + tp_inst._defaults.microsec > tp_inst._defaults.microsecMax ? tp_inst._defaults.microsecMax : tp_inst._defaults.microsec; + tp_inst.ampm = ''; + tp_inst.$input = $input; + + if (tp_inst._defaults.altField) { + tp_inst.$altInput = $(tp_inst._defaults.altField); + if (tp_inst._defaults.altRedirectFocus === true) { + tp_inst.$altInput.css({ + cursor: 'pointer' + }).focus(function () { + $input.trigger("focus"); + }); + } + } + + if (tp_inst._defaults.minDate === 0 || tp_inst._defaults.minDateTime === 0) { + tp_inst._defaults.minDate = new Date(); + } + if (tp_inst._defaults.maxDate === 0 || tp_inst._defaults.maxDateTime === 0) { + tp_inst._defaults.maxDate = new Date(); + } + + // datepicker needs minDate/maxDate, timepicker needs minDateTime/maxDateTime.. + if (tp_inst._defaults.minDate !== undefined && tp_inst._defaults.minDate instanceof Date) { + tp_inst._defaults.minDateTime = new Date(tp_inst._defaults.minDate.getTime()); + } + if (tp_inst._defaults.minDateTime !== undefined && tp_inst._defaults.minDateTime instanceof Date) { + tp_inst._defaults.minDate = new Date(tp_inst._defaults.minDateTime.getTime()); + } + if (tp_inst._defaults.maxDate !== undefined && tp_inst._defaults.maxDate instanceof Date) { + tp_inst._defaults.maxDateTime = new Date(tp_inst._defaults.maxDate.getTime()); + } + if (tp_inst._defaults.maxDateTime !== undefined && tp_inst._defaults.maxDateTime instanceof Date) { + tp_inst._defaults.maxDate = new Date(tp_inst._defaults.maxDateTime.getTime()); + } + tp_inst.$input.bind('focus', function () { + tp_inst._onFocus(); + }); + + return tp_inst; + }, + + /* + * add our sliders to the calendar + */ + _addTimePicker: function (dp_inst) { + var currDT = $.trim((this.$altInput && this._defaults.altFieldTimeOnly) ? this.$input.val() + ' ' + this.$altInput.val() : this.$input.val()); + + this.timeDefined = this._parseTime(currDT); + this._limitMinMaxDateTime(dp_inst, false); + this._injectTimePicker(); + this._afterInject(); + }, + + /* + * parse the time string from input value or _setTime + */ + _parseTime: function (timeString, withDate) { + if (!this.inst) { + this.inst = $.datepicker._getInst(this.$input[0]); + } + + if (withDate || !this._defaults.timeOnly) { + var dp_dateFormat = $.datepicker._get(this.inst, 'dateFormat'); + try { + var parseRes = parseDateTimeInternal(dp_dateFormat, this._defaults.timeFormat, timeString, $.datepicker._getFormatConfig(this.inst), this._defaults); + if (!parseRes.timeObj) { + return false; + } + $.extend(this, parseRes.timeObj); + } catch (err) { + $.timepicker.log("Error parsing the date/time string: " + err + + "\ndate/time string = " + timeString + + "\ntimeFormat = " + this._defaults.timeFormat + + "\ndateFormat = " + dp_dateFormat); + return false; + } + return true; + } else { + var timeObj = $.datepicker.parseTime(this._defaults.timeFormat, timeString, this._defaults); + if (!timeObj) { + return false; + } + $.extend(this, timeObj); + return true; + } + }, + + /* + * Handle callback option after injecting timepicker + */ + _afterInject: function() { + var o = this.inst.settings; + if ($.isFunction(o.afterInject)) { + o.afterInject.call(this); + } + }, + + /* + * generate and inject html for timepicker into ui datepicker + */ + _injectTimePicker: function () { + var $dp = this.inst.dpDiv, + o = this.inst.settings, + tp_inst = this, + litem = '', + uitem = '', + show = null, + max = {}, + gridSize = {}, + size = null, + i = 0, + l = 0; + + // Prevent displaying twice + if ($dp.find("div.ui-timepicker-div").length === 0 && o.showTimepicker) { + var noDisplay = ' ui_tpicker_unit_hide', + html = '<div class="ui-timepicker-div' + (o.isRTL ? ' ui-timepicker-rtl' : '') + (o.oneLine && o.controlType === 'select' ? ' ui-timepicker-oneLine' : '') + '"><dl>' + '<dt class="ui_tpicker_time_label' + ((o.showTime) ? '' : noDisplay) + '">' + o.timeText + '</dt>' + + '<dd class="ui_tpicker_time '+ ((o.showTime) ? '' : noDisplay) + '"><input class="ui_tpicker_time_input" ' + (o.timeInput ? '' : 'disabled') + '/></dd>'; + + // Create the markup + for (i = 0, l = this.units.length; i < l; i++) { + litem = this.units[i]; + uitem = litem.substr(0, 1).toUpperCase() + litem.substr(1); + show = o['show' + uitem] !== null ? o['show' + uitem] : this.support[litem]; + + // Added by Peter Medeiros: + // - Figure out what the hour/minute/second max should be based on the step values. + // - Example: if stepMinute is 15, then minMax is 45. + max[litem] = parseInt((o[litem + 'Max'] - ((o[litem + 'Max'] - o[litem + 'Min']) % o['step' + uitem])), 10); + gridSize[litem] = 0; + + html += '<dt class="ui_tpicker_' + litem + '_label' + (show ? '' : noDisplay) + '">' + o[litem + 'Text'] + '</dt>' + + '<dd class="ui_tpicker_' + litem + (show ? '' : noDisplay) + '"><div class="ui_tpicker_' + litem + '_slider' + (show ? '' : noDisplay) + '"></div>'; + + if (show && o[litem + 'Grid'] > 0) { + html += '<div style="padding-left: 1px"><table class="ui-tpicker-grid-label"><tr>'; + + if (litem === 'hour') { + for (var h = o[litem + 'Min']; h <= max[litem]; h += parseInt(o[litem + 'Grid'], 10)) { + gridSize[litem]++; + var tmph = $.datepicker.formatTime(this.support.ampm ? 'hht' : 'HH', {hour: h}, o); + html += '<td data-for="' + litem + '">' + tmph + '</td>'; + } + } + else { + for (var m = o[litem + 'Min']; m <= max[litem]; m += parseInt(o[litem + 'Grid'], 10)) { + gridSize[litem]++; + html += '<td data-for="' + litem + '">' + ((m < 10) ? '0' : '') + m + '</td>'; + } + } + + html += '</tr></table></div>'; + } + html += '</dd>'; + } + + // Timezone + var showTz = o.showTimezone !== null ? o.showTimezone : this.support.timezone; + html += '<dt class="ui_tpicker_timezone_label' + (showTz ? '' : noDisplay) + '">' + o.timezoneText + '</dt>'; + html += '<dd class="ui_tpicker_timezone' + (showTz ? '' : noDisplay) + '"></dd>'; + + // Create the elements from string + html += '</dl></div>'; + var $tp = $(html); + + // if we only want time picker... + if (o.timeOnly === true) { + $tp.prepend('<div class="ui-widget-header ui-helper-clearfix ui-corner-all">' + '<div class="ui-datepicker-title">' + o.timeOnlyTitle + '</div>' + '</div>'); + $dp.find('.ui-datepicker-header, .ui-datepicker-calendar').hide(); + } + + // add sliders, adjust grids, add events + for (i = 0, l = tp_inst.units.length; i < l; i++) { + litem = tp_inst.units[i]; + uitem = litem.substr(0, 1).toUpperCase() + litem.substr(1); + show = o['show' + uitem] !== null ? o['show' + uitem] : this.support[litem]; + + // add the slider + tp_inst[litem + '_slider'] = tp_inst.control.create(tp_inst, $tp.find('.ui_tpicker_' + litem + '_slider'), litem, tp_inst[litem], o[litem + 'Min'], max[litem], o['step' + uitem]); + + // adjust the grid and add click event + if (show && o[litem + 'Grid'] > 0) { + size = 100 * gridSize[litem] * o[litem + 'Grid'] / (max[litem] - o[litem + 'Min']); + $tp.find('.ui_tpicker_' + litem + ' table').css({ + width: size + "%", + marginLeft: o.isRTL ? '0' : ((size / (-2 * gridSize[litem])) + "%"), + marginRight: o.isRTL ? ((size / (-2 * gridSize[litem])) + "%") : '0', + borderCollapse: 'collapse' + }).find("td").click(function (e) { + var $t = $(this), + h = $t.html(), + n = parseInt(h.replace(/[^0-9]/g), 10), + ap = h.replace(/[^apm]/ig), + f = $t.data('for'); // loses scope, so we use data-for + + if (f === 'hour') { + if (ap.indexOf('p') !== -1 && n < 12) { + n += 12; + } + else { + if (ap.indexOf('a') !== -1 && n === 12) { + n = 0; + } + } + } + + tp_inst.control.value(tp_inst, tp_inst[f + '_slider'], litem, n); + + tp_inst._onTimeChange(); + tp_inst._onSelectHandler(); + }).css({ + cursor: 'pointer', + width: (100 / gridSize[litem]) + '%', + textAlign: 'center', + overflow: 'hidden' + }); + } // end if grid > 0 + } // end for loop + + // Add timezone options + this.timezone_select = $tp.find('.ui_tpicker_timezone').append('<select></select>').find("select"); + $.fn.append.apply(this.timezone_select, + $.map(o.timezoneList, function (val, idx) { + return $("<option />").val(typeof val === "object" ? val.value : val).text(typeof val === "object" ? val.label : val); + })); + if (typeof(this.timezone) !== "undefined" && this.timezone !== null && this.timezone !== "") { + var local_timezone = (new Date(this.inst.selectedYear, this.inst.selectedMonth, this.inst.selectedDay, 12)).getTimezoneOffset() * -1; + if (local_timezone === this.timezone) { + selectLocalTimezone(tp_inst); + } else { + this.timezone_select.val(this.timezone); + } + } else { + if (typeof(this.hour) !== "undefined" && this.hour !== null && this.hour !== "") { + this.timezone_select.val(o.timezone); + } else { + selectLocalTimezone(tp_inst); + } + } + this.timezone_select.change(function () { + tp_inst._onTimeChange(); + tp_inst._onSelectHandler(); + tp_inst._afterInject(); + }); + // End timezone options + + // inject timepicker into datepicker + var $buttonPanel = $dp.find('.ui-datepicker-buttonpane'); + if ($buttonPanel.length) { + $buttonPanel.before($tp); + } else { + $dp.append($tp); + } + + this.$timeObj = $tp.find('.ui_tpicker_time_input'); + this.$timeObj.change(function () { + var timeFormat = tp_inst.inst.settings.timeFormat; + var parsedTime = $.datepicker.parseTime(timeFormat, this.value); + var update = new Date(); + if (parsedTime) { + update.setHours(parsedTime.hour); + update.setMinutes(parsedTime.minute); + update.setSeconds(parsedTime.second); + $.datepicker._setTime(tp_inst.inst, update); + } else { + this.value = tp_inst.formattedTime; + this.blur(); + } + }); + + if (this.inst !== null) { + var timeDefined = this.timeDefined; + this._onTimeChange(); + this.timeDefined = timeDefined; + } + + // slideAccess integration: http://trentrichardson.com/2011/11/11/jquery-ui-sliders-and-touch-accessibility/ + if (this._defaults.addSliderAccess) { + var sliderAccessArgs = this._defaults.sliderAccessArgs, + rtl = this._defaults.isRTL; + sliderAccessArgs.isRTL = rtl; + + setTimeout(function () { // fix for inline mode + if ($tp.find('.ui-slider-access').length === 0) { + $tp.find('.ui-slider:visible').sliderAccess(sliderAccessArgs); + + // fix any grids since sliders are shorter + var sliderAccessWidth = $tp.find('.ui-slider-access:eq(0)').outerWidth(true); + if (sliderAccessWidth) { + $tp.find('table:visible').each(function () { + var $g = $(this), + oldWidth = $g.outerWidth(), + oldMarginLeft = $g.css(rtl ? 'marginRight' : 'marginLeft').toString().replace('%', ''), + newWidth = oldWidth - sliderAccessWidth, + newMarginLeft = ((oldMarginLeft * newWidth) / oldWidth) + '%', + css = { width: newWidth, marginRight: 0, marginLeft: 0 }; + css[rtl ? 'marginRight' : 'marginLeft'] = newMarginLeft; + $g.css(css); + }); + } + } + }, 10); + } + // end slideAccess integration + + tp_inst._limitMinMaxDateTime(this.inst, true); + } + }, + + /* + * This function tries to limit the ability to go outside the + * min/max date range + */ + _limitMinMaxDateTime: function (dp_inst, adjustSliders) { + var o = this._defaults, + dp_date = new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay); + + if (!this._defaults.showTimepicker) { + return; + } // No time so nothing to check here + + if ($.datepicker._get(dp_inst, 'minDateTime') !== null && $.datepicker._get(dp_inst, 'minDateTime') !== undefined && dp_date) { + var minDateTime = $.datepicker._get(dp_inst, 'minDateTime'), + minDateTimeDate = new Date(minDateTime.getFullYear(), minDateTime.getMonth(), minDateTime.getDate(), 0, 0, 0, 0); + + if (this.hourMinOriginal === null || this.minuteMinOriginal === null || this.secondMinOriginal === null || this.millisecMinOriginal === null || this.microsecMinOriginal === null) { + this.hourMinOriginal = o.hourMin; + this.minuteMinOriginal = o.minuteMin; + this.secondMinOriginal = o.secondMin; + this.millisecMinOriginal = o.millisecMin; + this.microsecMinOriginal = o.microsecMin; + } + + if (dp_inst.settings.timeOnly || minDateTimeDate.getTime() === dp_date.getTime()) { + this._defaults.hourMin = minDateTime.getHours(); + if (this.hour <= this._defaults.hourMin) { + this.hour = this._defaults.hourMin; + this._defaults.minuteMin = minDateTime.getMinutes(); + if (this.minute <= this._defaults.minuteMin) { + this.minute = this._defaults.minuteMin; + this._defaults.secondMin = minDateTime.getSeconds(); + if (this.second <= this._defaults.secondMin) { + this.second = this._defaults.secondMin; + this._defaults.millisecMin = minDateTime.getMilliseconds(); + if (this.millisec <= this._defaults.millisecMin) { + this.millisec = this._defaults.millisecMin; + this._defaults.microsecMin = minDateTime.getMicroseconds(); + } else { + if (this.microsec < this._defaults.microsecMin) { + this.microsec = this._defaults.microsecMin; + } + this._defaults.microsecMin = this.microsecMinOriginal; + } + } else { + this._defaults.millisecMin = this.millisecMinOriginal; + this._defaults.microsecMin = this.microsecMinOriginal; + } + } else { + this._defaults.secondMin = this.secondMinOriginal; + this._defaults.millisecMin = this.millisecMinOriginal; + this._defaults.microsecMin = this.microsecMinOriginal; + } + } else { + this._defaults.minuteMin = this.minuteMinOriginal; + this._defaults.secondMin = this.secondMinOriginal; + this._defaults.millisecMin = this.millisecMinOriginal; + this._defaults.microsecMin = this.microsecMinOriginal; + } + } else { + this._defaults.hourMin = this.hourMinOriginal; + this._defaults.minuteMin = this.minuteMinOriginal; + this._defaults.secondMin = this.secondMinOriginal; + this._defaults.millisecMin = this.millisecMinOriginal; + this._defaults.microsecMin = this.microsecMinOriginal; + } + } + + if ($.datepicker._get(dp_inst, 'maxDateTime') !== null && $.datepicker._get(dp_inst, 'maxDateTime') !== undefined && dp_date) { + var maxDateTime = $.datepicker._get(dp_inst, 'maxDateTime'), + maxDateTimeDate = new Date(maxDateTime.getFullYear(), maxDateTime.getMonth(), maxDateTime.getDate(), 0, 0, 0, 0); + + if (this.hourMaxOriginal === null || this.minuteMaxOriginal === null || this.secondMaxOriginal === null || this.millisecMaxOriginal === null) { + this.hourMaxOriginal = o.hourMax; + this.minuteMaxOriginal = o.minuteMax; + this.secondMaxOriginal = o.secondMax; + this.millisecMaxOriginal = o.millisecMax; + this.microsecMaxOriginal = o.microsecMax; + } + + if (dp_inst.settings.timeOnly || maxDateTimeDate.getTime() === dp_date.getTime()) { + this._defaults.hourMax = maxDateTime.getHours(); + if (this.hour >= this._defaults.hourMax) { + this.hour = this._defaults.hourMax; + this._defaults.minuteMax = maxDateTime.getMinutes(); + if (this.minute >= this._defaults.minuteMax) { + this.minute = this._defaults.minuteMax; + this._defaults.secondMax = maxDateTime.getSeconds(); + if (this.second >= this._defaults.secondMax) { + this.second = this._defaults.secondMax; + this._defaults.millisecMax = maxDateTime.getMilliseconds(); + if (this.millisec >= this._defaults.millisecMax) { + this.millisec = this._defaults.millisecMax; + this._defaults.microsecMax = maxDateTime.getMicroseconds(); + } else { + if (this.microsec > this._defaults.microsecMax) { + this.microsec = this._defaults.microsecMax; + } + this._defaults.microsecMax = this.microsecMaxOriginal; + } + } else { + this._defaults.millisecMax = this.millisecMaxOriginal; + this._defaults.microsecMax = this.microsecMaxOriginal; + } + } else { + this._defaults.secondMax = this.secondMaxOriginal; + this._defaults.millisecMax = this.millisecMaxOriginal; + this._defaults.microsecMax = this.microsecMaxOriginal; + } + } else { + this._defaults.minuteMax = this.minuteMaxOriginal; + this._defaults.secondMax = this.secondMaxOriginal; + this._defaults.millisecMax = this.millisecMaxOriginal; + this._defaults.microsecMax = this.microsecMaxOriginal; + } + } else { + this._defaults.hourMax = this.hourMaxOriginal; + this._defaults.minuteMax = this.minuteMaxOriginal; + this._defaults.secondMax = this.secondMaxOriginal; + this._defaults.millisecMax = this.millisecMaxOriginal; + this._defaults.microsecMax = this.microsecMaxOriginal; + } + } + + if (dp_inst.settings.minTime!==null) { + var tempMinTime=new Date("01/01/1970 " + dp_inst.settings.minTime); + if (this.hour<tempMinTime.getHours()) { + this.hour=this._defaults.hourMin=tempMinTime.getHours(); + this.minute=this._defaults.minuteMin=tempMinTime.getMinutes(); + } else if (this.hour===tempMinTime.getHours() && this.minute<tempMinTime.getMinutes()) { + this.minute=this._defaults.minuteMin=tempMinTime.getMinutes(); + } else { + if (this._defaults.hourMin<tempMinTime.getHours()) { + this._defaults.hourMin=tempMinTime.getHours(); + this._defaults.minuteMin=tempMinTime.getMinutes(); + } else if (this._defaults.hourMin===tempMinTime.getHours()===this.hour && this._defaults.minuteMin<tempMinTime.getMinutes()) { + this._defaults.minuteMin=tempMinTime.getMinutes(); + } else { + this._defaults.minuteMin=0; + } + } + } + + if (dp_inst.settings.maxTime!==null) { + var tempMaxTime=new Date("01/01/1970 " + dp_inst.settings.maxTime); + if (this.hour>tempMaxTime.getHours()) { + this.hour=this._defaults.hourMax=tempMaxTime.getHours(); + this.minute=this._defaults.minuteMax=tempMaxTime.getMinutes(); + } else if (this.hour===tempMaxTime.getHours() && this.minute>tempMaxTime.getMinutes()) { + this.minute=this._defaults.minuteMax=tempMaxTime.getMinutes(); + } else { + if (this._defaults.hourMax>tempMaxTime.getHours()) { + this._defaults.hourMax=tempMaxTime.getHours(); + this._defaults.minuteMax=tempMaxTime.getMinutes(); + } else if (this._defaults.hourMax===tempMaxTime.getHours()===this.hour && this._defaults.minuteMax>tempMaxTime.getMinutes()) { + this._defaults.minuteMax=tempMaxTime.getMinutes(); + } else { + this._defaults.minuteMax=59; + } + } + } + + if (adjustSliders !== undefined && adjustSliders === true) { + var hourMax = parseInt((this._defaults.hourMax - ((this._defaults.hourMax - this._defaults.hourMin) % this._defaults.stepHour)), 10), + minMax = parseInt((this._defaults.minuteMax - ((this._defaults.minuteMax - this._defaults.minuteMin) % this._defaults.stepMinute)), 10), + secMax = parseInt((this._defaults.secondMax - ((this._defaults.secondMax - this._defaults.secondMin) % this._defaults.stepSecond)), 10), + millisecMax = parseInt((this._defaults.millisecMax - ((this._defaults.millisecMax - this._defaults.millisecMin) % this._defaults.stepMillisec)), 10), + microsecMax = parseInt((this._defaults.microsecMax - ((this._defaults.microsecMax - this._defaults.microsecMin) % this._defaults.stepMicrosec)), 10); + + if (this.hour_slider) { + this.control.options(this, this.hour_slider, 'hour', { min: this._defaults.hourMin, max: hourMax, step: this._defaults.stepHour }); + this.control.value(this, this.hour_slider, 'hour', this.hour - (this.hour % this._defaults.stepHour)); + } + if (this.minute_slider) { + this.control.options(this, this.minute_slider, 'minute', { min: this._defaults.minuteMin, max: minMax, step: this._defaults.stepMinute }); + this.control.value(this, this.minute_slider, 'minute', this.minute - (this.minute % this._defaults.stepMinute)); + } + if (this.second_slider) { + this.control.options(this, this.second_slider, 'second', { min: this._defaults.secondMin, max: secMax, step: this._defaults.stepSecond }); + this.control.value(this, this.second_slider, 'second', this.second - (this.second % this._defaults.stepSecond)); + } + if (this.millisec_slider) { + this.control.options(this, this.millisec_slider, 'millisec', { min: this._defaults.millisecMin, max: millisecMax, step: this._defaults.stepMillisec }); + this.control.value(this, this.millisec_slider, 'millisec', this.millisec - (this.millisec % this._defaults.stepMillisec)); + } + if (this.microsec_slider) { + this.control.options(this, this.microsec_slider, 'microsec', { min: this._defaults.microsecMin, max: microsecMax, step: this._defaults.stepMicrosec }); + this.control.value(this, this.microsec_slider, 'microsec', this.microsec - (this.microsec % this._defaults.stepMicrosec)); + } + } + + }, + + /* + * when a slider moves, set the internal time... + * on time change is also called when the time is updated in the text field + */ + _onTimeChange: function () { + if (!this._defaults.showTimepicker) { + return; + } + var hour = (this.hour_slider) ? this.control.value(this, this.hour_slider, 'hour') : false, + minute = (this.minute_slider) ? this.control.value(this, this.minute_slider, 'minute') : false, + second = (this.second_slider) ? this.control.value(this, this.second_slider, 'second') : false, + millisec = (this.millisec_slider) ? this.control.value(this, this.millisec_slider, 'millisec') : false, + microsec = (this.microsec_slider) ? this.control.value(this, this.microsec_slider, 'microsec') : false, + timezone = (this.timezone_select) ? this.timezone_select.val() : false, + o = this._defaults, + pickerTimeFormat = o.pickerTimeFormat || o.timeFormat, + pickerTimeSuffix = o.pickerTimeSuffix || o.timeSuffix; + + if (typeof(hour) === 'object') { + hour = false; + } + if (typeof(minute) === 'object') { + minute = false; + } + if (typeof(second) === 'object') { + second = false; + } + if (typeof(millisec) === 'object') { + millisec = false; + } + if (typeof(microsec) === 'object') { + microsec = false; + } + if (typeof(timezone) === 'object') { + timezone = false; + } + + if (hour !== false) { + hour = parseInt(hour, 10); + } + if (minute !== false) { + minute = parseInt(minute, 10); + } + if (second !== false) { + second = parseInt(second, 10); + } + if (millisec !== false) { + millisec = parseInt(millisec, 10); + } + if (microsec !== false) { + microsec = parseInt(microsec, 10); + } + if (timezone !== false) { + timezone = timezone.toString(); + } + + var ampm = o[hour < 12 ? 'amNames' : 'pmNames'][0]; + + // If the update was done in the input field, the input field should not be updated. + // If the update was done using the sliders, update the input field. + var hasChanged = ( + hour !== parseInt(this.hour,10) || // sliders should all be numeric + minute !== parseInt(this.minute,10) || + second !== parseInt(this.second,10) || + millisec !== parseInt(this.millisec,10) || + microsec !== parseInt(this.microsec,10) || + (this.ampm.length > 0 && (hour < 12) !== ($.inArray(this.ampm.toUpperCase(), this.amNames) !== -1)) || + (this.timezone !== null && timezone !== this.timezone.toString()) // could be numeric or "EST" format, so use toString() + ); + + if (hasChanged) { + + if (hour !== false) { + this.hour = hour; + } + if (minute !== false) { + this.minute = minute; + } + if (second !== false) { + this.second = second; + } + if (millisec !== false) { + this.millisec = millisec; + } + if (microsec !== false) { + this.microsec = microsec; + } + if (timezone !== false) { + this.timezone = timezone; + } + + if (!this.inst) { + this.inst = $.datepicker._getInst(this.$input[0]); + } + + this._limitMinMaxDateTime(this.inst, true); + } + if (this.support.ampm) { + this.ampm = ampm; + } + + // Updates the time within the timepicker + this.formattedTime = $.datepicker.formatTime(o.timeFormat, this, o); + if (this.$timeObj) { + if (pickerTimeFormat === o.timeFormat) { + this.$timeObj.val(this.formattedTime + pickerTimeSuffix); + } + else { + this.$timeObj.val($.datepicker.formatTime(pickerTimeFormat, this, o) + pickerTimeSuffix); + } + if (this.$timeObj[0].setSelectionRange) { + var sPos = this.$timeObj[0].selectionStart; + var ePos = this.$timeObj[0].selectionEnd; + this.$timeObj[0].setSelectionRange(sPos, ePos); + } + } + + this.timeDefined = true; + if (hasChanged) { + this._updateDateTime(); + //this.$input.focus(); // may automatically open the picker on setDate + } + }, + + /* + * call custom onSelect. + * bind to sliders slidestop, and grid click. + */ + _onSelectHandler: function () { + var onSelect = this._defaults.onSelect || this.inst.settings.onSelect; + var inputEl = this.$input ? this.$input[0] : null; + if (onSelect && inputEl) { + onSelect.apply(inputEl, [this.formattedDateTime, this]); + } + }, + + /* + * update our input with the new date time.. + */ + _updateDateTime: function (dp_inst) { + dp_inst = this.inst || dp_inst; + var dtTmp = (dp_inst.currentYear > 0? + new Date(dp_inst.currentYear, dp_inst.currentMonth, dp_inst.currentDay) : + new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay)), + dt = $.datepicker._daylightSavingAdjust(dtTmp), + //dt = $.datepicker._daylightSavingAdjust(new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay)), + //dt = $.datepicker._daylightSavingAdjust(new Date(dp_inst.currentYear, dp_inst.currentMonth, dp_inst.currentDay)), + dateFmt = $.datepicker._get(dp_inst, 'dateFormat'), + formatCfg = $.datepicker._getFormatConfig(dp_inst), + timeAvailable = dt !== null && this.timeDefined; + this.formattedDate = $.datepicker.formatDate(dateFmt, (dt === null ? new Date() : dt), formatCfg); + var formattedDateTime = this.formattedDate; + + // if a slider was changed but datepicker doesn't have a value yet, set it + if (dp_inst.lastVal === "") { + dp_inst.currentYear = dp_inst.selectedYear; + dp_inst.currentMonth = dp_inst.selectedMonth; + dp_inst.currentDay = dp_inst.selectedDay; + } + + /* + * remove following lines to force every changes in date picker to change the input value + * Bug descriptions: when an input field has a default value, and click on the field to pop up the date picker. + * If the user manually empty the value in the input field, the date picker will never change selected value. + */ + //if (dp_inst.lastVal !== undefined && (dp_inst.lastVal.length > 0 && this.$input.val().length === 0)) { + // return; + //} + + if (this._defaults.timeOnly === true && this._defaults.timeOnlyShowDate === false) { + formattedDateTime = this.formattedTime; + } else if ((this._defaults.timeOnly !== true && (this._defaults.alwaysSetTime || timeAvailable)) || (this._defaults.timeOnly === true && this._defaults.timeOnlyShowDate === true)) { + formattedDateTime += this._defaults.separator + this.formattedTime + this._defaults.timeSuffix; + } + + this.formattedDateTime = formattedDateTime; + + if (!this._defaults.showTimepicker) { + this.$input.val(this.formattedDate); + } else if (this.$altInput && this._defaults.timeOnly === false && this._defaults.altFieldTimeOnly === true) { + this.$altInput.val(this.formattedTime); + this.$input.val(this.formattedDate); + } else if (this.$altInput) { + this.$input.val(formattedDateTime); + var altFormattedDateTime = '', + altSeparator = this._defaults.altSeparator !== null ? this._defaults.altSeparator : this._defaults.separator, + altTimeSuffix = this._defaults.altTimeSuffix !== null ? this._defaults.altTimeSuffix : this._defaults.timeSuffix; + + if (!this._defaults.timeOnly) { + if (this._defaults.altFormat) { + altFormattedDateTime = $.datepicker.formatDate(this._defaults.altFormat, (dt === null ? new Date() : dt), formatCfg); + } + else { + altFormattedDateTime = this.formattedDate; + } + + if (altFormattedDateTime) { + altFormattedDateTime += altSeparator; + } + } + + if (this._defaults.altTimeFormat !== null) { + altFormattedDateTime += $.datepicker.formatTime(this._defaults.altTimeFormat, this, this._defaults) + altTimeSuffix; + } + else { + altFormattedDateTime += this.formattedTime + altTimeSuffix; + } + this.$altInput.val(altFormattedDateTime); + } else { + this.$input.val(formattedDateTime); + } + + this.$input.trigger("change"); + }, + + _onFocus: function () { + if (!this.$input.val() && this._defaults.defaultValue) { + this.$input.val(this._defaults.defaultValue); + var inst = $.datepicker._getInst(this.$input.get(0)), + tp_inst = $.datepicker._get(inst, 'timepicker'); + if (tp_inst) { + if (tp_inst._defaults.timeOnly && (inst.input.val() !== inst.lastVal)) { + try { + $.datepicker._updateDatepicker(inst); + } catch (err) { + $.timepicker.log(err); + } + } + } + } + }, + + /* + * Small abstraction to control types + * We can add more, just be sure to follow the pattern: create, options, value + */ + _controls: { + // slider methods + slider: { + create: function (tp_inst, obj, unit, val, min, max, step) { + var rtl = tp_inst._defaults.isRTL; // if rtl go -60->0 instead of 0->60 + return obj.prop('slide', null).slider({ + orientation: "horizontal", + value: rtl ? val * -1 : val, + min: rtl ? max * -1 : min, + max: rtl ? min * -1 : max, + step: step, + slide: function (event, ui) { + tp_inst.control.value(tp_inst, $(this), unit, rtl ? ui.value * -1 : ui.value); + tp_inst._onTimeChange(); + }, + stop: function (event, ui) { + tp_inst._onSelectHandler(); + } + }); + }, + options: function (tp_inst, obj, unit, opts, val) { + if (tp_inst._defaults.isRTL) { + if (typeof(opts) === 'string') { + if (opts === 'min' || opts === 'max') { + if (val !== undefined) { + return obj.slider(opts, val * -1); + } + return Math.abs(obj.slider(opts)); + } + return obj.slider(opts); + } + var min = opts.min, + max = opts.max; + opts.min = opts.max = null; + if (min !== undefined) { + opts.max = min * -1; + } + if (max !== undefined) { + opts.min = max * -1; + } + return obj.slider(opts); + } + if (typeof(opts) === 'string' && val !== undefined) { + return obj.slider(opts, val); + } + return obj.slider(opts); + }, + value: function (tp_inst, obj, unit, val) { + if (tp_inst._defaults.isRTL) { + if (val !== undefined) { + return obj.slider('value', val * -1); + } + return Math.abs(obj.slider('value')); + } + if (val !== undefined) { + return obj.slider('value', val); + } + return obj.slider('value'); + } + }, + // select methods + select: { + create: function (tp_inst, obj, unit, val, min, max, step) { + var sel = '<select class="ui-timepicker-select ui-state-default ui-corner-all" data-unit="' + unit + '" data-min="' + min + '" data-max="' + max + '" data-step="' + step + '">', + format = tp_inst._defaults.pickerTimeFormat || tp_inst._defaults.timeFormat; + + for (var i = min; i <= max; i += step) { + sel += '<option value="' + i + '"' + (i === val ? ' selected' : '') + '>'; + if (unit === 'hour') { + sel += $.datepicker.formatTime($.trim(format.replace(/[^ht ]/ig, '')), {hour: i}, tp_inst._defaults); + } + else if (unit === 'millisec' || unit === 'microsec' || i >= 10) { sel += i; } + else {sel += '0' + i.toString(); } + sel += '</option>'; + } + sel += '</select>'; + + obj.children('select').remove(); + + $(sel).appendTo(obj).change(function (e) { + tp_inst._onTimeChange(); + tp_inst._onSelectHandler(); + tp_inst._afterInject(); + }); + + return obj; + }, + options: function (tp_inst, obj, unit, opts, val) { + var o = {}, + $t = obj.children('select'); + if (typeof(opts) === 'string') { + if (val === undefined) { + return $t.data(opts); + } + o[opts] = val; + } + else { o = opts; } + return tp_inst.control.create(tp_inst, obj, $t.data('unit'), $t.val(), o.min>=0 ? o.min : $t.data('min'), o.max || $t.data('max'), o.step || $t.data('step')); + }, + value: function (tp_inst, obj, unit, val) { + var $t = obj.children('select'); + if (val !== undefined) { + return $t.val(val); + } + return $t.val(); + } + } + } // end _controls + + }); + + $.fn.extend({ + /* + * shorthand just to use timepicker. + */ + timepicker: function (o) { + o = o || {}; + var tmp_args = Array.prototype.slice.call(arguments); + + if (typeof o === 'object') { + tmp_args[0] = $.extend(o, { + timeOnly: true + }); + } + + return $(this).each(function () { + $.fn.datetimepicker.apply($(this), tmp_args); + }); + }, + + /* + * extend timepicker to datepicker + */ + datetimepicker: function (o) { + o = o || {}; + var tmp_args = arguments; + + if (typeof(o) === 'string') { + if (o === 'getDate' || (o === 'option' && tmp_args.length === 2 && typeof (tmp_args[1]) === 'string')) { + return $.fn.datepicker.apply($(this[0]), tmp_args); + } else { + return this.each(function () { + var $t = $(this); + $t.datepicker.apply($t, tmp_args); + }); + } + } else { + return this.each(function () { + var $t = $(this); + $t.datepicker($.timepicker._newInst($t, o)._defaults); + }); + } + } + }); + + /* + * Public Utility to parse date and time + */ + $.datepicker.parseDateTime = function (dateFormat, timeFormat, dateTimeString, dateSettings, timeSettings) { + var parseRes = parseDateTimeInternal(dateFormat, timeFormat, dateTimeString, dateSettings, timeSettings); + if (parseRes.timeObj) { + var t = parseRes.timeObj; + parseRes.date.setHours(t.hour, t.minute, t.second, t.millisec); + parseRes.date.setMicroseconds(t.microsec); + } + + return parseRes.date; + }; + + /* + * Public utility to parse time + */ + $.datepicker.parseTime = function (timeFormat, timeString, options) { + var o = extendRemove(extendRemove({}, $.timepicker._defaults), options || {}), + iso8601 = (timeFormat.replace(/\'.*?\'/g, '').indexOf('Z') !== -1); + + // Strict parse requires the timeString to match the timeFormat exactly + var strictParse = function (f, s, o) { + + // pattern for standard and localized AM/PM markers + var getPatternAmpm = function (amNames, pmNames) { + var markers = []; + if (amNames) { + $.merge(markers, amNames); + } + if (pmNames) { + $.merge(markers, pmNames); + } + markers = $.map(markers, function (val) { + return val.replace(/[.*+?|()\[\]{}\\]/g, '\\$&'); + }); + return '(' + markers.join('|') + ')?'; + }; + + // figure out position of time elements.. cause js cant do named captures + var getFormatPositions = function (timeFormat) { + var finds = timeFormat.toLowerCase().match(/(h{1,2}|m{1,2}|s{1,2}|l{1}|c{1}|t{1,2}|z|'.*?')/g), + orders = { + h: -1, + m: -1, + s: -1, + l: -1, + c: -1, + t: -1, + z: -1 + }; + + if (finds) { + for (var i = 0; i < finds.length; i++) { + if (orders[finds[i].toString().charAt(0)] === -1) { + orders[finds[i].toString().charAt(0)] = i + 1; + } + } + } + return orders; + }; + + var regstr = '^' + f.toString() + .replace(/([hH]{1,2}|mm?|ss?|[tT]{1,2}|[zZ]|[lc]|'.*?')/g, function (match) { + var ml = match.length; + switch (match.charAt(0).toLowerCase()) { + case 'h': + return ml === 1 ? '(\\d?\\d)' : '(\\d{' + ml + '})'; + case 'm': + return ml === 1 ? '(\\d?\\d)' : '(\\d{' + ml + '})'; + case 's': + return ml === 1 ? '(\\d?\\d)' : '(\\d{' + ml + '})'; + case 'l': + return '(\\d?\\d?\\d)'; + case 'c': + return '(\\d?\\d?\\d)'; + case 'z': + return '(z|[-+]\\d\\d:?\\d\\d|\\S+)?'; + case 't': + return getPatternAmpm(o.amNames, o.pmNames); + default: // literal escaped in quotes + return '(' + match.replace(/\'/g, "").replace(/(\.|\$|\^|\\|\/|\(|\)|\[|\]|\?|\+|\*)/g, function (m) { return "\\" + m; }) + ')?'; + } + }) + .replace(/\s/g, '\\s?') + + o.timeSuffix + '$', + order = getFormatPositions(f), + ampm = '', + treg; + + treg = s.match(new RegExp(regstr, 'i')); + + var resTime = { + hour: 0, + minute: 0, + second: 0, + millisec: 0, + microsec: 0 + }; + + if (treg) { + if (order.t !== -1) { + if (treg[order.t] === undefined || treg[order.t].length === 0) { + ampm = ''; + resTime.ampm = ''; + } else { + ampm = $.inArray(treg[order.t].toUpperCase(), $.map(o.amNames, function (x,i) { return x.toUpperCase(); })) !== -1 ? 'AM' : 'PM'; + resTime.ampm = o[ampm === 'AM' ? 'amNames' : 'pmNames'][0]; + } + } + + if (order.h !== -1) { + if (ampm === 'AM' && treg[order.h] === '12') { + resTime.hour = 0; // 12am = 0 hour + } else { + if (ampm === 'PM' && treg[order.h] !== '12') { + resTime.hour = parseInt(treg[order.h], 10) + 12; // 12pm = 12 hour, any other pm = hour + 12 + } else { + resTime.hour = Number(treg[order.h]); + } + } + } + + if (order.m !== -1) { + resTime.minute = Number(treg[order.m]); + } + if (order.s !== -1) { + resTime.second = Number(treg[order.s]); + } + if (order.l !== -1) { + resTime.millisec = Number(treg[order.l]); + } + if (order.c !== -1) { + resTime.microsec = Number(treg[order.c]); + } + if (order.z !== -1 && treg[order.z] !== undefined) { + resTime.timezone = $.timepicker.timezoneOffsetNumber(treg[order.z]); + } + + + return resTime; + } + return false; + };// end strictParse + + // First try JS Date, if that fails, use strictParse + var looseParse = function (f, s, o) { + try { + var d = new Date('2012-01-01 ' + s); + if (isNaN(d.getTime())) { + d = new Date('2012-01-01T' + s); + if (isNaN(d.getTime())) { + d = new Date('01/01/2012 ' + s); + if (isNaN(d.getTime())) { + throw "Unable to parse time with native Date: " + s; + } + } + } + + return { + hour: d.getHours(), + minute: d.getMinutes(), + second: d.getSeconds(), + millisec: d.getMilliseconds(), + microsec: d.getMicroseconds(), + timezone: d.getTimezoneOffset() * -1 + }; + } + catch (err) { + try { + return strictParse(f, s, o); + } + catch (err2) { + $.timepicker.log("Unable to parse \ntimeString: " + s + "\ntimeFormat: " + f); + } + } + return false; + }; // end looseParse + + if (typeof o.parse === "function") { + return o.parse(timeFormat, timeString, o); + } + if (o.parse === 'loose') { + return looseParse(timeFormat, timeString, o); + } + return strictParse(timeFormat, timeString, o); + }; + + /** + * Public utility to format the time + * @param {string} format format of the time + * @param {Object} time Object not a Date for timezones + * @param {Object} [options] essentially the regional[].. amNames, pmNames, ampm + * @returns {string} the formatted time + */ + $.datepicker.formatTime = function (format, time, options) { + options = options || {}; + options = $.extend({}, $.timepicker._defaults, options); + time = $.extend({ + hour: 0, + minute: 0, + second: 0, + millisec: 0, + microsec: 0, + timezone: null + }, time); + + var tmptime = format, + ampmName = options.amNames[0], + hour = parseInt(time.hour, 10); + + if (hour > 11) { + ampmName = options.pmNames[0]; + } + + tmptime = tmptime.replace(/(?:HH?|hh?|mm?|ss?|[tT]{1,2}|[zZ]|[lc]|'.*?')/g, function (match) { + switch (match) { + case 'HH': + return ('0' + hour).slice(-2); + case 'H': + return hour; + case 'hh': + return ('0' + convert24to12(hour)).slice(-2); + case 'h': + return convert24to12(hour); + case 'mm': + return ('0' + time.minute).slice(-2); + case 'm': + return time.minute; + case 'ss': + return ('0' + time.second).slice(-2); + case 's': + return time.second; + case 'l': + return ('00' + time.millisec).slice(-3); + case 'c': + return ('00' + time.microsec).slice(-3); + case 'z': + return $.timepicker.timezoneOffsetString(time.timezone === null ? options.timezone : time.timezone, false); + case 'Z': + return $.timepicker.timezoneOffsetString(time.timezone === null ? options.timezone : time.timezone, true); + case 'T': + return ampmName.charAt(0).toUpperCase(); + case 'TT': + return ampmName.toUpperCase(); + case 't': + return ampmName.charAt(0).toLowerCase(); + case 'tt': + return ampmName.toLowerCase(); + default: + return match.replace(/'/g, ""); + } + }); + + return tmptime; + }; + + /* + * the bad hack :/ override datepicker so it doesn't close on select + // inspired: http://stackoverflow.com/questions/1252512/jquery-datepicker-prevent-closing-picker-when-clicking-a-date/1762378#1762378 + */ + $.datepicker._base_selectDate = $.datepicker._selectDate; + $.datepicker._selectDate = function (id, dateStr) { + var inst = this._getInst($(id)[0]), + tp_inst = this._get(inst, 'timepicker'), + was_inline; + + if (tp_inst && inst.settings.showTimepicker) { + tp_inst._limitMinMaxDateTime(inst, true); + was_inline = inst.inline; + inst.inline = inst.stay_open = true; + //This way the onSelect handler called from calendarpicker get the full dateTime + this._base_selectDate(id, dateStr); + inst.inline = was_inline; + inst.stay_open = false; + this._notifyChange(inst); + this._updateDatepicker(inst); + } else { + this._base_selectDate(id, dateStr); + } + }; + + /* + * second bad hack :/ override datepicker so it triggers an event when changing the input field + * and does not redraw the datepicker on every selectDate event + */ + $.datepicker._base_updateDatepicker = $.datepicker._updateDatepicker; + $.datepicker._updateDatepicker = function (inst) { + + // don't popup the datepicker if there is another instance already opened + var input = inst.input[0]; + if ($.datepicker._curInst && $.datepicker._curInst !== inst && $.datepicker._datepickerShowing && $.datepicker._lastInput !== input) { + return; + } + + if (typeof(inst.stay_open) !== 'boolean' || inst.stay_open === false) { + + this._base_updateDatepicker(inst); + + // Reload the time control when changing something in the input text field. + var tp_inst = this._get(inst, 'timepicker'); + if (tp_inst) { + tp_inst._addTimePicker(inst); + } + } + }; + + /* + * third bad hack :/ override datepicker so it allows spaces and colon in the input field + */ + $.datepicker._base_doKeyPress = $.datepicker._doKeyPress; + $.datepicker._doKeyPress = function (event) { + var inst = $.datepicker._getInst(event.target), + tp_inst = $.datepicker._get(inst, 'timepicker'); + + if (tp_inst) { + if ($.datepicker._get(inst, 'constrainInput')) { + var ampm = tp_inst.support.ampm, + tz = tp_inst._defaults.showTimezone !== null ? tp_inst._defaults.showTimezone : tp_inst.support.timezone, + dateChars = $.datepicker._possibleChars($.datepicker._get(inst, 'dateFormat')), + datetimeChars = tp_inst._defaults.timeFormat.toString() + .replace(/[hms]/g, '') + .replace(/TT/g, ampm ? 'APM' : '') + .replace(/Tt/g, ampm ? 'AaPpMm' : '') + .replace(/tT/g, ampm ? 'AaPpMm' : '') + .replace(/T/g, ampm ? 'AP' : '') + .replace(/tt/g, ampm ? 'apm' : '') + .replace(/t/g, ampm ? 'ap' : '') + + " " + tp_inst._defaults.separator + + tp_inst._defaults.timeSuffix + + (tz ? tp_inst._defaults.timezoneList.join('') : '') + + (tp_inst._defaults.amNames.join('')) + (tp_inst._defaults.pmNames.join('')) + + dateChars, + chr = String.fromCharCode(event.charCode === undefined ? event.keyCode : event.charCode); + return event.ctrlKey || (chr < ' ' || !dateChars || datetimeChars.indexOf(chr) > -1); + } + } + + return $.datepicker._base_doKeyPress(event); + }; + + /* + * Fourth bad hack :/ override _updateAlternate function used in inline mode to init altField + * Update any alternate field to synchronise with the main field. + */ + $.datepicker._base_updateAlternate = $.datepicker._updateAlternate; + $.datepicker._updateAlternate = function (inst) { + var tp_inst = this._get(inst, 'timepicker'); + if (tp_inst) { + var altField = tp_inst._defaults.altField; + if (altField) { // update alternate field too + var altFormat = tp_inst._defaults.altFormat || tp_inst._defaults.dateFormat, + date = this._getDate(inst), + formatCfg = $.datepicker._getFormatConfig(inst), + altFormattedDateTime = '', + altSeparator = tp_inst._defaults.altSeparator ? tp_inst._defaults.altSeparator : tp_inst._defaults.separator, + altTimeSuffix = tp_inst._defaults.altTimeSuffix ? tp_inst._defaults.altTimeSuffix : tp_inst._defaults.timeSuffix, + altTimeFormat = tp_inst._defaults.altTimeFormat !== null ? tp_inst._defaults.altTimeFormat : tp_inst._defaults.timeFormat; + + altFormattedDateTime += $.datepicker.formatTime(altTimeFormat, tp_inst, tp_inst._defaults) + altTimeSuffix; + if (!tp_inst._defaults.timeOnly && !tp_inst._defaults.altFieldTimeOnly && date !== null) { + if (tp_inst._defaults.altFormat) { + altFormattedDateTime = $.datepicker.formatDate(tp_inst._defaults.altFormat, date, formatCfg) + altSeparator + altFormattedDateTime; + } + else { + altFormattedDateTime = tp_inst.formattedDate + altSeparator + altFormattedDateTime; + } + } + $(altField).val( inst.input.val() ? altFormattedDateTime : ""); + } + } + else { + $.datepicker._base_updateAlternate(inst); + } + }; + + /* + * Override key up event to sync manual input changes. + */ + $.datepicker._base_doKeyUp = $.datepicker._doKeyUp; + $.datepicker._doKeyUp = function (event) { + var inst = $.datepicker._getInst(event.target), + tp_inst = $.datepicker._get(inst, 'timepicker'); + + if (tp_inst) { + if (tp_inst._defaults.timeOnly && (inst.input.val() !== inst.lastVal)) { + try { + $.datepicker._updateDatepicker(inst); + } catch (err) { + $.timepicker.log(err); + } + } + } + + return $.datepicker._base_doKeyUp(event); + }; + + /* + * override "Today" button to also grab the time and set it to input field. + */ + $.datepicker._base_gotoToday = $.datepicker._gotoToday; + $.datepicker._gotoToday = function (id) { + var inst = this._getInst($(id)[0]); + this._base_gotoToday(id); + var tp_inst = this._get(inst, 'timepicker'); + if (!tp_inst) { + return; + } + + var tzoffset = $.timepicker.timezoneOffsetNumber(tp_inst.timezone); + var now = new Date(); + now.setMinutes(now.getMinutes() + now.getTimezoneOffset() + parseInt(tzoffset, 10)); + this._setTime(inst, now); + this._setDate(inst, now); + tp_inst._onSelectHandler(); + }; + + /* + * Disable & enable the Time in the datetimepicker + */ + $.datepicker._disableTimepickerDatepicker = function (target) { + var inst = this._getInst(target); + if (!inst) { + return; + } + + var tp_inst = this._get(inst, 'timepicker'); + $(target).datepicker('getDate'); // Init selected[Year|Month|Day] + if (tp_inst) { + inst.settings.showTimepicker = false; + tp_inst._defaults.showTimepicker = false; + tp_inst._updateDateTime(inst); + } + }; + + $.datepicker._enableTimepickerDatepicker = function (target) { + var inst = this._getInst(target); + if (!inst) { + return; + } + + var tp_inst = this._get(inst, 'timepicker'); + $(target).datepicker('getDate'); // Init selected[Year|Month|Day] + if (tp_inst) { + inst.settings.showTimepicker = true; + tp_inst._defaults.showTimepicker = true; + tp_inst._addTimePicker(inst); // Could be disabled on page load + tp_inst._updateDateTime(inst); + } + }; + + /* + * Create our own set time function + */ + $.datepicker._setTime = function (inst, date) { + var tp_inst = this._get(inst, 'timepicker'); + if (tp_inst) { + var defaults = tp_inst._defaults; + + // calling _setTime with no date sets time to defaults + tp_inst.hour = date ? date.getHours() : defaults.hour; + tp_inst.minute = date ? date.getMinutes() : defaults.minute; + tp_inst.second = date ? date.getSeconds() : defaults.second; + tp_inst.millisec = date ? date.getMilliseconds() : defaults.millisec; + tp_inst.microsec = date ? date.getMicroseconds() : defaults.microsec; + + //check if within min/max times.. + tp_inst._limitMinMaxDateTime(inst, true); + + tp_inst._onTimeChange(); + tp_inst._updateDateTime(inst); + } + }; + + /* + * Create new public method to set only time, callable as $().datepicker('setTime', date) + */ + $.datepicker._setTimeDatepicker = function (target, date, withDate) { + var inst = this._getInst(target); + if (!inst) { + return; + } + + var tp_inst = this._get(inst, 'timepicker'); + + if (tp_inst) { + this._setDateFromField(inst); + var tp_date; + if (date) { + if (typeof date === "string") { + tp_inst._parseTime(date, withDate); + tp_date = new Date(); + tp_date.setHours(tp_inst.hour, tp_inst.minute, tp_inst.second, tp_inst.millisec); + tp_date.setMicroseconds(tp_inst.microsec); + } else { + tp_date = new Date(date.getTime()); + tp_date.setMicroseconds(date.getMicroseconds()); + } + if (tp_date.toString() === 'Invalid Date') { + tp_date = undefined; + } + this._setTime(inst, tp_date); + } + } + + }; + + /* + * override setDate() to allow setting time too within Date object + */ + $.datepicker._base_setDateDatepicker = $.datepicker._setDateDatepicker; + $.datepicker._setDateDatepicker = function (target, _date) { + var inst = this._getInst(target); + var date = _date; + if (!inst) { + return; + } + + if (typeof(_date) === 'string') { + date = new Date(_date); + if (!date.getTime()) { + this._base_setDateDatepicker.apply(this, arguments); + date = $(target).datepicker('getDate'); + } + } + + var tp_inst = this._get(inst, 'timepicker'); + var tp_date; + if (date instanceof Date) { + tp_date = new Date(date.getTime()); + tp_date.setMicroseconds(date.getMicroseconds()); + } else { + tp_date = date; + } + + // This is important if you are using the timezone option, javascript's Date + // object will only return the timezone offset for the current locale, so we + // adjust it accordingly. If not using timezone option this won't matter.. + // If a timezone is different in tp, keep the timezone as is + if (tp_inst && tp_date) { + // look out for DST if tz wasn't specified + if (!tp_inst.support.timezone && tp_inst._defaults.timezone === null) { + tp_inst.timezone = tp_date.getTimezoneOffset() * -1; + } + date = $.timepicker.timezoneAdjust(date, $.timepicker.timezoneOffsetString(-date.getTimezoneOffset()), tp_inst.timezone); + tp_date = $.timepicker.timezoneAdjust(tp_date, $.timepicker.timezoneOffsetString(-tp_date.getTimezoneOffset()), tp_inst.timezone); + } + + this._updateDatepicker(inst); + this._base_setDateDatepicker.apply(this, arguments); + this._setTimeDatepicker(target, tp_date, true); + }; + + /* + * override getDate() to allow getting time too within Date object + */ + $.datepicker._base_getDateDatepicker = $.datepicker._getDateDatepicker; + $.datepicker._getDateDatepicker = function (target, noDefault) { + var inst = this._getInst(target); + if (!inst) { + return; + } + + var tp_inst = this._get(inst, 'timepicker'); + + if (tp_inst) { + // if it hasn't yet been defined, grab from field + if (inst.lastVal === undefined) { + this._setDateFromField(inst, noDefault); + } + + var date = this._getDate(inst); + + var currDT = null; + + if (tp_inst.$altInput && tp_inst._defaults.altFieldTimeOnly) { + currDT = tp_inst.$input.val() + ' ' + tp_inst.$altInput.val(); + } + else if (tp_inst.$input.get(0).tagName !== 'INPUT' && tp_inst.$altInput) { + /** + * in case the datetimepicker has been applied to a non-input tag for inline UI, + * and the user has not configured the plugin to display only time in altInput, + * pick current date time from the altInput (and hope for the best, for now, until "ER1" is applied) + * + * @todo ER1. Since altInput can have a totally difference format, convert it to standard format by reading input format from "altFormat" and "altTimeFormat" option values + */ + currDT = tp_inst.$altInput.val(); + } + else { + currDT = tp_inst.$input.val(); + } + + if (date && tp_inst._parseTime(currDT, !inst.settings.timeOnly)) { + date.setHours(tp_inst.hour, tp_inst.minute, tp_inst.second, tp_inst.millisec); + date.setMicroseconds(tp_inst.microsec); + + // This is important if you are using the timezone option, javascript's Date + // object will only return the timezone offset for the current locale, so we + // adjust it accordingly. If not using timezone option this won't matter.. + if (tp_inst.timezone != null) { + // look out for DST if tz wasn't specified + if (!tp_inst.support.timezone && tp_inst._defaults.timezone === null) { + tp_inst.timezone = date.getTimezoneOffset() * -1; + } + date = $.timepicker.timezoneAdjust(date, tp_inst.timezone, $.timepicker.timezoneOffsetString(-date.getTimezoneOffset())); + } + } + return date; + } + return this._base_getDateDatepicker(target, noDefault); + }; + + /* + * override parseDate() because UI 1.8.14 throws an error about "Extra characters" + * An option in datapicker to ignore extra format characters would be nicer. + */ + $.datepicker._base_parseDate = $.datepicker.parseDate; + $.datepicker.parseDate = function (format, value, settings) { + var date; + try { + date = this._base_parseDate(format, value, settings); + } catch (err) { + // Hack! The error message ends with a colon, a space, and + // the "extra" characters. We rely on that instead of + // attempting to perfectly reproduce the parsing algorithm. + if (err.indexOf(":") >= 0) { + date = this._base_parseDate(format, value.substring(0, value.length - (err.length - err.indexOf(':') - 2)), settings); + $.timepicker.log("Error parsing the date string: " + err + "\ndate string = " + value + "\ndate format = " + format); + } else { + throw err; + } + } + return date; + }; + + /* + * override formatDate to set date with time to the input + */ + $.datepicker._base_formatDate = $.datepicker._formatDate; + $.datepicker._formatDate = function (inst, day, month, year) { + var tp_inst = this._get(inst, 'timepicker'); + if (tp_inst) { + tp_inst._updateDateTime(inst); + return tp_inst.$input.val(); + } + return this._base_formatDate(inst); + }; + + /* + * override options setter to add time to maxDate(Time) and minDate(Time). MaxDate + */ + $.datepicker._base_optionDatepicker = $.datepicker._optionDatepicker; + $.datepicker._optionDatepicker = function (target, name, value) { + var inst = this._getInst(target), + name_clone; + if (!inst) { + return null; + } + + var tp_inst = this._get(inst, 'timepicker'); + if (tp_inst) { + var min = null, + max = null, + onselect = null, + overrides = tp_inst._defaults.evnts, + fns = {}, + prop, + ret, + oldVal, + $target; + if (typeof name === 'string') { // if min/max was set with the string + if (name === 'minDate' || name === 'minDateTime') { + min = value; + } else if (name === 'maxDate' || name === 'maxDateTime') { + max = value; + } else if (name === 'onSelect') { + onselect = value; + } else if (overrides.hasOwnProperty(name)) { + if (typeof (value) === 'undefined') { + return overrides[name]; + } + fns[name] = value; + name_clone = {}; //empty results in exiting function after overrides updated + } + } else if (typeof name === 'object') { //if min/max was set with the JSON + if (name.minDate) { + min = name.minDate; + } else if (name.minDateTime) { + min = name.minDateTime; + } else if (name.maxDate) { + max = name.maxDate; + } else if (name.maxDateTime) { + max = name.maxDateTime; + } + for (prop in overrides) { + if (overrides.hasOwnProperty(prop) && name[prop]) { + fns[prop] = name[prop]; + } + } + } + for (prop in fns) { + if (fns.hasOwnProperty(prop)) { + overrides[prop] = fns[prop]; + if (!name_clone) { name_clone = $.extend({}, name); } + delete name_clone[prop]; + } + } + if (name_clone && isEmptyObject(name_clone)) { return; } + if (min) { //if min was set + if (min === 0) { + min = new Date(); + } else { + min = new Date(min); + } + tp_inst._defaults.minDate = min; + tp_inst._defaults.minDateTime = min; + } else if (max) { //if max was set + if (max === 0) { + max = new Date(); + } else { + max = new Date(max); + } + tp_inst._defaults.maxDate = max; + tp_inst._defaults.maxDateTime = max; + } else if (onselect) { + tp_inst._defaults.onSelect = onselect; + } + + // Datepicker will override our date when we call _base_optionDatepicker when + // calling minDate/maxDate, so we will first grab the value, call + // _base_optionDatepicker, then set our value back. + if(min || max){ + $target = $(target); + oldVal = $target.datetimepicker('getDate'); + ret = this._base_optionDatepicker.call($.datepicker, target, name_clone || name, value); + $target.datetimepicker('setDate', oldVal); + return ret; + } + } + if (value === undefined) { + return this._base_optionDatepicker.call($.datepicker, target, name); + } + return this._base_optionDatepicker.call($.datepicker, target, name_clone || name, value); + }; + + /* + * jQuery isEmptyObject does not check hasOwnProperty - if someone has added to the object prototype, + * it will return false for all objects + */ + var isEmptyObject = function (obj) { + var prop; + for (prop in obj) { + if (obj.hasOwnProperty(prop)) { + return false; + } + } + return true; + }; + + /* + * jQuery extend now ignores nulls! + */ + var extendRemove = function (target, props) { + $.extend(target, props); + for (var name in props) { + if (props[name] === null || props[name] === undefined) { + target[name] = props[name]; + } + } + return target; + }; + + /* + * Determine by the time format which units are supported + * Returns an object of booleans for each unit + */ + var detectSupport = function (timeFormat) { + var tf = timeFormat.replace(/'.*?'/g, '').toLowerCase(), // removes literals + isIn = function (f, t) { // does the format contain the token? + return f.indexOf(t) !== -1 ? true : false; + }; + return { + hour: isIn(tf, 'h'), + minute: isIn(tf, 'm'), + second: isIn(tf, 's'), + millisec: isIn(tf, 'l'), + microsec: isIn(tf, 'c'), + timezone: isIn(tf, 'z'), + ampm: isIn(tf, 't') && isIn(timeFormat, 'h'), + iso8601: isIn(timeFormat, 'Z') + }; + }; + + /* + * Converts 24 hour format into 12 hour + * Returns 12 hour without leading 0 + */ + var convert24to12 = function (hour) { + hour %= 12; + + if (hour === 0) { + hour = 12; + } + + return String(hour); + }; + + var computeEffectiveSetting = function (settings, property) { + return settings && settings[property] ? settings[property] : $.timepicker._defaults[property]; + }; + + /* + * Splits datetime string into date and time substrings. + * Throws exception when date can't be parsed + * Returns {dateString: dateString, timeString: timeString} + */ + var splitDateTime = function (dateTimeString, timeSettings) { + // The idea is to get the number separator occurrences in datetime and the time format requested (since time has + // fewer unknowns, mostly numbers and am/pm). We will use the time pattern to split. + var separator = computeEffectiveSetting(timeSettings, 'separator'), + format = computeEffectiveSetting(timeSettings, 'timeFormat'), + timeParts = format.split(separator), // how many occurrences of separator may be in our format? + timePartsLen = timeParts.length, + allParts = dateTimeString.split(separator), + allPartsLen = allParts.length; + + if (allPartsLen > 1) { + return { + dateString: allParts.splice(0, allPartsLen - timePartsLen).join(separator), + timeString: allParts.splice(0, timePartsLen).join(separator) + }; + } + + return { + dateString: dateTimeString, + timeString: '' + }; + }; + + /* + * Internal function to parse datetime interval + * Returns: {date: Date, timeObj: Object}, where + * date - parsed date without time (type Date) + * timeObj = {hour: , minute: , second: , millisec: , microsec: } - parsed time. Optional + */ + var parseDateTimeInternal = function (dateFormat, timeFormat, dateTimeString, dateSettings, timeSettings) { + var date, + parts, + parsedTime; + + parts = splitDateTime(dateTimeString, timeSettings); + date = $.datepicker._base_parseDate(dateFormat, parts.dateString, dateSettings); + + if (parts.timeString === '') { + return { + date: date + }; + } + + parsedTime = $.datepicker.parseTime(timeFormat, parts.timeString, timeSettings); + + if (!parsedTime) { + throw 'Wrong time format'; + } + + return { + date: date, + timeObj: parsedTime + }; + }; + + /* + * Internal function to set timezone_select to the local timezone + */ + var selectLocalTimezone = function (tp_inst, date) { + if (tp_inst && tp_inst.timezone_select) { + var now = date || new Date(); + tp_inst.timezone_select.val(-now.getTimezoneOffset()); + } + }; + + /* + * Create a Singleton Instance + */ + $.timepicker = new Timepicker(); + + /** + * Get the timezone offset as string from a date object (eg '+0530' for UTC+5.5) + * @param {number} tzMinutes if not a number, less than -720 (-1200), or greater than 840 (+1400) this value is returned + * @param {boolean} iso8601 if true formats in accordance to iso8601 "+12:45" + * @return {string} + */ + $.timepicker.timezoneOffsetString = function (tzMinutes, iso8601) { + if (isNaN(tzMinutes) || tzMinutes > 840 || tzMinutes < -720) { + return tzMinutes; + } + + var off = tzMinutes, + minutes = off % 60, + hours = (off - minutes) / 60, + iso = iso8601 ? ':' : '', + tz = (off >= 0 ? '+' : '-') + ('0' + Math.abs(hours)).slice(-2) + iso + ('0' + Math.abs(minutes)).slice(-2); + + if (tz === '+00:00') { + return 'Z'; + } + return tz; + }; + + /** + * Get the number in minutes that represents a timezone string + * @param {string} tzString formatted like "+0500", "-1245", "Z" + * @return {number} the offset minutes or the original string if it doesn't match expectations + */ + $.timepicker.timezoneOffsetNumber = function (tzString) { + var normalized = tzString.toString().replace(':', ''); // excuse any iso8601, end up with "+1245" + + if (normalized.toUpperCase() === 'Z') { // if iso8601 with Z, its 0 minute offset + return 0; + } + + if (!/^(\-|\+)\d{4}$/.test(normalized)) { // possibly a user defined tz, so just give it back + return parseInt(tzString, 10); + } + + return ((normalized.substr(0, 1) === '-' ? -1 : 1) * // plus or minus + ((parseInt(normalized.substr(1, 2), 10) * 60) + // hours (converted to minutes) + parseInt(normalized.substr(3, 2), 10))); // minutes + }; + + /** + * No way to set timezone in js Date, so we must adjust the minutes to compensate. (think setDate, getDate) + * @param {Date} date + * @param {string} fromTimezone formatted like "+0500", "-1245" + * @param {string} toTimezone formatted like "+0500", "-1245" + * @return {Date} + */ + $.timepicker.timezoneAdjust = function (date, fromTimezone, toTimezone) { + var fromTz = $.timepicker.timezoneOffsetNumber(fromTimezone); + var toTz = $.timepicker.timezoneOffsetNumber(toTimezone); + if (!isNaN(toTz)) { + date.setMinutes(date.getMinutes() + (-fromTz) - (-toTz)); + } + return date; + }; + + /** + * Calls `timepicker()` on the `startTime` and `endTime` elements, and configures them to + * enforce date range limits. + * n.b. The input value must be correctly formatted (reformatting is not supported) + * @param {Element} startTime + * @param {Element} endTime + * @param {Object} options Options for the timepicker() call + * @return {jQuery} + */ + $.timepicker.timeRange = function (startTime, endTime, options) { + return $.timepicker.handleRange('timepicker', startTime, endTime, options); + }; + + /** + * Calls `datetimepicker` on the `startTime` and `endTime` elements, and configures them to + * enforce date range limits. + * @param {Element} startTime + * @param {Element} endTime + * @param {Object} options Options for the `timepicker()` call. Also supports `reformat`, + * a boolean value that can be used to reformat the input values to the `dateFormat`. + * @param {string} method Can be used to specify the type of picker to be added + * @return {jQuery} + */ + $.timepicker.datetimeRange = function (startTime, endTime, options) { + $.timepicker.handleRange('datetimepicker', startTime, endTime, options); + }; + + /** + * Calls `datepicker` on the `startTime` and `endTime` elements, and configures them to + * enforce date range limits. + * @param {Element} startTime + * @param {Element} endTime + * @param {Object} options Options for the `timepicker()` call. Also supports `reformat`, + * a boolean value that can be used to reformat the input values to the `dateFormat`. + * @return {jQuery} + */ + $.timepicker.dateRange = function (startTime, endTime, options) { + $.timepicker.handleRange('datepicker', startTime, endTime, options); + }; + + /** + * Calls `method` on the `startTime` and `endTime` elements, and configures them to + * enforce date range limits. + * @param {string} method Can be used to specify the type of picker to be added + * @param {Element} startTime + * @param {Element} endTime + * @param {Object} options Options for the `timepicker()` call. Also supports `reformat`, + * a boolean value that can be used to reformat the input values to the `dateFormat`. + * @return {jQuery} + */ + $.timepicker.handleRange = function (method, startTime, endTime, options) { + options = $.extend({}, { + minInterval: 0, // min allowed interval in milliseconds + maxInterval: 0, // max allowed interval in milliseconds + start: {}, // options for start picker + end: {} // options for end picker + }, options); + + // for the mean time this fixes an issue with calling getDate with timepicker() + var timeOnly = false; + if(method === 'timepicker'){ + timeOnly = true; + method = 'datetimepicker'; + } + + function checkDates(changed, other) { + var startdt = startTime[method]('getDate'), + enddt = endTime[method]('getDate'), + changeddt = changed[method]('getDate'); + + if (startdt !== null) { + var minDate = new Date(startdt.getTime()), + maxDate = new Date(startdt.getTime()); + + minDate.setMilliseconds(minDate.getMilliseconds() + options.minInterval); + maxDate.setMilliseconds(maxDate.getMilliseconds() + options.maxInterval); + + if (options.minInterval > 0 && minDate > enddt) { // minInterval check + endTime[method]('setDate', minDate); + } + else if (options.maxInterval > 0 && maxDate < enddt) { // max interval check + endTime[method]('setDate', maxDate); + } + else if (startdt > enddt) { + other[method]('setDate', changeddt); + } + } + } + + function selected(changed, other, option) { + if (!changed.val()) { + return; + } + var date = changed[method].call(changed, 'getDate'); + if (date !== null && options.minInterval > 0) { + if (option === 'minDate') { + date.setMilliseconds(date.getMilliseconds() + options.minInterval); + } + if (option === 'maxDate') { + date.setMilliseconds(date.getMilliseconds() - options.minInterval); + } + } + + if (date.getTime) { + other[method].call(other, 'option', option, date); + } + } + + $.fn[method].call(startTime, $.extend({ + timeOnly: timeOnly, + onClose: function (dateText, inst) { + checkDates($(this), endTime); + }, + onSelect: function (selectedDateTime) { + selected($(this), endTime, 'minDate'); + } + }, options, options.start)); + $.fn[method].call(endTime, $.extend({ + timeOnly: timeOnly, + onClose: function (dateText, inst) { + checkDates($(this), startTime); + }, + onSelect: function (selectedDateTime) { + selected($(this), startTime, 'maxDate'); + } + }, options, options.end)); + + checkDates(startTime, endTime); + + selected(startTime, endTime, 'minDate'); + selected(endTime, startTime, 'maxDate'); + + return $([startTime.get(0), endTime.get(0)]); + }; + + /** + * Log error or data to the console during error or debugging + * @param {Object} err pass any type object to log to the console during error or debugging + * @return {void} + */ + $.timepicker.log = function () { + // Older IE (9, maybe 10) throw error on accessing `window.console.log.apply`, so check first. + if (window.console && window.console.log && window.console.log.apply) { + window.console.log.apply(window.console, Array.prototype.slice.call(arguments)); + } + }; + + /* + * Add util object to allow access to private methods for testability. + */ + $.timepicker._util = { + _extendRemove: extendRemove, + _isEmptyObject: isEmptyObject, + _convert24to12: convert24to12, + _detectSupport: detectSupport, + _selectLocalTimezone: selectLocalTimezone, + _computeEffectiveSetting: computeEffectiveSetting, + _splitDateTime: splitDateTime, + _parseDateTimeInternal: parseDateTimeInternal + }; + + /* + * Microsecond support + */ + if (!Date.prototype.getMicroseconds) { + Date.prototype.microseconds = 0; + Date.prototype.getMicroseconds = function () { return this.microseconds; }; + Date.prototype.setMicroseconds = function (m) { + this.setMilliseconds(this.getMilliseconds() + Math.floor(m / 1000)); + this.microseconds = m % 1000; + return this; + }; + } + + /* + * Keep up with the version + */ + $.timepicker.version = "@@version"; + +})); diff --git a/js/osticket.js b/js/osticket.js index 9584b92f8352666a4a57b2247c0a33221fad4b25..f60d3218f87e42cf1cf3a6f6156dd4375824c15e 100644 --- a/js/osticket.js +++ b/js/osticket.js @@ -197,7 +197,6 @@ $.translate_format = function(str) { 'yyyy': '`', 'yyy': '`', 'yy': 'y', - 'y': 'yy', '`': 'yy' }; // Change PHP formats to datepicker ones @@ -232,11 +231,8 @@ $(document).on('submit', 'form', function() { $('.dp', $(this)).each(function(i, e) { var $e = $(e), d = $e.datepicker('getDate'); - if (!d) return; - var day = ('0'+d.getDate()).substr(-2), - month = ('0'+(d.getMonth()+1)).substr(-2), - year = d.getFullYear(); - $e.val(year+'-'+month+'-'+day); + if (d) + $e.val(d.toISOString()); }); }); diff --git a/scp/js/scp.js b/scp/js/scp.js index f8c60770dd4341208166d1e9f7d2c6461acb53b1..5e520f389b91ed859194b6c8b76fe721e5c8b114 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -553,10 +553,7 @@ var fixupDatePickers = function() { var $e = $(e), d = $e.datepicker('getDate'); if (!d || $e.data('fixed')) return; - var day = ('0'+d.getDate()).substr(-2), - month = ('0'+(d.getMonth()+1)).substr(-2), - year = d.getFullYear(); - $e.val(year+'-'+month+'-'+day); + $e.val(d.toISOString()); $e.data('fixed', true); $e.on('change', function() { $(this).data('fixed', false); }); }); @@ -611,7 +608,6 @@ $.translate_format = function(str) { 'yyyy': '`', 'yyy': '`', 'yy': 'y', - 'y': 'yy', '`': 'yy' }; // Change PHP formats to datepicker ones @@ -1129,9 +1125,10 @@ $(document).on('pjax:complete', function() { if ($.support.pjax) { $(document).on('click', 'a', function(event) { var $this = $(this); + var href = $this.attr('href'); if (!$this.hasClass('no-pjax') && !$this.closest('.no-pjax').length - && $this.attr('href').charAt(0) != '#') + && href && href.charAt(0) != '#') $.pjax.click(event, {container: $this.data('pjaxContainer') || '#pjax-container', timeout: 30000}); }) } diff --git a/setup/inc/class.installer.php b/setup/inc/class.installer.php index defde3e8d0db26f992e8add2bbbed1bba3b8bc51..d6ad5fca3634a3e55d321668e073a3e5ededbbeb 100644 --- a/setup/inc/class.installer.php +++ b/setup/inc/class.installer.php @@ -190,8 +190,8 @@ class Installer extends SetupWizard { 'dept_id' => $dept_id, 'role_id' => $role_id, 'email' => $vars['admin_email'], - 'firstname' => $vars['fname'], - 'lastname' => $vars['lname'], + 'firstname' => Format::htmlchars($vars['fname']), + 'lastname' => Format::htmlchars($vars['lname']), 'username' => $vars['username'], )); $staff->updatePerms(array(