mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-10-03 02:14: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 {
|
.sci--layout-navigation-breadcrumbs {
|
||||||
--max-breadcrumbs-link-width: 11.25rem;
|
--max-breadcrumbs-link-width: 11.25rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: $color-white;
|
background-color: var(--sn-white);
|
||||||
column-gap: .875rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
padding-left: 1.5rem;
|
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 {
|
.breadcrumbs-link {
|
||||||
@include font-small;
|
@include font-small;
|
||||||
color: var(--sn-blue);
|
color: var(--sn-blue);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: var(--max-breadcrumbs-link-width);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
&:last-child {
|
&.shortened {
|
||||||
color: var(--sn-grey);
|
max-width: var(--max-breadcrumbs-link-width);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.plain-text {
|
&.plain-text {
|
||||||
|
@ -25,24 +46,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.delimiter {
|
&:hover {
|
||||||
padding-bottom: .25rem;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumbs-collapsed-container {
|
&:last-child .breadcrumbs-link {
|
||||||
color: var(--sn-blue);
|
color: var(--sn-grey);
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.show-breadcrumbs {
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
@include font-button;
|
|
||||||
color: var(--sn-blue);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 %>
|
<% if @breadcrumbs_items&.length %>
|
||||||
<% shortened = @breadcrumbs_items.length > 4 %>
|
<div id="breadcrumbs" data-behaviour="vue">
|
||||||
|
<breadcrumbs
|
||||||
<% if shortened %>
|
breadcrumbs-items="<%= @breadcrumbs_items.to_json %>"
|
||||||
<% first_breadcrumb_item = @breadcrumbs_items.shift
|
delimiter-url="<%= asset_path "icon_small/navigate_next.svg" %>"
|
||||||
last_breadcrumb_items = @breadcrumbs_items.pop(2) %>
|
/>
|
||||||
<%= link_to(first_breadcrumb_item[:label], first_breadcrumb_item[:url],
|
</div>
|
||||||
class: "breadcrumbs-link",
|
<%= javascript_include_tag 'vue_navigation_breadcrumbs' %>
|
||||||
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 %>
|
|
||||||
<% end %>
|
<% 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_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_top_menu: './app/javascript/packs/vue/navigation/top_menu.js',
|
||||||
vue_navigation_navigator: './app/javascript/packs/vue/navigation/navigator.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'
|
vue_components_action_toolbar: './app/javascript/packs/vue/action_toolbar.js'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue