Project

General

Profile

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

haketilo / html / options_main.js @ 5c685518

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

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

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

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

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

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

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

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

    
58
    let export_button = remove_button.nextElementSibling;
59
    export_button.addEventListener("click",
60
				   () => export_item(prefix, item));
61
    if (prefix === TYPE_PREFIX.REPO)
62
	export_button.remove();
63

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

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

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

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

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

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

    
90
//TODO: refactor the 2 functions below
91

    
92
function add_chbx_li(prefix, name)
93
{
94
    if (![TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(prefix))
95
	return;
96

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

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

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

    
107
    chbx_components_ul.appendChild(li);
108
}
109

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

    
112
function add_radio_li(prefix, name)
113
{
114
    if (![TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(prefix))
115
	return;
116

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

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

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

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

    
130
/* Used to reset edited repo. */
131
function reset_work_repo_li(ul, item, _)
132
{
133
    ul.work_name_input.value = maybe_string(item);
134
}
135

    
136
/* Used to get repo data for saving */
137
function work_repo_li_data(ul)
138
{
139
    return [ul.work_name_input.value, {}];
140
}
141

    
142
const page_payload_span = by_id("page_payload");
143

    
144
function set_page_components(components)
145
{
146
    if (components === undefined) {
147
	page_payload_span.setAttribute("data-payload", "no");
148
	page_payload_span.textContent = "(None)";
149
    } else {
150
	page_payload_span.setAttribute("data-payload", "yes");
151
	let [prefix, name] = components;
152
	page_payload_span.setAttribute("data-prefix", prefix);
153
	page_payload_span.setAttribute("data-name", name);
154
	page_payload_span.textContent = nice_name(prefix, name);
155
    }
156
}
157

    
158
const page_allow_chbx = by_id("page_allow_chbx");
159

    
160
/* Used to reset edited page. */
161
function reset_work_page_li(ul, item, settings)
162
{
163
    ul.work_name_input.value = maybe_string(item);
164
    settings = settings || {allow: false, components: undefined};
165
    page_allow_chbx.checked = !!settings.allow;
166

    
167
    set_page_components(settings.components);
168
}
169

    
170
function work_page_li_components()
171
{
172
    if (page_payload_span.getAttribute("data-payload") === "no")
173
	return undefined;
174

    
175
    let prefix = page_payload_span.getAttribute("data-prefix");
176
    let name = page_payload_span.getAttribute("data-name");
177
    return [prefix, name];
178
}
179

    
180
/* Used to get edited page data for saving. */
181
function work_page_li_data(ul)
182
{
183
    let url = ul.work_name_input.value;
184
    let settings = {
185
	components : work_page_li_components(),
186
	allow : !!page_allow_chbx.checked
187
    };
188

    
189
    return [url, settings];
190
}
191

    
192
const empty_bag_component_li = by_id("empty_bag_component_li");
193
var bag_components_ul = by_id("bag_components_ul");
194

    
195
/* Used to construct and update components list of edited bag. */
196
function add_bag_components(components)
197
{
198
    for (let component of components) {
199
	let [prefix, name] = component;
200
	let li = bag_component_li_template.cloneNode(true);
201
	li.setAttribute("data-prefix", prefix);
202
	li.setAttribute("data-name", name);
203
	let span = li.firstElementChild;
204
	span.textContent = nice_name(prefix, name);
205
	let remove_but = span.nextElementSibling;
206
	remove_but.addEventListener("click", () =>
207
				    bag_components_ul.removeChild(li));
208
	bag_components_ul.appendChild(li);
209
    }
210

    
211
    bag_components_ul.appendChild(empty_bag_component_li);
212
}
213

    
214
/* Used to reset edited bag. */
215
function reset_work_bag_li(ul, item, components)
216
{
217
    components = components || [];
218

    
219
    ul.work_name_input.value = maybe_string(item);
220
    let old_components_ul = bag_components_ul;
221
    bag_components_ul = old_components_ul.cloneNode(false);
222

    
223
    ul.work_li.insertBefore(bag_components_ul, old_components_ul);
224
    ul.work_li.removeChild(old_components_ul);
225

    
226
    add_bag_components(components);
227
}
228

    
229
/* Used to get edited bag data for saving. */
230
function work_bag_li_data(ul)
231
{
232
    let components_ul = ul.work_name_input.nextElementSibling;
233
    let component_li = components_ul.firstElementChild;
234

    
235
    let components = [];
236

    
237
    /* Last list element is empty li with id set. */
238
    while (component_li.id === '') {
239
	components.push([component_li.getAttribute("data-prefix"),
240
			 component_li.getAttribute("data-name")]);
241
	component_li = component_li.nextElementSibling;
242
    }
243

    
244
    return [ul.work_name_input.value, components];
245
}
246

    
247
const script_url_input = by_id("script_url_field");
248
const script_sha256_input = by_id("script_sha256_field");
249
const script_contents_field = by_id("script_contents_field");
250

    
251
function maybe_string(maybe_defined)
252
{
253
    return maybe_defined === undefined ? "" : maybe_defined + "";
254
}
255

    
256
/* Used to reset edited script. */
257
function reset_work_script_li(ul, name, data)
258
{
259
    ul.work_name_input.value = maybe_string(name);
260
    if (data === undefined)
261
	data = {};
262
    script_url_input.value = maybe_string(data.url);
263
    script_sha256_input.value = maybe_string(data.hash);
264
    script_contents_field.value = maybe_string(data.text);
265
}
266

    
267
/* Used to get edited script data for saving. */
268
function work_script_li_data(ul)
269
{
270
    return [ul.work_name_input.value, {
271
	url : script_url_input.value,
272
	hash : script_sha256_input.value,
273
	text : script_contents_field.value
274
    }];
275
}
276

    
277
function cancel_work(prefix)
278
{
279
    let ul = ul_by_prefix[prefix];
280

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

    
284
    if (ul.state === UL_STATE.EDITING_ENTRY) {
285
	add_li(prefix, ul.edited_item);
286
    }
287

    
288
    ul.work_li.classList.add("hide");
289
    ul.state = UL_STATE.IDLE;
290
}
291

    
292
function save_work(prefix)
293
{
294
    let ul = ul_by_prefix[prefix];
295

    
296
    if (ul.state === UL_STATE.IDLE)
297
	return;
298

    
299
    let [item, data] = ul.get_work_li_data(ul);
300

    
301
    /* Here we fire promises and return without waiting. */
302

    
303
    if (ul.state === UL_STATE.EDITING_ENTRY)
304
	storage.replace(prefix, ul.edited_item, item, data);
305
    if (ul.state === UL_STATE.ADDING_ENTRY)
306
	storage.set(prefix, item, data);
307

    
308
    cancel_work(prefix);
309
}
310

    
311
function edit_item(prefix, item)
312
{
313
    cancel_work(prefix);
314

    
315
    let ul = ul_by_prefix[prefix];
316
    let li = by_id(item_li_id(prefix, item));
317

    
318
    if (li === null) {
319
	add_new_item(prefix, item);
320
	return;
321
    }
322

    
323
    ul.reset_work_li(ul, item, storage.get(prefix, item));
324
    ul.ul.insertBefore(ul.work_li, li);
325
    ul.ul.removeChild(li);
326
    ul.work_li.classList.remove("hide");
327

    
328
    ul.state = UL_STATE.EDITING_ENTRY;
329
    ul.edited_item = item;
330
}
331

    
332
const file_downloader = by_id("file_downloader");
333

    
334
function recursively_export_item(prefix, name, added_items, items_data)
335
{
336
    let key = prefix + name;
337

    
338
    if (added_items.has(key))
339
	return;
340

    
341
    let data = storage.get(prefix, name);
342
    if (data === undefined) {
343
	console.log(`${TYPE_NAME[prefix]} '${name}' for export not found`);
344
	return;
345
    }
346

    
347
    if (prefix !== TYPE_PREFIX.SCRIPT) {
348
	let components = prefix === TYPE_PREFIX.BAG ?
349
	    data : [data.components];
350

    
351
	for (let [comp_prefix, comp_name] of components) {
352
	    recursively_export_item(comp_prefix, comp_name,
353
				    added_items, items_data);
354
	}
355
    }
356

    
357
    items_data.push({[key]: data});
358
    added_items.add(key);
359
}
360

    
361
function export_item(prefix, name)
362
{
363
    let added_items = new Set();
364
    let items_data = [];
365
    recursively_export_item(prefix, name, added_items, items_data);
366
    let file = new Blob([JSON.stringify(items_data)],
367
			{type: "application/json"});
368
    let url = URL.createObjectURL(file);
369
    file_downloader.setAttribute("href", url);
370
    file_downloader.setAttribute("download", prefix + name + ".json");
371
    file_downloader.click();
372
    file_downloader.removeAttribute("href");
373
    URL.revokeObjectURL(url);
374
}
375

    
376
function add_new_item(prefix, name)
377
{
378
    cancel_work(prefix);
379

    
380
    let ul = ul_by_prefix[prefix];
381
    ul.reset_work_li(ul);
382
    ul.work_li.classList.remove("hide");
383
    ul.ul.appendChild(ul.work_li);
384

    
385
    if (name !== undefined)
386
	ul.work_name_input.value = name;
387
    ul.state = UL_STATE.ADDING_ENTRY;
388
}
389

    
390
const chbx_components_window = by_id("chbx_components_window");
391

    
392
function bag_components()
393
{
394
    chbx_components_window.classList.remove("hide");
395
    radio_components_window.classList.add("hide");
396

    
397
    for (let li of chbx_components_ul.children) {
398
	let chbx = li.firstElementChild;
399
	chbx.checked = false;
400
    }
401
}
402

    
403
function commit_bag_components()
404
{
405
    let selected = [];
406

    
407
    for (let li of chbx_components_ul.children) {
408
	let chbx = li.firstElementChild;
409
	if (!chbx.checked)
410
	    continue;
411

    
412
	selected.push([li.getAttribute("data-prefix"),
413
		       li.getAttribute("data-name")]);
414
    }
415

    
416
    add_bag_components(selected);
417
    cancel_components();
418
}
419

    
420
const radio_components_window = by_id("radio_components_window");
421
var radio_component_none_input = by_id("radio_component_none_input");
422

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

    
428
    radio_component_none_input.checked = true;
429

    
430
    let components = work_page_li_components();
431
    if (components === undefined)
432
	return;
433

    
434
    let [prefix, item] = components;
435
    let li = by_id(radio_li_id(prefix, item));
436
    if (li === null)
437
	radio_component_none_input.checked = false;
438
    else
439
	li.firstElementChild.checked = true;
440
}
441

    
442
function commit_page_components()
443
{
444
    let components = null;
445

    
446
    for (let li of radio_components_ul.children) {
447
	let radio = li.firstElementChild;
448
	if (!radio.checked)
449
	    continue;
450

    
451
	components = [li.getAttribute("data-prefix"),
452
		      li.getAttribute("data-name")];
453

    
454
	if (radio.id === "radio_component_none_input")
455
	    components = undefined;
456

    
457
	break;
458
    }
459

    
460
    if (components !== null)
461
	set_page_components(components);
462
    cancel_components();
463
}
464

    
465
function cancel_components()
466
{
467
    chbx_components_window.classList.add("hide");
468
    radio_components_window.classList.add("hide");
469
}
470

    
471
const UL_STATE = {
472
    EDITING_ENTRY : 0,
473
    ADDING_ENTRY : 1,
474
    IDLE : 2
475
};
476

    
477
const ul_by_prefix = {
478
    [TYPE_PREFIX.REPO] : {
479
	ul : by_id("repos_ul"),
480
	work_li : by_id("work_repo_li"),
481
	work_name_input : by_id("repo_url_field"),
482
	reset_work_li : reset_work_repo_li,
483
	get_work_li_data : work_repo_li_data,
484
	state : UL_STATE.IDLE,
485
	edited_item : undefined,
486
    },
487
    [TYPE_PREFIX.PAGE] : {
488
	ul : by_id("pages_ul"),
489
	work_li : by_id("work_page_li"),
490
	work_name_input : by_id("page_url_field"),
491
	reset_work_li : reset_work_page_li,
492
	get_work_li_data : work_page_li_data,
493
	select_components : page_components,
494
	commit_components : commit_page_components,
495
	state : UL_STATE.IDLE,
496
	edited_item : undefined,
497
    },
498
    [TYPE_PREFIX.BAG] : {
499
	ul : by_id("bags_ul"),
500
	work_li : by_id("work_bag_li"),
501
	work_name_input : by_id("bag_name_field"),
502
	reset_work_li : reset_work_bag_li,
503
	get_work_li_data : work_bag_li_data,
504
	select_components : bag_components,
505
	commit_components : commit_bag_components,
506
	state : UL_STATE.IDLE,
507
	edited_item : undefined,
508
    },
509
    [TYPE_PREFIX.SCRIPT] : {
510
	ul : by_id("scripts_ul"),
511
	work_li : by_id("work_script_li"),
512
	work_name_input : by_id("script_name_field"),
513
	reset_work_li : reset_work_script_li,
514
	get_work_li_data : work_script_li_data,
515
	state : UL_STATE.IDLE,
516
	edited_item : undefined,
517
    }
518
}
519

    
520
const import_window = by_id("import_window");
521
const import_loading_radio = by_id("import_loading_radio");
522
const import_failed_radio = by_id("import_failed_radio");
523
const import_selection_radio = by_id("import_selection_radio");
524
const bad_file_errormsg = by_id("bad_file_errormsg");
525

    
526
/*
527
 * Newer browsers could utilise `text' method of File objects.
528
 * Older ones require FileReader.
529
 */
530

    
531
function _read_file(file, resolve, reject)
532
{
533
    let reader = new FileReader();
534

    
535
    reader.onload = () => resolve(reader.result);
536
    reader.onerror = () => reject(reader.error);
537
    reader.readAsText(file);
538
}
539

    
540
function read_file(file)
541
{
542
    return new Promise((resolve, reject) =>
543
		       _read_file(file, resolve, reject));
544
}
545

    
546
async function import_from_file(event)
547
{
548
    let files = event.target.files;
549
    if (files.length < 1)
550
	return;
551

    
552
    import_window.classList.remove("hide");
553
    import_loading_radio.checked = true;
554

    
555
    let result = undefined;
556

    
557
    try {
558
	result = JSON.parse(await read_file(files[0]));
559
    } catch(e) {
560
	bad_file_errormsg.textContent = "" + e;
561
	import_failed_radio.checked = true;
562
	return;
563
    }
564

    
565
    let errormsg = validate_settings(result);
566
    if (errormsg !== false) {
567
	bad_file_errormsg.textContent = errormsg;
568
	import_failed_radio.checked = true;
569
	return;
570
    }
571

    
572
    populate_import_list(result);
573
    import_selection_radio.checked = true;
574
}
575

    
576
function validate_settings(settings)
577
{
578
    // TODO
579
    return false;
580
}
581

    
582
function import_li_id(prefix, item)
583
{
584
    return `ili_${prefix}_${item}`;
585
}
586

    
587
let import_ul = by_id("import_ul");
588
let import_chbxs_colliding = undefined;
589
let settings_import_map = undefined;
590

    
591
function populate_import_list(settings)
592
{
593
    let old_children = import_ul.children;
594
    while (old_children[0] !== undefined)
595
	import_ul.removeChild(old_children[0]);
596

    
597
    import_chbxs_colliding = [];
598
    settings_import_map = new Map();
599

    
600
    for (let setting of settings) {
601
	let [key, value] = Object.entries(setting)[0];
602
	let prefix = key[0];
603
	let name = key.substring(1);
604
	add_import_li(prefix, name);
605
	settings_import_map.set(key, value);
606
    }
607
}
608

    
609
function add_import_li(prefix, name)
610
{
611
    let li = import_li_template.cloneNode(true);
612
    let name_span = li.firstElementChild;
613
    let chbx = name_span.nextElementSibling;
614
    let warning_span = chbx.nextElementSibling;
615

    
616
    li.setAttribute("data-prefix", prefix);
617
    li.setAttribute("data-name", name);
618
    li.id = import_li_id(prefix, name);
619
    name_span.textContent = nice_name(prefix, name);
620

    
621
    if (storage.get(prefix, name) !== undefined) {
622
	import_chbxs_colliding.push(chbx);
623
	warning_span.textContent = "(will overwrite existing setting!)";
624
    }
625

    
626
    import_ul.appendChild(li);
627
}
628

    
629
function check_all_imports()
630
{
631
    for (let li of import_ul.children)
632
	li.firstElementChild.nextElementSibling.checked = true;
633
}
634

    
635
function uncheck_all_imports()
636
{
637
    for (let li of import_ul.children)
638
	li.firstElementChild.nextElementSibling.checked = false;
639
}
640

    
641
function uncheck_colliding_imports()
642
{
643
    for (let chbx of import_chbxs_colliding)
644
	chbx.checked = false;
645
}
646

    
647
const file_opener_form = by_id("file_opener_form");
648

    
649
function hide_import_window()
650
{
651
    import_window.classList.add("hide");
652
    /* Let GC free some memory */
653
    import_chbxs_colliding = undefined;
654
    settings_import_map = undefined;
655

    
656
    /*
657
     * Reset file <input>. Without this, a second attempt to import the same
658
     * file would result in "change" event not happening on <input> element.
659
     */
660
    file_opener_form.reset();
661
}
662

    
663
function commit_import()
664
{
665
    for (let li of import_ul.children) {
666
	let chbx = li.firstElementChild.nextElementSibling;
667

    
668
	if (!chbx.checked)
669
	    continue;
670

    
671
	let prefix = li.getAttribute("data-prefix");
672
	let name = li.getAttribute("data-name");
673
	let key = prefix + name;
674
	let value = settings_import_map.get(key);
675
	storage.set(prefix, name, value);
676
    }
677

    
678
    hide_import_window();
679
}
680

    
681
function initialize_import_facility()
682
{
683
    let import_but = by_id("import_but");
684
    let file_opener = by_id("file_opener");
685
    let import_failok_but = by_id("import_failok_but");
686
    let check_all_import_but = by_id("check_all_import_but");
687
    let uncheck_all_import_but = by_id("uncheck_all_import_but");
688
    let uncheck_existing_import_but = by_id("uncheck_existing_import_but");
689
    let commit_import_but = by_id("commit_import_but");
690
    let cancel_import_but = by_id("cancel_import_but");
691
    import_but.addEventListener("click", () => file_opener.click());
692
    file_opener.addEventListener("change", import_from_file);
693
    import_failok_but.addEventListener("click", hide_import_window);
694
    check_all_import_but.addEventListener("click", check_all_imports);
695
    uncheck_all_import_but.addEventListener("click", uncheck_all_imports);
696
    uncheck_colliding_import_but
697
	.addEventListener("click", uncheck_colliding_imports);
698
    commit_import_but.addEventListener("click", commit_import);
699
    cancel_import_but.addEventListener("click", hide_import_window);
700
}
701

    
702
/*
703
 * If url has a target appended, e.g.
704
 * chrome-extension://hnhmbnpohhlmhehionjgongbnfdnabdl/html/options.html#smyhax
705
 * that target will be split into prefix and item name (e.g. "s" and "myhax")
706
 * and editing of that respective item will be started.
707
 *
708
 * We don't need to worry about the state of the page (e.g. some editing being
709
 * in progress) in jump_to_item() - this function is called at the beginning,
710
 * together with callbacks being assigned to buttons, so it is safe to assume
711
 * lists are initialized with items and page is in its virgin state with regard
712
 * to everything else.
713
 */
714
function jump_to_item(url_with_item)
715
{
716
    const [dummy1, base_url, dummy2, target] =
717
	  /^([^#]*)(#(.*))?$/i.exec(url_with_item);
718
    if (target === undefined)
719
	return;
720

    
721
    const prefix = target.substring(0, 1);
722

    
723
    if (!list_prefixes.includes(prefix)) {
724
	history.replaceState(null, "", base_url);
725
	return;
726
    }
727

    
728
    by_id(`show_${TYPE_NAME[prefix]}s`).checked = true;
729
    edit_item(prefix, decodeURIComponent(target.substring(1)));
730
}
731

    
732
async function main()
733
{
734
    storage = await get_remote_storage();
735

    
736
    for (let prefix of list_prefixes) {
737
	for (let item of storage.get_all_names(prefix).sort()) {
738
	    add_li(prefix, item, true);
739
	    add_chbx_li(prefix, item);
740
	    add_radio_li(prefix, item);
741
	}
742

    
743
	let name = TYPE_NAME[prefix];
744

    
745
	let add_but = by_id(`add_${name}_but`);
746
	let discard_but = by_id(`discard_${name}_but`);
747
	let save_but = by_id(`save_${name}_but`);
748

    
749
	add_but.addEventListener("click", () => add_new_item(prefix));
750
	discard_but.addEventListener("click", () => cancel_work(prefix));
751
	save_but.addEventListener("click", () => save_work(prefix));
752

    
753
	if ([TYPE_PREFIX.REPO, TYPE_PREFIX.SCRIPT].includes(prefix))
754
	    continue;
755

    
756
	let ul = ul_by_prefix[prefix];
757

    
758
	let commit_components_but = by_id(`commit_${name}_components_but`);
759
	let cancel_components_but = by_id(`cancel_${name}_components_but`);
760
	let select_components_but = by_id(`select_${name}_components_but`);
761

    
762
	commit_components_but
763
	    .addEventListener("click", ul.commit_components);
764
	select_components_but
765
	    .addEventListener("click", ul.select_components);
766
	cancel_components_but.addEventListener("click", cancel_components);
767
    }
768

    
769
    jump_to_item(document.URL);
770

    
771
    initialize_import_facility();
772

    
773
    storage.add_change_listener(handle_change);
774
}
775

    
776
function handle_change(change)
777
{
778
    if (change.old_val === undefined) {
779
	add_li(change.prefix, change.item);
780
	add_chbx_li(change.prefix, change.item);
781
	add_radio_li(change.prefix, change.item);
782

    
783
	return;
784
    }
785

    
786
    if (change.new_val !== undefined)
787
	return;
788

    
789
    let ul = ul_by_prefix[change.prefix];
790
    if (ul.state === UL_STATE.EDITING_ENTRY &&
791
	ul.edited_item === change.item) {
792
	ul.state = UL_STATE.ADDING_ENTRY;
793
	return;
794
    }
795

    
796
    let uls_creators = [[ul.ul, item_li_id]];
797

    
798
    if ([TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(change.prefix)) {
799
	uls_creators.push([chbx_components_ul, chbx_li_id]);
800
	uls_creators.push([radio_components_ul, radio_li_id]);
801
    }
802

    
803
    for (let [components_ul, id_creator] of uls_creators) {
804
	let li = by_id(id_creator(change.prefix, change.item));
805
	components_ul.removeChild(li);
806
    }
807
}
808

    
809
main();
(4-4/4)