mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-10-02 18:04:29 +08:00
Improve Breadcrumbs responsiveness [SCI-8494] (#5778)
- Refactor Breadcrumbs using VUE. - Implement responsiveness breakpoints
This commit is contained in:
parent
e7c238ab3d
commit
effdc9563a
7 changed files with 280 additions and 135 deletions
|
@ -1,23 +1,44 @@
|
|||
.sci--layout-navigation-breadcrumbs {
|
||||
--max-breadcrumbs-link-width: 11.25rem;
|
||||
align-items: center;
|
||||
background-color: $color-white;
|
||||
column-gap: .875rem;
|
||||
background-color: var(--sn-white);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.breadcrumbs-container {
|
||||
@include font-small;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
max-width: calc(100vw - var(--left-navigation-width) - 3rem);
|
||||
overflow: hidden;
|
||||
width: calc(100vw - var(--left-navigation-width) - 3rem);
|
||||
|
||||
.delimiter {
|
||||
@include font-button;
|
||||
color: var(--sn-grey);
|
||||
font-weight: bold;
|
||||
padding: 0 .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumbs-item {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
.breadcrumbs-link {
|
||||
@include font-small;
|
||||
color: var(--sn-blue);
|
||||
display: inline-block;
|
||||
max-width: var(--max-breadcrumbs-link-width);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&:last-child {
|
||||
color: var(--sn-grey);
|
||||
&.shortened {
|
||||
max-width: var(--max-breadcrumbs-link-width);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&.plain-text {
|
||||
|
@ -25,24 +46,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.delimiter {
|
||||
padding-bottom: .25rem;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumbs-collapsed-container {
|
||||
color: var(--sn-blue);
|
||||
position: relative;
|
||||
|
||||
.show-breadcrumbs {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
a {
|
||||
@include font-button;
|
||||
color: var(--sn-blue);
|
||||
}
|
||||
&:last-child .breadcrumbs-link {
|
||||
color: var(--sn-grey);
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumbs-collapsed-container .breadcrumbs-item .breadcrumbs-link {
|
||||
color: var(--sn-blue);
|
||||
}
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
.breadcrumbs-container {
|
||||
@include font-small;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.delimiter {
|
||||
@include font-button;
|
||||
color: $color-silver-chalice;
|
||||
font-weight: bold;
|
||||
padding: 0 .5em;
|
||||
}
|
||||
|
||||
.breadcrumbs-link {
|
||||
display: inline-block;
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumbs-collapsed-container {
|
||||
color: $brand-primary;
|
||||
position: relative;
|
||||
|
||||
.show-breadcrumbs {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
a {
|
||||
@include font-button;
|
||||
color: $brand-primary;
|
||||
}
|
||||
}
|
||||
}
|
17
app/javascript/packs/vue/navigation/breadcrumbs.js
Normal file
17
app/javascript/packs/vue/navigation/breadcrumbs.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import TurbolinksAdapter from 'vue-turbolinks';
|
||||
import Vue from 'vue/dist/vue.esm';
|
||||
import Breadcrumbs from '../../../vue/navigation/breadcrumbs/breadcrumbs.vue';
|
||||
import PerfectScrollbar from 'vue2-perfect-scrollbar';
|
||||
import 'vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css';
|
||||
|
||||
Vue.use(TurbolinksAdapter);
|
||||
Vue.use(PerfectScrollbar);
|
||||
Vue.prototype.i18n = window.I18n;
|
||||
|
||||
window.breadcrumbsComponent = new Vue({
|
||||
el: '#breadcrumbs',
|
||||
name: 'BreadcrumbsContainer',
|
||||
components: {
|
||||
breadcrumbs: Breadcrumbs
|
||||
}
|
||||
});
|
162
app/javascript/vue/navigation/breadcrumbs/breadcrumbs.vue
Normal file
162
app/javascript/vue/navigation/breadcrumbs/breadcrumbs.vue
Normal file
|
@ -0,0 +1,162 @@
|
|||
<template>
|
||||
<div class="breadcrumbs-container" ref="container">
|
||||
<a
|
||||
v-if="firstItem && firstItem.url !== lastItem.url"
|
||||
:href="firstItem.url"
|
||||
class="breadcrumbs-item"
|
||||
:title="firstItem.label"
|
||||
ref="firstItem"
|
||||
>
|
||||
<span
|
||||
class="breadcrumbs-link"
|
||||
:class="{
|
||||
shortened:
|
||||
state === State.SHORTENED || state === State.SHORTENED_COLLAPSED,
|
||||
'plain-text': !firstItem.url
|
||||
}"
|
||||
>{{ firstItem.label }}</span>
|
||||
<span class="delimiter">
|
||||
<img :src="delimiterUrl" alt="navigate next" class="navigate_next" />
|
||||
</span>
|
||||
</a>
|
||||
<template v-if="middleItems.length">
|
||||
<BreadcrumbsDropdown
|
||||
v-if="hiddenMiddleItems.length"
|
||||
:items="hiddenMiddleItems"
|
||||
:delimiterUrl="delimiterUrl"
|
||||
/>
|
||||
<a
|
||||
v-for="(item, index) in visibleMiddleItems"
|
||||
:key="item.url"
|
||||
:href="item.url"
|
||||
class="breadcrumbs-item"
|
||||
:title="item.label"
|
||||
:ref="`visibleMiddleItem-${index}`"
|
||||
>
|
||||
<span
|
||||
class="breadcrumbs-link"
|
||||
:class="{
|
||||
shortened:
|
||||
state === State.SHORTENED || state === State.SHORTENED_COLLAPSED
|
||||
}"
|
||||
>{{ item.label }}</span
|
||||
>
|
||||
<span class="delimiter">
|
||||
<img :src="delimiterUrl" alt="navigate next" class="navigate_next" />
|
||||
</span>
|
||||
</a>
|
||||
</template>
|
||||
<span class="breadcrumbs-item" title="lastItem.label" ref="lastItem">
|
||||
<span
|
||||
class="breadcrumbs-link"
|
||||
:title="lastItem.label"
|
||||
:class="{
|
||||
shortened:
|
||||
state === State.SHORTENED || state === State.SHORTENED_COLLAPSED
|
||||
}"
|
||||
>{{ lastItem.label }}</span
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BreadcrumbsDropdown from "./breadcrumbs_dropdown.vue";
|
||||
|
||||
const State = Object.freeze({
|
||||
INITIAL: "initial",
|
||||
EXPENDED: "expended",
|
||||
SHORTENED: "shortened",
|
||||
SHORTENED_COLLAPSED: "shortened_collapsed"
|
||||
});
|
||||
const dropdownWidth = 60;
|
||||
export default {
|
||||
name: "Breadcrumbs",
|
||||
props: {
|
||||
breadcrumbsItems: String,
|
||||
delimiterUrl: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dropdownWidth: dropdownWidth,
|
||||
State,
|
||||
state: State.INITIAL,
|
||||
items: [],
|
||||
hiddenMiddleItems: [],
|
||||
visibleMiddleItems: []
|
||||
};
|
||||
},
|
||||
components: {
|
||||
BreadcrumbsDropdown
|
||||
},
|
||||
watch: {
|
||||
breadcrumbsItems: {
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.items = JSON.parse(this.breadcrumbsItems);
|
||||
this.reset();
|
||||
this.$nextTick(() => {
|
||||
this.updateItems();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
firstItem() {
|
||||
if (this.items.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
return this.items[0];
|
||||
},
|
||||
lastItem() {
|
||||
return this.items[this.items.length - 1];
|
||||
},
|
||||
middleItems() {
|
||||
if (this.items.length <= 2) return [];
|
||||
|
||||
return this.items.slice(1, -1);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateItems() {
|
||||
const width = this.$refs.container.clientWidth;
|
||||
const scrollWidth = Array.from(this.$refs.container.children).reduce(
|
||||
(totalWidth, child) => totalWidth + child.offsetWidth,
|
||||
0
|
||||
);
|
||||
if (this.state === this.State.INITIAL) {
|
||||
if (width < scrollWidth) {
|
||||
this.state = this.State.SHORTENED;
|
||||
this.$nextTick(() => {
|
||||
this.updateItems();
|
||||
});
|
||||
} else {
|
||||
this.state = this.State.EXPENDED;
|
||||
}
|
||||
} else if (this.state === this.State.SHORTENED) {
|
||||
if (width < scrollWidth) {
|
||||
this.state = this.State.SHORTENED_COLLAPSED;
|
||||
let visibleWidth =
|
||||
this.dropdownWidth +
|
||||
this.$refs.firstItem.offsetWidth +
|
||||
this.$refs.lastItem.offsetWidth;
|
||||
let index = this.middleItems.length - 1;
|
||||
|
||||
while (visibleWidth <= width && index >= 0) {
|
||||
visibleWidth += this.$refs[`visibleMiddleItem-${index}`][0]
|
||||
.offsetWidth;
|
||||
index--;
|
||||
}
|
||||
this.visibleMiddleItems = this.middleItems.slice(index + 2);
|
||||
this.hiddenMiddleItems = this.middleItems.slice(0, index + 2);
|
||||
}
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
this.state = this.State.INITIAL;
|
||||
this.hiddenMiddleItems = [];
|
||||
this.visibleMiddleItems = this.middleItems;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,56 @@
|
|||
<template>
|
||||
<span class="breadcrumbs-collapsed-container">
|
||||
<span
|
||||
class="cursor-pointer text-sn-blue flex flex-nowrap whitespace-nowrap items-center gap-1"
|
||||
data-toggle="dropdown"
|
||||
:title="i18n.t('projects.index.breadcrumbs_collapsed')"
|
||||
@click="open = !open"
|
||||
>
|
||||
•••
|
||||
<span class="caret"></span>
|
||||
<span class="delimiter">
|
||||
<img :src="delimiterUrl" alt="navigate next" class="navigate_next" />
|
||||
</span>
|
||||
</span>
|
||||
<perfect-scrollbar>
|
||||
<ul
|
||||
class="absolute top-11 left-0 w-56 max-h-36 flex flex-col items-stretch gap-2 overflow-auto
|
||||
bg-sn-white border border-sn-grey rounded-md shadow-md list-none p-2"
|
||||
:class="{ hidden: !open }"
|
||||
>
|
||||
<li v-for="item in items" :key="item.url">
|
||||
<a :href="item.url" class="breadcrumbs-item breadcrumb-link shortened" title="item.label">
|
||||
<span
|
||||
class="breadcrumbs-link shortened"
|
||||
>{{ item.label }}</span>
|
||||
<span class="delimiter">
|
||||
<img
|
||||
:src="delimiterUrl"
|
||||
alt="navigate next"
|
||||
class="navigate_next"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</perfect-scrollbar>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { PerfectScrollbar } from 'vue2-perfect-scrollbar';
|
||||
|
||||
export default {
|
||||
name: "BreadcrumbsDropdown",
|
||||
props: {
|
||||
items: Array,
|
||||
delimiterUrl: String
|
||||
},
|
||||
components: { PerfectScrollbar },
|
||||
data() {
|
||||
return {
|
||||
open: false
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -1,70 +1,9 @@
|
|||
<% if @breadcrumbs_items&.length %>
|
||||
<% shortened = @breadcrumbs_items.length > 4 %>
|
||||
|
||||
<% if shortened %>
|
||||
<% first_breadcrumb_item = @breadcrumbs_items.shift
|
||||
last_breadcrumb_items = @breadcrumbs_items.pop(2) %>
|
||||
<%= link_to(first_breadcrumb_item[:label], first_breadcrumb_item[:url],
|
||||
class: "breadcrumbs-link",
|
||||
title: first_breadcrumb_item[:label]) %>
|
||||
<span class="delimiter">
|
||||
<%= image_tag "icon_small/navigate_next.svg",
|
||||
alt: "navigate next",
|
||||
class: "navigate_next" %>
|
||||
</span>
|
||||
<span class="breadcrumbs-collapsed-container">
|
||||
<span class="show-breadcrumbs" data-toggle="dropdown" title="<%= t('projects.index.breadcrumbs_collapsed') %>">
|
||||
•••
|
||||
<span class="caret pull-right"></span>
|
||||
</span>
|
||||
<ul class="dropdown-menu breadcrumbs-dropdown" role="menu">
|
||||
<% @breadcrumbs_items.each do |item| %>
|
||||
<li>
|
||||
<%= link_to(item[:label], item[:url]) %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</span>
|
||||
<span class="delimiter">
|
||||
<%= image_tag "icon_small/navigate_next.svg",
|
||||
alt: "navigate next",
|
||||
class: "navigate_next" %>
|
||||
</span>
|
||||
<% item = last_breadcrumb_items.first %>
|
||||
<%= link_to(item[:label], item[:url],
|
||||
class: "breadcrumbs-link",
|
||||
title: item[:label]) %>
|
||||
<span class="delimiter">
|
||||
<%= image_tag "icon_small/navigate_next.svg",
|
||||
alt: "navigate next",
|
||||
class: "navigate_next" %>
|
||||
</span>
|
||||
<% last_item = last_breadcrumb_items.last %>
|
||||
<span class="breadcrumbs-link" title="<%= last_item[:label] %>">
|
||||
<%= last_item[:label] %>
|
||||
</span>
|
||||
<% else %>
|
||||
<% last_item = @breadcrumbs_items.pop %>
|
||||
<% @breadcrumbs_items.each do |item| %>
|
||||
<% if item[:url] %>
|
||||
<%= link_to(item[:label], item[:url],
|
||||
class: "breadcrumbs-link",
|
||||
title: item[:label]) %>
|
||||
<% else %>
|
||||
<%= content_tag(:span, item[:label],
|
||||
class: "breadcrumbs-link plain-text",
|
||||
title: item[:label]) %>
|
||||
<% end %>
|
||||
|
||||
<span class="delimiter">
|
||||
<img src="<%= asset_path "icon_small/navigate_next.svg"%>"
|
||||
alt="navigate next"
|
||||
class="navigate_next">
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<span class="breadcrumbs-link" title="<%= last_item[:label] %>">
|
||||
<%= last_item[:label] %>
|
||||
</span>
|
||||
<% end %>
|
||||
<div id="breadcrumbs" data-behaviour="vue">
|
||||
<breadcrumbs
|
||||
breadcrumbs-items="<%= @breadcrumbs_items.to_json %>"
|
||||
delimiter-url="<%= asset_path "icon_small/navigate_next.svg" %>"
|
||||
/>
|
||||
</div>
|
||||
<%= javascript_include_tag 'vue_navigation_breadcrumbs' %>
|
||||
<% end %>
|
||||
|
|
|
@ -33,6 +33,7 @@ const entryList = {
|
|||
vue_repository_assign_items_to_task_modal: './app/javascript/packs/vue/assign_items_to_task_modal.js',
|
||||
vue_navigation_top_menu: './app/javascript/packs/vue/navigation/top_menu.js',
|
||||
vue_navigation_navigator: './app/javascript/packs/vue/navigation/navigator.js',
|
||||
vue_navigation_breadcrumbs: './app/javascript/packs/vue/navigation/breadcrumbs.js',
|
||||
vue_components_action_toolbar: './app/javascript/packs/vue/action_toolbar.js'
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue