Bug 636627 - Implement stepDown() and stepUp() for <input type='number'>. r=sicking

--HG--
extra : rebase_source : 94d0543e68d5c2aa021e07b8b62d674e280f10dd
This commit is contained in:
Mounir Lamouri 2012-07-05 16:33:47 +02:00
Родитель a643785d84
Коммит 2e4b9d817e
5 изменённых файлов: 504 добавлений и 15 удалений

Просмотреть файл

@ -1060,6 +1060,129 @@ nsHTMLInputElement::GetList(nsIDOMHTMLElement** aValue)
return NS_OK;
}
void
nsHTMLInputElement::SetValue(double aValue)
{
nsAutoString value;
value.AppendFloat(aValue);
SetValue(value);
}
double
nsHTMLInputElement::GetStepBase() const
{
if (!HasAttr(kNameSpaceID_None, nsGkAtoms::min)) {
return kDefaultStepBase;
}
nsAutoString minStr;
GetAttr(kNameSpaceID_None, nsGkAtoms::min, minStr);
PRInt32 ec;
double stepBase = minStr.ToDouble(&ec);
return NS_FAILED(ec) ? kDefaultStepBase : stepBase;
}
nsresult
nsHTMLInputElement::ApplyStep(PRInt32 aStep)
{
if (!DoStepDownStepUpApply()) {
return NS_ERROR_DOM_INVALID_STATE_ERR;
}
double step = GetStep();
if (step == kStepAny) {
return NS_ERROR_DOM_INVALID_STATE_ERR;
}
double value = GetValueAsDouble();
if (value != value) { // NaN
return NS_OK;
}
// TODO: refactorize with GetMin(), see bug 636634.
double min = std::numeric_limits<double>::quiet_NaN();
if (HasAttr(kNameSpaceID_None, nsGkAtoms::min)) {
nsAutoString minStr;
GetAttr(kNameSpaceID_None, nsGkAtoms::min, minStr);
PRInt32 ec;
double minTmp = minStr.ToDouble(&ec);
if (!NS_FAILED(ec)) {
min = minTmp;
}
}
// TODO: refactorize with GetMax(), see bug 636634.
double max = std::numeric_limits<double>::quiet_NaN();
if (HasAttr(kNameSpaceID_None, nsGkAtoms::max)) {
nsAutoString maxStr;
GetAttr(kNameSpaceID_None, nsGkAtoms::max, maxStr);
PRInt32 ec;
double maxTmp = maxStr.ToDouble(&ec);
if (!NS_FAILED(ec)) {
// "max - (max - stepBase) % step" is the nearest valid value to max.
max = maxTmp - NS_floorModulo(maxTmp - GetStepBase(), step);
}
}
// Cases where we are clearly going in the wrong way.
// We don't use ValidityState because we can be higher than the maximal
// allowed value and still not suffer from range overflow in the case of
// of the value specified in @max isn't in the step.
if ((value <= min && aStep < 0) ||
(value >= max && aStep > 0)) {
return NS_OK;
}
if (GetValidityState(VALIDITY_STATE_STEP_MISMATCH) &&
value != min && value != max) {
if (aStep > 0) {
value -= NS_floorModulo(value - GetStepBase(), step);
} else if (aStep < 0) {
value -= NS_floorModulo(value - GetStepBase(), step);
value += step;
}
}
value += aStep * step;
// When stepUp() is called and the value is below min, we should clamp on
// min unless stepUp() moves us higher than min.
if (GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW) && aStep > 0 &&
value <= min) {
MOZ_ASSERT(min == min); // min can't be NaN if we are here!
value = min;
// Same goes for stepDown() and max.
} else if (GetValidityState(VALIDITY_STATE_RANGE_OVERFLOW) && aStep < 0 &&
value >= max) {
MOZ_ASSERT(max == max); // max can't be NaN if we are here!
value = max;
// If we go down, we want to clamp on min.
} else if (aStep < 0 && min == min) {
value = NS_MAX(value, min);
// If we go up, we want to clamp on max.
} else if (aStep > 0 && max == max) {
value = NS_MIN(value, max);
}
SetValue(value);
return NS_OK;
}
NS_IMETHODIMP
nsHTMLInputElement::StepDown(PRInt32 n, PRUint8 optional_argc)
{
return ApplyStep(optional_argc ? -n : -1);
}
NS_IMETHODIMP
nsHTMLInputElement::StepUp(PRInt32 n, PRUint8 optional_argc)
{
return ApplyStep(optional_argc ? n : 1);
}
NS_IMETHODIMP
nsHTMLInputElement::MozGetFileNameArray(PRUint32 *aLength, PRUnichar ***aFileNames)
{
@ -3897,21 +4020,8 @@ nsHTMLInputElement::HasStepMismatch() const
return false;
}
double stepBase = kDefaultStepBase;
if (HasAttr(kNameSpaceID_None, nsGkAtoms::min)) {
nsAutoString minStr;
GetAttr(kNameSpaceID_None, nsGkAtoms::min, minStr);
PRInt32 ec;
stepBase = minStr.ToDouble(&ec);
if (NS_FAILED(ec)) {
stepBase = kDefaultStepBase;
}
}
// Value has to be an integral multiple of step.
return fmod(value - stepBase, step) != 0;
return fmod(value - GetStepBase(), step) != 0;
}
void

Просмотреть файл

@ -479,6 +479,11 @@ protected:
*/
bool DoesStepApply() const { return DoesMinMaxApply(); }
/**
* Returns if stepDown and stepUp methods apply for the current type.
*/
bool DoStepDownStepUpApply() const { return DoesStepApply(); }
/**
* Returns if the maxlength attribute applies for the current type.
*/
@ -557,6 +562,13 @@ protected:
*/
double GetValueAsDouble() const;
/**
* Sets the value of the element to the string representation of the double.
*
* @param aValue The double that will be used to set the value.
*/
void SetValue(double aValue);
/**
* Update the HAS_RANGE bit field value.
*/
@ -570,6 +582,22 @@ protected:
*/
double GetStep() const;
/**
* Return the base used to compute if a value matches step.
* Basically, it's the min attribute if present and a default value otherwise.
*
* @return The step base.
*/
double GetStepBase() const;
/**
* Apply a step change from stepUp or stepDown by multiplying aStep by the
* current step value.
*
* @param aStep The value used to be multiplied against the step value.
*/
nsresult ApplyStep(PRInt32 aStep);
nsCOMPtr<nsIControllers> mControllers;
/*

Просмотреть файл

@ -45,6 +45,7 @@ _TEST_FILES = \
test_max_attribute.html \
test_min_attribute.html \
test_step_attribute.html \
test_stepup_stepdown.html \
$(NULL)
libs:: $(_TEST_FILES)

Просмотреть файл

@ -0,0 +1,347 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=636627
-->
<head>
<title>Test for Bug 636627</title>
<script type="application/javascript" src="/MochiKit/packed.js"></script>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=636627">Mozilla Bug 636627</a>
<p id="display"></p>
<pre id="test">
<script type="application/javascript">
/** Test for Bug 636627 **/
/**
* This test is testing stepDown() and stepUp().
*/
function checkPresence()
{
var input = document.createElement('input');
is('stepDown' in input, true, 'stepDown() should be an input function');
is('stepUp' in input, true, 'stepUp() should be an input function');
}
function checkAvailability()
{
var testData =
[
["text", false],
["password", false],
["search", false],
["telephone", false],
["email", false],
["url", false],
["hidden", false],
["checkbox", false],
["radio", false],
["file", false],
["submit", false],
["image", false],
["reset", false],
["button", false],
["number", true],
// The next types have not been implemented but will fallback to "text"
// which has the same value.
["color", false],
];
var todoList =
[
["datetime", true],
["date", true],
["month", true],
["week", true],
["time", true],
["datetime-local", true],
["range", true],
];
var element = document.createElement("input");
element.setAttribute('value', '0');
for each (data in testData) {
var exceptionCaught = false;
element.type = data[0];
try {
element.stepDown();
} catch (e) {
exceptionCaught = true;
}
is(exceptionCaught, !data[1], "stepDown() availability is not correct");
exceptionCaught = false;
try {
element.stepUp();
} catch (e) {
exceptionCaught = true;
}
is(exceptionCaught, !data[1], "stepUp() availability is not correct");
}
for each (data in todoList) {
var exceptionCaught = false;
element.type = data[0];
try {
element.stepDown();
} catch (e) {
exceptionCaught = true;
}
todo_is(exceptionCaught, !data[1],
"stepDown() availability is not correct");
exceptionCaught = false;
try {
element.stepUp();
} catch (e) {
exceptionCaught = true;
}
todo_is(exceptionCaught, !data[1],
"stepUp() availability is not correct");
}
}
function checkStepDownForNumber()
{
// This testData is very similar to the one in checkStepUpForNumber
// with some changes relative to stepDown.
var testData = [
/* Initial value | step | min | max | stepDown arg | final value | exception */
// Regular case.
[ '1', null, null, null, null, '0', false ],
// Argument testing.
[ '1', null, null, null, 1, '0', false ],
[ '9', null, null, null, 9, '0', false ],
[ '1', null, null, null, -1, '2', false ],
[ '1', null, null, null, 0, '1', false ],
// Float values are rounded to integer (1.1 -> 1).
[ '1', null, null, null, 1.1, '0', false ],
// With step values.
[ '1', '0.5', null, null, null, '0.5', false ],
[ null, '0.5', null, null, null, '0', false ],
[ null, '0.5', null, null, null, '-0.5', false ],
[ '1', '0.25', null, null, 4, '0', false ],
// step = 0 isn't allowed (-> step = 1).
[ '1', '0', null, null, null, '0', false ],
// step < 0 isn't allowed (-> step = 1).
[ '1', '-1', null, null, null, '0', false ],
// step = NaN isn't allowed (-> step = 1).
[ '1', 'foo', null, null, null, '0', false ],
// Min values testing.
[ '1', '1', 'foo', null, null, '0', false ],
[ '1', null, '-10', null, null, '0', false ],
[ '1', null, '0', null, null, '0', false ],
[ '1', null, '10', null, null, '1', false ],
[ '1', null, '2', null, null, '1', false ],
[ '1', null, '1', null, null, '1', false ],
// Max values testing.
[ '1', '1', null, 'foo', null, '0', false ],
[ '1', null, null, '10', null, '0', false ],
[ '1', null, null, '0', null, '0', false ],
[ '1', null, null, '-10', null, '-10', false ],
[ '1', null, null, '1', null, '0', false ],
[ '5', null, null, '3', '3', '2', false ],
[ '5', '2', null, '3', '2', '2', false ],
[ '-3', '5', '-10', '-3', null, '-5', false ],
// Step mismatch.
[ '1', '2', '-2', null, null, '0', false ],
[ '3', '2', '-2', null, null, '2', false ],
[ '3', '2', '-2', null, '2', '0', false ],
[ '3', '2', '-2', null, '-2', '6', false ],
[ '1', '2', null, null, null, '0', false ],
[ '1', '2', '-2', null, null, '0', false ],
[ '1', '3', null, null, null, '0', false ],
[ '2', '3', null, null, null, '0', false ],
[ '2', '3', '1', null, null, '1', false ],
[ '5', '3', '1', null, null, '4', false ],
[ '3', '2', null, null, null, '2', false ],
[ '5', '2', null, null, null, '4', false ],
[ '6', '2', '1', null, null, '5', false ],
[ '8', '3', '1', null, null, '7', false ],
[ '9', '2', '-10', null, null, '8', false ],
[ '7', '3', '-10', null, null, '5', false ],
[ '-2', '3', '-10', null, null, '-4', false ],
// Clamping.
[ '0', '2', '-1', null, null, '-1', false ],
[ '10', '2', '0', '4', '10', '0', false ],
[ '10', '2', '0', '4', '5', '0', false ],
// value = "" (NaN).
[ '', null, null, null, null, '', false ],
// With step = 'any'.
[ '0', 'any', null, null, 1, null, true ],
[ '0', 'ANY', null, null, 1, null, true ],
[ '0', 'AnY', null, null, 1, null, true ],
[ '0', 'aNy', null, null, 1, null, true ],
];
var element = document.createElement("input");
element.type = 'number';
for each (var data in testData) {
if (data[0] != null) {
element.value = data[0];
}
if (data[1] != null) {
element.step = data[1];
}
if (data[2] != null) {
element.min = data[2];
}
if (data[3] != null) {
element.max = data[3];
}
var exceptionCaught = false;
try {
if (data[4] != null) {
element.stepDown(data[4]);
} else {
element.stepDown();
}
is(element.value, data[5], "The value should be " + data[5]);
} catch (e) {
exceptionCaught = true;
is(element.value, data[0], e.name + "The value should not have changed");
is(e.name, 'InvalidStateError',
"It should be a InvalidStateError exception.");
} finally {
is(exceptionCaught, data[6], "exception status should be " + data[6]);
}
element.removeAttribute('step');
element.removeAttribute('min');
element.removeAttribute('max');
}
}
function checkStepUpForNumber()
{
// This testData is very similar to the one in checkStepDownForNumber
// with some changes relative to stepUp.
var testData = [
/* Initial value | step | min | max | stepUp arg | final value | exception */
// Regular case.
[ '1', null, null, null, null, '2', false ],
// Argument testing.
[ '1', null, null, null, 1, '2', false ],
[ '9', null, null, null, 9, '18', false ],
[ '1', null, null, null, -1, '0', false ],
[ '1', null, null, null, 0, '1', false ],
// Float values are rounded to integer (1.1 -> 1).
[ '1', null, null, null, 1.1, '2', false ],
// With step values.
[ '1', '0.5', null, null, null, '1.5', false ],
[ null, '0.5', null, null, null, '2', false ],
[ null, '0.5', null, null, null, '2.5', false ],
[ '1', '0.25', null, null, 4, '2', false ],
// step = 0 isn't allowed (-> step = 1).
[ '1', '0', null, null, null, '2', false ],
// step < 0 isn't allowed (-> step = 1).
[ '1', '-1', null, null, null, '2', false ],
// step = NaN isn't allowed (-> step = 1).
[ '1', 'foo', null, null, null, '2', false ],
// Min values testing.
[ '1', '1', 'foo', null, null, '2', false ],
[ '1', null, '-10', null, null, '2', false ],
[ '1', null, '0', null, null, '2', false ],
[ '1', null, '10', null, null, '10', false ],
[ '1', null, '2', null, null, '2', false ],
[ '1', null, '1', null, null, '2', false ],
[ '0', null, '4', null, '5', '5', false ],
[ '0', '2', '5', null, '3', '5', false ],
// Max values testing.
[ '1', '1', null, 'foo', null, '2', false ],
[ '1', null, null, '10', null, '2', false ],
[ '1', null, null, '0', null, '1', false ],
[ '1', null, null, '-10', null, '1', false ],
[ '1', null, null, '1', null, '1', false ],
[ '-3', '5', '-10', '-3', null, '-3', false ],
// Step mismatch.
[ '1', '2', '0', null, null, '2', false ],
[ '1', '2', '0', null, '2', '4', false ],
[ '8', '2', null, '9', null, '8', false ],
[ '-3', '2', null, null, null, '-2', false ],
[ '9', '3', '-10', null, null, '11', false ],
[ '7', '3', '-10', null, null, '8', false ],
[ '7', '3', '5', null, null, '8', false ],
[ '9', '4', '3', null, null, '11', false ],
[ '-2', '3', '-6', null, null, '0', false ],
[ '7', '3', '6', null, null, '9', false ],
// Clamping.
[ '1', '2', '0', '3', null, '2', false ],
[ '0', '5', '1', '8', '10', '6', false ],
[ '-9', '3', '-8', '-1', '5', '-2', false ],
[ '-9', '3', '8', '15', '15', '14', false ],
[ '-1', '3', '-1', '4', '3', '2', false ],
[ '-3', '2', null, '-2', null, '-2', false ],
[ '-3', '2', null, '-1', null, '-2', false ],
// value = "" (NaN).
[ '', null, null, null, null, '', false ],
// With step = 'any'.
[ '0', 'any', null, null, 1, null, true ],
[ '0', 'ANY', null, null, 1, null, true ],
[ '0', 'AnY', null, null, 1, null, true ],
[ '0', 'aNy', null, null, 1, null, true ],
];
var element = document.createElement("input");
element.type = 'number';
for each (var data in testData) {
if (data[0] != null) {
element.value = data[0];
}
if (data[1] != null) {
element.step = data[1];
}
if (data[2] != null) {
element.min = data[2];
}
if (data[3] != null) {
element.max = data[3];
}
var exceptionCaught = false;
try {
if (data[4] != null) {
element.stepUp(data[4]);
} else {
element.stepUp();
}
is(element.value, data[5], "The value should be " + data[5]);
} catch (e) {
exceptionCaught = true;
is(element.value, data[0], e.name + "The value should not have changed");
is(e.name, 'InvalidStateError',
"It should be a InvalidStateError exception.");
} finally {
is(exceptionCaught, data[6], "exception status should be " + data[6]);
}
element.removeAttribute('step');
element.removeAttribute('min');
element.removeAttribute('max');
}
}
checkPresence();
checkAvailability();
checkStepDownForNumber();
checkStepUpForNumber();
</script>
</pre>
</body>
</html>

Просмотреть файл

@ -20,7 +20,7 @@ interface nsIDOMValidityState;
* http://www.whatwg.org/specs/web-apps/current-work/
*/
[scriptable, uuid(e20b7e26-e952-4bba-9192-99bd1a4b3816)]
[scriptable, uuid(71fb50df-5470-4e88-9a28-6ff2912e3ca7)]
interface nsIDOMHTMLInputElement : nsIDOMHTMLElement
{
attribute DOMString accept;
@ -67,6 +67,9 @@ interface nsIDOMHTMLInputElement : nsIDOMHTMLElement
attribute DOMString defaultValue;
attribute DOMString value;
[optional_argc] void stepDown([optional] in long n);
[optional_argc] void stepUp([optional] in long n);
// The following lines are part of the constraint validation API, see:
// http://www.whatwg.org/specs/web-apps/current-work/#the-constraint-validation-api
readonly attribute boolean willValidate;