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/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";

View file

@ -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;
}
} }
} }

View file

@ -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,11 +39,17 @@
margin: 0; margin: 0;
border: none; border: none;
} }
}
}
@media (min-width: $control-width-md) { .input-group {
width: $searchbox-width-md; 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

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -1,13 +1,9 @@
{% 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="d-flex">
<div class="input-group"> <div class="input-group">
<span id="search-input-wrap">
<input type="search" class="form-input" name="q" placeholder="Search for words or #tags" <input type="search" class="form-input" name="q" placeholder="Search for words or #tags"
value="{{ search.query }}"> value="{{ search.query }}">
</span>
<input type="submit" value="Search" class="btn input-group-btn"> <input type="submit" value="Search" class="btn input-group-btn">
</div> </div>
<div class="search-options dropdown dropdown-right"> <div class="search-options dropdown dropdown-right">
@ -36,13 +32,11 @@
</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>

View file

@ -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>
<div class="header-controls">
{% bookmark_search bookmark_list.search tag_cloud.tags mode='shared' %} {% 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 }}"