Project

General

Profile

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

haketilo / html / options_main.js @ b93f26bf

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
"use strict";
9

    
10
(() => {
11
    const get_storage = window.get_storage;
12
    const TYPE_PREFIX = window.TYPE_PREFIX;
13
    const TYPE_NAME = window.TYPE_NAME;
14
    const list_prefixes = window.list_prefixes;
15

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

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

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

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

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

    
51
	let span = li.firstElementChild;
52
	span.textContent = item;
53

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

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

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

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

    
70
		ul.ul.insertBefore(li, element);
71
		return;
72
	    }
73
	}
74

    
75
	ul.ul.appendChild(li);
76
    }
77

    
78
    const chbx_components_ul = by_id("chbx_components_ul");
79
    const radio_components_ul = by_id("radio_components_ul");
80

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

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

    
91
    //TODO: refactor the 2 functions below
92

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

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

    
103
	let chbx = li.firstElementChild;
104
	let span = chbx.nextElementSibling;
105

    
106
	span.textContent = nice_name(prefix, name);
107

    
108
	chbx_components_ul.appendChild(li);
109
    }
110

    
111
    var radio_component_none_li = by_id("radio_component_none_li");
112

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

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

    
123
	let radio = li.firstElementChild;
124
	let span = radio.nextElementSibling;
125

    
126
	span.textContent = nice_name(prefix, name);
127

    
128
	radio_components_ul.insertBefore(li, radio_component_none_li);
129
    }
130

    
131
    const page_payload_span = by_id("page_payload");
132

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

    
147
    const page_allow_chbx = by_id("page_allow_chbx");
148

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

    
156
	set_page_components(settings.components);
157
    }
158

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

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

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

    
178
	return [url, settings];
179
    }
180

    
181
    const empty_bag_component_li = by_id("empty_bag_component_li");
182
    var bag_components_ul = by_id("bag_components_ul");
183

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

    
200
	bag_components_ul.appendChild(empty_bag_component_li);
201
    }
202

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

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

    
212
	ul.work_li.insertBefore(bag_components_ul, old_components_ul);
213
	ul.work_li.removeChild(old_components_ul);
214

    
215
	add_bag_components(components);
216
    }
217

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

    
224
	let components = [];
225

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

    
233
	return [ul.work_name_input.value, components];
234
    }
235

    
236
    const script_url_input = by_id("script_url_field");
237
    const script_sha256_input = by_id("script_sha256_field");
238
    const script_contents_field = by_id("script_contents_field");
239

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

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

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

    
266
    function cancel_work(prefix)
267
    {
268
	let ul = ul_by_prefix[prefix];
269

    
270
	if (ul.state === UL_STATE.IDLE)
271
	    return;
272

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

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

    
281
    function save_work(prefix)
282
    {
283
	let ul = ul_by_prefix[prefix];
284

    
285
	if (ul.state === UL_STATE.IDLE)
286
	    return;
287

    
288
	let [item, data] = ul.get_work_li_data(ul);
289

    
290
	/* Here we fire promises and return without waiting. */
291

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

    
297
	cancel_work(prefix);
298
    }
299

    
300
    function edit_item(prefix, item)
301
    {
302
	cancel_work(prefix);
303

    
304
	let ul = ul_by_prefix[prefix];
305
	let li = by_id(item_li_id(prefix, item));
306
	ul.reset_work_li(ul, item, storage.get(prefix, item));
307
	ul.ul.insertBefore(ul.work_li, li);
308
	ul.ul.removeChild(li);
309
	ul.work_li.classList.remove("hide");
310

    
311
	ul.state = UL_STATE.EDITING_ENTRY;
312
	ul.edited_item = item;
313
    }
314

    
315
    const file_downloader = by_id("file_downloader");
316

    
317
    function recursively_export_item(prefix, name, added_items, items_data)
318
    {
319
	let key = prefix + name;
320

    
321
	if (added_items.has(key))
322
	    return;
323

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

    
330
	if (prefix !== TYPE_PREFIX.SCRIPT) {
331
	    let components = prefix === TYPE_PREFIX.BAG ?
332
		data : [data.components];
333

    
334
	    for (let [comp_prefix, comp_name] of components) {
335
		recursively_export_item(comp_prefix, comp_name,
336
					added_items, items_data);
337
	    }
338
	}
339

    
340
	items_data.push({[key]: data});
341
	added_items.add(key);
342
    }
343

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

    
359
    function add_new_item(prefix)
360
    {
361
	cancel_work(prefix);
362

    
363
	let ul = ul_by_prefix[prefix];
364
	ul.reset_work_li(ul);
365
	ul.work_li.classList.remove("hide");
366
	ul.ul.appendChild(ul.work_li);
367

    
368
	ul.state = UL_STATE.ADDING_ENTRY;
369
    }
370

    
371
    const chbx_components_window = by_id("chbx_components_window");
372

    
373
    function bag_components()
374
    {
375
	chbx_components_window.classList.remove("hide");
376
	radio_components_window.classList.add("hide");
377

    
378
	for (let li of chbx_components_ul.children) {
379
	    let chbx = li.firstElementChild;
380
	    chbx.checked = false;
381
	}
382
    }
383

    
384
    function commit_bag_components()
385
    {
386
	let selected = [];
387

    
388
	for (let li of chbx_components_ul.children) {
389
	    let chbx = li.firstElementChild;
390
	    if (!chbx.checked)
391
		continue;
392

    
393
	    selected.push([li.getAttribute("data-prefix"),
394
			   li.getAttribute("data-name")]);
395
	}
396

    
397
	add_bag_components(selected);
398
	cancel_components();
399
    }
400

    
401
    const radio_components_window = by_id("radio_components_window");
402
    var radio_component_none_input = by_id("radio_component_none_input");
403

    
404
    function page_components()
405
    {
406
	radio_components_window.classList.remove("hide");
407
	chbx_components_window.classList.add("hide");
408

    
409
	radio_component_none_input.checked = true;
410

    
411
	let components = work_page_li_components();
412
	if (components === undefined)
413
	    return;
414

    
415
	let [prefix, item] = components;
416
	let li = by_id(radio_li_id(prefix, item));
417
	if (li === null)
418
	    radio_component_none_input.checked = false;
419
	else
420
	    li.firstElementChild.checked = true;
421
    }
422

    
423
    function commit_page_components()
424
    {
425
	let components = null;
426

    
427
	for (let li of radio_components_ul.children) {
428
	    let radio = li.firstElementChild;
429
	    if (!radio.checked)
430
		continue;
431

    
432
	    components = [li.getAttribute("data-prefix"),
433
			  li.getAttribute("data-name")];
434

    
435
	    if (radio.id === "radio_component_none_input")
436
		components = undefined;
437

    
438
	    break;
439
	}
440

    
441
	if (components !== null)
442
	    set_page_components(components);
443
	cancel_components();
444
    }
445

    
446
    function cancel_components()
447
    {
448
	chbx_components_window.classList.add("hide");
449
	radio_components_window.classList.add("hide");
450
    }
451

    
452
    const UL_STATE = {
453
	EDITING_ENTRY : 0,
454
	ADDING_ENTRY : 1,
455
	IDLE : 2
456
    };
457

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

    
492
    const import_window = by_id("import_window");
493
    const import_loading_radio = by_id("import_loading_radio");
494
    const import_failed_radio = by_id("import_failed_radio");
495
    const import_selection_radio = by_id("import_selection_radio");
496
    const bad_file_errormsg = by_id("bad_file_errormsg");
497

    
498
    /*
499
     * Newer browsers could utilise `text' method of File objects.
500
     * Older ones require FileReader.
501
     */
502

    
503
    function _read_file(file, resolve, reject)
504
    {
505
	let reader = new FileReader();
506

    
507
	reader.onload = () => resolve(reader.result);
508
	reader.onerror = () => reject(reader.error);
509
	reader.readAsText(file);
510
    }
511

    
512
    function read_file(file)
513
    {
514
	return new Promise((resolve, reject) =>
515
			   _read_file(file, resolve, reject));
516
    }
517

    
518
    async function import_from_file(event)
519
    {
520
	let files = event.target.files;
521
	if (files.length < 1)
522
	    return;
523

    
524
	import_window.classList.remove("hide");
525
	import_loading_radio.checked = true;
526

    
527
	let result = undefined;
528

    
529
	try {
530
	    result = JSON.parse(await read_file(files[0]));
531
	} catch(e) {
532
	    bad_file_errormsg.textContent = "" + e;
533
	    import_failed_radio.checked = true;
534
	    return;
535
	}
536

    
537
	let errormsg = validate_settings(result);
538
	if (errormsg !== false) {
539
	    bad_file_errormsg.textContent = errormsg;
540
	    import_failed_radio.checked = true;
541
	    return;
542
	}
543

    
544
	populate_import_list(result);
545
	import_selection_radio.checked = true;
546
    }
547

    
548
    function validate_settings(settings)
549
    {
550
	// TODO
551
	return false;
552
    }
553

    
554
    function import_li_id(prefix, item)
555
    {
556
	return `ili_${prefix}_${item}`;
557
    }
558

    
559
    let import_ul = by_id("import_ul");
560
    let import_chbxs_colliding = undefined;
561
    let settings_import_map = undefined;
562

    
563
    function populate_import_list(settings)
564
    {
565
	let old_children = import_ul.children;
566
	while (old_children[0] !== undefined)
567
	    import_ul.removeChild(old_children[0]);
568

    
569
	import_chbxs_colliding = [];
570
	settings_import_map = new Map();
571

    
572
	for (let setting of settings) {
573
	    let [key, value] = Object.entries(setting)[0];
574
	    let prefix = key[0];
575
	    let name = key.substring(1);
576
	    add_import_li(prefix, name);
577
	    settings_import_map.set(key, value);
578
	}
579
    }
580

    
581
    function add_import_li(prefix, name)
582
    {
583
	let li = import_li_template.cloneNode(true);
584
	let name_span = li.firstElementChild;
585
	let chbx = name_span.nextElementSibling;
586
	let warning_span = chbx.nextElementSibling;
587

    
588
	li.setAttribute("data-prefix", prefix);
589
	li.setAttribute("data-name", name);
590
	li.id = import_li_id(prefix, name);
591
	name_span.textContent = nice_name(prefix, name);
592

    
593
	if (storage.get(prefix, name) !== undefined) {
594
	    import_chbxs_colliding.push(chbx);
595
	    warning_span.textContent = "(will overwrite existing setting!)";
596
	}
597

    
598
	import_ul.appendChild(li);
599
    }
600

    
601
    function check_all_imports()
602
    {
603
	for (let li of import_ul.children)
604
	    li.firstElementChild.nextElementSibling.checked = true;
605
    }
606

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

    
613
    function uncheck_colliding_imports()
614
    {
615
	for (let chbx of import_chbxs_colliding)
616
	    chbx.checked = false;
617
    }
618

    
619
    const file_opener_form = by_id("file_opener_form");
620

    
621
    function hide_import_window()
622
    {
623
	import_window.classList.add("hide");
624
	/* Let GC free some memory */
625
	import_chbxs_colliding = undefined;
626
	settings_import_map = undefined;
627

    
628
	/*
629
	 * Reset file <input>. Without this, a second attempt to import the same
630
	 * file would result in "change" event on happening on <input> element.
631
	 */
632
	file_opener_form.reset();
633
    }
634

    
635
    function commit_import()
636
    {
637
	for (let li of import_ul.children) {
638
	    let chbx = li.firstElementChild.nextElementSibling;
639

    
640
	    if (!chbx.checked)
641
		continue;
642

    
643
	    let prefix = li.getAttribute("data-prefix");
644
	    let name = li.getAttribute("data-name");
645
	    let key = prefix + name;
646
	    let value = settings_import_map.get(key);
647
	    storage.set(prefix, name, value);
648
	}
649

    
650
	hide_import_window();
651
    }
652

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

    
674
    async function main()
675
    {
676
	storage = await get_storage();
677

    
678
	for (let prefix of list_prefixes) {
679
	    for (let item of storage.get_all_names(prefix).sort()) {
680
		add_li(prefix, item, true);
681
		add_chbx_li(prefix, item);
682
		add_radio_li(prefix, item);
683
	    }
684

    
685
	    let name = TYPE_NAME[prefix];
686

    
687
	    let add_but = by_id(`add_${name}_but`);
688
	    let discard_but = by_id(`discard_${name}_but`);
689
	    let save_but = by_id(`save_${name}_but`);
690

    
691
	    add_but.addEventListener("click", () => add_new_item(prefix));
692
	    discard_but.addEventListener("click", () => cancel_work(prefix));
693
	    save_but.addEventListener("click", () => save_work(prefix));
694

    
695
	    if (prefix === TYPE_PREFIX.SCRIPT)
696
		continue;
697

    
698
	    let ul = ul_by_prefix[prefix];
699

    
700
	    let commit_components_but = by_id(`commit_${name}_components_but`);
701
	    let cancel_components_but = by_id(`cancel_${name}_components_but`);
702
	    let select_components_but = by_id(`select_${name}_components_but`);
703

    
704
	    commit_components_but
705
		.addEventListener("click", ul.commit_components);
706
	    select_components_but
707
		.addEventListener("click", ul.select_components);
708
	    cancel_components_but.addEventListener("click", cancel_components);
709
	}
710

    
711
	initialize_import_facility();
712

    
713
	storage.add_change_listener(handle_change);
714
    }
715

    
716
    function handle_change(change)
717
    {
718
	if (change.old_val === undefined) {
719
	    add_li(change.prefix, change.item);
720
	    add_chbx_li(change.prefix, change.item);
721
	    add_radio_li(change.prefix, change.item);
722

    
723
	    return;
724
	}
725

    
726
	if (change.new_val !== undefined)
727
	    return;
728

    
729
	let ul = ul_by_prefix[change.prefix];
730
	if (ul.state === UL_STATE.EDITING_ENTRY &&
731
	    ul.edited_item === change.item) {
732
	    ul.state = UL_STATE.ADDING_ENTRY;
733
	    return;
734
	}
735

    
736
	let uls_creators = [[ul.ul, item_li_id]];
737

    
738
	if (change.prefix !== TYPE_PREFIX.PAGE) {
739
	    uls_creators.push([chbx_components_ul, chbx_li_id]);
740
	    uls_creators.push([radio_components_ul, radio_li_id]);
741
	}
742

    
743
	for (let [components_ul, id_creator] of uls_creators) {
744
	    let li = by_id(id_creator(change.prefix, change.item));
745
	    components_ul.removeChild(li);
746
	}
747
    }
748

    
749
    main();
750
})();
(4-4/4)