Merge pull request #2054 from maxberger/master

UI: More reorganization of code, support browser back/forward buttons!
This commit is contained in:
Peter Bieringer
2026-03-29 07:05:54 +02:00
committed by GitHub
14 changed files with 1483 additions and 1321 deletions

View File

@@ -126,3 +126,24 @@ def test_navigation_refresh_button(
page.click('#logoutview a[data-name="refresh"]') page.click('#logoutview a[data-name="refresh"]')
# It shows LoadingScene briefly then back to CollectionsScene # It shows LoadingScene briefly then back to CollectionsScene
expect(page.locator("#collectionsscene")).to_be_visible() expect(page.locator("#collectionsscene")).to_be_visible()
def test_navigation_browser_history(
context: BrowserContext, page: Page, radicale_server: str, config: Config
) -> None:
login(page, radicale_server, config, context=context)
expect(page.locator("#collectionsscene")).to_be_visible()
# Navigate forward inside the SPA to CreateEditCollectionScene
page.click('a[data-name="new"]')
expect(page.locator("#createcollectionscene")).to_be_visible()
# Emulate browser Back button
page.go_back()
expect(page.locator("#createcollectionscene")).to_be_hidden()
expect(page.locator("#collectionsscene")).to_be_visible()
# Emulate browser Forward button
page.go_forward()
expect(page.locator("#collectionsscene")).to_be_hidden()
expect(page.locator("#createcollectionscene")).to_be_visible()

View File

@@ -191,7 +191,7 @@ main {
#collectionsscene article p { #collectionsscene article p {
font-size: 1em; font-size: 1em;
max-height: 130px; max-height: 130px;
overflow: overlay; overflow: auto;
word-wrap: break-word; word-wrap: break-word;
} }
@@ -476,7 +476,8 @@ a.blue:active {
} }
button.inline { button.inline {
padding-inline: 1px; padding-left: 1px;
padding-right: 1px;
min-width: 2em; min-width: 2em;
margin: 0; margin: 0;
} }

View File

@@ -127,13 +127,13 @@
<option value="WEBCAL">webcal</option> <option value="WEBCAL">webcal</option>
</select> </select>
<label for="displayname">Title:</label> <label for="displayname">Title:</label>
<input data-name="displayname" type="text"> <input data-name="displayname" id="displayname" type="text">
<label for="description">Description:</label> <label for="description">Description:</label>
<input data-name="description" type="text"> <input data-name="description" id="description" type="text">
<label for="source">Source:</label> <label for="source">Source:</label>
<input data-name="source" type="url"> <input data-name="source" id="source" type="url">
<label for="color">Color:</label> <label for="color">Color:</label>
<input data-name="color" type="color"> <input data-name="color" id="color" type="color">
<br> <br>
<span class="error hidden" data-name="error"></span> <span class="error hidden" data-name="error"></span>
<br> <br>
@@ -309,15 +309,15 @@
<option value="WEBCAL">Webcal</option> <option value="WEBCAL">Webcal</option>
</select> </select>
<label for="href">HREF:</label> <label for="href">HREF:</label>
<input data-name="href" type="text"> <input data-name="href" id="href" type="text">
<label for="displayname">Title:</label> <label for="displayname">Title:</label>
<input data-name="displayname" type="text"> <input data-name="displayname" id="create_displayname" type="text">
<label for="description">Description:</label> <label for="description">Description:</label>
<input data-name="description" type="text"> <input data-name="description" id="create_description" type="text">
<label for="source">Source:</label> <label for="source">Source:</label>
<input data-name="source" type="url"> <input data-name="source" id="create_source" type="url">
<label for="color">Color:</label> <label for="color">Color:</label>
<input data-name="color" type="color"> <input data-name="color" id="create_color" type="color">
<br> <br>
<span class="error" data-name="error"></span> <span class="error" data-name="error"></span>
<br> <br>
@@ -342,9 +342,9 @@
</div> </div>
<form> <form>
<label for="uploadfile">File:</label> <label for="uploadfile">File:</label>
<input data-name="uploadfile" type="file" accept=".ics, .vcf" multiple> <input data-name="uploadfile" id="uploadfile" type="file" accept=".ics, .vcf" multiple>
<label for="href">HREF:</label> <label for="upload_href">HREF:</label>
<input data-name="href" type="text"> <input data-name="href" id="upload_href" type="text">
<small data-name="hreflimitmsg" class="hidden">You can only specify the HREF if you upload 1 file.</small> <small data-name="hreflimitmsg" class="hidden">You can only specify the HREF if you upload 1 file.</small>
<button type="submit" class="green" data-name="submit">Upload</button> <button type="submit" class="green" data-name="submit">Upload</button>
<button type="button" class="red" data-name="close">Close</button> <button type="button" class="red" data-name="close">Close</button>

View File

@@ -45,250 +45,259 @@ export class CollectionsScene {
* scene is popped. * scene is popped.
*/ */
constructor(user, password, principal_collection, onerror) { constructor(user, password, principal_collection, onerror) {
/** @type {HTMLElement} */ let html_scene = get_element_by_id("collectionsscene"); this._user = user;
/** @type {HTMLElement} */ let template = get_element(html_scene, "[data-name=collectiontemplate]"); this._password = password;
/** @type {HTMLElement} */ let new_btn = get_element(html_scene, "[data-name=new]"); this._principal_collection = principal_collection;
/** @type {HTMLElement} */ let upload_btn = get_element(html_scene, "[data-name=upload]"); this._onerror = onerror;
/** @type {HTMLElement} */ let incomingshares_btn = get_element(html_scene, "[data-name=incomingshares]");
/** @type {HTMLElement} */ let error_div = get_element(html_scene, "[data-name=collectionsscene_error]");
/** @type {Array<HTMLElement>} */ let nodes = []; this._html_scene = get_element_by_id("collectionsscene");
let errorHandler = new ErrorHandler(error_div); this._template = get_element(this._html_scene, "[data-name=collectiontemplate]");
this._new_btn = get_element(this._html_scene, "[data-name=new]");
this._upload_btn = get_element(this._html_scene, "[data-name=upload]");
this._incomingshares_btn = get_element(this._html_scene, "[data-name=incomingshares]");
this._error_div = get_element(this._html_scene, "[data-name=collectionsscene_error]");
function onnew() { /** @type {Array<HTMLElement>} */ this._nodes = [];
try { this._errorHandler = new ErrorHandler(this._error_div);
let create_collection_scene = new CreateEditCollectionScene(user, password, principal_collection);
push_scene(create_collection_scene);
} catch (err) {
console.error(err);
}
return false;
}
function onupload() {
try {
let upload_scene = new UploadCollectionScene(user, password, principal_collection);
push_scene(upload_scene);
} catch (err) {
console.error(err);
}
return false;
}
function onincomingshares() {
try {
let incoming_sharing_scene = new IncomingSharingScene(user, password);
push_scene(incoming_sharing_scene);
} catch (err) {
console.error(err);
}
return false;
}
/**
* @param {Collection} collection
*/
function onedit(collection) {
try {
let edit_collection_scene = new CreateEditCollectionScene(user, password, collection);
push_scene(edit_collection_scene);
} catch (err) {
console.error(err);
}
return false;
}
/**
* @param {Collection} collection
*/
function onshare(collection) {
try {
let share_collection_scene = new ShareCollectionScene(user, password, collection);
push_scene(share_collection_scene);
} catch (err) {
console.error(err);
}
return false;
}
/**
* @param {Collection} collection
*/
function ondelete(collection) {
try {
let delete_collection_scene = new DeleteConfirmationScene(
user, password, "Delete Collection", collection, collection.displayname || collection.href,
delete_collection, true
);
push_scene(delete_collection_scene);
} catch (err) {
console.error(err);
}
return false;
}
/**
* @param {any[]} collections
* @param {import("../api/sharing.js").Share[]} shares
* @param {boolean} clear_error
*/
function show_collections(collections, shares, clear_error) {
/** @type {HTMLElement} */ let navBar = get_element(document, "#logoutview");
let heightOfNavBar = navBar.offsetHeight + "px";
html_scene.style.marginTop = heightOfNavBar;
html_scene.style.height = "calc(100vh - " + heightOfNavBar + ")";
if (clear_error) {
errorHandler.clearError();
}
// Clear old nodes
nodes.forEach(function (node) {
if (node.parentNode) {
node.parentNode.removeChild(node);
}
});
nodes = [];
collections.forEach(function (/** @type {Collection} */ collection) {
/** @type {HTMLElement} */ let node = /** @type {HTMLElement} */(template.cloneNode(true));
node.classList.remove("hidden");
/** @type {HTMLElement} */ let title_form = get_element(node, "[data-name=title]");
/** @type {HTMLElement} */ let description_form = get_element(node, "[data-name=description]");
/** @type {HTMLElement} */ let contentcount_form = get_element(node, "[data-name=contentcount]");
/** @type {HTMLInputElement} */ let url_form = /** @type {HTMLInputElement} */ (get_element(node, "[data-name=url]"));
/** @type {HTMLElement} */ let color_form = get_element(node, "[data-name=color]");
/** @type {HTMLElement} */ let delete_btn = get_element(node, "[data-name=delete]");
/** @type {HTMLElement} */ let edit_btn = get_element(node, "[data-name=edit]");
/** @type {HTMLElement} */ let share_btn = get_element(node, "[data-name=share]");
/** @type {HTMLAnchorElement} */ let download_btn = /** @type {HTMLAnchorElement} */ (get_element(node, "[data-name=download]"));
if (collection.color) {
color_form.style.background = collection.color;
}
let possible_types = [CollectionType.ADDRESSBOOK, CollectionType.WEBCAL];
[CollectionType.CALENDAR, ""].forEach(function (e) {
[CollectionType.union(e, CollectionType.JOURNAL), e].forEach(function (e) {
[CollectionType.union(e, CollectionType.TASKS), e].forEach(function (e) {
if (e) {
possible_types.push(e);
}
});
});
});
possible_types.forEach(function (e) {
if (e !== collection.type) {
get_element(node, "[data-name=" + e + "]").classList.add("hidden");
}
});
let share_info = get_element(node, "[data-name=shared-by]");
let transformed_from = get_element(node, "[data-name=transformed-from]");
let share = (shares || []).find(
s => (s.ShareType === "map") &&
(s.PathOrToken || "").replace(/\/+$/, "") === (collection.href || "").replace(/\/+$/, ""));
if (share) {
if (share.Owner !== user) {
share_info.classList.remove("hidden");
get_element(node, "[data-name=shared-by-owner]").textContent = share.Owner;
} else {
transformed_from.classList.remove("hidden");
}
let share_option = get_element(node, "[data-name=shareoption]");
if (share_option) {
share_option.classList.add("hidden");
share_option.removeAttribute("data-name");
}
delete_btn.classList.add("hidden");
if (!/w/i.test(share.Permissions || "")) {
edit_btn.classList.add("hidden");
} else {
edit_btn.classList.remove("hidden");
}
}
title_form.textContent = collection.displayname || collection.href;
if (title_form.textContent.length > 30) {
title_form.classList.add("smalltext");
}
description_form.textContent = collection.description;
if (description_form.textContent.length > 150) {
description_form.classList.add("smalltext");
}
if (collection.type != CollectionType.WEBCAL) {
let contentcount_form_txt = (collection.contentcount > 0 ? Number(collection.contentcount).toLocaleString() : "No") + " item" + (collection.contentcount == 1 ? "" : "s") + " in collection";
if (collection.contentcount > 0) {
contentcount_form_txt += " (" + bytesToHumanReadable(collection.size) + ")";
}
contentcount_form.textContent = contentcount_form_txt;
}
let href = SERVER + collection.href;
url_form.value = href;
download_btn.href = href;
download_btn.onclick = function (event) {
event.preventDefault();
let auth = get_auth_header(user, password);
let headers = auth ? {
'Authorization': auth
} : undefined;
fetch(href, { headers: headers }).then(function (response) {
if (response.ok) {
return response.blob();
}
throw new Error("Download failed: " + response.statusText);
}).then(function (blob) {
let url = window.URL.createObjectURL(blob);
let a = document.createElement("a");
a.href = url;
a.download = (collection.displayname || collection.href).replace(/\/+$/, "") + (collection.type === CollectionType.ADDRESSBOOK ? ".vcf" : ".ics");
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
})["catch"](function (error) {
errorHandler.setError(error.message);
});
};
if (collection.type == CollectionType.WEBCAL) {
if (download_btn.parentElement) {
download_btn.parentElement.classList.add("hidden");
}
}
delete_btn.onclick = function () { return ondelete(collection); };
edit_btn.onclick = function () { return onedit(collection); };
share_btn.onclick = function () { return onshare(collection); };
node.classList.remove("hidden");
nodes.push(node);
if (template.parentNode) {
template.parentNode.insertBefore(node, template);
}
});
}
this.errorwrapper = function (/** @type {string} */ error) {
errorHandler.setError(error);
if (onerror) onerror(error);
};
this.show = function () {
html_scene.classList.remove("hidden");
new_btn.onclick = onnew;
upload_btn.onclick = onupload;
incomingshares_btn.onclick = onincomingshares;
collectionsCache.getChildCollections(user, password, principal_collection, this.errorwrapper, show_collections);
collectionsCache.getServerFeatures(user, password, this.errorwrapper, maybe_enable_sharing_options);
};
this.hide = function () {
html_scene.classList.add("hidden");
new_btn.onclick = null;
upload_btn.onclick = null;
incomingshares_btn.onclick = null;
// remove collection
nodes.forEach(function (node) {
if (node.parentNode) {
node.parentNode.removeChild(node);
}
});
nodes = [];
};
this.release = function () {
};
} }
_onnew() {
try {
let create_collection_scene = new CreateEditCollectionScene(this._user, this._password, this._principal_collection);
push_scene(create_collection_scene);
} catch (err) {
console.error(err);
}
return false;
}
_onupload() {
try {
let upload_scene = new UploadCollectionScene(this._user, this._password, this._principal_collection);
push_scene(upload_scene);
} catch (err) {
console.error(err);
}
return false;
}
_onincomingshares() {
try {
let incoming_sharing_scene = new IncomingSharingScene(this._user, this._password);
push_scene(incoming_sharing_scene);
} catch (err) {
console.error(err);
}
return false;
}
/**
* @param {Collection} collection
*/
_onedit(collection) {
try {
let edit_collection_scene = new CreateEditCollectionScene(this._user, this._password, collection);
push_scene(edit_collection_scene);
} catch (err) {
console.error(err);
}
return false;
}
/**
* @param {Collection} collection
*/
_onshare(collection) {
try {
let share_collection_scene = new ShareCollectionScene(this._user, this._password, collection);
push_scene(share_collection_scene);
} catch (err) {
console.error(err);
}
return false;
}
/**
* @param {Collection} collection
*/
_ondelete(collection) {
try {
let delete_collection_scene = new DeleteConfirmationScene(
this._user, this._password, "Delete Collection", collection, collection.displayname || collection.href,
delete_collection, true
);
push_scene(delete_collection_scene);
} catch (err) {
console.error(err);
}
return false;
}
/**
* @param {any[]} collections
* @param {import("../api/sharing.js").Share[]} shares
* @param {boolean} clear_error
*/
_show_collections(collections, shares, clear_error) {
/** @type {HTMLElement} */ let navBar = get_element(document, "#logoutview");
let heightOfNavBar = navBar.offsetHeight + "px";
this._html_scene.style.marginTop = heightOfNavBar;
this._html_scene.style.height = "calc(100vh - " + heightOfNavBar + ")";
if (clear_error) {
this._errorHandler.clearError();
}
// Clear old nodes
this._nodes.forEach(function (node) {
if (node.parentNode) {
node.parentNode.removeChild(node);
}
});
this._nodes = [];
collections.forEach((/** @type {Collection} */ collection) => {
/** @type {HTMLElement} */ let node = /** @type {HTMLElement} */(this._template.cloneNode(true));
node.classList.remove("hidden");
/** @type {HTMLElement} */ let title_form = get_element(node, "[data-name=title]");
/** @type {HTMLElement} */ let description_form = get_element(node, "[data-name=description]");
/** @type {HTMLElement} */ let contentcount_form = get_element(node, "[data-name=contentcount]");
/** @type {HTMLInputElement} */ let url_form = /** @type {HTMLInputElement} */ (get_element(node, "[data-name=url]"));
/** @type {HTMLElement} */ let color_form = get_element(node, "[data-name=color]");
/** @type {HTMLElement} */ let delete_btn = get_element(node, "[data-name=delete]");
/** @type {HTMLElement} */ let edit_btn = get_element(node, "[data-name=edit]");
/** @type {HTMLElement} */ let share_btn = get_element(node, "[data-name=share]");
/** @type {HTMLAnchorElement} */ let download_btn = /** @type {HTMLAnchorElement} */ (get_element(node, "[data-name=download]"));
if (collection.color) {
color_form.style.background = collection.color;
}
let possible_types = [CollectionType.ADDRESSBOOK, CollectionType.WEBCAL];
[CollectionType.CALENDAR, ""].forEach(function (e) {
[CollectionType.union(e, CollectionType.JOURNAL), e].forEach(function (e) {
[CollectionType.union(e, CollectionType.TASKS), e].forEach(function (e) {
if (e) {
possible_types.push(e);
}
});
});
});
possible_types.forEach(function (e) {
if (e !== collection.type) {
get_element(node, "[data-name=" + e + "]").classList.add("hidden");
}
});
let share_info = get_element(node, "[data-name=shared-by]");
let transformed_from = get_element(node, "[data-name=transformed-from]");
let share = (shares || []).find(
s => (s.ShareType === "map") &&
(s.PathOrToken || "").replace(/\/+$/, "") === (collection.href || "").replace(/\/+$/, ""));
if (share) {
if (share.Owner !== this._user) {
share_info.classList.remove("hidden");
get_element(node, "[data-name=shared-by-owner]").textContent = share.Owner;
} else {
transformed_from.classList.remove("hidden");
}
let share_option = get_element(node, "[data-name=shareoption]");
if (share_option) {
share_option.classList.add("hidden");
share_option.removeAttribute("data-name");
}
delete_btn.classList.add("hidden");
if (!/w/i.test(share.Permissions || "")) {
edit_btn.classList.add("hidden");
} else {
edit_btn.classList.remove("hidden");
}
}
title_form.textContent = collection.displayname || collection.href;
if (title_form.textContent.length > 30) {
title_form.classList.add("smalltext");
}
description_form.textContent = collection.description;
if (description_form.textContent.length > 150) {
description_form.classList.add("smalltext");
}
if (collection.type != CollectionType.WEBCAL) {
let contentcount_form_txt = (collection.contentcount > 0 ? Number(collection.contentcount).toLocaleString() : "No") + " item" + (collection.contentcount == 1 ? "" : "s") + " in collection";
if (collection.contentcount > 0) {
contentcount_form_txt += " (" + bytesToHumanReadable(collection.size) + ")";
}
contentcount_form.textContent = contentcount_form_txt;
}
let href = SERVER + collection.href;
url_form.value = href;
download_btn.href = href;
download_btn.onclick = (event) => {
event.preventDefault();
let auth = get_auth_header(this._user, this._password);
let headers = auth ? {
'Authorization': auth
} : undefined;
fetch(href, { headers: headers }).then(function (response) {
if (response.ok) {
return response.blob();
}
throw new Error("Download failed: " + response.statusText);
}).then(function (blob) {
let url = window.URL.createObjectURL(blob);
let a = document.createElement("a");
a.href = url;
a.download = (collection.displayname || collection.href).replace(/\/+$/, "") + (collection.type === CollectionType.ADDRESSBOOK ? ".vcf" : ".ics");
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
})["catch"]((error) => {
this._errorHandler.setError(error.message);
});
};
if (collection.type == CollectionType.WEBCAL) {
if (download_btn.parentElement) {
download_btn.parentElement.classList.add("hidden");
}
}
delete_btn.onclick = () => { return this._ondelete(collection); };
edit_btn.onclick = () => { return this._onedit(collection); };
share_btn.onclick = () => { return this._onshare(collection); };
node.classList.remove("hidden");
this._nodes.push(node);
if (this._template.parentNode) {
this._template.parentNode.insertBefore(node, this._template);
}
});
}
_errorwrapper(/** @type {string} */ error) {
this._errorHandler.setError(error);
if (this._onerror) this._onerror(error);
}
show() {
this._html_scene.classList.remove("hidden");
this._new_btn.onclick = () => this._onnew();
this._upload_btn.onclick = () => this._onupload();
this._incomingshares_btn.onclick = () => this._onincomingshares();
collectionsCache.getChildCollections(this._user, this._password, this._principal_collection, (e) => this._errorwrapper(e), (c, s, ce) => this._show_collections(c, s, ce));
collectionsCache.getServerFeatures(this._user, this._password, (e) => this._errorwrapper(e), maybe_enable_sharing_options);
}
hide() {
this._html_scene.classList.add("hidden");
this._new_btn.onclick = null;
this._upload_btn.onclick = null;
this._incomingshares_btn.onclick = null;
// remove collection
this._nodes.forEach(function (node) {
if (node.parentNode) {
node.parentNode.removeChild(node);
}
});
this._nodes = [];
}
release() {
}
is_transient() { return false; }
} }

View File

@@ -41,181 +41,187 @@ export class CreateEditCollectionScene {
* Otherwise the collection will be edited. * Otherwise the collection will be edited.
*/ */
constructor(user, password, collection) { constructor(user, password, collection) {
let edit = collection.type !== CollectionType.PRINCIPAL; this._user = user;
let html_scene = get_element_by_id(edit ? "editcollectionscene" : "createcollectionscene"); this._password = password;
/** @type {?HTMLElement} */ let title_form = edit ? get_element(html_scene, "[data-name=title]") : null; this._collection = collection;
/** @type {HTMLElement} */ let error_form = get_element(html_scene, "[data-name=error]"); this._edit = collection.type !== CollectionType.PRINCIPAL;
/** @type {?HTMLInputElement} */ let href_form = edit ? null : /** @type {HTMLInputElement} */(get_element(html_scene, "[data-name=href]"));
/** @type {HTMLInputElement} */ let displayname_form = /** @type {HTMLInputElement} */(get_element(html_scene, "[data-name=displayname]"));
/** @type {HTMLInputElement} */ let description_form = /** @type {HTMLInputElement} */(get_element(html_scene, "[data-name=description]"));
/** @type {HTMLInputElement} */ let source_form = /** @type {HTMLInputElement} */(get_element(html_scene, "[data-name=source]"));
/** @type {HTMLElement} */ let source_label = get_element(html_scene, "label[for=source]");
/** @type {HTMLSelectElement} */ let type_form = /** @type {HTMLSelectElement} */(get_element(html_scene, "[data-name=type]"));
/** @type {HTMLInputElement} */ let color_form = /** @type {HTMLInputElement} */(get_element(html_scene, "[data-name=color]"));
/** @type {HTMLElement} */ let submit_btn = get_element(html_scene, "[data-name=submit]");
/** @type {HTMLElement} */ let cancel_btn = get_element(html_scene, "[data-name=cancel]");
this._html_scene = get_element_by_id(this._edit ? "editcollectionscene" : "createcollectionscene");
this._title_form = this._edit ? get_element(this._html_scene, "[data-name=title]") : null;
this._error_form = get_element(this._html_scene, "[data-name=error]");
this._href_form = this._edit ? null : /** @type {HTMLInputElement} */(get_element(this._html_scene, "[data-name=href]"));
this._displayname_form = /** @type {HTMLInputElement} */(get_element(this._html_scene, "[data-name=displayname]"));
this._description_form = /** @type {HTMLInputElement} */(get_element(this._html_scene, "[data-name=description]"));
this._source_form = /** @type {HTMLInputElement} */(get_element(this._html_scene, "[data-name=source]"));
this._source_label = get_element(this._html_scene, "label[for$=source]");
this._type_form = /** @type {HTMLSelectElement} */(get_element(this._html_scene, "[data-name=type]"));
this._color_form = /** @type {HTMLInputElement} */(get_element(this._html_scene, "[data-name=color]"));
this._submit_btn = get_element(this._html_scene, "[data-name=submit]");
this._cancel_btn = get_element(this._html_scene, "[data-name=cancel]");
/** @type {?XMLHttpRequest} */ let create_edit_req = null; /** @type {?XMLHttpRequest} */ this._create_edit_req = null;
/** @type {?HTMLSelectElement} */ let saved_type_form = null; /** @type {?HTMLSelectElement} */ this._saved_type_form = null;
let errorHandler = new ErrorHandler(error_form); this._errorHandler = new ErrorHandler(this._error_form);
let validator = new FormValidator(errorHandler); this._validator = new FormValidator(this._errorHandler);
if (!edit && href_form) { if (!this._edit && this._href_form) {
validator.addValidator(href_form, validate_href(href_form, "HREF")); this._validator.addValidator(this._href_form, validate_href(this._href_form, "HREF"));
} }
validator.addValidator(color_form, validate_color(color_form, "Color")); this._validator.addValidator(this._color_form, validate_color(this._color_form, "Color"));
let href = edit ? collection.href : collection.href + random_uuid() + "/"; this._href = this._edit ? collection.href : collection.href + random_uuid() + "/";
let displayname = edit ? collection.displayname : ""; this._displayname = this._edit ? collection.displayname : "";
let description = edit ? collection.description : ""; this._description = this._edit ? collection.description : "";
let source = edit ? collection.source : ""; this._source = this._edit ? collection.source : "";
let type = edit ? collection.type : CollectionType.CALENDAR_JOURNAL_TASKS; this._type = this._edit ? collection.type : CollectionType.CALENDAR_JOURNAL_TASKS;
let color = edit && collection.color ? collection.color : "#" + random_hex(6); this._color = this._edit && collection.color ? collection.color : "#" + random_hex(6);
if (!edit && href_form) { if (!this._edit && this._href_form) {
href_form.addEventListener("input", onCleanHREFinput); this._href_form.addEventListener("input", onCleanHREFinput);
} }
}
function remove_invalid_types() { _remove_invalid_types() {
if (!edit) { if (!this._edit) {
return; return;
}
/** @type {HTMLOptionsCollection} */ let options = this._type_form.options;
let valid_type_options = CollectionType.valid_options_for_type(this._type);
for (let i = options.length - 1; i >= 0; i--) {
if (valid_type_options.indexOf(options[i].value) < 0) {
options.remove(i);
} }
/** @type {HTMLOptionsCollection} */ let options = type_form.options; }
// remove all options that are not supersets }
let valid_type_options = CollectionType.valid_options_for_type(type);
for (let i = options.length - 1; i >= 0; i--) { _read_form() {
if (valid_type_options.indexOf(options[i].value) < 0) { if (!this._edit && this._href_form) {
options.remove(i); cleanHREFinput(this._href_form);
let newhreftxtvalue = this._href_form.value.trim().toLowerCase();
this._href = this._collection.href + newhreftxtvalue + "/";
}
this._displayname = this._displayname_form.value;
this._description = this._description_form.value;
this._source = this._source_form.value;
this._type = this._type_form.value;
this._color = this._color_form.value;
return true;
}
_fill_form() {
if (!this._edit && this._href_form) {
this._href_form.value = random_uuid();
}
this._displayname_form.value = this._displayname;
this._description_form.value = this._description;
this._source_form.value = this._source;
this._type_form.value = this._type;
this._color_form.value = this._color;
this._onTypeChange(null);
this._type_form.addEventListener("change", (e) => this._onTypeChange(e));
}
_onsubmit() {
try {
if (!this._validator.validate()) {
return false;
}
this._read_form();
let sane_color = this._color.trim();
if (sane_color) {
/** @type {?RegExpExecArray} */ let match = COLOR_RE.exec(sane_color);
if (match) {
sane_color = match[1];
} }
} }
} let loading_scene = new LoadingScene();
push_scene(loading_scene);
function read_form() { let collection = new Collection(this._href, this._type, this._displayname, this._description, sane_color, 0, 0, this._source);
if (!edit && href_form) { let callback = (/** @type {?string} */ error1) => {
cleanHREFinput(href_form); if (!is_current_scene(loading_scene)) {
let newhreftxtvalue = href_form.value.trim().toLowerCase(); return;
href = collection.href + newhreftxtvalue + "/";
}
displayname = displayname_form.value;
description = description_form.value;
source = source_form.value;
type = type_form.value;
color = color_form.value;
return true;
}
function fill_form() {
if (!edit && href_form) {
href_form.value = random_uuid();
}
displayname_form.value = displayname;
description_form.value = description;
source_form.value = source;
type_form.value = type;
color_form.value = color;
onTypeChange(null);
type_form.addEventListener("change", onTypeChange);
}
function onsubmit() {
try {
if (!validator.validate()) {
return false;
} }
read_form(); this._create_edit_req = null;
let sane_color = color.trim(); if (error1) {
if (sane_color) { this._errorHandler.setError(error1);
/** @type {?RegExpExecArray} */ let match = COLOR_RE.exec(sane_color); pop_scene();
if (match) {
sane_color = match[1];
}
}
let loading_scene = new LoadingScene();
push_scene(loading_scene);
let collection = new Collection(href, type, displayname, description, sane_color, 0, 0, source);
let callback = function (/** @type {?string} */ error1) {
if (!is_current_scene(loading_scene)) {
return;
}
create_edit_req = null;
if (error1) {
errorHandler.setError(error1);
pop_scene();
} else {
collectionsCache.invalidate();
pop_to_parent();
}
};
if (edit) {
create_edit_req = edit_collection(user, password, collection, callback);
} else { } else {
create_edit_req = create_collection(user, password, collection, callback); collectionsCache.invalidate();
pop_to_parent();
} }
} catch (err) { };
console.error(err); if (this._edit) {
} this._create_edit_req = edit_collection(this._user, this._password, collection, callback);
return false;
}
function oncancel() {
try {
pop_scene();
} catch (err) {
console.error(err);
}
return false;
}
/**
* @param {?Event} _e
*/
function onTypeChange(_e) {
if (type_form.value == CollectionType.WEBCAL) {
source_label.classList.remove("hidden");
source_form.classList.remove("hidden");
} else { } else {
source_label.classList.add("hidden"); this._create_edit_req = create_collection(this._user, this._password, collection, callback);
source_form.classList.add("hidden");
} }
} catch (err) {
console.error(err);
} }
return false;
}
this.show = function () { _oncancel() {
this.release(); try {
// Clone type_form because it's impossible to hide options without removing them pop_scene();
saved_type_form = type_form; } catch (err) {
type_form = /** @type {HTMLSelectElement} */ (type_form.cloneNode(true)); console.error(err);
if (saved_type_form.parentNode) { }
saved_type_form.parentNode.replaceChild(type_form, saved_type_form); return false;
} }
remove_invalid_types();
html_scene.classList.remove("hidden"); /**
if (edit && title_form) { * @param {?Event} _e
title_form.textContent = collection.displayname || collection.href; */
} _onTypeChange(_e) {
fill_form(); if (this._type_form.value == CollectionType.WEBCAL) {
submit_btn.onclick = onsubmit; this._source_label.classList.remove("hidden");
cancel_btn.onclick = oncancel; this._source_form.classList.remove("hidden");
validator.validate(); } else {
}; this._source_label.classList.add("hidden");
this.hide = function () { this._source_form.classList.add("hidden");
read_form(); }
html_scene.classList.add("hidden"); }
// restore type_form
if (type_form.parentNode && saved_type_form) { show() {
type_form.parentNode.replaceChild(saved_type_form, type_form); this.release();
type_form = saved_type_form; // Clone type_form because it's impossible to hide options without removing them
} this._saved_type_form = this._type_form;
saved_type_form = null; this._type_form = /** @type {HTMLSelectElement} */ (this._type_form.cloneNode(true));
submit_btn.onclick = null; if (this._saved_type_form.parentNode) {
cancel_btn.onclick = null; this._saved_type_form.parentNode.replaceChild(this._type_form, this._saved_type_form);
}; }
this.release = function () { this._remove_invalid_types();
if (create_edit_req !== null) { this._html_scene.classList.remove("hidden");
create_edit_req.abort(); if (this._edit && this._title_form) {
create_edit_req = null; this._title_form.textContent = this._collection.displayname || this._collection.href;
} }
}; this._fill_form();
this._submit_btn.onclick = () => this._onsubmit();
this._cancel_btn.onclick = () => this._oncancel();
this._validator.validate();
}
hide() {
this._read_form();
this._html_scene.classList.add("hidden");
// restore type_form
if (this._type_form.parentNode && this._saved_type_form) {
this._type_form.parentNode.replaceChild(this._saved_type_form, this._type_form);
this._type_form = this._saved_type_form;
}
this._saved_type_form = null;
this._submit_btn.onclick = null;
this._cancel_btn.onclick = null;
}
is_transient() { return false; }
release() {
if (this._create_edit_req !== null) {
this._create_edit_req.abort();
this._create_edit_req = null;
}
} }
} }

View File

@@ -39,331 +39,337 @@ export class CreateEditShareScene {
* @param {Share} [share] If provided, the scene will be in edit mode. * @param {Share} [share] If provided, the scene will be in edit mode.
*/ */
constructor(user, password, collection, shareType, share) { constructor(user, password, collection, shareType, share) {
let self = this; this._user = user;
let edit = !!share; this._password = password;
let pathMapped = collection.href; this._collection = collection;
/** @type {HTMLElement} */ let html_scene = get_element_by_id("newshare"); this._shareType = shareType;
/** @type {HTMLFormElement} */ let form = /** @type {HTMLFormElement} */ (get_element(html_scene, "form")); this._share = share;
/** @type {HTMLElement} */ let sharemapfields = get_element(html_scene, "[data-name=sharemapfields]"); this._edit = !!share;
/** @type {HTMLInputElement} */ let shareuser_input = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=shareuser]")); this._pathMapped = collection.href;
/** @type {HTMLInputElement} */ let sharehref_input = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=sharehref]"));
/** @type {HTMLInputElement} */ let enabled_checkbox = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=enabled]"));
/** @type {HTMLInputElement} */ let hidden_checkbox = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=hidden]"));
let permissions_ro_radio = /** @type {HTMLInputElement} */ (get_element_by_id("newshare_attr_permissions_ro"));
let permissions_rw_radio = /** @type {HTMLInputElement} */ (get_element_by_id("newshare_attr_permissions_rw"));
/** @type {HTMLDetailsElement} */ let conversions_details = /** @type {HTMLDetailsElement} */ (get_element(html_scene, "[data-name=conversions]"));
/** @type {HTMLElement} */ let conversions_container = get_element(html_scene, "[data-name=conversions_container]");
/** @type {HTMLDetailsElement} */ let properties_fieldset = /** @type {HTMLDetailsElement} */ (get_element(html_scene, "[data-name=properties_override]")); this._html_scene = get_element_by_id("newshare");
/** @type {HTMLInputElement} */ let displayname_override_enabled = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=displayname_override_enabled]")); this._form = /** @type {HTMLFormElement} */ (get_element(this._html_scene, "form"));
/** @type {HTMLInputElement} */ let displayname_override_input = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=displayname_override]")); this._sharemapfields = get_element(this._html_scene, "[data-name=sharemapfields]");
/** @type {HTMLInputElement} */ let description_override_enabled = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=description_override_enabled]")); this._shareuser_input = /** @type {HTMLInputElement} */ (get_element(this._html_scene, "[data-name=shareuser]"));
/** @type {HTMLInputElement} */ let description_override_input = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=description_override]")); this._sharehref_input = /** @type {HTMLInputElement} */ (get_element(this._html_scene, "[data-name=sharehref]"));
/** @type {HTMLInputElement} */ let color_override_enabled = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=color_override_enabled]")); this._enabled_checkbox = /** @type {HTMLInputElement} */ (get_element(this._html_scene, "[data-name=enabled]"));
/** @type {HTMLInputElement} */ let color_override_input = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=color_override]")); this._hidden_checkbox = /** @type {HTMLInputElement} */ (get_element(this._html_scene, "[data-name=hidden]"));
this._permissions_ro_radio = /** @type {HTMLInputElement} */ (get_element_by_id("newshare_attr_permissions_ro"));
this._permissions_rw_radio = /** @type {HTMLInputElement} */ (get_element_by_id("newshare_attr_permissions_rw"));
this._conversions_details = /** @type {HTMLDetailsElement} */ (get_element(this._html_scene, "[data-name=conversions]"));
this._conversions_container = get_element(this._html_scene, "[data-name=conversions_container]");
/** @type {HTMLElement} */ let error_form = get_element(html_scene, "[data-name=error]"); this._properties_fieldset = /** @type {HTMLDetailsElement} */ (get_element(this._html_scene, "[data-name=properties_override]"));
/** @type {HTMLElement} */ let submit_btn = get_element(html_scene, "[data-name=submit]"); this._displayname_override_enabled = /** @type {HTMLInputElement} */ (get_element(this._html_scene, "[data-name=displayname_override_enabled]"));
/** @type {HTMLElement} */ let cancel_btn = get_element(html_scene, "[data-name=cancel]"); this._displayname_override_input = /** @type {HTMLInputElement} */ (get_element(this._html_scene, "[data-name=displayname_override]"));
this._description_override_enabled = /** @type {HTMLInputElement} */ (get_element(this._html_scene, "[data-name=description_override_enabled]"));
this._description_override_input = /** @type {HTMLInputElement} */ (get_element(this._html_scene, "[data-name=description_override]"));
this._color_override_enabled = /** @type {HTMLInputElement} */ (get_element(this._html_scene, "[data-name=color_override_enabled]"));
this._color_override_input = /** @type {HTMLInputElement} */ (get_element(this._html_scene, "[data-name=color_override]"));
let errorHandler = new ErrorHandler(error_form); this._error_form = get_element(this._html_scene, "[data-name=error]");
let map_validator = new FormValidator(errorHandler); this._submit_btn = get_element(this._html_scene, "[data-name=submit]");
this._cancel_btn = get_element(this._html_scene, "[data-name=cancel]");
map_validator.addValidator(shareuser_input, function () { this._errorHandler = new ErrorHandler(this._error_form);
let conversion = get_selected_conversion(); this._map_validator = new FormValidator(this._errorHandler);
if (conversion === "bday") {
return validate_non_empty(shareuser_input, "Share User")(); this._map_validator.addValidator(this._shareuser_input, () => {
let conversion = this._get_selected_conversion();
if (conversion != "none") {
return validate_non_empty(this._shareuser_input, "Share User")();
} else { } else {
return validate_not_empty_or_equals(shareuser_input, user, "Share User")(); return validate_not_empty_or_equals(this._shareuser_input, user, "Share User")();
} }
}); });
map_validator.addValidator(sharehref_input, validate_href(sharehref_input, "Share Href")); this._map_validator.addValidator(this._sharehref_input, validate_href(this._sharehref_input, "Share Href"));
sharehref_input.addEventListener("input", onCleanHREFinput); this._sharehref_input.addEventListener("input", onCleanHREFinput);
displayname_override_enabled.onchange = function () { this._displayname_override_enabled.onchange = () => {
displayname_override_input.disabled = !displayname_override_enabled.checked; this._displayname_override_input.disabled = !this._displayname_override_enabled.checked;
}; };
description_override_enabled.onchange = function () { this._description_override_enabled.onchange = () => {
description_override_input.disabled = !description_override_enabled.checked; this._description_override_input.disabled = !this._description_override_enabled.checked;
}; };
color_override_enabled.onchange = function () { this._color_override_enabled.onchange = () => {
color_override_input.disabled = !color_override_enabled.checked; this._color_override_input.disabled = !this._color_override_enabled.checked;
};
function get_selected_conversion() {
/** @type {HTMLInputElement | null} */
let checked = conversions_container.querySelector("input[name=conversion]:checked");
return checked ? checked.value : "none";
}
function on_conversion_change() {
let conversion = get_selected_conversion();
if (conversion === "bday") {
permissions_ro_radio.checked = true;
permissions_rw_radio.checked = false;
permissions_ro_radio.disabled = true;
permissions_rw_radio.disabled = true;
enabled_checkbox.checked = true;
hidden_checkbox.checked = false;
} else {
permissions_ro_radio.disabled = false;
permissions_rw_radio.disabled = false;
}
if (shareType === "map") {
map_validator.validate();
}
}
function oncancel() {
try {
pop_scene();
} catch (err) {
console.error(err);
}
return false;
}
function onsubmit() {
try {
if (shareType === "map") {
if (!map_validator.validate()) {
return false;
}
}
let conversion = get_selected_conversion();
let is_birthday = conversion === "bday";
let enabled_by_owner = is_birthday ? true : enabled_checkbox.checked;
let hidden_by_owner = is_birthday ? false : hidden_checkbox.checked;
let permissions = is_birthday ? "r" : (permissions_rw_radio.checked ? "rw" : "r");
/** @type {string} */ let conversion_value = conversion;
/** @type {Object<string, string>} */ let properties = {};
if (displayname_override_enabled.checked) {
let key = get_property_key(collection.type, "DISPLAYNAME");
if (key) properties[key] = displayname_override_input.value;
}
if (description_override_enabled.checked) {
let key = get_property_key(collection.type, "DESCRIPTION");
if (key) properties[key] = description_override_input.value;
}
if (color_override_enabled.checked) {
let key = get_property_key(collection.type, "COLOR");
if (key) properties[key] = color_override_input.value + (color_override_input.value ? "ff" : "");
}
let callback = function (/** @type {?string} */ error) {
if (!is_current_scene(self)) {
return;
}
if (error) {
errorHandler.setError(error);
} else {
// On any share-to-self (currently only bday conversion), invalidate the
// collections cache so the virtual calendar appears immediately.
if (shareuser_input.value === user) {
collectionsCache.invalidate();
}
pop_scene();
}
};
let new_share = new Share({
ShareType: shareType,
PathMapped: pathMapped,
Permissions: permissions,
EnabledByOwner: enabled_by_owner,
EnabledByUser: (edit && share) ? share.EnabledByUser : null,
HiddenByOwner: hidden_by_owner,
HiddenByUser: (edit && share) ? share.HiddenByUser : null,
Properties: properties,
User: (edit && share) ? share.User : shareuser_input.value,
PathOrToken: (edit && share) ? share.PathOrToken : (shareType === "map" ? "/" + shareuser_input.value + "/" + sharehref_input.value + "/" : ""),
Conversion: conversion_value,
});
if (edit) {
if (shareType === "map") {
update_share_by_map(user, password, new_share, callback);
} else {
update_share_by_token(user, password, new_share, callback);
}
} else {
if (shareType === "map") {
add_share_by_map(user, password, new_share, callback);
} else {
add_share_by_token(user, password, new_share, callback);
}
}
} catch (err) {
console.error(err);
}
return false;
}
this.show = function () {
this.release();
html_scene.classList.remove("hidden");
html_scene.querySelectorAll("details").forEach(function (details) {
if (details.dataset.name !== "properties_override") {
details.open = true;
} else {
details.open = false;
}
});
cancel_btn.onclick = oncancel;
form.onsubmit = onsubmit;
/** @type {HTMLHeadingElement} */ let title = /** @type {HTMLHeadingElement} */ (get_element(html_scene, "h1"));
title.textContent = edit ? "Edit Share" : "New Share";
submit_btn.textContent = edit ? "Save" : "Create";
shareuser_input.value = (edit && share) ? share.User : "";
shareuser_input.disabled = edit;
enabled_checkbox.checked = (edit && share && share.EnabledByOwner !== null) ? share.EnabledByOwner : true;
hidden_checkbox.checked = (edit && share && share.HiddenByOwner !== null) ? share.HiddenByOwner : false;
let initial_conversion = (edit && share && share.Conversion) ? share.Conversion : "none";
collectionsCache.getServerFeatures(user, password, errorHandler.setError, (features) => {
conversions_container.innerHTML = "";
let supported = (features.sharing && features.sharing.SupportedConversions) || [];
if (supported.length > 0) {
let options = ["none", ...supported.filter(opt => opt !== "none")];
options.forEach(opt => {
let id = "newshare_conv_" + opt;
let radio = document.createElement("input");
radio.type = "radio";
radio.name = "conversion";
radio.value = opt;
radio.id = id;
radio.checked = (opt === initial_conversion);
radio.onchange = on_conversion_change;
let label = document.createElement("label");
label.htmlFor = id;
if (opt === "bday") {
label.textContent = "Birthday";
} else {
label.textContent = opt.charAt(0).toUpperCase() + opt.slice(1);
}
conversions_container.appendChild(radio);
conversions_container.appendChild(label);
});
let is_addressbook = collection.type === CollectionType.ADDRESSBOOK;
if (is_addressbook) {
conversions_details.classList.remove("hidden");
conversions_details.open = true;
} else {
conversions_details.classList.add("hidden");
}
} else {
conversions_details.classList.add("hidden");
}
on_conversion_change();
});
let displayname = collection.displayname || "";
let description = collection.description || "";
let color = collection.color || "#ffffff";
let displayname_override_enabled_value = false;
let description_override_enabled_value = false;
let color_override_enabled_value = false;
if (edit && share && share.Properties) {
let displayname_key = get_property_key(collection.type, "DISPLAYNAME");
if (displayname_key && share.Properties[displayname_key] !== undefined) {
displayname = share.Properties[displayname_key];
displayname_override_enabled_value = true;
}
let description_key = get_property_key(collection.type, "DESCRIPTION");
if (description_key && share.Properties[description_key] !== undefined) {
description = share.Properties[description_key];
description_override_enabled_value = true;
}
let color_key = get_property_key(collection.type, "COLOR");
if (color_key && share.Properties[color_key] !== undefined) {
color = share.Properties[color_key];
if (color.length === 9 && color.endsWith("ff")) {
color = color.substring(0, 7);
}
color_override_enabled_value = true;
}
}
displayname_override_enabled.checked = displayname_override_enabled_value;
displayname_override_input.value = displayname;
displayname_override_input.disabled = !displayname_override_enabled_value;
description_override_enabled.checked = description_override_enabled_value;
description_override_input.value = description;
description_override_input.disabled = !description_override_enabled_value;
color_override_enabled.checked = color_override_enabled_value;
color_override_input.value = color;
color_override_input.disabled = !color_override_enabled_value;
properties_fieldset.classList.remove("hidden");
if (displayname_override_enabled_value || description_override_enabled_value || color_override_enabled_value) {
properties_fieldset.open = true;
}
let displayname_key = get_property_key(collection.type, "DISPLAYNAME");
if (!displayname_key) {
if (displayname_override_enabled.parentElement) {
displayname_override_enabled.parentElement.classList.add("hidden");
}
} else {
if (displayname_override_enabled.parentElement) {
displayname_override_enabled.parentElement.classList.remove("hidden");
}
}
let description_key = get_property_key(collection.type, "DESCRIPTION");
if (!description_key) {
if (description_override_enabled.parentElement) {
description_override_enabled.parentElement.classList.add("hidden");
}
} else {
if (description_override_enabled.parentElement) {
description_override_enabled.parentElement.classList.remove("hidden");
}
}
let color_key = get_property_key(collection.type, "COLOR");
if (!color_key) {
if (color_override_enabled.parentElement) {
color_override_enabled.parentElement.classList.add("hidden");
}
} else {
if (color_override_enabled.parentElement) {
color_override_enabled.parentElement.classList.remove("hidden");
}
}
if (shareType === "map") {
if (edit && share) {
sharehref_input.value = share.PathOrToken.split("/").filter(Boolean).pop() || "";
} else {
sharehref_input.value = random_uuid();
}
sharehref_input.disabled = edit;
sharemapfields.classList.remove("hidden");
map_validator.validate();
} else {
sharehref_input.value = "";
sharemapfields.classList.add("hidden");
errorHandler.clearError();
}
};
this.hide = function () {
html_scene.classList.add("hidden");
cancel_btn.onclick = null;
form.onsubmit = null;
};
this.release = function () {
}; };
} }
_get_selected_conversion() {
/** @type {HTMLInputElement | null} */
let checked = this._conversions_container.querySelector("input[name=conversion]:checked");
return checked ? checked.value : "none";
}
_on_conversion_change() {
let conversion = this._get_selected_conversion();
if (conversion != "none") {
this._permissions_ro_radio.disabled = true;
this._permissions_rw_radio.disabled = true;
this._enabled_checkbox.checked = true;
this._hidden_checkbox.checked = false;
} else {
this._permissions_ro_radio.disabled = false;
this._permissions_rw_radio.disabled = false;
}
if (this._shareType === "map") {
this._map_validator.validate();
}
}
_oncancel() {
try {
pop_scene();
} catch (err) {
console.error(err);
}
return false;
}
_onsubmit() {
try {
if (this._shareType === "map") {
if (!this._map_validator.validate()) {
return false;
}
}
let conversion = this._get_selected_conversion();
let is_conversion = conversion != "none";
let enabled_by_owner = is_conversion ? true : this._enabled_checkbox.checked;
let hidden_by_owner = is_conversion ? false : this._hidden_checkbox.checked;
let permissions = is_conversion ? "r" : (this._permissions_rw_radio.checked ? "rw" : "r");
/** @type {string} */ let conversion_value = conversion;
/** @type {Object<string, string>} */ let properties = {};
if (this._displayname_override_enabled.checked) {
let key = get_property_key(this._collection.type, "DISPLAYNAME");
if (key) properties[key] = this._displayname_override_input.value;
}
if (this._description_override_enabled.checked) {
let key = get_property_key(this._collection.type, "DESCRIPTION");
if (key) properties[key] = this._description_override_input.value;
}
if (this._color_override_enabled.checked) {
let key = get_property_key(this._collection.type, "COLOR");
if (key) properties[key] = this._color_override_input.value + (this._color_override_input.value ? "ff" : "");
}
let callback = (/** @type {?string} */ error) => {
if (!is_current_scene(this)) {
return;
}
if (error) {
this._errorHandler.setError(error);
} else {
// On any share-to-self (currently only bday conversion), invalidate the
// collections cache so the virtual calendar appears immediately.
if (this._shareuser_input.value === this._user) {
collectionsCache.invalidate();
}
pop_scene();
}
};
let new_share = new Share({
ShareType: this._shareType,
PathMapped: this._pathMapped,
Permissions: permissions,
EnabledByOwner: enabled_by_owner,
EnabledByUser: (this._edit && this._share) ? this._share.EnabledByUser : null,
HiddenByOwner: hidden_by_owner,
HiddenByUser: (this._edit && this._share) ? this._share.HiddenByUser : null,
Properties: properties,
User: (this._edit && this._share) ? this._share.User : this._shareuser_input.value,
PathOrToken: (this._edit && this._share) ? this._share.PathOrToken : (this._shareType === "map" ? "/" + this._shareuser_input.value + "/" + this._sharehref_input.value + "/" : ""),
Conversion: conversion_value,
});
if (this._edit) {
if (this._shareType === "map") {
update_share_by_map(this._user, this._password, new_share, callback);
} else {
update_share_by_token(this._user, this._password, new_share, callback);
}
} else {
if (this._shareType === "map") {
add_share_by_map(this._user, this._password, new_share, callback);
} else {
add_share_by_token(this._user, this._password, new_share, callback);
}
}
} catch (err) {
console.error(err);
}
return false;
}
show() {
this.release();
this._html_scene.classList.remove("hidden");
this._html_scene.querySelectorAll("details").forEach(function (details) {
if (details.dataset.name !== "properties_override") {
details.open = true;
} else {
details.open = false;
}
});
this._cancel_btn.onclick = () => this._oncancel();
this._form.onsubmit = () => this._onsubmit();
/** @type {HTMLHeadingElement} */ let title = /** @type {HTMLHeadingElement} */ (get_element(this._html_scene, "h1"));
title.textContent = this._edit ? "Edit Share" : "New Share";
this._submit_btn.textContent = this._edit ? "Save" : "Create";
this._shareuser_input.value = (this._edit && this._share) ? this._share.User : "";
this._shareuser_input.disabled = this._edit;
this._enabled_checkbox.checked = (this._edit && this._share && this._share.EnabledByOwner !== null) ? this._share.EnabledByOwner : true;
this._hidden_checkbox.checked = (this._edit && this._share && this._share.HiddenByOwner !== null) ? this._share.HiddenByOwner : false;
let initial_conversion = (this._edit && this._share && this._share.Conversion) ? this._share.Conversion : "none";
collectionsCache.getServerFeatures(this._user, this._password, this._errorHandler.setError, (features) => {
this._conversions_container.innerHTML = "";
let supported = (features.sharing && features.sharing.SupportedConversions) || [];
if (supported.length > 0) {
let options = ["none", ...supported.filter(opt => opt !== "none")];
options.forEach(opt => {
let id = "newshare_conv_" + opt;
let radio = document.createElement("input");
radio.type = "radio";
radio.name = "conversion";
radio.value = opt;
radio.id = id;
radio.checked = (opt === initial_conversion);
radio.onchange = () => this._on_conversion_change();
let label = document.createElement("label");
label.htmlFor = id;
if (opt === "bday") {
label.textContent = "Birthday";
} else {
label.textContent = opt.charAt(0).toUpperCase() + opt.slice(1);
}
this._conversions_container.appendChild(radio);
this._conversions_container.appendChild(label);
});
let is_addressbook = this._collection.type === CollectionType.ADDRESSBOOK;
if (is_addressbook) {
this._conversions_details.classList.remove("hidden");
this._conversions_details.open = true;
} else {
this._conversions_details.classList.add("hidden");
}
} else {
this._conversions_details.classList.add("hidden");
}
this._on_conversion_change();
});
let displayname = this._collection.displayname || "";
let description = this._collection.description || "";
let color = this._collection.color || "#ffffff";
let displayname_override_enabled_value = false;
let description_override_enabled_value = false;
let color_override_enabled_value = false;
if (this._edit && this._share && this._share.Properties) {
let displayname_key = get_property_key(this._collection.type, "DISPLAYNAME");
if (displayname_key && this._share.Properties[displayname_key] !== undefined) {
displayname = this._share.Properties[displayname_key];
displayname_override_enabled_value = true;
}
let description_key = get_property_key(this._collection.type, "DESCRIPTION");
if (description_key && this._share.Properties[description_key] !== undefined) {
description = this._share.Properties[description_key];
description_override_enabled_value = true;
}
let color_key = get_property_key(this._collection.type, "COLOR");
if (color_key && this._share.Properties[color_key] !== undefined) {
color = this._share.Properties[color_key];
if (color.length === 9 && color.endsWith("ff")) {
color = color.substring(0, 7);
}
color_override_enabled_value = true;
}
}
this._displayname_override_enabled.checked = displayname_override_enabled_value;
this._displayname_override_input.value = displayname;
this._displayname_override_input.disabled = !displayname_override_enabled_value;
this._description_override_enabled.checked = description_override_enabled_value;
this._description_override_input.value = description;
this._description_override_input.disabled = !description_override_enabled_value;
this._color_override_enabled.checked = color_override_enabled_value;
this._color_override_input.value = color;
this._color_override_input.disabled = !color_override_enabled_value;
this._properties_fieldset.classList.remove("hidden");
if (displayname_override_enabled_value || description_override_enabled_value || color_override_enabled_value) {
this._properties_fieldset.open = true;
}
let displayname_key = get_property_key(this._collection.type, "DISPLAYNAME");
if (!displayname_key) {
if (this._displayname_override_enabled.parentElement) {
this._displayname_override_enabled.parentElement.classList.add("hidden");
}
} else {
if (this._displayname_override_enabled.parentElement) {
this._displayname_override_enabled.parentElement.classList.remove("hidden");
}
}
let description_key = get_property_key(this._collection.type, "DESCRIPTION");
if (!description_key) {
if (this._description_override_enabled.parentElement) {
this._description_override_enabled.parentElement.classList.add("hidden");
}
} else {
if (this._description_override_enabled.parentElement) {
this._description_override_enabled.parentElement.classList.remove("hidden");
}
}
let color_key = get_property_key(this._collection.type, "COLOR");
if (!color_key) {
if (this._color_override_enabled.parentElement) {
this._color_override_enabled.parentElement.classList.add("hidden");
}
} else {
if (this._color_override_enabled.parentElement) {
this._color_override_enabled.parentElement.classList.remove("hidden");
}
}
if (this._shareType === "map") {
if (this._edit && this._share) {
this._sharehref_input.value = this._share.PathOrToken.split("/").filter(Boolean).pop() || "";
} else {
this._sharehref_input.value = random_uuid();
}
this._sharehref_input.disabled = this._edit;
this._sharemapfields.classList.remove("hidden");
this._map_validator.validate();
} else {
this._sharehref_input.value = "";
this._sharemapfields.classList.add("hidden");
this._errorHandler.clearError();
}
}
hide() {
this._html_scene.classList.add("hidden");
this._cancel_btn.onclick = null;
this._form.onsubmit = null;
}
release() {
}
is_transient() { return false; }
} }

View File

@@ -42,103 +42,115 @@ export class DeleteConfirmationScene {
* @param {function} [on_success] * @param {function} [on_success]
*/ */
constructor(user, password, header_title, item, item_title, delete_action, needsconfirmation, on_success) { constructor(user, password, header_title, item, item_title, delete_action, needsconfirmation, on_success) {
/** @type {HTMLElement} */ let html_scene = get_element_by_id("deleteconfirmationscene"); this._user = user;
/** @type {HTMLElement} */ let header_html = get_element(html_scene, "[data-name=headertitle]"); this._password = password;
this._item = item;
this._item_title = item_title;
this._delete_action = delete_action;
this._needsconfirmation = needsconfirmation;
this._on_success = on_success;
this._html_scene = get_element_by_id("deleteconfirmationscene");
/** @type {HTMLElement} */ let header_html = get_element(this._html_scene, "[data-name=headertitle]");
if (header_html) header_html.textContent = header_title; if (header_html) header_html.textContent = header_title;
/** @type {HTMLElement} */ let title_form = get_element(html_scene, "[data-name=title]"); this._title_form = get_element(this._html_scene, "[data-name=title]");
/** @type {HTMLElement} */ let error_form = get_element(html_scene, "[data-name=error]"); this._error_form = get_element(this._html_scene, "[data-name=error]");
/** @type {HTMLElement} */ let confirmation_prompt = get_element(html_scene, "[data-name=confirmationprompt]"); this._confirmation_prompt = get_element(this._html_scene, "[data-name=confirmationprompt]");
/** @type {HTMLInputElement} */ let confirmation_txt = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=confirmationtxt]")); this._confirmation_txt = /** @type {HTMLInputElement} */ (get_element(this._html_scene, "[data-name=confirmationtxt]"));
/** @type {HTMLElement} */ let delete_confirmation_lbl = get_element(html_scene, "[data-name=deleteconfirmationtext]"); this._delete_confirmation_lbl = get_element(this._html_scene, "[data-name=deleteconfirmationtext]");
/** @type {HTMLElement} */ let delete_btn = get_element(html_scene, "[data-name=delete]"); this._delete_btn = get_element(this._html_scene, "[data-name=delete]");
/** @type {HTMLElement} */ let cancel_btn = get_element(html_scene, "[data-name=cancel]"); this._cancel_btn = get_element(this._html_scene, "[data-name=cancel]");
if (needsconfirmation) { if (needsconfirmation) {
delete_confirmation_lbl.innerHTML = DELETE_CONFIRMATION_TEXT; this._delete_confirmation_lbl.innerHTML = DELETE_CONFIRMATION_TEXT;
confirmation_txt.value = ""; this._confirmation_txt.value = "";
confirmation_txt.addEventListener("keydown", onkeydown); this._confirmation_txt.addEventListener("keydown", (e) => this._onkeydown(e));
confirmation_prompt.classList.remove("hidden"); this._confirmation_prompt.classList.remove("hidden");
confirmation_txt.classList.remove("hidden"); this._confirmation_txt.classList.remove("hidden");
} else { } else {
confirmation_prompt.classList.add("hidden"); this._confirmation_prompt.classList.add("hidden");
confirmation_txt.classList.add("hidden"); this._confirmation_txt.classList.add("hidden");
confirmation_txt.removeEventListener("keydown", onkeydown);
} }
/** @type {?XMLHttpRequest} */ let delete_req = null; /** @type {?XMLHttpRequest} */ this._delete_req = null;
let errorHandler = new ErrorHandler(error_form); this._errorHandler = new ErrorHandler(this._error_form);
let validator = new FormValidator(errorHandler); this._validator = new FormValidator(this._errorHandler);
if (needsconfirmation) { if (needsconfirmation) {
validator.addValidator(confirmation_txt, validate_equals(confirmation_txt, DELETE_CONFIRMATION_TEXT, "confirmation")); this._validator.addValidator(this._confirmation_txt, validate_equals(this._confirmation_txt, DELETE_CONFIRMATION_TEXT, "confirmation"));
} }
}
function ondelete() { _ondelete() {
if (needsconfirmation && !validator.validate()) { if (this._needsconfirmation && !this._validator.validate()) {
return false; return false;
} }
try { try {
let loading_scene = new LoadingScene(); let loading_scene = new LoadingScene();
push_scene(loading_scene); push_scene(loading_scene);
delete_req = delete_action(user, password, item, function (/** @type {?string} */ error1) { this._delete_req = this._delete_action(this._user, this._password, this._item, (/** @type {?string} */ error1) => {
if (!is_current_scene(loading_scene)) { if (!is_current_scene(loading_scene)) {
return; return;
} }
delete_req = null; this._delete_req = null;
if (error1) { if (error1) {
pop_scene(); pop_scene();
errorHandler.setError(error1); this._errorHandler.setError(error1);
} else {
pop_scene();
if (this._on_success) {
this._on_success();
} else { } else {
collectionsCache.invalidate();
pop_scene(); pop_scene();
if (on_success) {
on_success();
} else {
collectionsCache.invalidate();
pop_scene();
}
} }
}); }
} catch (err) { });
console.error(err); } catch (err) {
} console.error(err);
return false;
} }
return false;
}
function oncancel() { _oncancel() {
try { try {
pop_scene(); pop_scene();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
}
return false;
} }
return false;
}
function onkeydown(/** @type {KeyboardEvent}*/ event) { /** @param {KeyboardEvent} event */
if (event.code !== "Enter") { _onkeydown(event) {
return; if (event.code !== "Enter") {
} return;
ondelete();
} }
this._ondelete();
}
this.show = function () { show() {
this.release(); this.release();
html_scene.classList.remove("hidden"); this._html_scene.classList.remove("hidden");
title_form.textContent = item_title; this._title_form.textContent = this._item_title;
delete_btn.onclick = ondelete; this._delete_btn.onclick = () => this._ondelete();
cancel_btn.onclick = oncancel; this._cancel_btn.onclick = () => this._oncancel();
validator.validate(); this._validator.validate();
}; }
this.hide = function () {
html_scene.classList.add("hidden"); hide() {
cancel_btn.onclick = null; this._html_scene.classList.add("hidden");
delete_btn.onclick = null; this._cancel_btn.onclick = null;
}; this._delete_btn.onclick = null;
this.release = function () { }
if (delete_req !== null) {
delete_req.abort(); is_transient() { return false; }
delete_req = null;
} release() {
}; if (this._delete_req !== null) {
this._delete_req.abort();
this._delete_req = null;
}
} }
} }

View File

@@ -35,136 +35,136 @@ export class IncomingSharingScene {
* @param {?string} password * @param {?string} password
*/ */
constructor(user, password) { constructor(user, password) {
/** @type {HTMLElement} */ let html_scene = get_element_by_id("incomingsharingscene"); this._user = user;
/** @type {HTMLElement} */ let cancel_btn = get_element(html_scene, "[data-name=cancel]"); this._password = password;
/** @type {HTMLElement} */ let error_element = get_element(html_scene, "[data-name=error]");
/** @type {HTMLElement} */ let tbody = get_element(html_scene, "tbody[data-name=incomingsharesbody]");
/** @type {HTMLElement} */ let template = get_element(tbody, "[data-name=incomingsharerowtemplate]");
/** @type {HTMLElement} */ let table = get_element(html_scene, "table");
/** @type {HTMLElement} */ let noshares_message = get_element(html_scene, "[data-name=nosharesmessage]");
let error_handler = new ErrorHandler(error_element); this._html_scene = get_element_by_id("incomingsharingscene");
this._cancel_btn = get_element(this._html_scene, "[data-name=cancel]");
this._error_element = get_element(this._html_scene, "[data-name=error]");
this._tbody = get_element(this._html_scene, "tbody[data-name=incomingsharesbody]");
this._template = get_element(this._tbody, "[data-name=incomingsharerowtemplate]");
this._table = get_element(this._html_scene, "table");
this._noshares_message = get_element(this._html_scene, "[data-name=nosharesmessage]");
/** @type {Array<HTMLElement>} */ let nodes = []; this._error_handler = new ErrorHandler(this._error_element);
function on_cancel() { /** @type {Array<HTMLElement>} */ this._nodes = [];
pop_scene(); }
}
/** /**
* @param {import("../api/sharing.js").Share} share * @param {import("../api/sharing.js").Share} share
* @param {HTMLElement} node * @param {HTMLElement} node
*/ */
function toggle_share(share, node) { _toggle_share(share, node) {
let enabled_cb = /** @type {HTMLInputElement} */ (node.querySelector("[data-name=enabled]")); let enabled_cb = /** @type {HTMLInputElement} */ (node.querySelector("[data-name=enabled]"));
let shown_cb = /** @type {HTMLInputElement} */ (node.querySelector("[data-name=shown]")); let shown_cb = /** @type {HTMLInputElement} */ (node.querySelector("[data-name=shown]"));
// disable checkboxes while updating enabled_cb.disabled = true;
enabled_cb.disabled = true; shown_cb.disabled = true;
shown_cb.disabled = true;
let old_enabled = share.EnabledByUser || false; let old_enabled = share.EnabledByUser || false;
let old_hidden = share.HiddenByUser || false; let old_hidden = share.HiddenByUser || false;
share.EnabledByUser = enabled_cb.checked; share.EnabledByUser = enabled_cb.checked;
share.HiddenByUser = !shown_cb.checked; share.HiddenByUser = !shown_cb.checked;
error_handler.clearError(); this._error_handler.clearError();
update_incoming_share(user, password, share, function (error) { update_incoming_share(this._user, this._password, share, (error) => {
enabled_cb.disabled = false; enabled_cb.disabled = false;
shown_cb.disabled = !share.EnabledByUser; shown_cb.disabled = !share.EnabledByUser;
if (error) { if (error) {
error_handler.setError(error); this._error_handler.setError(error);
enabled_cb.checked = old_enabled; enabled_cb.checked = old_enabled;
shown_cb.checked = !old_hidden; shown_cb.checked = !old_hidden;
shown_cb.disabled = !old_enabled; shown_cb.disabled = !old_enabled;
} else {
collectionsCache.invalidate();
}
});
}
/**
* @param {import("../api/sharing.js").Share[]} shares
*/
function render_shares(shares) {
// clear old nodes
nodes.forEach(function (node) {
if (node.parentNode) {
node.parentNode.removeChild(node);
}
});
nodes = [];
let prefix = "/" + user + "/";
let filtered_shares = shares.filter(
share => (share.ShareType === "map")
&& share.PathOrToken.startsWith(prefix));
if (filtered_shares.length === 0) {
table.classList.add("hidden");
noshares_message.classList.remove("hidden");
} else { } else {
table.classList.remove("hidden"); collectionsCache.invalidate();
noshares_message.classList.add("hidden"); }
});
}
/**
* @param {import("../api/sharing.js").Share[]} shares
*/
_render_shares(shares) {
// clear old nodes
this._nodes.forEach(function (node) {
if (node.parentNode) {
node.parentNode.removeChild(node);
}
});
this._nodes = [];
let prefix = "/" + this._user + "/";
let filtered_shares = shares.filter(
share => (share.ShareType === "map")
&& share.PathOrToken.startsWith(prefix));
if (filtered_shares.length === 0) {
this._table.classList.add("hidden");
this._noshares_message.classList.remove("hidden");
} else {
this._table.classList.remove("hidden");
this._noshares_message.classList.add("hidden");
}
filtered_shares.forEach((share) => {
let node = /** @type {HTMLElement} */(this._template.cloneNode(true));
node.classList.remove("hidden");
let pathortoken = /** @type {HTMLInputElement} */ (get_element(node, "[data-name=pathortoken]"));
let owner_td = get_element(node, "[data-name=owner]");
let permissions_td = /** @type {HTMLElement} */ (get_element(node, "[data-name=permissions]"));
let enabled_cb = /** @type {HTMLInputElement} */ (get_element(node, "[data-name=enabled]"));
let shown_cb = /** @type {HTMLInputElement} */ (get_element(node, "[data-name=shown]"));
let displayPath = share.PathOrToken.substring(prefix.length);
if (displayPath.endsWith("/")) {
displayPath = displayPath.substring(0, displayPath.length - 1);
} }
filtered_shares.forEach(function (share) { pathortoken.value = displayPath;
let node = /** @type {HTMLElement} */(template.cloneNode(true)); owner_td.textContent = share.Owner;
node.classList.remove("hidden"); displayPermissionsOrConversion(share.Conversion, share.Permissions, permissions_td);
let pathortoken = /** @type {HTMLInputElement} */ (get_element(node, "[data-name=pathortoken]")); let enabled = share.EnabledByUser !== null ? share.EnabledByUser : true;
let owner_td = get_element(node, "[data-name=owner]"); let shown = share.HiddenByUser !== null ? !share.HiddenByUser : true;
let permissions_td = /** @type {HTMLElement} */ (get_element(node, "[data-name=permissions]"));
let enabled_cb = /** @type {HTMLInputElement} */ (get_element(node, "[data-name=enabled]"));
let shown_cb = /** @type {HTMLInputElement} */ (get_element(node, "[data-name=shown]"));
let displayPath = share.PathOrToken.substring(prefix.length); enabled_cb.checked = enabled;
if (displayPath.endsWith("/")) { shown_cb.checked = shown;
displayPath = displayPath.substring(0, displayPath.length - 1); shown_cb.disabled = !enabled;
}
pathortoken.value = displayPath; enabled_cb.onchange = () => { this._toggle_share(share, node); };
owner_td.textContent = share.Owner; shown_cb.onchange = () => { this._toggle_share(share, node); };
displayPermissionsOrConversion(share.Conversion, share.Permissions, permissions_td);
let enabled = share.EnabledByUser !== null ? share.EnabledByUser : true; this._nodes.push(node);
let shown = share.HiddenByUser !== null ? !share.HiddenByUser : true; this._tbody.appendChild(node);
});
}
enabled_cb.checked = enabled; show() {
shown_cb.checked = shown; this._html_scene.classList.remove("hidden");
shown_cb.disabled = !enabled; this._cancel_btn.onclick = () => pop_scene();
this._error_handler.clearError();
collectionsCache.getIncomingShares(this._user, this._password, this._error_handler.setError, (shares) => this._render_shares(shares));
}
enabled_cb.onchange = function () { toggle_share(share, node); }; hide() {
shown_cb.onchange = function () { toggle_share(share, node); }; this._html_scene.classList.add("hidden");
this._cancel_btn.onclick = null;
this._error_handler.clearError();
}
nodes.push(node); is_transient() { return false; }
tbody.appendChild(node);
});
}
this.show = function () { release() {
html_scene.classList.remove("hidden"); this._error_handler.clearError();
cancel_btn.onclick = on_cancel; this._nodes.forEach(function (node) {
error_handler.clearError(); if (node.parentNode) {
collectionsCache.getIncomingShares(user, password, error_handler.setError, render_shares); node.parentNode.removeChild(node);
}; }
});
this.hide = function () { this._nodes = [];
html_scene.classList.add("hidden");
cancel_btn.onclick = null;
error_handler.clearError();
};
this.release = function () {
error_handler.clearError();
nodes.forEach(function (node) {
if (node.parentNode) {
node.parentNode.removeChild(node);
}
});
nodes = [];
};
} }
} }

View File

@@ -36,4 +36,5 @@ export class LoadingScene {
this.html_scene.classList.add("hidden"); this.html_scene.classList.add("hidden");
} }
release() { } release() { }
is_transient() { return true; }
} }

View File

@@ -31,165 +31,167 @@ import { LoadingScene } from "./LoadingScene.js";
import { Scene, is_current_scene, pop_scene, pop_to_root, push_scene, replace_scene } from "./scene_manager.js"; import { Scene, is_current_scene, pop_scene, pop_to_root, push_scene, replace_scene } from "./scene_manager.js";
/** /**
* @constructor
* @implements {Scene} * @implements {Scene}
*/ */
export class LoginScene { export class LoginScene {
constructor() { constructor() {
/** @type {HTMLElement} */ let html_scene = get_element_by_id("loginscene"); this._html_scene = get_element_by_id("loginscene");
/** @type {HTMLElement} */ let form = get_element(html_scene, "[data-name=form]"); this._form = get_element(this._html_scene, "[data-name=form]");
/** @type {HTMLInputElement} */ let user_form = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=user]")); this._user_form = /** @type {HTMLInputElement} */ (get_element(this._html_scene, "[data-name=user]"));
/** @type {HTMLInputElement} */ let password_form = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=password]")); this._password_form = /** @type {HTMLInputElement} */ (get_element(this._html_scene, "[data-name=password]"));
/** @type {HTMLElement} */ let error_form = get_element(html_scene, "[data-name=error]"); this._error_form = get_element(this._html_scene, "[data-name=error]");
/** @type {HTMLElement} */ let logout_view = get_element_by_id("logoutview"); this._logout_view = get_element_by_id("logoutview");
/** @type {HTMLElement} */ let logout_user_form = get_element(logout_view, "[data-name=user]"); this._logout_user_form = get_element(this._logout_view, "[data-name=user]");
/** @type {HTMLElement} */ let logout_btn = get_element(logout_view, "[data-name=logout]"); this._logout_btn = get_element(this._logout_view, "[data-name=logout]");
/** @type {HTMLElement} */ let refresh_btn = get_element(logout_view, "[data-name=refresh]"); this._refresh_btn = get_element(this._logout_view, "[data-name=refresh]");
let user = ""; this._user = "";
/** @type {?XMLHttpRequest} */ let principal_req = null; /** @type {?XMLHttpRequest} */ this._principal_req = null;
let errorHandler = new ErrorHandler(error_form); this._errorHandler = new ErrorHandler(this._error_form);
let validator = new FormValidator(errorHandler); this._validator = new FormValidator(this._errorHandler);
validator.addValidator(user_form, validate_non_empty(user_form, "Username")); this._validator.addValidator(this._user_form, validate_non_empty(this._user_form, "Username"));
}
function read_form() { _read_form() {
user = user_form.value; this._user = this._user_form.value;
}
_fill_form() {
this._user_form.value = this._user;
this._password_form.value = "";
}
/**
* @param {string} p_user
* @param {?string} p_password
*/
_perform_login(p_user, p_password) {
this._user = p_user;
this._fill_form();
// setup logout
this._logout_view.classList.remove("hidden");
if (p_password === null) {
this._logout_btn.classList.add("hidden");
} else {
this._logout_btn.classList.remove("hidden");
} }
this._logout_btn.onclick = () => this._onlogout();
function fill_form() { this._refresh_btn.onclick = () => this._refresh();
user_form.value = user; this._logout_user_form.textContent = this._user + "'s Collections";
password_form.value = ""; // Fetch principal
} let loading_scene = new LoadingScene();
push_scene(loading_scene);
/** this._principal_req = get_principal(this._user, p_password, (/** @type {?Collection} */ principal_collection, error1) => {
* @param {string} p_user if (!is_current_scene(loading_scene)) {
* @param {?string} p_password return;
*/
function perform_login(p_user, p_password) {
user = p_user;
fill_form();
// setup logout
logout_view.classList.remove("hidden");
if (p_password === null) {
logout_btn.classList.add("hidden");
} else {
logout_btn.classList.remove("hidden");
} }
logout_btn.onclick = onlogout; this._principal_req = null;
refresh_btn.onclick = refresh; if (error1) {
logout_user_form.textContent = user + "'s Collections"; this._errorHandler.setError(error1);
// Fetch principal pop_scene();
let loading_scene = new LoadingScene(); } else if (principal_collection) {
push_scene(loading_scene); // show collections
principal_req = get_principal(user, p_password, function (/** @type {?Collection} */ principal_collection, error1) { let saved_user = this._user;
if (!is_current_scene(loading_scene)) { this._user = "";
return; let collections_scene = new CollectionsScene(
} saved_user, p_password, principal_collection, (/** @type {?string} */ error1) => {
principal_req = null; this._errorHandler.setError(error1);
if (error1) { this._user = saved_user;
errorHandler.setError(error1);
pop_scene();
} else if (principal_collection) {
// show collections
let saved_user = user;
user = "";
let collections_scene = new CollectionsScene(
saved_user, p_password, principal_collection, function (/** @type {?string} */ error1) {
errorHandler.setError(error1);
user = saved_user;
});
replace_scene(collections_scene);
}
});
}
function onlogin() {
try {
collectionsCache.invalidate();
read_form();
let password = password_form.value;
if (!validator.validate()) {
return false;
}
perform_login(user, password);
} catch (err) {
console.error(err);
}
return false;
}
let onlogout = function () {
try {
collectionsCache.invalidate();
user = "";
pop_to_root();
} catch (err) {
console.error(err);
}
return false;
};
function remove_logout() {
logout_view.classList.add("hidden");
logout_btn.onclick = null;
refresh_btn.onclick = null;
logout_user_form.textContent = "";
}
function refresh() {
// The easiest way to refresh is to push a LoadingScene onto the stack and then pop it
// forcing the scene below it, the Collections Scene to refresh itself.
collectionsCache.invalidate();
push_scene(new LoadingScene());
pop_scene();
}
this.show = function () {
remove_logout();
fill_form();
form.onsubmit = onlogin;
html_scene.classList.remove("hidden");
user_form.focus();
// Probe for existing authentication (e.g. X-Remote-User)
// Use fetch with credentials: 'omit' to avoid browser login prompt on 401
fetch(SERVER + ROOT_PATH, {
method: 'PROPFIND',
headers: { 'Depth': '0' },
credentials: 'omit'
}).then(function (response) {
if (response.ok) {
// Authenticated! Now it's safe to call get_principal
get_principal(null, null, function (/** @type {?Collection} */ principal_collection, error) {
if (!error && principal_collection) {
let authenticated_user = principal_collection.displayname;
if (!authenticated_user) {
let href = principal_collection.href.replace(/\/+$/, "");
if (href && href !== ROOT_PATH.replace(/\/+$/, "")) {
authenticated_user = href.substring(href.lastIndexOf("/") + 1);
}
}
perform_login(authenticated_user, null);
}
}); });
} replace_scene(collections_scene);
})["catch"](function () {
// Ignore error: we are not authenticated or something else went wrong
});
};
this.hide = function () {
read_form();
html_scene.classList.add("hidden");
form.onsubmit = null;
};
this.release = function () {
// cancel pending requests
if (principal_req !== null) {
principal_req.abort();
principal_req = null;
} }
remove_logout(); });
}; }
_onlogin() {
try {
collectionsCache.invalidate();
this._read_form();
let password = this._password_form.value;
if (!this._validator.validate()) {
return false;
}
this._perform_login(this._user, password);
} catch (err) {
console.error(err);
}
return false;
}
_onlogout() {
try {
collectionsCache.invalidate();
this._user = "";
pop_to_root();
} catch (err) {
console.error(err);
}
return false;
}
_remove_logout() {
this._logout_view.classList.add("hidden");
this._logout_btn.onclick = null;
this._refresh_btn.onclick = null;
this._logout_user_form.textContent = "";
}
_refresh() {
// The easiest way to refresh is to push a LoadingScene onto the stack and then pop it
// forcing the scene below it, the Collections Scene to refresh itself.
collectionsCache.invalidate();
push_scene(new LoadingScene());
pop_scene();
}
show() {
this._remove_logout();
this._fill_form();
this._form.onsubmit = () => this._onlogin();
this._html_scene.classList.remove("hidden");
this._user_form.focus();
// Probe for existing authentication (e.g. X-Remote-User)
// Use fetch with credentials: 'omit' to avoid browser login prompt on 401
fetch(SERVER + ROOT_PATH, {
method: 'PROPFIND',
headers: { 'Depth': '0' },
credentials: 'omit'
}).then((response) => {
if (response.ok) {
// Authenticated! Now it's safe to call get_principal
get_principal(null, null, (/** @type {?Collection} */ principal_collection, error) => {
if (!error && principal_collection) {
let authenticated_user = principal_collection.displayname;
if (!authenticated_user) {
let href = principal_collection.href.replace(/\/+$/, "");
if (href && href !== ROOT_PATH.replace(/\/+$/, "")) {
authenticated_user = href.substring(href.lastIndexOf("/") + 1);
}
}
this._perform_login(authenticated_user, null);
}
});
}
})["catch"](function () {
// Ignore error: we are not authenticated or something else went wrong
});
}
hide() {
this._read_form();
this._html_scene.classList.add("hidden");
this._form.onsubmit = null;
}
is_transient() { return false; }
release() {
// cancel pending requests
if (this._principal_req !== null) {
this._principal_req.abort();
this._principal_req = null;
}
this._remove_logout();
} }
} }

View File

@@ -43,90 +43,96 @@ export class ShareCollectionScene {
* @param {Collection} collection The collection on which to edit sharing setting. Must exist. * @param {Collection} collection The collection on which to edit sharing setting. Must exist.
*/ */
constructor(user, password, collection) { constructor(user, password, collection) {
this._user = user;
this._password = password;
this._collection = collection;
let html_scene = get_element_by_id("sharecollectionscene"); this._html_scene = get_element_by_id("sharecollectionscene");
this._cancel_btn = get_element(this._html_scene, "[data-name=cancel]");
this._share_by_token_btn = get_element(this._html_scene, "button[data-name=sharebytoken]");
this._share_by_map_btn = get_element(this._html_scene, "button[data-name=sharebymap]");
this._share_by_token_div = get_element(this._html_scene, "div[data-name=sharebytoken]");
this._share_by_map_div = get_element(this._html_scene, "div[data-name=sharebymap]");
this._error_form = get_element(this._html_scene, "[data-name=error]");
/** @type {HTMLElement} */ let cancel_btn = get_element(html_scene, "[data-name=cancel]"); this._errorHandler = new ErrorHandler(this._error_form);
/** @type {HTMLElement} */ let share_by_token_btn = get_element(html_scene, "button[data-name=sharebytoken]");
/** @type {HTMLElement} */ let share_by_map_btn = get_element(html_scene, "button[data-name=sharebymap]");
/** @type {HTMLElement} */ let share_by_token_div = get_element(html_scene, "div[data-name=sharebytoken]");
/** @type {HTMLElement} */ let share_by_map_div = get_element(html_scene, "div[data-name=sharebymap]");
/** @type {HTMLElement} */ let error_form = get_element(html_scene, "[data-name=error]");
let errorHandler = new ErrorHandler(error_form); this._title = get_element(this._html_scene, "[data-name=title]");
/** @type {HTMLElement} */ let title = get_element(html_scene, "[data-name=title]");
function oncancel() {
try {
pop_scene();
} catch (err) {
console.error(err);
}
return false;
}
function onsharebytoken() {
let create_edit_share_scene = new CreateEditShareScene(user, password, collection, "token");
push_scene(create_edit_share_scene);
}
function onsharebymap() {
let create_edit_share_scene = new CreateEditShareScene(user, password, collection, "map");
push_scene(create_edit_share_scene);
}
this.show = function () {
this.release();
html_scene.classList.remove("hidden");
html_scene.querySelectorAll("details").forEach(function (details) {
details.open = true;
});
cancel_btn.onclick = oncancel;
collectionsCache.getServerFeatures(user, password, errorHandler.setError, (features) => {
if (features.sharing && features.sharing.PermittedCreateCollectionByToken) {
if (share_by_token_btn) {
share_by_token_btn.classList.remove("hidden");
share_by_token_btn.onclick = onsharebytoken;
}
} else {
if (share_by_token_btn) share_by_token_btn.classList.add("hidden");
}
if (features.sharing && features.sharing.FeatureEnabledCollectionByToken) {
if (share_by_token_div) share_by_token_div.classList.remove("hidden");
} else {
if (share_by_token_div) share_by_token_div.classList.add("hidden");
}
if (features.sharing && features.sharing.PermittedCreateCollectionByMap) {
if (share_by_map_btn) {
share_by_map_btn.classList.remove("hidden");
share_by_map_btn.onclick = onsharebymap;
}
} else {
if (share_by_map_btn) share_by_map_btn.classList.add("hidden");
}
if (features.sharing && features.sharing.FeatureEnabledCollectionByMap) {
if (share_by_map_div) share_by_map_div.classList.remove("hidden");
} else {
if (share_by_map_div) share_by_map_div.classList.add("hidden");
}
});
title.textContent = collection.displayname || collection.href;
update_share_list(user, password, collection, errorHandler);
};
this.hide = function () {
html_scene.classList.add("hidden");
cancel_btn.onclick = null;
};
this.release = function () {
};
} }
_oncancel() {
try {
pop_scene();
} catch (err) {
console.error(err);
}
return false;
}
_onsharebytoken() {
let create_edit_share_scene = new CreateEditShareScene(this._user, this._password, this._collection, "token");
push_scene(create_edit_share_scene);
}
_onsharebymap() {
let create_edit_share_scene = new CreateEditShareScene(this._user, this._password, this._collection, "map");
push_scene(create_edit_share_scene);
}
show() {
this.release();
this._html_scene.classList.remove("hidden");
this._html_scene.querySelectorAll("details").forEach(function (details) {
details.open = true;
});
this._cancel_btn.onclick = () => this._oncancel();
collectionsCache.getServerFeatures(this._user, this._password, this._errorHandler.setError, (features) => {
if (features.sharing && features.sharing.PermittedCreateCollectionByToken) {
if (this._share_by_token_btn) {
this._share_by_token_btn.classList.remove("hidden");
this._share_by_token_btn.onclick = () => this._onsharebytoken();
}
} else {
if (this._share_by_token_btn) this._share_by_token_btn.classList.add("hidden");
}
if (features.sharing && features.sharing.FeatureEnabledCollectionByToken) {
if (this._share_by_token_div) this._share_by_token_div.classList.remove("hidden");
} else {
if (this._share_by_token_div) this._share_by_token_div.classList.add("hidden");
}
if (features.sharing && features.sharing.PermittedCreateCollectionByMap) {
if (this._share_by_map_btn) {
this._share_by_map_btn.classList.remove("hidden");
this._share_by_map_btn.onclick = () => this._onsharebymap();
}
} else {
if (this._share_by_map_btn) this._share_by_map_btn.classList.add("hidden");
}
if (features.sharing && features.sharing.FeatureEnabledCollectionByMap) {
if (this._share_by_map_div) this._share_by_map_div.classList.remove("hidden");
} else {
if (this._share_by_map_div) this._share_by_map_div.classList.add("hidden");
}
});
this._title.textContent = this._collection.displayname || this._collection.href;
update_share_list(this._user, this._password, this._collection, this._errorHandler);
}
hide() {
this._html_scene.classList.add("hidden");
this._cancel_btn.onclick = null;
}
release() {
}
is_transient() { return false; }
} }
/** /**

View File

@@ -37,201 +37,208 @@ export class UploadCollectionScene {
* @param {Collection} collection parent collection * @param {Collection} collection parent collection
*/ */
constructor(user, password, collection) { constructor(user, password, collection) {
/** @type {HTMLElement} */ let html_scene = get_element_by_id("uploadcollectionscene"); this._user = user;
/** @type {HTMLElement} */ let template = get_element(html_scene, "[data-name=filetemplate]"); this._password = password;
/** @type {HTMLElement} */ let upload_btn = get_element(html_scene, "[data-name=submit]"); this._collection = collection;
/** @type {HTMLElement} */ let close_btn = get_element(html_scene, "[data-name=close]");
/** @type {HTMLInputElement} */ let uploadfile_form = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=uploadfile]"));
/** @type {HTMLElement} */ let uploadfile_lbl = get_element(html_scene, "label[for=uploadfile]");
/** @type {HTMLInputElement} */ let href_form = /** @type {HTMLInputElement} */ (get_element(html_scene, "[data-name=href]"));
/** @type {HTMLElement} */ let href_label = get_element(html_scene, "label[for=href]");
/** @type {HTMLElement} */ let hreflimitmsg_html = get_element(html_scene, "[data-name=hreflimitmsg]");
/** @type {HTMLElement} */ let pending_html = get_element(html_scene, "[data-name=pending]");
/** @type {HTMLElement} */ let error_form = get_element(html_scene, ":scope > span[data-name=error]");
let files = uploadfile_form.files; this._html_scene = get_element_by_id("uploadcollectionscene");
href_form.addEventListener("input", onCleanHREFinput); this._template = get_element(this._html_scene, "[data-name=filetemplate]");
upload_btn.onclick = upload_start; this._upload_btn = get_element(this._html_scene, "[data-name=submit]");
uploadfile_form.onchange = onfileschange; this._close_btn = get_element(this._html_scene, "[data-name=close]");
this._uploadfile_form = /** @type {HTMLInputElement} */ (get_element(this._html_scene, "[data-name=uploadfile]"));
this._uploadfile_lbl = get_element(this._html_scene, "label[for=uploadfile]");
this._href_form = /** @type {HTMLInputElement} */ (get_element(this._html_scene, "[data-name=href]"));
this._href_label = get_element(this._html_scene, "label[for$=href]");
this._hreflimitmsg_html = get_element(this._html_scene, "[data-name=hreflimitmsg]");
this._pending_html = get_element(this._html_scene, "[data-name=pending]");
this._error_form = get_element(this._html_scene, ":scope > span[data-name=error]");
href_form.value = ""; this._files = this._uploadfile_form.files;
let href = ""; this._href_form.addEventListener("input", onCleanHREFinput);
this._upload_btn.onclick = () => this._upload_start();
this._uploadfile_form.onchange = () => this._onfileschange();
let errorHandler = new ErrorHandler(error_form); this._href_form.value = "";
let validator = new FormValidator(errorHandler); this._href = "";
validator.addValidator(href_form, validate_href(href_form, "HREF")); this._errorHandler = new ErrorHandler(this._error_form);
validator.addValidator(uploadfile_form, validate_files(uploadfile_form, "file")); this._validator = new FormValidator(this._errorHandler);
/** @type {?XMLHttpRequest} */ let upload_req = null; this._validator.addValidator(this._href_form, validate_href(this._href_form, "HREF"));
/** @type {Array<?string>} */ let results = []; this._validator.addValidator(this._uploadfile_form, validate_files(this._uploadfile_form, "file"));
/** @type {?Array<HTMLElement>} */ let nodes = null;
function upload_start() { /** @type {?XMLHttpRequest} */ this._upload_req = null;
try { /** @type {Array<?string>} */ this._results = [];
if (!validator.validate()) { /** @type {?Array<HTMLElement>} */ this._nodes = null;
return false; }
}
if (!files) {
return false;
}
read_form();
uploadfile_form.classList.add("hidden");
uploadfile_lbl.classList.add("hidden");
href_form.classList.add("hidden");
href_label.classList.add("hidden");
hreflimitmsg_html.classList.add("hidden");
upload_btn.classList.add("hidden");
close_btn.classList.add("hidden");
pending_html.classList.remove("hidden"); _upload_start() {
try {
nodes = []; if (!this._validator.validate()) {
for (let i = 0; i < files.length; i++) { return false;
let file = files[i];
let node = /** @type {HTMLElement} */ (template.cloneNode(true));
node.classList.remove("hidden");
let name_form = get_element(node, "[data-name=name]");
name_form.textContent = file.name;
node.classList.remove("hidden");
nodes.push(node);
updateFileStatus(i);
if (template.parentNode) {
template.parentNode.insertBefore(node, template);
}
}
upload_next();
} catch (err) {
console.error(err);
} }
return false; if (!this._files) {
} return false;
}
this._read_form();
this._uploadfile_form.classList.add("hidden");
this._uploadfile_lbl.classList.add("hidden");
this._href_form.classList.add("hidden");
this._href_label.classList.add("hidden");
this._hreflimitmsg_html.classList.add("hidden");
this._upload_btn.classList.add("hidden");
this._close_btn.classList.add("hidden");
function upload_next() { this._pending_html.classList.remove("hidden");
if (!files) {
this._nodes = [];
for (let i = 0; i < this._files.length; i++) {
let file = this._files[i];
let node = /** @type {HTMLElement} */ (this._template.cloneNode(true));
node.classList.remove("hidden");
let name_form = get_element(node, "[data-name=name]");
name_form.textContent = file.name;
node.classList.remove("hidden");
this._nodes.push(node);
this._updateFileStatus(i);
if (this._template.parentNode) {
this._template.parentNode.insertBefore(node, this._template);
}
}
this._upload_next();
} catch (err) {
console.error(err);
}
return false;
}
_upload_next() {
if (!this._files) {
return;
}
try {
if (this._files.length === this._results.length) {
this._pending_html.classList.add("hidden");
this._close_btn.classList.remove("hidden");
return; return;
}
try {
if (files.length === results.length) {
pending_html.classList.add("hidden");
close_btn.classList.remove("hidden");
return;
} else {
let file = files[results.length];
if (files.length > 1 || href.length == 0) {
href = random_uuid();
}
let upload_href = collection.href + href + "/";
upload_req = upload_collection(user, password, upload_href, file, function (result) {
upload_req = null;
results.push(result);
updateFileStatus(results.length - 1);
upload_next();
});
}
} catch (err) {
console.error(err);
}
}
function onclose() {
try {
if (results.length > 0) {
collectionsCache.invalidate();
}
pop_scene();
} catch (err) {
console.error(err);
}
return false;
}
/**
* @param {number} i
*/
function updateFileStatus(i) {
if (nodes === null) {
return;
}
/** @type {HTMLElement} */ let file_success_form = get_element(nodes[i], "[data-name=success]");
/** @type {HTMLElement} */ let file_error_form = get_element(nodes[i], "[data-name=error]");
if (results.length > i) {
if (results[i]) {
file_success_form.classList.add("hidden");
file_error_form.textContent = "Error: " + results[i];
error_form.classList.remove("hidden");
} else {
file_success_form.classList.remove("hidden");
file_error_form.classList.add("hidden");
}
} else { } else {
let file = this._files[this._results.length];
if (this._files.length > 1 || this._href.length == 0) {
this._href = random_uuid();
}
let upload_href = this._collection.href + this._href + "/";
this._upload_req = upload_collection(this._user, this._password, upload_href, file, (result) => {
this._upload_req = null;
this._results.push(result);
this._updateFileStatus(this._results.length - 1);
this._upload_next();
});
}
} catch (err) {
console.error(err);
}
}
_onclose() {
try {
if (this._results.length > 0) {
collectionsCache.invalidate();
}
pop_scene();
} catch (err) {
console.error(err);
}
return false;
}
/**
* @param {number} i
*/
_updateFileStatus(i) {
if (this._nodes === null) {
return;
}
/** @type {HTMLElement} */ let file_success_form = get_element(this._nodes[i], "[data-name=success]");
/** @type {HTMLElement} */ let file_error_form = get_element(this._nodes[i], "[data-name=error]");
if (this._results.length > i) {
if (this._results[i]) {
file_success_form.classList.add("hidden"); file_success_form.classList.add("hidden");
file_error_form.textContent = "Error: " + this._results[i];
this._error_form.classList.remove("hidden");
} else {
file_success_form.classList.remove("hidden");
file_error_form.classList.add("hidden"); file_error_form.classList.add("hidden");
} }
} else {
file_success_form.classList.add("hidden");
file_error_form.classList.add("hidden");
} }
}
function read_form() { _read_form() {
cleanHREFinput(href_form); cleanHREFinput(this._href_form);
href = href_form.value.trim().toLowerCase(); this._href = this._href_form.value.trim().toLowerCase();
files = uploadfile_form.files; this._files = this._uploadfile_form.files;
return true; return true;
} }
function onfileschange() { _onfileschange() {
files = uploadfile_form.files; this._files = this._uploadfile_form.files;
if (files && files.length > 1) { if (this._files && this._files.length > 1) {
hreflimitmsg_html.classList.remove("hidden"); this._hreflimitmsg_html.classList.remove("hidden");
href_form.classList.add("hidden"); this._href_form.classList.add("hidden");
href_label.classList.add("hidden"); this._href_label.classList.add("hidden");
href_form.value = random_uuid(); // fake HREF, will be replaced on upload this._href_form.value = random_uuid(); // fake HREF, will be replaced on upload
} else {
this._hreflimitmsg_html.classList.add("hidden");
this._href_form.classList.remove("hidden");
this._href_label.classList.remove("hidden");
if (this._files && this._files.length > 0) {
this._href_form.value = this._files[0].name.replace(/\.(ics|vcf)$/, '');
} else { } else {
hreflimitmsg_html.classList.add("hidden"); this._href_form.value = "";
href_form.classList.remove("hidden");
href_label.classList.remove("hidden");
if (files && files.length > 0) {
href_form.value = files[0].name.replace(/\.(ics|vcf)$/, '');
} else {
href_form.value = "";
}
} }
return false;
} }
return false;
}
this.show = function () { show() {
html_scene.classList.remove("hidden"); this._html_scene.classList.remove("hidden");
close_btn.onclick = onclose; this._close_btn.onclick = () => this._onclose();
}; }
this.hide = function () { hide() {
html_scene.classList.add("hidden"); this._html_scene.classList.add("hidden");
close_btn.classList.remove("hidden"); this._close_btn.classList.remove("hidden");
upload_btn.classList.remove("hidden"); this._upload_btn.classList.remove("hidden");
uploadfile_form.classList.remove("hidden"); this._uploadfile_form.classList.remove("hidden");
uploadfile_lbl.classList.remove("hidden"); this._uploadfile_lbl.classList.remove("hidden");
href_form.classList.remove("hidden"); this._href_form.classList.remove("hidden");
href_label.classList.remove("hidden"); this._href_label.classList.remove("hidden");
hreflimitmsg_html.classList.add("hidden"); this._hreflimitmsg_html.classList.add("hidden");
pending_html.classList.add("hidden"); this._pending_html.classList.add("hidden");
errorHandler.clearError(); this._errorHandler.clearError();
close_btn.onclick = null; this._close_btn.onclick = null;
upload_btn.onclick = null; this._upload_btn.onclick = null;
href_form.value = ""; this._href_form.value = "";
uploadfile_form.value = ""; this._uploadfile_form.value = "";
if (nodes == null) { if (this._nodes == null) {
return; return;
}
this._nodes.forEach(function (node) {
if (node.parentNode) {
node.parentNode.removeChild(node);
} }
nodes.forEach(function (node) { });
if (node.parentNode) { this._nodes = null;
node.parentNode.removeChild(node); }
}
}); is_transient() { return false; }
nodes = null;
}; release() {
this.release = function () { if (this._upload_req !== null) {
if (upload_req !== null) { this._upload_req.abort();
upload_req.abort(); this._upload_req = null;
upload_req = null; }
}
};
} }
} }

View File

@@ -36,6 +36,11 @@ export class Scene {
* Scene is removed from scene stack. * Scene is removed from scene stack.
*/ */
release() { } release() { }
/**
* Whether the scene should be excluded from browser history.
* @returns boolean
*/
is_transient() { return false; }
} }
@@ -44,6 +49,87 @@ export class Scene {
*/ */
let scene_stack = []; let scene_stack = [];
/** @type {Array<Array<Scene>>} */
let history_array = [];
let current_history_index = -1;
let is_navigating_history = false;
function record_history() {
if (is_navigating_history) return;
let current_scene = scene_stack.length > 0 ? scene_stack[scene_stack.length - 1] : null;
if (current_scene && current_scene.is_transient && current_scene.is_transient()) {
return;
}
// Compare with current history to avoid duplicates
if (history_array.length > 0 && current_history_index >= 0) {
let last_stack = history_array[current_history_index];
if (last_stack && last_stack.length === scene_stack.length) {
let is_identical = true;
for (let i = 0; i < scene_stack.length; i++) {
if (last_stack[i] !== scene_stack[i]) {
is_identical = false;
break;
}
}
if (is_identical) return;
}
}
history_array.splice(current_history_index + 1);
history_array.push(scene_stack.slice());
current_history_index++;
// Check if this is the first history entry we are recording
if (typeof window !== "undefined" && window.history) {
if (current_history_index === 0) {
history.replaceState({ history_index: current_history_index }, '');
} else {
history.pushState({ history_index: current_history_index }, '');
}
}
}
if (typeof window !== "undefined" && window.history) {
if (!history.state || typeof history.state.history_index !== "number") {
current_history_index = -1;
} else {
// If there's an existing state but we just loaded, we don't have the history_array memory.
// We'll reset it. The user will be redirected to the root if they go back out of bounds.
current_history_index = -1;
}
window.addEventListener("popstate", (event) => {
if (!event.state || typeof event.state.history_index !== "number") return;
let new_index = event.state.history_index;
if (new_index >= 0 && new_index < history_array.length) {
is_navigating_history = true;
try {
let new_stack = history_array[new_index];
if (scene_stack.length > 0) {
scene_stack[scene_stack.length - 1].hide();
}
scene_stack = new_stack.slice();
if (scene_stack.length > 0) {
scene_stack[scene_stack.length - 1].show();
}
current_history_index = new_index;
} finally {
is_navigating_history = false;
}
} else {
// Out of bounds (e.g., from a previous session after a reload)
window.location.reload();
}
});
}
/** /**
* Push scene onto stack. * Push scene onto stack.
* @param {Scene} scene * @param {Scene} scene
@@ -54,6 +140,7 @@ export function push_scene(scene) {
} }
scene_stack.push(scene); scene_stack.push(scene);
scene.show(); scene.show();
record_history();
} }
/** /**
@@ -80,6 +167,7 @@ export function replace_scene(scene) {
} }
scene_stack.push(scene); scene_stack.push(scene);
scene.show(); scene.show();
record_history();
} }
/** /**
@@ -94,6 +182,7 @@ export function pop_scene() {
if (scene_stack.length >= 1) { if (scene_stack.length >= 1) {
scene_stack[scene_stack.length - 1].show(); scene_stack[scene_stack.length - 1].show();
} }
record_history();
} }
/** /**
@@ -112,6 +201,7 @@ export function pop_to_parent() {
if (scene_stack.length >= 1) { if (scene_stack.length >= 1) {
scene_stack[scene_stack.length - 1].show(); scene_stack[scene_stack.length - 1].show();
} }
record_history();
} }
/** /**
@@ -131,6 +221,7 @@ export function pop_to_root() {
if (scene_stack.length === 1) { if (scene_stack.length === 1) {
scene_stack[0].show(); // Ensure the root scene is visible scene_stack[0].show(); // Ensure the root scene is visible
} }
record_history();
} }
/** /**

View File

@@ -28,7 +28,7 @@ export function displayPermissionsOrConversion(conversion, permissions, node) {
const roElement = get_element(node, "[data-name=ro]"); const roElement = get_element(node, "[data-name=ro]");
const rwElement = get_element(node, "[data-name=rw]"); const rwElement = get_element(node, "[data-name=rw]");
let fixedConversion = (conversion || "").toLowerCase(); let fixedConversion = (conversion || "").toLowerCase();
if (fixedConversion === "bday") { if (fixedConversion != "none" && fixedConversion != "") {
conversionElement.classList.remove("hidden"); conversionElement.classList.remove("hidden");
roElement.classList.add("hidden"); roElement.classList.add("hidden");
rwElement.classList.add("hidden"); rwElement.classList.add("hidden");