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,19 +45,25 @@ 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 = [];
this._errorHandler = new ErrorHandler(this._error_div);
}
_onnew() {
try { try {
let create_collection_scene = new CreateEditCollectionScene(user, password, principal_collection); let create_collection_scene = new CreateEditCollectionScene(this._user, this._password, this._principal_collection);
push_scene(create_collection_scene); push_scene(create_collection_scene);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -65,9 +71,9 @@ export class CollectionsScene {
return false; return false;
} }
function onupload() { _onupload() {
try { try {
let upload_scene = new UploadCollectionScene(user, password, principal_collection); let upload_scene = new UploadCollectionScene(this._user, this._password, this._principal_collection);
push_scene(upload_scene); push_scene(upload_scene);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -75,9 +81,9 @@ export class CollectionsScene {
return false; return false;
} }
function onincomingshares() { _onincomingshares() {
try { try {
let incoming_sharing_scene = new IncomingSharingScene(user, password); let incoming_sharing_scene = new IncomingSharingScene(this._user, this._password);
push_scene(incoming_sharing_scene); push_scene(incoming_sharing_scene);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -88,9 +94,9 @@ export class CollectionsScene {
/** /**
* @param {Collection} collection * @param {Collection} collection
*/ */
function onedit(collection) { _onedit(collection) {
try { try {
let edit_collection_scene = new CreateEditCollectionScene(user, password, collection); let edit_collection_scene = new CreateEditCollectionScene(this._user, this._password, collection);
push_scene(edit_collection_scene); push_scene(edit_collection_scene);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -101,9 +107,9 @@ export class CollectionsScene {
/** /**
* @param {Collection} collection * @param {Collection} collection
*/ */
function onshare(collection) { _onshare(collection) {
try { try {
let share_collection_scene = new ShareCollectionScene(user, password, collection); let share_collection_scene = new ShareCollectionScene(this._user, this._password, collection);
push_scene(share_collection_scene); push_scene(share_collection_scene);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -114,10 +120,10 @@ export class CollectionsScene {
/** /**
* @param {Collection} collection * @param {Collection} collection
*/ */
function ondelete(collection) { _ondelete(collection) {
try { try {
let delete_collection_scene = new DeleteConfirmationScene( let delete_collection_scene = new DeleteConfirmationScene(
user, password, "Delete Collection", collection, collection.displayname || collection.href, this._user, this._password, "Delete Collection", collection, collection.displayname || collection.href,
delete_collection, true delete_collection, true
); );
push_scene(delete_collection_scene); push_scene(delete_collection_scene);
@@ -132,26 +138,26 @@ export class CollectionsScene {
* @param {import("../api/sharing.js").Share[]} shares * @param {import("../api/sharing.js").Share[]} shares
* @param {boolean} clear_error * @param {boolean} clear_error
*/ */
function show_collections(collections, shares, clear_error) { _show_collections(collections, shares, clear_error) {
/** @type {HTMLElement} */ let navBar = get_element(document, "#logoutview"); /** @type {HTMLElement} */ let navBar = get_element(document, "#logoutview");
let heightOfNavBar = navBar.offsetHeight + "px"; let heightOfNavBar = navBar.offsetHeight + "px";
html_scene.style.marginTop = heightOfNavBar; this._html_scene.style.marginTop = heightOfNavBar;
html_scene.style.height = "calc(100vh - " + heightOfNavBar + ")"; this._html_scene.style.height = "calc(100vh - " + heightOfNavBar + ")";
if (clear_error) { if (clear_error) {
errorHandler.clearError(); this._errorHandler.clearError();
} }
// Clear old nodes // Clear old nodes
nodes.forEach(function (node) { this._nodes.forEach(function (node) {
if (node.parentNode) { if (node.parentNode) {
node.parentNode.removeChild(node); node.parentNode.removeChild(node);
} }
}); });
nodes = []; this._nodes = [];
collections.forEach(function (/** @type {Collection} */ collection) { collections.forEach((/** @type {Collection} */ collection) => {
/** @type {HTMLElement} */ let node = /** @type {HTMLElement} */(template.cloneNode(true)); /** @type {HTMLElement} */ let node = /** @type {HTMLElement} */(this._template.cloneNode(true));
node.classList.remove("hidden"); node.classList.remove("hidden");
/** @type {HTMLElement} */ let title_form = get_element(node, "[data-name=title]"); /** @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 description_form = get_element(node, "[data-name=description]");
@@ -186,7 +192,7 @@ export class CollectionsScene {
s => (s.ShareType === "map") && s => (s.ShareType === "map") &&
(s.PathOrToken || "").replace(/\/+$/, "") === (collection.href || "").replace(/\/+$/, "")); (s.PathOrToken || "").replace(/\/+$/, "") === (collection.href || "").replace(/\/+$/, ""));
if (share) { if (share) {
if (share.Owner !== user) { if (share.Owner !== this._user) {
share_info.classList.remove("hidden"); share_info.classList.remove("hidden");
get_element(node, "[data-name=shared-by-owner]").textContent = share.Owner; get_element(node, "[data-name=shared-by-owner]").textContent = share.Owner;
} else { } else {
@@ -222,9 +228,9 @@ export class CollectionsScene {
let href = SERVER + collection.href; let href = SERVER + collection.href;
url_form.value = href; url_form.value = href;
download_btn.href = href; download_btn.href = href;
download_btn.onclick = function (event) { download_btn.onclick = (event) => {
event.preventDefault(); event.preventDefault();
let auth = get_auth_header(user, password); let auth = get_auth_header(this._user, this._password);
let headers = auth ? { let headers = auth ? {
'Authorization': auth 'Authorization': auth
} : undefined; } : undefined;
@@ -242,8 +248,8 @@ export class CollectionsScene {
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
})["catch"](function (error) { })["catch"]((error) => {
errorHandler.setError(error.message); this._errorHandler.setError(error.message);
}); });
}; };
if (collection.type == CollectionType.WEBCAL) { if (collection.type == CollectionType.WEBCAL) {
@@ -251,44 +257,47 @@ export class CollectionsScene {
download_btn.parentElement.classList.add("hidden"); download_btn.parentElement.classList.add("hidden");
} }
} }
delete_btn.onclick = function () { return ondelete(collection); }; delete_btn.onclick = () => { return this._ondelete(collection); };
edit_btn.onclick = function () { return onedit(collection); }; edit_btn.onclick = () => { return this._onedit(collection); };
share_btn.onclick = function () { return onshare(collection); }; share_btn.onclick = () => { return this._onshare(collection); };
node.classList.remove("hidden"); node.classList.remove("hidden");
nodes.push(node); this._nodes.push(node);
if (template.parentNode) { if (this._template.parentNode) {
template.parentNode.insertBefore(node, template); this._template.parentNode.insertBefore(node, this._template);
} }
}); });
} }
this.errorwrapper = function (/** @type {string} */ error) { _errorwrapper(/** @type {string} */ error) {
errorHandler.setError(error); this._errorHandler.setError(error);
if (onerror) onerror(error); if (this._onerror) this._onerror(error);
}; }
this.show = function () { show() {
html_scene.classList.remove("hidden"); this._html_scene.classList.remove("hidden");
new_btn.onclick = onnew; this._new_btn.onclick = () => this._onnew();
upload_btn.onclick = onupload; this._upload_btn.onclick = () => this._onupload();
incomingshares_btn.onclick = onincomingshares; this._incomingshares_btn.onclick = () => this._onincomingshares();
collectionsCache.getChildCollections(user, password, principal_collection, this.errorwrapper, show_collections); collectionsCache.getChildCollections(this._user, this._password, this._principal_collection, (e) => this._errorwrapper(e), (c, s, ce) => this._show_collections(c, s, ce));
collectionsCache.getServerFeatures(user, password, this.errorwrapper, maybe_enable_sharing_options); collectionsCache.getServerFeatures(this._user, this._password, (e) => this._errorwrapper(e), maybe_enable_sharing_options);
}; }
this.hide = function () {
html_scene.classList.add("hidden"); hide() {
new_btn.onclick = null; this._html_scene.classList.add("hidden");
upload_btn.onclick = null; this._new_btn.onclick = null;
incomingshares_btn.onclick = null; this._upload_btn.onclick = null;
this._incomingshares_btn.onclick = null;
// remove collection // remove collection
nodes.forEach(function (node) { this._nodes.forEach(function (node) {
if (node.parentNode) { if (node.parentNode) {
node.parentNode.removeChild(node); node.parentNode.removeChild(node);
} }
}); });
nodes = []; this._nodes = [];
};
this.release = function () {
};
} }
release() {
}
is_transient() { return false; }
} }

View File

@@ -41,50 +41,53 @@ 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 = type_form.options; /** @type {HTMLOptionsCollection} */ let options = this._type_form.options;
// remove all options that are not supersets let valid_type_options = CollectionType.valid_options_for_type(this._type);
let valid_type_options = CollectionType.valid_options_for_type(type);
for (let i = options.length - 1; i >= 0; i--) { for (let i = options.length - 1; i >= 0; i--) {
if (valid_type_options.indexOf(options[i].value) < 0) { if (valid_type_options.indexOf(options[i].value) < 0) {
options.remove(i); options.remove(i);
@@ -92,40 +95,40 @@ export class CreateEditCollectionScene {
} }
} }
function read_form() { _read_form() {
if (!edit && href_form) { if (!this._edit && this._href_form) {
cleanHREFinput(href_form); cleanHREFinput(this._href_form);
let newhreftxtvalue = href_form.value.trim().toLowerCase(); let newhreftxtvalue = this._href_form.value.trim().toLowerCase();
href = collection.href + newhreftxtvalue + "/"; this._href = this._collection.href + newhreftxtvalue + "/";
} }
displayname = displayname_form.value; this._displayname = this._displayname_form.value;
description = description_form.value; this._description = this._description_form.value;
source = source_form.value; this._source = this._source_form.value;
type = type_form.value; this._type = this._type_form.value;
color = color_form.value; this._color = this._color_form.value;
return true; return true;
} }
function fill_form() { _fill_form() {
if (!edit && href_form) { if (!this._edit && this._href_form) {
href_form.value = random_uuid(); this._href_form.value = random_uuid();
} }
displayname_form.value = displayname; this._displayname_form.value = this._displayname;
description_form.value = description; this._description_form.value = this._description;
source_form.value = source; this._source_form.value = this._source;
type_form.value = type; this._type_form.value = this._type;
color_form.value = color; this._color_form.value = this._color;
onTypeChange(null); this._onTypeChange(null);
type_form.addEventListener("change", onTypeChange); this._type_form.addEventListener("change", (e) => this._onTypeChange(e));
} }
function onsubmit() { _onsubmit() {
try { try {
if (!validator.validate()) { if (!this._validator.validate()) {
return false; return false;
} }
read_form(); this._read_form();
let sane_color = color.trim(); let sane_color = this._color.trim();
if (sane_color) { if (sane_color) {
/** @type {?RegExpExecArray} */ let match = COLOR_RE.exec(sane_color); /** @type {?RegExpExecArray} */ let match = COLOR_RE.exec(sane_color);
if (match) { if (match) {
@@ -134,24 +137,24 @@ export class CreateEditCollectionScene {
} }
let loading_scene = new LoadingScene(); let loading_scene = new LoadingScene();
push_scene(loading_scene); push_scene(loading_scene);
let collection = new Collection(href, type, displayname, description, sane_color, 0, 0, source); let collection = new Collection(this._href, this._type, this._displayname, this._description, sane_color, 0, 0, this._source);
let callback = function (/** @type {?string} */ error1) { let callback = (/** @type {?string} */ error1) => {
if (!is_current_scene(loading_scene)) { if (!is_current_scene(loading_scene)) {
return; return;
} }
create_edit_req = null; this._create_edit_req = null;
if (error1) { if (error1) {
errorHandler.setError(error1); this._errorHandler.setError(error1);
pop_scene(); pop_scene();
} else { } else {
collectionsCache.invalidate(); collectionsCache.invalidate();
pop_to_parent(); pop_to_parent();
} }
}; };
if (edit) { if (this._edit) {
create_edit_req = edit_collection(user, password, collection, callback); this._create_edit_req = edit_collection(this._user, this._password, collection, callback);
} else { } else {
create_edit_req = create_collection(user, password, collection, callback); this._create_edit_req = create_collection(this._user, this._password, collection, callback);
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -159,7 +162,7 @@ export class CreateEditCollectionScene {
return false; return false;
} }
function oncancel() { _oncancel() {
try { try {
pop_scene(); pop_scene();
} catch (err) { } catch (err) {
@@ -171,51 +174,54 @@ export class CreateEditCollectionScene {
/** /**
* @param {?Event} _e * @param {?Event} _e
*/ */
function onTypeChange(_e) { _onTypeChange(_e) {
if (type_form.value == CollectionType.WEBCAL) { if (this._type_form.value == CollectionType.WEBCAL) {
source_label.classList.remove("hidden"); this._source_label.classList.remove("hidden");
source_form.classList.remove("hidden"); this._source_form.classList.remove("hidden");
} else { } else {
source_label.classList.add("hidden"); this._source_label.classList.add("hidden");
source_form.classList.add("hidden"); this._source_form.classList.add("hidden");
} }
} }
this.show = function () { show() {
this.release(); this.release();
// Clone type_form because it's impossible to hide options without removing them // Clone type_form because it's impossible to hide options without removing them
saved_type_form = type_form; this._saved_type_form = this._type_form;
type_form = /** @type {HTMLSelectElement} */ (type_form.cloneNode(true)); this._type_form = /** @type {HTMLSelectElement} */ (this._type_form.cloneNode(true));
if (saved_type_form.parentNode) { if (this._saved_type_form.parentNode) {
saved_type_form.parentNode.replaceChild(type_form, saved_type_form); this._saved_type_form.parentNode.replaceChild(this._type_form, this._saved_type_form);
} }
remove_invalid_types(); this._remove_invalid_types();
html_scene.classList.remove("hidden"); this._html_scene.classList.remove("hidden");
if (edit && title_form) { if (this._edit && this._title_form) {
title_form.textContent = collection.displayname || collection.href; this._title_form.textContent = this._collection.displayname || this._collection.href;
} }
fill_form(); this._fill_form();
submit_btn.onclick = onsubmit; this._submit_btn.onclick = () => this._onsubmit();
cancel_btn.onclick = oncancel; this._cancel_btn.onclick = () => this._oncancel();
validator.validate(); this._validator.validate();
}; }
this.hide = function () {
read_form(); hide() {
html_scene.classList.add("hidden"); this._read_form();
this._html_scene.classList.add("hidden");
// restore type_form // restore type_form
if (type_form.parentNode && saved_type_form) { if (this._type_form.parentNode && this._saved_type_form) {
type_form.parentNode.replaceChild(saved_type_form, type_form); this._type_form.parentNode.replaceChild(this._saved_type_form, this._type_form);
type_form = saved_type_form; this._type_form = this._saved_type_form;
} }
saved_type_form = null; this._saved_type_form = null;
submit_btn.onclick = null; this._submit_btn.onclick = null;
cancel_btn.onclick = null; this._cancel_btn.onclick = null;
}; }
this.release = function () {
if (create_edit_req !== null) { is_transient() { return false; }
create_edit_req.abort();
create_edit_req = null; release() {
if (this._create_edit_req !== null) {
this._create_edit_req.abort();
this._create_edit_req = null;
} }
};
} }
} }

View File

@@ -39,82 +39,87 @@ 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() { }
_get_selected_conversion() {
/** @type {HTMLInputElement | null} */ /** @type {HTMLInputElement | null} */
let checked = conversions_container.querySelector("input[name=conversion]:checked"); let checked = this._conversions_container.querySelector("input[name=conversion]:checked");
return checked ? checked.value : "none"; return checked ? checked.value : "none";
} }
function on_conversion_change() { _on_conversion_change() {
let conversion = get_selected_conversion(); let conversion = this._get_selected_conversion();
if (conversion === "bday") { if (conversion != "none") {
permissions_ro_radio.checked = true; this._permissions_ro_radio.disabled = true;
permissions_rw_radio.checked = false; this._permissions_rw_radio.disabled = true;
permissions_ro_radio.disabled = true; this._enabled_checkbox.checked = true;
permissions_rw_radio.disabled = true; this._hidden_checkbox.checked = false;
enabled_checkbox.checked = true;
hidden_checkbox.checked = false;
} else { } else {
permissions_ro_radio.disabled = false; this._permissions_ro_radio.disabled = false;
permissions_rw_radio.disabled = false; this._permissions_rw_radio.disabled = false;
} }
if (shareType === "map") { if (this._shareType === "map") {
map_validator.validate(); this._map_validator.validate();
} }
} }
function oncancel() { _oncancel() {
try { try {
pop_scene(); pop_scene();
} catch (err) { } catch (err) {
@@ -123,44 +128,44 @@ export class CreateEditShareScene {
return false; return false;
} }
function onsubmit() { _onsubmit() {
try { try {
if (shareType === "map") { if (this._shareType === "map") {
if (!map_validator.validate()) { if (!this._map_validator.validate()) {
return false; return false;
} }
} }
let conversion = get_selected_conversion(); let conversion = this._get_selected_conversion();
let is_birthday = conversion === "bday"; let is_conversion = conversion != "none";
let enabled_by_owner = is_birthday ? true : enabled_checkbox.checked; let enabled_by_owner = is_conversion ? true : this._enabled_checkbox.checked;
let hidden_by_owner = is_birthday ? false : hidden_checkbox.checked; let hidden_by_owner = is_conversion ? false : this._hidden_checkbox.checked;
let permissions = is_birthday ? "r" : (permissions_rw_radio.checked ? "rw" : "r"); let permissions = is_conversion ? "r" : (this._permissions_rw_radio.checked ? "rw" : "r");
/** @type {string} */ let conversion_value = conversion; /** @type {string} */ let conversion_value = conversion;
/** @type {Object<string, string>} */ let properties = {}; /** @type {Object<string, string>} */ let properties = {};
if (displayname_override_enabled.checked) { if (this._displayname_override_enabled.checked) {
let key = get_property_key(collection.type, "DISPLAYNAME"); let key = get_property_key(this._collection.type, "DISPLAYNAME");
if (key) properties[key] = displayname_override_input.value; if (key) properties[key] = this._displayname_override_input.value;
} }
if (description_override_enabled.checked) { if (this._description_override_enabled.checked) {
let key = get_property_key(collection.type, "DESCRIPTION"); let key = get_property_key(this._collection.type, "DESCRIPTION");
if (key) properties[key] = description_override_input.value; if (key) properties[key] = this._description_override_input.value;
} }
if (color_override_enabled.checked) { if (this._color_override_enabled.checked) {
let key = get_property_key(collection.type, "COLOR"); let key = get_property_key(this._collection.type, "COLOR");
if (key) properties[key] = color_override_input.value + (color_override_input.value ? "ff" : ""); if (key) properties[key] = this._color_override_input.value + (this._color_override_input.value ? "ff" : "");
} }
let callback = function (/** @type {?string} */ error) { let callback = (/** @type {?string} */ error) => {
if (!is_current_scene(self)) { if (!is_current_scene(this)) {
return; return;
} }
if (error) { if (error) {
errorHandler.setError(error); this._errorHandler.setError(error);
} else { } else {
// On any share-to-self (currently only bday conversion), invalidate the // On any share-to-self (currently only bday conversion), invalidate the
// collections cache so the virtual calendar appears immediately. // collections cache so the virtual calendar appears immediately.
if (shareuser_input.value === user) { if (this._shareuser_input.value === this._user) {
collectionsCache.invalidate(); collectionsCache.invalidate();
} }
pop_scene(); pop_scene();
@@ -168,30 +173,30 @@ export class CreateEditShareScene {
}; };
let new_share = new Share({ let new_share = new Share({
ShareType: shareType, ShareType: this._shareType,
PathMapped: pathMapped, PathMapped: this._pathMapped,
Permissions: permissions, Permissions: permissions,
EnabledByOwner: enabled_by_owner, EnabledByOwner: enabled_by_owner,
EnabledByUser: (edit && share) ? share.EnabledByUser : null, EnabledByUser: (this._edit && this._share) ? this._share.EnabledByUser : null,
HiddenByOwner: hidden_by_owner, HiddenByOwner: hidden_by_owner,
HiddenByUser: (edit && share) ? share.HiddenByUser : null, HiddenByUser: (this._edit && this._share) ? this._share.HiddenByUser : null,
Properties: properties, Properties: properties,
User: (edit && share) ? share.User : shareuser_input.value, User: (this._edit && this._share) ? this._share.User : this._shareuser_input.value,
PathOrToken: (edit && share) ? share.PathOrToken : (shareType === "map" ? "/" + shareuser_input.value + "/" + sharehref_input.value + "/" : ""), PathOrToken: (this._edit && this._share) ? this._share.PathOrToken : (this._shareType === "map" ? "/" + this._shareuser_input.value + "/" + this._sharehref_input.value + "/" : ""),
Conversion: conversion_value, Conversion: conversion_value,
}); });
if (edit) { if (this._edit) {
if (shareType === "map") { if (this._shareType === "map") {
update_share_by_map(user, password, new_share, callback); update_share_by_map(this._user, this._password, new_share, callback);
} else { } else {
update_share_by_token(user, password, new_share, callback); update_share_by_token(this._user, this._password, new_share, callback);
} }
} else { } else {
if (shareType === "map") { if (this._shareType === "map") {
add_share_by_map(user, password, new_share, callback); add_share_by_map(this._user, this._password, new_share, callback);
} else { } else {
add_share_by_token(user, password, new_share, callback); add_share_by_token(this._user, this._password, new_share, callback);
} }
} }
} catch (err) { } catch (err) {
@@ -200,32 +205,32 @@ export class CreateEditShareScene {
return false; return false;
} }
this.show = function () { show() {
this.release(); this.release();
html_scene.classList.remove("hidden"); this._html_scene.classList.remove("hidden");
html_scene.querySelectorAll("details").forEach(function (details) { this._html_scene.querySelectorAll("details").forEach(function (details) {
if (details.dataset.name !== "properties_override") { if (details.dataset.name !== "properties_override") {
details.open = true; details.open = true;
} else { } else {
details.open = false; details.open = false;
} }
}); });
cancel_btn.onclick = oncancel; this._cancel_btn.onclick = () => this._oncancel();
form.onsubmit = onsubmit; this._form.onsubmit = () => this._onsubmit();
/** @type {HTMLHeadingElement} */ let title = /** @type {HTMLHeadingElement} */ (get_element(html_scene, "h1")); /** @type {HTMLHeadingElement} */ let title = /** @type {HTMLHeadingElement} */ (get_element(this._html_scene, "h1"));
title.textContent = edit ? "Edit Share" : "New Share"; title.textContent = this._edit ? "Edit Share" : "New Share";
submit_btn.textContent = edit ? "Save" : "Create"; this._submit_btn.textContent = this._edit ? "Save" : "Create";
shareuser_input.value = (edit && share) ? share.User : ""; this._shareuser_input.value = (this._edit && this._share) ? this._share.User : "";
shareuser_input.disabled = edit; this._shareuser_input.disabled = this._edit;
enabled_checkbox.checked = (edit && share && share.EnabledByOwner !== null) ? share.EnabledByOwner : true; this._enabled_checkbox.checked = (this._edit && this._share && this._share.EnabledByOwner !== null) ? this._share.EnabledByOwner : true;
hidden_checkbox.checked = (edit && share && share.HiddenByOwner !== null) ? share.HiddenByOwner : false; this._hidden_checkbox.checked = (this._edit && this._share && this._share.HiddenByOwner !== null) ? this._share.HiddenByOwner : false;
let initial_conversion = (edit && share && share.Conversion) ? share.Conversion : "none"; let initial_conversion = (this._edit && this._share && this._share.Conversion) ? this._share.Conversion : "none";
collectionsCache.getServerFeatures(user, password, errorHandler.setError, (features) => { collectionsCache.getServerFeatures(this._user, this._password, this._errorHandler.setError, (features) => {
conversions_container.innerHTML = ""; this._conversions_container.innerHTML = "";
let supported = (features.sharing && features.sharing.SupportedConversions) || []; let supported = (features.sharing && features.sharing.SupportedConversions) || [];
if (supported.length > 0) { if (supported.length > 0) {
let options = ["none", ...supported.filter(opt => opt !== "none")]; let options = ["none", ...supported.filter(opt => opt !== "none")];
@@ -237,7 +242,7 @@ export class CreateEditShareScene {
radio.value = opt; radio.value = opt;
radio.id = id; radio.id = id;
radio.checked = (opt === initial_conversion); radio.checked = (opt === initial_conversion);
radio.onchange = on_conversion_change; radio.onchange = () => this._on_conversion_change();
let label = document.createElement("label"); let label = document.createElement("label");
label.htmlFor = id; label.htmlFor = id;
@@ -247,44 +252,44 @@ export class CreateEditShareScene {
label.textContent = opt.charAt(0).toUpperCase() + opt.slice(1); label.textContent = opt.charAt(0).toUpperCase() + opt.slice(1);
} }
conversions_container.appendChild(radio); this._conversions_container.appendChild(radio);
conversions_container.appendChild(label); this._conversions_container.appendChild(label);
}); });
let is_addressbook = collection.type === CollectionType.ADDRESSBOOK; let is_addressbook = this._collection.type === CollectionType.ADDRESSBOOK;
if (is_addressbook) { if (is_addressbook) {
conversions_details.classList.remove("hidden"); this._conversions_details.classList.remove("hidden");
conversions_details.open = true; this._conversions_details.open = true;
} else { } else {
conversions_details.classList.add("hidden"); this._conversions_details.classList.add("hidden");
} }
} else { } else {
conversions_details.classList.add("hidden"); this._conversions_details.classList.add("hidden");
} }
on_conversion_change(); this._on_conversion_change();
}); });
let displayname = collection.displayname || ""; let displayname = this._collection.displayname || "";
let description = collection.description || ""; let description = this._collection.description || "";
let color = collection.color || "#ffffff"; let color = this._collection.color || "#ffffff";
let displayname_override_enabled_value = false; let displayname_override_enabled_value = false;
let description_override_enabled_value = false; let description_override_enabled_value = false;
let color_override_enabled_value = false; let color_override_enabled_value = false;
if (edit && share && share.Properties) { if (this._edit && this._share && this._share.Properties) {
let displayname_key = get_property_key(collection.type, "DISPLAYNAME"); let displayname_key = get_property_key(this._collection.type, "DISPLAYNAME");
if (displayname_key && share.Properties[displayname_key] !== undefined) { if (displayname_key && this._share.Properties[displayname_key] !== undefined) {
displayname = share.Properties[displayname_key]; displayname = this._share.Properties[displayname_key];
displayname_override_enabled_value = true; displayname_override_enabled_value = true;
} }
let description_key = get_property_key(collection.type, "DESCRIPTION"); let description_key = get_property_key(this._collection.type, "DESCRIPTION");
if (description_key && share.Properties[description_key] !== undefined) { if (description_key && this._share.Properties[description_key] !== undefined) {
description = share.Properties[description_key]; description = this._share.Properties[description_key];
description_override_enabled_value = true; description_override_enabled_value = true;
} }
let color_key = get_property_key(collection.type, "COLOR"); let color_key = get_property_key(this._collection.type, "COLOR");
if (color_key && share.Properties[color_key] !== undefined) { if (color_key && this._share.Properties[color_key] !== undefined) {
color = share.Properties[color_key]; color = this._share.Properties[color_key];
if (color.length === 9 && color.endsWith("ff")) { if (color.length === 9 && color.endsWith("ff")) {
color = color.substring(0, 7); color = color.substring(0, 7);
} }
@@ -292,78 +297,79 @@ export class CreateEditShareScene {
} }
} }
displayname_override_enabled.checked = displayname_override_enabled_value; this._displayname_override_enabled.checked = displayname_override_enabled_value;
displayname_override_input.value = displayname; this._displayname_override_input.value = displayname;
displayname_override_input.disabled = !displayname_override_enabled_value; this._displayname_override_input.disabled = !displayname_override_enabled_value;
description_override_enabled.checked = description_override_enabled_value; this._description_override_enabled.checked = description_override_enabled_value;
description_override_input.value = description; this._description_override_input.value = description;
description_override_input.disabled = !description_override_enabled_value; this._description_override_input.disabled = !description_override_enabled_value;
color_override_enabled.checked = color_override_enabled_value; this._color_override_enabled.checked = color_override_enabled_value;
color_override_input.value = color; this._color_override_input.value = color;
color_override_input.disabled = !color_override_enabled_value; this._color_override_input.disabled = !color_override_enabled_value;
properties_fieldset.classList.remove("hidden"); this._properties_fieldset.classList.remove("hidden");
if (displayname_override_enabled_value || description_override_enabled_value || color_override_enabled_value) { if (displayname_override_enabled_value || description_override_enabled_value || color_override_enabled_value) {
properties_fieldset.open = true; this._properties_fieldset.open = true;
} }
let displayname_key = get_property_key(collection.type, "DISPLAYNAME"); let displayname_key = get_property_key(this._collection.type, "DISPLAYNAME");
if (!displayname_key) { if (!displayname_key) {
if (displayname_override_enabled.parentElement) { if (this._displayname_override_enabled.parentElement) {
displayname_override_enabled.parentElement.classList.add("hidden"); this._displayname_override_enabled.parentElement.classList.add("hidden");
} }
} else { } else {
if (displayname_override_enabled.parentElement) { if (this._displayname_override_enabled.parentElement) {
displayname_override_enabled.parentElement.classList.remove("hidden"); this._displayname_override_enabled.parentElement.classList.remove("hidden");
} }
} }
let description_key = get_property_key(collection.type, "DESCRIPTION"); let description_key = get_property_key(this._collection.type, "DESCRIPTION");
if (!description_key) { if (!description_key) {
if (description_override_enabled.parentElement) { if (this._description_override_enabled.parentElement) {
description_override_enabled.parentElement.classList.add("hidden"); this._description_override_enabled.parentElement.classList.add("hidden");
} }
} else { } else {
if (description_override_enabled.parentElement) { if (this._description_override_enabled.parentElement) {
description_override_enabled.parentElement.classList.remove("hidden"); this._description_override_enabled.parentElement.classList.remove("hidden");
} }
} }
let color_key = get_property_key(collection.type, "COLOR"); let color_key = get_property_key(this._collection.type, "COLOR");
if (!color_key) { if (!color_key) {
if (color_override_enabled.parentElement) { if (this._color_override_enabled.parentElement) {
color_override_enabled.parentElement.classList.add("hidden"); this._color_override_enabled.parentElement.classList.add("hidden");
} }
} else { } else {
if (color_override_enabled.parentElement) { if (this._color_override_enabled.parentElement) {
color_override_enabled.parentElement.classList.remove("hidden"); this._color_override_enabled.parentElement.classList.remove("hidden");
} }
} }
if (shareType === "map") { if (this._shareType === "map") {
if (edit && share) { if (this._edit && this._share) {
sharehref_input.value = share.PathOrToken.split("/").filter(Boolean).pop() || ""; this._sharehref_input.value = this._share.PathOrToken.split("/").filter(Boolean).pop() || "";
} else { } else {
sharehref_input.value = random_uuid(); this._sharehref_input.value = random_uuid();
} }
sharehref_input.disabled = edit; this._sharehref_input.disabled = this._edit;
sharemapfields.classList.remove("hidden"); this._sharemapfields.classList.remove("hidden");
map_validator.validate(); this._map_validator.validate();
} else { } else {
sharehref_input.value = ""; this._sharehref_input.value = "";
sharemapfields.classList.add("hidden"); this._sharemapfields.classList.add("hidden");
errorHandler.clearError(); this._errorHandler.clearError();
} }
};
this.hide = function () {
html_scene.classList.add("hidden");
cancel_btn.onclick = null;
form.onsubmit = null;
};
this.release = function () {
};
} }
hide() {
this._html_scene.classList.add("hidden");
this._cancel_btn.onclick = null;
this._form.onsubmit = null;
}
release() {
}
is_transient() { return false; }
} }

View File

@@ -42,57 +42,65 @@ 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 { } else {
pop_scene(); pop_scene();
if (on_success) { if (this._on_success) {
on_success(); this._on_success();
} else { } else {
collectionsCache.invalidate(); collectionsCache.invalidate();
pop_scene(); pop_scene();
@@ -105,7 +113,7 @@ export class DeleteConfirmationScene {
return false; return false;
} }
function oncancel() { _oncancel() {
try { try {
pop_scene(); pop_scene();
} catch (err) { } catch (err) {
@@ -114,31 +122,35 @@ export class DeleteConfirmationScene {
return false; return false;
} }
function onkeydown(/** @type {KeyboardEvent}*/ event) { /** @param {KeyboardEvent} event */
_onkeydown(event) {
if (event.code !== "Enter") { if (event.code !== "Enter") {
return; 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,31 +35,30 @@ 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;
@@ -68,14 +67,14 @@ export class IncomingSharingScene {
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;
@@ -88,30 +87,30 @@ export class IncomingSharingScene {
/** /**
* @param {import("../api/sharing.js").Share[]} shares * @param {import("../api/sharing.js").Share[]} shares
*/ */
function render_shares(shares) { _render_shares(shares) {
// clear old nodes // clear old nodes
nodes.forEach(function (node) { this._nodes.forEach(function (node) {
if (node.parentNode) { if (node.parentNode) {
node.parentNode.removeChild(node); node.parentNode.removeChild(node);
} }
}); });
nodes = []; this._nodes = [];
let prefix = "/" + user + "/"; let prefix = "/" + this._user + "/";
let filtered_shares = shares.filter( let filtered_shares = shares.filter(
share => (share.ShareType === "map") share => (share.ShareType === "map")
&& share.PathOrToken.startsWith(prefix)); && share.PathOrToken.startsWith(prefix));
if (filtered_shares.length === 0) { if (filtered_shares.length === 0) {
table.classList.add("hidden"); this._table.classList.add("hidden");
noshares_message.classList.remove("hidden"); this._noshares_message.classList.remove("hidden");
} else { } else {
table.classList.remove("hidden"); this._table.classList.remove("hidden");
noshares_message.classList.add("hidden"); this._noshares_message.classList.add("hidden");
} }
filtered_shares.forEach(function (share) { filtered_shares.forEach((share) => {
let node = /** @type {HTMLElement} */(template.cloneNode(true)); let node = /** @type {HTMLElement} */(this._template.cloneNode(true));
node.classList.remove("hidden"); node.classList.remove("hidden");
let pathortoken = /** @type {HTMLInputElement} */ (get_element(node, "[data-name=pathortoken]")); let pathortoken = /** @type {HTMLInputElement} */ (get_element(node, "[data-name=pathortoken]"));
@@ -136,35 +135,36 @@ export class IncomingSharingScene {
shown_cb.checked = shown; shown_cb.checked = shown;
shown_cb.disabled = !enabled; shown_cb.disabled = !enabled;
enabled_cb.onchange = function () { toggle_share(share, node); }; enabled_cb.onchange = () => { this._toggle_share(share, node); };
shown_cb.onchange = function () { toggle_share(share, node); }; shown_cb.onchange = () => { this._toggle_share(share, node); };
nodes.push(node); this._nodes.push(node);
tbody.appendChild(node); this._tbody.appendChild(node);
}); });
} }
this.show = function () { show() {
html_scene.classList.remove("hidden"); this._html_scene.classList.remove("hidden");
cancel_btn.onclick = on_cancel; this._cancel_btn.onclick = () => pop_scene();
error_handler.clearError(); this._error_handler.clearError();
collectionsCache.getIncomingShares(user, password, error_handler.setError, render_shares); collectionsCache.getIncomingShares(this._user, this._password, this._error_handler.setError, (shares) => this._render_shares(shares));
}; }
this.hide = function () { hide() {
html_scene.classList.add("hidden"); this._html_scene.classList.add("hidden");
cancel_btn.onclick = null; this._cancel_btn.onclick = null;
error_handler.clearError(); this._error_handler.clearError();
}; }
this.release = function () { is_transient() { return false; }
error_handler.clearError();
nodes.forEach(function (node) { release() {
this._error_handler.clearError();
this._nodes.forEach(function (node) {
if (node.parentNode) { if (node.parentNode) {
node.parentNode.removeChild(node); node.parentNode.removeChild(node);
} }
}); });
nodes = []; this._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,113 +31,112 @@ 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() {
user = user_form.value;
} }
function fill_form() { _read_form() {
user_form.value = user; this._user = this._user_form.value;
password_form.value = ""; }
_fill_form() {
this._user_form.value = this._user;
this._password_form.value = "";
} }
/** /**
* @param {string} p_user * @param {string} p_user
* @param {?string} p_password * @param {?string} p_password
*/ */
function perform_login(p_user, p_password) { _perform_login(p_user, p_password) {
user = p_user; this._user = p_user;
fill_form(); this._fill_form();
// setup logout // setup logout
logout_view.classList.remove("hidden"); this._logout_view.classList.remove("hidden");
if (p_password === null) { if (p_password === null) {
logout_btn.classList.add("hidden"); this._logout_btn.classList.add("hidden");
} else { } else {
logout_btn.classList.remove("hidden"); this._logout_btn.classList.remove("hidden");
} }
logout_btn.onclick = onlogout; this._logout_btn.onclick = () => this._onlogout();
refresh_btn.onclick = refresh; this._refresh_btn.onclick = () => this._refresh();
logout_user_form.textContent = user + "'s Collections"; this._logout_user_form.textContent = this._user + "'s Collections";
// Fetch principal // Fetch principal
let loading_scene = new LoadingScene(); let loading_scene = new LoadingScene();
push_scene(loading_scene); push_scene(loading_scene);
principal_req = get_principal(user, p_password, function (/** @type {?Collection} */ principal_collection, error1) { this._principal_req = get_principal(this._user, p_password, (/** @type {?Collection} */ principal_collection, error1) => {
if (!is_current_scene(loading_scene)) { if (!is_current_scene(loading_scene)) {
return; return;
} }
principal_req = null; this._principal_req = null;
if (error1) { if (error1) {
errorHandler.setError(error1); this._errorHandler.setError(error1);
pop_scene(); pop_scene();
} else if (principal_collection) { } else if (principal_collection) {
// show collections // show collections
let saved_user = user; let saved_user = this._user;
user = ""; this._user = "";
let collections_scene = new CollectionsScene( let collections_scene = new CollectionsScene(
saved_user, p_password, principal_collection, function (/** @type {?string} */ error1) { saved_user, p_password, principal_collection, (/** @type {?string} */ error1) => {
errorHandler.setError(error1); this._errorHandler.setError(error1);
user = saved_user; this._user = saved_user;
}); });
replace_scene(collections_scene); replace_scene(collections_scene);
} }
}); });
} }
function onlogin() { _onlogin() {
try { try {
collectionsCache.invalidate(); collectionsCache.invalidate();
read_form(); this._read_form();
let password = password_form.value; let password = this._password_form.value;
if (!validator.validate()) { if (!this._validator.validate()) {
return false; return false;
} }
perform_login(user, password); this._perform_login(this._user, password);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
return false; return false;
} }
let onlogout = function () { _onlogout() {
try { try {
collectionsCache.invalidate(); collectionsCache.invalidate();
user = ""; this._user = "";
pop_to_root(); pop_to_root();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
return false; return false;
};
function remove_logout() {
logout_view.classList.add("hidden");
logout_btn.onclick = null;
refresh_btn.onclick = null;
logout_user_form.textContent = "";
} }
function refresh() { _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 // 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. // forcing the scene below it, the Collections Scene to refresh itself.
collectionsCache.invalidate(); collectionsCache.invalidate();
@@ -145,12 +144,12 @@ export class LoginScene {
pop_scene(); pop_scene();
} }
this.show = function () { show() {
remove_logout(); this._remove_logout();
fill_form(); this._fill_form();
form.onsubmit = onlogin; this._form.onsubmit = () => this._onlogin();
html_scene.classList.remove("hidden"); this._html_scene.classList.remove("hidden");
user_form.focus(); this._user_form.focus();
// Probe for existing authentication (e.g. X-Remote-User) // Probe for existing authentication (e.g. X-Remote-User)
// Use fetch with credentials: 'omit' to avoid browser login prompt on 401 // Use fetch with credentials: 'omit' to avoid browser login prompt on 401
@@ -158,10 +157,10 @@ export class LoginScene {
method: 'PROPFIND', method: 'PROPFIND',
headers: { 'Depth': '0' }, headers: { 'Depth': '0' },
credentials: 'omit' credentials: 'omit'
}).then(function (response) { }).then((response) => {
if (response.ok) { if (response.ok) {
// Authenticated! Now it's safe to call get_principal // Authenticated! Now it's safe to call get_principal
get_principal(null, null, function (/** @type {?Collection} */ principal_collection, error) { get_principal(null, null, (/** @type {?Collection} */ principal_collection, error) => {
if (!error && principal_collection) { if (!error && principal_collection) {
let authenticated_user = principal_collection.displayname; let authenticated_user = principal_collection.displayname;
if (!authenticated_user) { if (!authenticated_user) {
@@ -170,26 +169,29 @@ export class LoginScene {
authenticated_user = href.substring(href.lastIndexOf("/") + 1); authenticated_user = href.substring(href.lastIndexOf("/") + 1);
} }
} }
perform_login(authenticated_user, null); this._perform_login(authenticated_user, null);
} }
}); });
} }
})["catch"](function () { })["catch"](function () {
// Ignore error: we are not authenticated or something else went wrong // 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();
}; 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,21 +43,24 @@ 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]"); _oncancel() {
function oncancel() {
try { try {
pop_scene(); pop_scene();
} catch (err) { } catch (err) {
@@ -66,67 +69,70 @@ export class ShareCollectionScene {
return false; return false;
} }
function onsharebytoken() { _onsharebytoken() {
let create_edit_share_scene = new CreateEditShareScene(user, password, collection, "token"); let create_edit_share_scene = new CreateEditShareScene(this._user, this._password, this._collection, "token");
push_scene(create_edit_share_scene); push_scene(create_edit_share_scene);
} }
function onsharebymap() { _onsharebymap() {
let create_edit_share_scene = new CreateEditShareScene(user, password, collection, "map"); let create_edit_share_scene = new CreateEditShareScene(this._user, this._password, this._collection, "map");
push_scene(create_edit_share_scene); push_scene(create_edit_share_scene);
} }
this.show = function () { show() {
this.release(); this.release();
html_scene.classList.remove("hidden"); this._html_scene.classList.remove("hidden");
html_scene.querySelectorAll("details").forEach(function (details) { this._html_scene.querySelectorAll("details").forEach(function (details) {
details.open = true; details.open = true;
}); });
cancel_btn.onclick = oncancel; this._cancel_btn.onclick = () => this._oncancel();
collectionsCache.getServerFeatures(user, password, errorHandler.setError, (features) => { collectionsCache.getServerFeatures(this._user, this._password, this._errorHandler.setError, (features) => {
if (features.sharing && features.sharing.PermittedCreateCollectionByToken) { if (features.sharing && features.sharing.PermittedCreateCollectionByToken) {
if (share_by_token_btn) { if (this._share_by_token_btn) {
share_by_token_btn.classList.remove("hidden"); this._share_by_token_btn.classList.remove("hidden");
share_by_token_btn.onclick = onsharebytoken; this._share_by_token_btn.onclick = () => this._onsharebytoken();
} }
} else { } else {
if (share_by_token_btn) share_by_token_btn.classList.add("hidden"); if (this._share_by_token_btn) this._share_by_token_btn.classList.add("hidden");
} }
if (features.sharing && features.sharing.FeatureEnabledCollectionByToken) { if (features.sharing && features.sharing.FeatureEnabledCollectionByToken) {
if (share_by_token_div) share_by_token_div.classList.remove("hidden"); if (this._share_by_token_div) this._share_by_token_div.classList.remove("hidden");
} else { } else {
if (share_by_token_div) share_by_token_div.classList.add("hidden"); if (this._share_by_token_div) this._share_by_token_div.classList.add("hidden");
} }
if (features.sharing && features.sharing.PermittedCreateCollectionByMap) { if (features.sharing && features.sharing.PermittedCreateCollectionByMap) {
if (share_by_map_btn) { if (this._share_by_map_btn) {
share_by_map_btn.classList.remove("hidden"); this._share_by_map_btn.classList.remove("hidden");
share_by_map_btn.onclick = onsharebymap; this._share_by_map_btn.onclick = () => this._onsharebymap();
} }
} else { } else {
if (share_by_map_btn) share_by_map_btn.classList.add("hidden"); if (this._share_by_map_btn) this._share_by_map_btn.classList.add("hidden");
} }
if (features.sharing && features.sharing.FeatureEnabledCollectionByMap) { if (features.sharing && features.sharing.FeatureEnabledCollectionByMap) {
if (share_by_map_div) share_by_map_div.classList.remove("hidden"); if (this._share_by_map_div) this._share_by_map_div.classList.remove("hidden");
} else { } else {
if (share_by_map_div) share_by_map_div.classList.add("hidden"); if (this._share_by_map_div) this._share_by_map_div.classList.add("hidden");
} }
}); });
title.textContent = collection.displayname || collection.href; this._title.textContent = this._collection.displayname || this._collection.href;
update_share_list(user, password, collection, errorHandler); update_share_list(this._user, this._password, this._collection, this._errorHandler);
};
this.hide = function () {
html_scene.classList.add("hidden");
cancel_btn.onclick = null;
};
this.release = function () {
};
} }
hide() {
this._html_scene.classList.add("hidden");
this._cancel_btn.onclick = null;
}
release() {
}
is_transient() { return false; }
} }
/** /**

View File

@@ -37,96 +37,101 @@ 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;
/** @type {Array<?string>} */ this._results = [];
/** @type {?Array<HTMLElement>} */ this._nodes = null;
}
_upload_start() {
try { try {
if (!validator.validate()) { if (!this._validator.validate()) {
return false; return false;
} }
if (!files) { if (!this._files) {
return false; return false;
} }
read_form(); this._read_form();
uploadfile_form.classList.add("hidden"); this._uploadfile_form.classList.add("hidden");
uploadfile_lbl.classList.add("hidden"); this._uploadfile_lbl.classList.add("hidden");
href_form.classList.add("hidden"); this._href_form.classList.add("hidden");
href_label.classList.add("hidden"); this._href_label.classList.add("hidden");
hreflimitmsg_html.classList.add("hidden"); this._hreflimitmsg_html.classList.add("hidden");
upload_btn.classList.add("hidden"); this._upload_btn.classList.add("hidden");
close_btn.classList.add("hidden"); this._close_btn.classList.add("hidden");
pending_html.classList.remove("hidden"); this._pending_html.classList.remove("hidden");
nodes = []; this._nodes = [];
for (let i = 0; i < files.length; i++) { for (let i = 0; i < this._files.length; i++) {
let file = files[i]; let file = this._files[i];
let node = /** @type {HTMLElement} */ (template.cloneNode(true)); let node = /** @type {HTMLElement} */ (this._template.cloneNode(true));
node.classList.remove("hidden"); node.classList.remove("hidden");
let name_form = get_element(node, "[data-name=name]"); let name_form = get_element(node, "[data-name=name]");
name_form.textContent = file.name; name_form.textContent = file.name;
node.classList.remove("hidden"); node.classList.remove("hidden");
nodes.push(node); this._nodes.push(node);
updateFileStatus(i); this._updateFileStatus(i);
if (template.parentNode) { if (this._template.parentNode) {
template.parentNode.insertBefore(node, template); this._template.parentNode.insertBefore(node, this._template);
} }
} }
upload_next(); this._upload_next();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
return false; return false;
} }
function upload_next() { _upload_next() {
if (!files) { if (!this._files) {
return; return;
} }
try { try {
if (files.length === results.length) { if (this._files.length === this._results.length) {
pending_html.classList.add("hidden"); this._pending_html.classList.add("hidden");
close_btn.classList.remove("hidden"); this._close_btn.classList.remove("hidden");
return; return;
} else { } else {
let file = files[results.length]; let file = this._files[this._results.length];
if (files.length > 1 || href.length == 0) { if (this._files.length > 1 || this._href.length == 0) {
href = random_uuid(); this._href = random_uuid();
} }
let upload_href = collection.href + href + "/"; let upload_href = this._collection.href + this._href + "/";
upload_req = upload_collection(user, password, upload_href, file, function (result) { this._upload_req = upload_collection(this._user, this._password, upload_href, file, (result) => {
upload_req = null; this._upload_req = null;
results.push(result); this._results.push(result);
updateFileStatus(results.length - 1); this._updateFileStatus(this._results.length - 1);
upload_next(); this._upload_next();
}); });
} }
} catch (err) { } catch (err) {
@@ -134,9 +139,9 @@ export class UploadCollectionScene {
} }
} }
function onclose() { _onclose() {
try { try {
if (results.length > 0) { if (this._results.length > 0) {
collectionsCache.invalidate(); collectionsCache.invalidate();
} }
pop_scene(); pop_scene();
@@ -149,17 +154,17 @@ export class UploadCollectionScene {
/** /**
* @param {number} i * @param {number} i
*/ */
function updateFileStatus(i) { _updateFileStatus(i) {
if (nodes === null) { if (this._nodes === null) {
return; return;
} }
/** @type {HTMLElement} */ let file_success_form = get_element(nodes[i], "[data-name=success]"); /** @type {HTMLElement} */ let file_success_form = get_element(this._nodes[i], "[data-name=success]");
/** @type {HTMLElement} */ let file_error_form = get_element(nodes[i], "[data-name=error]"); /** @type {HTMLElement} */ let file_error_form = get_element(this._nodes[i], "[data-name=error]");
if (results.length > i) { if (this._results.length > i) {
if (results[i]) { if (this._results[i]) {
file_success_form.classList.add("hidden"); file_success_form.classList.add("hidden");
file_error_form.textContent = "Error: " + results[i]; file_error_form.textContent = "Error: " + this._results[i];
error_form.classList.remove("hidden"); this._error_form.classList.remove("hidden");
} else { } else {
file_success_form.classList.remove("hidden"); file_success_form.classList.remove("hidden");
file_error_form.classList.add("hidden"); file_error_form.classList.add("hidden");
@@ -170,68 +175,70 @@ export class UploadCollectionScene {
} }
} }
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 { } else {
hreflimitmsg_html.classList.add("hidden"); this._hreflimitmsg_html.classList.add("hidden");
href_form.classList.remove("hidden"); this._href_form.classList.remove("hidden");
href_label.classList.remove("hidden"); this._href_label.classList.remove("hidden");
if (files && files.length > 0) { if (this._files && this._files.length > 0) {
href_form.value = files[0].name.replace(/\.(ics|vcf)$/, ''); this._href_form.value = this._files[0].name.replace(/\.(ics|vcf)$/, '');
} else { } else {
href_form.value = ""; this._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;
} }
nodes.forEach(function (node) { this._nodes.forEach(function (node) {
if (node.parentNode) { if (node.parentNode) {
node.parentNode.removeChild(node); node.parentNode.removeChild(node);
} }
}); });
nodes = null; this._nodes = null;
}; }
this.release = function () {
if (upload_req !== null) { is_transient() { return false; }
upload_req.abort();
upload_req = null; release() {
if (this._upload_req !== null) {
this._upload_req.abort();
this._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");