Project

General

Profile

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

haketilo / html / options_main.js @ d0ae3939

1
/**
2
 * Myext 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
 * IMPORTS_END
16
 */
17

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

    
24
function nice_name(prefix, name)
25
{
26
    return `${name} (${TYPE_NAME[prefix]})`;
27
}
28

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

    
41
function item_li_id(prefix, item)
42
{
43
    return `li_${prefix}_${item}`;
44
}
45

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

    
53
    let span = li.firstElementChild;
54
    span.textContent = item;
55

    
56
    let edit_button = span.nextElementSibling;
57
    edit_button.addEventListener("click", () => edit_item(prefix, item));
58

    
59
    let remove_button = edit_button.nextElementSibling;
60
    remove_button.addEventListener("click",
61
				   () => storage.remove(prefix, item));
62

    
63
    let export_button = remove_button.nextElementSibling;
64
    export_button.addEventListener("click",
65
				   () => export_item(prefix, item));
66

    
67
    if (!at_the_end) {
68
	for (let element of ul.ul.children) {
69
	    if (element.id < li.id || element.id.startsWith("work_"))
70
		continue;
71

    
72
	    ul.ul.insertBefore(li, element);
73
	    return;
74
	}
75
    }
76

    
77
    ul.ul.appendChild(li);
78
}
79

    
80
const chbx_components_ul = by_id("chbx_components_ul");
81
const radio_components_ul = by_id("radio_components_ul");
82

    
83
function chbx_li_id(prefix, item)
84
{
85
    return `cli_${prefix}_${item}`;
86
}
87

    
88
function radio_li_id(prefix, item)
89
{
90
    return `rli_${prefix}_${item}`;
91
}
92

    
93
//TODO: refactor the 2 functions below
94

    
95
function add_chbx_li(prefix, name)
96
{
97
    if (prefix === TYPE_PREFIX.PAGE)
98
	return;
99

    
100
    let li = chbx_component_li_template.cloneNode(true);
101
    li.id = chbx_li_id(prefix, name);
102
    li.setAttribute("data-prefix", prefix);
103
    li.setAttribute("data-name", name);
104

    
105
    let chbx = li.firstElementChild;
106
    let span = chbx.nextElementSibling;
107

    
108
    span.textContent = nice_name(prefix, name);
109

    
110
    chbx_components_ul.appendChild(li);
111
}
112

    
113
var radio_component_none_li = by_id("radio_component_none_li");
114

    
115
function add_radio_li(prefix, name)
116
{
117
    if (prefix === TYPE_PREFIX.PAGE)
118
	return;
119

    
120
    let li = radio_component_li_template.cloneNode(true);
121
    li.id = radio_li_id(prefix, name);
122
    li.setAttribute("data-prefix", prefix);
123
    li.setAttribute("data-name", name);
124

    
125
    let radio = li.firstElementChild;
126
    let span = radio.nextElementSibling;
127

    
128
    span.textContent = nice_name(prefix, name);
129

    
130
    radio_components_ul.insertBefore(li, radio_component_none_li);
131
}
132

    
133
const page_payload_span = by_id("page_payload");
134

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

    
149
const page_allow_chbx = by_id("page_allow_chbx");
150

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

    
158
    set_page_components(settings.components);
159
}
160

    
161
function work_page_li_components()
162
{
163
    if (page_payload_span.getAttribute("data-payload") === "no")
164
	return undefined;
165

    
166
    let prefix = page_payload_span.getAttribute("data-prefix");
167
    let name = page_payload_span.getAttribute("data-name");
168
    return [prefix, name];
169
}
170

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

    
180
    return [url, settings];
181
}
182

    
183
const empty_bag_component_li = by_id("empty_bag_component_li");
184
var bag_components_ul = by_id("bag_components_ul");
185

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

    
202
    bag_components_ul.appendChild(empty_bag_component_li);
203
}
204

    
205
/* Used to reset edited bag. */
206
function reset_work_bag_li(ul, item, components)
207
{
208
    components = components || [];
209

    
210
    ul.work_name_input.value = maybe_string(item);
211
    let old_components_ul = bag_components_ul;
212
    bag_components_ul = old_components_ul.cloneNode(false);
213

    
214
    ul.work_li.insertBefore(bag_components_ul, old_components_ul);
215
    ul.work_li.removeChild(old_components_ul);
216

    
217
    add_bag_components(components);
218
}
219

    
220
/* Used to get edited bag data for saving. */
221
function work_bag_li_data(ul)
222
{
223
    let components_ul = ul.work_name_input.nextElementSibling;
224
    let component_li = components_ul.firstElementChild;
225

    
226
    let components = [];
227

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

    
235
    return [ul.work_name_input.value, components];
236
}
237

    
238
const script_url_input = by_id("script_url_field");
239
const script_sha256_input = by_id("script_sha256_field");
240
const script_contents_field = by_id("script_contents_field");
241

    
242
function maybe_string(maybe_defined)
243
{
244
    return maybe_defined === undefined ? "" : maybe_defined + "";
245
}
246

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

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

    
268
function cancel_work(prefix)
269
{
270
    let ul = ul_by_prefix[prefix];
271

    
272
    if (ul.state === UL_STATE.IDLE)
273
	return;
274

    
275
    if (ul.state === UL_STATE.EDITING_ENTRY) {
276
	add_li(prefix, ul.edited_item);
277
    }
278

    
279
    ul.work_li.classList.add("hide");
280
    ul.state = UL_STATE.IDLE;
281
}
282

    
283
function save_work(prefix)
284
{
285
    let ul = ul_by_prefix[prefix];
286

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

    
290
    let [item, data] = ul.get_work_li_data(ul);
291

    
292
    /* Here we fire promises and return without waiting. */
293

    
294
    if (ul.state === UL_STATE.EDITING_ENTRY)
295
	storage.replace(prefix, ul.edited_item, item, data);
296
    if (ul.state === UL_STATE.ADDING_ENTRY)
297
	storage.set(prefix, item, data);
298

    
299
    cancel_work(prefix);
300
}
301

    
302
function edit_item(prefix, item)
303
{
304
    cancel_work(prefix);
305

    
306
    let ul = ul_by_prefix[prefix];
307
    let li = by_id(item_li_id(prefix, item));
308

    
309
    if (li === null) {
310
	add_new_item(prefix, item);
311
	return;
312
    }
313

    
314
    ul.reset_work_li(ul, item, storage.get(prefix, item));
315
    ul.ul.insertBefore(ul.work_li, li);
316
    ul.ul.removeChild(li);
317
    ul.work_li.classList.remove("hide");
318

    
319
    ul.state = UL_STATE.EDITING_ENTRY;
320
    ul.edited_item = item;
321
}
322

    
323
const file_downloader = by_id("file_downloader");
324

    
325
function recursively_export_item(prefix, name, added_items, items_data)
326
{
327
    let key = prefix + name;
328

    
329
    if (added_items.has(key))
330
	return;
331

    
332
    let data = storage.get(prefix, name);
333
    if (data === undefined) {
334
	console.log(`${TYPE_NAME[prefix]} '${name}' for export not found`);
335
	return;
336
    }
337

    
338
    if (prefix !== TYPE_PREFIX.SCRIPT) {
339
	let components = prefix === TYPE_PREFIX.BAG ?
340
	    data : [data.components];
341

    
342
	for (let [comp_prefix, comp_name] of components) {
343
	    recursively_export_item(comp_prefix, comp_name,
344
				    added_items, items_data);
345
	}
346
    }
347

    
348
    items_data.push({[key]: data});
349
    added_items.add(key);
350
}
351

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

    
367
function add_new_item(prefix, name)
368
{
369
    cancel_work(prefix);
370

    
371
    let ul = ul_by_prefix[prefix];
372
    ul.reset_work_li(ul);
373
    ul.work_li.classList.remove("hide");
374
    ul.ul.appendChild(ul.work_li);
375

    
376
    if (name !== undefined)
377
	ul.work_name_input.value = name;
378
    ul.state = UL_STATE.ADDING_ENTRY;
379
}
380

    
381
const chbx_components_window = by_id("chbx_components_window");
382

    
383
function bag_components()
384
{
385
    chbx_components_window.classList.remove("hide");
386
    radio_components_window.classList.add("hide");
387

    
388
    for (let li of chbx_components_ul.children) {
389
	let chbx = li.firstElementChild;
390
	chbx.checked = false;
391
    }
392
}
393

    
394
function commit_bag_components()
395
{
396
    let selected = [];
397

    
398
    for (let li of chbx_components_ul.children) {
399
	let chbx = li.firstElementChild;
400
	if (!chbx.checked)
401
	    continue;
402

    
403
	selected.push([li.getAttribute("data-prefix"),
404
		       li.getAttribute("data-name")]);
405
    }
406

    
407
    add_bag_components(selected);
408
    cancel_components();
409
}
410

    
411
const radio_components_window = by_id("radio_components_window");
412
var radio_component_none_input = by_id("radio_component_none_input");
413

    
414
function page_components()
415
{
416
    radio_components_window.classList.remove("hide");
417
    chbx_components_window.classList.add("hide");
418

    
419
    radio_component_none_input.checked = true;
420

    
421
    let components = work_page_li_components();
422
    if (components === undefined)
423
	return;
424

    
425
    let [prefix, item] = components;
426
    let li = by_id(radio_li_id(prefix, item));
427
    if (li === null)
428
	radio_component_none_input.checked = false;
429
    else
430
	li.firstElementChild.checked = true;
431
}
432

    
433
function commit_page_components()
434
{
435
    let components = null;
436

    
437
    for (let li of radio_components_ul.children) {
438
	let radio = li.firstElementChild;
439
	if (!radio.checked)
440
	    continue;
441

    
442
	components = [li.getAttribute("data-prefix"),
443
		      li.getAttribute("data-name")];
444

    
445
	if (radio.id === "radio_component_none_input")
446
	    components = undefined;
447

    
448
	break;
449
    }
450

    
451
    if (components !== null)
452
	set_page_components(components);
453
    cancel_components();
454
}
455

    
456
function cancel_components()
457
{
458
    chbx_components_window.classList.add("hide");
459
    radio_components_window.classList.add("hide");
460
}
461

    
462
const UL_STATE = {
463
    EDITING_ENTRY : 0,
464
    ADDING_ENTRY : 1,
465
    IDLE : 2
466
};
467

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

    
502
const import_window = by_id("import_window");
503
const import_loading_radio = by_id("import_loading_radio");
504
const import_failed_radio = by_id("import_failed_radio");
505
const import_selection_radio = by_id("import_selection_radio");
506
const bad_file_errormsg = by_id("bad_file_errormsg");
507

    
508
/*
509
 * Newer browsers could utilise `text' method of File objects.
510
 * Older ones require FileReader.
511
 */
512

    
513
function _read_file(file, resolve, reject)
514
{
515
    let reader = new FileReader();
516

    
517
    reader.onload = () => resolve(reader.result);
518
    reader.onerror = () => reject(reader.error);
519
    reader.readAsText(file);
520
}
521

    
522
function read_file(file)
523
{
524
    return new Promise((resolve, reject) =>
525
		       _read_file(file, resolve, reject));
526
}
527

    
528
async function import_from_file(event)
529
{
530
    let files = event.target.files;
531
    if (files.length < 1)
532
	return;
533

    
534
    import_window.classList.remove("hide");
535
    import_loading_radio.checked = true;
536

    
537
    let result = undefined;
538

    
539
    try {
540
	result = JSON.parse(await read_file(files[0]));
541
    } catch(e) {
542
	bad_file_errormsg.textContent = "" + e;
543
	import_failed_radio.checked = true;
544
	return;
545
    }
546

    
547
    let errormsg = validate_settings(result);
548
    if (errormsg !== false) {
549
	bad_file_errormsg.textContent = errormsg;
550
	import_failed_radio.checked = true;
551
	return;
552
    }
553

    
554
    populate_import_list(result);
555
    import_selection_radio.checked = true;
556
}
557

    
558
function validate_settings(settings)
559
{
560
    // TODO
561
    return false;
562
}
563

    
564
function import_li_id(prefix, item)
565
{
566
    return `ili_${prefix}_${item}`;
567
}
568

    
569
let import_ul = by_id("import_ul");
570
let import_chbxs_colliding = undefined;
571
let settings_import_map = undefined;
572

    
573
function populate_import_list(settings)
574
{
575
    let old_children = import_ul.children;
576
    while (old_children[0] !== undefined)
577
	import_ul.removeChild(old_children[0]);
578

    
579
    import_chbxs_colliding = [];
580
    settings_import_map = new Map();
581

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

    
591
function add_import_li(prefix, name)
592
{
593
    let li = import_li_template.cloneNode(true);
594
    let name_span = li.firstElementChild;
595
    let chbx = name_span.nextElementSibling;
596
    let warning_span = chbx.nextElementSibling;
597

    
598
    li.setAttribute("data-prefix", prefix);
599
    li.setAttribute("data-name", name);
600
    li.id = import_li_id(prefix, name);
601
    name_span.textContent = nice_name(prefix, name);
602

    
603
    if (storage.get(prefix, name) !== undefined) {
604
	import_chbxs_colliding.push(chbx);
605
	warning_span.textContent = "(will overwrite existing setting!)";
606
    }
607

    
608
    import_ul.appendChild(li);
609
}
610

    
611
function check_all_imports()
612
{
613
    for (let li of import_ul.children)
614
	li.firstElementChild.nextElementSibling.checked = true;
615
}
616

    
617
function uncheck_all_imports()
618
{
619
    for (let li of import_ul.children)
620
	li.firstElementChild.nextElementSibling.checked = false;
621
}
622

    
623
function uncheck_colliding_imports()
624
{
625
    for (let chbx of import_chbxs_colliding)
626
	chbx.checked = false;
627
}
628

    
629
const file_opener_form = by_id("file_opener_form");
630

    
631
function hide_import_window()
632
{
633
    import_window.classList.add("hide");
634
    /* Let GC free some memory */
635
    import_chbxs_colliding = undefined;
636
    settings_import_map = undefined;
637

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

    
645
function commit_import()
646
{
647
    for (let li of import_ul.children) {
648
	let chbx = li.firstElementChild.nextElementSibling;
649

    
650
	if (!chbx.checked)
651
	    continue;
652

    
653
	let prefix = li.getAttribute("data-prefix");
654
	let name = li.getAttribute("data-name");
655
	let key = prefix + name;
656
	let value = settings_import_map.get(key);
657
	storage.set(prefix, name, value);
658
    }
659

    
660
    hide_import_window();
661
}
662

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

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

    
700
    if (parsed_url.target === undefined)
701
	return;
702

    
703
    const prefix = parsed_url.target.substring(1, 2);
704

    
705
    if (!list_prefixes.includes(prefix)) {
706
	history.replaceState(null, "", parsed_url.base_url);
707
	return;
708
    }
709

    
710
    by_id(`show_${TYPE_NAME[prefix]}s`).checked = true;
711
    edit_item(prefix, decodeURIComponent(parsed_url.target.substring(2)));
712
}
713

    
714
async function main()
715
{
716
    storage = await get_remote_storage();
717

    
718
    for (let prefix of list_prefixes) {
719
	for (let item of storage.get_all_names(prefix).sort()) {
720
	    add_li(prefix, item, true);
721
	    add_chbx_li(prefix, item);
722
	    add_radio_li(prefix, item);
723
	}
724

    
725
	jump_to_item(document.URL);
726

    
727
	let name = TYPE_NAME[prefix];
728

    
729
	let add_but = by_id(`add_${name}_but`);
730
	let discard_but = by_id(`discard_${name}_but`);
731
	let save_but = by_id(`save_${name}_but`);
732

    
733
	add_but.addEventListener("click", () => add_new_item(prefix));
734
	discard_but.addEventListener("click", () => cancel_work(prefix));
735
	save_but.addEventListener("click", () => save_work(prefix));
736

    
737
	if (prefix === TYPE_PREFIX.SCRIPT)
738
	    continue;
739

    
740
	let ul = ul_by_prefix[prefix];
741

    
742
	let commit_components_but = by_id(`commit_${name}_components_but`);
743
	let cancel_components_but = by_id(`cancel_${name}_components_but`);
744
	let select_components_but = by_id(`select_${name}_components_but`);
745

    
746
	commit_components_but
747
	    .addEventListener("click", ul.commit_components);
748
	select_components_but
749
	    .addEventListener("click", ul.select_components);
750
	cancel_components_but.addEventListener("click", cancel_components);
751
    }
752

    
753
    initialize_import_facility();
754

    
755
    storage.add_change_listener(handle_change);
756
}
757

    
758
function handle_change(change)
759
{
760
    if (change.old_val === undefined) {
761
	add_li(change.prefix, change.item);
762
	add_chbx_li(change.prefix, change.item);
763
	add_radio_li(change.prefix, change.item);
764

    
765
	return;
766
    }
767

    
768
    if (change.new_val !== undefined)
769
	return;
770

    
771
    let ul = ul_by_prefix[change.prefix];
772
    if (ul.state === UL_STATE.EDITING_ENTRY &&
773
	ul.edited_item === change.item) {
774
	ul.state = UL_STATE.ADDING_ENTRY;
775
	return;
776
    }
777

    
778
    let uls_creators = [[ul.ul, item_li_id]];
779

    
780
    if (change.prefix !== TYPE_PREFIX.PAGE) {
781
	uls_creators.push([chbx_components_ul, chbx_li_id]);
782
	uls_creators.push([radio_components_ul, radio_li_id]);
783
    }
784

    
785
    for (let [components_ul, id_creator] of uls_creators) {
786
	let li = by_id(id_creator(change.prefix, change.item));
787
	components_ul.removeChild(li);
788
    }
789
}
790

    
791
main();
(4-4/4)