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:
Sascha Ißbrücker 2023-09-10 09:44:49 +03:00 committed by GitHub
parent 0975914a86
commit d9c4ddb4d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 160 additions and 65 deletions

View 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);

View file

@ -4,6 +4,7 @@ import { ApiClient } from "./api";
import "./behaviors/bookmark-page";
import "./behaviors/bulk-edit";
import "./behaviors/confirm-button";
import "./behaviors/modal";
import "./behaviors/global-shortcuts";
import "./behaviors/tag-autocomplete";

View file

@ -50,14 +50,20 @@ section.content-area {
border-bottom: solid 1px $border-color;
display: flex;
flex-wrap: wrap;
column-gap: $unit-6;
padding-bottom: $unit-2;
margin-bottom: $unit-4;
h2 {
flex: 0 0 auto;
line-height: 1.8rem;
margin-right: auto;
margin-bottom: 0;
}
.header-controls {
flex: 1 1 0;
display: flex;
}
}
}

View file

@ -2,31 +2,36 @@
grid-gap: $unit-10;
}
/* Bookmark search box */
.bookmarks-page .search {
$searchbox-width: 180px;
$searchbox-width-md: 300px;
$searchbox-height: 1.8rem;
/* Bookmark area header controls */
.bookmarks-page .content-area-header {
--searchbox-max-width: 350px;
--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
input[type='search'] {
width: $searchbox-width;
height: $searchbox-height;
height: var(--searchbox-height);
-webkit-appearance: none;
@media (min-width: $control-width-md) {
width: $searchbox-width-md;
}
}
// Enhanced auto-complete input
// This needs a bit more wrangling to make the CSS component align with the attached button
.form-autocomplete {
height: $searchbox-height;
height: var(--searchbox-height);
.form-autocomplete-input {
width: $searchbox-width;
height: $searchbox-height;
width: 100%;
height: var(--searchbox-height);
input[type='search'] {
width: 100%;
@ -34,13 +39,19 @@
margin: 0;
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
.input-group input[type='submit'] {
border-top-right-radius: 0;

View file

@ -24,6 +24,7 @@
@import "../../node_modules/spectre.css/src/dropdowns";
@import "../../node_modules/spectre.css/src/empty";
@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/tabs";
@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
// on mobile devices. 430px relates to the "normalized" iPhone 14 Pro Max
// viewport size

View file

@ -14,9 +14,10 @@
<section class="content-area col-2">
<div class="content-area-header mb-0">
<h2>Archived bookmarks</h2>
<div class="d-flex">
<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>
</div>
</div>

View file

@ -14,9 +14,10 @@
<section class="content-area col-2">
<div class="content-area-header mb-0">
<h2>Bookmarks</h2>
<div class="d-flex">
<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>
</div>
</div>

View file

@ -1,48 +1,42 @@
{% load widget_tweaks %}
<div class="search">
<form action="" method="get" role="search">
<div class="d-flex">
<div class="input-group">
<span id="search-input-wrap">
<input type="search" class="form-input" name="q" placeholder="Search for words or #tags"
value="{{ search.query }}">
</span>
<input type="submit" value="Search" class="btn input-group-btn">
<form id="search" action="" method="get" role="search">
<div class="input-group">
<input type="search" class="form-input" name="q" placeholder="Search for words or #tags"
value="{{ search.query }}">
<input type="submit" value="Search" class="btn input-group-btn">
</div>
<div class="search-options dropdown dropdown-right">
<button type="button" class="btn dropdown-toggle">
<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="search-options dropdown dropdown-right">
<button type="button" class="btn dropdown-toggle">
<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 class="actions">
<button type="submit" class="btn btn-sm btn-primary">Apply</button>
</div>
</div>
</div>
{% for hidden_field in form.hidden_fields %}
{{ hidden_field }}
{% endfor %}
</form>
</div>
{% for hidden_field in form.hidden_fields %}
{{ hidden_field }}
{% endfor %}
</form>
{# Replace search input with auto-complete component #}
<script type="application/javascript">
@ -55,10 +49,10 @@
user: '{{ search.user }}',
}
const apiClient = new linkding.ApiClient('{% url 'bookmarks:api-root' %}')
const wrapper = document.getElementById('search-input-wrap')
const newWrapper = document.createElement('div')
const input = document.querySelector('#search input[name="q"]')
const wrapper = document.createElement('div')
new linkding.SearchAutoComplete({
target: newWrapper,
target: wrapper,
props: {
name: 'q',
placeholder: 'Search for words or #tags',
@ -70,6 +64,6 @@
search,
}
})
wrapper.parentElement.replaceChild(newWrapper, wrapper)
input.replaceWith(wrapper.firstElementChild);
});
</script>

View file

@ -13,7 +13,10 @@
<section class="content-area col-2">
<div class="content-area-header">
<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>
<form class="bookmark-actions" action="{{ bookmark_list.action_url|safe }}"