Project

General

Profile

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

haketilo / html / options_main.js @ 6247f163

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 page_payload_span = by_id("page_payload");
161

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

    
176
const page_allow_chbx = by_id("page_allow_chbx");
177

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

    
185
    set_page_components(settings.components);
186
}
187

    
188
function work_page_li_components()
189
{
190
    if (page_payload_span.getAttribute("data-payload") === "no")
191
	return undefined;
192

    
193
    let prefix = page_payload_span.getAttribute("data-prefix");
194
    let name = page_payload_span.getAttribute("data-name");
195
    return [prefix, name];
196
}
197

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

    
207
    return [url, settings];
208
}
209

    
210
const empty_bag_component_li = by_id("empty_bag_component_li");
211
var bag_components_ul = by_id("bag_components_ul");
212

    
213
function remove_bag_component_entry(entry)
214
{
215
    const list = entry.parentElement;
216
    entry.remove();
217
    list_set_scrollbar(list);
218
}
219

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

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

    
237
    bag_components_ul.appendChild(empty_bag_component_li);
238
    list_set_scrollbar(bag_components_ul);
239
}
240

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

    
246
    ul.work_name_input.value = maybe_string(item);
247
    let old_components_ul = bag_components_ul;
248
    bag_components_ul = old_components_ul.cloneNode(false);
249

    
250
    old_components_ul.replaceWith(bag_components_ul);
251

    
252
    add_bag_components(components);
253
}
254

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

    
260
    let components = [];
261

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

    
269
    return [ul.work_name_input.value, components];
270
}
271

    
272
const script_url_input = by_id("script_url_field");
273
const script_sha256_input = by_id("script_sha256_field");
274
const script_contents_field = by_id("script_contents_field");
275

    
276
function maybe_string(maybe_defined)
277
{
278
    return maybe_defined === undefined ? "" : maybe_defined + "";
279
}
280

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

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

    
302
function cancel_work(prefix)
303
{
304
    let ul = ul_by_prefix[prefix];
305

    
306
    if (ul.state === UL_STATE.IDLE)
307
	return;
308

    
309
    if (ul.state === UL_STATE.EDITING_ENTRY) {
310
	add_li(prefix, ul.edited_item);
311
    }
312

    
313
    ul.work_li.classList.add("hide");
314
    ul.ul.append(ul.work_li);
315
    list_set_scrollbar(ul.ul);
316
    ul.state = UL_STATE.IDLE;
317
}
318

    
319
function save_work(prefix)
320
{
321
    let ul = ul_by_prefix[prefix];
322

    
323
    if (ul.state === UL_STATE.IDLE)
324
	return;
325

    
326
    let [item, data] = ul.get_work_li_data(ul);
327

    
328
    /* Here we fire promises and return without waiting. */
329

    
330
    if (ul.state === UL_STATE.EDITING_ENTRY)
331
	storage.replace(prefix, ul.edited_item, item, data);
332
    if (ul.state === UL_STATE.ADDING_ENTRY)
333
	storage.set(prefix, item, data);
334

    
335
    cancel_work(prefix);
336
}
337

    
338
function edit_item(prefix, item)
339
{
340
    cancel_work(prefix);
341

    
342
    let ul = ul_by_prefix[prefix];
343
    let li = by_id(item_li_id(prefix, item));
344

    
345
    if (li === null) {
346
	add_new_item(prefix, item);
347
	return;
348
    }
349

    
350
    ul.reset_work_li(ul, item, storage.get(prefix, item));
351
    ul.ul.insertBefore(ul.work_li, li);
352
    ul.ul.removeChild(li);
353
    ul.work_li.classList.remove("hide");
354
    list_set_scrollbar(ul.ul);
355

    
356
    ul.state = UL_STATE.EDITING_ENTRY;
357
    ul.edited_item = item;
358
}
359

    
360
const file_downloader = by_id("file_downloader");
361

    
362
function recursively_export_item(prefix, name, added_items, items_data)
363
{
364
    let key = prefix + name;
365

    
366
    if (added_items.has(key))
367
	return;
368

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

    
375
    if (prefix !== TYPE_PREFIX.SCRIPT) {
376
	let components = prefix === TYPE_PREFIX.BAG ?
377
	    data : [data.components];
378

    
379
	for (let [comp_prefix, comp_name] of components) {
380
	    recursively_export_item(comp_prefix, comp_name,
381
				    added_items, items_data);
382
	}
383
    }
384

    
385
    items_data.push({[key]: data});
386
    added_items.add(key);
387
}
388

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

    
404
function add_new_item(prefix, name)
405
{
406
    cancel_work(prefix);
407

    
408
    let ul = ul_by_prefix[prefix];
409
    ul.reset_work_li(ul);
410
    ul.work_li.classList.remove("hide");
411
    ul.ul.appendChild(ul.work_li);
412
    list_set_scrollbar(ul.ul);
413

    
414
    if (name !== undefined)
415
	ul.work_name_input.value = name;
416
    ul.state = UL_STATE.ADDING_ENTRY;
417
}
418

    
419
const chbx_components_window = by_id("chbx_components_window");
420

    
421
function bag_components()
422
{
423
    chbx_components_window.classList.remove("hide");
424
    radio_components_window.classList.add("hide");
425

    
426
    for (let li of chbx_components_ul.children) {
427
	let chbx = li.firstElementChild.firstElementChild;
428
	chbx.checked = false;
429
    }
430
}
431

    
432
function commit_bag_components()
433
{
434
    let selected = [];
435

    
436
    for (let li of chbx_components_ul.children) {
437
	let chbx = li.firstElementChild.firstElementChild;
438
	if (!chbx.checked)
439
	    continue;
440

    
441
	selected.push([li.getAttribute("data-prefix"),
442
		       li.getAttribute("data-name")]);
443
    }
444

    
445
    add_bag_components(selected);
446
    cancel_components();
447
}
448

    
449
const radio_components_window = by_id("radio_components_window");
450
var radio_component_none_input = by_id("radio_component_none_input");
451

    
452
function page_components()
453
{
454
    radio_components_window.classList.remove("hide");
455
    chbx_components_window.classList.add("hide");
456

    
457
    radio_component_none_input.checked = true;
458

    
459
    let components = work_page_li_components();
460
    if (components === undefined)
461
	return;
462

    
463
    let [prefix, item] = components;
464
    let li = by_id(radio_li_id(prefix, item));
465

    
466
    if (li === null)
467
	radio_component_none_input.checked = false;
468
    else
469
	li.firstElementChild.firstElementChild.checked = true;
470
}
471

    
472
function commit_page_components()
473
{
474
    let components = null;
475

    
476
    for (let li of radio_components_ul.children) {
477
	let radio = li.firstElementChild.firstElementChild;
478
	if (!radio.checked)
479
	    continue;
480

    
481
	components = [li.getAttribute("data-prefix"),
482
		      li.getAttribute("data-name")];
483

    
484
	if (radio.id === "radio_component_none_input")
485
	    components = undefined;
486

    
487
	break;
488
    }
489

    
490
    if (components !== null)
491
	set_page_components(components);
492
    cancel_components();
493
}
494

    
495
function cancel_components()
496
{
497
    chbx_components_window.classList.add("hide");
498
    radio_components_window.classList.add("hide");
499
}
500

    
501
const UL_STATE = {
502
    EDITING_ENTRY : 0,
503
    ADDING_ENTRY : 1,
504
    IDLE : 2
505
};
506

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

    
550
/*
551
 * Newer browsers could utilise `text' method of File objects.
552
 * Older ones require FileReader.
553
 */
554

    
555
function _read_file(file, resolve, reject)
556
{
557
    let reader = new FileReader();
558

    
559
    reader.onload = () => resolve(reader.result);
560
    reader.onerror = () => reject(reader.error);
561
    reader.readAsText(file);
562
}
563

    
564
function read_file(file)
565
{
566
    return new Promise((resolve, reject) =>
567
		       _read_file(file, resolve, reject));
568
}
569

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

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

    
592
const import_window = by_id("import_window");
593
let import_frame;
594

    
595
async function import_from_file(event)
596
{
597
    let files = event.target.files;
598
    if (files.length < 1)
599
	return;
600

    
601
    import_window.classList.remove("hide");
602
    import_frame.show_loading();
603

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

    
612
    import_frame.show_selection(result);
613
}
614

    
615
const file_opener_form = by_id("file_opener_form");
616

    
617
function hide_import_window()
618
{
619
    import_window.classList.add("hide");
620

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

    
628
async function initialize_import_facility()
629
{
630
    let import_but = by_id("import_but");
631
    let file_opener = by_id("file_opener");
632

    
633
    import_but.addEventListener("click", () => file_opener.click());
634
    file_opener.addEventListener("change", import_from_file);
635

    
636
    import_frame = await get_import_frame();
637
    import_frame.onclose = hide_import_window;
638
    import_frame.style_table("has_bottom_line", "always_scrollbar",
639
			     "has_upper_line", "tight_table");
640
}
641

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

    
661
    const prefix = target.substring(0, 1);
662

    
663
    if (!list_prefixes.includes(prefix)) {
664
	history.replaceState(null, "", base_url);
665
	return;
666
    }
667

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

    
672
async function main()
673
{
674
    init_default_policy_dialog();
675

    
676
    storage = await get_remote_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 ([TYPE_PREFIX.REPO, TYPE_PREFIX.SCRIPT].includes(prefix))
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
    jump_to_item(document.URL);
712

    
713
    storage.add_change_listener(handle_change);
714

    
715
    await initialize_import_facility();
716
}
717

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

    
725
	return;
726
    }
727

    
728
    if (change.new_val !== undefined)
729
	return;
730

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

    
738
    let uls_creators = [[ul.ul, item_li_id]];
739

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

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

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