aboutsummaryrefslogtreecommitdiff
/**
 * This file is part of Haketilo.
 *
 * Function: Showing a list of resources/mappings in a browser.
 *
 * Copyright (C) 2022 Wojtek Kosior
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * As additional permission under GNU GPL version 3 section 7, you
 * may distribute forms of that code without the copy of the GNU
 * GPL normally required by section 4, provided you include this
 * license notice and, in case of non-source distribution, a URL
 * through which recipients can access the Corresponding Source.
 * If you modify file(s) with this exception, you may extend this
 * exception to your version of the file(s), but you are not
 * obligated to do so. If you do not wish to do so, delete this
 * exception statement from your version.
 *
 * As a special exception to the GPL, any HTML file which merely
 * makes function calls to this code, and for that purpose
 * includes it by reference shall be deemed a separate work for
 * copyright law purposes. If you modify this code, you may extend
 * this exception to your version of the code, but you are not
 * obligated to do so. If you do not wish to do so, delete this
 * exception statement from your version.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
 * license. Although I request that you do not make use of this code in a
 * proprietary program, I am not going to enforce this in court.
 */

#IMPORT common/indexeddb.js AS haketilodb
#IMPORT html/dialog.js

#FROM html/item_preview.js IMPORT resource_preview, mapping_preview
#FROM html/DOM_helpers.js IMPORT clone_template

function preview_item(list_ctx, item, ignore_dialog=false)
{
    if (list_ctx.dialog_ctx.shown && !ignore_dialog)
	return;

    list_ctx.preview_ctx =
	list_ctx.preview_cb(item.definition, list_ctx.preview_ctx);
    list_ctx.preview_container.prepend(list_ctx.preview_ctx.main_div);

    if (list_ctx.previewed_item !== null)
	list_ctx.previewed_item.li.classList.remove("item_li_highlight");

    list_ctx.previewed_item = item;
    item.li.classList.add("item_li_highlight");

    list_ctx.preview_container.classList.remove("hide");
}

function insert_item(list_ctx, definition, idx)
{
    const li = document.createElement("li");
    li.innerText = definition.long_name;
    if (idx)
	list_ctx.items[idx - 1].li.after(li);
    else
	list_ctx.ul.prepend(li);

    const item = {definition, li};
    list_ctx.items.splice(idx, 0, item);
    list_ctx.by_identifier.set(definition.identifier, item);

    li.addEventListener("click", () => preview_item(list_ctx, item));

    return item;
}

const coll = new Intl.Collator();

function item_cmp(def1, def2)
{
    return coll.compare(def1.long_name, def2.long_name) ||
	coll.compare(def1.identifier, def2.identifier);
}

function find_item_idx(list_ctx, definition)
{
    /* Perform a binary search of item's (new or not) index in sorted array. */
    let left = 0, right = list_ctx.items.length;

    while (left < right) {
	const mid = (left + right) >> 1;
	if (item_cmp(definition, list_ctx.items[mid].definition) > 0)
	    left = mid + 1;
	else /* <= 0 */
	    right = mid;
    }

    return left;
}

function item_changed(list_ctx, change)
{
    /* Remove item. */
    const old_item = list_ctx.by_identifier.get(change.key);
    if (old_item !== undefined) {
	list_ctx.items.splice(find_item_idx(list_ctx, old_item.definition), 1);
	list_ctx.by_identifier.delete(change.key);
	old_item.li.remove();

	if (list_ctx.previewed_item === old_item) {
	    list_ctx.preview_container.classList.add("hide");
	    list_ctx.previewed_item = null;
	}
    }

    if (change.new_val === undefined)
	return;

    const new_item = insert_item(list_ctx, change.new_val,
				 find_item_idx(list_ctx, change.new_val));
    if (list_ctx.previewed_item === old_item)
	preview_item(list_ctx, new_item, true);
}

async function remove_clicked(list_ctx)
{
    if (list_ctx.dialog_ctx.shown || list_ctx.previewed_item === null)
	return;

    const identifier = list_ctx.previewed_item.definition.identifier;

    if (!(await dialog.ask(list_ctx.dialog_ctx,
			   `Are you sure you want to delete '${identifier}'?`)))
	return;

    try {
	await list_ctx.remove_cb(identifier);
    } catch(e) {
	console.error("Haketilo:", e);
	dialog.error(list_ctx.dialog_ctx, `Couldn't remove '${identifier}' :(`)
    }
}

async function item_list(preview_cb, track_cb, remove_cb)
{
    const list_ctx = clone_template("item_list");

    const [tracking, definitions] =
	  await track_cb(ch => item_changed(list_ctx, ch));

    definitions.sort(item_cmp);

    Object.assign(list_ctx, {
	items: [],
	by_identifier: new Map(),
	tracking,
	previewed_item: null,
	preview_cb,
	remove_cb,
	dialog_ctx: dialog.make(() => on_dialog_show(list_ctx),
				() => on_dialog_hide(list_ctx))
    });
    list_ctx.dialog_container.append(list_ctx.dialog_ctx.main_div);

    for (const def of definitions)
	insert_item(list_ctx, def, list_ctx.items.length);

    list_ctx.remove_but
	.addEventListener("click", () => remove_clicked(list_ctx));

    return list_ctx;
}

function on_dialog_show(list_ctx)
{
    list_ctx.ul.classList.add("list_disabled");
    list_ctx.preview_container.classList.add("hide");
    list_ctx.dialog_container.classList.remove("hide");
}

function on_dialog_hide(list_ctx)
{
    list_ctx.ul.classList.remove("list_disabled");
    if (list_ctx.previewed_item !== null)
	list_ctx.preview_container.classList.remove("hide");
    list_ctx.dialog_container.classList.add("hide");
}

async function remove_single_item(item_type, identifier)
{
    const transaction_ctx =
	  await haketilodb.start_items_transaction([item_type], {});
    await haketilodb[`remove_${item_type}`](identifier, transaction_ctx);
    await haketilodb.finalize_transaction(transaction_ctx);
}

function resource_list()
{
      return item_list(resource_preview, haketilodb.track.resource,
		       id => remove_single_item("resource", id));
}
#EXPORT resource_list

function mapping_list()
{
      return item_list(mapping_preview, haketilodb.track.mapping,
		       id => remove_single_item("mapping", id));
}
#EXPORT mapping_list

function destroy_list(list_ctx)
{
    haketilodb.untrack(list_ctx.tracking);
    list_ctx.main_div.remove();
}
#EXPORT destroy_list