Project

General

Profile

Download (20.4 KB) Statistics
| Branch: | Tag: | Revision:

haketilo / html / options_main.js @ 6b12a034

1
/**
2
 * Hachette HTML options page main script
3
 *
4
 * Copyright (C) 2021 Wojtek Kosior
5
 * Redistribution terms are gathered in the `copyright' file.
6
 */
7

    
8
/*
9
 * IMPORTS_START
10
 * IMPORT get_remote_storage
11
 * IMPORT TYPE_PREFIX
12
 * IMPORT TYPE_NAME
13
 * IMPORT list_prefixes
14
 * IMPORT url_extract_target
15
 * IMPORT nice_name
16
 * IMPORTS_END
17
 */
18

    
19
var storage;
20
function by_id(id)
21
{
22
    return document.getElementById(id);
23
}
24

    
25
const item_li_template = by_id("item_li_template");
26
const bag_component_li_template = by_id("bag_component_li_template");
27
const chbx_component_li_template = by_id("chbx_component_li_template");
28
const radio_component_li_template = by_id("radio_component_li_template");
29
const import_li_template = by_id("import_li_template");
30
/* Make sure they are later cloned without id. */
31
item_li_template.removeAttribute("id");
32
bag_component_li_template.removeAttribute("id");
33
chbx_component_li_template.removeAttribute("id");
34
radio_component_li_template.removeAttribute("id");
35
import_li_template.removeAttribute("id");
36

    
37
function item_li_id(prefix, item)
38
{
39
    return `li_${prefix}_${item}`;
40
}
41

    
42
/* Insert into list of bags/pages/scripts */
43
function add_li(prefix, item, at_the_end=false)
44
{
45
    let ul = ul_by_prefix[prefix];
46
    let li = item_li_template.cloneNode(true);
47
    li.id = item_li_id(prefix, item);
48

    
49
    let span = li.firstElementChild;
50
    span.textContent = item;
51

    
52
    let edit_button = span.nextElementSibling;
53
    edit_button.addEventListener("click", () => edit_item(prefix, item));
54

    
55
    let remove_button = edit_button.nextElementSibling;
56
    remove_button.addEventListener("click",
57
				   () => storage.remove(prefix, item));
58

    
59
    let export_button = remove_button.nextElementSibling;
60
    export_button.addEventListener("click",
61
				   () => export_item(prefix, item));
62

    
63
    if (!at_the_end) {
64
	for (let element of ul.ul.children) {
65
	    if (element.id < li.id || element.id.startsWith("work_"))
66
		continue;
67

    
68
	    ul.ul.insertBefore(li, element);
69
	    return;
70
	}
71
    }
72

    
73
    ul.ul.appendChild(li);
74
}
75

    
76
const chbx_components_ul = by_id("chbx_components_ul");
77
const radio_components_ul = by_id("radio_components_ul");
78

    
79
function chbx_li_id(prefix, item)
80
{
81
    return `cli_${prefix}_${item}`;
82
}
83

    
84
function radio_li_id(prefix, item)
85
{
86
    return `rli_${prefix}_${item}`;
87
}
88

    
89
//TODO: refactor the 2 functions below
90

    
91
function add_chbx_li(prefix, name)
92
{
93
    if (prefix === TYPE_PREFIX.PAGE)
94
	return;
95

    
96
    let li = chbx_component_li_template.cloneNode(true);
97
    li.id = chbx_li_id(prefix, name);
98
    li.setAttribute("data-prefix", prefix);
99
    li.setAttribute("data-name", name);
100

    
101
    let chbx = li.firstElementChild;
102
    let span = chbx.nextElementSibling;
103

    
104
    span.textContent = nice_name(prefix, name);
105

    
106
    chbx_components_ul.appendChild(li);
107
}
108

    
109
var radio_component_none_li = by_id("radio_component_none_li");
110

    
111
function add_radio_li(prefix, name)
112
{
113
    if (prefix === TYPE_PREFIX.PAGE)
114
	return;
115

    
116
    let li = radio_component_li_template.cloneNode(true);
117
    li.id = radio_li_id(prefix, name);
118
    li.setAttribute("data-prefix", prefix);
119
    li.setAttribute("data-name", name);
120

    
121
    let radio = li.firstElementChild;
122
    let span = radio.nextElementSibling;
123

    
124
    span.textContent = nice_name(prefix, name);
125

    
126
    radio_components_ul.insertBefore(li, radio_component_none_li);
127
}
128

    
129
const page_payload_span = by_id("page_payload");
130

    
131
function set_page_components(components)
132
{
133
    if (components === undefined) {
134
	page_payload_span.setAttribute("data-payload", "no");
135
	page_payload_span.textContent = "(None)";
136
    } else {
137
	page_payload_span.setAttribute("data-payload", "yes");
138
	let [prefix, name] = components;
139
	page_payload_span.setAttribute("data-prefix", prefix);
140
	page_payload_span.setAttribute("data-name", name);
141
	page_payload_span.textContent = nice_name(prefix, name);
142
    }
143
}
144

    
145
const page_allow_chbx = by_id("page_allow_chbx");
146

    
147
/* Used to reset edited page. */
148
function reset_work_page_li(ul, item, settings)
149
{
150
    ul.work_name_input.value = maybe_string(item);
151
    settings = settings || {allow: false, components: undefined};
152
    page_allow_chbx.checked = !!settings.allow;
153

    
154
    set_page_components(settings.components);
155
}
156

    
157
function work_page_li_components()
158
{
159
    if (page_payload_span.getAttribute("data-payload") === "no")
160
	return undefined;
161

    
162
    let prefix = page_payload_span.getAttribute("data-prefix");
163
    let name = page_payload_span.getAttribute("data-name");
164
    return [prefix, name];
165
}
166

    
167
/* Used to get edited page data for saving. */
168
function work_page_li_data(ul)
169
{
170
    let url = ul.work_name_input.value;
171
    let settings = {
172
	components : work_page_li_components(),
173
	allow : !!page_allow_chbx.checked
174
    };
175

    
176
    return [url, settings];
177
}
178

    
179
const empty_bag_component_li = by_id("empty_bag_component_li");
180
var bag_components_ul = by_id("bag_components_ul");
181

    
182
/* Used to construct and update components list of edited bag. */
183
function add_bag_components(components)
184
{
185
    for (let component of components) {
186
	let [prefix, name] = component;
187
	let li = bag_component_li_template.cloneNode(true);
188
	li.setAttribute("data-prefix", prefix);
189
	li.setAttribute("data-name", name);
190
	let span = li.firstElementChild;
191
	span.textContent = nice_name(prefix, name);
192
	let remove_but = span.nextElementSibling;
193
	remove_but.addEventListener("click", () =>
194
				    bag_components_ul.removeChild(li));
195
	bag_components_ul.appendChild(li);
196
    }
197

    
198
    bag_components_ul.appendChild(empty_bag_component_li);
199
}
200

    
201
/* Used to reset edited bag. */
202
function reset_work_bag_li(ul, item, components)
203
{
204
    components = components || [];
205

    
206
    ul.work_name_input.value = maybe_string(item);
207
    let old_components_ul = bag_components_ul;
208
    bag_components_ul = old_components_ul.cloneNode(false);
209

    
210
    ul.work_li.insertBefore(bag_components_ul, old_components_ul);
211
    ul.work_li.removeChild(old_components_ul);
212

    
213
    add_bag_components(components);
214
}
215

    
216
/* Used to get edited bag data for saving. */
217
function work_bag_li_data(ul)
218
{
219
    let components_ul = ul.work_name_input.nextElementSibling;
220
    let component_li = components_ul.firstElementChild;
221

    
222
    let components = [];
223

    
224
    /* Last list element is empty li with id set. */
225
    while (component_li.id === '') {
226
	components.push([component_li.getAttribute("data-prefix"),
227
			 component_li.getAttribute("data-name")]);
228
	component_li = component_li.nextElementSibling;
229
    }
230

    
231
    return [ul.work_name_input.value, components];
232
}
233

    
234
const script_url_input = by_id("script_url_field");
235
const script_sha256_input = by_id("script_sha256_field");
236
const script_contents_field = by_id("script_contents_field");
237

    
238
function maybe_string(maybe_defined)
239
{
240
    return maybe_defined === undefined ? "" : maybe_defined + "";
241
}
242

    
243
/* Used to reset edited script. */
244
function reset_work_script_li(ul, name, data)
245
{
246
    ul.work_name_input.value = maybe_string(name);
247
    if (data === undefined)
248
	data = {};
249
    script_url_input.value = maybe_string(data.url);
250
    script_sha256_input.value = maybe_string(data.hash);
251
    script_contents_field.value = maybe_string(data.text);
252
}
253

    
254
/* Used to get edited script data for saving. */
255
function work_script_li_data(ul)
256
{
257
    return [ul.work_name_input.value, {
258
	url : script_url_input.value,
259
	hash : script_sha256_input.value,
260
	text : script_contents_field.value
261
    }];
262
}
263

    
264
function cancel_work(prefix)
265
{
266
    let ul = ul_by_prefix[prefix];
267

    
268
    if (ul.state === UL_STATE.IDLE)
269
	return;
270

    
271
    if (ul.state === UL_STATE.EDITING_ENTRY) {
272
	add_li(prefix, ul.edited_item);
273
    }
274

    
275
    ul.work_li.classList.add("hide");
276
    ul.state = UL_STATE.IDLE;
277
}
278

    
279
function save_work(prefix)
280
{
281
    let ul = ul_by_prefix[prefix];
282

    
283
    if (ul.state === UL_STATE.IDLE)
284
	return;
285

    
286
    let [item, data] = ul.get_work_li_data(ul);
287

    
288
    /* Here we fire promises and return without waiting. */
289

    
290
    if (ul.state === UL_STATE.EDITING_ENTRY)
291
	storage.replace(prefix, ul.edited_item, item, data);
292
    if (ul.state === UL_STATE.ADDING_ENTRY)
293
	storage.set(prefix, item, data);
294

    
295
    cancel_work(prefix);
296
}
297

    
298
function edit_item(prefix, item)
299
{
300
    cancel_work(prefix);
301

    
302
    let ul = ul_by_prefix[prefix];
303
    let li = by_id(item_li_id(prefix, item));
304

    
305
    if (li === null) {
306
	add_new_item(prefix, item);
307
	return;
308
    }
309

    
310
    ul.reset_work_li(ul, item, storage.get(prefix, item));
311
    ul.ul.insertBefore(ul.work_li, li);
312
    ul.ul.removeChild(li);
313
    ul.work_li.classList.remove("hide");
314

    
315
    ul.state = UL_STATE.EDITING_ENTRY;
316
    ul.edited_item = item;
317
}
318

    
319
const file_downloader = by_id("file_downloader");
320

    
321
function recursively_export_item(prefix, name, added_items, items_data)
322
{
323
    let key = prefix + name;
324

    
325
    if (added_items.has(key))
326
	return;
327

    
328
    let data = storage.get(prefix, name);
329
    if (data === undefined) {
330
	console.log(`${TYPE_NAME[prefix]} '${name}' for export not found`);
331
	return;
332
    }
333

    
334
    if (prefix !== TYPE_PREFIX.SCRIPT) {
335
	let components = prefix === TYPE_PREFIX.BAG ?
336
	    data : [data.components];
337

    
338
	for (let [comp_prefix, comp_name] of components) {
339
	    recursively_export_item(comp_prefix, comp_name,
340
				    added_items, items_data);
341
	}
342
    }
343

    
344
    items_data.push({[key]: data});
345
    added_items.add(key);
346
}
347

    
348
function export_item(prefix, name)
349
{
350
    let added_items = new Set();
351
    let items_data = [];
352
    recursively_export_item(prefix, name, added_items, items_data);
353
    let file = new Blob([JSON.stringify(items_data)],
354
			{type: "application/json"});
355
    let url = URL.createObjectURL(file);
356
    file_downloader.setAttribute("href", url);
357
    file_downloader.setAttribute("download", prefix + name + ".json");
358
    file_downloader.click();
359
    file_downloader.removeAttribute("href");
360
    URL.revokeObjectURL(url);
361
}
362

    
363
function add_new_item(prefix, name)
364
{
365
    cancel_work(prefix);
366

    
367
    let ul = ul_by_prefix[prefix];
368
    ul.reset_work_li(ul);
369
    ul.work_li.classList.remove("hide");
370
    ul.ul.appendChild(ul.work_li);
371

    
372
    if (name !== undefined)
373
	ul.work_name_input.value = name;
374
    ul.state = UL_STATE.ADDING_ENTRY;
375
}
376

    
377
const chbx_components_window = by_id("chbx_components_window");
378

    
379
function bag_components()
380
{
381
    chbx_components_window.classList.remove("hide");
382
    radio_components_window.classList.add("hide");
383

    
384
    for (let li of chbx_components_ul.children) {
385
	let chbx = li.firstElementChild;
386
	chbx.checked = false;
387
    }
388
}
389

    
390
function commit_bag_components()
391
{
392
    let selected = [];
393

    
394
    for (let li of chbx_components_ul.children) {
395
	let chbx = li.firstElementChild;
396
	if (!chbx.checked)
397
	    continue;
398

    
399
	selected.push([li.getAttribute("data-prefix"),
400
		       li.getAttribute("data-name")]);
401
    }
402

    
403
    add_bag_components(selected);
404
    cancel_components();
405
}
406

    
407
const radio_components_window = by_id("radio_components_window");
408
var radio_component_none_input = by_id("radio_component_none_input");
409

    
410
function page_components()
411
{
412
    radio_components_window.classList.remove("hide");
413
    chbx_components_window.classList.add("hide");
414

    
415
    radio_component_none_input.checked = true;
416

    
417
    let components = work_page_li_components();
418
    if (components === undefined)
419
	return;
420

    
421
    let [prefix, item] = components;
422
    let li = by_id(radio_li_id(prefix, item));
423
    if (li === null)
424
	radio_component_none_input.checked = false;
425
    else
426
	li.firstElementChild.checked = true;
427
}
428

    
429
function commit_page_components()
430
{
431
    let components = null;
432

    
433
    for (let li of radio_components_ul.children) {
434
	let radio = li.firstElementChild;
435
	if (!radio.checked)
436
	    continue;
437

    
438
	components = [li.getAttribute("data-prefix"),
439
		      li.getAttribute("data-name")];
440

    
441
	if (radio.id === "radio_component_none_input")
442
	    components = undefined;
443

    
444
	break;
445
    }
446

    
447
    if (components !== null)
448
	set_page_components(components);
449
    cancel_components();
450
}
451

    
452
function cancel_components()
453
{
454
    chbx_components_window.classList.add("hide");
455
    radio_components_window.classList.add("hide");
456
}
457

    
458
const UL_STATE = {
459
    EDITING_ENTRY : 0,
460
    ADDING_ENTRY : 1,
461
    IDLE : 2
462
};
463

    
464
const ul_by_prefix = {
465
    [TYPE_PREFIX.PAGE] : {
466
	ul : by_id("pages_ul"),
467
	work_li : by_id("work_page_li"),
468
	work_name_input : by_id("page_url_field"),
469
	reset_work_li : reset_work_page_li,
470
	get_work_li_data : work_page_li_data,
471
	select_components : page_components,
472
	commit_components : commit_page_components,
473
	state : UL_STATE.IDLE,
474
	edited_item : undefined,
475
    },
476
    [TYPE_PREFIX.BAG] : {
477
	ul : by_id("bags_ul"),
478
	work_li : by_id("work_bag_li"),
479
	work_name_input : by_id("bag_name_field"),
480
	reset_work_li : reset_work_bag_li,
481
	get_work_li_data : work_bag_li_data,
482
	select_components : bag_components,
483
	commit_components : commit_bag_components,
484
	state : UL_STATE.IDLE,
485
	edited_item : undefined,
486
    },
487
    [TYPE_PREFIX.SCRIPT] : {
488
	ul : by_id("scripts_ul"),
489
	work_li : by_id("work_script_li"),
490
	work_name_input : by_id("script_name_field"),
491
	reset_work_li : reset_work_script_li,
492
	get_work_li_data : work_script_li_data,
493
	state : UL_STATE.IDLE,
494
	edited_item : undefined,
495
    }
496
}
497

    
498
const import_window = by_id("import_window");
499
const import_loading_radio = by_id("import_loading_radio");
500
const import_failed_radio = by_id("import_failed_radio");
501
const import_selection_radio = by_id("import_selection_radio");
502
const bad_file_errormsg = by_id("bad_file_errormsg");
503

    
504
/*
505
 * Newer browsers could utilise `text' method of File objects.
506
 * Older ones require FileReader.
507
 */
508

    
509
function _read_file(file, resolve, reject)
510
{
511
    let reader = new FileReader();
512

    
513
    reader.onload = () => resolve(reader.result);
514
    reader.onerror = () => reject(reader.error);
515
    reader.readAsText(file);
516
}
517

    
518
function read_file(file)
519
{
520
    return new Promise((resolve, reject) =>
521
		       _read_file(file, resolve, reject));
522
}
523

    
524
async function import_from_file(event)
525
{
526
    let files = event.target.files;
527
    if (files.length < 1)
528
	return;
529

    
530
    import_window.classList.remove("hide");
531
    import_loading_radio.checked = true;
532

    
533
    let result = undefined;
534

    
535
    try {
536
	result = JSON.parse(await read_file(files[0]));
537
    } catch(e) {
538
	bad_file_errormsg.textContent = "" + e;
539
	import_failed_radio.checked = true;
540
	return;
541
    }
542

    
543
    let errormsg = validate_settings(result);
544
    if (errormsg !== false) {
545
	bad_file_errormsg.textContent = errormsg;
546
	import_failed_radio.checked = true;
547
	return;
548
    }
549

    
550
    populate_import_list(result);
551
    import_selection_radio.checked = true;
552
}
553

    
554
function validate_settings(settings)
555
{
556
    // TODO
557
    return false;
558
}
559

    
560
function import_li_id(prefix, item)
561
{
562
    return `ili_${prefix}_${item}`;
563
}
564

    
565
let import_ul = by_id("import_ul");
566
let import_chbxs_colliding = undefined;
567
let settings_import_map = undefined;
568

    
569
function populate_import_list(settings)
570
{
571
    let old_children = import_ul.children;
572
    while (old_children[0] !== undefined)
573
	import_ul.removeChild(old_children[0]);
574

    
575
    import_chbxs_colliding = [];
576
    settings_import_map = new Map();
577

    
578
    for (let setting of settings) {
579
	let [key, value] = Object.entries(setting)[0];
580
	let prefix = key[0];
581
	let name = key.substring(1);
582
	add_import_li(prefix, name);
583
	settings_import_map.set(key, value);
584
    }
585
}
586

    
587
function add_import_li(prefix, name)
588
{
589
    let li = import_li_template.cloneNode(true);
590
    let name_span = li.firstElementChild;
591
    let chbx = name_span.nextElementSibling;
592
    let warning_span = chbx.nextElementSibling;
593

    
594
    li.setAttribute("data-prefix", prefix);
595
    li.setAttribute("data-name", name);
596
    li.id = import_li_id(prefix, name);
597
    name_span.textContent = nice_name(prefix, name);
598

    
599
    if (storage.get(prefix, name) !== undefined) {
600
	import_chbxs_colliding.push(chbx);
601
	warning_span.textContent = "(will overwrite existing setting!)";
602
    }
603

    
604
    import_ul.appendChild(li);
605
}
606

    
607
function check_all_imports()
608
{
609
    for (let li of import_ul.children)
610
	li.firstElementChild.nextElementSibling.checked = true;
611
}
612

    
613
function uncheck_all_imports()
614
{
615
    for (let li of import_ul.children)
616
	li.firstElementChild.nextElementSibling.checked = false;
617
}
618

    
619
function uncheck_colliding_imports()
620
{
621
    for (let chbx of import_chbxs_colliding)
622
	chbx.checked = false;
623
}
624

    
625
const file_opener_form = by_id("file_opener_form");
626

    
627
function hide_import_window()
628
{
629
    import_window.classList.add("hide");
630
    /* Let GC free some memory */
631
    import_chbxs_colliding = undefined;
632
    settings_import_map = undefined;
633

    
634
    /*
635
     * Reset file <input>. Without this, a second attempt to import the same
636
     * file would result in "change" event on happening on <input> element.
637
     */
638
    file_opener_form.reset();
639
}
640

    
641
function commit_import()
642
{
643
    for (let li of import_ul.children) {
644
	let chbx = li.firstElementChild.nextElementSibling;
645

    
646
	if (!chbx.checked)
647
	    continue;
648

    
649
	let prefix = li.getAttribute("data-prefix");
650
	let name = li.getAttribute("data-name");
651
	let key = prefix + name;
652
	let value = settings_import_map.get(key);
653
	storage.set(prefix, name, value);
654
    }
655

    
656
    hide_import_window();
657
}
658

    
659
function initialize_import_facility()
660
{
661
    let import_but = by_id("import_but");
662
    let file_opener = by_id("file_opener");
663
    let import_failok_but = by_id("import_failok_but");
664
    let check_all_import_but = by_id("check_all_import_but");
665
    let uncheck_all_import_but = by_id("uncheck_all_import_but");
666
    let uncheck_existing_import_but = by_id("uncheck_existing_import_but");
667
    let commit_import_but = by_id("commit_import_but");
668
    let cancel_import_but = by_id("cancel_import_but");
669
    import_but.addEventListener("click", () => file_opener.click());
670
    file_opener.addEventListener("change", import_from_file);
671
    import_failok_but.addEventListener("click", hide_import_window);
672
    check_all_import_but.addEventListener("click", check_all_imports);
673
    uncheck_all_import_but.addEventListener("click", uncheck_all_imports);
674
    uncheck_colliding_import_but
675
	.addEventListener("click", uncheck_colliding_imports);
676
    commit_import_but.addEventListener("click", commit_import);
677
    cancel_import_but.addEventListener("click", hide_import_window);
678
}
679

    
680
/*
681
 * If url has a target appended, e.g.
682
 * chrome-extension://hnhmbnpohhlmhehionjgongbnfdnabdl/html/options.html#smyhax
683
 * that target will be split into prefix and item name (e.g. "s" and "myhax")
684
 * and editing of that respective item will be started.
685
 *
686
 * We don't need to worry about the state of the page (e.g. some editing being
687
 * in progress) in jump_to_item() - this function is called at the beginning,
688
 * before callbacks are assigned to buttons, so it is safe to assume lists are
689
 * initialized with items and page is in its virgin state with regard to
690
 * everything else.
691
 */
692
function jump_to_item(url_with_item)
693
{
694
    const parsed_url = url_extract_target(url_with_item);
695

    
696
    if (parsed_url.target === undefined)
697
	return;
698

    
699
    const prefix = parsed_url.target.substring(1, 2);
700

    
701
    if (!list_prefixes.includes(prefix)) {
702
	history.replaceState(null, "", parsed_url.base_url);
703
	return;
704
    }
705

    
706
    by_id(`show_${TYPE_NAME[prefix]}s`).checked = true;
707
    edit_item(prefix, decodeURIComponent(parsed_url.target.substring(2)));
708
}
709

    
710
async function main()
711
{
712
    storage = await get_remote_storage();
713

    
714
    for (let prefix of list_prefixes) {
715
	for (let item of storage.get_all_names(prefix).sort()) {
716
	    add_li(prefix, item, true);
717
	    add_chbx_li(prefix, item);
718
	    add_radio_li(prefix, item);
719
	}
720

    
721
	jump_to_item(document.URL);
722

    
723
	let name = TYPE_NAME[prefix];
724

    
725
	let add_but = by_id(`add_${name}_but`);
726
	let discard_but = by_id(`discard_${name}_but`);
727
	let save_but = by_id(`save_${name}_but`);
728

    
729
	add_but.addEventListener("click", () => add_new_item(prefix));
730
	discard_but.addEventListener("click", () => cancel_work(prefix));
731
	save_but.addEventListener("click", () => save_work(prefix));
732

    
733
	if (prefix === TYPE_PREFIX.SCRIPT)
734
	    continue;
735

    
736
	let ul = ul_by_prefix[prefix];
737

    
738
	let commit_components_but = by_id(`commit_${name}_components_but`);
739
	let cancel_components_but = by_id(`cancel_${name}_components_but`);
740
	let select_components_but = by_id(`select_${name}_components_but`);
741

    
742
	commit_components_but
743
	    .addEventListener("click", ul.commit_components);
744
	select_components_but
745
	    .addEventListener("click", ul.select_components);
746
	cancel_components_but.addEventListener("click", cancel_components);
747
    }
748

    
749
    initialize_import_facility();
750

    
751
    storage.add_change_listener(handle_change);
752
}
753

    
754
function handle_change(change)
755
{
756
    if (change.old_val === undefined) {
757
	add_li(change.prefix, change.item);
758
	add_chbx_li(change.prefix, change.item);
759
	add_radio_li(change.prefix, change.item);
760

    
761
	return;
762
    }
763

    
764
    if (change.new_val !== undefined)
765
	return;
766

    
767
    let ul = ul_by_prefix[change.prefix];
768
    if (ul.state === UL_STATE.EDITING_ENTRY &&
769
	ul.edited_item === change.item) {
770
	ul.state = UL_STATE.ADDING_ENTRY;
771
	return;
772
    }
773

    
774
    let uls_creators = [[ul.ul, item_li_id]];
775

    
776
    if (change.prefix !== TYPE_PREFIX.PAGE) {
777
	uls_creators.push([chbx_components_ul, chbx_li_id]);
778
	uls_creators.push([radio_components_ul, radio_li_id]);
779
    }
780

    
781
    for (let [components_ul, id_creator] of uls_creators) {
782
	let li = by_id(id_creator(change.prefix, change.item));
783
	components_ul.removeChild(li);
784
    }
785
}
786

    
787
main();
(4-4/4)