Commit d5d68cec authored by Romain Courteaud's avatar Romain Courteaud

[erp5_web_renderjs_ui] Reimplement DateTimeField

parent 5ad60e06
......@@ -3,7 +3,7 @@
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no" />
<title>ERP5 Floatfield</title>
<title>ERP5 Datetimefield</title>
<!-- renderjs -->
<script src="rsvp.js" type="text/javascript"></script>
......@@ -13,6 +13,6 @@
</head>
<body>
<input type="date" />
<div class="datetimefield"></div>
</body>
</html>
\ No newline at end of file
......@@ -220,7 +220,7 @@
</item>
<item>
<key> <string>actor</string> </key>
<value> <string>romain</string> </value>
<value> <string>zope</string> </value>
</item>
<item>
<key> <string>comment</string> </key>
......@@ -234,7 +234,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>940.50766.7916.31675</string> </value>
<value> <string>954.44229.8719.22835</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -252,8 +252,8 @@
</tuple>
<state>
<tuple>
<float>1423235572.23</float>
<string>GMT</string>
<float>1476776016.7</float>
<string>UTC</string>
</tuple>
</state>
</object>
......
/*global window, rJS, RSVP, document, loopEventListener */
/*jslint indent: 2 */
(function (window, rJS, RSVP, document, loopEventListener) {
/*global window, rJS, RSVP*/
/*jslint indent: 2, maxlen: 80 */
(function (window, rJS, RSVP) {
"use strict";
var ZONE_LIST = [
["GMT-12", "-1200"],
["GMT-11", "-1100"],
["GMT-10", "-1000"],
["GMT-9", "-0900"],
["GMT-8", "-0800"],
["GMT-7", "-0700"],
["GMT-6", "-0600"],
["GMT-5", "-0500"],
["GMT-4", "-0400"],
["GMT-3", "-0300"],
["GMT-2", "-0200"],
["GMT-1", "-0100"],
["GMT", "+0000"],
["GMT+1", "+0100"],
["GMT+2", "+0200"],
["GMT+3", "+0300"],
["GMT+4", "+0400"],
["GMT+5", "+0500"],
["GMT+6", "+0600"],
["GMT+7", "+0700"],
["GMT+8", "+0800"],
["GMT+9", "+0900"],
["GMT+10", "+1000"],
["GMT+11", "+1100"],
["GMT+12", "+1200"]
];
rJS(window)
.ready(function (gadget) {
return gadget.getElement()
.push(function (element) {
gadget.element = element;
gadget.props = {};
});
})
.declareAcquiredMethod("notifyInvalid", "notifyInvalid")
.declareAcquiredMethod("notifyValid", "notifyValid")
.declareMethod('getTextContent', function () {
return this.element.querySelector('input').getAttribute('value') || "";
})
.declareMethod('render', function (options) {
var input = this.element.querySelector('input'),
var field_json = options.field_json || {},
state_dict = {
value: field_json.value || field_json.default || "",
editable: field_json.editable,
required: field_json.required,
name: field_json.key,
key: field_json.key,
title: field_json.title,
timezone_style: field_json.timezone_style,
date_only: field_json.date_only,
hide_day: field_json.hide_day,
allow_empty_time: field_json.allow_empty_time,
ampm_time_style: field_json.ampm_time_style,
subfield_ampm_key: field_json.subfield_ampm_key,
subfield_hour_key: field_json.subfield_hour_key,
subfield_minute_key: field_json.subfield_minute_key,
hidden_day_is_last_day: field_json.hidden_day_is_last_day,
subfield_year_key: field_json.subfield_year_key,
subfield_month_key: field_json.subfield_month_key,
subfield_day_key: field_json.subfield_day_key,
subfield_timezone_key: field_json.subfield_timezone_key,
start_datetime: field_json.start_datetime,
end_datetime: field_json.end_datetime
};
return this.changeState(state_dict);
})
.onStateChange(function (modification_dict) {
var element = this.element.querySelector('.datetimefield'),
gadget = this,
date,
tmp,
timezone,
......@@ -25,41 +72,85 @@
tmp_date,
tmp_hour,
tmp_minute,
select,
leap_year,
time = "",
leapyear,
i,
field_json = options.field_json || {},
lastDateOfMonth = [[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]],//leapyear
select_options = ["GMT-12", "GMT-11", "GMT-10", "GMT-9", "GMT-8", "GMT-7", "GMT-6",
"GMT-5", "GMT-4", "GMT-3", "GMT-2", "GMT-1", "GMT", "GMT+1",
"GMT+2", "GMT+3", "GMT+4", "GMT+5", "GMT+6", "GMT+7", "GMT+8",
"GMT+9", "GMT+10", "GMT+11", "GMT+12"],
select_option,
value = field_json.value || field_json.default || "";
this.props.field_json = field_json;
last_month_date = [
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
],//leapyear
queue = new RSVP.Queue(),
promise_list,
input_state = {
name: gadget.state.key + '_input',
editable: gadget.state.editable,
required: gadget.state.required,
type: gadget.state.date_only ? "date" : "datetime-local"
},
select_state = {
name: gadget.state.key + '_select',
value: "+0000",
item_list: ZONE_LIST,
editable: gadget.state.editable,
required: gadget.state.required
// name: field_json.key,
// title: field_json.title
},
p_state = {
tag: "p"
};
if (field_json.timezone_style) {
//change date to local
select = document.createElement("select");
for (i = 0; i < select_options.length; i += 1) {
select_option = document.createElement("option");
select_option.value = select_options[i];
select_option.innerHTML = select_options[i];
select.appendChild(select_option);
// Create or fetch sub gadgets
if (modification_dict.hasOwnProperty('editable') ||
modification_dict.hasOwnProperty('timezone_style')) {
if (gadget.state.editable) {
promise_list = [
gadget.declareGadget('gadget_html5_input.html', {scope: 'INPUT'})
];
if (gadget.state.timezone_style) {
promise_list.push(gadget.declareGadget('gadget_html5_select.html',
{scope: 'SELECT'}));
}
} else {
promise_list = [
gadget.declareGadget('gadget_html5_element.html', {scope: 'P'})
];
}
select.setAttribute("class", "gmt_select");
select.selectedIndex = 12;
this.element.appendChild(select);
}
if (field_json.date_only === 0) {
input.setAttribute("type", "datetime-local");
queue
.push(function () {
return RSVP.all(promise_list);
})
.push(function (result_list) {
// Clear first to DOM, append after to reduce flickering/manip
while (element.firstChild) {
element.removeChild(element.firstChild);
}
var i;
for (i = 0; i < result_list.length; i += 1) {
element.appendChild(result_list[i].element);
}
return result_list;
});
} else {
if (gadget.state.editable) {
promise_list = [
gadget.getDeclaredGadget('INPUT')
];
if (gadget.state.timezone_style) {
promise_list.push(gadget.getDeclaredGadget('SELECT'));
}
} else {
promise_list = [gadget.getDeclaredGadget('P')];
}
queue
.push(function () {
return RSVP.all(promise_list);
});
}
// Calculate sub gadget states
//Change type to datetime/datetime local if configured in the field
if (value !== "") {
tmp = new Date(value);
if (gadget.state.value) {
tmp = new Date(gadget.state.value);
//get date without timezone
tmp_date = tmp.getUTCDate();
tmp_month = tmp.getUTCMonth() + 1;
......@@ -70,12 +161,13 @@
//timezone required
//convert time to GMT
timezone = parseInt(value.slice(-5), 10) / 100;
timezone = parseInt(gadget.state.value.slice(-5), 10) / 100;
if (field_json.timezone_style) {
select.selectedIndex = timezone + 12;
if (gadget.state.timezone_style) {
select_state.value = ZONE_LIST[timezone + 12][1];
}
leapyear = (tmp_year % 4 === 0 && tmp_year % 100 !== 0) ? 1 : 0;
leap_year = (tmp_year % 4 === 0 && tmp_year % 100 !== 0) ? 1 : 0;
if (timezone !== 0) {
tmp_hour += timezone;
if (tmp_hour < 0) {
......@@ -87,12 +179,12 @@
tmp_month = 12;
tmp_year -= 1;
}
tmp_date = lastDateOfMonth[leapyear][tmp_month - 1];
tmp_date = last_month_date[leap_year][tmp_month - 1];
}
} else if (tmp_hour > 23) {
tmp_hour -= 24;
tmp_date += 1;
if (tmp_date > lastDateOfMonth[leapyear][tmp_month - 1]) {
if (tmp_date > last_month_date[leap_year][tmp_month - 1]) {
tmp_date = 1;
tmp_month += 1;
if (tmp_month > 12) {
......@@ -102,208 +194,162 @@
}
}
}
if (field_json.date_only === 0) {
time = "T" + Math.floor(tmp_hour / 10) + tmp_hour % 10 + ":"
+ Math.floor(tmp_minute / 10) + (tmp_minute % 10) + ":00";
if (!gadget.state.date_only) {
time = "T" + Math.floor(tmp_hour / 10) + tmp_hour % 10 + ":" +
Math.floor(tmp_minute / 10) + (tmp_minute % 10) + ":00";
}
date = tmp_year + "-" + Math.floor(tmp_month / 10) + (tmp_month % 10) + "-"
+ Math.floor(tmp_date / 10) + (tmp_date % 10);
date = tmp_year + "-" + Math.floor(tmp_month / 10) +
(tmp_month % 10) + "-" +
Math.floor(tmp_date / 10) + (tmp_date % 10);
input.setAttribute(
'value',
date + time
);
}
input.setAttribute('name', field_json.key);
input.setAttribute('title', field_json.title);
if (field_json.required === 1) {
input.setAttribute('required', 'required');
input_state.value = date + time;
}
if (field_json.editable !== 1) {
input.setAttribute('readonly', 'readonly');
input.setAttribute('data-wrapper-class', 'ui-state-disabled ui-state-readonly');
input.setAttribute('disabled', 'disabled');
// Render
if (gadget.state.editable) {
queue
.push(function (gadget_list) {
promise_list = [
gadget_list[0].render(input_state)
];
if (gadget.state.timezone_style) {
promise_list.push(gadget_list[1].render(select_state));
}
return RSVP.all(promise_list);
});
} else {
queue
.push(function (gadget_list) {
p_state.text_content = gadget.state.value;
return gadget_list[0].render(p_state);
});
}
return queue;
})
.declareMethod('getContent', function (options) {
var input = this.element.querySelector('input'),
var gadget = this,
result = {},
select,
year,
month,
field_json = this.props.field_json,
date,
hour,
minute,
timezone,
zone_list = {"GMT-12": "-1200", "GMT-11": "-1100",
"GMT-9": "-0900", "GMT-8": "-0800",
"GMT-7": "-0700", "GMT-6": "-0600",
"GMT-5": "-0500", "GMT-4": "-0400",
"GMT-3": "-0300", "GMT-2": "-0200",
"GMT-1": "-0100", "GMT": "+0000",
"GMT+1": "+0100", "GMT+2": "+0200",
"GMT+3": "+0300", "GMT+4": "+0400",
"GMT+5": "+0500", "GMT+6": "+0600",
"GMT+7": "+0700", "GMT+8": "+0800",
"GMT+9": "+0900", "GMT+10": "+1000",
"GMT+11": "+1100", "GMT+12": "+1200"},
value = input.value;
if (options === undefined || options.format === "erp5") {
if (value !== "") {
if (field_json.date_only === 0) {
value += "+0000";
}
value = new Date(value);
year = value.getUTCFullYear();
month = value.getUTCMonth() + 1;
date = value.getUTCDate();
if (field_json.hide_day === 1) {
date = 1;
}
//get time
if (field_json.date_only === 0) {
if (field_json.allow_empty_time === 1) {
hour = 0;
minute = 0;
} else {
hour = value.getUTCHours();
minute = value.getUTCMinutes();
promise_list;
if (gadget.state.editable) {
promise_list = [gadget.getDeclaredGadget('INPUT')];
if (gadget.state.timezone_style) {
promise_list.push(gadget.getDeclaredGadget('SELECT'));
}
return new RSVP.Queue()
.push(function () {
return RSVP.all(promise_list);
})
.push(function (result_list) {
var i;
promise_list = [];
for (i = 0; i < result_list.length; i += 1) {
promise_list.push(result_list[i].getContent());
}
return RSVP.all(promise_list);
})
.push(function (result_list) {
var value = result_list[0][gadget.state.key + '_input'],
timezone = "+0000",
year,
month,
date,
hour,
minute,
j;
if (gadget.state.timezone_style) {
timezone = result_list[1][gadget.state.key + '_select'];
}
if (field_json.ampm_time_style === 1) {
if (hour > 12) {
result[field_json.subfield_ampm_key] = "pm";
hour -= 12;
if (options === undefined || options.format === "erp5") {
if (value !== "") {
if (gadget.state.date_only === 0) {
value += "+0000";
}
value = new Date(value);
year = value.getUTCFullYear();
month = value.getUTCMonth() + 1;
date = value.getUTCDate();
if (gadget.state.hide_day === 1) {
date = 1;
}
//get time
if (gadget.state.date_only === 0) {
  • @romain sorry to comment on such an old commit, but I ran into a case where this was behaving differently from xhtml_style. I had a DateTimeField with python: False in TALES for date_only, the field is properly rendered with hours and minutes, but saving does not work because a KeyError : "Field 'hour' is not present in request object." error during validation.

    I'm thinking we should change this to be more tolerant regarding types, I mean using something like this

                    if (!gadget.state.date_only) {

    here and something similar in other places of this function checking "boolean" values of datetime field.

    This would match the python implementation of this validator that simply uses if field.get_value('date_only')

    let me know if that makes sense I'll try to do it next year.

  • I'm thinking we should change this to be more tolerant regarding types

    gadget_global.js has a asBoolean function (already used by multiple gadgets) to help correctly casting to boolean.

    As you encountered an issue related to this, it is probably time to update datetimefield.js to use it.

    Do not forget to reproduce the issue in a test.

  • Thanks @romain ( et bonne année :) . I don't understand one thing, asBoolean does this:

        if (typeof obj === "string") {
          return obj.toLowerCase() === "true" || obj === "1";
        }

    so if provided with a string value like "this", it will return false, but in python such a string is true, so if we change to using asBoolean here it will be another case where ERP5JS implementation of the field behave differently from the formulator version.

    It's probably just a detail, in practice what's problematic is that it does not support boolean types and I guess they are much more common than strings here, but I wanted to raise this, because it looks a wrong behavior of asBoolean ( it seems to have been introduced in 347a9303 BTW )

  • Bonne année Jérome!

    so if provided with a string value like "this", it will return false, but in python such a string is true, so if we change to using asBoolean here it will be another case where ERP5JS implementation of the field behave differently from the formulator version.

    I don't know why those 3 lines have been introduced. I don't imagine dropping them would be a problem.

    It's probably just a detail, in practice what's problematic is that it does not support boolean types

    I don't understand. If a boolean is passed as parameter, asBoolean returns it directly. So, using it will not introduce a new problem in your case (if datetimefield.js stops comparing to 0 of course).

    asBoolean can still be useful when such TALES expression returns a python list list, as python casts an empty list to True, and javascript casts an empty array to false.

    >>> bool([])
    False
    
    -> Boolean([])
    true
  • Thanks @romain sorry for being late here. In !1873 (merged) I am trying to remove these 3 lines from asBoolean and also to make it use isEmpty so that it behaves sames a python regarding empty lists.

Please register or sign in to reply
if (gadget.state.allow_empty_time === 1) {
hour = 0;
minute = 0;
} else {
hour = value.getUTCHours();
minute = value.getUTCMinutes();
}
if (gadget.state.ampm_time_style === 1) {
if (hour > 12) {
result[gadget.state.subfield_ampm_key] = "pm";
hour -= 12;
} else {
result[gadget.state.subfield_ampm_key] = "am";
}
}
result[gadget.state.subfield_hour_key] = hour;
result[gadget.state.subfield_minute_key] = minute;
}
if (gadget.state.hidden_day_is_last_day === 1) {
if (month === 12) {
year += 1;
month = 1;
} else {
month += 1;
}
}
result[gadget.state.subfield_year_key] = year;
result[gadget.state.subfield_month_key] = month;
result[gadget.state.subfield_day_key] = date;
if (gadget.state.timezone_style) {
//set timezone
for (j = 0; j < ZONE_LIST.length; j += 1) {
if (timezone === ZONE_LIST[j][1]) {
result[gadget.state.subfield_timezone_key] =
ZONE_LIST[j][0];
}
}
}
} else {
result[field_json.subfield_ampm_key] = "am";
//if no value, return empty data
if (gadget.state.date_only === 0) {
result[gadget.state.subfield_hour_key] = "";
result[gadget.state.subfield_minute_key] = "";
}
result[gadget.state.subfield_year_key] = "";
result[gadget.state.subfield_month_key] = "";
result[gadget.state.subfield_day_key] = "";
}
return result;
}
result[field_json.subfield_hour_key] = hour;
result[field_json.subfield_minute_key] = minute;
}
if (field_json.hidden_day_is_last_day === 1) {
if (month === 12) {
year += 1;
month = 1;
} else {
month += 1;
if (gadget.state.date_only) {
value += "T00:00";
}
}
result[field_json.subfield_year_key] = year;
result[field_json.subfield_month_key] = month;
result[field_json.subfield_day_key] = date;
if (field_json.timezone_style) {
//set timezone
select = this.element.querySelector("select");
result[field_json.subfield_timezone_key] = select.options[select.selectedIndex].value;
}
} else {
//if no value, return empty data
if (field_json.date_only === 0) {
result[field_json.subfield_hour_key] = "";
result[field_json.subfield_minute_key] = "";
}
result[field_json.subfield_year_key] = "";
result[field_json.subfield_month_key] = "";
result[field_json.subfield_day_key] = "";
}
return result;
}
if (field_json.date_only) {
value += "T00:00";
result[gadget.state.key] = value + timezone;
return result;
});
}
if (field_json.timezone_style) {
//set timezone
select = this.element.querySelector("select");
timezone = select.options[select.selectedIndex].value;
} else {
timezone = "GMT";
}
result[field_json.key] = value + zone_list[timezone];
return result;
})
.declareMethod('checkValidity', function () {
var gadget = this,
valide = true,
start_datetime = false,
end_datetime = false,
datetime_string,
select = gadget.element.querySelector("select"),
datetime,
input = gadget.element.querySelector('input'),
field_json = gadget.props.field_json;
if (!input.checkValidity()) {
return false;
}
return new RSVP.Queue()
.push(function () {
return gadget.notifyValid();
})
.push(function () {
return gadget.getContent();
})
.push(function (result) {
datetime_string = result[field_json.subfield_month_key];
datetime_string += "," + result[field_json.subfield_day_key];
datetime_string += "," + result[field_json.subfield_year_key];
if (field_json.date_only === 0) {
if (result[field_json.subfield_ampm_key] === "pm") {
result[field_json.subfield_hour_key] += 12;
}
datetime_string += " " + result[field_json.subfield_hour_key];
datetime_string += ":" + result[field_json.subfield_minute_key] + ":00";
datetime_string += "+0000";
}
if (datetime_string.indexOf("NaN") !== -1) {
valide = false;
return gadget.notifyInvalid("Invalide DateTime");
}
if (field_json.start_datetime) {
start_datetime = Date.parse(field_json.start_datetime);
}
if (field_json.end_datetime) {
end_datetime = Date.parse(field_json.end_datetime);
}
if ((start_datetime === false) && (end_datetime === false)) {
return;
}
datetime = Date.parse(datetime_string);
datetime -= (select.selectedIndex - 12) * 60 * 60 * 1000;
if (start_datetime) {
if (start_datetime > datetime) {
valide = false;
return gadget.notifyInvalid("The date and time you entered earlier than the start time");
}
}
if (end_datetime) {
if (end_datetime <= datetime) {
valide = false;
return gadget.notifyInvalid("The date and time you entered later than the end time");
}
}
})
.push(function () {
return valide;
});
.declareMethod('getTextContent', function () {
return this.state.value || "";
})
.declareService(function () {
////////////////////////////////////
// Inform when the field input is invalid
////////////////////////////////////
var field_gadget = this;
function notifyInvalid(evt) {
return field_gadget.notifyInvalid(evt.target.validationMessage);
.declareMethod('checkValidity', function () {
var gadget = this;
if (gadget.state.editable) {
return gadget.getDeclaredGadget('INPUT')
.push(function (result) {
return result.checkValidity();
});
}
// Listen to input change
return loopEventListener(
field_gadget.element.querySelector('input'),
'invalid',
false,
notifyInvalid
);
return true;
});
}(window, rJS, RSVP, document, loopEventListener));
\ No newline at end of file
}(window, rJS, RSVP));
\ No newline at end of file
......@@ -224,7 +224,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>952.4467.13140.25890</string> </value>
<value> <string>954.44614.5572.28040</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -242,7 +242,7 @@
</tuple>
<state>
<tuple>
<float>1466755364.27</float>
<float>1476799333.29</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -1025,6 +1025,18 @@ div[data-gadget-scope='header'] .ui-header ul a {
background-color: #2b2b2b;
}
/**********************************************
* Gadget: datetime field
**********************************************/
.datetimefield {
display: flex;
}
.datetimefield div[data-gadget-scope=INPUT] {
flex: 2;
}
.datetimefield div[data-gadget-scope=SELECT] {
flex: 1;
}
/**********************************************
* Listbox
**********************************************/
div[data-gadget-scope='erp5_searchfield'] .ui-input-text {
......
......@@ -236,7 +236,7 @@
</item>
<item>
<key> <string>serial</string> </key>
<value> <string>954.22971.45413.40994</string> </value>
<value> <string>954.44246.45584.9130</string> </value>
</item>
<item>
<key> <string>state</string> </key>
......@@ -254,7 +254,7 @@
</tuple>
<state>
<tuple>
<float>1475501272.94</float>
<float>1476777160.07</float>
<string>UTC</string>
</tuple>
</state>
......
......@@ -1197,6 +1197,19 @@ div[data-gadget-scope='header'] .ui-header {
}
}
/**********************************************
* Gadget: datetime field
**********************************************/
.datetimefield {
display: flex;
div[data-gadget-scope=INPUT] {
flex: 2;
}
div[data-gadget-scope=SELECT] {
flex: 1;
}
}
/**********************************************
* Listbox
**********************************************/
......
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