mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-07 10:58:25 +02:00
Add button to show tags on smaller screens (#529)
* Implement tag modal * Improve header controls responsiveness * Improve modal styles * Cleanup
This commit is contained in:
65
bookmarks/frontend/behaviors/modal.js
Normal file
65
bookmarks/frontend/behaviors/modal.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { registerBehavior } from "./index";
|
||||||
|
|
||||||
|
class ModalBehavior {
|
||||||
|
constructor(element) {
|
||||||
|
const toggle = element;
|
||||||
|
toggle.addEventListener("click", this.onToggleClick.bind(this));
|
||||||
|
this.toggle = toggle;
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleClick() {
|
||||||
|
const contentSelector = this.toggle.getAttribute("modal-content");
|
||||||
|
const content = document.querySelector(contentSelector);
|
||||||
|
if (!content) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create modal
|
||||||
|
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="btn btn-link 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>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
// Teleport content back
|
||||||
|
this.contentOwner.append(this.content);
|
||||||
|
|
||||||
|
// Remove modal
|
||||||
|
this.modal.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerBehavior("ld-modal", ModalBehavior);
|
@@ -4,6 +4,7 @@ import { ApiClient } from "./api";
|
|||||||
import "./behaviors/bookmark-page";
|
import "./behaviors/bookmark-page";
|
||||||
import "./behaviors/bulk-edit";
|
import "./behaviors/bulk-edit";
|
||||||
import "./behaviors/confirm-button";
|
import "./behaviors/confirm-button";
|
||||||
|
import "./behaviors/modal";
|
||||||
import "./behaviors/global-shortcuts";
|
import "./behaviors/global-shortcuts";
|
||||||
import "./behaviors/tag-autocomplete";
|
import "./behaviors/tag-autocomplete";
|
||||||
|
|
||||||
|
@@ -50,14 +50,20 @@ section.content-area {
|
|||||||
border-bottom: solid 1px $border-color;
|
border-bottom: solid 1px $border-color;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
column-gap: $unit-6;
|
||||||
padding-bottom: $unit-2;
|
padding-bottom: $unit-2;
|
||||||
margin-bottom: $unit-4;
|
margin-bottom: $unit-4;
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
|
flex: 0 0 auto;
|
||||||
line-height: 1.8rem;
|
line-height: 1.8rem;
|
||||||
margin-right: auto;
|
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-controls {
|
||||||
|
flex: 1 1 0;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,31 +2,36 @@
|
|||||||
grid-gap: $unit-10;
|
grid-gap: $unit-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bookmark search box */
|
/* Bookmark area header controls */
|
||||||
.bookmarks-page .search {
|
.bookmarks-page .content-area-header {
|
||||||
$searchbox-width: 180px;
|
--searchbox-max-width: 350px;
|
||||||
$searchbox-width-md: 300px;
|
--searchbox-height: 1.8rem;
|
||||||
$searchbox-height: 1.8rem;
|
|
||||||
|
@media (max-width: $size-sm) {
|
||||||
|
--searchbox-max-width: initial;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmarks-page #search {
|
||||||
|
flex: 1 1 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
// Regular input
|
// Regular input
|
||||||
input[type='search'] {
|
input[type='search'] {
|
||||||
width: $searchbox-width;
|
height: var(--searchbox-height);
|
||||||
height: $searchbox-height;
|
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
|
||||||
@media (min-width: $control-width-md) {
|
|
||||||
width: $searchbox-width-md;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced auto-complete input
|
// Enhanced auto-complete input
|
||||||
// This needs a bit more wrangling to make the CSS component align with the attached button
|
// This needs a bit more wrangling to make the CSS component align with the attached button
|
||||||
.form-autocomplete {
|
.form-autocomplete {
|
||||||
height: $searchbox-height;
|
height: var(--searchbox-height);
|
||||||
|
|
||||||
.form-autocomplete-input {
|
.form-autocomplete-input {
|
||||||
width: $searchbox-width;
|
width: 100%;
|
||||||
height: $searchbox-height;
|
height: var(--searchbox-height);
|
||||||
|
|
||||||
input[type='search'] {
|
input[type='search'] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -34,13 +39,19 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: $control-width-md) {
|
|
||||||
width: $searchbox-width-md;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: var(--searchbox-min-width);
|
||||||
|
max-width: var(--searchbox-max-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group > :first-child {
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Group search options button with search button
|
// Group search options button with search button
|
||||||
.input-group input[type='submit'] {
|
.input-group input[type='submit'] {
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
|
@@ -24,6 +24,7 @@
|
|||||||
@import "../../node_modules/spectre.css/src/dropdowns";
|
@import "../../node_modules/spectre.css/src/dropdowns";
|
||||||
@import "../../node_modules/spectre.css/src/empty";
|
@import "../../node_modules/spectre.css/src/empty";
|
||||||
@import "../../node_modules/spectre.css/src/menus";
|
@import "../../node_modules/spectre.css/src/menus";
|
||||||
|
@import "../../node_modules/spectre.css/src/modals";
|
||||||
@import "../../node_modules/spectre.css/src/pagination";
|
@import "../../node_modules/spectre.css/src/pagination";
|
||||||
@import "../../node_modules/spectre.css/src/tabs";
|
@import "../../node_modules/spectre.css/src/tabs";
|
||||||
@import "../../node_modules/spectre.css/src/toasts";
|
@import "../../node_modules/spectre.css/src/toasts";
|
||||||
@@ -100,6 +101,18 @@ ul.menu li:first-child {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
// Add border to separate from background in dark mode
|
||||||
|
.modal-container {
|
||||||
|
border: solid 1px $border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix modal header to use default color
|
||||||
|
.modal-header {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Increase input font size on small viewports to prevent zooming on focus the input
|
// Increase input font size on small viewports to prevent zooming on focus the input
|
||||||
// on mobile devices. 430px relates to the "normalized" iPhone 14 Pro Max
|
// on mobile devices. 430px relates to the "normalized" iPhone 14 Pro Max
|
||||||
// viewport size
|
// viewport size
|
||||||
|
@@ -14,9 +14,10 @@
|
|||||||
<section class="content-area col-2">
|
<section class="content-area col-2">
|
||||||
<div class="content-area-header mb-0">
|
<div class="content-area-header mb-0">
|
||||||
<h2>Archived bookmarks</h2>
|
<h2>Archived bookmarks</h2>
|
||||||
<div class="d-flex">
|
<div class="header-controls">
|
||||||
{% bookmark_search bookmark_list.search tag_cloud.tags mode='archived' %}
|
{% bookmark_search bookmark_list.search tag_cloud.tags mode='archived' %}
|
||||||
{% include 'bookmarks/bulk_edit/toggle.html' %}
|
{% include 'bookmarks/bulk_edit/toggle.html' %}
|
||||||
|
<button ld-modal modal-content=".tag-cloud" class="btn ml-2 show-md">Tags</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -14,9 +14,10 @@
|
|||||||
<section class="content-area col-2">
|
<section class="content-area col-2">
|
||||||
<div class="content-area-header mb-0">
|
<div class="content-area-header mb-0">
|
||||||
<h2>Bookmarks</h2>
|
<h2>Bookmarks</h2>
|
||||||
<div class="d-flex">
|
<div class="header-controls">
|
||||||
{% bookmark_search bookmark_list.search tag_cloud.tags %}
|
{% bookmark_search bookmark_list.search tag_cloud.tags %}
|
||||||
{% include 'bookmarks/bulk_edit/toggle.html' %}
|
{% include 'bookmarks/bulk_edit/toggle.html' %}
|
||||||
|
<button ld-modal modal-content=".tag-cloud" class="btn ml-2 show-md">Tags</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -1,48 +1,42 @@
|
|||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
|
|
||||||
<div class="search">
|
<form id="search" action="" method="get" role="search">
|
||||||
<form action="" method="get" role="search">
|
<div class="input-group">
|
||||||
<div class="d-flex">
|
<input type="search" class="form-input" name="q" placeholder="Search for words or #tags"
|
||||||
<div class="input-group">
|
value="{{ search.query }}">
|
||||||
<span id="search-input-wrap">
|
<input type="submit" value="Search" class="btn input-group-btn">
|
||||||
<input type="search" class="form-input" name="q" placeholder="Search for words or #tags"
|
</div>
|
||||||
value="{{ search.query }}">
|
<div class="search-options dropdown dropdown-right">
|
||||||
</span>
|
<button type="button" class="btn dropdown-toggle">
|
||||||
<input type="submit" value="Search" class="btn input-group-btn">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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="M4 10a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path>
|
||||||
|
<path d="M6 4v4"></path>
|
||||||
|
<path d="M6 12v8"></path>
|
||||||
|
<path d="M10 16a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path>
|
||||||
|
<path d="M12 4v10"></path>
|
||||||
|
<path d="M12 18v2"></path>
|
||||||
|
<path d="M16 7a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path>
|
||||||
|
<path d="M18 4v1"></path>
|
||||||
|
<path d="M18 9v11"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="menu text-sm" tabindex="0">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.sort.id_for_label }}" class="form-label">Sort by</label>
|
||||||
|
{{ form.sort|add_class:"form-select select-sm" }}
|
||||||
</div>
|
</div>
|
||||||
<div class="search-options dropdown dropdown-right">
|
<div class="actions">
|
||||||
<button type="button" class="btn dropdown-toggle">
|
<button type="submit" class="btn btn-sm btn-primary">Apply</button>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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="M4 10a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path>
|
|
||||||
<path d="M6 4v4"></path>
|
|
||||||
<path d="M6 12v8"></path>
|
|
||||||
<path d="M10 16a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path>
|
|
||||||
<path d="M12 4v10"></path>
|
|
||||||
<path d="M12 18v2"></path>
|
|
||||||
<path d="M16 7a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path>
|
|
||||||
<path d="M18 4v1"></path>
|
|
||||||
<path d="M18 9v11"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div class="menu text-sm" tabindex="0">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="{{ form.sort.id_for_label }}" class="form-label">Sort by</label>
|
|
||||||
{{ form.sort|add_class:"form-select select-sm" }}
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<button type="submit" class="btn btn-sm btn-primary">Apply</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% for hidden_field in form.hidden_fields %}
|
{% for hidden_field in form.hidden_fields %}
|
||||||
{{ hidden_field }}
|
{{ hidden_field }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
|
|
||||||
{# Replace search input with auto-complete component #}
|
{# Replace search input with auto-complete component #}
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
@@ -55,10 +49,10 @@
|
|||||||
user: '{{ search.user }}',
|
user: '{{ search.user }}',
|
||||||
}
|
}
|
||||||
const apiClient = new linkding.ApiClient('{% url 'bookmarks:api-root' %}')
|
const apiClient = new linkding.ApiClient('{% url 'bookmarks:api-root' %}')
|
||||||
const wrapper = document.getElementById('search-input-wrap')
|
const input = document.querySelector('#search input[name="q"]')
|
||||||
const newWrapper = document.createElement('div')
|
const wrapper = document.createElement('div')
|
||||||
new linkding.SearchAutoComplete({
|
new linkding.SearchAutoComplete({
|
||||||
target: newWrapper,
|
target: wrapper,
|
||||||
props: {
|
props: {
|
||||||
name: 'q',
|
name: 'q',
|
||||||
placeholder: 'Search for words or #tags',
|
placeholder: 'Search for words or #tags',
|
||||||
@@ -70,6 +64,6 @@
|
|||||||
search,
|
search,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
wrapper.parentElement.replaceChild(newWrapper, wrapper)
|
input.replaceWith(wrapper.firstElementChild);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
@@ -13,7 +13,10 @@
|
|||||||
<section class="content-area col-2">
|
<section class="content-area col-2">
|
||||||
<div class="content-area-header">
|
<div class="content-area-header">
|
||||||
<h2>Shared bookmarks</h2>
|
<h2>Shared bookmarks</h2>
|
||||||
{% bookmark_search bookmark_list.search tag_cloud.tags mode='shared' %}
|
<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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="bookmark-actions" action="{{ bookmark_list.action_url|safe }}"
|
<form class="bookmark-actions" action="{{ bookmark_list.action_url|safe }}"
|
||||||
|
Reference in New Issue
Block a user