/* Timelinize Copyright (c) 2013 Matthew Holt This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ // AJQuery: https://github.com/coolaj86/ajquery.js (modified slightly by me) function $(sel, el) { return typeof sel === 'string' ? ((typeof el === 'string' ? $(el) : el) || document).querySelector(sel) : sel; } function $$(sel, el) { return (el || document).querySelectorAll(sel); } async function get(endpoint, data, signal) { if (typeof data == 'object') { const qs = new URLSearchParams(); for (const key in data) { qs.append(key, data[key]); } endpoint += '?'+qs.toString(); } return myFetch(signal, endpoint, undefined, 'GET'); } async function post(endpoint, data, method = 'POST') { return myFetch(null, endpoint, data, method); } async function abortablePost(signal, endpoint, data, method = 'POST') { return myFetch(signal, endpoint, data, method) } async function myFetch(signal, endpoint, data, method) { // disable form inputs if no modal is visible const formFields = $$('form input, form button, form textarea'); if ($('#modal-preview')?.offsetParent === null) { formFields.forEach((elem) => { elem.disabled = true; }); } let headers = {}; if (data) { data = JSON.stringify(data); headers['Content-Type'] = 'application/json' } return fetch(endpoint, { method: method, headers: headers, body: data, signal: signal }).then(async (response) => { if (response.ok) { if (response.headers.get('Content-Type') == 'application/json') return response.json(); else return response; } if (response.headers.get('Content-Type') == 'application/json') { throw { response: response, error: await response.json() } } throw {response: response}; }).finally(function() { // re-enable form inputs if ($('#modal-preview')?.offsetParent === null) { formFields.forEach((elem) => { elem.disabled = false; }); } }); } const app = { AddEntity(repo_id, entity) { return post("/api/add-entity", { repo_id, entity }); }, CancelJobs(repo_id, job_ids) { return post("/api/cancel-jobs", { repo_id, job_ids }); }, ChangeSettings(settings) { return post("/api/change-settings", settings); }, CloseRepository(repo_id) { return post("/api/close-repository", repo_id); }, DataSources() { return get("/api/data-sources"); }, FileStat(filename) { return post("/api/file-stat", filename); }, FileListing(path, params) { params.path = path; return post("/api/file-listing", params); }, FileSelectorRoots() { return get("/api/file-selector-roots"); }, GetEntity(repo_id, entity_id) { return post("/api/get-entity", { repo_id, entity_id }); }, GetSettings() { return get("/api/settings"); }, Import(params) { return post("/api/import", params); }, ItemClassifications(repoID) { return post("/api/item-classifications", repoID); }, Jobs(params) { return post("/api/jobs", params, "QUERY"); }, LoadConversation(params) { return post("/api/conversation", params); }, ChartStats(name, repo_id, data) { return get("/api/charts", { name, repo_id, ...data }); }, LoadRecentConversations(params) { return post("/api/recent-conversations", params); }, MergeEntities(repo_id, base_entity_id, other_entity_ids) { return post("/api/merge-entities", { repo_id, base_entity_id, other_entity_ids }); }, NextGraph(repo_id, job_id) { return get("/api/next-graph", { repo_id, job_id }); }, OpenRepository(repo_path, create) { return post("/api/open-repository", { repo_path, create }); }, OpenRepositories() { return get("/api/open-repositories"); }, PauseJob(repo_id, job_id) { return post("/api/pause-job", { repo_id, job_id }); }, PlanImport(params) { return post("/api/plan-import", params); }, Recognize(filename) { return post("/api/recognize", { filename }); }, RepositoryIsEmpty(repoID) { return post("/api/repository-empty", repoID); }, SearchItems(params) { return post("/api/search-items", params); }, SearchEntities(params) { return post("/api/search-entities", params); }, SubmitGraph(repo_id, job_id, graph, skip) { return post("/api/submit-graph", { repo_id, job_id, graph, skip }); }, StartJob(repo_id, job_id, start_over) { return post("/api/start-job", { repo_id, job_id, start_over }); }, UnpauseJob(repo_id, job_id) { return post("/api/unpause-job", { repo_id, job_id }); }, }; function store(key, val) { if (typeof val === 'object') { val = JSON.stringify(val); } // TODO: I'm not set on whether to use sessionStorage or localStorage. sessionStorage.setItem(key, val); } function load(key) { let val = sessionStorage.getItem(key); if (typeof val === 'string') { try { val = JSON.parse(val); } catch { // just leave as string } } return val; } function on(eventName, elemSelector, handler, capture) { let events = [eventName]; if (eventName.indexOf(',') >= 0 || eventName.indexOf(' ') >= 0) { eventName = eventName.replace(/,/, ' '); events = eventName.split(' '); } events.forEach(eventName => { eventName = eventName.trim() if (!eventName) return; // from youmightnotneedjquery.com document.addEventListener(eventName, function (e) { // loop parent nodes from the target to the delegation node // TODO: maybe we should also attach the closest element matching the selector, since the event bubbles up... for (let target = e.target; target && target != this; target = target.parentNode) { if (NodeList.prototype.isPrototypeOf(elemSelector)) { for (el of elemSelector) { if (el == target) { handler.call(target, e); return; } } } else if (!elemSelector || (target && target.matches(elemSelector))) { handler.call(target, e); return; } } }, capture); // I find capture=true helpful when using :not() selectors to exclude one elem of the node tree }); } function trigger(el, eventType, detail) { if (typeof el === 'string') { el = $(el); // assume it was a selector, for convenience } // from youmightnotneedjquery.com if (typeof eventType === 'string' && typeof el[eventType] === 'function') { el[eventType](); } else { const event = typeof eventType === 'string' ? new CustomEvent(eventType, { bubbles: true, cancelable: true, detail: detail }) : eventType; el.dispatchEvent(event); } } // cloneTemplate does a deep clone of the