onreset и динамически генерируемые <select>'ы

Sad Spirit

мизантроп (Старожил PHPClub)
Команда форума
onreset и динамически генерируемые <select>'ы

Пытаюсь закрыть баг в Quickform::hierselect.

Вкратце: есть несколько связанных select'ов, выбор в первом влияет на набор option'ов в последующих. При нажатии кнопы Reset, натурально, происходит лажа: выбор в первом select'е возвращается на исходную позицию, а списки option'ов в последующих не меняются.

Написал функцию, которая отрабатывает на onreset и заполняет списки нужными значениями. Всё волшебно работает, но: функция вызывается до отработки собственно reset'а и когда отрабатывает уже он, то выбранные значения в свежеперестроенных select'ах сбрасываются. Если возвращать из функции false, то они очевидно не сбрасываются, но и не чистятся все остальные поля в форме.

Вопрос: как бы это побороть? То есть как
а) запретить сбрасывать значения в отдельных полях или
б) запустить функцию не до сброса, а после?

Пока есть решение с установкой в onreset setTimeout() с нужным кодом, но интересует более эстетичное.

-~{}~ 15.07.05 23:20:

вывалю-ка я сюда килограмм кода, чтобы было понятно, о чём вообще речь:
Код:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
	<title>hierselect megabugfix</title>
</head>

<body>
<form action="" method="get" id="frmHierselect" onreset="setTimeout(function () { hierselectReset(this, 'hotels'); }, 50);">
<div>
<table border="0">
	<tr>
		<td align="right" valign="top"><b>Input box:</b></td>
		<td valign="top" align="left"><input type="text" name="foo" value="" size="32" /></td>
	</tr>

	<tr>
		<td align="right" valign="top"><b>Location:</b></td>
		<td valign="top" align="left"><script type="text/javascript">
//<![CDATA[

if (typeof _qf_hierselect == 'undefined') {
	_qf_hierselect = {};
	_qf_hierselect_defaults = {};
}
_qf_hierselect['hotels'] = [
{'0': {"0":"London", "1":"Manchester", "2":"Liverpool"}, '2': {"5":"Fort Worth", "6":"Boston", "7":"Los Angeles"}},
{'0': {'0': {"0":"London Hotel 1", "1":"London Hotel 2"}, '1': {"0":"Manchester Hotel 1", "1":"Manchester Hotel 2"}, '2': {"1":"Liverpool Hotel 1", "3":"Liverpool Hotel 2"}}, 
	   '2': {'5': {"1":"Fort Worth Hotel 1", "3":"Fort Worth Hotel 2"}, '6': {"1":"Boston Hotel 1", "3":"Boston Hotel 2"}, '7': {"1":"Los Angeles Hotel 1", "2":"Los Angeles Hotel 2"}}}
];
_qf_hierselect_defaults['hotels'] = [2, 6, 3];

// Recursive function to get rid of eval()
function findOptions(ary, keys)
{
	var key = keys.shift();
	if (!key in ary) {
		return {};
	} else if (0 == keys.length) {
		return ary[key];
	} else {
		return findOptions(ary[key], keys);
	}
}

function swapOptionsNew(form, groupName, selectIndex)
{
	var hsValue = [];
	var ctl;

    for (var i = 0; i < _qf_hierselect[groupName].length; i++) {
        ctl = form[groupName+'['+i+']'];
        if (!ctl) {
            ctl = form[groupName+'['+i+'][]'];
        }
        if (i <= selectIndex) {
            hsValue[i] = ctl.value;
        } else {
            ctl.length = 0;
        }
    }

	var optionList = findOptions(_qf_hierselect[groupName][selectIndex], hsValue);
	var n = selectIndex + 1;
	ctl = form[groupName+'['+ n +']'];
	if (!ctl) {
	    ctl = form[groupName+'['+ n +'][]'];
	}
	var j = 0;
	for (i in optionList) {
	    ctl.options[j++] = new Option(optionList[i], i, false, false);
	}
    if (selectIndex + 1 < _qf_hierselect[groupName].length) {
        swapOptionsNew(form, groupName, selectIndex + 1);
    }
}

function hierselectReset(form, groupName)
{
	for (var i = 0; i <= _qf_hierselect[groupName].length; i++) {
		ctl = form[groupName+'['+i+']'];
        if (!ctl) {
            ctl = form[groupName+'['+i+'][]'];
        }
		
		for (var j = 0; j < ctl.options.length; j++) {
			if (ctl.options[j].value == _qf_hierselect_defaults[groupName][i]) {
				ctl.options[j].selected = true;
				//alert(ctl.options[j].value + ctl.options[j].text);
			}
		}
		if (i < _qf_hierselect[groupName].length) {
			var optionList = findOptions(_qf_hierselect[groupName][i], _qf_hierselect_defaults[groupName].slice(0, i + 1));

			var n = i + 1;
			ctl = form[groupName+'['+ n +']'];
			if (!ctl) {
			    ctl = form[groupName+'['+ n +'][]'];
			}
			j = 0;
			for (var k in optionList) {
			    ctl.options[j++] = new Option(optionList[k], k, false, false);
			}
		}
	}
	return false;
}
//]]>
</script><select name="hotels[0]" onchange="swapOptionsNew(this.form, 'hotels', 0);">
	<option value="0">England</option>
	<option value="2" selected="selected">USA</option>
</select>&nbsp;<select name="hotels[1]" onchange="swapOptionsNew(this.form, 'hotels', 1);">
	<option value="5">Fort Worth</option>
	<option value="6" selected="selected">Boston</option>
	<option value="7">Los Angeles</option>
</select>&nbsp;<select name="hotels[2]">
	<option value="1">Boston Hotel 1</option>
	<option value="3" selected="selected">Boston Hotel 2</option>
</select></td>
	</tr>
	<tr>
		<td align="right" valign="top"><b></b></td>
		<td valign="top" align="left"><input name="buttons[btnSubmit]" value="Submit" type="submit" />&nbsp;<input name="buttons[btnReset]" value="Reset" type="reset" /></td>
	</tr>
</table>
</div>
</form>
</body>
</html>
Если заменить в onreset фразу
Код:
setTimeout(function () { hierselectReset(this, 'hotels'); }, 50);
на "очевидную"
Код:
hierselectReset(this, 'hotels');
то получаем вышеописанную проблему. Если вообще убрать onreset, то получаем баг #2970 :)
 

Profic

just Profic (PHP5 BetaTeam)
Код:
<form action="" method="get" id="frmHierselect" onreset="setTimeout(function () { hierselectReset(this, 'hotels'); }, 50);">
в опере не работает, да и не должно :), работает так:
Код:
<form action="" method="get" id="frmHierselect" onreset="this_ = this; setTimeout(function () { hierselectReset(this_, 'hotels'); }, 50);">
А более эстетично так, имхо:
Код:
...
<form action="" method="get" id="frmHierselect" onreset="return hierselectResetHandler(this, 'hotels');">
...
function hierselectResetHandler(form, groupName)
{
var temp = form.onreset;
form.onreset = function() {};
form.reset();
hierselectReset(form, groupName);
form.onreset = temp;
return false;
}
...
В опере и ослике все работает на ура :) мозиллы под рукой нет.

ЗЫ. Кстати был неприятно удивлен тем, что IE6 в WinXP SP2 не дает работать этому скрипту...
 

Sad Spirit

мизантроп (Старожил PHPClub)
Команда форума
Автор оригинала: Profic
Код:
<form action="" method="get" id="frmHierselect" onreset="setTimeout(function () { hierselectReset(this, 'hotels'); }, 50);">
в опере не работает, да и не должно :), работает так:
Код:
<form action="" method="get" id="frmHierselect" onreset="this_ = this; setTimeout(function () { hierselectReset(this_, 'hotels'); }, 50);">
Да, этот момент я уже заметил и исправил.

А более эстетично так, имхо:
Код:
...
<form action="" method="get" id="frmHierselect" onreset="return hierselectResetHandler(this, 'hotels');">
...
function hierselectResetHandler(form, groupName)
{
var temp = form.onreset;
form.onreset = function() {};
form.reset();
hierselectReset(form, groupName);
form.onreset = temp;
return false;
}
...
В опере и ослике все работает на ура :) мозиллы под рукой нет.
Здесь проблема в следующем: пользователь может навесить свой скрипт на onreset, и хорошо бы его не затирать (потому как скрипт вполне может представлять из себя window.confirm('вы действительно хотите сбросить форму, которую заполняли перед этим целых 15 минут?');).

В принципе этот момент я обошёл сохранением значений hierselect'а перед setTimeout() и проверкой, изменились ли они после его выполнения; но тут вылезла другая интересная особенность. :(

Вот короче код, генерируемый текущей версией hierselect'а:

Код:
<form onreset="if (typeof _hs_setupOnReset != 'undefined') { _hs_setupOnReset(this, 'hotels'); } return window.confirm('Are you sure?');" action="F:\work\qf-hierselect-3.2.5.php" method="get" name="frmHierselect" id="frmHierselect">
<div>
<table border="0">

	<tr>
		<td align="right" valign="top"><b>Input field:</b></td>
		<td valign="top" align="left"><input size="32" name="tstText" type="text" /></td>
	</tr>
	<tr>
		<td align="right" valign="top"><b>Location:</b></td>
		<td valign="top" align="left"><script type="text/javascript">
//<![CDATA[
function _hs_findOptions(ary, keys)
{
    var key = keys.shift();
    if (!key in ary) {
        return {};
    } else if (0 == keys.length) {
        return ary[key];
    } else {
        return _hs_findOptions(ary[key], keys);
    }
}

function _hs_findSelect(form, groupName, selectIndex)
{
    if (groupName+'['+ selectIndex +']' in form) {
        return form[groupName+'['+ selectIndex +']']; 
    } else {
        return form[groupName+'['+ selectIndex +'][]']; 
    }
}

function _hs_replaceOptions(ctl, optionList)
{
    var j = 0;
    ctl.options.length = 0;
    for (i in optionList) {
        ctl.options[j++] = new Option(optionList[i], i, false, false);
    }
}

function _hs_setValue(ctl, value)
{
    var testValue = {};
    if (value instanceof Array) {
        for (var i = 0; i < value.length; i++) {
            testValue[value[i]] = true;
        }
    } else {
        testValue[value] = true;
    }
    for (var i = 0; i < ctl.options.length; i++) {
        if (ctl.options[i].value in testValue) {
            ctl.options[i].selected = true;
        }
    }
}

function _hs_getValues(form, groupName)
{
    var ret = [];
    for (var i = 0; i <= _hs_options[groupName].length; i++) {
        var ctl = _hs_findSelect(form, groupName, i);
        if ('select-multiple' == ctl.type) {
            ret[i] = [];
            for (var j = 0; j < ctl.options.length; j++) {
                if (ctl.options[j].selected) {
                    ret[i].push(ctl.options[j].value);
                }
            }
        } else {
            ret[i] = ctl.value;
        }
    }
    return ret;
}

function _hs_swapOptions(form, groupName, selectIndex)
{
    var hsValue = [];
    for (var i = 0; i <= selectIndex; i++) {
        hsValue[i] = _hs_findSelect(form, groupName, i).value;
    }

    _hs_replaceOptions(_hs_findSelect(form, groupName, selectIndex + 1), 
                       _hs_findOptions(_hs_options[groupName][selectIndex], hsValue));
    if (selectIndex + 1 < _hs_options[groupName].length) {
        _hs_swapOptions(form, groupName, selectIndex + 1);
    }
}

function _hs_onReset(form, groupName)
{
    var currVal   = _hs_getValues(form, groupName);
    var unchanged = true;
    for (var i = 0; i < currVal.length; i++) {
        if (currVal[i].toString() != _hs_values[groupName][i].toString()) {
            unchanged = false;
            break;
        }
    }
    if (unchanged) {
        return true;
    }

    for (i = 0; i <= _hs_options[groupName].length; i++) {
        _hs_setValue(_hs_findSelect(form, groupName, i), _hs_defaults[groupName][i]);
        if (i < _hs_options[groupName].length) {
            _hs_replaceOptions(_hs_findSelect(form, groupName, i + 1), 
                               _hs_findOptions(_hs_options[groupName][i], _hs_defaults[groupName].slice(0, i + 1)));
        }
    }
}

function _hs_setupOnReset(form, groupName)
{
    _hs_values[groupName] = _hs_getValues(form, groupName);
    setTimeout(function() { _hs_onReset(form, groupName); }, 50);
}

function _hs_onReload()
{
    var ctl;
    for (var i = 0; i < document.forms.length; i++) {
        for (var j in _hs_defaults) {
            if (ctl = _hs_findSelect(document.forms[i], j, 0)) {
                for (var k = 0; k < _hs_defaults[j].length; k++) {
                    _hs_setValue(_hs_findSelect(document.forms[i], j, k), _hs_defaults[j][k]);
                }
            }
        }
    }

    if (_hs_prevOnload) {
        _hs_prevOnload();
    }
}

var _hs_prevOnload = null;
if (window.onload) {
    _hs_prevOnload = window.onload;
}
window.onload = _hs_onReload;

var _hs_options = {};
var _hs_defaults = {};
var _hs_values = {};

_hs_options['hotels'] = [
{ '0': { '0': 'London', '1': 'Manchester', '2': 'Liverpool' }, '2': { '5': 'Fort Worth', 'oh\'no\\!': 'Boston', '7': 'Los Angeles' } },
{ '0': { '0': { '0': 'London Hotel 1', '1': 'London Hotel 2' }, '1': { '0': 'Manchester Hotel 1', '1': 'Manchester Hotel 2' }, '2': { '1': 'Liverpool Hotel 1', '3': 'Liverpool Hotel 2' } }, '2': { '5': { '1': 'Fort Worth Hotel 1', '3': 'Fort Worth Hotel 2' }, 'oh\'no\\!': { 'o\'really 1': 'Boston Hotel 1', 'o\'really 3': 'Boston Hotel 2' }, '7': { '1': 'Los Angeles Hotel 1', '2': 'Los Angeles Hotel 2' } } }
];
_hs_defaults['hotels'] = [2, 'oh\'no\\!', 'o\'really 3'];
//]]>
</script><select style="width: 250px" name="hotels[0]" onchange="_hs_swapOptions(this.form, 'hotels', 0);">
	<option value="0">England</option>
	<option value="2" selected="selected">USA</option>
</select><br /><select style="width: 250px" name="hotels[1]" onchange="_hs_swapOptions(this.form, 'hotels', 1);">
	<option value="5">Fort Worth</option>
	<option value="oh'no\!" selected="selected">Boston</option>
	<option value="7">Los Angeles</option>
</select><br /><select style="width: 250px" name="hotels[2]">
	<option value="o'really 1">Boston Hotel 1</option>
	<option value="o'really 3" selected="selected">Boston Hotel 2</option>
</select></td>
	</tr>
	<tr>
		<td align="right" valign="top"><b></b></td>
		<td valign="top" align="left"><input name="buttons[btnSubmit]" value="Submit" type="submit" />&nbsp;<input name="buttons[btnReset]" value="Reset" type="reset" /></td>
	</tr>
</table>
</div>
</form>
reset правильно отрабатывает в MSIE, но не в Mozilla. Был бы очень благодарен, если ты его в Opera погоняешь.
 

Profic

just Profic (PHP5 BetaTeam)
Был бы очень благодарен, если ты его в Opera погоняешь
Погонял, нормально так пашет, но мне кажется, что получается слишком большой overhead.
Я бы сделал так (при этом все остальное оставляем как в моем первом посте):
Код:
<form action="" method="get" id="frmHierselect" onreset="
var temp = function() { return window.confirm('Are you sure?'); };
if (!temp()) return false;
return hierselectResetHandler(this, 'hotels');
">
Таким образом мы уходим от setTimeout и позволяем пользователю повесить свой обработчик.
 

Sad Spirit

мизантроп (Старожил PHPClub)
Команда форума
О, точно, такое решение как-то в голову не пришло... Спасибо!

Остаётся только разобраться со случаем, когда в форме несколько hierselect'ов. Но тут уже скорее вопрос переписывания функции и вызова в onreset.
 

Profic

just Profic (PHP5 BetaTeam)
Остаётся только разобраться со случаем, когда в форме несколько hierselect'ов.
угу, например, передавать вместо одного groupName-а их массив - самый "натуральный" способ :)
 

Sad Spirit

мизантроп (Старожил PHPClub)
Команда форума
Так, тестирование (в т.ч. с использованием встроенного debugger'а) показало, что в Mozilla form.reset() при вызове из onreset handler'а видимо, вообще не отрабатывает! Урроды!

В общем придётся вернуться к setTimeout(), но вместо ненужной проверки значений использовать предложенный подход с оборачиванием старого onreset в функцию.
 

Profic

just Profic (PHP5 BetaTeam)
Так, тестирование (в т.ч. с использованием встроенного debugger'а) показало, что в Mozilla form.reset() при вызове из onreset handler'а видимо, вообще не отрабатывает! Урроды!
Мозилла, и этим все сказано :) Можно конечно поматериться на мозильщиков, может даже и поможет, но не скоро...
В таком случае setTimeout() единственный кроссбраузерный способ :(
 
Сверху