add animation for navigation bar indicator (#257)
This commit is contained in:
parent
8f5a4aadca
commit
b7c90a4206
|
@ -29,6 +29,8 @@
|
||||||
firstTime = false
|
firstTime = false
|
||||||
this.refs.container.focus()
|
this.refs.container.focus()
|
||||||
}
|
}
|
||||||
|
let { page } = this.get()
|
||||||
|
this.store.set({currentPage: page})
|
||||||
},
|
},
|
||||||
store: () => store
|
store: () => store
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,16 @@
|
||||||
<nav class="main-nav">
|
<nav class="main-nav">
|
||||||
<ul class="main-nav-ul">
|
<ul class="main-nav-ul">
|
||||||
<li class="main-nav-li">
|
{#each $navPages as navPage (navPage.name)}
|
||||||
<NavItem {page} name="home" href="/" svg="#pinafore-logo" label="Home" />
|
|
||||||
</li>
|
|
||||||
<li class="main-nav-li">
|
|
||||||
<NavItem {page} name="notifications" href="/notifications" svg="#fa-bell" label="Notifications" />
|
|
||||||
</li>
|
|
||||||
{#if $pinnedPage === '/local'}
|
|
||||||
<li class="main-nav-li">
|
<li class="main-nav-li">
|
||||||
<NavItem {page} name="local" href="/local" svg="#fa-users" label="Local" />
|
<NavItem
|
||||||
|
{page}
|
||||||
|
name={navPage.name}
|
||||||
|
href={navPage.href}
|
||||||
|
svg={navPage.svg}
|
||||||
|
label={navPage.label}
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
{:elseif $pinnedPage === '/federated'}
|
{/each}
|
||||||
<li class="main-nav-li">
|
|
||||||
<NavItem {page} name="federated" href="/federated" svg="#fa-globe" label="Federated" />
|
|
||||||
</li>
|
|
||||||
{:elseif $pinnedPage === '/favorites'}
|
|
||||||
<li class="main-nav-li">
|
|
||||||
<NavItem {page} name="favorites" href="/favorites" svg="#fa-star" label="Favorites" />
|
|
||||||
</li>
|
|
||||||
{:elseif $pinnedPage.startsWith('/lists/')}
|
|
||||||
<li class="main-nav-li">
|
|
||||||
<NavItem {page} name="lists" href={$pinnedPage} svg="#fa-bars" label={$pinnedListTitle} />
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
<li class="main-nav-li">
|
|
||||||
<NavItem {page} name="community" href="/community" svg="#fa-comments" label="Community" />
|
|
||||||
</li>
|
|
||||||
<li class="main-nav-li">
|
|
||||||
<NavItem {page} name="search" href="/search" svg="#fa-search" label="Search" />
|
|
||||||
</li>
|
|
||||||
<li class="main-nav-li">
|
|
||||||
<NavItem {page} name="settings" href="/settings" svg="#fa-gear" label="Settings" />
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
|
@ -3,28 +3,41 @@
|
||||||
aria-current={selected}
|
aria-current={selected}
|
||||||
on:click="onClick(event)"
|
on:click="onClick(event)"
|
||||||
{href} >
|
{href} >
|
||||||
{#if name === 'notifications'}
|
<div class="nav-icon-and-label">
|
||||||
|
{#if name === 'notifications'}
|
||||||
<div class="nav-link-svg-wrapper">
|
<div class="nav-link-svg-wrapper">
|
||||||
<svg class="nav-link-svg">
|
<svg class="nav-link-svg">
|
||||||
<use xlink:href={svg} />
|
<use xlink:href={svg} />
|
||||||
</svg>
|
</svg>
|
||||||
{#if $hasNotifications}
|
{#if $hasNotifications}
|
||||||
<span class="nav-link-notifications" aria-hidden="true">
|
<span class="nav-link-notifications" aria-hidden="true">
|
||||||
{$numberOfNotifications}
|
{$numberOfNotifications}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<svg class="nav-link-svg">
|
<svg class="nav-link-svg">
|
||||||
<use xlink:href={svg} />
|
<use xlink:href={svg} />
|
||||||
</svg>
|
</svg>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="nav-link-label">{label}</span>
|
<span class="nav-link-label">{label}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-indicator"
|
||||||
|
nav-indicator-key={name}
|
||||||
|
ref:indicator
|
||||||
|
></div>
|
||||||
</a>
|
</a>
|
||||||
<style>
|
<style>
|
||||||
.main-nav-link {
|
.main-nav-link {
|
||||||
border-bottom: 1px solid var(--nav-a-border);
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon-and-label {
|
||||||
padding: 15px 20px;
|
padding: 15px 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -62,18 +75,38 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-nav-link.selected {
|
.main-nav-link.selected {
|
||||||
border-bottom: 1px solid var(--nav-a-selected-border);
|
|
||||||
background: var(--nav-a-selected-bg);
|
background: var(--nav-a-selected-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-nav-link.selected:hover {
|
.main-nav-link.selected:hover {
|
||||||
border-bottom: 1px solid var(--nav-a-selected-border-hover);
|
|
||||||
background: var(--nav-a-selected-bg-hover);
|
background: var(--nav-a-selected-bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-indicator {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--nav-a-border);
|
||||||
|
transform-origin: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-indicator.animate {
|
||||||
|
transition: transform 333ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-nav-link:hover .nav-indicator {
|
||||||
|
background: var(--nav-a-border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-nav-link.selected .nav-indicator {
|
||||||
|
background: var(--nav-a-selected-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-nav-link.selected:hover .nav-indicator {
|
||||||
|
background: var(--nav-a-selected-border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
.main-nav-link:hover {
|
.main-nav-link:hover {
|
||||||
background-color: var(--nav-a-bg-hover);
|
background-color: var(--nav-a-bg-hover);
|
||||||
border-bottom: 1px solid var(--nav-a-border-hover);
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,7 +139,7 @@
|
||||||
width: 25px;
|
width: 25px;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
}
|
}
|
||||||
.main-nav-link {
|
.nav-icon-and-label {
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
}
|
}
|
||||||
.nav-link-notifications {
|
.nav-link-notifications {
|
||||||
|
@ -118,8 +151,44 @@
|
||||||
<script>
|
<script>
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { smoothScrollToTop } from '../_utils/smoothScrollToTop'
|
import { smoothScrollToTop } from '../_utils/smoothScrollToTop'
|
||||||
|
import { on, emit } from '../_utils/eventBus'
|
||||||
|
import { mark, stop } from '../_utils/marks'
|
||||||
|
import { doubleRAF } from '../_utils/doubleRAF'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
oncreate () {
|
||||||
|
let { name } = this.get()
|
||||||
|
let indicator = this.refs.indicator
|
||||||
|
on('animateNavPart1', this, ({fromPage, toPage}) => {
|
||||||
|
if (fromPage !== name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mark('animateNavPart1 gBCR')
|
||||||
|
let fromRect = indicator.getBoundingClientRect()
|
||||||
|
stop('animateNavPart1 gBCR')
|
||||||
|
emit('animateNavPart2', {fromRect, fromPage, toPage})
|
||||||
|
})
|
||||||
|
on('animateNavPart2', this, ({fromPage, fromRect, toPage}) => {
|
||||||
|
if (toPage !== name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mark('animateNavPart2 gBCR')
|
||||||
|
let toRect = indicator.getBoundingClientRect()
|
||||||
|
stop('animateNavPart2 gBCR')
|
||||||
|
let translateX = fromRect.left - toRect.left
|
||||||
|
let scaleX = fromRect.width / toRect.width
|
||||||
|
indicator.style.transform = `translateX(${translateX}px) scaleX(${scaleX})`
|
||||||
|
let onTransitionEnd = () => {
|
||||||
|
indicator.removeEventListener('transitionend', onTransitionEnd)
|
||||||
|
indicator.classList.remove('animate')
|
||||||
|
}
|
||||||
|
indicator.addEventListener('transitionend', onTransitionEnd)
|
||||||
|
doubleRAF(() => {
|
||||||
|
indicator.classList.add('animate')
|
||||||
|
indicator.style.transform = ''
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
store: () => store,
|
store: () => store,
|
||||||
computed: {
|
computed: {
|
||||||
selected: ({ page, name }) => page === name,
|
selected: ({ page, name }) => page === name,
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { instanceComputations } from './instanceComputations'
|
import { instanceComputations } from './instanceComputations'
|
||||||
import { timelineComputations } from './timelineComputations'
|
import { timelineComputations } from './timelineComputations'
|
||||||
|
import { navComputations } from './navComputations'
|
||||||
|
|
||||||
export function computations (store) {
|
export function computations (store) {
|
||||||
instanceComputations(store)
|
instanceComputations(store)
|
||||||
timelineComputations(store)
|
timelineComputations(store)
|
||||||
|
navComputations(store)
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,17 +47,4 @@ export function instanceComputations (store) {
|
||||||
['currentInstanceData'],
|
['currentInstanceData'],
|
||||||
(currentInstanceData) => currentInstanceData && currentInstanceData.access_token
|
(currentInstanceData) => currentInstanceData && currentInstanceData.access_token
|
||||||
)
|
)
|
||||||
|
|
||||||
store.compute(
|
|
||||||
'pinnedListTitle',
|
|
||||||
['lists', 'pinnedPage'],
|
|
||||||
(lists, pinnedPage) => {
|
|
||||||
if (!pinnedPage.startsWith('/lists')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let listId = pinnedPage.split('/').slice(-1)[0]
|
|
||||||
let list = lists.find(_ => _.id === listId)
|
|
||||||
return list ? list.title : ''
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
85
routes/_store/computations/navComputations.js
Normal file
85
routes/_store/computations/navComputations.js
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
export function navComputations (store) {
|
||||||
|
store.compute(
|
||||||
|
'pinnedListTitle',
|
||||||
|
['lists', 'pinnedPage'],
|
||||||
|
(lists, pinnedPage) => {
|
||||||
|
if (!pinnedPage.startsWith('/lists')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let listId = pinnedPage.split('/').slice(-1)[0]
|
||||||
|
let list = lists.find(_ => _.id === listId)
|
||||||
|
return list ? list.title : ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
store.compute(
|
||||||
|
'navPages',
|
||||||
|
['pinnedPage', 'pinnedListTitle'],
|
||||||
|
(pinnedPage, pinnedListTitle) => {
|
||||||
|
let pinnedPageObject
|
||||||
|
if (pinnedPage === '/federated') {
|
||||||
|
pinnedPageObject = {
|
||||||
|
name: 'federated',
|
||||||
|
href: '/federated',
|
||||||
|
svg: '#fa-globe',
|
||||||
|
label: 'Federated'
|
||||||
|
}
|
||||||
|
} else if (pinnedPage === '/favorites') {
|
||||||
|
pinnedPageObject = {
|
||||||
|
name: 'favorites',
|
||||||
|
href: '/favorites',
|
||||||
|
svg: '#fa-star',
|
||||||
|
label: 'Favorites'
|
||||||
|
}
|
||||||
|
} else if (pinnedPage.startsWith('/lists/')) {
|
||||||
|
pinnedPageObject = {
|
||||||
|
name: 'lists',
|
||||||
|
href: pinnedPage,
|
||||||
|
svg: '#fa-bars',
|
||||||
|
label: pinnedListTitle
|
||||||
|
}
|
||||||
|
} else { // local
|
||||||
|
pinnedPageObject = {
|
||||||
|
name: 'local',
|
||||||
|
href: '/local',
|
||||||
|
svg: '#fa-users',
|
||||||
|
label: 'Local'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'home',
|
||||||
|
href: '/',
|
||||||
|
svg: '#pinafore-logo',
|
||||||
|
label: 'Home'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'notifications',
|
||||||
|
href: '/notifications',
|
||||||
|
svg: '#fa-bell',
|
||||||
|
label: 'Notifications'
|
||||||
|
},
|
||||||
|
pinnedPageObject,
|
||||||
|
{
|
||||||
|
name: 'community',
|
||||||
|
href: '/community',
|
||||||
|
svg: '#fa-comments',
|
||||||
|
label: 'Community'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'search',
|
||||||
|
href: '/search',
|
||||||
|
svg: '#fa-search',
|
||||||
|
label: 'Search'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'settings',
|
||||||
|
href: '/settings',
|
||||||
|
svg: '#fa-gear',
|
||||||
|
label: 'Settings'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
21
routes/_store/observers/navObservers.js
Normal file
21
routes/_store/observers/navObservers.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { emit } from '../../_utils/eventBus'
|
||||||
|
|
||||||
|
export function navObservers (store) {
|
||||||
|
function pageIsInNav (store, page) {
|
||||||
|
let { navPages } = store.get()
|
||||||
|
return !!navPages.find(_ => _.name === page)
|
||||||
|
}
|
||||||
|
|
||||||
|
store.observe('currentPage', (currentPage, previousPage) => {
|
||||||
|
if (currentPage && previousPage &&
|
||||||
|
pageIsInNav(store, currentPage) &&
|
||||||
|
pageIsInNav(store, previousPage)) {
|
||||||
|
emit('animateNavPart1', {
|
||||||
|
fromPage: previousPage,
|
||||||
|
toPage: currentPage
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
init: false
|
||||||
|
})
|
||||||
|
}
|
|
@ -2,10 +2,12 @@ import { instanceObservers } from './instanceObservers'
|
||||||
import { timelineObservers } from './timelineObservers'
|
import { timelineObservers } from './timelineObservers'
|
||||||
import { notificationObservers } from './notificationObservers'
|
import { notificationObservers } from './notificationObservers'
|
||||||
import { onlineObservers } from './onlineObservers'
|
import { onlineObservers } from './onlineObservers'
|
||||||
|
import { navObservers } from './navObservers'
|
||||||
|
|
||||||
export function observers (store) {
|
export function observers (store) {
|
||||||
instanceObservers(store)
|
instanceObservers(store)
|
||||||
timelineObservers(store)
|
timelineObservers(store)
|
||||||
notificationObservers(store)
|
notificationObservers(store)
|
||||||
onlineObservers(store)
|
onlineObservers(store)
|
||||||
|
navObservers(store)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue