Project

General

Profile

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

haketilo / html / options_main.js @ 2fa41a54

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

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

    
25
const item_li_template = by_id("item_li_template");
26
const bag_component_li_template = by_id("bag_component_li_template");
27
const chbx_component_li_template = by_id("chbx_component_li_template");
28
const radio_component_li_template = by_id("radio_component_li_template");
29
const import_li_template = by_id("import_li_template");
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
import_li_template.removeAttribute("id");
36

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

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

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

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

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

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

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

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

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

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

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

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

    
91
//TODO: refactor the 2 functions below
92

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

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

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

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

    
108
    chbx_components_ul.appendChild(li);
109
}
110

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

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

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

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

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

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

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

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

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

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

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

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

    
168
    set_page_components(settings.components);
169
}
170

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

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

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

    
190
    return [url, settings];
191
}
192

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

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

    
212
    bag_components_ul.appendChild(empty_bag_component_li);
213
}
214

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

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

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

    
227
    console.log("bag components", components);
228
    add_bag_components(components);
229
}
230

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

    
237
    let components = [];
238

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

    
246
    return [ul.work_name_input.value, components];
247
}
248

    
249
const script_url_input = by_id("script_url_field");
250
const script_sha256_input = by_id("script_sha256_field");
251
const script_contents_field = by_id("script_contents_field");
252

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

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

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

    
279
function cancel_work(prefix)
280
{
281
    let ul = ul_by_prefix[prefix];
282

    
283
    if (ul.state === UL_STATE.IDLE)
284
	return;
285

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

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

    
294
function save_work(prefix)
295
{
296
    let ul = ul_by_prefix[prefix];
297

    
298
    if (ul.state === UL_STATE.IDLE)
299
	return;
300

    
301
    let [item, data] = ul.get_work_li_data(ul);
302

    
303
    /* Here we fire promises and return without waiting. */
304

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

    
310
    cancel_work(prefix);
311
}
312

    
313
function edit_item(prefix, item)
314
{
315
    cancel_work(prefix);
316

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

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

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

    
330
    ul.state = UL_STATE.EDITING_ENTRY;
331
    ul.edited_item = item;
332
}
333

    
334
const file_downloader = by_id("file_downloader");
335

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

    
340
    if (added_items.has(key))
341
	return;
342

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

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

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

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

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

    
378
function add_new_item(prefix, name)
379
{
380
    cancel_work(prefix);
381

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

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

    
392
const chbx_components_window = by_id("chbx_components_window");
393

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

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

    
405
function commit_bag_components()
406
{
407
    let selected = [];
408

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

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

    
418
    add_bag_components(selected);
419
    cancel_components();
420
}
421

    
422
const radio_components_window = by_id("radio_components_window");
423
var radio_component_none_input = by_id("radio_component_none_input");
424

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

    
430
    radio_component_none_input.checked = true;
431

    
432
    let components = work_page_li_components();
433
    if (components === undefined)
434
	return;
435

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

    
444
function commit_page_components()
445
{
446
    let components = null;
447

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

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

    
456
	if (radio.id === "radio_component_none_input")
457
	    components = undefined;
458

    
459
	break;
460
    }
461

    
462
    if (components !== null)
463
	set_page_components(components);
464
    cancel_components();
465
}
466

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

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

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

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

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

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

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

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

    
548
const url_regex = /^[a-z0-9]+:\/\/[^/]+\.[^/]{2}(\/[^?#]*)?$/;
549
const sha256_regex = /^[0-9a-f]{64}$/;
550
const component_schema = [
551
    new RegExp(`^[${TYPE_PREFIX.SCRIPT}${TYPE_PREFIX.BAG}]$`),
552
    /.+/
553
];
554

    
555
const settings_schema = [
556
    [{}, "matchentry", "minentries", 1,
557
     new RegExp(`^${TYPE_PREFIX.SCRIPT}`), {
558
	 /* script data */
559
	 "url":    ["optional", url_regex],
560
	 "sha256": ["optional", sha256_regex],
561
	 "text":   ["optional", "string"]
562
     },
563
     new RegExp(`^${TYPE_PREFIX.BAG}`), [
564
	 "optional",
565
	 [component_schema, "repeat"],
566
	 "default", undefined
567
     ],
568
     new RegExp(`^${TYPE_PREFIX.PAGE}`), {
569
	 /* page data */
570
	 "components": ["optional", component_schema]
571
     }], "repeat"
572
]
573

    
574
async function import_from_file(event)
575
{
576
    let files = event.target.files;
577
    if (files.length < 1)
578
	return;
579

    
580
    import_window.classList.remove("hide");
581
    import_loading_radio.checked = true;
582

    
583
    let result = undefined;
584

    
585
    try {
586
	result = parse_json_with_schema(settings_schema,
587
					await read_file(files[0]));
588
    } catch(e) {
589
	bad_file_errormsg.textContent = "" + e;
590
	import_failed_radio.checked = true;
591
	return;
592
    }
593

    
594
    populate_import_list(result);
595
    import_selection_radio.checked = true;
596
}
597

    
598
function import_li_id(prefix, item)
599
{
600
    return `ili_${prefix}_${item}`;
601
}
602

    
603
let import_ul = by_id("import_ul");
604
let import_chbxs_colliding = undefined;
605
let settings_import_map = undefined;
606

    
607
function populate_import_list(settings)
608
{
609
    let old_children = import_ul.children;
610
    while (old_children[0] !== undefined)
611
	import_ul.removeChild(old_children[0]);
612

    
613
    import_chbxs_colliding = [];
614
    settings_import_map = new Map();
615

    
616
    for (let setting of settings) {
617
	let [key, value] = Object.entries(setting)[0];
618
	let prefix = key[0];
619
	let name = key.substring(1);
620
	add_import_li(prefix, name);
621
	settings_import_map.set(key, value);
622
    }
623
}
624

    
625
function add_import_li(prefix, name)
626
{
627
    let li = import_li_template.cloneNode(true);
628
    let name_span = li.firstElementChild;
629
    let chbx = name_span.nextElementSibling;
630
    let warning_span = chbx.nextElementSibling;
631

    
632
    li.setAttribute("data-prefix", prefix);
633
    li.setAttribute("data-name", name);
634
    li.id = import_li_id(prefix, name);
635
    name_span.textContent = nice_name(prefix, name);
636

    
637
    if (storage.get(prefix, name) !== undefined) {
638
	import_chbxs_colliding.push(chbx);
639
	warning_span.textContent = "(will overwrite existing setting!)";
640
    }
641

    
642
    import_ul.appendChild(li);
643
}
644

    
645
function check_all_imports()
646
{
647
    for (let li of import_ul.children)
648
	li.firstElementChild.nextElementSibling.checked = true;
649
}
650

    
651
function uncheck_all_imports()
652
{
653
    for (let li of import_ul.children)
654
	li.firstElementChild.nextElementSibling.checked = false;
655
}
656

    
657
function uncheck_colliding_imports()
658
{
659
    for (let chbx of import_chbxs_colliding)
660
	chbx.checked = false;
661
}
662

    
663
const file_opener_form = by_id("file_opener_form");
664

    
665
function hide_import_window()
666
{
667
    import_window.classList.add("hide");
668
    /* Let GC free some memory */
669
    import_chbxs_colliding = undefined;
670
    settings_import_map = undefined;
671

    
672
    /*
673
     * Reset file <input>. Without this, a second attempt to import the same
674
     * file would result in "change" event not happening on <input> element.
675
     */
676
    file_opener_form.reset();
677
}
678

    
679
function commit_import()
680
{
681
    for (let li of import_ul.children) {
682
	let chbx = li.firstElementChild.nextElementSibling;
683

    
684
	if (!chbx.checked)
685
	    continue;
686

    
687
	let prefix = li.getAttribute("data-prefix");
688
	let name = li.getAttribute("data-name");
689
	let key = prefix + name;
690
	let value = settings_import_map.get(key);
691
	storage.set(prefix, name, value);
692
    }
693

    
694
    hide_import_window();
695
}
696

    
697
function initialize_import_facility()
698
{
699
    let import_but = by_id("import_but");
700
    let file_opener = by_id("file_opener");
701
    let import_failok_but = by_id("import_failok_but");
702
    let check_all_import_but = by_id("check_all_import_but");
703
    let uncheck_all_import_but = by_id("uncheck_all_import_but");
704
    let uncheck_existing_import_but = by_id("uncheck_existing_import_but");
705
    let commit_import_but = by_id("commit_import_but");
706
    let cancel_import_but = by_id("cancel_import_but");
707
    import_but.addEventListener("click", () => file_opener.click());
708
    file_opener.addEventListener("change", import_from_file);
709
    import_failok_but.addEventListener("click", hide_import_window);
710
    check_all_import_but.addEventListener("click", check_all_imports);
711
    uncheck_all_import_but.addEventListener("click", uncheck_all_imports);
712
    uncheck_colliding_import_but
713
	.addEventListener("click", uncheck_colliding_imports);
714
    commit_import_but.addEventListener("click", commit_import);
715
    cancel_import_but.addEventListener("click", hide_import_window);
716
}
717

    
718
/*
719
 * If url has a target appended, e.g.
720
 * chrome-extension://hnhmbnpohhlmhehionjgongbnfdnabdl/html/options.html#smyhax
721
 * that target will be split into prefix and item name (e.g. "s" and "myhax")
722
 * and editing of that respective item will be started.
723
 *
724
 * We don't need to worry about the state of the page (e.g. some editing being
725
 * in progress) in jump_to_item() - this function is called at the beginning,
726
 * together with callbacks being assigned to buttons, so it is safe to assume
727
 * lists are initialized with items and page is in its virgin state with regard
728
 * to everything else.
729
 */
730
function jump_to_item(url_with_item)
731
{
732
    const [dummy1, base_url, dummy2, target] =
733
	  /^([^#]*)(#(.*))?$/i.exec(url_with_item);
734
    if (target === undefined)
735
	return;
736

    
737
    const prefix = target.substring(0, 1);
738

    
739
    if (!list_prefixes.includes(prefix)) {
740
	history.replaceState(null, "", base_url);
741
	return;
742
    }
743

    
744
    by_id(`show_${TYPE_NAME[prefix]}s`).checked = true;
745
    edit_item(prefix, decodeURIComponent(target.substring(1)));
746
}
747

    
748
async function main()
749
{
750
    storage = await get_remote_storage();
751

    
752
    for (let prefix of list_prefixes) {
753
	for (let item of storage.get_all_names(prefix).sort()) {
754
	    add_li(prefix, item, true);
755
	    add_chbx_li(prefix, item);
756
	    add_radio_li(prefix, item);
757
	}
758

    
759
	let name = TYPE_NAME[prefix];
760

    
761
	let add_but = by_id(`add_${name}_but`);
762
	let discard_but = by_id(`discard_${name}_but`);
763
	let save_but = by_id(`save_${name}_but`);
764

    
765
	add_but.addEventListener("click", () => add_new_item(prefix));
766
	discard_but.addEventListener("click", () => cancel_work(prefix));
767
	save_but.addEventListener("click", () => save_work(prefix));
768

    
769
	if ([TYPE_PREFIX.REPO, TYPE_PREFIX.SCRIPT].includes(prefix))
770
	    continue;
771

    
772
	let ul = ul_by_prefix[prefix];
773

    
774
	let commit_components_but = by_id(`commit_${name}_components_but`);
775
	let cancel_components_but = by_id(`cancel_${name}_components_but`);
776
	let select_components_but = by_id(`select_${name}_components_but`);
777

    
778
	commit_components_but
779
	    .addEventListener("click", ul.commit_components);
780
	select_components_but
781
	    .addEventListener("click", ul.select_components);
782
	cancel_components_but.addEventListener("click", cancel_components);
783
    }
784

    
785
    jump_to_item(document.URL);
786

    
787
    initialize_import_facility();
788

    
789
    storage.add_change_listener(handle_change);
790
}
791

    
792
function handle_change(change)
793
{
794
    if (change.old_val === undefined) {
795
	add_li(change.prefix, change.item);
796
	add_chbx_li(change.prefix, change.item);
797
	add_radio_li(change.prefix, change.item);
798

    
799
	return;
800
    }
801

    
802
    if (change.new_val !== undefined)
803
	return;
804

    
805
    let ul = ul_by_prefix[change.prefix];
806
    if (ul.state === UL_STATE.EDITING_ENTRY &&
807
	ul.edited_item === change.item) {
808
	ul.state = UL_STATE.ADDING_ENTRY;
809
	return;
810
    }
811

    
812
    let uls_creators = [[ul.ul, item_li_id]];
813

    
814
    if ([TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(change.prefix)) {
815
	uls_creators.push([chbx_components_ul, chbx_li_id]);
816
	uls_creators.push([radio_components_ul, radio_li_id]);
817
    }
818

    
819
    for (let [components_ul, id_creator] of uls_creators) {
820
	let li = by_id(id_creator(change.prefix, change.item));
821
	components_ul.removeChild(li);
822
    }
823
}
824

    
825
main();
(4-4/4)