Project

General

Profile

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

haketilo / html / options_main.js @ 82836b92

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 nice_name
15
 * IMPORTS_END
16
 */
17

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
88
//TODO: refactor the 2 functions below
89

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

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

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

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

    
105
    chbx_components_ul.appendChild(li);
106
}
107

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

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

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

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

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

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

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

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

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

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

    
153
    set_page_components(settings.components);
154
}
155

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

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

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

    
175
    return [url, settings];
176
}
177

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

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

    
197
    bag_components_ul.appendChild(empty_bag_component_li);
198
}
199

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

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

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

    
212
    add_bag_components(components);
213
}
214

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

    
221
    let components = [];
222

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
294
    cancel_work(prefix);
295
}
296

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
414
    radio_component_none_input.checked = true;
415

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

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

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

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

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

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

    
443
	break;
444
    }
445

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

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

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

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

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

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

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

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

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

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

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

    
532
    let result = undefined;
533

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

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

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

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

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

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

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

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

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

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

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

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

    
603
    import_ul.appendChild(li);
604
}
605

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

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

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

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

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

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

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

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

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

    
655
    hide_import_window();
656
}
657

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

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

    
698
    const prefix = target.substring(0, 1);
699

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

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

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

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

    
720
	let name = TYPE_NAME[prefix];
721

    
722
	let add_but = by_id(`add_${name}_but`);
723
	let discard_but = by_id(`discard_${name}_but`);
724
	let save_but = by_id(`save_${name}_but`);
725

    
726
	add_but.addEventListener("click", () => add_new_item(prefix));
727
	discard_but.addEventListener("click", () => cancel_work(prefix));
728
	save_but.addEventListener("click", () => save_work(prefix));
729

    
730
	if (prefix === TYPE_PREFIX.SCRIPT)
731
	    continue;
732

    
733
	let ul = ul_by_prefix[prefix];
734

    
735
	let commit_components_but = by_id(`commit_${name}_components_but`);
736
	let cancel_components_but = by_id(`cancel_${name}_components_but`);
737
	let select_components_but = by_id(`select_${name}_components_but`);
738

    
739
	commit_components_but
740
	    .addEventListener("click", ul.commit_components);
741
	select_components_but
742
	    .addEventListener("click", ul.select_components);
743
	cancel_components_but.addEventListener("click", cancel_components);
744
    }
745

    
746
    jump_to_item(document.URL);
747

    
748
    initialize_import_facility();
749

    
750
    storage.add_change_listener(handle_change);
751
}
752

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

    
760
	return;
761
    }
762

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

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

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

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

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

    
786
main();
(4-4/4)