1
|
/**
|
2
|
* This file is part of Haketilo.
|
3
|
*
|
4
|
* Function: Showing a list of resources/mappings in a browser.
|
5
|
*
|
6
|
* Copyright (C) 2022 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 of this code in a
|
41
|
* proprietary program, I am not going to enforce this in court.
|
42
|
*/
|
43
|
|
44
|
#IMPORT common/indexeddb.js AS haketilodb
|
45
|
#IMPORT html/dialog.js
|
46
|
|
47
|
#FROM html/item_preview.js IMPORT resource_preview, mapping_preview
|
48
|
#FROM html/DOM_helpers.js IMPORT clone_template
|
49
|
|
50
|
function preview_item(list_ctx, item, ignore_dialog=false)
|
51
|
{
|
52
|
if (list_ctx.dialog_ctx.shown && !ignore_dialog)
|
53
|
return;
|
54
|
|
55
|
list_ctx.preview_ctx =
|
56
|
list_ctx.preview_cb(item.definition, list_ctx.preview_ctx);
|
57
|
list_ctx.preview_container.prepend(list_ctx.preview_ctx.main_div);
|
58
|
|
59
|
if (list_ctx.previewed_item !== null)
|
60
|
list_ctx.previewed_item.li.classList.remove("item_li_highlight");
|
61
|
|
62
|
list_ctx.previewed_item = item;
|
63
|
item.li.classList.add("item_li_highlight");
|
64
|
|
65
|
list_ctx.preview_container.classList.remove("hide");
|
66
|
}
|
67
|
|
68
|
function insert_item(list_ctx, definition, idx)
|
69
|
{
|
70
|
const li = document.createElement("li");
|
71
|
li.innerText = definition.long_name;
|
72
|
if (idx)
|
73
|
list_ctx.items[idx - 1].li.after(li);
|
74
|
else
|
75
|
list_ctx.ul.prepend(li);
|
76
|
|
77
|
const item = {definition, li};
|
78
|
list_ctx.items.splice(idx, 0, item);
|
79
|
list_ctx.by_identifier.set(definition.identifier, item);
|
80
|
|
81
|
li.addEventListener("click", () => preview_item(list_ctx, item));
|
82
|
|
83
|
return item;
|
84
|
}
|
85
|
|
86
|
const coll = new Intl.Collator();
|
87
|
|
88
|
function item_cmp(def1, def2)
|
89
|
{
|
90
|
return coll.compare(def1.long_name, def2.long_name) ||
|
91
|
coll.compare(def1.identifier, def2.identifier);
|
92
|
}
|
93
|
|
94
|
function find_item_idx(list_ctx, definition)
|
95
|
{
|
96
|
/* Perform a binary search of item's (new or not) index in sorted array. */
|
97
|
let left = 0, right = list_ctx.items.length;
|
98
|
|
99
|
while (left < right) {
|
100
|
const mid = (left + right) >> 1;
|
101
|
if (item_cmp(definition, list_ctx.items[mid].definition) > 0)
|
102
|
left = mid + 1;
|
103
|
else /* <= 0 */
|
104
|
right = mid;
|
105
|
}
|
106
|
|
107
|
return left;
|
108
|
}
|
109
|
|
110
|
function item_changed(list_ctx, change)
|
111
|
{
|
112
|
/* Remove item. */
|
113
|
const old_item = list_ctx.by_identifier.get(change.key);
|
114
|
if (old_item !== undefined) {
|
115
|
list_ctx.items.splice(find_item_idx(list_ctx, old_item.definition), 1);
|
116
|
list_ctx.by_identifier.delete(change.key);
|
117
|
old_item.li.remove();
|
118
|
|
119
|
if (list_ctx.previewed_item === old_item) {
|
120
|
list_ctx.preview_container.classList.add("hide");
|
121
|
list_ctx.previewed_item = null;
|
122
|
}
|
123
|
}
|
124
|
|
125
|
if (change.new_val === undefined)
|
126
|
return;
|
127
|
|
128
|
const new_item = insert_item(list_ctx, change.new_val,
|
129
|
find_item_idx(list_ctx, change.new_val));
|
130
|
if (list_ctx.previewed_item === old_item)
|
131
|
preview_item(list_ctx, new_item, true);
|
132
|
}
|
133
|
|
134
|
async function remove_clicked(list_ctx)
|
135
|
{
|
136
|
if (list_ctx.dialog_ctx.shown || list_ctx.previewed_item === null)
|
137
|
return;
|
138
|
|
139
|
const identifier = list_ctx.previewed_item.definition.identifier;
|
140
|
|
141
|
if (!(await dialog.ask(list_ctx.dialog_ctx,
|
142
|
`Are you sure you want to delete '${identifier}'?`)))
|
143
|
return;
|
144
|
|
145
|
try {
|
146
|
await list_ctx.remove_cb(identifier);
|
147
|
} catch(e) {
|
148
|
console.error("Haketilo:", e);
|
149
|
dialog.error(list_ctx.dialog_ctx, `Couldn't remove '${identifier}' :(`)
|
150
|
}
|
151
|
}
|
152
|
|
153
|
async function item_list(preview_cb, track_cb, remove_cb)
|
154
|
{
|
155
|
const list_ctx = clone_template("item_list");
|
156
|
|
157
|
const [tracking, definitions] =
|
158
|
await track_cb(ch => item_changed(list_ctx, ch));
|
159
|
|
160
|
definitions.sort(item_cmp);
|
161
|
|
162
|
Object.assign(list_ctx, {
|
163
|
items: [],
|
164
|
by_identifier: new Map(),
|
165
|
tracking,
|
166
|
previewed_item: null,
|
167
|
preview_cb,
|
168
|
remove_cb,
|
169
|
dialog_ctx: dialog.make(() => on_dialog_show(list_ctx),
|
170
|
() => on_dialog_hide(list_ctx))
|
171
|
});
|
172
|
list_ctx.dialog_container.append(list_ctx.dialog_ctx.main_div);
|
173
|
|
174
|
for (const def of definitions)
|
175
|
insert_item(list_ctx, def, list_ctx.items.length);
|
176
|
|
177
|
list_ctx.remove_but
|
178
|
.addEventListener("click", () => remove_clicked(list_ctx));
|
179
|
|
180
|
return list_ctx;
|
181
|
}
|
182
|
|
183
|
function on_dialog_show(list_ctx)
|
184
|
{
|
185
|
list_ctx.ul.classList.add("list_disabled");
|
186
|
list_ctx.preview_container.classList.add("hide");
|
187
|
list_ctx.dialog_container.classList.remove("hide");
|
188
|
}
|
189
|
|
190
|
function on_dialog_hide(list_ctx)
|
191
|
{
|
192
|
list_ctx.ul.classList.remove("list_disabled");
|
193
|
if (list_ctx.previewed_item !== null)
|
194
|
list_ctx.preview_container.classList.remove("hide");
|
195
|
list_ctx.dialog_container.classList.add("hide");
|
196
|
}
|
197
|
|
198
|
async function remove_single_item(item_type, identifier)
|
199
|
{
|
200
|
const transaction_ctx =
|
201
|
await haketilodb.start_items_transaction([item_type], {});
|
202
|
await haketilodb[`remove_${item_type}`](identifier, transaction_ctx);
|
203
|
await haketilodb.finalize_transaction(transaction_ctx);
|
204
|
}
|
205
|
|
206
|
function resource_list()
|
207
|
{
|
208
|
return item_list(resource_preview, haketilodb.track.resource,
|
209
|
id => remove_single_item("resource", id));
|
210
|
}
|
211
|
#EXPORT resource_list
|
212
|
|
213
|
function mapping_list()
|
214
|
{
|
215
|
return item_list(mapping_preview, haketilodb.track.mapping,
|
216
|
id => remove_single_item("mapping", id));
|
217
|
}
|
218
|
#EXPORT mapping_list
|
219
|
|
220
|
function destroy_list(list_ctx)
|
221
|
{
|
222
|
haketilodb.untrack(list_ctx.tracking);
|
223
|
list_ctx.main_div.remove();
|
224
|
}
|
225
|
#EXPORT destroy_list
|