补全页:手工填过的值自动进下拉候选,后面同类地点不用再打一遍
下拉的候选是渲染时定好的,这一页手工新填的活动名/类别进不去候选,
后面遇到同类地点只能照着再敲一遍——这正是「其它(自己写)」最费事的地方。
现在填过的值会被记住并补进同类下拉,标成「新填:X」,插在建议之后、
真正的候选之前,一眼就能挑到;本页实时同步(打字即生效),
翻页和点「用高德分析」重载也留得住,提交成功后清掉(那之后它们已经是真候选了)。
顺带两处:
- 草稿保存改成按 name 合并,翻页不再把上一页填的覆盖掉;分页链接也接了保存。
- 重画候选前先把「这一格已经选中的新填值」留住再还回去,
否则改一下文本框就会把别的行选好的值悄悄抹成空。
文本框的监听直接挂在框上而不是委托到 document:`new Event('input')` 默认不冒泡,
探针第一次就是这么假绿的——直接挂不依赖冒泡,也少一层隐式约定。
This commit is contained in:
@@ -301,3 +301,24 @@ func TestFillupCustomActivityEmptyIsSkipped(t *testing.T) {
|
||||
t.Fatalf("没填名字不该建类型,活动数 %d → %d", before, n)
|
||||
}
|
||||
}
|
||||
|
||||
// 「手工填过的值自动进候选」是纯前端行为,Go 测不到运行时,只能钉住它还在模板里。
|
||||
// 这东西少一半都不会报错,只会悄悄退回「后面每行都得重新打一遍」——
|
||||
// 所以把三个关键标记都钉住:存下来的键、注入选项的标记、显示用的前缀。
|
||||
func TestFillupKeepsHandwrittenCandidates(t *testing.T) {
|
||||
f := newMovementFixture(t)
|
||||
fillupPlace(t, f)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/admin/fillup", nil).WithContext(f.ctx)
|
||||
f.srv.adminFillupPage(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("渲染失败: %d", rec.Code)
|
||||
}
|
||||
body := rec.Body.String()
|
||||
for _, want := range []string{"rond-fillup-newopts", "data-local", "新填:"} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("模板里找不到 %q —— 「手工填过的值自动进候选」失效了", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
<script>
|
||||
(function () {
|
||||
var KEY = 'rond-fillup-draft';
|
||||
var NEW = 'rond-fillup-newopts';
|
||||
|
||||
// 活动和类别是同一套下拉:选了「其它(自己写)」就露出旁边的文本框
|
||||
document.querySelectorAll('select.fill-sel').forEach(function (sel) {
|
||||
@@ -127,6 +128,70 @@
|
||||
sync();
|
||||
});
|
||||
|
||||
// 下拉的候选是渲染时定好的,手工新填的名字进不了候选,后面遇到同类地点还得重打一遍。
|
||||
// 所以把填过的值记下来补进同类下拉:本页实时生效,翻页/点「用高德分析」也留得住,
|
||||
// 提交成功后清掉(那之后它们已经在真正的候选里了)。
|
||||
var fromOther = {act: [], cat: []}; // 从别处(别的页/上次)带来的
|
||||
var fromPage = {act: [], cat: []}; // 本页文本框里当前填着的
|
||||
try {
|
||||
var saved = JSON.parse(sessionStorage.getItem(NEW) || '{}');
|
||||
fromOther.act = saved.act || [];
|
||||
fromOther.cat = saved.cat || [];
|
||||
} catch (e) { /* 坏数据就当没有 */ }
|
||||
|
||||
var paint = function (prefix) {
|
||||
var vals = fromOther[prefix].concat(fromPage[prefix].filter(function (v) {
|
||||
return fromOther[prefix].indexOf(v) < 0;
|
||||
}));
|
||||
document.querySelectorAll('select[name^="' + prefix + '_"]').forEach(function (sel) {
|
||||
// 重画不能动用户已经选好的东西:这一格选的要是「新填」里的值,先留住它
|
||||
var keep = sel.value;
|
||||
if (keep && vals.indexOf(keep) < 0 && [].some.call(sel.options, function (o) {
|
||||
return o.value === keep && o.getAttribute('data-local');
|
||||
})) vals.push(keep);
|
||||
[].forEach.call(sel.querySelectorAll('option[data-local]'), function (o) { o.remove(); });
|
||||
// 插在「(不动)」/建议之后、真正的候选之前,免得沉到列表最底下去找
|
||||
var anchor = null;
|
||||
for (var i = 0; i < sel.options.length; i++) {
|
||||
var o = sel.options[i];
|
||||
if (o.getAttribute('data-local') || !o.value || o.value === sel.dataset.hint) continue;
|
||||
anchor = o;
|
||||
break;
|
||||
}
|
||||
vals.forEach(function (v) {
|
||||
if ([].some.call(sel.options, function (o) { return o.value === v; })) return;
|
||||
var o = document.createElement('option');
|
||||
o.value = v;
|
||||
o.textContent = '新填:' + v;
|
||||
o.setAttribute('data-local', '1');
|
||||
sel.insertBefore(o, anchor);
|
||||
});
|
||||
// 选项重插之后浏览器不会自己选回来,得手工还回去
|
||||
if (keep && sel.value !== keep) sel.value = keep;
|
||||
});
|
||||
};
|
||||
|
||||
var collect = function (prefix) {
|
||||
fromPage[prefix] = [];
|
||||
document.querySelectorAll('input[name^="' + prefix + '_"][name$="__custom"]').forEach(function (box) {
|
||||
var v = box.value.trim();
|
||||
if (v && fromPage[prefix].indexOf(v) < 0) fromPage[prefix].push(v);
|
||||
});
|
||||
paint(prefix);
|
||||
try {
|
||||
sessionStorage.setItem(NEW, JSON.stringify({
|
||||
act: fromOther.act.concat(fromPage.act),
|
||||
cat: fromOther.cat.concat(fromPage.cat)
|
||||
}));
|
||||
} catch (e) { /* 隐私模式就算了 */ }
|
||||
};
|
||||
|
||||
// 文本框里打字就同步,不用等失焦或提交。直接挂在框上,不靠事件冒泡——
|
||||
// 省得以后有人在这行 DOM 上动手脚(或程序化派发)时静默失效。
|
||||
document.querySelectorAll('input[name$="__custom"]').forEach(function (box) {
|
||||
box.addEventListener('input', function () { collect(box.name.split('_')[0]); });
|
||||
});
|
||||
|
||||
// 采纳本页建议:把建议项选上(活动和类别都是下拉)
|
||||
var pick = document.getElementById('pick-hints');
|
||||
if (pick) {
|
||||
@@ -141,18 +206,25 @@
|
||||
});
|
||||
}
|
||||
|
||||
// 「用高德分析」会整页重载,先把这一页填过的存起来、回来再恢复——
|
||||
// 否则挑了半天,点一下就全没了。
|
||||
// 离开本页(翻页 / 「用高德分析」)之前把填过的存起来、回来接着填——
|
||||
// 否则挑了半天,点一下就全没了。按 name 合并,所以翻页也留得住。
|
||||
var save = function () {
|
||||
var draft = {};
|
||||
try { draft = JSON.parse(sessionStorage.getItem(KEY) || '{}'); } catch (e) { draft = {}; }
|
||||
document.querySelectorAll('select.fill-sel, input[name$="__custom"]').forEach(function (el) {
|
||||
if (el.value) draft[el.name] = el.value;
|
||||
else delete draft[el.name]; // 本页明确清空的就是不要了,别让旧值顶上来
|
||||
});
|
||||
try { sessionStorage.setItem(KEY, JSON.stringify(draft)); } catch (e) { /* 隐私模式就算了 */ }
|
||||
};
|
||||
var amap = document.getElementById('amap-btn');
|
||||
if (amap) amap.addEventListener('click', save);
|
||||
document.querySelectorAll('.pager a').forEach(function (a) { a.addEventListener('click', save); });
|
||||
|
||||
// 先把「新填」的候选补上再恢复草稿:草稿里选中的可能就是新填的值,
|
||||
// 选项不在跟前的话 select.value 是设不进去的。
|
||||
paint('act');
|
||||
paint('cat');
|
||||
try {
|
||||
var draft = JSON.parse(sessionStorage.getItem(KEY) || '{}');
|
||||
Object.keys(draft).forEach(function (name) {
|
||||
@@ -162,11 +234,17 @@
|
||||
if (el.tagName === 'SELECT') el.dispatchEvent(new Event('change'));
|
||||
});
|
||||
} catch (e) { /* 坏数据就当没有 */ }
|
||||
// 恢复出来的文本框(`set value` 不触发 input)也要算进「新填」
|
||||
collect('act');
|
||||
collect('cat');
|
||||
|
||||
// 提交成功后草稿就没用了,清掉,免得下次进来还带着旧选择
|
||||
var form = document.getElementById('fillup-form');
|
||||
if (form) form.addEventListener('submit', function () {
|
||||
try { sessionStorage.removeItem(KEY); } catch (e) {}
|
||||
try {
|
||||
sessionStorage.removeItem(KEY);
|
||||
sessionStorage.removeItem(NEW);
|
||||
} catch (e) {}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user