Commit 9dfa47cc authored by mkanat%bugzilla.org's avatar mkanat%bugzilla.org

Bug 397099: Date/Time Fields should have a JavaScript widget for picking a date

Patch By Max Kanat-Alexander <mkanat@bugzilla.org> r=LpSolit, r=glob, a=mkanat
parent ab73b625
...@@ -201,6 +201,12 @@ use constant NUMERIC_COLUMNS => qw( ...@@ -201,6 +201,12 @@ use constant NUMERIC_COLUMNS => qw(
remaining_time remaining_time
); );
sub DATE_COLUMNS {
my @fields = Bugzilla->get_fields(
{ custom => 1, type => FIELD_TYPE_DATETIME });
return map { $_->name } @fields;
}
# This is used by add_comment to know what we validate before putting in # This is used by add_comment to know what we validate before putting in
# the DB. # the DB.
use constant UPDATE_COMMENT_COLUMNS => qw( use constant UPDATE_COMMENT_COLUMNS => qw(
......
...@@ -211,16 +211,13 @@ sub FILESYSTEM { ...@@ -211,16 +211,13 @@ sub FILESYSTEM {
foreach my $skin_dir ("$skinsdir/custom", <$skinsdir/contrib/*>) { foreach my $skin_dir ("$skinsdir/custom", <$skinsdir/contrib/*>) {
next unless -d $skin_dir; next unless -d $skin_dir;
next if basename($skin_dir) =~ /^cvs$/i; next if basename($skin_dir) =~ /^cvs$/i;
foreach (<$skinsdir/standard/*.css>) { $create_dirs{"$skin_dir/yui"} = $ws_dir_readable;
my $standard_css_file = basename($_); foreach my $base_css (<$skinsdir/standard/*.css>) {
my $custom_css_file = "$skin_dir/$standard_css_file"; _add_custom_css($skin_dir, basename($base_css), \%create_files, $ws_readable);
$create_files{$custom_css_file} = { perms => $ws_readable, contents => <<EOT }
/* foreach my $dir_css (<$skinsdir/standard/*/*.css>) {
* Custom rules for $standard_css_file. $dir_css =~ s{.+?([^/]+/[^/]+)$}{$1};
* The rules you put here override rules in that stylesheet. _add_custom_css($skin_dir, $dir_css, \%create_files, $ws_readable);
*/
EOT
}
} }
} }
...@@ -378,6 +375,18 @@ EOT ...@@ -378,6 +375,18 @@ EOT
} }
# A simple helper for creating "empty" CSS files.
sub _add_custom_css {
my ($skin_dir, $path, $create_files, $perms) = @_;
$create_files->{"$skin_dir/$path"} = { perms => $perms, contents => <<EOT
/*
* Custom rules for $path.
* The rules you put here override rules in that stylesheet.
*/
EOT
};
}
sub create_htaccess { sub create_htaccess {
_create_files(%{FILESYSTEM()->{htaccess}}); _create_files(%{FILESYSTEM()->{htaccess}});
......
...@@ -27,12 +27,15 @@ use Bugzilla::Constants; ...@@ -27,12 +27,15 @@ use Bugzilla::Constants;
use Bugzilla::Util; use Bugzilla::Util;
use Bugzilla::Error; use Bugzilla::Error;
use Date::Parse;
use constant NAME_FIELD => 'name'; use constant NAME_FIELD => 'name';
use constant ID_FIELD => 'id'; use constant ID_FIELD => 'id';
use constant LIST_ORDER => NAME_FIELD; use constant LIST_ORDER => NAME_FIELD;
use constant UPDATE_VALIDATORS => {}; use constant UPDATE_VALIDATORS => {};
use constant NUMERIC_COLUMNS => (); use constant NUMERIC_COLUMNS => ();
use constant DATE_COLUMNS => ();
############################### ###############################
#### Initialization #### #### Initialization ####
...@@ -237,6 +240,7 @@ sub update { ...@@ -237,6 +240,7 @@ sub update {
my $old_self = $self->new($self->id); my $old_self = $self->new($self->id);
my %numeric = map { $_ => 1 } $self->NUMERIC_COLUMNS; my %numeric = map { $_ => 1 } $self->NUMERIC_COLUMNS;
my %date = map { $_ => 1 } $self->DATE_COLUMNS;
my (@update_columns, @values, %changes); my (@update_columns, @values, %changes);
foreach my $column ($self->UPDATE_COLUMNS) { foreach my $column ($self->UPDATE_COLUMNS) {
my ($old, $new) = ($old_self->{$column}, $self->{$column}); my ($old, $new) = ($old_self->{$column}, $self->{$column});
...@@ -246,7 +250,9 @@ sub update { ...@@ -246,7 +250,9 @@ sub update {
if (!defined $new || !defined $old) { if (!defined $new || !defined $old) {
next if !defined $new && !defined $old; next if !defined $new && !defined $old;
} }
elsif ( ($numeric{$column} && $old == $new) || $old eq $new ) { elsif ( ($numeric{$column} && $old == $new)
|| ($date{$column} && str2time($old) == str2time($new))
|| $old eq $new ) {
next; next;
} }
...@@ -474,6 +480,12 @@ current value in the database. It only updates columns that have changed. ...@@ -474,6 +480,12 @@ current value in the database. It only updates columns that have changed.
Any column listed in NUMERIC_COLUMNS is treated as a number, not as a string, Any column listed in NUMERIC_COLUMNS is treated as a number, not as a string,
during these comparisons. during these comparisons.
=item C<DATE_COLUMNS>
This is much like L</NUMERIC_COLUMNS>, except that it treats strings as
dates when being compared. So, for example, C<2007-01-01> would be
equal to C<2007-01-01 00:00:00>.
=back =back
=head1 METHODS =head1 METHODS
......
...@@ -465,9 +465,9 @@ sub validate_time { ...@@ -465,9 +465,9 @@ sub validate_time {
my $ts = str2time($time); my $ts = str2time($time);
if ($ts) { if ($ts) {
$time2 = time2str("%H:%M:%S", $ts); $time2 = time2str("%H:%M:%S", $ts);
if (trim($time) =~ /^\d\d:\d\d$/) {
$time =~ s/(\d+):(\d+?):(\d+?)/$1:$2:$3/; $time .= ':00';
$time2 =~ s/(\d+):(\d+?):(\d+?)/$1:$2:$3/; }
} }
my $ret = ($ts && $time eq $time2); my $ret = ($ts && $time eq $time2);
return $ret ? 1 : 0; return $ret ? 1 : 0;
......
/* The contents of this file are subject to the Mozilla Public
* License Version 1.1 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of
* the License at http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS
* IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
* implied. See the License for the specific language governing
* rights and limitations under the License.
*
* The Original Code is the Bugzilla Bug Tracking System.
*
* The Initial Developer of the Original Code is Everything Solved, Inc.
* Portions created by Everything Solved are Copyright (C) 2007 Everything
* Solved, Inc. All Rights Reserved.
*
* Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
*/
/* This library assumes that the needed YUI libraries have been loaded
already. */
function createCalendar(name) {
var cal = new YAHOO.widget.Calendar('calendar_' + name,
'con_calendar_' + name);
YAHOO.bugzilla['calendar_' + name] = cal;
var field = document.getElementById(name);
cal.selectEvent.subscribe(setFieldFromCalendar, field, false);
updateCalendarFromField(field);
cal.render();
}
/* The onclick handlers for the button that shows the calendar. */
function showCalendar(field_name) {
var calendar = YAHOO.bugzilla["calendar_" + field_name];
var field = document.getElementById(field_name);
var button = document.getElementById('button_calendar_' + field_name);
bz_overlayBelow(calendar.oDomContainer, field);
calendar.show();
button.onclick = function() { hideCalendar(field_name); };
// Because of the way removeListener works, this has to be a function
// attached directly to this calendar.
calendar.bz_myBodyCloser = function(event) {
var container = this.oDomContainer;
var target = YAHOO.util.Event.getTarget(event);
if (target != container && target != button
&& !YAHOO.util.Dom.isAncestor(container, target))
{
hideCalendar(field_name);
}
};
// If somebody clicks outside the calendar, hide it.
YAHOO.util.Event.addListener(document.body, 'click',
calendar.bz_myBodyCloser, calendar, true);
// Make Esc close the calendar.
calendar.bz_escCal = function (event) {
var key = YAHOO.util.Event.getCharCode(event);
if (key == 27) {
hideCalendar(field_name);
}
};
YAHOO.util.Event.addListener(document.body, 'keydown', calendar.bz_escCal);
}
function hideCalendar(field_name) {
var cal = YAHOO.bugzilla["calendar_" + field_name];
cal.hide();
var button = document.getElementById('button_calendar_' + field_name);
button.onclick = function() { showCalendar(field_name); };
YAHOO.util.Event.removeListener(document.body, 'click',
cal.bz_myBodyCloser);
YAHOO.util.Event.removeListener(document.body, 'keydown', cal.bz_escCal);
}
/* This is the selectEvent for our Calendar objects on our custom
* DateTime fields.
*/
function setFieldFromCalendar(type, args, date_field) {
var dates = args[0];
var setDate = dates[0];
// We can't just write the date straight into the field, because there
// might already be a time there.
var timeRe = /(\d\d):(\d\d)(?::(\d\d))?/;
var currentTime = timeRe.exec(date_field.value);
var d = new Date(setDate[0], setDate[1] - 1, setDate[2]);
if (currentTime) {
d.setHours(currentTime[1], currentTime[2]);
if (currentTime[3]) {
d.setSeconds(currentTime[3]);
}
}
var year = d.getFullYear();
// JavaScript's "Date" represents January as 0 and December as 11.
var month = d.getMonth() + 1;
if (month < 10) month = '0' + String(month);
var day = d.getDate();
if (day < 10) day = '0' + String(day);
var dateStr = year + '-' + month + '-' + day;
if (currentTime) {
var hours = d.getHours();
if (hours < 10) hours = '0' + String(hours);
d.setHours(hours);
var minutes = d.getMinutes();
if (minutes < 10) minutes = '0' + String(minutes);
var seconds = d.getSeconds();
if (seconds > 0 && seconds < 10) {
seconds = '0' + String(seconds);
}
dateStr = dateStr + ' ' + hours + ':' + minutes;
if (seconds) dateStr = dateStr + ':' + seconds;
}
date_field.value = dateStr;
hideCalendar(date_field.id);
}
/* Sets the calendar based on the current field value.
*/
function updateCalendarFromField(date_field) {
var dateRe = /(\d\d\d\d)-(\d\d?)-(\d\d?)/;
var pieces = dateRe.exec(date_field.value);
if (pieces) {
var cal = YAHOO.bugzilla["calendar_" + date_field.id];
cal.select(new Date(pieces[1], pieces[2] - 1, pieces[3]));
var selectedArray = cal.getSelectedDates();
var selected = selectedArray[0];
cal.cfg.setProperty("pagedate", (selected.getMonth() + 1) + '/'
+ selected.getFullYear());
cal.render();
}
}
...@@ -65,17 +65,8 @@ KeywordChooser.prototype = ...@@ -65,17 +65,8 @@ KeywordChooser.prototype =
positionChooser: function() positionChooser: function()
{ {
if (this._positioned) { if (this._positioned) return;
return; bz_overlayBelow(this._chooser, this._parent);
}
var elemY = bz_findPosY(this._parent);
var elemX = bz_findPosX(this._parent);
var elemH = this._parent.offsetHeight;
this._chooser.style.left = elemX + "px";
this._chooser.style.top = elemY + elemH + 1 + "px";
this._positioned = true; this._positioned = true;
}, },
......
...@@ -117,6 +117,24 @@ function bz_getFullWidth(fromObj) ...@@ -117,6 +117,24 @@ function bz_getFullWidth(fromObj)
} }
/** /**
* Causes a block to appear directly underneath another block,
* overlaying anything below it.
*
* @param item The block that you want to move.
* @param parent The block that it goes on top of.
* @return nothing
*/
function bz_overlayBelow(item, parent) {
var elemY = bz_findPosY(parent);
var elemX = bz_findPosX(parent);
var elemH = parent.offsetHeight;
item.style.position = 'absolute';
item.style.left = elemX + "px";
item.style.top = elemY + elemH + 1 + "px";
}
/**
* Create wanted options in a select form control. * Create wanted options in a select form control.
* *
* @param aSelect Select form control to manipulate. * @param aSelect Select form control to manipulate.
......
...@@ -13,3 +13,4 @@ release-notes.css ...@@ -13,3 +13,4 @@ release-notes.css
show_multiple.css show_multiple.css
summarize-time.css summarize-time.css
voting.css voting.css
yui
...@@ -360,6 +360,21 @@ div.user_match { ...@@ -360,6 +360,21 @@ div.user_match {
vertical-align: top; vertical-align: top;
} }
.calendar_button {
background: transparent url("global/calendar.png") no-repeat;
width: 20px;
height: 20px;
vertical-align: middle;
}
.calendar_button span { display: none }
/* These classes are set by YUI. */
.yui-calcontainer {
display: none;
background-color: white;
padding: 10px;
border: 1px solid #404D6C;
}
form#Create th { form#Create th {
text-align: right; text-align: right;
} }
......
/*
Copyright (c) 2007, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.net/yui/license.txt
version: 2.3.1
*/
.yui-calcontainer{position:relative;float:left;_overflow:hidden;}.yui-calcontainer iframe{position:absolute;border:none;margin:0;padding:0;z-index:0;width:100%;height:100%;left:0px;top:0px;}.yui-calcontainer iframe.fixedsize{width:50em;height:50em;top:-1px;left:-1px;}.yui-calcontainer.multi .groupcal{z-index:1;float:left;position:relative;}.yui-calcontainer .title{position:relative;z-index:1;}.yui-calcontainer .close-icon{position:absolute;z-index:1;}.yui-calendar{position:relative;}.yui-calendar .calnavleft{position:absolute;z-index:1;}.yui-calendar .calnavright{position:absolute;z-index:1;}.yui-calendar .calheader{position:relative;width:100%;text-align:center;}.yui-calendar .calbody a:hover{background:inherit;}p#clear{clear:left;padding-top:10px;}.yui-skin-sam .yui-calcontainer{background-color:#f2f2f2;border:1px solid #808080;padding:10px;}.yui-skin-sam .yui-calcontainer.multi{padding:0 5px 0 5px;}.yui-skin-sam .yui-calcontainer.multi .groupcal{background-color:transparent;border:none;padding:10px 5px 10px 5px;margin:0;}.yui-skin-sam .yui-calcontainer .title{background:url(sprite.png) repeat-x 0 0;border-bottom:1px solid #cccccc;font:100% sans-serif;color:#000;font-weight:bold;height:auto;padding:.4em;margin:0 -10px 10px -10px;top:0;left:0;text-align:left;}.yui-skin-sam .yui-calcontainer.multi .title{margin:0 -5px 0 -5px;}.yui-skin-sam .yui-calcontainer.withtitle{padding-top:0;}.yui-skin-sam .yui-calcontainer .calclose{background:url(sprite.png) no-repeat 0 -300px;width:25px;height:15px;top:.4em;right:.4em;cursor:pointer;}.yui-skin-sam .yui-calendar{border-spacing:0;border-collapse:collapse;font:100% sans-serif;text-align:center;}.yui-skin-sam .yui-calendar .calhead{background:transparent;border:none;vertical-align:middle;}.yui-skin-sam .yui-calendar .calheader{background:transparent;font-weight:bold;padding:0 0 .6em 0;text-align:center;}.yui-skin-sam .yui-calendar .calheader img{border:none;}.yui-skin-sam .yui-calendar .calnavleft{background:url(sprite.png) no-repeat 0 -450px;width:25px;height:15px;top:0;bottom:0;left:-10px;margin-left:.4em;cursor:pointer;}.yui-skin-sam .yui-calendar .calnavright{background:url(sprite.png) no-repeat 0 -500px;width:25px;height:15px;top:0;bottom:0;right:-10px;margin-right:.4em;cursor:pointer;}.yui-skin-sam .yui-calendar .calweekdayrow{height:2em;}.yui-skin-sam .yui-calendar .calweekdaycell{color:#000;font-weight:bold;text-align:center;width:2em;}.yui-skin-sam .yui-calendar .calfoot{background-color:#f2f2f2;}.yui-skin-sam .yui-calendar .calrowhead,.yui-skin-sam .yui-calendar .calrowfoot{color:#a6a6a6;font-size:85%;font-style:normal;font-weight:normal;}.yui-skin-sam .yui-calendar .calrowhead{text-align:right;padding-right:2px;}.yui-skin-sam .yui-calendar .calrowfoot{text-align:left;padding-left:2px;}.yui-skin-sam .yui-calendar td.calcell{border:1px solid #cccccc;background:#fff;padding:1px;height:1.6em;line-height:1.6em;text-align:center;white-space:nowrap;}.yui-skin-sam .yui-calendar td.calcell a{color:#0066cc;display:block;height:100%;text-decoration:none;}.yui-skin-sam .yui-calendar td.calcell.today{background-color:#000;}.yui-skin-sam .yui-calendar td.calcell.today a{background-color:#fff;}.yui-skin-sam .yui-calendar td.calcell.oom{background-color:#cccccc;color:#a6a6a6;cursor:default;}.yui-skin-sam .yui-calendar td.calcell.selected{background-color:#fff;color:#000;}.yui-skin-sam .yui-calendar td.calcell.selected a{background-color:#b3d4ff;color:#000;}.yui-skin-sam .yui-calendar td.calcell.calcellhover{background-color:#426fd9;color:#fff;cursor:pointer;}.yui-skin-sam .yui-calendar td.calcell.calcellhover a{background-color:#426fd9;color:#fff;}.yui-skin-sam .yui-calendar td.calcell.previous{color:#e0e0e0;}.yui-skin-sam .yui-calendar td.calcell.restricted{text-decoration:line-through;}.yui-skin-sam .yui-calendar td.calcell.highlight1{background-color:#ccff99;}.yui-skin-sam .yui-calendar td.calcell.highlight2{background-color:#99ccff;}.yui-skin-sam .yui-calendar td.calcell.highlight3{background-color:#ffcccc;}.yui-skin-sam .yui-calendar td.calcell.highlight4{background-color:#ccff99;}
...@@ -30,8 +30,11 @@ ...@@ -30,8 +30,11 @@
[% PROCESS global/header.html.tmpl [% PROCESS global/header.html.tmpl
title = title title = title
style_urls = [ 'skins/standard/create_attachment.css' ] style_urls = [ 'skins/standard/create_attachment.css',
javascript_urls = [ "js/attachment.js", "js/util.js", "js/keyword-chooser.js" ] 'skins/standard/yui/calendar.css' ]
javascript_urls = [ "js/attachment.js", "js/util.js", "js/keyword-chooser.js",
"js/yui/yahoo-dom-event.js", "js/yui/calendar.js",
"js/field.js" ]
%] %]
<script type="text/javascript"> <script type="text/javascript">
......
...@@ -45,7 +45,21 @@ ...@@ -45,7 +45,21 @@
value="[% value FILTER html %]" size="60"> value="[% value FILTER html %]" size="60">
[% CASE constants.FIELD_TYPE_DATETIME %] [% CASE constants.FIELD_TYPE_DATETIME %]
<input name="[% field.name FILTER html %]" size="20" <input name="[% field.name FILTER html %]" size="20"
value="[% value FILTER html %]"> id="[% field.name FILTER html %]"
value="[% value FILTER html %]"
onchange="updateCalendarFromField(this)">
<button type="button" class="calendar_button"
id="button_calendar_[% field.name FILTER html %]"
onclick="showCalendar('[% field.name FILTER js %]')">
<span>Calendar</span>
</button>
<div id="con_calendar_[% field.name FILTER html %]"
class="yui-skin-sam"></div>
<script type="text/javascript">
createCalendar('[% field.name FILTER js %]')
</script>
[% CASE [ constants.FIELD_TYPE_SINGLE_SELECT [% CASE [ constants.FIELD_TYPE_SINGLE_SELECT
constants.FIELD_TYPE_MULTI_SELECT ] %] constants.FIELD_TYPE_MULTI_SELECT ] %]
<select id="[% field.name FILTER html %]" <select id="[% field.name FILTER html %]"
......
...@@ -42,5 +42,7 @@ ...@@ -42,5 +42,7 @@
[% END %] [% END %]
[% PROCESS global/header.html.tmpl [% PROCESS global/header.html.tmpl
javascript_urls = [ "js/util.js", "js/keyword-chooser.js" ] javascript_urls = [ "js/util.js", "js/keyword-chooser.js", "js/field.js",
"js/yui/yahoo-dom-event.js", "js/yui/calendar.js" ]
style_urls = [ "skins/standard/yui/calendar.css" ]
%] %]
...@@ -39,8 +39,10 @@ ...@@ -39,8 +39,10 @@
"bz_component_$bug.component", "bz_component_$bug.component",
"bz_bug_$bug.bug_id" "bz_bug_$bug.bug_id"
] ]
javascript_urls = [ "js/util.js", "js/keyword-chooser.js" ] javascript_urls = [ "js/util.js", "js/keyword-chooser.js", "js/field.js",
doc_section = "bug_page.html" "js/yui/yahoo-dom-event.js", "js/yui/calendar.js" ]
style_urls = [ "skins/standard/yui/calendar.css" ]
doc_section = "bug_page.html"
%] %]
[% END %] [% END %]
......
...@@ -74,11 +74,16 @@ ...@@ -74,11 +74,16 @@
[% javascript %] [% javascript %]
</script> </script>
[% END %] [% END %]
[% IF javascript_urls %] [% IF javascript_urls %]
[% FOREACH javascript_url = javascript_urls %] [% FOREACH javascript_url = javascript_urls %]
<script src="[% javascript_url FILTER html %]" type="text/javascript"></script> <script src="[% javascript_url FILTER html %]" type="text/javascript"></script>
[% END %] [% END %]
[% IF javascript_urls.grep('yui/').size %]
<script type="text/javascript">
YAHOO.namespace('bugzilla');
</script>
[% END %]
[% END %] [% END %]
[%# Set up the skin CSS cascade: [%# Set up the skin CSS cascade:
......
...@@ -48,7 +48,9 @@ ...@@ -48,7 +48,9 @@
title = title title = title
style = style style = style
atomlink = "buglist.cgi?$urlquerypart&title=$title&ctype=atom" atomlink = "buglist.cgi?$urlquerypart&title=$title&ctype=atom"
javascript_urls = [ "js/util.js", "js/keyword-chooser.js" ] javascript_urls = [ "js/util.js", "js/keyword-chooser.js", "js/field.js",
"js/yui/yahoo-dom-event.js", "js/yui/calendar.js" ]
style_urls = [ "skins/standard/yui/calendar.css" ]
doc_section = "query.html#list" doc_section = "query.html#list"
%] %]
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment