Project

General

Profile

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

haketilo / html / options_main.js @ 659f532e

1
/**
2
 * Myext HTML options page main script
3
 *
4
 * Copyright (C) 2021 Wojtek Kosior
5
 *
6
 * This code is dual-licensed under:
7
 * - Asshole license 1.0,
8
 * - GPLv3 or (at your option) any later version
9
 *
10
 * "dual-licensed" means you can choose the license you prefer.
11
 *
12
 * This code is released under a permissive license because I disapprove of
13
 * copyright and wouldn't be willing to sue a violator. Despite not putting
14
 * this code under copyleft (which is also kind of copyright), I do not want
15
 * it to be made proprietary. Hence, the permissive alternative to GPL is the
16
 * Asshole license 1.0 that allows me to call you an asshole if you use it.
17
 * This means you're legally ok regardless of how you utilize this code but if
18
 * you make it into something nonfree, you're an asshole.
19
 *
20
 * You should have received a copy of both GPLv3 and Asshole license 1.0
21
 * together with this code. If not, please see:
22
 * - https://www.gnu.org/licenses/gpl-3.0.en.html
23
 * - https://koszko.org/asshole-license.txt
24
 */
25

    
26
"use strict";
27

    
28
(() => {
29
    const get_storage = window.get_storage;
30
    const TYPE_PREFIX = window.TYPE_PREFIX;
31
    const TYPE_NAME = window.TYPE_NAME;
32
    const list_prefixes = window.list_prefixes;
33

    
34
    var storage;
35
    function by_id(id)
36
    {
37
	return document.getElementById(id);
38
    }
39

    
40
    function nice_name(prefix, name)
41
    {
42
	return `${name} (${TYPE_NAME[prefix]})`;
43
    }
44

    
45
    const item_li_template = by_id("item_li_template");
46
    const bag_component_li_template = by_id("bag_component_li_template");
47
    const chbx_component_li_template = by_id("chbx_component_li_template");
48
    const radio_component_li_template = by_id("radio_component_li_template");
49
    const import_li_template = by_id("import_li_template");
50
    /* Make sure they are later cloned without id. */
51
    item_li_template.removeAttribute("id");
52
    bag_component_li_template.removeAttribute("id");
53
    chbx_component_li_template.removeAttribute("id");
54
    radio_component_li_template.removeAttribute("id");
55
    import_li_template.removeAttribute("id");
56

    
57
    function item_li_id(prefix, item)
58
    {
59
	return `li_${prefix}_${item}`;
60
    }
61

    
62
    /* Insert into list of bags/pages/scripts */
63
    function add_li(prefix, item, at_the_end=false)
64
    {
65
	let ul = ul_by_prefix[prefix];
66
	let li = item_li_template.cloneNode(true);
67
	li.id = item_li_id(prefix, item);
68

    
69
	let span = li.firstElementChild;
70
	span.textContent = item;
71

    
72
	let edit_button = span.nextElementSibling;
73
	edit_button.addEventListener("click", () => edit_item(prefix, item));
74

    
75
	let remove_button = edit_button.nextElementSibling;
76
	remove_button.addEventListener("click",
77
				       () => storage.remove(prefix, item));
78

    
79
	let export_button = remove_button.nextElementSibling;
80
	export_button.addEventListener("click",
81
				       () => export_item(prefix, item));
82

    
83
	if (!at_the_end) {
84
	    for (let element of ul.ul.children) {
85
		if (element.id < li.id || element.id.startsWith("work_"))
86
		    continue;
87

    
88
		ul.ul.insertBefore(li, element);
89
		return;
90
	    }
91
	}
92

    
93
	ul.ul.appendChild(li);
94
    }
95

    
96
    const chbx_components_ul = by_id("chbx_components_ul");
97
    const radio_components_ul = by_id("radio_components_ul");
98

    
99
    function chbx_li_id(prefix, item)
100
    {
101
	return `cli_${prefix}_${item}`;
102
    }
103

    
104
    function radio_li_id(prefix, item)
105
    {
106
	return `rli_${prefix}_${item}`;
107
    }
108

    
109
    //TODO: refactor the 2 functions below
110

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

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

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

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

    
126
	chbx_components_ul.appendChild(li);
127
    }
128

    
129
    var radio_component_none_li = by_id("radio_component_none_li");
130

    
131
    function add_radio_li(prefix, name)
132
    {
133
	if (prefix === TYPE_PREFIX.PAGE)
134
	    return;
135

    
136
	let li = radio_component_li_template.cloneNode(true);
137
	li.id = radio_li_id(prefix, name);
138
	li.setAttribute("data-prefix", prefix);
139
	li.setAttribute("data-name", name);
140

    
141
	let radio = li.firstElementChild;
142
	let span = radio.nextElementSibling;
143

    
144
	span.textContent = nice_name(prefix, name);
145

    
146
	radio_components_ul.insertBefore(li, radio_component_none_li);
147
    }
148

    
149
    const page_payload_span = by_id("page_payload");
150

    
151
    function set_page_components(components)
152
    {
153
	if (components === undefined) {
154
	    page_payload_span.setAttribute("data-payload", "no");
155
	    page_payload_span.textContent = "(None)";
156
	} else {
157
	    page_payload_span.setAttribute("data-payload", "yes");
158
	    let [prefix, name] = components;
159
	    page_payload_span.setAttribute("data-prefix", prefix);
160
	    page_payload_span.setAttribute("data-name", name);
161
	    page_payload_span.textContent = nice_name(prefix, name);
162
	}
163
    }
164

    
165
    const page_allow_chbx = by_id("page_allow_chbx");
166

    
167
    /* Used to reset edited page. */
168
    function reset_work_page_li(ul, item, settings)
169
    {
170
	ul.work_name_input.value = maybe_string(item);
171
	settings = settings || {allow: false, components: undefined};
172
	page_allow_chbx.checked = !!settings.allow;
173

    
174
	set_page_components(settings.components);
175
    }
176

    
177
    function work_page_li_components()
178
    {
179
	if (page_payload_span.getAttribute("data-payload") === "no")
180
	    return undefined;
181

    
182
	let prefix = page_payload_span.getAttribute("data-prefix");
183
	let name = page_payload_span.getAttribute("data-name");
184
	return [prefix, name];
185
    }
186

    
187
    /* Used to get edited page data for saving. */
188
    function work_page_li_data(ul)
189
    {
190
	let url = ul.work_name_input.value;
191
	let settings = {
192
	    components : work_page_li_components(),
193
	    allow : !!page_allow_chbx.checked
194
	};
195

    
196
	return [url, settings];
197
    }
198

    
199
    const empty_bag_component_li = by_id("empty_bag_component_li");
200
    var bag_components_ul = by_id("bag_components_ul");
201

    
202
    /* Used to construct and update components list of edited bag. */
203
    function add_bag_components(components)
204
    {
205
	for (let component of components) {
206
	    let [prefix, name] = component;
207
	    let li = bag_component_li_template.cloneNode(true);
208
	    li.setAttribute("data-prefix", prefix);
209
	    li.setAttribute("data-name", name);
210
	    let span = li.firstElementChild;
211
	    span.textContent = nice_name(prefix, name);
212
	    let remove_but = span.nextElementSibling;
213
	    remove_but.addEventListener("click", () =>
214
					bag_components_ul.removeChild(li));
215
	    bag_components_ul.appendChild(li);
216
	}
217

    
218
	bag_components_ul.appendChild(empty_bag_component_li);
219
    }
220

    
221
    /* Used to reset edited bag. */
222
    function reset_work_bag_li(ul, item, components)
223
    {
224
	components = components || [];
225

    
226
	ul.work_name_input.value = maybe_string(item);
227
	let old_components_ul = bag_components_ul;
228
	bag_components_ul = old_components_ul.cloneNode(false);
229

    
230
	ul.work_li.insertBefore(bag_components_ul, old_components_ul);
231
	ul.work_li.removeChild(old_components_ul);
232

    
233
	add_bag_components(components);
234
    }
235

    
236
    /* Used to get edited bag data for saving. */
237
    function work_bag_li_data(ul)
238
    {
239
	let components_ul = ul.work_name_input.nextElementSibling;
240
	let component_li = components_ul.firstElementChild;
241

    
242
	let components = [];
243

    
244
	/* Last list element is empty li with id set. */
245
	while (component_li.id === '') {
246
	    components.push([component_li.getAttribute("data-prefix"),
247
			     component_li.getAttribute("data-name")]);
248
	    component_li = component_li.nextElementSibling;
249
	}
250

    
251
	return [ul.work_name_input.value, components];
252
    }
253

    
254
    const script_url_input = by_id("script_url_field");
255
    const script_sha256_input = by_id("script_sha256_field");
256
    const script_contents_field = by_id("script_contents_field");
257

    
258
    function maybe_string(maybe_defined)
259
    {
260
	return maybe_defined === undefined ? "" : maybe_defined + "";
261
    }
262

    
263
    /* Used to reset edited script. */
264
    function reset_work_script_li(ul, name, data)
265
    {
266
	ul.work_name_input.value = maybe_string(name);
267
	if (data === undefined)
268
	    data = {};
269
	script_url_input.value = maybe_string(data.url);
270
	script_sha256_input.value = maybe_string(data.hash);
271
	script_contents_field.value = maybe_string(data.text);
272
    }
273

    
274
    /* Used to get edited script data for saving. */
275
    function work_script_li_data(ul)
276
    {
277
	return [ul.work_name_input.value, {
278
	    url : script_url_input.value,
279
	    hash : script_sha256_input.value,
280
	    text : script_contents_field.value
281
	}];
282
    }
283

    
284
    function cancel_work(prefix)
285
    {
286
	let ul = ul_by_prefix[prefix];
287

    
288
	if (ul.state === UL_STATE.IDLE)
289
	    return;
290

    
291
	if (ul.state === UL_STATE.EDITING_ENTRY) {
292
	    add_li(prefix, ul.edited_item);
293
	}
294

    
295
	ul.work_li.classList.add("hide");
296
	ul.state = UL_STATE.IDLE;
297
    }
298

    
299
    function save_work(prefix)
300
    {
301
	let ul = ul_by_prefix[prefix];
302

    
303
	if (ul.state === UL_STATE.IDLE)
304
	    return;
305

    
306
	let [item, data] = ul.get_work_li_data(ul);
307

    
308
	/* Here we fire promises and return without waiting. */
309

    
310
	if (ul.state === UL_STATE.EDITING_ENTRY)
311
	    storage.replace(prefix, ul.edited_item, item, data);
312
	if (ul.state === UL_STATE.ADDING_ENTRY)
313
	    storage.set(prefix, item, data);
314

    
315
	cancel_work(prefix);
316
    }
317

    
318
    function edit_item(prefix, item)
319
    {
320
	cancel_work(prefix);
321

    
322
	let ul = ul_by_prefix[prefix];
323
	let li = by_id(item_li_id(prefix, item));
324
	ul.reset_work_li(ul, item, storage.get(prefix, item));
325
	ul.ul.insertBefore(ul.work_li, li);
326
	ul.ul.removeChild(li);
327
	ul.work_li.classList.remove("hide");
328

    
329
	ul.state = UL_STATE.EDITING_ENTRY;
330
	ul.edited_item = item;
331
    }
332

    
333
    const file_downloader = by_id("file_downloader");
334

    
335
    function recursively_export_item(prefix, name, added_items, items_data)
336
    {
337
	let key = prefix + name;
338

    
339
	if (added_items.has(key))
340
	    return;
341

    
342
	let data = storage.get(prefix, name);
343
	if (data === undefined) {
344
	    console.log(`${TYPE_NAME[prefix]} '${name}' for export not found`);
345
	    return;
346
	}
347

    
348
	if (prefix !== TYPE_PREFIX.SCRIPT) {
349
	    let components = prefix === TYPE_PREFIX.BAG ?
350
		data : [data.components];
351

    
352
	    for (let [comp_prefix, comp_name] of components) {
353
		recursively_export_item(comp_prefix, comp_name,
354
					added_items, items_data);
355
	    }
356
	}
357

    
358
	items_data.push({[key]: data});
359
	added_items.add(key);
360
    }
361

    
362
    function export_item(prefix, name)
363
    {
364
	let added_items = new Set();
365
	let items_data = [];
366
	recursively_export_item(prefix, name, added_items, items_data);
367
	let file = new Blob([JSON.stringify(items_data)],
368
			    {type: "application/json"});
369
	let url = URL.createObjectURL(file);
370
	file_downloader.setAttribute("href", url);
371
	file_downloader.setAttribute("download", prefix + name + ".json");
372
	file_downloader.click();
373
	file_downloader.removeAttribute("href");
374
	URL.revokeObjectURL(url);
375
    }
376

    
377
    function add_new_item(prefix)
378
    {
379
	cancel_work(prefix);
380

    
381
	let ul = ul_by_prefix[prefix];
382
	ul.reset_work_li(ul);
383
	ul.work_li.classList.remove("hide");
384
	ul.ul.appendChild(ul.work_li);
385

    
386
	ul.state = UL_STATE.ADDING_ENTRY;
387
    }
388

    
389
    const chbx_components_window = by_id("chbx_components_window");
390

    
391
    function bag_components()
392
    {
393
	chbx_components_window.classList.remove("hide");
394
	radio_components_window.classList.add("hide");
395

    
396
	for (let li of chbx_components_ul.children) {
397
	    let chbx = li.firstElementChild;
398
	    chbx.checked = false;
399
	}
400
    }
401

    
402
    function commit_bag_components()
403
    {
404
	let selected = [];
405

    
406
	for (let li of chbx_components_ul.children) {
407
	    let chbx = li.firstElementChild;
408
	    if (!chbx.checked)
409
		continue;
410

    
411
	    selected.push([li.getAttribute("data-prefix"),
412
			   li.getAttribute("data-name")]);
413
	}
414

    
415
	add_bag_components(selected);
416
	cancel_components();
417
    }
418

    
419
    const radio_components_window = by_id("radio_components_window");
420
    var radio_component_none_input = by_id("radio_component_none_input");
421

    
422
    function page_components()
423
    {
424
	radio_components_window.classList.remove("hide");
425
	chbx_components_window.classList.add("hide");
426

    
427
	radio_component_none_input.checked = true;
428

    
429
	let components = work_page_li_components();
430
	if (components === undefined)
431
	    return;
432

    
433
	let [prefix, item] = components;
434
	let li = by_id(radio_li_id(prefix, item));
435
	if (li === null)
436
	    radio_component_none_input.checked = false;
437
	else
438
	    li.firstElementChild.checked = true;
439
    }
440

    
441
    function commit_page_components()
442
    {
443
	let components = null;
444

    
445
	for (let li of radio_components_ul.children) {
446
	    let radio = li.firstElementChild;
447
	    if (!radio.checked)
448
		continue;
449

    
450
	    components = [li.getAttribute("data-prefix"),
451
			  li.getAttribute("data-name")];
452

    
453
	    if (radio.id === "radio_component_none_input")
454
		components = undefined;
455

    
456
	    break;
457
	}
458

    
459
	if (components !== null)
460
	    set_page_components(components);
461
	cancel_components();
462
    }
463

    
464
    function cancel_components()
465
    {
466
	chbx_components_window.classList.add("hide");
467
	radio_components_window.classList.add("hide");
468
    }
469

    
470
    const UL_STATE = {
471
	EDITING_ENTRY : 0,
472
	ADDING_ENTRY : 1,
473
	IDLE : 2
474
    };
475

    
476
    const ul_by_prefix = {
477
	[TYPE_PREFIX.PAGE] : {
478
	    ul : by_id("pages_ul"),
479
	    work_li : by_id("work_page_li"),
480
	    work_name_input : by_id("page_url_field"),
481
	    reset_work_li : reset_work_page_li,
482
	    get_work_li_data : work_page_li_data,
483
	    select_components : page_components,
484
	    commit_components : commit_page_components,
485
	    state : UL_STATE.IDLE,
486
	    edited_item : undefined,
487
	},
488
	[TYPE_PREFIX.BAG] : {
489
	    ul : by_id("bags_ul"),
490
	    work_li : by_id("work_bag_li"),
491
	    work_name_input : by_id("bag_name_field"),
492
	    reset_work_li : reset_work_bag_li,
493
	    get_work_li_data : work_bag_li_data,
494
	    select_components : bag_components,
495
	    commit_components : commit_bag_components,
496
	    state : UL_STATE.IDLE,
497
	    edited_item : undefined,
498
	},
499
	[TYPE_PREFIX.SCRIPT] : {
500
	    ul : by_id("scripts_ul"),
501
	    work_li : by_id("work_script_li"),
502
	    work_name_input : by_id("script_name_field"),
503
	    reset_work_li : reset_work_script_li,
504
	    get_work_li_data : work_script_li_data,
505
	    state : UL_STATE.IDLE,
506
	    edited_item : undefined,
507
	}
508
    }
509

    
510
    const import_window = by_id("import_window");
511
    const import_loading_radio = by_id("import_loading_radio");
512
    const import_failed_radio = by_id("import_failed_radio");
513
    const import_selection_radio = by_id("import_selection_radio");
514
    const bad_file_errormsg = by_id("bad_file_errormsg");
515

    
516
    /*
517
     * Newer browsers could utilise `text' method of File objects.
518
     * Older ones require FileReader.
519
     */
520

    
521
    function _read_file(file, resolve, reject)
522
    {
523
	let reader = new FileReader();
524

    
525
	reader.onload = () => resolve(reader.result);
526
	reader.onerror = () => reject(reader.error);
527
	reader.readAsText(file);
528
    }
529

    
530
    function read_file(file)
531
    {
532
	return new Promise((resolve, reject) =>
533
			   _read_file(file, resolve, reject));
534
    }
535

    
536
    async function import_from_file(event)
537
    {
538
	let files = event.target.files;
539
	if (files.length < 1)
540
	    return;
541

    
542
	import_window.classList.remove("hide");
543
	import_loading_radio.checked = true;
544

    
545
	let result = undefined;
546

    
547
	try {
548
	    result = JSON.parse(await read_file(files[0]));
549
	} catch(e) {
550
	    bad_file_errormsg.textContent = "" + e;
551
	    import_failed_radio.checked = true;
552
	    return;
553
	}
554

    
555
	let errormsg = validate_settings(result);
556
	if (errormsg !== false) {
557
	    bad_file_errormsg.textContent = errormsg;
558
	    import_failed_radio.checked = true;
559
	    return;
560
	}
561

    
562
	populate_import_list(result);
563
	import_selection_radio.checked = true;
564
    }
565

    
566
    function validate_settings(settings)
567
    {
568
	// TODO
569
	return false;
570
    }
571

    
572
    function import_li_id(prefix, item)
573
    {
574
	return `ili_${prefix}_${item}`;
575
    }
576

    
577
    let import_ul = by_id("import_ul");
578
    let import_chbxs_colliding = undefined;
579
    let settings_import_map = undefined;
580

    
581
    function populate_import_list(settings)
582
    {
583
	let old_children = import_ul.children;
584
	while (old_children[0] !== undefined)
585
	    import_ul.removeChild(old_children[0]);
586

    
587
	import_chbxs_colliding = [];
588
	settings_import_map = new Map();
589

    
590
	for (let setting of settings) {
591
	    let [key, value] = Object.entries(setting)[0];
592
	    let prefix = key[0];
593
	    let name = key.substring(1);
594
	    add_import_li(prefix, name);
595
	    settings_import_map.set(key, value);
596
	}
597
    }
598

    
599
    function add_import_li(prefix, name)
600
    {
601
	let li = import_li_template.cloneNode(true);
602
	let name_span = li.firstElementChild;
603
	let chbx = name_span.nextElementSibling;
604
	let warning_span = chbx.nextElementSibling;
605

    
606
	li.setAttribute("data-prefix", prefix);
607
	li.setAttribute("data-name", name);
608
	li.id = import_li_id(prefix, name);
609
	name_span.textContent = nice_name(prefix, name);
610

    
611
	if (storage.get(prefix, name) !== undefined) {
612
	    import_chbxs_colliding.push(chbx);
613
	    warning_span.textContent = "(will overwrite existing setting!)";
614
	}
615

    
616
	import_ul.appendChild(li);
617
    }
618

    
619
    function check_all_imports()
620
    {
621
	for (let li of import_ul.children)
622
	    li.firstElementChild.nextElementSibling.checked = true;
623
    }
624

    
625
    function uncheck_all_imports()
626
    {
627
	for (let li of import_ul.children)
628
	    li.firstElementChild.nextElementSibling.checked = false;
629
    }
630

    
631
    function uncheck_colliding_imports()
632
    {
633
	for (let chbx of import_chbxs_colliding)
634
	    chbx.checked = false;
635
    }
636

    
637
    const file_opener_form = by_id("file_opener_form");
638

    
639
    function hide_import_window()
640
    {
641
	import_window.classList.add("hide");
642
	/* Let GC free some memory */
643
	import_chbxs_colliding = undefined;
644
	settings_import_map = undefined;
645

    
646
	/*
647
	 * Reset file <input>. Without this, a second attempt to import the same
648
	 * file would result in "change" event on happening on <input> element.
649
	 */
650
	file_opener_form.reset();
651
    }
652

    
653
    function commit_import()
654
    {
655
	for (let li of import_ul.children) {
656
	    let chbx = li.firstElementChild.nextElementSibling;
657

    
658
	    if (!chbx.checked)
659
		continue;
660

    
661
	    let prefix = li.getAttribute("data-prefix");
662
	    let name = li.getAttribute("data-name");
663
	    let key = prefix + name;
664
	    let value = settings_import_map.get(key);
665
	    storage.set(prefix, name, value);
666
	}
667

    
668
	hide_import_window();
669
    }
670

    
671
    function initialize_import_facility()
672
    {
673
	let import_but = by_id("import_but");
674
	let file_opener = by_id("file_opener");
675
	let import_failok_but = by_id("import_failok_but");
676
	let check_all_import_but = by_id("check_all_import_but");
677
	let uncheck_all_import_but = by_id("uncheck_all_import_but");
678
	let uncheck_existing_import_but = by_id("uncheck_existing_import_but");
679
	let commit_import_but = by_id("commit_import_but");
680
	let cancel_import_but = by_id("cancel_import_but");
681
	import_but.addEventListener("click", () => file_opener.click());
682
	file_opener.addEventListener("change", import_from_file);
683
	import_failok_but.addEventListener("click", hide_import_window);
684
	check_all_import_but.addEventListener("click", check_all_imports);
685
	uncheck_all_import_but.addEventListener("click", uncheck_all_imports);
686
	uncheck_colliding_import_but
687
	    .addEventListener("click", uncheck_colliding_imports);
688
	commit_import_but.addEventListener("click", commit_import);
689
	cancel_import_but.addEventListener("click", hide_import_window);
690
    }
691

    
692
    async function main()
693
    {
694
	storage = await get_storage();
695

    
696
	for (let prefix of list_prefixes) {
697
	    for (let item of storage.get_all_names(prefix).sort()) {
698
		add_li(prefix, item, true);
699
		add_chbx_li(prefix, item);
700
		add_radio_li(prefix, item);
701
	    }
702

    
703
	    let name = TYPE_NAME[prefix];
704

    
705
	    let add_but = by_id(`add_${name}_but`);
706
	    let discard_but = by_id(`discard_${name}_but`);
707
	    let save_but = by_id(`save_${name}_but`);
708

    
709
	    add_but.addEventListener("click", () => add_new_item(prefix));
710
	    discard_but.addEventListener("click", () => cancel_work(prefix));
711
	    save_but.addEventListener("click", () => save_work(prefix));
712

    
713
	    if (prefix === TYPE_PREFIX.SCRIPT)
714
		continue;
715

    
716
	    let ul = ul_by_prefix[prefix];
717

    
718
	    let commit_components_but = by_id(`commit_${name}_components_but`);
719
	    let cancel_components_but = by_id(`cancel_${name}_components_but`);
720
	    let select_components_but = by_id(`select_${name}_components_but`);
721

    
722
	    commit_components_but
723
		.addEventListener("click", ul.commit_components);
724
	    select_components_but
725
		.addEventListener("click", ul.select_components);
726
	    cancel_components_but.addEventListener("click", cancel_components);
727
	}
728

    
729
	initialize_import_facility();
730

    
731
	storage.add_change_listener(handle_change);
732
    }
733

    
734
    function handle_change(change)
735
    {
736
	if (change.old_val === undefined) {
737
	    add_li(change.prefix, change.item);
738
	    add_chbx_li(change.prefix, change.item);
739
	    add_radio_li(change.prefix, change.item);
740

    
741
	    return;
742
	}
743

    
744
	if (change.new_val !== undefined)
745
	    return;
746

    
747
	let ul = ul_by_prefix[change.prefix];
748
	if (ul.state === UL_STATE.EDITING_ENTRY &&
749
	    ul.edited_item === change.item) {
750
	    ul.state = UL_STATE.ADDING_ENTRY;
751
	    return;
752
	}
753

    
754
	let uls_creators = [[ul.ul, item_li_id]];
755

    
756
	if (change.prefix !== TYPE_PREFIX.PAGE) {
757
	    uls_creators.push([chbx_components_ul, chbx_li_id]);
758
	    uls_creators.push([radio_components_ul, radio_li_id]);
759
	}
760

    
761
	for (let [components_ul, id_creator] of uls_creators) {
762
	    let li = by_id(id_creator(change.prefix, change.item));
763
	    components_ul.removeChild(li);
764
	}
765
    }
766

    
767
    main();
768
})();
(4-4/4)