Project

General

Profile

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

haketilo / html / options_main.js @ 5c75d744

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

    
8
/*
9
 * IMPORTS_START
10
 * IMPORT get_remote_storage
11
 * IMPORT TYPE_PREFIX
12
 * IMPORT TYPE_NAME
13
 * IMPORT list_prefixes
14
 * IMPORT nice_name
15
 * IMPORT parse_json_with_schema
16
 * IMPORT get_template
17
 * IMPORT by_id
18
 * IMPORT matchers
19
 * IMPORT get_import_frame
20
 * IMPORT init_default_policy_dialog
21
 * IMPORTS_END
22
 */
23

    
24
var storage;
25

    
26
const item_li_template = get_template("item_li");
27
const bag_component_li_template = get_template("bag_component_li");
28
const chbx_component_li_template = get_template("chbx_component_li");
29
const radio_component_li_template = get_template("radio_component_li");
30
/* Make sure they are later cloned without id. */
31
item_li_template.removeAttribute("id");
32
bag_component_li_template.removeAttribute("id");
33
chbx_component_li_template.removeAttribute("id");
34
radio_component_li_template.removeAttribute("id");
35

    
36
function list_set_scrollbar(list_elem)
37
{
38
    const op = ((list_elem.children.length === 1 &&
39
		 list_elem.children[0].classList.contains("hide")) ||
40
		list_elem.children.length < 1) ? "remove" : "add";
41
    while (!list_elem.classList.contains("table_wrapper"))
42
	list_elem = list_elem.parentElement;
43
    list_elem.classList[op]("always_scrollbar");
44
}
45

    
46
function item_li_id(prefix, item)
47
{
48
    return `li_${prefix}_${item}`;
49
}
50

    
51
/* Insert into list of bags/pages/scripts/repos */
52
function add_li(prefix, item, at_the_end=false)
53
{
54
    let ul = ul_by_prefix[prefix];
55
    let li = item_li_template.cloneNode(true);
56
    li.id = item_li_id(prefix, item);
57

    
58
    let span = li.firstElementChild;
59
    span.textContent = item;
60

    
61
    let edit_button = span.nextElementSibling;
62
    edit_button.addEventListener("click", () => edit_item(prefix, item));
63

    
64
    let remove_button = edit_button.nextElementSibling;
65
    remove_button.addEventListener("click",
66
				   () => storage.remove(prefix, item));
67

    
68
    let export_button = remove_button.nextElementSibling;
69
    export_button.addEventListener("click",
70
				   () => export_item(prefix, item));
71
    if (prefix === TYPE_PREFIX.REPO)
72
	export_button.remove();
73

    
74
    if (!at_the_end) {
75
	for (let element of ul.ul.children) {
76
	    if (element.id < li.id || element.id.startsWith("work_"))
77
		continue;
78

    
79
	    ul.ul.insertBefore(li, element);
80
	    break;
81
	}
82
    }
83
    if (!li.parentElement) {
84
	if (ul.work_li !== ul.ul.lastElementChild)
85
	    ul.ul.appendChild(li);
86
	else
87
	    ul.work_li.before(li);
88
    }
89

    
90
    list_set_scrollbar(ul.ul);
91
}
92

    
93
const chbx_components_ul = by_id("chbx_components_ul");
94
const radio_components_ul = by_id("radio_components_ul");
95

    
96
function chbx_li_id(prefix, item)
97
{
98
    return `cli_${prefix}_${item}`;
99
}
100

    
101
function radio_li_id(prefix, item)
102
{
103
    return `rli_${prefix}_${item}`;
104
}
105

    
106
//TODO: refactor the 2 functions below
107

    
108
function add_chbx_li(prefix, name)
109
{
110
    if (![TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(prefix))
111
	return;
112

    
113
    let li = chbx_component_li_template.cloneNode(true);
114
    li.id = chbx_li_id(prefix, name);
115
    li.setAttribute("data-prefix", prefix);
116
    li.setAttribute("data-name", name);
117

    
118
    let chbx = li.firstElementChild.firstElementChild;
119
    let span = chbx.nextElementSibling;
120

    
121
    span.textContent = nice_name(prefix, name);
122

    
123
    chbx_components_ul.appendChild(li);
124
    list_set_scrollbar(chbx_components_ul);
125
}
126

    
127
var radio_component_none_li = by_id("radio_component_none_li");
128

    
129
function add_radio_li(prefix, name)
130
{
131
    if (![TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(prefix))
132
	return;
133

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

    
139
    let radio = li.firstElementChild.firstElementChild;
140
    let span = radio.nextElementSibling;
141

    
142
    span.textContent = nice_name(prefix, name);
143

    
144
    radio_component_none_li.before(li);
145
    list_set_scrollbar(radio_components_ul);
146
}
147

    
148
/* Used to reset edited repo. */
149
function reset_work_repo_li(ul, item, _)
150
{
151
    ul.work_name_input.value = maybe_string(item);
152
}
153

    
154
/* Used to get repo data for saving */
155
function work_repo_li_data(ul)
156
{
157
    return [ul.work_name_input.value, {}];
158
}
159

    
160
const allow_native_scripts_container = by_id("allow_native_scripts_container");
161
const page_payload_span = by_id("page_payload");
162

    
163
function set_page_components(components)
164
{
165
    if (components === undefined) {
166
	page_payload_span.setAttribute("data-payload", "no");
167
	page_payload_span.textContent = "(None)";
168
	allow_native_scripts_container.classList.remove("form_disabled");
169
    } else {
170
	page_payload_span.setAttribute("data-payload", "yes");
171
	let [prefix, name] = components;
172
	page_payload_span.setAttribute("data-prefix", prefix);
173
	page_payload_span.setAttribute("data-name", name);
174
	page_payload_span.textContent = nice_name(prefix, name);
175
	allow_native_scripts_container.classList.add("form_disabled");
176
    }
177
}
178

    
179
const page_allow_chbx = by_id("page_allow_chbx");
180

    
181
/* Used to reset edited page. */
182
function reset_work_page_li(ul, item, settings)
183
{
184
    ul.work_name_input.value = maybe_string(item);
185
    settings = settings || {allow: false, components: undefined};
186
    page_allow_chbx.checked = !!settings.allow;
187

    
188
    set_page_components(settings.components);
189
}
190

    
191
function work_page_li_components()
192
{
193
    if (page_payload_span.getAttribute("data-payload") === "no")
194
	return undefined;
195

    
196
    let prefix = page_payload_span.getAttribute("data-prefix");
197
    let name = page_payload_span.getAttribute("data-name");
198
    return [prefix, name];
199
}
200

    
201
/* Used to get edited page data for saving. */
202
function work_page_li_data(ul)
203
{
204
    let url = ul.work_name_input.value;
205
    let settings = {
206
	components : work_page_li_components(),
207
	allow : !!page_allow_chbx.checked
208
    };
209

    
210
    return [url, settings];
211
}
212

    
213
const empty_bag_component_li = by_id("empty_bag_component_li");
214
var bag_components_ul = by_id("bag_components_ul");
215

    
216
function remove_bag_component_entry(entry)
217
{
218
    const list = entry.parentElement;
219
    entry.remove();
220
    list_set_scrollbar(list);
221
}
222

    
223
/* Used to construct and update components list of edited bag. */
224
function add_bag_components(components)
225
{
226
    for (let component of components) {
227
	let [prefix, name] = component;
228
	let li = bag_component_li_template.cloneNode(true);
229
	li.setAttribute("data-prefix", prefix);
230
	li.setAttribute("data-name", name);
231

    
232
	let span = li.firstElementChild;
233
	span.textContent = nice_name(prefix, name);
234
	let remove_but = span.nextElementSibling;
235
	remove_but.addEventListener("click",
236
				    () => remove_bag_component_entry(li));
237
	bag_components_ul.appendChild(li);
238
    }
239

    
240
    bag_components_ul.appendChild(empty_bag_component_li);
241
    list_set_scrollbar(bag_components_ul);
242
}
243

    
244
/* Used to reset edited bag. */
245
function reset_work_bag_li(ul, item, components)
246
{
247
    components = components || [];
248

    
249
    ul.work_name_input.value = maybe_string(item);
250
    let old_components_ul = bag_components_ul;
251
    bag_components_ul = old_components_ul.cloneNode(false);
252

    
253
    old_components_ul.replaceWith(bag_components_ul);
254

    
255
    add_bag_components(components);
256
}
257

    
258
/* Used to get edited bag data for saving. */
259
function work_bag_li_data(ul)
260
{
261
    let component_li = bag_components_ul.firstElementChild;
262

    
263
    let components = [];
264

    
265
    /* Last list element is empty li with id set. */
266
    while (component_li.id === '') {
267
	components.push([component_li.getAttribute("data-prefix"),
268
			 component_li.getAttribute("data-name")]);
269
	component_li = component_li.nextElementSibling;
270
    }
271

    
272
    return [ul.work_name_input.value, components];
273
}
274

    
275
const script_url_input = by_id("script_url_field");
276
const script_sha256_input = by_id("script_sha256_field");
277
const script_contents_field = by_id("script_contents_field");
278

    
279
function maybe_string(maybe_defined)
280
{
281
    return maybe_defined === undefined ? "" : maybe_defined + "";
282
}
283

    
284
/* Used to reset edited script. */
285
function reset_work_script_li(ul, name, data)
286
{
287
    ul.work_name_input.value = maybe_string(name);
288
    if (data === undefined)
289
	data = {};
290
    script_url_input.value = maybe_string(data.url);
291
    script_sha256_input.value = maybe_string(data.hash);
292
    script_contents_field.value = maybe_string(data.text);
293
}
294

    
295
/* Used to get edited script data for saving. */
296
function work_script_li_data(ul)
297
{
298
    return [ul.work_name_input.value, {
299
	url : script_url_input.value,
300
	hash : script_sha256_input.value,
301
	text : script_contents_field.value
302
    }];
303
}
304

    
305
function cancel_work(prefix)
306
{
307
    let ul = ul_by_prefix[prefix];
308

    
309
    if (ul.state === UL_STATE.IDLE)
310
	return;
311

    
312
    if (ul.state === UL_STATE.EDITING_ENTRY) {
313
	add_li(prefix, ul.edited_item);
314
    }
315

    
316
    ul.work_li.classList.add("hide");
317
    ul.ul.append(ul.work_li);
318
    list_set_scrollbar(ul.ul);
319
    ul.state = UL_STATE.IDLE;
320
}
321

    
322
function save_work(prefix)
323
{
324
    let ul = ul_by_prefix[prefix];
325

    
326
    if (ul.state === UL_STATE.IDLE)
327
	return;
328

    
329
    let [item, data] = ul.get_work_li_data(ul);
330

    
331
    /* Here we fire promises and return without waiting. */
332

    
333
    if (ul.state === UL_STATE.EDITING_ENTRY)
334
	storage.replace(prefix, ul.edited_item, item, data);
335
    if (ul.state === UL_STATE.ADDING_ENTRY)
336
	storage.set(prefix, item, data);
337

    
338
    cancel_work(prefix);
339
}
340

    
341
function edit_item(prefix, item)
342
{
343
    cancel_work(prefix);
344

    
345
    let ul = ul_by_prefix[prefix];
346
    let li = by_id(item_li_id(prefix, item));
347

    
348
    if (li === null) {
349
	add_new_item(prefix, item);
350
	return;
351
    }
352

    
353
    ul.reset_work_li(ul, item, storage.get(prefix, item));
354
    ul.ul.insertBefore(ul.work_li, li);
355
    ul.ul.removeChild(li);
356
    ul.work_li.classList.remove("hide");
357
    list_set_scrollbar(ul.ul);
358

    
359
    ul.state = UL_STATE.EDITING_ENTRY;
360
    ul.edited_item = item;
361
}
362

    
363
const file_downloader = by_id("file_downloader");
364

    
365
function recursively_export_item(prefix, name, added_items, items_data)
366
{
367
    let key = prefix + name;
368

    
369
    if (added_items.has(key))
370
	return;
371

    
372
    let data = storage.get(prefix, name);
373
    if (data === undefined) {
374
	console.log(`${TYPE_NAME[prefix]} '${name}' for export not found`);
375
	return;
376
    }
377

    
378
    if (prefix !== TYPE_PREFIX.SCRIPT) {
379
	let components = prefix === TYPE_PREFIX.BAG ?
380
	    data : [data.components];
381

    
382
	for (let [comp_prefix, comp_name] of components) {
383
	    recursively_export_item(comp_prefix, comp_name,
384
				    added_items, items_data);
385
	}
386
    }
387

    
388
    items_data.push({[key]: data});
389
    added_items.add(key);
390
}
391

    
392
function export_item(prefix, name)
393
{
394
    let added_items = new Set();
395
    let items_data = [];
396
    recursively_export_item(prefix, name, added_items, items_data);
397
    let file = new Blob([JSON.stringify(items_data)],
398
			{type: "application/json"});
399
    let url = URL.createObjectURL(file);
400
    file_downloader.setAttribute("href", url);
401
    file_downloader.setAttribute("download", prefix + name + ".json");
402
    file_downloader.click();
403
    file_downloader.removeAttribute("href");
404
    URL.revokeObjectURL(url);
405
}
406

    
407
function add_new_item(prefix, name)
408
{
409
    cancel_work(prefix);
410

    
411
    let ul = ul_by_prefix[prefix];
412
    ul.reset_work_li(ul);
413
    ul.work_li.classList.remove("hide");
414
    ul.ul.appendChild(ul.work_li);
415
    list_set_scrollbar(ul.ul);
416

    
417
    if (name !== undefined)
418
	ul.work_name_input.value = name;
419
    ul.state = UL_STATE.ADDING_ENTRY;
420
}
421

    
422
const chbx_components_window = by_id("chbx_components_window");
423

    
424
function bag_components()
425
{
426
    chbx_components_window.classList.remove("hide");
427
    radio_components_window.classList.add("hide");
428

    
429
    for (let li of chbx_components_ul.children) {
430
	let chbx = li.firstElementChild.firstElementChild;
431
	chbx.checked = false;
432
    }
433
}
434

    
435
function commit_bag_components()
436
{
437
    let selected = [];
438

    
439
    for (let li of chbx_components_ul.children) {
440
	let chbx = li.firstElementChild.firstElementChild;
441
	if (!chbx.checked)
442
	    continue;
443

    
444
	selected.push([li.getAttribute("data-prefix"),
445
		       li.getAttribute("data-name")]);
446
    }
447

    
448
    add_bag_components(selected);
449
    cancel_components();
450
}
451

    
452
const radio_components_window = by_id("radio_components_window");
453
var radio_component_none_input = by_id("radio_component_none_input");
454

    
455
function page_components()
456
{
457
    radio_components_window.classList.remove("hide");
458
    chbx_components_window.classList.add("hide");
459

    
460
    radio_component_none_input.checked = true;
461

    
462
    let components = work_page_li_components();
463
    if (components === undefined)
464
	return;
465

    
466
    let [prefix, item] = components;
467
    let li = by_id(radio_li_id(prefix, item));
468

    
469
    if (li === null)
470
	radio_component_none_input.checked = false;
471
    else
472
	li.firstElementChild.firstElementChild.checked = true;
473
}
474

    
475
function commit_page_components()
476
{
477
    let components = null;
478

    
479
    for (let li of radio_components_ul.children) {
480
	let radio = li.firstElementChild.firstElementChild;
481
	if (!radio.checked)
482
	    continue;
483

    
484
	components = [li.getAttribute("data-prefix"),
485
		      li.getAttribute("data-name")];
486

    
487
	if (radio.id === "radio_component_none_input")
488
	    components = undefined;
489

    
490
	break;
491
    }
492

    
493
    if (components !== null)
494
	set_page_components(components);
495
    cancel_components();
496
}
497

    
498
function cancel_components()
499
{
500
    chbx_components_window.classList.add("hide");
501
    radio_components_window.classList.add("hide");
502
}
503

    
504
const UL_STATE = {
505
    EDITING_ENTRY : 0,
506
    ADDING_ENTRY : 1,
507
    IDLE : 2
508
};
509

    
510
const ul_by_prefix = {
511
    [TYPE_PREFIX.REPO] : {
512
	ul : by_id("repos_ul"),
513
	work_li : by_id("work_repo_li"),
514
	work_name_input : by_id("repo_url_field"),
515
	reset_work_li : reset_work_repo_li,
516
	get_work_li_data : work_repo_li_data,
517
	state : UL_STATE.IDLE,
518
	edited_item : undefined,
519
    },
520
    [TYPE_PREFIX.PAGE] : {
521
	ul : by_id("pages_ul"),
522
	work_li : by_id("work_page_li"),
523
	work_name_input : by_id("page_url_field"),
524
	reset_work_li : reset_work_page_li,
525
	get_work_li_data : work_page_li_data,
526
	select_components : page_components,
527
	commit_components : commit_page_components,
528
	state : UL_STATE.IDLE,
529
	edited_item : undefined,
530
    },
531
    [TYPE_PREFIX.BAG] : {
532
	ul : by_id("bags_ul"),
533
	work_li : by_id("work_bag_li"),
534
	work_name_input : by_id("bag_name_field"),
535
	reset_work_li : reset_work_bag_li,
536
	get_work_li_data : work_bag_li_data,
537
	select_components : bag_components,
538
	commit_components : commit_bag_components,
539
	state : UL_STATE.IDLE,
540
	edited_item : undefined,
541
    },
542
    [TYPE_PREFIX.SCRIPT] : {
543
	ul : by_id("scripts_ul"),
544
	work_li : by_id("work_script_li"),
545
	work_name_input : by_id("script_name_field"),
546
	reset_work_li : reset_work_script_li,
547
	get_work_li_data : work_script_li_data,
548
	state : UL_STATE.IDLE,
549
	edited_item : undefined,
550
    }
551
}
552

    
553
/*
554
 * Newer browsers could utilise `text' method of File objects.
555
 * Older ones require FileReader.
556
 */
557

    
558
function _read_file(file, resolve, reject)
559
{
560
    let reader = new FileReader();
561

    
562
    reader.onload = () => resolve(reader.result);
563
    reader.onerror = () => reject(reader.error);
564
    reader.readAsText(file);
565
}
566

    
567
function read_file(file)
568
{
569
    return new Promise((resolve, reject) =>
570
		       _read_file(file, resolve, reject));
571
}
572

    
573
const url_regex = /^[a-z0-9]+:\/\/[^/]+\.[^/]{2,}(\/[^?#]*)?$/;
574
const empty_regex = /^$/;
575

    
576
const settings_schema = [
577
    [{}, "matchentry", "minentries", 1,
578
     new RegExp(`^${TYPE_PREFIX.SCRIPT}`), {
579
	 /* script data */
580
	 "url":    ["optional", url_regex, "or", empty_regex],
581
	 "sha256": ["optional", matchers.sha256, "or", empty_regex],
582
	 "text":   ["optional", "string"]
583
     },
584
     new RegExp(`^${TYPE_PREFIX.BAG}`), [
585
	 "optional",
586
	 [matchers.component, "repeat"],
587
	 "default", undefined
588
     ],
589
     new RegExp(`^${TYPE_PREFIX.PAGE}`), {
590
	 /* page data */
591
	 "components": ["optional", matchers.component]
592
     }], "repeat"
593
];
594

    
595
const import_window = by_id("import_window");
596
let import_frame;
597

    
598
async function import_from_file(event)
599
{
600
    let files = event.target.files;
601
    if (files.length < 1)
602
	return;
603

    
604
    import_window.classList.remove("hide");
605
    import_frame.show_loading();
606

    
607
    try {
608
	const file = await read_file(files[0]);
609
	var result = parse_json_with_schema(settings_schema, file);
610
    } catch(e) {
611
	import_frame.show_error("Bad file :(", "" + e);
612
	return;
613
    }
614

    
615
    import_frame.show_selection(result);
616
}
617

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

    
620
function hide_import_window()
621
{
622
    import_window.classList.add("hide");
623

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

    
631
async function initialize_import_facility()
632
{
633
    let import_but = by_id("import_but");
634
    let file_opener = by_id("file_opener");
635

    
636
    import_but.addEventListener("click", () => file_opener.click());
637
    file_opener.addEventListener("change", import_from_file);
638

    
639
    import_frame = await get_import_frame();
640
    import_frame.onclose = hide_import_window;
641
    import_frame.style_table("has_bottom_line", "always_scrollbar",
642
			     "has_upper_line", "tight_table");
643
}
644

    
645
/*
646
 * If url has a target appended, e.g.
647
 * chrome-extension://hnhmbnpohhlmhehionjgongbnfdnabdl/html/options.html#smyhax
648
 * that target will be split into prefix and item name (e.g. "s" and "myhax")
649
 * and editing of that respective item will be started.
650
 *
651
 * We don't need to worry about the state of the page (e.g. some editing being
652
 * in progress) in jump_to_item() - this function is called at the beginning,
653
 * together with callbacks being assigned to buttons, so it is safe to assume
654
 * lists are initialized with items and page is in its virgin state with regard
655
 * to everything else.
656
 */
657
function jump_to_item(url_with_item)
658
{
659
    const [dummy1, base_url, dummy2, target] =
660
	  /^([^#]*)(#(.*))?$/i.exec(url_with_item);
661
    if (target === undefined)
662
	return;
663

    
664
    const prefix = target.substring(0, 1);
665

    
666
    if (!list_prefixes.includes(prefix)) {
667
	history.replaceState(null, "", base_url);
668
	return;
669
    }
670

    
671
    by_id(`show_${TYPE_NAME[prefix]}s`).checked = true;
672
    edit_item(prefix, decodeURIComponent(target.substring(1)));
673
}
674

    
675
async function main()
676
{
677
    init_default_policy_dialog();
678

    
679
    storage = await get_remote_storage();
680

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

    
688
	let name = TYPE_NAME[prefix];
689

    
690
	let add_but = by_id(`add_${name}_but`);
691
	let discard_but = by_id(`discard_${name}_but`);
692
	let save_but = by_id(`save_${name}_but`);
693

    
694
	add_but.addEventListener("click", () => add_new_item(prefix));
695
	discard_but.addEventListener("click", () => cancel_work(prefix));
696
	save_but.addEventListener("click", () => save_work(prefix));
697

    
698
	if ([TYPE_PREFIX.REPO, TYPE_PREFIX.SCRIPT].includes(prefix))
699
	    continue;
700

    
701
	let ul = ul_by_prefix[prefix];
702

    
703
	let commit_components_but = by_id(`commit_${name}_components_but`);
704
	let cancel_components_but = by_id(`cancel_${name}_components_but`);
705
	let select_components_but = by_id(`select_${name}_components_but`);
706

    
707
	commit_components_but
708
	    .addEventListener("click", ul.commit_components);
709
	select_components_but
710
	    .addEventListener("click", ul.select_components);
711
	cancel_components_but.addEventListener("click", cancel_components);
712
    }
713

    
714
    jump_to_item(document.URL);
715

    
716
    storage.add_change_listener(handle_change);
717

    
718
    await initialize_import_facility();
719
}
720

    
721
function handle_change(change)
722
{
723
    if (change.old_val === undefined) {
724
	add_li(change.prefix, change.item);
725
	add_chbx_li(change.prefix, change.item);
726
	add_radio_li(change.prefix, change.item);
727

    
728
	return;
729
    }
730

    
731
    if (change.new_val !== undefined)
732
	return;
733

    
734
    let ul = ul_by_prefix[change.prefix];
735
    if (ul.state === UL_STATE.EDITING_ENTRY &&
736
	ul.edited_item === change.item) {
737
	ul.state = UL_STATE.ADDING_ENTRY;
738
	return;
739
    }
740

    
741
    let uls_creators = [[ul.ul, item_li_id]];
742

    
743
    if ([TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(change.prefix)) {
744
	uls_creators.push([chbx_components_ul, chbx_li_id]);
745
	uls_creators.push([radio_components_ul, radio_li_id]);
746
    }
747

    
748
    for (let [components_ul, id_creator] of uls_creators) {
749
	let li = by_id(id_creator(change.prefix, change.item));
750
	components_ul.removeChild(li);
751
	list_set_scrollbar(components_ul);
752
    }
753
}
754

    
755
main();
(12-12/14)