mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-06-07 03:14:33 +08:00
Image manager: supported a tabbed interface on mobile
Makes interface relatively usable now on mobile sizes. Required updating of tab handling to support tabs being active at only mobile screen sizes, include change on resize, upon support for potentially nested tab usage. Tab component will now search within sensible depths for finding its own tabs and panels to control.
This commit is contained in:
@ -21,15 +21,23 @@ export class Tabs extends Component {
|
|||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
this.container = this.$el;
|
this.container = this.$el;
|
||||||
this.tabs = Array.from(this.container.querySelectorAll('[role="tab"]'));
|
this.tabList = this.container.querySelector('[role="tablist"]');
|
||||||
this.panels = Array.from(this.container.querySelectorAll('[role="tabpanel"]'));
|
this.tabs = Array.from(this.tabList.querySelectorAll('[role="tab"]'));
|
||||||
|
this.panels = Array.from(this.container.querySelectorAll(':scope > [role="tabpanel"], :scope > * > [role="tabpanel"]'));
|
||||||
|
this.activeUnder = this.$opts.activeUnder ? Number(this.$opts.activeUnder) : 10000;
|
||||||
|
this.active = null;
|
||||||
|
|
||||||
this.container.addEventListener('click', event => {
|
this.container.addEventListener('click', event => {
|
||||||
const button = event.target.closest('[role="tab"]');
|
const tab = event.target.closest('[role="tab"]');
|
||||||
if (button) {
|
if (tab && this.tabs.includes(tab)) {
|
||||||
this.show(button.getAttribute('aria-controls'));
|
this.show(tab.getAttribute('aria-controls'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener('resize', this.updateActiveState.bind(this), {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
this.updateActiveState();
|
||||||
}
|
}
|
||||||
|
|
||||||
show(sectionId) {
|
show(sectionId) {
|
||||||
@ -46,4 +54,34 @@ export class Tabs extends Component {
|
|||||||
this.$emit('change', {showing: sectionId});
|
this.$emit('change', {showing: sectionId});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateActiveState() {
|
||||||
|
const active = window.innerWidth < this.activeUnder;
|
||||||
|
if (active === this.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
this.activate();
|
||||||
|
} else {
|
||||||
|
this.deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.active = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
activate() {
|
||||||
|
this.show(this.panels[0].id);
|
||||||
|
this.tabList.toggleAttribute('hidden', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivate() {
|
||||||
|
for (const panel of this.panels) {
|
||||||
|
panel.removeAttribute('hidden');
|
||||||
|
}
|
||||||
|
for (const tab of this.tabs) {
|
||||||
|
tab.setAttribute('aria-selected', 'false');
|
||||||
|
}
|
||||||
|
this.tabList.toggleAttribute('hidden', true);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -380,6 +380,12 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
|||||||
background-color: #FFF;
|
background-color: #FFF;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include smaller-than($s) {
|
||||||
|
.image-manager-filter-bar .contained-search-box input {
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
.image-manager-filters {
|
.image-manager-filters {
|
||||||
box-shadow: $bs-med;
|
box-shadow: $bs-med;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@ -475,6 +481,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
|||||||
|
|
||||||
.image-manager-sidebar {
|
.image-manager-sidebar {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
margin: 0 auto;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
border-inline-start: 1px solid #DDD;
|
border-inline-start: 1px solid #DDD;
|
||||||
@ -500,6 +507,11 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@include smaller-than($m) {
|
||||||
|
.image-manager-sidebar {
|
||||||
|
border-inline-start: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.image-manager-content {
|
.image-manager-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -298,6 +298,10 @@ body.flexbox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Border radiuses
|
* Border radiuses
|
||||||
*/
|
*/
|
||||||
|
@ -24,67 +24,91 @@
|
|||||||
<button refs="popup@hide" type="button" class="popup-header-close">@icon('close')</button>
|
<button refs="popup@hide" type="button" class="popup-header-close">@icon('close')</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div refs="dropzone@drop-target" class="flex-fill image-manager-body">
|
<div component="tabs"
|
||||||
<div class="image-manager-content">
|
option:tabs:active-under="880"
|
||||||
<div class="image-manager-filter-bar flex-container-row justify-space-between">
|
refs="dropzone@drop-target"
|
||||||
<div class="primary-background image-manager-filter-bar-bg"></div>
|
class="flex-container-column image-manager-body">
|
||||||
<div>
|
<div class="tab-container">
|
||||||
<form refs="image-manager@searchForm" class="contained-search-box">
|
<div role="tablist" class="hide-over-m mb-none">
|
||||||
<input refs="image-manager@searchInput"
|
<button id="image-manager-list-tab"
|
||||||
placeholder="{{ trans('components.image_search_hint') }}"
|
aria-selected="true"
|
||||||
type="text">
|
aria-controls="image-manager-list"
|
||||||
<button refs="image-manager@cancelSearch"
|
role="tab">Image List</button>
|
||||||
title="{{ trans('common.search_clear') }}"
|
<button id="image-manager-info-tab"
|
||||||
type="button"
|
aria-selected="true"
|
||||||
class="cancel">@icon('close')</button>
|
aria-controls="image-manager-info"
|
||||||
<button type="submit"
|
role="tab">Image Details</button>
|
||||||
title="{{ trans('common.search') }}">@icon('search')</button>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
<div class="flex-container-row flex-fill">
|
||||||
<div class="tab-container bordered tab-primary">
|
<div id="image-manager-list"
|
||||||
<div role="tablist" class="image-manager-filters flex-container-row">
|
tabindex="0"
|
||||||
<button refs="image-manager@filterTabs"
|
role="tabpanel"
|
||||||
data-filter="all"
|
aria-labelledby="image-manager-list-tab"
|
||||||
role="tab"
|
class="image-manager-content">
|
||||||
aria-selected="true"
|
<div class="image-manager-filter-bar flex-container-row wrap justify-space-between">
|
||||||
type="button"
|
<div class="primary-background image-manager-filter-bar-bg"></div>
|
||||||
title="{{ trans('components.image_all_title') }}">@icon('images')</button>
|
<div>
|
||||||
<button refs="image-manager@filterTabs"
|
<form refs="image-manager@searchForm" class="contained-search-box">
|
||||||
data-filter="book"
|
<input refs="image-manager@searchInput"
|
||||||
role="tab"
|
placeholder="{{ trans('components.image_search_hint') }}"
|
||||||
aria-selected="false"
|
type="text">
|
||||||
type="button"
|
<button refs="image-manager@cancelSearch"
|
||||||
title="{{ trans('components.image_book_title') }}">@icon('book', ['class' => 'svg-icon'])</button>
|
title="{{ trans('common.search_clear') }}"
|
||||||
<button refs="image-manager@filterTabs"
|
type="button"
|
||||||
data-filter="page"
|
class="cancel">@icon('close')</button>
|
||||||
role="tab"
|
<button type="submit"
|
||||||
aria-selected="false"
|
title="{{ trans('common.search') }}">@icon('search')</button>
|
||||||
type="button"
|
</form>
|
||||||
title="{{ trans('components.image_page_title') }}">@icon('page', ['class' => 'svg-icon'])</button>
|
</div>
|
||||||
|
<div class="tab-container bordered tab-primary">
|
||||||
|
<div role="tablist" class="image-manager-filters flex-container-row">
|
||||||
|
<button refs="image-manager@filterTabs"
|
||||||
|
data-filter="all"
|
||||||
|
role="tab"
|
||||||
|
aria-selected="true"
|
||||||
|
type="button"
|
||||||
|
title="{{ trans('components.image_all_title') }}">@icon('images')</button>
|
||||||
|
<button refs="image-manager@filterTabs"
|
||||||
|
data-filter="book"
|
||||||
|
role="tab"
|
||||||
|
aria-selected="false"
|
||||||
|
type="button"
|
||||||
|
title="{{ trans('components.image_book_title') }}">@icon('book', ['class' => 'svg-icon'])</button>
|
||||||
|
<button refs="image-manager@filterTabs"
|
||||||
|
data-filter="page"
|
||||||
|
role="tab"
|
||||||
|
aria-selected="false"
|
||||||
|
type="button"
|
||||||
|
title="{{ trans('components.image_page_title') }}">@icon('page', ['class' => 'svg-icon'])</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div refs="image-manager@listContainer" class="image-manager-list"></div>
|
||||||
|
<div refs="image-manager@loadMore" class="load-more" hidden>
|
||||||
|
<button type="button" class="button small outline">Load More</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div refs="image-manager@listContainer" class="image-manager-list"></div>
|
|
||||||
<div refs="image-manager@loadMore" class="load-more" hidden>
|
<div id="image-manager-info"
|
||||||
<button type="button" class="button small outline">Load More</button>
|
tabindex="0"
|
||||||
|
role="tabpanel"
|
||||||
|
aria-labelledby="image-manager-info-tab"
|
||||||
|
class="image-manager-sidebar flex-container-column">
|
||||||
|
|
||||||
|
<div refs="image-manager@dropzoneContainer">
|
||||||
|
<div refs="dropzone@status-area"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div refs="image-manager@form-container-placeholder" class="p-m text-small text-muted">
|
||||||
|
<p>{{ trans('components.image_intro') }}</p>
|
||||||
|
<p refs="image-manager@upload-hint">{{ trans('components.image_intro_upload') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div refs="image-manager@formContainer" class="inner flex">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="image-manager-sidebar flex-container-column">
|
|
||||||
|
|
||||||
<div refs="image-manager@dropzoneContainer">
|
|
||||||
<div refs="dropzone@status-area"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div refs="image-manager@form-container-placeholder" class="p-m text-small text-muted">
|
|
||||||
<p>{{ trans('components.image_intro') }}</p>
|
|
||||||
<p refs="image-manager@upload-hint">{{ trans('components.image_intro_upload') }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div refs="image-manager@formContainer" class="inner flex">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="popup-footer">
|
<div class="popup-footer">
|
||||||
|
Reference in New Issue
Block a user