mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-13 13:39:27 +02:00
Add bookmark details view (#665)
* Experiment with bookmark details * Add basic tests * Refactor details into modal * Implement edit and delete button * Remove slide down animation * Add fallback details view * Add status actions * Improve dark theme * Improve return URLs * Make bookmark details sharable * Fix E2E tests
This commit is contained in:
38
bookmarks/frontend/behaviors/bookmark-details.js
Normal file
38
bookmarks/frontend/behaviors/bookmark-details.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { registerBehavior } from "./index";
|
||||
|
||||
class BookmarkDetails {
|
||||
constructor(element) {
|
||||
this.form = element.querySelector(".status form");
|
||||
if (!this.form) {
|
||||
// Form may not exist if user does not own the bookmark
|
||||
return;
|
||||
}
|
||||
this.form.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
this.submitForm();
|
||||
});
|
||||
|
||||
const inputs = this.form.querySelectorAll("input");
|
||||
inputs.forEach((input) => {
|
||||
input.addEventListener("change", () => {
|
||||
this.submitForm();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async submitForm() {
|
||||
const url = this.form.action;
|
||||
const formData = new FormData(this.form);
|
||||
|
||||
await fetch(url, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
redirect: "manual", // ignore redirect
|
||||
});
|
||||
|
||||
// Refresh bookmark page if it exists
|
||||
document.dispatchEvent(new CustomEvent("bookmark-page-refresh"));
|
||||
}
|
||||
}
|
||||
|
||||
registerBehavior("ld-bookmark-details", BookmarkDetails);
|
@@ -8,6 +8,10 @@ class BookmarkPage {
|
||||
|
||||
this.bookmarkList = element.querySelector(".bookmark-list-container");
|
||||
this.tagCloud = element.querySelector(".tag-cloud-container");
|
||||
|
||||
document.addEventListener("bookmark-page-refresh", () => {
|
||||
this.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
async onFormSubmit(event) {
|
||||
|
@@ -38,10 +38,14 @@ class ConfirmButtonBehavior {
|
||||
container.append(question);
|
||||
}
|
||||
|
||||
const buttonClasses = Array.from(this.button.classList.values())
|
||||
.filter((cls) => cls.startsWith("btn"))
|
||||
.join(" ");
|
||||
|
||||
const cancelButton = document.createElement(this.button.nodeName);
|
||||
cancelButton.type = "button";
|
||||
cancelButton.innerText = question ? "No" : "Cancel";
|
||||
cancelButton.className = "btn btn-link btn-sm mr-1";
|
||||
cancelButton.className = `${buttonClasses} mr-1`;
|
||||
cancelButton.addEventListener("click", this.reset.bind(this));
|
||||
|
||||
const confirmButton = document.createElement(this.button.nodeName);
|
||||
@@ -49,7 +53,7 @@ class ConfirmButtonBehavior {
|
||||
confirmButton.name = this.button.dataset.name;
|
||||
confirmButton.value = this.button.dataset.value;
|
||||
confirmButton.innerText = question ? "Yes" : "Confirm";
|
||||
confirmButton.className = "btn btn-link btn-sm";
|
||||
confirmButton.className = buttonClasses;
|
||||
confirmButton.addEventListener("click", this.reset.bind(this));
|
||||
|
||||
container.append(cancelButton, confirmButton);
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { registerBehavior } from "./index";
|
||||
import { applyBehaviors, registerBehavior } from "./index";
|
||||
|
||||
class ModalBehavior {
|
||||
constructor(element) {
|
||||
@@ -7,14 +7,50 @@ class ModalBehavior {
|
||||
this.toggle = toggle;
|
||||
}
|
||||
|
||||
onToggleClick() {
|
||||
async onToggleClick(event) {
|
||||
// Ignore Ctrl + click
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Create modal either by teleporting existing content or fetching from URL
|
||||
const modal = this.toggle.hasAttribute("modal-content")
|
||||
? this.createFromContent()
|
||||
: await this.createFromUrl();
|
||||
|
||||
if (!modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register close handlers
|
||||
const modalOverlay = modal.querySelector(".modal-overlay");
|
||||
const closeButton = modal.querySelector("button.close");
|
||||
modalOverlay.addEventListener("click", this.onClose.bind(this));
|
||||
closeButton.addEventListener("click", this.onClose.bind(this));
|
||||
|
||||
document.body.append(modal);
|
||||
applyBehaviors(document.body);
|
||||
this.modal = modal;
|
||||
}
|
||||
|
||||
async createFromUrl() {
|
||||
const url = this.toggle.getAttribute("modal-url");
|
||||
const modalHtml = await fetch(url).then((response) => response.text());
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(modalHtml, "text/html");
|
||||
return doc.querySelector(".modal");
|
||||
}
|
||||
|
||||
createFromContent() {
|
||||
const contentSelector = this.toggle.getAttribute("modal-content");
|
||||
const content = document.querySelector(contentSelector);
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create modal
|
||||
// Todo: make title configurable, only used for tag cloud for now
|
||||
const modal = document.createElement("div");
|
||||
modal.classList.add("modal", "active");
|
||||
modal.innerHTML = `
|
||||
@@ -22,7 +58,7 @@ class ModalBehavior {
|
||||
<div class="modal-container">
|
||||
<div class="modal-header d-flex justify-between align-center">
|
||||
<div class="modal-title h5">Tags</div>
|
||||
<button class="btn btn-link close">
|
||||
<button class="close">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M18 6l-12 12"></path>
|
||||
@@ -36,29 +72,28 @@ class ModalBehavior {
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Teleport content element
|
||||
const contentOwner = content.parentElement;
|
||||
const contentContainer = modal.querySelector(".content");
|
||||
contentContainer.append(content);
|
||||
this.content = content;
|
||||
this.contentOwner = contentOwner;
|
||||
|
||||
// Register close handlers
|
||||
const modalOverlay = modal.querySelector(".modal-overlay");
|
||||
const closeButton = modal.querySelector(".btn.close");
|
||||
modalOverlay.addEventListener("click", this.onClose.bind(this));
|
||||
closeButton.addEventListener("click", this.onClose.bind(this));
|
||||
|
||||
document.body.append(modal);
|
||||
this.modal = modal;
|
||||
return modal;
|
||||
}
|
||||
|
||||
onClose() {
|
||||
// Teleport content back
|
||||
this.contentOwner.append(this.content);
|
||||
if (this.content && this.contentOwner) {
|
||||
this.contentOwner.append(this.content);
|
||||
}
|
||||
|
||||
// Remove modal
|
||||
this.modal.remove();
|
||||
this.modal.classList.add("closing");
|
||||
this.modal.addEventListener("animationend", (event) => {
|
||||
if (event.animationName === "fade-out") {
|
||||
this.modal.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user