mirror of
https://github.com/sissbruecker/linkding
synced 2024-11-24 20:33:04 +00:00
Refactor client-side fetch logic (#693)
* extract generic behaviors * preserve query string when refreshing content * refactor details modal refresh * refactor bulk edit * update tests * restore tag modal * Make IntelliJ aware of custom attributes * improve e2e test coverage
This commit is contained in:
parent
82f86bf537
commit
65f0eb2a04
32 changed files with 630 additions and 418 deletions
|
@ -1,3 +1,4 @@
|
|||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from playwright.sync_api import sync_playwright, expect
|
||||
|
||||
|
@ -44,14 +45,17 @@ class BookmarkDetailsModalE2ETestCase(LinkdingE2ETestCase):
|
|||
details_modal = self.open_details_modal(bookmark)
|
||||
details_modal.get_by_text("Archived", exact=False).click()
|
||||
expect(self.locate_bookmark(bookmark.title)).not_to_be_visible()
|
||||
self.assertReloads(0)
|
||||
|
||||
# unarchive
|
||||
url = reverse("bookmarks:archived")
|
||||
self.page.goto(self.live_server_url + url)
|
||||
self.resetReloads()
|
||||
|
||||
details_modal = self.open_details_modal(bookmark)
|
||||
details_modal.get_by_text("Archived", exact=False).click()
|
||||
expect(self.locate_bookmark(bookmark.title)).not_to_be_visible()
|
||||
self.assertReloads(0)
|
||||
|
||||
def test_toggle_unread(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
@ -66,11 +70,13 @@ class BookmarkDetailsModalE2ETestCase(LinkdingE2ETestCase):
|
|||
details_modal.get_by_text("Unread").click()
|
||||
bookmark_item = self.locate_bookmark(bookmark.title)
|
||||
expect(bookmark_item.get_by_text("Unread")).to_be_visible()
|
||||
self.assertReloads(0)
|
||||
|
||||
# mark as read
|
||||
details_modal.get_by_text("Unread").click()
|
||||
bookmark_item = self.locate_bookmark(bookmark.title)
|
||||
expect(bookmark_item.get_by_text("Unread")).not_to_be_visible()
|
||||
self.assertReloads(0)
|
||||
|
||||
def test_toggle_shared(self):
|
||||
profile = self.get_or_create_test_user().profile
|
||||
|
@ -89,11 +95,13 @@ class BookmarkDetailsModalE2ETestCase(LinkdingE2ETestCase):
|
|||
details_modal.get_by_text("Shared").click()
|
||||
bookmark_item = self.locate_bookmark(bookmark.title)
|
||||
expect(bookmark_item.get_by_text("Shared")).to_be_visible()
|
||||
self.assertReloads(0)
|
||||
|
||||
# unshare bookmark
|
||||
details_modal.get_by_text("Shared").click()
|
||||
bookmark_item = self.locate_bookmark(bookmark.title)
|
||||
expect(bookmark_item.get_by_text("Shared")).not_to_be_visible()
|
||||
self.assertReloads(0)
|
||||
|
||||
def test_edit_return_url(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
@ -131,3 +139,33 @@ class BookmarkDetailsModalE2ETestCase(LinkdingE2ETestCase):
|
|||
expect(self.locate_bookmark(bookmark.title)).not_to_be_visible()
|
||||
|
||||
self.assertEqual(Bookmark.objects.count(), 0)
|
||||
|
||||
@override_settings(LD_ENABLE_SNAPSHOTS=True)
|
||||
def test_create_snapshot_remove_snapshot(self):
|
||||
bookmark = self.setup_bookmark()
|
||||
|
||||
with sync_playwright() as p:
|
||||
url = reverse("bookmarks:index") + f"?q={bookmark.title}"
|
||||
self.open(url, p)
|
||||
|
||||
details_modal = self.open_details_modal(bookmark)
|
||||
asset_list = details_modal.locator(".assets")
|
||||
|
||||
# No snapshots initially
|
||||
snapshot = asset_list.get_by_text("HTML snapshot from", exact=False)
|
||||
expect(snapshot).not_to_be_visible()
|
||||
|
||||
# Create snapshot
|
||||
details_modal.get_by_text("Create HTML snapshot", exact=False).click()
|
||||
self.assertReloads(0)
|
||||
|
||||
# Has new snapshots
|
||||
expect(snapshot).to_be_visible()
|
||||
|
||||
# Create snapshot
|
||||
asset_list.get_by_text("Remove", exact=False).click()
|
||||
asset_list.get_by_text("Confirm", exact=False).click()
|
||||
|
||||
# Snapshot is removed
|
||||
expect(snapshot).not_to_be_visible()
|
||||
self.assertReloads(0)
|
||||
|
|
|
@ -194,7 +194,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
|
||||
checkboxes = page.locator("label[ld-bulk-edit-checkbox] input")
|
||||
checkboxes = page.locator("label.bulk-edit-checkbox input")
|
||||
self.assertEqual(6, checkboxes.count())
|
||||
for i in range(checkboxes.count()):
|
||||
expect(checkboxes.nth(i)).not_to_be_checked()
|
||||
|
@ -264,13 +264,13 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||
|
||||
# Hide select across by toggling a single bookmark
|
||||
self.locate_bookmark("Bookmark 1").locator(
|
||||
"label[ld-bulk-edit-checkbox]"
|
||||
"label.bulk-edit-checkbox"
|
||||
).click()
|
||||
expect(self.locate_bulk_edit_select_across()).not_to_be_visible()
|
||||
|
||||
# Show select across again, verify it is unchecked
|
||||
self.locate_bookmark("Bookmark 1").locator(
|
||||
"label[ld-bulk-edit-checkbox]"
|
||||
"label.bulk-edit-checkbox"
|
||||
).click()
|
||||
expect(self.locate_bulk_edit_select_across()).not_to_be_checked()
|
||||
|
||||
|
@ -297,7 +297,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||
expect(bookmark_list).not_to_be_visible()
|
||||
|
||||
# Verify bulk edit checkboxes are reset
|
||||
checkboxes = page.locator("label[ld-bulk-edit-checkbox] input")
|
||||
checkboxes = page.locator("label.bulk-edit-checkbox input")
|
||||
self.assertEqual(31, checkboxes.count())
|
||||
for i in range(checkboxes.count()):
|
||||
expect(checkboxes.nth(i)).not_to_be_checked()
|
||||
|
|
|
@ -169,7 +169,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
self.locate_bookmark("Bookmark 2").locator(
|
||||
"label[ld-bulk-edit-checkbox]"
|
||||
"label.bulk-edit-checkbox"
|
||||
).click()
|
||||
self.select_bulk_action("Archive")
|
||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||
|
@ -187,7 +187,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
self.locate_bookmark("Bookmark 2").locator(
|
||||
"label[ld-bulk-edit-checkbox]"
|
||||
"label.bulk-edit-checkbox"
|
||||
).click()
|
||||
self.select_bulk_action("Delete")
|
||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||
|
@ -230,7 +230,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
self.locate_bookmark("Archived Bookmark 2").locator(
|
||||
"label[ld-bulk-edit-checkbox]"
|
||||
"label.bulk-edit-checkbox"
|
||||
).click()
|
||||
self.select_bulk_action("Unarchive")
|
||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||
|
@ -248,7 +248,7 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
|
|||
|
||||
self.locate_bulk_edit_toggle().click()
|
||||
self.locate_bookmark("Archived Bookmark 2").locator(
|
||||
"label[ld-bulk-edit-checkbox]"
|
||||
"label.bulk-edit-checkbox"
|
||||
).click()
|
||||
self.select_bulk_action("Delete")
|
||||
self.locate_bulk_edit_bar().get_by_text("Execute").click()
|
||||
|
|
76
bookmarks/e2e/e2e_test_tag_cloud_modal.py
Normal file
76
bookmarks/e2e/e2e_test_tag_cloud_modal.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from playwright.sync_api import sync_playwright, expect, Locator
|
||||
|
||||
from bookmarks.e2e.helpers import LinkdingE2ETestCase
|
||||
from bookmarks.models import Bookmark
|
||||
|
||||
|
||||
class TagCloudModalE2ETestCase(LinkdingE2ETestCase):
|
||||
def test_show_modal_close_modal(self):
|
||||
self.setup_bookmark(tags=[self.setup_tag(name="cooking")])
|
||||
self.setup_bookmark(tags=[self.setup_tag(name="hiking")])
|
||||
|
||||
with sync_playwright() as p:
|
||||
page = self.open(reverse("bookmarks:index"), p)
|
||||
|
||||
# use smaller viewport to make tags button visible
|
||||
page.set_viewport_size({"width": 375, "height": 812})
|
||||
|
||||
# open tag cloud modal
|
||||
modal_trigger = page.locator(".content-area-header").get_by_role(
|
||||
"button", name="Tags"
|
||||
)
|
||||
modal_trigger.click()
|
||||
|
||||
# verify modal is visible
|
||||
modal = page.locator(".modal")
|
||||
expect(modal).to_be_visible()
|
||||
expect(modal.locator(".modal-title")).to_have_text("Tags")
|
||||
|
||||
# close with close button
|
||||
modal.locator("button.close").click()
|
||||
expect(modal).to_be_hidden()
|
||||
|
||||
# open modal again
|
||||
modal_trigger.click()
|
||||
|
||||
# close with backdrop
|
||||
backdrop = modal.locator(".modal-overlay")
|
||||
backdrop.click(position={"x": 0, "y": 0})
|
||||
expect(modal).to_be_hidden()
|
||||
|
||||
def test_select_tag(self):
|
||||
self.setup_bookmark(tags=[self.setup_tag(name="cooking")])
|
||||
self.setup_bookmark(tags=[self.setup_tag(name="hiking")])
|
||||
|
||||
with sync_playwright() as p:
|
||||
page = self.open(reverse("bookmarks:index"), p)
|
||||
|
||||
# use smaller viewport to make tags button visible
|
||||
page.set_viewport_size({"width": 375, "height": 812})
|
||||
|
||||
# open tag cloud modal
|
||||
modal_trigger = page.locator(".content-area-header").get_by_role(
|
||||
"button", name="Tags"
|
||||
)
|
||||
modal_trigger.click()
|
||||
|
||||
# verify tags are displayed
|
||||
modal = page.locator(".modal")
|
||||
unselected_tags = modal.locator(".unselected-tags")
|
||||
expect(unselected_tags.get_by_text("cooking")).to_be_visible()
|
||||
expect(unselected_tags.get_by_text("hiking")).to_be_visible()
|
||||
|
||||
# select tag
|
||||
unselected_tags.get_by_text("cooking").click()
|
||||
|
||||
# open modal again
|
||||
modal_trigger.click()
|
||||
|
||||
# verify tag is selected, other tag is not visible anymore
|
||||
selected_tags = modal.locator(".selected-tags")
|
||||
expect(selected_tags.get_by_text("cooking")).to_be_visible()
|
||||
|
||||
expect(unselected_tags.get_by_text("cooking")).not_to_be_visible()
|
||||
expect(unselected_tags.get_by_text("hiking")).not_to_be_visible()
|
|
@ -39,6 +39,9 @@ class LinkdingE2ETestCase(LiveServerTestCase, BookmarkFactoryMixin):
|
|||
def assertReloads(self, count: int):
|
||||
self.assertEqual(self.num_loads, count)
|
||||
|
||||
def resetReloads(self):
|
||||
self.num_loads = 0
|
||||
|
||||
def locate_bookmark_list(self):
|
||||
return self.page.locator("ul[ld-bookmark-list]")
|
||||
|
||||
|
@ -62,7 +65,7 @@ class LinkdingE2ETestCase(LiveServerTestCase, BookmarkFactoryMixin):
|
|||
return self.page.locator(".bulk-edit-bar")
|
||||
|
||||
def locate_bulk_edit_select_all(self):
|
||||
return self.locate_bulk_edit_bar().locator("label[ld-bulk-edit-checkbox][all]")
|
||||
return self.locate_bulk_edit_bar().locator("label.bulk-edit-checkbox.all")
|
||||
|
||||
def locate_bulk_edit_select_across(self):
|
||||
return self.locate_bulk_edit_bar().locator("label.select-across")
|
||||
|
|
|
@ -1,63 +1,4 @@
|
|||
import { registerBehavior, swapContent } from "./index";
|
||||
|
||||
class BookmarkPage {
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
this.form = element.querySelector("form.bookmark-actions");
|
||||
this.form.addEventListener("submit", this.onFormSubmit.bind(this));
|
||||
|
||||
this.bookmarkList = element.querySelector(".bookmark-list-container");
|
||||
this.tagCloud = element.querySelector(".tag-cloud-container");
|
||||
|
||||
document.addEventListener("bookmark-page-refresh", () => {
|
||||
this.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
async onFormSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const url = this.form.action;
|
||||
const formData = new FormData(this.form);
|
||||
formData.append(event.submitter.name, event.submitter.value);
|
||||
|
||||
await fetch(url, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
redirect: "manual", // ignore redirect
|
||||
});
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
const query = window.location.search;
|
||||
const bookmarksUrl = this.element.getAttribute("bookmarks-url");
|
||||
const tagsUrl = this.element.getAttribute("tags-url");
|
||||
Promise.all([
|
||||
fetch(`${bookmarksUrl}${query}`).then((response) => response.text()),
|
||||
fetch(`${tagsUrl}${query}`).then((response) => response.text()),
|
||||
]).then(([bookmarkListHtml, tagCloudHtml]) => {
|
||||
swapContent(this.bookmarkList, bookmarkListHtml);
|
||||
swapContent(this.tagCloud, tagCloudHtml);
|
||||
|
||||
// Dispatch list updated event
|
||||
const listElement = this.bookmarkList.querySelector(
|
||||
"ul[data-bookmarks-total]",
|
||||
);
|
||||
const bookmarksTotal =
|
||||
(listElement && listElement.dataset.bookmarksTotal) || 0;
|
||||
|
||||
this.bookmarkList.dispatchEvent(
|
||||
new CustomEvent("bookmark-list-updated", {
|
||||
bubbles: true,
|
||||
detail: { bookmarksTotal },
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerBehavior("ld-bookmark-page", BookmarkPage);
|
||||
import { registerBehavior } from "./index";
|
||||
|
||||
class BookmarkItem {
|
||||
constructor(element) {
|
||||
|
|
|
@ -4,43 +4,56 @@ class BulkEdit {
|
|||
constructor(element) {
|
||||
this.element = element;
|
||||
this.active = false;
|
||||
this.actionSelect = element.querySelector("select[name='bulk_action']");
|
||||
this.tagAutoComplete = element.querySelector(".tag-autocomplete");
|
||||
this.selectAcross = element.querySelector("label.select-across");
|
||||
|
||||
element.addEventListener(
|
||||
"bulk-edit-toggle-active",
|
||||
this.onToggleActive.bind(this),
|
||||
);
|
||||
element.addEventListener(
|
||||
"bulk-edit-toggle-all",
|
||||
this.onToggleAll.bind(this),
|
||||
);
|
||||
element.addEventListener(
|
||||
"bulk-edit-toggle-bookmark",
|
||||
this.onToggleBookmark.bind(this),
|
||||
);
|
||||
element.addEventListener(
|
||||
"bookmark-list-updated",
|
||||
this.onListUpdated.bind(this),
|
||||
);
|
||||
this.onToggleActive = this.onToggleActive.bind(this);
|
||||
this.onToggleAll = this.onToggleAll.bind(this);
|
||||
this.onToggleBookmark = this.onToggleBookmark.bind(this);
|
||||
this.onActionSelected = this.onActionSelected.bind(this);
|
||||
|
||||
this.actionSelect.addEventListener(
|
||||
"change",
|
||||
this.onActionSelected.bind(this),
|
||||
);
|
||||
this.init();
|
||||
// Reset when bookmarks are refreshed
|
||||
document.addEventListener("refresh-bookmark-list-done", () => this.init());
|
||||
}
|
||||
|
||||
get allCheckbox() {
|
||||
return this.element.querySelector("[ld-bulk-edit-checkbox][all] input");
|
||||
}
|
||||
init() {
|
||||
// Update elements
|
||||
this.activeToggle = this.element.querySelector(".bulk-edit-active-toggle");
|
||||
this.actionSelect = this.element.querySelector(
|
||||
"select[name='bulk_action']",
|
||||
);
|
||||
this.tagAutoComplete = this.element.querySelector(".tag-autocomplete");
|
||||
this.selectAcross = this.element.querySelector("label.select-across");
|
||||
this.allCheckbox = this.element.querySelector(
|
||||
".bulk-edit-checkbox.all input",
|
||||
);
|
||||
this.bookmarkCheckboxes = Array.from(
|
||||
this.element.querySelectorAll(".bulk-edit-checkbox:not(.all) input"),
|
||||
);
|
||||
|
||||
get bookmarkCheckboxes() {
|
||||
return [
|
||||
...this.element.querySelectorAll(
|
||||
"[ld-bulk-edit-checkbox]:not([all]) input",
|
||||
),
|
||||
];
|
||||
// Remove previous listeners if elements are the same
|
||||
this.activeToggle.removeEventListener("click", this.onToggleActive);
|
||||
this.actionSelect.removeEventListener("change", this.onActionSelected);
|
||||
this.allCheckbox.removeEventListener("change", this.onToggleAll);
|
||||
this.bookmarkCheckboxes.forEach((checkbox) => {
|
||||
checkbox.removeEventListener("change", this.onToggleBookmark);
|
||||
});
|
||||
|
||||
// Reset checkbox states
|
||||
this.reset();
|
||||
|
||||
// Update total number of bookmarks
|
||||
const totalHolder = this.element.querySelector("[data-bookmarks-total]");
|
||||
const total = totalHolder?.dataset.bookmarksTotal || 0;
|
||||
const totalSpan = this.selectAcross.querySelector("span.total");
|
||||
totalSpan.textContent = total;
|
||||
|
||||
// Add new listeners
|
||||
this.activeToggle.addEventListener("click", this.onToggleActive);
|
||||
this.actionSelect.addEventListener("change", this.onActionSelected);
|
||||
this.allCheckbox.addEventListener("change", this.onToggleAll);
|
||||
this.bookmarkCheckboxes.forEach((checkbox) => {
|
||||
checkbox.addEventListener("change", this.onToggleBookmark);
|
||||
});
|
||||
}
|
||||
|
||||
onToggleActive() {
|
||||
|
@ -81,16 +94,6 @@ class BulkEdit {
|
|||
}
|
||||
}
|
||||
|
||||
onListUpdated(event) {
|
||||
// Reset checkbox states
|
||||
this.reset();
|
||||
|
||||
// Update total number of bookmarks
|
||||
const total = event.detail.bookmarksTotal;
|
||||
const totalSpan = this.selectAcross.querySelector("span.total");
|
||||
totalSpan.textContent = total;
|
||||
}
|
||||
|
||||
updateSelectAcross(allChecked) {
|
||||
if (allChecked) {
|
||||
this.selectAcross.classList.remove("d-none");
|
||||
|
@ -109,33 +112,4 @@ class BulkEdit {
|
|||
}
|
||||
}
|
||||
|
||||
class BulkEditActiveToggle {
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
element.addEventListener("click", this.onClick.bind(this));
|
||||
}
|
||||
|
||||
onClick() {
|
||||
this.element.dispatchEvent(
|
||||
new CustomEvent("bulk-edit-toggle-active", { bubbles: true }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BulkEditCheckbox {
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
element.addEventListener("change", this.onChange.bind(this));
|
||||
}
|
||||
|
||||
onChange() {
|
||||
const type = this.element.hasAttribute("all") ? "all" : "bookmark";
|
||||
this.element.dispatchEvent(
|
||||
new CustomEvent(`bulk-edit-toggle-${type}`, { bubbles: true }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
registerBehavior("ld-bulk-edit", BulkEdit);
|
||||
registerBehavior("ld-bulk-edit-active-toggle", BulkEditActiveToggle);
|
||||
registerBehavior("ld-bulk-edit-checkbox", BulkEditCheckbox);
|
||||
|
|
|
@ -19,7 +19,7 @@ class ConfirmButtonBehavior {
|
|||
const container = document.createElement("span");
|
||||
container.className = "confirmation";
|
||||
|
||||
const icon = this.button.getAttribute("confirm-icon");
|
||||
const icon = this.button.getAttribute("ld-confirm-icon");
|
||||
if (icon) {
|
||||
const iconElement = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
|
@ -31,7 +31,7 @@ class ConfirmButtonBehavior {
|
|||
container.append(iconElement);
|
||||
}
|
||||
|
||||
const question = this.button.getAttribute("confirm-question");
|
||||
const question = this.button.getAttribute("ld-confirm-question");
|
||||
if (question) {
|
||||
const questionElement = document.createElement("span");
|
||||
questionElement.innerText = question;
|
||||
|
|
25
bookmarks/frontend/behaviors/fetch.js
Normal file
25
bookmarks/frontend/behaviors/fetch.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { fireEvents, registerBehavior, swap } from "./index";
|
||||
|
||||
class FetchBehavior {
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
const eventName = element.getAttribute("ld-on");
|
||||
|
||||
element.addEventListener(eventName, this.onFetch.bind(this));
|
||||
}
|
||||
|
||||
async onFetch(event) {
|
||||
event.preventDefault();
|
||||
const url = this.element.getAttribute("ld-fetch");
|
||||
const html = await fetch(url).then((response) => response.text());
|
||||
|
||||
const target = this.element.getAttribute("ld-target");
|
||||
const select = this.element.getAttribute("ld-select");
|
||||
swap(this.element, html, { target, select });
|
||||
|
||||
const events = this.element.getAttribute("ld-fire");
|
||||
fireEvents(events);
|
||||
}
|
||||
}
|
||||
|
||||
registerBehavior("ld-fetch", FetchBehavior);
|
|
@ -1,12 +1,12 @@
|
|||
import { registerBehavior, swap } from "./index";
|
||||
import { fireEvents, registerBehavior } from "./index";
|
||||
|
||||
class FormBehavior {
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
element.addEventListener("submit", this.onFormSubmit.bind(this));
|
||||
element.addEventListener("submit", this.onSubmit.bind(this));
|
||||
}
|
||||
|
||||
async onFormSubmit(event) {
|
||||
async onSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const url = this.element.action;
|
||||
|
@ -21,34 +21,21 @@ class FormBehavior {
|
|||
redirect: "manual", // ignore redirect
|
||||
});
|
||||
|
||||
// Dispatch refresh events
|
||||
const refreshEvents = this.element.getAttribute("refresh-events");
|
||||
if (refreshEvents) {
|
||||
refreshEvents.split(",").forEach((eventName) => {
|
||||
document.dispatchEvent(new CustomEvent(eventName));
|
||||
});
|
||||
const events = this.element.getAttribute("ld-fire");
|
||||
if (fireEvents) {
|
||||
fireEvents(events);
|
||||
}
|
||||
|
||||
// Refresh form
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
const refreshUrl = this.element.getAttribute("refresh-url");
|
||||
const html = await fetch(refreshUrl).then((response) => response.text());
|
||||
swap(this.element, html);
|
||||
}
|
||||
}
|
||||
|
||||
class FormAutoSubmitBehavior {
|
||||
class AutoSubmitBehavior {
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
this.element.addEventListener("change", () => {
|
||||
const form = this.element.closest("form");
|
||||
element.addEventListener("change", () => {
|
||||
const form = element.closest("form");
|
||||
form.dispatchEvent(new Event("submit", { cancelable: true }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerBehavior("ld-form", FormBehavior);
|
||||
registerBehavior("ld-form-auto-submit", FormAutoSubmitBehavior);
|
||||
registerBehavior("ld-auto-submit", AutoSubmitBehavior);
|
||||
|
|
|
@ -37,14 +37,49 @@ export function applyBehaviors(container, behaviorNames = null) {
|
|||
});
|
||||
}
|
||||
|
||||
export function swap(element, html) {
|
||||
export function swap(element, html, options) {
|
||||
const dom = new DOMParser().parseFromString(html, "text/html");
|
||||
const newElement = dom.body.firstChild;
|
||||
element.replaceWith(newElement);
|
||||
applyBehaviors(newElement);
|
||||
|
||||
let targetElement = element;
|
||||
let strategy = "innerHTML";
|
||||
if (options.target) {
|
||||
const parts = options.target.split("|");
|
||||
targetElement =
|
||||
parts[0] === "self" ? element : document.querySelector(parts[0]);
|
||||
strategy = parts[1] || "innerHTML";
|
||||
}
|
||||
|
||||
let contents = Array.from(dom.body.children);
|
||||
if (options.select) {
|
||||
contents = Array.from(dom.querySelectorAll(options.select));
|
||||
}
|
||||
|
||||
switch (strategy) {
|
||||
case "append":
|
||||
targetElement.append(...contents);
|
||||
break;
|
||||
case "outerHTML":
|
||||
targetElement.parentElement.replaceChild(contents[0], targetElement);
|
||||
break;
|
||||
case "innerHTML":
|
||||
default:
|
||||
targetElement.innerHTML = "";
|
||||
targetElement.append(...contents);
|
||||
}
|
||||
contents.forEach((content) => applyBehaviors(content));
|
||||
}
|
||||
|
||||
export function swapContent(element, html) {
|
||||
element.innerHTML = html;
|
||||
applyBehaviors(element);
|
||||
export function fireEvents(events) {
|
||||
if (!events) {
|
||||
return;
|
||||
}
|
||||
events.split(",").forEach((eventName) => {
|
||||
const targets = Array.from(
|
||||
document.querySelectorAll(`[ld-on='${eventName}']`),
|
||||
);
|
||||
targets.push(document);
|
||||
targets.forEach((target) => {
|
||||
target.dispatchEvent(new CustomEvent(eventName));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,97 +1,20 @@
|
|||
import { applyBehaviors, registerBehavior } from "./index";
|
||||
import { registerBehavior } from "./index";
|
||||
|
||||
class ModalBehavior {
|
||||
constructor(element) {
|
||||
const toggle = element;
|
||||
toggle.addEventListener("click", this.onToggleClick.bind(this));
|
||||
this.toggle = toggle;
|
||||
}
|
||||
this.element = element;
|
||||
|
||||
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");
|
||||
const modalOverlay = element.querySelector(".modal-overlay");
|
||||
const closeButton = element.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;
|
||||
}
|
||||
|
||||
// Todo: make title configurable, only used for tag cloud for now
|
||||
const modal = document.createElement("div");
|
||||
modal.classList.add("modal", "active");
|
||||
modal.innerHTML = `
|
||||
<div class="modal-overlay" aria-label="Close"></div>
|
||||
<div class="modal-container">
|
||||
<div class="modal-header d-flex justify-between align-center">
|
||||
<div class="modal-title h5">Tags</div>
|
||||
<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>
|
||||
<path d="M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="content"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const contentOwner = content.parentElement;
|
||||
const contentContainer = modal.querySelector(".content");
|
||||
contentContainer.append(content);
|
||||
this.content = content;
|
||||
this.contentOwner = contentOwner;
|
||||
|
||||
return modal;
|
||||
}
|
||||
|
||||
onClose() {
|
||||
// Teleport content back
|
||||
if (this.content && this.contentOwner) {
|
||||
this.contentOwner.append(this.content);
|
||||
}
|
||||
|
||||
// Remove modal
|
||||
this.modal.classList.add("closing");
|
||||
this.modal.addEventListener("animationend", (event) => {
|
||||
this.element.classList.add("closing");
|
||||
this.element.addEventListener("animationend", (event) => {
|
||||
if (event.animationName === "fade-out") {
|
||||
this.modal.remove();
|
||||
this.element.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import "./behaviors/bookmark-page";
|
|||
import "./behaviors/bulk-edit";
|
||||
import "./behaviors/confirm-button";
|
||||
import "./behaviors/dropdown";
|
||||
import "./behaviors/fetch";
|
||||
import "./behaviors/form";
|
||||
import "./behaviors/modal";
|
||||
import "./behaviors/global-shortcuts";
|
||||
|
|
|
@ -130,7 +130,7 @@ li[ld-bookmark-item] {
|
|||
position: relative;
|
||||
margin-top: $unit-2;
|
||||
|
||||
[ld-bulk-edit-checkbox].form-checkbox {
|
||||
.form-checkbox.bulk-edit-checkbox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -323,7 +323,7 @@ $bulk-edit-transition-duration: 400ms;
|
|||
}
|
||||
|
||||
/* All checkbox */
|
||||
[ld-bulk-edit-checkbox][all].form-checkbox {
|
||||
.form-checkbox.bulk-edit-checkbox.all {
|
||||
display: block;
|
||||
width: $bulk-edit-toggle-width;
|
||||
margin: 0 0 0 $bulk-edit-toggle-offset;
|
||||
|
@ -331,7 +331,7 @@ $bulk-edit-transition-duration: 400ms;
|
|||
}
|
||||
|
||||
/* Bookmark checkboxes */
|
||||
li[ld-bookmark-item] [ld-bulk-edit-checkbox].form-checkbox {
|
||||
li[ld-bookmark-item] .form-checkbox.bulk-edit-checkbox {
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: $bulk-edit-toggle-width;
|
||||
|
@ -350,7 +350,7 @@ $bulk-edit-transition-duration: 400ms;
|
|||
}
|
||||
}
|
||||
|
||||
&.active li[ld-bookmark-item] [ld-bulk-edit-checkbox].form-checkbox {
|
||||
&.active li[ld-bookmark-item] .form-checkbox.bulk-edit-checkbox {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
|
@ -4,11 +4,7 @@
|
|||
{% load bookmarks %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bookmarks-page grid columns-md-1"
|
||||
ld-bulk-edit
|
||||
ld-bookmark-page
|
||||
bookmarks-url="{% url 'bookmarks:partials.bookmark_list.archived' %}"
|
||||
tags-url="{% url 'bookmarks:partials.tag_cloud.archived' %}">
|
||||
<div ld-bulk-edit class="bookmarks-page grid columns-md-1">
|
||||
|
||||
{# Bookmark list #}
|
||||
<section class="content-area col-2">
|
||||
|
@ -17,17 +13,22 @@
|
|||
<div class="header-controls">
|
||||
{% bookmark_search bookmark_list.search tag_cloud.tags mode='archived' %}
|
||||
{% include 'bookmarks/bulk_edit/toggle.html' %}
|
||||
<button ld-modal modal-content=".tag-cloud" class="btn ml-2 show-md">Tags</button>
|
||||
<button ld-fetch="{{ bookmark_list.tag_modal_url }}" ld-target="body|append" ld-on="click"
|
||||
class="btn ml-2 show-md">Tags
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="bookmark-actions"
|
||||
<form ld-form ld-fire="refresh-bookmark-list,refresh-tag-cloud"
|
||||
class="bookmark-actions"
|
||||
action="{{ bookmark_list.action_url|safe }}"
|
||||
method="post" autocomplete="off">
|
||||
{% csrf_token %}
|
||||
{% include 'bookmarks/bulk_edit/bar.html' with disable_actions='bulk_archive' %}
|
||||
|
||||
<div class="bookmark-list-container">
|
||||
<div ld-fetch="{{ bookmark_list.refresh_url }}" ld-on="refresh-bookmark-list"
|
||||
ld-fire="refresh-bookmark-list-done"
|
||||
class="bookmark-list-container">
|
||||
{% include 'bookmarks/bookmark_list.html' %}
|
||||
</div>
|
||||
</form>
|
||||
|
@ -38,7 +39,8 @@
|
|||
<div class="content-area-header">
|
||||
<h2>Tags</h2>
|
||||
</div>
|
||||
<div class="tag-cloud-container">
|
||||
<div ld-fetch="{{ tag_cloud.refresh_url }}" ld-on="refresh-tag-cloud"
|
||||
class="tag-cloud-container">
|
||||
{% include 'bookmarks/tag_cloud.html' %}
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
{% for bookmark_item in bookmark_list.items %}
|
||||
<li ld-bookmark-item{% if bookmark_item.css_classes %} class="{{ bookmark_item.css_classes }}"{% endif %}>
|
||||
<div class="title">
|
||||
<label ld-bulk-edit-checkbox class="form-checkbox">
|
||||
<label class="form-checkbox bulk-edit-checkbox">
|
||||
<input type="checkbox" name="bookmark_id" value="{{ bookmark_item.id }}">
|
||||
<i class="form-icon"></i>
|
||||
</label>
|
||||
|
@ -81,8 +81,8 @@
|
|||
{% endif %}
|
||||
{# View link is visible for both owned and shared bookmarks #}
|
||||
{% if bookmark_list.show_view_action %}
|
||||
<a ld-modal
|
||||
modal-url="{% url 'bookmarks:details_modal' bookmark_item.id %}?return_url={{ bookmark_list.return_url|urlencode }}"
|
||||
<a ld-fetch="{% url 'bookmarks:details_modal' bookmark_item.id %}?return_url={{ bookmark_list.return_url|urlencode }}"
|
||||
ld-on="click" ld-target="body|append"
|
||||
href="{% url 'bookmarks:details' bookmark_item.id %}">View</a>
|
||||
{% endif %}
|
||||
{% if bookmark_item.is_editable %}
|
||||
|
@ -118,7 +118,7 @@
|
|||
{% if bookmark_item.show_mark_as_read %}
|
||||
<button type="submit" name="mark_as_read" value="{{ bookmark_item.id }}"
|
||||
class="btn btn-link btn-sm btn-icon"
|
||||
ld-confirm-button confirm-icon="ld-icon-read" confirm-question="Mark as read?">
|
||||
ld-confirm-button ld-confirm-icon="ld-icon-read" ld-confirm-question="Mark as read?">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||
<use xlink:href="#ld-icon-unread"></use>
|
||||
</svg>
|
||||
|
@ -128,7 +128,7 @@
|
|||
{% if bookmark_item.show_unshare %}
|
||||
<button type="submit" name="unshare" value="{{ bookmark_item.id }}"
|
||||
class="btn btn-link btn-sm btn-icon"
|
||||
ld-confirm-button confirm-icon="ld-icon-unshare" confirm-question="Unshare?">
|
||||
ld-confirm-button ld-confirm-icon="ld-icon-unshare" ld-confirm-question="Unshare?">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||
<use xlink:href="#ld-icon-share"></use>
|
||||
</svg>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{% htmlmin %}
|
||||
<div class="bulk-edit-bar">
|
||||
<div class="bulk-edit-actions bg-gray">
|
||||
<label ld-bulk-edit-checkbox all class="form-checkbox">
|
||||
<label class="form-checkbox bulk-edit-checkbox all">
|
||||
<input type="checkbox">
|
||||
<i class="form-icon"></i>
|
||||
</label>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<button ld-bulk-edit-active-toggle class="btn hide-sm ml-2" title="Bulk edit">
|
||||
<button class="btn hide-sm ml-2 bulk-edit-active-toggle" title="Bulk edit">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" width="20px"
|
||||
height="20px">
|
||||
<path
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
{% load static %}
|
||||
{% load shared %}
|
||||
|
||||
<form ld-form action="{% url 'bookmarks:details' details.bookmark.id %}"
|
||||
refresh-url="{% url 'bookmarks:partials.details_form' details.bookmark.id %}"
|
||||
refresh-events="bookmark-page-refresh"
|
||||
<form ld-form ld-fire="refresh-bookmark-list,refresh-tag-cloud,refresh-details"
|
||||
action="{% url 'bookmarks:details' details.bookmark.id %}"
|
||||
method="post">
|
||||
<div class="weblinks">
|
||||
<a class="weblink" href="{{ details.bookmark.url }}" rel="noopener"
|
||||
|
@ -35,14 +34,14 @@
|
|||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label class="form-switch">
|
||||
<input ld-form-auto-submit type="checkbox" name="is_archived"
|
||||
<input ld-auto-submit type="checkbox" name="is_archived"
|
||||
{% if details.bookmark.is_archived %}checked{% endif %}>
|
||||
<i class="form-icon"></i> Archived
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-switch">
|
||||
<input ld-form-auto-submit type="checkbox" name="unread"
|
||||
<input ld-auto-submit type="checkbox" name="unread"
|
||||
{% if details.bookmark.unread %}checked{% endif %}>
|
||||
<i class="form-icon"></i> Unread
|
||||
</label>
|
||||
|
@ -50,7 +49,7 @@
|
|||
{% if details.profile.enable_sharing %}
|
||||
<div class="form-group">
|
||||
<label class="form-switch">
|
||||
<input ld-form-auto-submit type="checkbox" name="shared"
|
||||
<input ld-auto-submit type="checkbox" name="shared"
|
||||
{% if details.bookmark.shared %}checked{% endif %}>
|
||||
<i class="form-icon"></i> Shared
|
||||
</label>
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<div class="modal active bookmark-details">
|
||||
<div ld-modal
|
||||
ld-fetch="{% url 'bookmarks:details_modal' details.bookmark.id %}" ld-on="refresh-details"
|
||||
ld-select=".content" ld-target=".modal.bookmark-details .content|outerHTML"
|
||||
class="modal active bookmark-details">
|
||||
<div class="modal-overlay" aria-label="Close"></div>
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
|
|
|
@ -4,11 +4,7 @@
|
|||
{% load bookmarks %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bookmarks-page grid columns-md-1"
|
||||
ld-bulk-edit
|
||||
ld-bookmark-page
|
||||
bookmarks-url="{% url 'bookmarks:partials.bookmark_list.active' %}"
|
||||
tags-url="{% url 'bookmarks:partials.tag_cloud.active' %}">
|
||||
<div ld-bulk-edit class="bookmarks-page grid columns-md-1">
|
||||
|
||||
{# Bookmark list #}
|
||||
<section class="content-area col-2">
|
||||
|
@ -17,17 +13,22 @@
|
|||
<div class="header-controls">
|
||||
{% bookmark_search bookmark_list.search tag_cloud.tags %}
|
||||
{% include 'bookmarks/bulk_edit/toggle.html' %}
|
||||
<button ld-modal modal-content=".tag-cloud" class="btn ml-2 show-md">Tags</button>
|
||||
<button ld-fetch="{{ bookmark_list.tag_modal_url }}" ld-target="body|append" ld-on="click"
|
||||
class="btn ml-2 show-md">Tags
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="bookmark-actions"
|
||||
<form ld-form ld-fire="refresh-bookmark-list,refresh-tag-cloud"
|
||||
class="bookmark-actions"
|
||||
action="{{ bookmark_list.action_url|safe }}"
|
||||
method="post" autocomplete="off">
|
||||
{% csrf_token %}
|
||||
{% include 'bookmarks/bulk_edit/bar.html' with disable_actions='bulk_unarchive' %}
|
||||
|
||||
<div class="bookmark-list-container">
|
||||
<div ld-fetch="{{ bookmark_list.refresh_url }}" ld-on="refresh-bookmark-list"
|
||||
ld-fire="refresh-bookmark-list-done"
|
||||
class="bookmark-list-container">
|
||||
{% include 'bookmarks/bookmark_list.html' %}
|
||||
</div>
|
||||
</form>
|
||||
|
@ -38,7 +39,8 @@
|
|||
<div class="content-area-header">
|
||||
<h2>Tags</h2>
|
||||
</div>
|
||||
<div class="tag-cloud-container">
|
||||
<div ld-fetch="{{ tag_cloud.refresh_url }}" ld-on="refresh-tag-cloud"
|
||||
class="tag-cloud-container">
|
||||
{% include 'bookmarks/tag_cloud.html' %}
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -4,10 +4,7 @@
|
|||
{% load bookmarks %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bookmarks-page grid columns-md-1"
|
||||
ld-bookmark-page
|
||||
bookmarks-url="{% url 'bookmarks:partials.bookmark_list.shared' %}"
|
||||
tags-url="{% url 'bookmarks:partials.tag_cloud.shared' %}">
|
||||
<div class="bookmarks-page grid columns-md-1">
|
||||
|
||||
{# Bookmark list #}
|
||||
<section class="content-area col-2">
|
||||
|
@ -15,15 +12,20 @@
|
|||
<h2>Shared bookmarks</h2>
|
||||
<div class="header-controls">
|
||||
{% bookmark_search bookmark_list.search tag_cloud.tags mode='shared' %}
|
||||
<button ld-modal modal-content=".tag-cloud" class="btn ml-2 show-md">Tags</button>
|
||||
<button ld-fetch="{{ bookmark_list.tag_modal_url }}" ld-target="body|append" ld-on="click"
|
||||
class="btn ml-2 show-md">Tags
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="bookmark-actions" action="{{ bookmark_list.action_url|safe }}"
|
||||
method="post">
|
||||
<form ld-form ld-fire="refresh-bookmark-list,refresh-tag-cloud"
|
||||
class="bookmark-actions"
|
||||
action="{{ bookmark_list.action_url|safe }}"
|
||||
method="post" autocomplete="off">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="bookmark-list-container">
|
||||
<div ld-fetch="{{ bookmark_list.refresh_url }}" ld-on="refresh-bookmark-list"
|
||||
ld-fire="refresh-bookmark-list-done"
|
||||
class="bookmark-list-container">
|
||||
{% include 'bookmarks/bookmark_list.html' %}
|
||||
</div>
|
||||
</form>
|
||||
|
@ -41,7 +43,8 @@
|
|||
<div class="content-area-header">
|
||||
<h2>Tags</h2>
|
||||
</div>
|
||||
<div class="tag-cloud-container">
|
||||
<div ld-fetch="{{ tag_cloud.refresh_url }}" ld-on="refresh-tag-cloud"
|
||||
class="tag-cloud-container">
|
||||
{% include 'bookmarks/tag_cloud.html' %}
|
||||
</div>
|
||||
</section>
|
||||
|
|
21
bookmarks/templates/bookmarks/tag_modal.html
Normal file
21
bookmarks/templates/bookmarks/tag_modal.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
<div ld-modal class="modal active">
|
||||
<div class="modal-overlay" aria-label="Close"></div>
|
||||
<div class="modal-container">
|
||||
<div class="modal-header d-flex justify-between align-center">
|
||||
<div class="modal-title h5">Tags</div>
|
||||
<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>
|
||||
<path d="M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="content">
|
||||
{% include 'bookmarks/tag_cloud.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -94,15 +94,10 @@ class BookmarkArchivedViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
|||
)
|
||||
|
||||
def assertBulkActionForm(self, response, url: str):
|
||||
html = collapse_whitespace(response.content.decode())
|
||||
needle = collapse_whitespace(
|
||||
f"""
|
||||
<form class="bookmark-actions"
|
||||
action="{url}"
|
||||
method="post" autocomplete="off">
|
||||
"""
|
||||
)
|
||||
self.assertIn(needle, html)
|
||||
soup = self.make_soup(response.content.decode())
|
||||
form = soup.select_one("form.bookmark-actions")
|
||||
self.assertIsNotNone(form)
|
||||
self.assertEqual(form.attrs["action"], url)
|
||||
|
||||
def test_should_list_archived_and_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user(
|
||||
|
|
|
@ -105,18 +105,6 @@ class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
|||
def test_access_with_sharing(self):
|
||||
self.details_route_sharing_access_test(self.get_view_name(), True)
|
||||
|
||||
def test_form_partial_access(self):
|
||||
# form partial is only used when submitting forms, which should be only
|
||||
# accessible to the owner of the bookmark. As such assume it requires
|
||||
# login.
|
||||
self.details_route_access_test("bookmarks:partials.details_form", False)
|
||||
|
||||
def test_form_partial_access_with_sharing(self):
|
||||
# form partial is only used when submitting forms, which should be only
|
||||
# accessible to the owner of the bookmark. As such assume it requires
|
||||
# login.
|
||||
self.details_route_sharing_access_test("bookmarks:partials.details_form", False)
|
||||
|
||||
def test_displays_title(self):
|
||||
# with title
|
||||
bookmark = self.setup_bookmark(title="Test title")
|
||||
|
|
|
@ -94,15 +94,10 @@ class BookmarkIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||
)
|
||||
|
||||
def assertBulkActionForm(self, response, url: str):
|
||||
html = collapse_whitespace(response.content.decode())
|
||||
needle = collapse_whitespace(
|
||||
f"""
|
||||
<form class="bookmark-actions"
|
||||
action="{url}"
|
||||
method="post" autocomplete="off">
|
||||
"""
|
||||
)
|
||||
self.assertIn(needle, html)
|
||||
soup = self.make_soup(response.content.decode())
|
||||
form = soup.select_one("form.bookmark-actions")
|
||||
self.assertIsNotNone(form)
|
||||
self.assertEqual(form.attrs["action"], url)
|
||||
|
||||
def test_should_list_unarchived_and_user_owned_bookmarks(self):
|
||||
other_user = User.objects.create_user(
|
||||
|
|
|
@ -80,7 +80,9 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||
details_modal_url = reverse("bookmarks:details_modal", args=[bookmark.id])
|
||||
self.assertInHTML(
|
||||
f"""
|
||||
<a ld-modal modal-url="{details_modal_url}?return_url={return_url}" href="{details_url}">View</a>
|
||||
<a ld-fetch="{details_modal_url}?return_url={return_url}"
|
||||
ld-on="click" ld-target="body|append"
|
||||
href="{details_url}">View</a>
|
||||
""",
|
||||
html,
|
||||
count=count,
|
||||
|
@ -216,7 +218,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||
f"""
|
||||
<button type="submit" name="unshare" value="{bookmark.id}"
|
||||
class="btn btn-link btn-sm btn-icon"
|
||||
ld-confirm-button confirm-icon="ld-icon-unshare" confirm-question="Unshare?">
|
||||
ld-confirm-button ld-confirm-icon="ld-icon-unshare" ld-confirm-question="Unshare?">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||
<use xlink:href="#ld-icon-share"></use>
|
||||
</svg>
|
||||
|
@ -232,7 +234,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||
f"""
|
||||
<button type="submit" name="mark_as_read" value="{bookmark.id}"
|
||||
class="btn btn-link btn-sm btn-icon"
|
||||
ld-confirm-button confirm-icon="ld-icon-read" confirm-question="Mark as read?">
|
||||
ld-confirm-button ld-confirm-icon="ld-icon-read" ld-confirm-question="Mark as read?">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||
<use xlink:href="#ld-icon-unread"></use>
|
||||
</svg>
|
||||
|
@ -782,10 +784,16 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||
|
||||
def test_note_cleans_html(self):
|
||||
self.setup_bookmark(notes='<script>alert("test")</script>')
|
||||
self.setup_bookmark(
|
||||
notes='<b ld-fetch="https://example.com" ld-on="click">bold text</b>'
|
||||
)
|
||||
html = self.render_template()
|
||||
|
||||
note_html = '<script>alert("test")</script>'
|
||||
self.assertNotes(html, note_html, 1)
|
||||
self.assertIn(note_html, html, 1)
|
||||
|
||||
note_html = "<b>bold text</b>"
|
||||
self.assertIn(note_html, html, 1)
|
||||
|
||||
def test_notes_are_hidden_initially_by_default(self):
|
||||
self.setup_bookmark(notes="Test note")
|
||||
|
|
|
@ -61,6 +61,11 @@ urlpatterns = [
|
|||
partials.active_tag_cloud,
|
||||
name="partials.tag_cloud.active",
|
||||
),
|
||||
path(
|
||||
"bookmarks/partials/tag-modal/active",
|
||||
partials.active_tag_modal,
|
||||
name="partials.tag_modal.active",
|
||||
),
|
||||
path(
|
||||
"bookmarks/partials/bookmark-list/archived",
|
||||
partials.archived_bookmark_list,
|
||||
|
@ -71,6 +76,11 @@ urlpatterns = [
|
|||
partials.archived_tag_cloud,
|
||||
name="partials.tag_cloud.archived",
|
||||
),
|
||||
path(
|
||||
"bookmarks/partials/tag-modal/archived",
|
||||
partials.archived_tag_modal,
|
||||
name="partials.tag_modal.archived",
|
||||
),
|
||||
path(
|
||||
"bookmarks/partials/bookmark-list/shared",
|
||||
partials.shared_bookmark_list,
|
||||
|
@ -82,9 +92,9 @@ urlpatterns = [
|
|||
name="partials.tag_cloud.shared",
|
||||
),
|
||||
path(
|
||||
"bookmarks/partials/details-form/<int:bookmark_id>",
|
||||
partials.details_form,
|
||||
name="partials.details_form",
|
||||
"bookmarks/partials/tag-modal/shared",
|
||||
partials.shared_tag_modal,
|
||||
name="partials.tag_modal.shared",
|
||||
),
|
||||
# Settings
|
||||
path("settings", views.settings.general, name="settings.index"),
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import Http404
|
||||
from django.shortcuts import render
|
||||
|
||||
from bookmarks.models import Bookmark
|
||||
from bookmarks.views.partials import contexts
|
||||
|
||||
|
||||
|
@ -24,6 +22,13 @@ def active_tag_cloud(request):
|
|||
return render(request, "bookmarks/tag_cloud.html", {"tag_cloud": tag_cloud_context})
|
||||
|
||||
|
||||
@login_required
|
||||
def active_tag_modal(request):
|
||||
tag_cloud_context = contexts.ActiveTagCloudContext(request)
|
||||
|
||||
return render(request, "bookmarks/tag_modal.html", {"tag_cloud": tag_cloud_context})
|
||||
|
||||
|
||||
@login_required
|
||||
def archived_bookmark_list(request):
|
||||
bookmark_list_context = contexts.ArchivedBookmarkListContext(request)
|
||||
|
@ -43,6 +48,12 @@ def archived_tag_cloud(request):
|
|||
|
||||
|
||||
@login_required
|
||||
def archived_tag_modal(request):
|
||||
tag_cloud_context = contexts.ArchivedTagCloudContext(request)
|
||||
|
||||
return render(request, "bookmarks/tag_modal.html", {"tag_cloud": tag_cloud_context})
|
||||
|
||||
|
||||
def shared_bookmark_list(request):
|
||||
bookmark_list_context = contexts.SharedBookmarkListContext(request)
|
||||
|
||||
|
@ -53,20 +64,13 @@ def shared_bookmark_list(request):
|
|||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def shared_tag_cloud(request):
|
||||
tag_cloud_context = contexts.SharedTagCloudContext(request)
|
||||
|
||||
return render(request, "bookmarks/tag_cloud.html", {"tag_cloud": tag_cloud_context})
|
||||
|
||||
|
||||
@login_required
|
||||
def details_form(request, bookmark_id: int):
|
||||
try:
|
||||
bookmark = Bookmark.objects.get(pk=bookmark_id, owner=request.user)
|
||||
except Bookmark.DoesNotExist:
|
||||
raise Http404("Bookmark does not exist")
|
||||
def shared_tag_modal(request):
|
||||
tag_cloud_context = contexts.SharedTagCloudContext(request)
|
||||
|
||||
details_context = contexts.BookmarkDetailsContext(request, bookmark)
|
||||
|
||||
return render(request, "bookmarks/details/form.html", {"details": details_context})
|
||||
return render(request, "bookmarks/tag_modal.html", {"tag_cloud": tag_cloud_context})
|
||||
|
|
|
@ -23,6 +23,113 @@ DEFAULT_PAGE_SIZE = 30
|
|||
CJK_RE = re.compile(r"[\u4e00-\u9fff]+")
|
||||
|
||||
|
||||
class RequestContext:
|
||||
index_view = "bookmarks:index"
|
||||
action_view = "bookmarks:index.action"
|
||||
bookmark_list_partial_view = "bookmarks:partials.bookmark_list.active"
|
||||
tag_cloud_partial_view = "bookmarks:partials.tag_cloud.active"
|
||||
tag_modal_partial_view = "bookmarks:partials.tag_modal.active"
|
||||
|
||||
def __init__(self, request: WSGIRequest):
|
||||
self.request = request
|
||||
self.index_url = reverse(self.index_view)
|
||||
self.action_url = reverse(self.action_view)
|
||||
self.bookmark_list_partial_url = reverse(self.bookmark_list_partial_view)
|
||||
self.tag_cloud_partial_url = reverse(self.tag_cloud_partial_view)
|
||||
self.tag_modal_partial_url = reverse(self.tag_modal_partial_view)
|
||||
self.query_params = request.GET.copy()
|
||||
self.query_params.pop("details", None)
|
||||
|
||||
def get_url(self, view_url: str, add: dict = None, remove: dict = None) -> str:
|
||||
query_params = self.query_params.copy()
|
||||
if add:
|
||||
query_params.update(add)
|
||||
if remove:
|
||||
for key in remove:
|
||||
query_params.pop(key, None)
|
||||
encoded_params = query_params.urlencode()
|
||||
return view_url + "?" + encoded_params if encoded_params else view_url
|
||||
|
||||
def index(self) -> str:
|
||||
return self.get_url(self.index_url)
|
||||
|
||||
def action(self, return_url: str) -> str:
|
||||
return self.get_url(self.action_url, add={"return_url": return_url})
|
||||
|
||||
def bookmark_list_partial(self) -> str:
|
||||
return self.get_url(self.bookmark_list_partial_url)
|
||||
|
||||
def tag_cloud_partial(self) -> str:
|
||||
return self.get_url(self.tag_cloud_partial_url)
|
||||
|
||||
def tag_modal_partial(self) -> str:
|
||||
return self.get_url(self.tag_modal_partial_url)
|
||||
|
||||
def get_bookmark_query_set(self, search: BookmarkSearch):
|
||||
raise Exception("Must be implemented by subclass")
|
||||
|
||||
def get_tag_query_set(self, search: BookmarkSearch):
|
||||
raise Exception("Must be implemented by subclass")
|
||||
|
||||
|
||||
class ActiveBookmarksContext(RequestContext):
|
||||
index_view = "bookmarks:index"
|
||||
action_view = "bookmarks:index.action"
|
||||
bookmark_list_partial_view = "bookmarks:partials.bookmark_list.active"
|
||||
tag_cloud_partial_view = "bookmarks:partials.tag_cloud.active"
|
||||
tag_modal_partial_view = "bookmarks:partials.tag_modal.active"
|
||||
|
||||
def get_bookmark_query_set(self, search: BookmarkSearch):
|
||||
return queries.query_bookmarks(
|
||||
self.request.user, self.request.user_profile, search
|
||||
)
|
||||
|
||||
def get_tag_query_set(self, search: BookmarkSearch):
|
||||
return queries.query_bookmark_tags(
|
||||
self.request.user, self.request.user_profile, search
|
||||
)
|
||||
|
||||
|
||||
class ArchivedBookmarksContext(RequestContext):
|
||||
index_view = "bookmarks:archived"
|
||||
action_view = "bookmarks:archived.action"
|
||||
bookmark_list_partial_view = "bookmarks:partials.bookmark_list.archived"
|
||||
tag_cloud_partial_view = "bookmarks:partials.tag_cloud.archived"
|
||||
tag_modal_partial_view = "bookmarks:partials.tag_modal.archived"
|
||||
|
||||
def get_bookmark_query_set(self, search: BookmarkSearch):
|
||||
return queries.query_archived_bookmarks(
|
||||
self.request.user, self.request.user_profile, search
|
||||
)
|
||||
|
||||
def get_tag_query_set(self, search: BookmarkSearch):
|
||||
return queries.query_archived_bookmark_tags(
|
||||
self.request.user, self.request.user_profile, search
|
||||
)
|
||||
|
||||
|
||||
class SharedBookmarksContext(RequestContext):
|
||||
index_view = "bookmarks:shared"
|
||||
action_view = "bookmarks:shared.action"
|
||||
bookmark_list_partial_view = "bookmarks:partials.bookmark_list.shared"
|
||||
tag_cloud_partial_view = "bookmarks:partials.tag_cloud.shared"
|
||||
tag_modal_partial_view = "bookmarks:partials.tag_modal.shared"
|
||||
|
||||
def get_bookmark_query_set(self, search: BookmarkSearch):
|
||||
user = User.objects.filter(username=search.user).first()
|
||||
public_only = not self.request.user.is_authenticated
|
||||
return queries.query_shared_bookmarks(
|
||||
user, self.request.user_profile, search, public_only
|
||||
)
|
||||
|
||||
def get_tag_query_set(self, search: BookmarkSearch):
|
||||
user = User.objects.filter(username=search.user).first()
|
||||
public_only = not self.request.user.is_authenticated
|
||||
return queries.query_shared_bookmark_tags(
|
||||
user, self.request.user_profile, search, public_only
|
||||
)
|
||||
|
||||
|
||||
class BookmarkItem:
|
||||
def __init__(self, bookmark: Bookmark, user: User, profile: UserProfile) -> None:
|
||||
self.bookmark = bookmark
|
||||
|
@ -67,7 +174,10 @@ class BookmarkItem:
|
|||
|
||||
|
||||
class BookmarkListContext:
|
||||
request_context = RequestContext
|
||||
|
||||
def __init__(self, request: WSGIRequest) -> None:
|
||||
request_context = self.request_context(request)
|
||||
user = request.user
|
||||
user_profile = request.user_profile
|
||||
|
||||
|
@ -76,7 +186,7 @@ class BookmarkListContext:
|
|||
self.request.GET, user_profile.search_preferences
|
||||
)
|
||||
|
||||
query_set = self.get_bookmark_query_set()
|
||||
query_set = request_context.get_bookmark_query_set(self.search)
|
||||
page_number = request.GET.get("page")
|
||||
paginator = Paginator(query_set, DEFAULT_PAGE_SIZE)
|
||||
bookmarks_page = paginator.get_page(page_number)
|
||||
|
@ -86,16 +196,15 @@ class BookmarkListContext:
|
|||
self.items = [
|
||||
BookmarkItem(bookmark, user, user_profile) for bookmark in bookmarks_page
|
||||
]
|
||||
|
||||
self.is_empty = paginator.count == 0
|
||||
self.bookmarks_page = bookmarks_page
|
||||
self.bookmarks_total = paginator.count
|
||||
self.return_url = self.generate_return_url(
|
||||
self.search, self.get_base_url(), page_number
|
||||
)
|
||||
self.action_url = self.generate_action_url(
|
||||
self.search, self.get_base_action_url(), self.return_url
|
||||
)
|
||||
|
||||
self.return_url = request_context.index()
|
||||
self.action_url = request_context.action(return_url=self.return_url)
|
||||
self.refresh_url = request_context.bookmark_list_partial()
|
||||
self.tag_modal_url = request_context.tag_modal_partial()
|
||||
|
||||
self.link_target = user_profile.bookmark_link_target
|
||||
self.date_display = user_profile.bookmark_date_display
|
||||
self.description_display = user_profile.bookmark_description_display
|
||||
|
@ -131,55 +240,17 @@ class BookmarkListContext:
|
|||
else base_action_url + "?" + query_string
|
||||
)
|
||||
|
||||
def get_base_url(self):
|
||||
raise Exception("Must be implemented by subclass")
|
||||
|
||||
def get_base_action_url(self):
|
||||
raise Exception("Must be implemented by subclass")
|
||||
|
||||
def get_bookmark_query_set(self):
|
||||
raise Exception("Must be implemented by subclass")
|
||||
|
||||
|
||||
class ActiveBookmarkListContext(BookmarkListContext):
|
||||
def get_base_url(self):
|
||||
return reverse("bookmarks:index")
|
||||
|
||||
def get_base_action_url(self):
|
||||
return reverse("bookmarks:index.action")
|
||||
|
||||
def get_bookmark_query_set(self):
|
||||
return queries.query_bookmarks(
|
||||
self.request.user, self.request.user_profile, self.search
|
||||
)
|
||||
request_context = ActiveBookmarksContext
|
||||
|
||||
|
||||
class ArchivedBookmarkListContext(BookmarkListContext):
|
||||
def get_base_url(self):
|
||||
return reverse("bookmarks:archived")
|
||||
|
||||
def get_base_action_url(self):
|
||||
return reverse("bookmarks:archived.action")
|
||||
|
||||
def get_bookmark_query_set(self):
|
||||
return queries.query_archived_bookmarks(
|
||||
self.request.user, self.request.user_profile, self.search
|
||||
)
|
||||
request_context = ArchivedBookmarksContext
|
||||
|
||||
|
||||
class SharedBookmarkListContext(BookmarkListContext):
|
||||
def get_base_url(self):
|
||||
return reverse("bookmarks:shared")
|
||||
|
||||
def get_base_action_url(self):
|
||||
return reverse("bookmarks:shared.action")
|
||||
|
||||
def get_bookmark_query_set(self):
|
||||
user = User.objects.filter(username=self.search.user).first()
|
||||
public_only = not self.request.user.is_authenticated
|
||||
return queries.query_shared_bookmarks(
|
||||
user, self.request.user_profile, self.search, public_only
|
||||
)
|
||||
request_context = SharedBookmarksContext
|
||||
|
||||
|
||||
class TagGroup:
|
||||
|
@ -218,7 +289,10 @@ class TagGroup:
|
|||
|
||||
|
||||
class TagCloudContext:
|
||||
request_context = RequestContext
|
||||
|
||||
def __init__(self, request: WSGIRequest) -> None:
|
||||
request_context = self.request_context(request)
|
||||
user_profile = request.user_profile
|
||||
|
||||
self.request = request
|
||||
|
@ -226,7 +300,7 @@ class TagCloudContext:
|
|||
self.request.GET, user_profile.search_preferences
|
||||
)
|
||||
|
||||
query_set = self.get_tag_query_set()
|
||||
query_set = request_context.get_tag_query_set(self.search)
|
||||
tags = list(query_set)
|
||||
selected_tags = self.get_selected_tags(tags)
|
||||
unique_tags = utils.unique(tags, key=lambda x: str.lower(x.name))
|
||||
|
@ -242,8 +316,7 @@ class TagCloudContext:
|
|||
self.selected_tags = unique_selected_tags
|
||||
self.has_selected_tags = has_selected_tags
|
||||
|
||||
def get_tag_query_set(self):
|
||||
raise Exception("Must be implemented by subclass")
|
||||
self.refresh_url = request_context.tag_cloud_partial()
|
||||
|
||||
def get_selected_tags(self, tags: List[Tag]):
|
||||
parsed_query = queries.parse_query_string(self.search.q)
|
||||
|
@ -256,26 +329,15 @@ class TagCloudContext:
|
|||
|
||||
|
||||
class ActiveTagCloudContext(TagCloudContext):
|
||||
def get_tag_query_set(self):
|
||||
return queries.query_bookmark_tags(
|
||||
self.request.user, self.request.user_profile, self.search
|
||||
)
|
||||
request_context = ActiveBookmarksContext
|
||||
|
||||
|
||||
class ArchivedTagCloudContext(TagCloudContext):
|
||||
def get_tag_query_set(self):
|
||||
return queries.query_archived_bookmark_tags(
|
||||
self.request.user, self.request.user_profile, self.search
|
||||
)
|
||||
request_context = ArchivedBookmarksContext
|
||||
|
||||
|
||||
class SharedTagCloudContext(TagCloudContext):
|
||||
def get_tag_query_set(self):
|
||||
user = User.objects.filter(username=self.search.user).first()
|
||||
public_only = not self.request.user.is_authenticated
|
||||
return queries.query_shared_bookmark_tags(
|
||||
user, self.request.user_profile, self.search, public_only
|
||||
)
|
||||
request_context = SharedBookmarksContext
|
||||
|
||||
|
||||
class BookmarkAssetItem:
|
||||
|
|
|
@ -28,5 +28,6 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"web-types": "./web-types.json"
|
||||
}
|
||||
|
|
116
web-types.json
Normal file
116
web-types.json
Normal file
|
@ -0,0 +1,116 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/JetBrains/web-types/master/schema/web-types.json",
|
||||
"name": "linkding",
|
||||
"version": "1.0.0",
|
||||
"contributions": {
|
||||
"html": {
|
||||
"attributes": [
|
||||
{
|
||||
"name": "ld-fetch",
|
||||
"description": "Fetches the HTML content of the given URL and replaces the content of an element with it. Fires events afterwards to notify other behaviors.",
|
||||
"value": {
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ld-on",
|
||||
"description": "The event that triggers a fetch, such as `click` or a custom event name fired by another behavior",
|
||||
"value": {
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ld-target",
|
||||
"description": "The target element to replace the content of and the replacement strategy, for example `body|append`",
|
||||
"value": {
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ld-fire",
|
||||
"description": "Fires one or more events once a behavior, such as ld-fetch or ld-form, is finished",
|
||||
"value": {
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ld-form",
|
||||
"description": "Converts a form into a fetch request. Fires events afterwards to notify other behaviors.",
|
||||
"value": {
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ld-auto-submit",
|
||||
"description": "Automatically submits the nearest form when the value of the input changes",
|
||||
"value": {
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ld-modal",
|
||||
"description": "Adds Javascript behavior to a modal HTML component",
|
||||
"value": {
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ld-dropdown",
|
||||
"description": "Adds Javascript behavior to a dropdown HTML component",
|
||||
"value": {
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ld-confirm-button",
|
||||
"description": "Converts a button into a confirmation button that shows confirm / cancel buttons when clicked",
|
||||
"value": {
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ld-confirm-icon",
|
||||
"description": "Icon to show when the confirm button is clicked",
|
||||
"value": {
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ld-confirm-question",
|
||||
"description": "Question to show when the confirm button is clicked",
|
||||
"value": {
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ld-bookmark-item",
|
||||
"description": "Adds Javascript behavior to a bookmark list item",
|
||||
"value": {
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ld-bulk-edit",
|
||||
"description": "Adds Javascript behavior for bulk editing the bookmark list",
|
||||
"value": {
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ld-global-shortcuts",
|
||||
"description": "Adds Javascript behavior for global shortcuts",
|
||||
"value": {
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ld-tag-autocomplete",
|
||||
"description": "Adds Javascript behavior for converting a plain input into a tag autocomplete Svelte component",
|
||||
"value": {
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue