Project

General

Profile

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

haketilo / html / options_main.js @ 263d03d5

1
/**
2
 * This file is part of Haketilo.
3
 *
4
 * Function: Settings page logic.
5
 *
6
 * Copyright (C) 2021 Wojtek Kosior
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * As additional permission under GNU GPL version 3 section 7, you
19
 * may distribute forms of that code without the copy of the GNU
20
 * GPL normally required by section 4, provided you include this
21
 * license notice and, in case of non-source distribution, a URL
22
 * through which recipients can access the Corresponding Source.
23
 * If you modify file(s) with this exception, you may extend this
24
 * exception to your version of the file(s), but you are not
25
 * obligated to do so. If you do not wish to do so, delete this
26
 * exception statement from your version.
27
 *
28
 * As a special exception to the GPL, any HTML file which merely
29
 * makes function calls to this code, and for that purpose
30
 * includes it by reference shall be deemed a separate work for
31
 * copyright law purposes. If you modify this code, you may extend
32
 * this exception to your version of the code, but you are not
33
 * obligated to do so. If you do not wish to do so, delete this
34
 * exception statement from your version.
35
 *
36
 * You should have received a copy of the GNU General Public License
37
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
38
 *
39
 * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
40
 * license. Although I request that you do not make use this code in a
41
 * proprietary program, I am not going to enforce this in court.
42
 */
43

    
44
/*
45
 * IMPORTS_START
46
 * IMPORT get_remote_storage
47
 * IMPORT TYPE_PREFIX
48
 * IMPORT TYPE_NAME
49
 * IMPORT list_prefixes
50
 * IMPORT nice_name
51
 * IMPORT parse_json_with_schema
52
 * IMPORT get_template
53
 * IMPORT by_id
54
 * IMPORT matchers
55
 * IMPORT get_import_frame
56
 * IMPORT init_default_policy_dialog
57
 * IMPORTS_END
58
 */
59

    
60
var storage;
61

    
62
const item_li_template = get_template("item_li");
63
const bag_component_li_template = get_template("bag_component_li");
64
const chbx_component_li_template = get_template("chbx_component_li");
65
const radio_component_li_template = get_template("radio_component_li");
66
/* Make sure they are later cloned without id. */
67
item_li_template.removeAttribute("id");
68
bag_component_li_template.removeAttribute("id");
69
chbx_component_li_template.removeAttribute("id");
70
radio_component_li_template.removeAttribute("id");
71

    
72
function list_set_scrollbar(list_elem)
73
{
74
    const op = ((list_elem.children.length === 1 &&
75
		 list_elem.children[0].classList.contains("hide")) ||
76
		list_elem.children.length < 1) ? "remove" : "add";
77
    while (!list_elem.classList.contains("table_wrapper"))
78
	list_elem = list_elem.parentElement;
79
    list_elem.classList[op]("always_scrollbar");
80
}
81

    
82
function item_li_id(prefix, item)
83
{
84
    return `li_${prefix}_${item}`;
85
}
86

    
87
/* Insert into list of bags/pages/scripts/repos */
88
function add_li(prefix, item, at_the_end=false)
89
{
90
    let ul = ul_by_prefix[prefix];
91
    let li = item_li_template.cloneNode(true);
92
    li.id = item_li_id(prefix, item);
93

    
94
    let span = li.firstElementChild;
95
    span.textContent = item;
96

    
97
    let edit_button = span.nextElementSibling;
98
    edit_button.addEventListener("click", () => edit_item(prefix, item));
99

    
100
    let remove_button = edit_button.nextElementSibling;
101
    remove_button.addEventListener("click",
102
				   () => storage.remove(prefix, item));
103

    
104
    let export_button = remove_button.nextElementSibling;
105
    export_button.addEventListener("click",
106
				   () => export_item(prefix, item));
107
    if (prefix === TYPE_PREFIX.REPO)
108
	export_button.remove();
109

    
110
    if (!at_the_end) {
111
	for (let element of ul.ul.children) {
112
	    if (element.id < li.id || element.id.startsWith("work_"))
113
		continue;
114

    
115
	    ul.ul.insertBefore(li, element);
116
	    break;
117
	}
118
    }
119
    if (!li.parentElement) {
120
	if (ul.work_li !== ul.ul.lastElementChild)
121
	    ul.ul.appendChild(li);
122
	else
123
	    ul.work_li.before(li);
124
    }
125

    
126
    list_set_scrollbar(ul.ul);
127
}
128

    
129
const chbx_components_ul = by_id("chbx_components_ul");
130
const radio_components_ul = by_id("radio_components_ul");
131

    
132
function chbx_li_id(prefix, item)
133
{
134
    return `cli_${prefix}_${item}`;
135
}
136

    
137
function radio_li_id(prefix, item)
138
{
139
    return `rli_${prefix}_${item}`;
140
}
141

    
142
//TODO: refactor the 2 functions below
143

    
144
function add_chbx_li(prefix, name)
145
{
146
    if (![TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(prefix))
147
	return;
148

    
149
    let li = chbx_component_li_template.cloneNode(true);
150
    li.id = chbx_li_id(prefix, name);
151
    li.setAttribute("data-prefix", prefix);
152
    li.setAttribute("data-name", name);
153

    
154
    let chbx = li.firstElementChild.firstElementChild;
155
    let span = chbx.nextElementSibling;
156

    
157
    span.textContent = nice_name(prefix, name);
158

    
159
    chbx_components_ul.appendChild(li);
160
    list_set_scrollbar(chbx_components_ul);
161
}
162

    
163
var radio_component_none_li = by_id("radio_component_none_li");
164

    
165
function add_radio_li(prefix, name)
166
{
167
    if (![TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(prefix))
168
	return;
169

    
170
    let li = radio_component_li_template.cloneNode(true);
171
    li.id = radio_li_id(prefix, name);
172
    li.setAttribute("data-prefix", prefix);
173
    li.setAttribute("data-name", name);
174

    
175
    let radio = li.firstElementChild.firstElementChild;
176
    let span = radio.nextElementSibling;
177

    
178
    span.textContent = nice_name(prefix, name);
179

    
180
    radio_component_none_li.before(li);
181
    list_set_scrollbar(radio_components_ul);
182
}
183

    
184
/* Used to reset edited repo. */
185
function reset_work_repo_li(ul, item, _)
186
{
187
    ul.work_name_input.value = maybe_string(item);
188
}
189

    
190
/* Used to get repo data for saving */
191
function work_repo_li_data(ul)
192
{
193
    return [ul.work_name_input.value, {}];
194
}
195

    
196
const allow_native_scripts_container = by_id("allow_native_scripts_container");
197
const page_payload_span = by_id("page_payload");
198

    
199
function set_page_components(components)
200
{
201
    if (components === undefined) {
202
	page_payload_span.setAttribute("data-payload", "no");
203
	page_payload_span.textContent = "(None)";
204
	allow_native_scripts_container.classList.remove("form_disabled");
205
    } else {
206
	page_payload_span.setAttribute("data-payload", "yes");
207
	let [prefix, name] = components;
208
	page_payload_span.setAttribute("data-prefix", prefix);
209
	page_payload_span.setAttribute("data-name", name);
210
	page_payload_span.textContent = nice_name(prefix, name);
211
	allow_native_scripts_container.classList.add("form_disabled");
212
    }
213
}
214

    
215
const page_allow_chbx = by_id("page_allow_chbx");
216

    
217
/* Used to reset edited page. */
218
function reset_work_page_li(ul, item, settings)
219
{
220
    ul.work_name_input.value = maybe_string(item);
221
    settings = settings || {allow: false, components: undefined};
222
    page_allow_chbx.checked = !!settings.allow;
223

    
224
    set_page_components(settings.components);
225
}
226

    
227
function work_page_li_components()
228
{
229
    if (page_payload_span.getAttribute("data-payload") === "no")
230
	return undefined;
231

    
232
    let prefix = page_payload_span.getAttribute("data-prefix");
233
    let name = page_payload_span.getAttribute("data-name");
234
    return [prefix, name];
235
}
236

    
237
/* Used to get edited page data for saving. */
238
function work_page_li_data(ul)
239
{
240
    let url = ul.work_name_input.value;
241
    let settings = {
242
	components : work_page_li_components(),
243
	allow : !!page_allow_chbx.checked
244
    };
245

    
246
    return [url, settings];
247
}
248

    
249
const empty_bag_component_li = by_id("empty_bag_component_li");
250
var bag_components_ul = by_id("bag_components_ul");
251

    
252
function remove_bag_component_entry(entry)
253
{
254
    const list = entry.parentElement;
255
    entry.remove();
256
    list_set_scrollbar(list);
257
}
258

    
259
/* Used to construct and update components list of edited bag. */
260
function add_bag_components(components)
261
{
262
    for (let component of components) {
263
	let [prefix, name] = component;
264
	let li = bag_component_li_template.cloneNode(true);
265
	li.setAttribute("data-prefix", prefix);
266
	li.setAttribute("data-name", name);
267

    
268
	let span = li.firstElementChild;
269
	span.textContent = nice_name(prefix, name);
270
	let remove_but = span.nextElementSibling;
271
	remove_but.addEventListener("click",
272
				    () => remove_bag_component_entry(li));
273
	bag_components_ul.appendChild(li);
274
    }
275

    
276
    bag_components_ul.appendChild(empty_bag_component_li);
277
    list_set_scrollbar(bag_components_ul);
278
}
279

    
280
/* Used to reset edited bag. */
281
function reset_work_bag_li(ul, item, components)
282
{
283
    components = components || [];
284

    
285
    ul.work_name_input.value = maybe_string(item);
286
    let old_components_ul = bag_components_ul;
287
    bag_components_ul = old_components_ul.cloneNode(false);
288

    
289
    old_components_ul.replaceWith(bag_components_ul);
290

    
291
    add_bag_components(components);
292
}
293

    
294
/* Used to get edited bag data for saving. */
295
function work_bag_li_data(ul)
296
{
297
    let component_li = bag_components_ul.firstElementChild;
298

    
299
    let components = [];
300

    
301
    /* Last list element is empty li with id set. */
302
    while (component_li.id === '') {
303
	components.push([component_li.getAttribute("data-prefix"),
304
			 component_li.getAttribute("data-name")]);
305
	component_li = component_li.nextElementSibling;
306
    }
307

    
308
    return [ul.work_name_input.value, components];
309
}
310

    
311
const script_url_input = by_id("script_url_field");
312
const script_sha256_input = by_id("script_sha256_field");
313
const script_contents_field = by_id("script_contents_field");
314

    
315
function maybe_string(maybe_defined)
316
{
317
    return maybe_defined === undefined ? "" : maybe_defined + "";
318
}
319

    
320
/* Used to reset edited script. */
321
function reset_work_script_li(ul, name, data)
322
{
323
    ul.work_name_input.value = maybe_string(name);
324
    if (data === undefined)
325
	data = {};
326
    script_url_input.value = maybe_string(data.url);
327
    script_sha256_input.value = maybe_string(data.hash);
328
    script_contents_field.value = maybe_string(data.text);
329
}
330

    
331
/* Used to get edited script data for saving. */
332
function work_script_li_data(ul)
333
{
334
    return [ul.work_name_input.value, {
335
	url : script_url_input.value,
336
	hash : script_sha256_input.value,
337
	text : script_contents_field.value
338
    }];
339
}
340

    
341
function cancel_work(prefix)
342
{
343
    let ul = ul_by_prefix[prefix];
344

    
345
    if (ul.state === UL_STATE.IDLE)
346
	return;
347

    
348
    if (ul.state === UL_STATE.EDITING_ENTRY) {
349
	add_li(prefix, ul.edited_item);
350
    }
351

    
352
    ul.work_li.classList.add("hide");
353
    ul.ul.append(ul.work_li);
354
    list_set_scrollbar(ul.ul);
355
    ul.state = UL_STATE.IDLE;
356
}
357

    
358
function save_work(prefix)
359
{
360
    let ul = ul_by_prefix[prefix];
361

    
362
    if (ul.state === UL_STATE.IDLE)
363
	return;
364

    
365
    let [item, data] = ul.get_work_li_data(ul);
366

    
367
    /* Here we fire promises and return without waiting. */
368

    
369
    if (ul.state === UL_STATE.EDITING_ENTRY)
370
	storage.replace(prefix, ul.edited_item, item, data);
371
    if (ul.state === UL_STATE.ADDING_ENTRY)
372
	storage.set(prefix, item, data);
373

    
374
    cancel_work(prefix);
375
}
376

    
377
function edit_item(prefix, item)
378
{
379
    cancel_work(prefix);
380

    
381
    let ul = ul_by_prefix[prefix];
382
    let li = by_id(item_li_id(prefix, item));
383

    
384
    if (li === null) {
385
	add_new_item(prefix, item);
386
	return;
387
    }
388

    
389
    ul.reset_work_li(ul, item, storage.get(prefix, item));
390
    ul.ul.insertBefore(ul.work_li, li);
391
    ul.ul.removeChild(li);
392
    ul.work_li.classList.remove("hide");
393
    list_set_scrollbar(ul.ul);
394

    
395
    ul.state = UL_STATE.EDITING_ENTRY;
396
    ul.edited_item = item;
397
}
398

    
399
const file_downloader = by_id("file_downloader");
400

    
401
function recursively_export_item(prefix, name, added_items, items_data)
402
{
403
    let key = prefix + name;
404

    
405
    if (added_items.has(key))
406
	return;
407

    
408
    let data = storage.get(prefix, name);
409
    if (data === undefined) {
410
	console.log(`${TYPE_NAME[prefix]} '${name}' for export not found`);
411
	return;
412
    }
413

    
414
    if (prefix !== TYPE_PREFIX.SCRIPT) {
415
	let components = prefix === TYPE_PREFIX.BAG ?
416
	    data : [data.components];
417

    
418
	for (let [comp_prefix, comp_name] of components) {
419
	    recursively_export_item(comp_prefix, comp_name,
420
				    added_items, items_data);
421
	}
422
    }
423

    
424
    items_data.push({[key]: data});
425
    added_items.add(key);
426
}
427

    
428
function export_item(prefix, name)
429
{
430
    let added_items = new Set();
431
    let items_data = [];
432
    recursively_export_item(prefix, name, added_items, items_data);
433
    let file = new Blob([JSON.stringify(items_data)],
434
			{type: "application/json"});
435
    let url = URL.createObjectURL(file);
436
    file_downloader.setAttribute("href", url);
437
    file_downloader.setAttribute("download", prefix + name + ".json");
438
    file_downloader.click();
439
    file_downloader.removeAttribute("href");
440
    URL.revokeObjectURL(url);
441
}
442

    
443
function add_new_item(prefix, name)
444
{
445
    cancel_work(prefix);
446

    
447
    let ul = ul_by_prefix[prefix];
448
    ul.reset_work_li(ul);
449
    ul.work_li.classList.remove("hide");
450
    ul.ul.appendChild(ul.work_li);
451
    list_set_scrollbar(ul.ul);
452

    
453
    if (name !== undefined)
454
	ul.work_name_input.value = name;
455
    ul.state = UL_STATE.ADDING_ENTRY;
456
}
457

    
458
const chbx_components_window = by_id("chbx_components_window");
459

    
460
function bag_components()
461
{
462
    chbx_components_window.classList.remove("hide");
463
    radio_components_window.classList.add("hide");
464

    
465
    for (let li of chbx_components_ul.children) {
466
	let chbx = li.firstElementChild.firstElementChild;
467
	chbx.checked = false;
468
    }
469
}
470

    
471
function commit_bag_components()
472
{
473
    let selected = [];
474

    
475
    for (let li of chbx_components_ul.children) {
476
	let chbx = li.firstElementChild.firstElementChild;
477
	if (!chbx.checked)
478
	    continue;
479

    
480
	selected.push([li.getAttribute("data-prefix"),
481
		       li.getAttribute("data-name")]);
482
    }
483

    
484
    add_bag_components(selected);
485
    cancel_components();
486
}
487

    
488
const radio_components_window = by_id("radio_components_window");
489
var radio_component_none_input = by_id("radio_component_none_input");
490

    
491
function page_components()
492
{
493
    radio_components_window.classList.remove("hide");
494
    chbx_components_window.classList.add("hide");
495

    
496
    radio_component_none_input.checked = true;
497

    
498
    let components = work_page_li_components();
499
    if (components === undefined)
500
	return;
501

    
502
    let [prefix, item] = components;
503
    let li = by_id(radio_li_id(prefix, item));
504

    
505
    if (li === null)
506
	radio_component_none_input.checked = false;
507
    else
508
	li.firstElementChild.firstElementChild.checked = true;
509
}
510

    
511
function commit_page_components()
512
{
513
    let components = null;
514

    
515
    for (let li of radio_components_ul.children) {
516
	let radio = li.firstElementChild.firstElementChild;
517
	if (!radio.checked)
518
	    continue;
519

    
520
	components = [li.getAttribute("data-prefix"),
521
		      li.getAttribute("data-name")];
522

    
523
	if (radio.id === "radio_component_none_input")
524
	    components = undefined;
525

    
526
	break;
527
    }
528

    
529
    if (components !== null)
530
	set_page_components(components);
531
    cancel_components();
532
}
533

    
534
function cancel_components()
535
{
536
    chbx_components_window.classList.add("hide");
537
    radio_components_window.classList.add("hide");
538
}
539

    
540
const UL_STATE = {
541
    EDITING_ENTRY : 0,
542
    ADDING_ENTRY : 1,
543
    IDLE : 2
544
};
545

    
546
const ul_by_prefix = {
547
    [TYPE_PREFIX.REPO] : {
548
	ul : by_id("repos_ul"),
549
	work_li : by_id("work_repo_li"),
550
	work_name_input : by_id("repo_url_field"),
551
	reset_work_li : reset_work_repo_li,
552
	get_work_li_data : work_repo_li_data,
553
	state : UL_STATE.IDLE,
554
	edited_item : undefined,
555
    },
556
    [TYPE_PREFIX.PAGE] : {
557
	ul : by_id("pages_ul"),
558
	work_li : by_id("work_page_li"),
559
	work_name_input : by_id("page_url_field"),
560
	reset_work_li : reset_work_page_li,
561
	get_work_li_data : work_page_li_data,
562
	select_components : page_components,
563
	commit_components : commit_page_components,
564
	state : UL_STATE.IDLE,
565
	edited_item : undefined,
566
    },
567
    [TYPE_PREFIX.BAG] : {
568
	ul : by_id("bags_ul"),
569
	work_li : by_id("work_bag_li"),
570
	work_name_input : by_id("bag_name_field"),
571
	reset_work_li : reset_work_bag_li,
572
	get_work_li_data : work_bag_li_data,
573
	select_components : bag_components,
574
	commit_components : commit_bag_components,
575
	state : UL_STATE.IDLE,
576
	edited_item : undefined,
577
    },
578
    [TYPE_PREFIX.SCRIPT] : {
579
	ul : by_id("scripts_ul"),
580
	work_li : by_id("work_script_li"),
581
	work_name_input : by_id("script_name_field"),
582
	reset_work_li : reset_work_script_li,
583
	get_work_li_data : work_script_li_data,
584
	state : UL_STATE.IDLE,
585
	edited_item : undefined,
586
    }
587
}
588

    
589
/*
590
 * Newer browsers could utilise `text' method of File objects.
591
 * Older ones require FileReader.
592
 */
593

    
594
function _read_file(file, resolve, reject)
595
{
596
    let reader = new FileReader();
597

    
598
    reader.onload = () => resolve(reader.result);
599
    reader.onerror = () => reject(reader.error);
600
    reader.readAsText(file);
601
}
602

    
603
function read_file(file)
604
{
605
    return new Promise((resolve, reject) =>
606
		       _read_file(file, resolve, reject));
607
}
608

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

    
612
const settings_schema = [
613
    [{}, "matchentry", "minentries", 1,
614
     new RegExp(`^${TYPE_PREFIX.SCRIPT}`), {
615
	 /* script data */
616
	 "url":    ["optional", url_regex, "or", empty_regex],
617
	 "sha256": ["optional", matchers.sha256, "or", empty_regex],
618
	 "text":   ["optional", "string"]
619
     },
620
     new RegExp(`^${TYPE_PREFIX.BAG}`), [
621
	 "optional",
622
	 [matchers.component, "repeat"],
623
	 "default", undefined
624
     ],
625
     new RegExp(`^${TYPE_PREFIX.PAGE}`), {
626
	 /* page data */
627
	 "components": ["optional", matchers.component]
628
     }], "repeat"
629
];
630

    
631
const import_window = by_id("import_window");
632
let import_frame;
633

    
634
async function import_from_file(event)
635
{
636
    let files = event.target.files;
637
    if (files.length < 1)
638
	return;
639

    
640
    import_window.classList.remove("hide");
641
    import_frame.show_loading();
642

    
643
    try {
644
	const file = await read_file(files[0]);
645
	var result = parse_json_with_schema(settings_schema, file);
646
    } catch(e) {
647
	import_frame.show_error("Bad file :(", "" + e);
648
	return;
649
    }
650

    
651
    import_frame.show_selection(result);
652
}
653

    
654
const file_opener_form = by_id("file_opener_form");
655

    
656
function hide_import_window()
657
{
658
    import_window.classList.add("hide");
659

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

    
667
async function initialize_import_facility()
668
{
669
    let import_but = by_id("import_but");
670
    let file_opener = by_id("file_opener");
671

    
672
    import_but.addEventListener("click", () => file_opener.click());
673
    file_opener.addEventListener("change", import_from_file);
674

    
675
    import_frame = await get_import_frame();
676
    import_frame.onclose = hide_import_window;
677
    import_frame.style_table("has_bottom_line", "always_scrollbar",
678
			     "has_upper_line", "tight_table");
679
}
680

    
681
/*
682
 * If url has a target appended, e.g.
683
 * chrome-extension://hnhmbnpohhlmhehionjgongbnfdnabdl/html/options.html#smyhax
684
 * that target will be split into prefix and item name (e.g. "s" and "myhax")
685
 * and editing of that respective item will be started.
686
 *
687
 * We don't need to worry about the state of the page (e.g. some editing being
688
 * in progress) in jump_to_item() - this function is called at the beginning,
689
 * together with callbacks being assigned to buttons, so it is safe to assume
690
 * lists are initialized with items and page is in its virgin state with regard
691
 * to everything else.
692
 */
693
function jump_to_item(url_with_item)
694
{
695
    const [dummy1, base_url, dummy2, target] =
696
	  /^([^#]*)(#(.*))?$/i.exec(url_with_item);
697
    if (target === undefined)
698
	return;
699

    
700
    const prefix = target.substring(0, 1);
701

    
702
    if (!list_prefixes.includes(prefix)) {
703
	history.replaceState(null, "", base_url);
704
	return;
705
    }
706

    
707
    by_id(`show_${TYPE_NAME[prefix]}s`).checked = true;
708
    edit_item(prefix, decodeURIComponent(target.substring(1)));
709
}
710

    
711
async function main()
712
{
713
    init_default_policy_dialog();
714

    
715
    storage = await get_remote_storage();
716

    
717
    for (let prefix of list_prefixes) {
718
	for (let item of storage.get_all_names(prefix).sort()) {
719
	    add_li(prefix, item, true);
720
	    add_chbx_li(prefix, item);
721
	    add_radio_li(prefix, item);
722
	}
723

    
724
	let name = TYPE_NAME[prefix];
725

    
726
	let add_but = by_id(`add_${name}_but`);
727
	let discard_but = by_id(`discard_${name}_but`);
728
	let save_but = by_id(`save_${name}_but`);
729

    
730
	add_but.addEventListener("click", () => add_new_item(prefix));
731
	discard_but.addEventListener("click", () => cancel_work(prefix));
732
	save_but.addEventListener("click", () => save_work(prefix));
733

    
734
	if ([TYPE_PREFIX.REPO, TYPE_PREFIX.SCRIPT].includes(prefix))
735
	    continue;
736

    
737
	let ul = ul_by_prefix[prefix];
738

    
739
	let commit_components_but = by_id(`commit_${name}_components_but`);
740
	let cancel_components_but = by_id(`cancel_${name}_components_but`);
741
	let select_components_but = by_id(`select_${name}_components_but`);
742

    
743
	commit_components_but
744
	    .addEventListener("click", ul.commit_components);
745
	select_components_but
746
	    .addEventListener("click", ul.select_components);
747
	cancel_components_but.addEventListener("click", cancel_components);
748
    }
749

    
750
    jump_to_item(document.URL);
751

    
752
    storage.add_change_listener(handle_change);
753

    
754
    await initialize_import_facility();
755
}
756

    
757
function handle_change(change)
758
{
759
    if (change.old_val === undefined) {
760
	add_li(change.prefix, change.item);
761
	add_chbx_li(change.prefix, change.item);
762
	add_radio_li(change.prefix, change.item);
763

    
764
	return;
765
    }
766

    
767
    if (change.new_val !== undefined)
768
	return;
769

    
770
    let ul = ul_by_prefix[change.prefix];
771
    if (ul.state === UL_STATE.EDITING_ENTRY &&
772
	ul.edited_item === change.item) {
773
	ul.state = UL_STATE.ADDING_ENTRY;
774
	return;
775
    }
776

    
777
    let uls_creators = [[ul.ul, item_li_id]];
778

    
779
    if ([TYPE_PREFIX.BAG, TYPE_PREFIX.SCRIPT].includes(change.prefix)) {
780
	uls_creators.push([chbx_components_ul, chbx_li_id]);
781
	uls_creators.push([radio_components_ul, radio_li_id]);
782
    }
783

    
784
    for (let [components_ul, id_creator] of uls_creators) {
785
	let li = by_id(id_creator(change.prefix, change.item));
786
	components_ul.removeChild(li);
787
	list_set_scrollbar(components_ul);
788
    }
789
}
790

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