Project

General

Profile

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

haketilo / html / options_main.js @ 2bd35bc4

1
/**
2
 * This file is part of Haketilo.
3
 *
4
 * Function: Settings page logic.
5
 *
6
 * Copyright (C) 2021 Wojtek Kosior
7
 * Redistribution terms are gathered in the `copyright' file.
8
 */
9

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

    
26
var storage;
27

    
28
const item_li_template = get_template("item_li");
29
const bag_component_li_template = get_template("bag_component_li");
30
const chbx_component_li_template = get_template("chbx_component_li");
31
const radio_component_li_template = get_template("radio_component_li");
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

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

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

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

    
60
    let span = li.firstElementChild;
61
    span.textContent = item;
62

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

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

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

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

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

    
92
    list_set_scrollbar(ul.ul);
93
}
94

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

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

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

    
108
//TODO: refactor the 2 functions below
109

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

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

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

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

    
125
    chbx_components_ul.appendChild(li);
126
    list_set_scrollbar(chbx_components_ul);
127
}
128

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

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

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

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

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

    
146
    radio_component_none_li.before(li);
147
    list_set_scrollbar(radio_components_ul);
148
}
149

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

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

    
162
const allow_native_scripts_container = by_id("allow_native_scripts_container");
163
const page_payload_span = by_id("page_payload");
164

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

    
181
const page_allow_chbx = by_id("page_allow_chbx");
182

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

    
190
    set_page_components(settings.components);
191
}
192

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

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

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

    
212
    return [url, settings];
213
}
214

    
215
const empty_bag_component_li = by_id("empty_bag_component_li");
216
var bag_components_ul = by_id("bag_components_ul");
217

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

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

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

    
242
    bag_components_ul.appendChild(empty_bag_component_li);
243
    list_set_scrollbar(bag_components_ul);
244
}
245

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

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

    
255
    old_components_ul.replaceWith(bag_components_ul);
256

    
257
    add_bag_components(components);
258
}
259

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

    
265
    let components = [];
266

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

    
274
    return [ul.work_name_input.value, components];
275
}
276

    
277
const script_url_input = by_id("script_url_field");
278
const script_sha256_input = by_id("script_sha256_field");
279
const script_contents_field = by_id("script_contents_field");
280

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

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

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

    
307
function cancel_work(prefix)
308
{
309
    let ul = ul_by_prefix[prefix];
310

    
311
    if (ul.state === UL_STATE.IDLE)
312
	return;
313

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

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

    
324
function save_work(prefix)
325
{
326
    let ul = ul_by_prefix[prefix];
327

    
328
    if (ul.state === UL_STATE.IDLE)
329
	return;
330

    
331
    let [item, data] = ul.get_work_li_data(ul);
332

    
333
    /* Here we fire promises and return without waiting. */
334

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

    
340
    cancel_work(prefix);
341
}
342

    
343
function edit_item(prefix, item)
344
{
345
    cancel_work(prefix);
346

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

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

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

    
361
    ul.state = UL_STATE.EDITING_ENTRY;
362
    ul.edited_item = item;
363
}
364

    
365
const file_downloader = by_id("file_downloader");
366

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

    
371
    if (added_items.has(key))
372
	return;
373

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

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

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

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

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

    
409
function add_new_item(prefix, name)
410
{
411
    cancel_work(prefix);
412

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

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

    
424
const chbx_components_window = by_id("chbx_components_window");
425

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

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

    
437
function commit_bag_components()
438
{
439
    let selected = [];
440

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

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

    
450
    add_bag_components(selected);
451
    cancel_components();
452
}
453

    
454
const radio_components_window = by_id("radio_components_window");
455
var radio_component_none_input = by_id("radio_component_none_input");
456

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

    
462
    radio_component_none_input.checked = true;
463

    
464
    let components = work_page_li_components();
465
    if (components === undefined)
466
	return;
467

    
468
    let [prefix, item] = components;
469
    let li = by_id(radio_li_id(prefix, item));
470

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

    
477
function commit_page_components()
478
{
479
    let components = null;
480

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

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

    
489
	if (radio.id === "radio_component_none_input")
490
	    components = undefined;
491

    
492
	break;
493
    }
494

    
495
    if (components !== null)
496
	set_page_components(components);
497
    cancel_components();
498
}
499

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

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

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

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

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

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

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

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

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

    
597
const import_window = by_id("import_window");
598
let import_frame;
599

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

    
606
    import_window.classList.remove("hide");
607
    import_frame.show_loading();
608

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

    
617
    import_frame.show_selection(result);
618
}
619

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

    
622
function hide_import_window()
623
{
624
    import_window.classList.add("hide");
625

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

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

    
638
    import_but.addEventListener("click", () => file_opener.click());
639
    file_opener.addEventListener("change", import_from_file);
640

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

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

    
666
    const prefix = target.substring(0, 1);
667

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

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

    
677
async function main()
678
{
679
    init_default_policy_dialog();
680

    
681
    storage = await get_remote_storage();
682

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

    
690
	let name = TYPE_NAME[prefix];
691

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

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

    
700
	if ([TYPE_PREFIX.REPO, TYPE_PREFIX.SCRIPT].includes(prefix))
701
	    continue;
702

    
703
	let ul = ul_by_prefix[prefix];
704

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

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

    
716
    jump_to_item(document.URL);
717

    
718
    storage.add_change_listener(handle_change);
719

    
720
    await initialize_import_facility();
721
}
722

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

    
730
	return;
731
    }
732

    
733
    if (change.new_val !== undefined)
734
	return;
735

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

    
743
    let uls_creators = [[ul.ul, item_li_id]];
744

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

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

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