Project

General

Profile

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

haketilo / html / options_main.js @ 261548ff

1
/**
2
 * Myext 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
 * IMPORTS_END
15
 */
16

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

    
23
function nice_name(prefix, name)
24
{
25
    return `${name} (${TYPE_NAME[prefix]})`;
26
}
27

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

    
40
function item_li_id(prefix, item)
41
{
42
    return `li_${prefix}_${item}`;
43
}
44

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

    
52
    let span = li.firstElementChild;
53
    span.textContent = item;
54

    
55
    let edit_button = span.nextElementSibling;
56
    edit_button.addEventListener("click", () => edit_item(prefix, item));
57

    
58
    let remove_button = edit_button.nextElementSibling;
59
    remove_button.addEventListener("click",
60
				   () => storage.remove(prefix, item));
61

    
62
    let export_button = remove_button.nextElementSibling;
63
    export_button.addEventListener("click",
64
				   () => export_item(prefix, item));
65

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

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

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

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

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

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

    
92
//TODO: refactor the 2 functions below
93

    
94
function add_chbx_li(prefix, name)
95
{
96
    if (prefix === TYPE_PREFIX.PAGE)
97
	return;
98

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

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

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

    
109
    chbx_components_ul.appendChild(li);
110
}
111

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

    
114
function add_radio_li(prefix, name)
115
{
116
    if (prefix === TYPE_PREFIX.PAGE)
117
	return;
118

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

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

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

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

    
132
const page_payload_span = by_id("page_payload");
133

    
134
function set_page_components(components)
135
{
136
    if (components === undefined) {
137
	page_payload_span.setAttribute("data-payload", "no");
138
	page_payload_span.textContent = "(None)";
139
    } else {
140
	page_payload_span.setAttribute("data-payload", "yes");
141
	let [prefix, name] = components;
142
	page_payload_span.setAttribute("data-prefix", prefix);
143
	page_payload_span.setAttribute("data-name", name);
144
	page_payload_span.textContent = nice_name(prefix, name);
145
    }
146
}
147

    
148
const page_allow_chbx = by_id("page_allow_chbx");
149

    
150
/* Used to reset edited page. */
151
function reset_work_page_li(ul, item, settings)
152
{
153
    ul.work_name_input.value = maybe_string(item);
154
    settings = settings || {allow: false, components: undefined};
155
    page_allow_chbx.checked = !!settings.allow;
156

    
157
    set_page_components(settings.components);
158
}
159

    
160
function work_page_li_components()
161
{
162
    if (page_payload_span.getAttribute("data-payload") === "no")
163
	return undefined;
164

    
165
    let prefix = page_payload_span.getAttribute("data-prefix");
166
    let name = page_payload_span.getAttribute("data-name");
167
    return [prefix, name];
168
}
169

    
170
/* Used to get edited page data for saving. */
171
function work_page_li_data(ul)
172
{
173
    let url = ul.work_name_input.value;
174
    let settings = {
175
	components : work_page_li_components(),
176
	allow : !!page_allow_chbx.checked
177
    };
178

    
179
    return [url, settings];
180
}
181

    
182
const empty_bag_component_li = by_id("empty_bag_component_li");
183
var bag_components_ul = by_id("bag_components_ul");
184

    
185
/* Used to construct and update components list of edited bag. */
186
function add_bag_components(components)
187
{
188
    for (let component of components) {
189
	let [prefix, name] = component;
190
	let li = bag_component_li_template.cloneNode(true);
191
	li.setAttribute("data-prefix", prefix);
192
	li.setAttribute("data-name", name);
193
	let span = li.firstElementChild;
194
	span.textContent = nice_name(prefix, name);
195
	let remove_but = span.nextElementSibling;
196
	remove_but.addEventListener("click", () =>
197
				    bag_components_ul.removeChild(li));
198
	bag_components_ul.appendChild(li);
199
    }
200

    
201
    bag_components_ul.appendChild(empty_bag_component_li);
202
}
203

    
204
/* Used to reset edited bag. */
205
function reset_work_bag_li(ul, item, components)
206
{
207
    components = components || [];
208

    
209
    ul.work_name_input.value = maybe_string(item);
210
    let old_components_ul = bag_components_ul;
211
    bag_components_ul = old_components_ul.cloneNode(false);
212

    
213
    ul.work_li.insertBefore(bag_components_ul, old_components_ul);
214
    ul.work_li.removeChild(old_components_ul);
215

    
216
    add_bag_components(components);
217
}
218

    
219
/* Used to get edited bag data for saving. */
220
function work_bag_li_data(ul)
221
{
222
    let components_ul = ul.work_name_input.nextElementSibling;
223
    let component_li = components_ul.firstElementChild;
224

    
225
    let components = [];
226

    
227
    /* Last list element is empty li with id set. */
228
    while (component_li.id === '') {
229
	components.push([component_li.getAttribute("data-prefix"),
230
			 component_li.getAttribute("data-name")]);
231
	component_li = component_li.nextElementSibling;
232
    }
233

    
234
    return [ul.work_name_input.value, components];
235
}
236

    
237
const script_url_input = by_id("script_url_field");
238
const script_sha256_input = by_id("script_sha256_field");
239
const script_contents_field = by_id("script_contents_field");
240

    
241
function maybe_string(maybe_defined)
242
{
243
    return maybe_defined === undefined ? "" : maybe_defined + "";
244
}
245

    
246
/* Used to reset edited script. */
247
function reset_work_script_li(ul, name, data)
248
{
249
    ul.work_name_input.value = maybe_string(name);
250
    if (data === undefined)
251
	data = {};
252
    script_url_input.value = maybe_string(data.url);
253
    script_sha256_input.value = maybe_string(data.hash);
254
    script_contents_field.value = maybe_string(data.text);
255
}
256

    
257
/* Used to get edited script data for saving. */
258
function work_script_li_data(ul)
259
{
260
    return [ul.work_name_input.value, {
261
	url : script_url_input.value,
262
	hash : script_sha256_input.value,
263
	text : script_contents_field.value
264
    }];
265
}
266

    
267
function cancel_work(prefix)
268
{
269
    let ul = ul_by_prefix[prefix];
270

    
271
    if (ul.state === UL_STATE.IDLE)
272
	return;
273

    
274
    if (ul.state === UL_STATE.EDITING_ENTRY) {
275
	add_li(prefix, ul.edited_item);
276
    }
277

    
278
    ul.work_li.classList.add("hide");
279
    ul.state = UL_STATE.IDLE;
280
}
281

    
282
function save_work(prefix)
283
{
284
    let ul = ul_by_prefix[prefix];
285

    
286
    if (ul.state === UL_STATE.IDLE)
287
	return;
288

    
289
    let [item, data] = ul.get_work_li_data(ul);
290

    
291
    /* Here we fire promises and return without waiting. */
292

    
293
    if (ul.state === UL_STATE.EDITING_ENTRY)
294
	storage.replace(prefix, ul.edited_item, item, data);
295
    if (ul.state === UL_STATE.ADDING_ENTRY)
296
	storage.set(prefix, item, data);
297

    
298
    cancel_work(prefix);
299
}
300

    
301
function edit_item(prefix, item)
302
{
303
    cancel_work(prefix);
304

    
305
    let ul = ul_by_prefix[prefix];
306
    let li = by_id(item_li_id(prefix, item));
307
    ul.reset_work_li(ul, item, storage.get(prefix, item));
308
    ul.ul.insertBefore(ul.work_li, li);
309
    ul.ul.removeChild(li);
310
    ul.work_li.classList.remove("hide");
311

    
312
    ul.state = UL_STATE.EDITING_ENTRY;
313
    ul.edited_item = item;
314
}
315

    
316
const file_downloader = by_id("file_downloader");
317

    
318
function recursively_export_item(prefix, name, added_items, items_data)
319
{
320
    let key = prefix + name;
321

    
322
    if (added_items.has(key))
323
	return;
324

    
325
    let data = storage.get(prefix, name);
326
    if (data === undefined) {
327
	console.log(`${TYPE_NAME[prefix]} '${name}' for export not found`);
328
	return;
329
    }
330

    
331
    if (prefix !== TYPE_PREFIX.SCRIPT) {
332
	let components = prefix === TYPE_PREFIX.BAG ?
333
	    data : [data.components];
334

    
335
	for (let [comp_prefix, comp_name] of components) {
336
	    recursively_export_item(comp_prefix, comp_name,
337
				    added_items, items_data);
338
	}
339
    }
340

    
341
    items_data.push({[key]: data});
342
    added_items.add(key);
343
}
344

    
345
function export_item(prefix, name)
346
{
347
    let added_items = new Set();
348
    let items_data = [];
349
    recursively_export_item(prefix, name, added_items, items_data);
350
    let file = new Blob([JSON.stringify(items_data)],
351
			{type: "application/json"});
352
    let url = URL.createObjectURL(file);
353
    file_downloader.setAttribute("href", url);
354
    file_downloader.setAttribute("download", prefix + name + ".json");
355
    file_downloader.click();
356
    file_downloader.removeAttribute("href");
357
    URL.revokeObjectURL(url);
358
}
359

    
360
function add_new_item(prefix)
361
{
362
    cancel_work(prefix);
363

    
364
    let ul = ul_by_prefix[prefix];
365
    ul.reset_work_li(ul);
366
    ul.work_li.classList.remove("hide");
367
    ul.ul.appendChild(ul.work_li);
368

    
369
    ul.state = UL_STATE.ADDING_ENTRY;
370
}
371

    
372
const chbx_components_window = by_id("chbx_components_window");
373

    
374
function bag_components()
375
{
376
    chbx_components_window.classList.remove("hide");
377
    radio_components_window.classList.add("hide");
378

    
379
    for (let li of chbx_components_ul.children) {
380
	let chbx = li.firstElementChild;
381
	chbx.checked = false;
382
    }
383
}
384

    
385
function commit_bag_components()
386
{
387
    let selected = [];
388

    
389
    for (let li of chbx_components_ul.children) {
390
	let chbx = li.firstElementChild;
391
	if (!chbx.checked)
392
	    continue;
393

    
394
	selected.push([li.getAttribute("data-prefix"),
395
		       li.getAttribute("data-name")]);
396
    }
397

    
398
    add_bag_components(selected);
399
    cancel_components();
400
}
401

    
402
const radio_components_window = by_id("radio_components_window");
403
var radio_component_none_input = by_id("radio_component_none_input");
404

    
405
function page_components()
406
{
407
    radio_components_window.classList.remove("hide");
408
    chbx_components_window.classList.add("hide");
409

    
410
    radio_component_none_input.checked = true;
411

    
412
    let components = work_page_li_components();
413
    if (components === undefined)
414
	return;
415

    
416
    let [prefix, item] = components;
417
    let li = by_id(radio_li_id(prefix, item));
418
    if (li === null)
419
	radio_component_none_input.checked = false;
420
    else
421
	li.firstElementChild.checked = true;
422
}
423

    
424
function commit_page_components()
425
{
426
    let components = null;
427

    
428
    for (let li of radio_components_ul.children) {
429
	let radio = li.firstElementChild;
430
	if (!radio.checked)
431
	    continue;
432

    
433
	components = [li.getAttribute("data-prefix"),
434
		      li.getAttribute("data-name")];
435

    
436
	if (radio.id === "radio_component_none_input")
437
	    components = undefined;
438

    
439
	break;
440
    }
441

    
442
    if (components !== null)
443
	set_page_components(components);
444
    cancel_components();
445
}
446

    
447
function cancel_components()
448
{
449
    chbx_components_window.classList.add("hide");
450
    radio_components_window.classList.add("hide");
451
}
452

    
453
const UL_STATE = {
454
    EDITING_ENTRY : 0,
455
    ADDING_ENTRY : 1,
456
    IDLE : 2
457
};
458

    
459
const ul_by_prefix = {
460
    [TYPE_PREFIX.PAGE] : {
461
	ul : by_id("pages_ul"),
462
	work_li : by_id("work_page_li"),
463
	work_name_input : by_id("page_url_field"),
464
	reset_work_li : reset_work_page_li,
465
	get_work_li_data : work_page_li_data,
466
	select_components : page_components,
467
	commit_components : commit_page_components,
468
	state : UL_STATE.IDLE,
469
	edited_item : undefined,
470
    },
471
    [TYPE_PREFIX.BAG] : {
472
	ul : by_id("bags_ul"),
473
	work_li : by_id("work_bag_li"),
474
	work_name_input : by_id("bag_name_field"),
475
	reset_work_li : reset_work_bag_li,
476
	get_work_li_data : work_bag_li_data,
477
	select_components : bag_components,
478
	commit_components : commit_bag_components,
479
	state : UL_STATE.IDLE,
480
	edited_item : undefined,
481
    },
482
    [TYPE_PREFIX.SCRIPT] : {
483
	ul : by_id("scripts_ul"),
484
	work_li : by_id("work_script_li"),
485
	work_name_input : by_id("script_name_field"),
486
	reset_work_li : reset_work_script_li,
487
	get_work_li_data : work_script_li_data,
488
	state : UL_STATE.IDLE,
489
	edited_item : undefined,
490
    }
491
}
492

    
493
const import_window = by_id("import_window");
494
const import_loading_radio = by_id("import_loading_radio");
495
const import_failed_radio = by_id("import_failed_radio");
496
const import_selection_radio = by_id("import_selection_radio");
497
const bad_file_errormsg = by_id("bad_file_errormsg");
498

    
499
/*
500
 * Newer browsers could utilise `text' method of File objects.
501
 * Older ones require FileReader.
502
 */
503

    
504
function _read_file(file, resolve, reject)
505
{
506
    let reader = new FileReader();
507

    
508
    reader.onload = () => resolve(reader.result);
509
    reader.onerror = () => reject(reader.error);
510
    reader.readAsText(file);
511
}
512

    
513
function read_file(file)
514
{
515
    return new Promise((resolve, reject) =>
516
		       _read_file(file, resolve, reject));
517
}
518

    
519
async function import_from_file(event)
520
{
521
    let files = event.target.files;
522
    if (files.length < 1)
523
	return;
524

    
525
    import_window.classList.remove("hide");
526
    import_loading_radio.checked = true;
527

    
528
    let result = undefined;
529

    
530
    try {
531
	result = JSON.parse(await read_file(files[0]));
532
    } catch(e) {
533
	bad_file_errormsg.textContent = "" + e;
534
	import_failed_radio.checked = true;
535
	return;
536
    }
537

    
538
    let errormsg = validate_settings(result);
539
    if (errormsg !== false) {
540
	bad_file_errormsg.textContent = errormsg;
541
	import_failed_radio.checked = true;
542
	return;
543
    }
544

    
545
    populate_import_list(result);
546
    import_selection_radio.checked = true;
547
}
548

    
549
function validate_settings(settings)
550
{
551
    // TODO
552
    return false;
553
}
554

    
555
function import_li_id(prefix, item)
556
{
557
    return `ili_${prefix}_${item}`;
558
}
559

    
560
let import_ul = by_id("import_ul");
561
let import_chbxs_colliding = undefined;
562
let settings_import_map = undefined;
563

    
564
function populate_import_list(settings)
565
{
566
    let old_children = import_ul.children;
567
    while (old_children[0] !== undefined)
568
	import_ul.removeChild(old_children[0]);
569

    
570
    import_chbxs_colliding = [];
571
    settings_import_map = new Map();
572

    
573
    for (let setting of settings) {
574
	let [key, value] = Object.entries(setting)[0];
575
	let prefix = key[0];
576
	let name = key.substring(1);
577
	add_import_li(prefix, name);
578
	settings_import_map.set(key, value);
579
    }
580
}
581

    
582
function add_import_li(prefix, name)
583
{
584
    let li = import_li_template.cloneNode(true);
585
    let name_span = li.firstElementChild;
586
    let chbx = name_span.nextElementSibling;
587
    let warning_span = chbx.nextElementSibling;
588

    
589
    li.setAttribute("data-prefix", prefix);
590
    li.setAttribute("data-name", name);
591
    li.id = import_li_id(prefix, name);
592
    name_span.textContent = nice_name(prefix, name);
593

    
594
    if (storage.get(prefix, name) !== undefined) {
595
	import_chbxs_colliding.push(chbx);
596
	warning_span.textContent = "(will overwrite existing setting!)";
597
    }
598

    
599
    import_ul.appendChild(li);
600
}
601

    
602
function check_all_imports()
603
{
604
    for (let li of import_ul.children)
605
	li.firstElementChild.nextElementSibling.checked = true;
606
}
607

    
608
function uncheck_all_imports()
609
{
610
    for (let li of import_ul.children)
611
	li.firstElementChild.nextElementSibling.checked = false;
612
}
613

    
614
function uncheck_colliding_imports()
615
{
616
    for (let chbx of import_chbxs_colliding)
617
	chbx.checked = false;
618
}
619

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

    
622
function hide_import_window()
623
{
624
    import_window.classList.add("hide");
625
    /* Let GC free some memory */
626
    import_chbxs_colliding = undefined;
627
    settings_import_map = undefined;
628

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

    
636
function commit_import()
637
{
638
    for (let li of import_ul.children) {
639
	let chbx = li.firstElementChild.nextElementSibling;
640

    
641
	if (!chbx.checked)
642
	    continue;
643

    
644
	let prefix = li.getAttribute("data-prefix");
645
	let name = li.getAttribute("data-name");
646
	let key = prefix + name;
647
	let value = settings_import_map.get(key);
648
	storage.set(prefix, name, value);
649
    }
650

    
651
    hide_import_window();
652
}
653

    
654
function initialize_import_facility()
655
{
656
    let import_but = by_id("import_but");
657
    let file_opener = by_id("file_opener");
658
    let import_failok_but = by_id("import_failok_but");
659
    let check_all_import_but = by_id("check_all_import_but");
660
    let uncheck_all_import_but = by_id("uncheck_all_import_but");
661
    let uncheck_existing_import_but = by_id("uncheck_existing_import_but");
662
    let commit_import_but = by_id("commit_import_but");
663
    let cancel_import_but = by_id("cancel_import_but");
664
    import_but.addEventListener("click", () => file_opener.click());
665
    file_opener.addEventListener("change", import_from_file);
666
    import_failok_but.addEventListener("click", hide_import_window);
667
    check_all_import_but.addEventListener("click", check_all_imports);
668
    uncheck_all_import_but.addEventListener("click", uncheck_all_imports);
669
    uncheck_colliding_import_but
670
	.addEventListener("click", uncheck_colliding_imports);
671
    commit_import_but.addEventListener("click", commit_import);
672
    cancel_import_but.addEventListener("click", hide_import_window);
673
}
674

    
675
async function main()
676
{
677
    storage = await get_remote_storage();
678

    
679
    for (let prefix of list_prefixes) {
680
	for (let item of storage.get_all_names(prefix).sort()) {
681
	    add_li(prefix, item, true);
682
	    add_chbx_li(prefix, item);
683
	    add_radio_li(prefix, item);
684
	}
685

    
686
	let name = TYPE_NAME[prefix];
687

    
688
	let add_but = by_id(`add_${name}_but`);
689
	let discard_but = by_id(`discard_${name}_but`);
690
	let save_but = by_id(`save_${name}_but`);
691

    
692
	add_but.addEventListener("click", () => add_new_item(prefix));
693
	discard_but.addEventListener("click", () => cancel_work(prefix));
694
	save_but.addEventListener("click", () => save_work(prefix));
695

    
696
	if (prefix === TYPE_PREFIX.SCRIPT)
697
	    continue;
698

    
699
	let ul = ul_by_prefix[prefix];
700

    
701
	let commit_components_but = by_id(`commit_${name}_components_but`);
702
	let cancel_components_but = by_id(`cancel_${name}_components_but`);
703
	let select_components_but = by_id(`select_${name}_components_but`);
704

    
705
	commit_components_but
706
	    .addEventListener("click", ul.commit_components);
707
	select_components_but
708
	    .addEventListener("click", ul.select_components);
709
	cancel_components_but.addEventListener("click", cancel_components);
710
    }
711

    
712
    initialize_import_facility();
713

    
714
    storage.add_change_listener(handle_change);
715
}
716

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

    
724
	return;
725
    }
726

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

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

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

    
739
    if (change.prefix !== TYPE_PREFIX.PAGE) {
740
	uls_creators.push([chbx_components_ul, chbx_li_id]);
741
	uls_creators.push([radio_components_ul, radio_li_id]);
742
    }
743

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

    
750
main();
(4-4/4)