mirror of
https://github.com/nkanaev/yarr.git
synced 2026-07-15 11:06:31 +00:00
frontend: extract vue components
This commit is contained in:
@@ -4,222 +4,33 @@ import api from './api'
|
|||||||
import template from './templates/index.html' with {type: 'text'}
|
import template from './templates/index.html' with {type: 'text'}
|
||||||
import icons from './icons'
|
import icons from './icons'
|
||||||
import { setupKeybindings } from './key'
|
import { setupKeybindings } from './key'
|
||||||
|
import { scrollto, debounce, dateRepr } from './utils'
|
||||||
|
import drag from './components/drag'
|
||||||
|
import dropdown from './components/dropdown'
|
||||||
|
import modal from './components/modal'
|
||||||
|
import relativeTime from './components/relative-time'
|
||||||
|
import icon from './components/icon'
|
||||||
|
import scrollDir from './directives/scroll'
|
||||||
|
import focusDir from './directives/focus'
|
||||||
|
|
||||||
var app = window.app
|
var app = window.app
|
||||||
var vm
|
var vm
|
||||||
|
|
||||||
var TITLE = document.title
|
var TITLE = document.title
|
||||||
|
|
||||||
function scrollto(target, scroll) {
|
|
||||||
var padding = 10
|
|
||||||
var targetRect = target.getBoundingClientRect()
|
|
||||||
var scrollRect = scroll.getBoundingClientRect()
|
|
||||||
|
|
||||||
// target
|
|
||||||
var relativeOffset = targetRect.y - scrollRect.y
|
|
||||||
var absoluteOffset = relativeOffset + scroll.scrollTop
|
|
||||||
|
|
||||||
if (padding <= relativeOffset && relativeOffset + targetRect.height <= scrollRect.height - padding) return
|
|
||||||
|
|
||||||
var newPos = scroll.scrollTop
|
|
||||||
if (relativeOffset < padding) {
|
|
||||||
newPos = absoluteOffset - padding
|
|
||||||
} else {
|
|
||||||
newPos = absoluteOffset - scrollRect.height + targetRect.height + padding
|
|
||||||
}
|
|
||||||
scroll.scrollTop = Math.round(newPos)
|
|
||||||
}
|
|
||||||
|
|
||||||
var debounce = function(callback, wait) {
|
|
||||||
var timeout
|
|
||||||
return function() {
|
|
||||||
var ctx = this, args = arguments
|
|
||||||
clearTimeout(timeout)
|
|
||||||
timeout = setTimeout(function() {
|
|
||||||
callback.apply(ctx, args)
|
|
||||||
}, wait)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.directive('scroll', {
|
|
||||||
inserted: function(el, binding) {
|
|
||||||
el.addEventListener('scroll', debounce(function(event) {
|
|
||||||
binding.value(event, el)
|
|
||||||
}, 200))
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
Vue.directive('focus', {
|
|
||||||
inserted: function(el) {
|
|
||||||
el.focus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Vue.component('drag', {
|
|
||||||
props: ['width'],
|
|
||||||
template: '<div class="drag"></div>',
|
|
||||||
mounted: function() {
|
|
||||||
var self = this
|
|
||||||
var startX = undefined
|
|
||||||
var initW = undefined
|
|
||||||
var onMouseMove = function(e) {
|
|
||||||
var offset = e.clientX - startX
|
|
||||||
var newWidth = initW + offset
|
|
||||||
self.$emit('resize', newWidth)
|
|
||||||
}
|
|
||||||
var onMouseUp = function(e) {
|
|
||||||
document.removeEventListener('mousemove', onMouseMove)
|
|
||||||
document.removeEventListener('mouseup', onMouseUp)
|
|
||||||
}
|
|
||||||
this.$el.addEventListener('mousedown', function(e) {
|
|
||||||
startX = e.clientX
|
|
||||||
initW = self.width
|
|
||||||
document.addEventListener('mousemove', onMouseMove)
|
|
||||||
document.addEventListener('mouseup', onMouseUp)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
Vue.component('dropdown', {
|
|
||||||
props: ['toggle-class', 'drop', 'title'],
|
|
||||||
data: function() {
|
|
||||||
return {open: false}
|
|
||||||
},
|
|
||||||
template: `
|
|
||||||
<div class="dropdown" :class="$attrs.class">
|
|
||||||
<button ref="btn" @click="toggle" :class="btnToggleClass" :title="$props.title"><slot name="button"></slot></button>
|
|
||||||
<div ref="menu" class="dropdown-menu" :class="{show: open}"><slot v-if="open"></slot></div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
computed: {
|
|
||||||
btnToggleClass: function() {
|
|
||||||
var c = this.$props.toggleClass || ''
|
|
||||||
c += ' dropdown-toggle dropdown-toggle-no-caret'
|
|
||||||
c += this.open ? ' show' : ''
|
|
||||||
return c.trim()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
toggle: function(e) {
|
|
||||||
this.open ? this.hide() : this.show()
|
|
||||||
},
|
|
||||||
show: function(e) {
|
|
||||||
this.open = true
|
|
||||||
this.$refs.menu.style.top = this.$refs.btn.offsetHeight + 'px'
|
|
||||||
var drop = this.$props.drop
|
|
||||||
|
|
||||||
if (drop === 'right') {
|
|
||||||
this.$refs.menu.style.left = 'auto'
|
|
||||||
this.$refs.menu.style.right = '0'
|
|
||||||
} else if (drop === 'center') {
|
|
||||||
this.$nextTick(function() {
|
|
||||||
var btnWidth = this.$refs.btn.getBoundingClientRect().width
|
|
||||||
var menuWidth = this.$refs.menu.getBoundingClientRect().width
|
|
||||||
this.$refs.menu.style.left = '-' + ((menuWidth - btnWidth) / 2) + 'px'
|
|
||||||
}.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('click', this.clickHandler)
|
|
||||||
},
|
|
||||||
hide: function() {
|
|
||||||
this.open = false
|
|
||||||
document.removeEventListener('click', this.clickHandler)
|
|
||||||
},
|
|
||||||
clickHandler: function(e) {
|
|
||||||
var dropdown = e.target.closest('.dropdown')
|
|
||||||
if (dropdown == null || dropdown != this.$el) return this.hide()
|
|
||||||
if (e.target.closest('.dropdown-item') != null) return this.hide()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
Vue.component('modal', {
|
|
||||||
props: ['open'],
|
|
||||||
template: `
|
|
||||||
<div class="modal custom-modal" tabindex="-1" v-if="$props.open">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content" ref="content">
|
|
||||||
<div class="modal-body">
|
|
||||||
<slot v-if="$props.open"></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
data: function() {
|
|
||||||
return {opening: false}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
'open': function(newVal) {
|
|
||||||
if (newVal) {
|
|
||||||
this.opening = true
|
|
||||||
document.addEventListener('click', this.handleClick)
|
|
||||||
} else {
|
|
||||||
document.removeEventListener('click', this.handleClick)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleClick: function(e) {
|
|
||||||
if (this.opening) {
|
|
||||||
this.opening = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (e.target.closest('.modal-content') == null) this.$emit('hide')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
function dateRepr(d) {
|
|
||||||
var sec = (new Date().getTime() - d.getTime()) / 1000
|
|
||||||
var neg = sec < 0
|
|
||||||
var out = ''
|
|
||||||
|
|
||||||
sec = Math.abs(sec)
|
|
||||||
if (sec < 2700) // less than 45 minutes
|
|
||||||
out = Math.round(sec / 60) + 'm'
|
|
||||||
else if (sec < 86400) // less than 24 hours
|
|
||||||
out = Math.round(sec / 3600) + 'h'
|
|
||||||
else if (sec < 604800) // less than a week
|
|
||||||
out = Math.round(sec / 86400) + 'd'
|
|
||||||
else
|
|
||||||
out = d.toLocaleDateString(undefined, {year: "numeric", month: "long", day: "numeric"})
|
|
||||||
|
|
||||||
if (neg) return '-' + out
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.component('relative-time', {
|
|
||||||
props: ['val'],
|
|
||||||
data: function() {
|
|
||||||
var d = new Date(this.val)
|
|
||||||
return {
|
|
||||||
'date': d,
|
|
||||||
'formatted': dateRepr(d),
|
|
||||||
'interval': null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
template: '<time :datetime="val">{{ formatted }}</time>',
|
|
||||||
mounted: function() {
|
|
||||||
this.interval = setInterval(function() {
|
|
||||||
this.formatted = dateRepr(this.date)
|
|
||||||
}.bind(this), 600000) // every 10 minutes
|
|
||||||
},
|
|
||||||
destroyed: function() {
|
|
||||||
clearInterval(this.interval)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
Vue.component('v-icon', {
|
|
||||||
props: ['name'],
|
|
||||||
template: '<span class="icon" v-html="content"></span>',
|
|
||||||
computed: {
|
|
||||||
content: function () { return icons[this.name] || '' }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
template: template,
|
template: template,
|
||||||
|
components: {
|
||||||
|
'v-drag': drag,
|
||||||
|
'v-dropdown': dropdown,
|
||||||
|
'v-modal': modal,
|
||||||
|
'v-relative-time': relativeTime,
|
||||||
|
'v-icon': icon,
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
scroll: scrollDir,
|
||||||
|
focus: focusDir,
|
||||||
|
},
|
||||||
created: function() {
|
created: function() {
|
||||||
vm = this
|
vm = this
|
||||||
this.refreshStats()
|
this.refreshStats()
|
||||||
|
|||||||
24
src/frontend/js/components/drag.ts
Normal file
24
src/frontend/js/components/drag.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export default {
|
||||||
|
props: ['width'],
|
||||||
|
template: '<div class="drag"></div>',
|
||||||
|
mounted: function() {
|
||||||
|
var self = this
|
||||||
|
var startX = undefined
|
||||||
|
var initW = undefined
|
||||||
|
var onMouseMove = function(e) {
|
||||||
|
var offset = e.clientX - startX
|
||||||
|
var newWidth = initW + offset
|
||||||
|
self.$emit('resize', newWidth)
|
||||||
|
}
|
||||||
|
var onMouseUp = function(e) {
|
||||||
|
document.removeEventListener('mousemove', onMouseMove)
|
||||||
|
document.removeEventListener('mouseup', onMouseUp)
|
||||||
|
}
|
||||||
|
this.$el.addEventListener('mousedown', function(e) {
|
||||||
|
startX = e.clientX
|
||||||
|
initW = self.width
|
||||||
|
document.addEventListener('mousemove', onMouseMove)
|
||||||
|
document.addEventListener('mouseup', onMouseUp)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
52
src/frontend/js/components/dropdown.ts
Normal file
52
src/frontend/js/components/dropdown.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
export default {
|
||||||
|
props: ['toggle-class', 'drop', 'title'],
|
||||||
|
data: function() {
|
||||||
|
return {open: false}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div class="dropdown" :class="$attrs.class">
|
||||||
|
<button ref="btn" @click="toggle" :class="btnToggleClass" :title="$props.title"><slot name="button"></slot></button>
|
||||||
|
<div ref="menu" class="dropdown-menu" :class="{show: open}"><slot v-if="open"></slot></div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
computed: {
|
||||||
|
btnToggleClass: function() {
|
||||||
|
var c = this.$props.toggleClass || ''
|
||||||
|
c += ' dropdown-toggle dropdown-toggle-no-caret'
|
||||||
|
c += this.open ? ' show' : ''
|
||||||
|
return c.trim()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggle: function(e) {
|
||||||
|
this.open ? this.hide() : this.show()
|
||||||
|
},
|
||||||
|
show: function(e) {
|
||||||
|
this.open = true
|
||||||
|
this.$refs.menu.style.top = this.$refs.btn.offsetHeight + 'px'
|
||||||
|
var drop = this.$props.drop
|
||||||
|
|
||||||
|
if (drop === 'right') {
|
||||||
|
this.$refs.menu.style.left = 'auto'
|
||||||
|
this.$refs.menu.style.right = '0'
|
||||||
|
} else if (drop === 'center') {
|
||||||
|
this.$nextTick(function() {
|
||||||
|
var btnWidth = this.$refs.btn.getBoundingClientRect().width
|
||||||
|
var menuWidth = this.$refs.menu.getBoundingClientRect().width
|
||||||
|
this.$refs.menu.style.left = '-' + ((menuWidth - btnWidth) / 2) + 'px'
|
||||||
|
}.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', this.clickHandler)
|
||||||
|
},
|
||||||
|
hide: function() {
|
||||||
|
this.open = false
|
||||||
|
document.removeEventListener('click', this.clickHandler)
|
||||||
|
},
|
||||||
|
clickHandler: function(e) {
|
||||||
|
var dropdown = e.target.closest('.dropdown')
|
||||||
|
if (dropdown == null || dropdown != this.$el) return this.hide()
|
||||||
|
if (e.target.closest('.dropdown-item') != null) return this.hide()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
9
src/frontend/js/components/icon.ts
Normal file
9
src/frontend/js/components/icon.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import icons from '../icons'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['name'],
|
||||||
|
template: '<span class="icon" v-html="content"></span>',
|
||||||
|
computed: {
|
||||||
|
content: function () { return icons[this.name] || '' }
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/frontend/js/components/modal.ts
Normal file
36
src/frontend/js/components/modal.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
export default {
|
||||||
|
props: ['open'],
|
||||||
|
template: `
|
||||||
|
<div class="modal custom-modal" tabindex="-1" v-if="$props.open">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content" ref="content">
|
||||||
|
<div class="modal-body">
|
||||||
|
<slot v-if="$props.open"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
data: function() {
|
||||||
|
return {opening: false}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'open': function(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.opening = true
|
||||||
|
document.addEventListener('click', this.handleClick)
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('click', this.handleClick)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClick: function(e) {
|
||||||
|
if (this.opening) {
|
||||||
|
this.opening = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.target.closest('.modal-content') == null) this.$emit('hide')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
22
src/frontend/js/components/relative-time.ts
Normal file
22
src/frontend/js/components/relative-time.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { dateRepr } from '../utils'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['val'],
|
||||||
|
data: function() {
|
||||||
|
var d = new Date(this.val)
|
||||||
|
return {
|
||||||
|
'date': d,
|
||||||
|
'formatted': dateRepr(d),
|
||||||
|
'interval': null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: '<time :datetime="val">{{ formatted }}</time>',
|
||||||
|
mounted: function() {
|
||||||
|
this.interval = setInterval(function() {
|
||||||
|
this.formatted = dateRepr(this.date)
|
||||||
|
}.bind(this), 600000) // every 10 minutes
|
||||||
|
},
|
||||||
|
destroyed: function() {
|
||||||
|
clearInterval(this.interval)
|
||||||
|
},
|
||||||
|
}
|
||||||
5
src/frontend/js/directives/focus.ts
Normal file
5
src/frontend/js/directives/focus.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
inserted: function(el) {
|
||||||
|
el.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/frontend/js/directives/scroll.ts
Normal file
9
src/frontend/js/directives/scroll.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { debounce } from '../utils'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
inserted: function(el, binding) {
|
||||||
|
el.addEventListener('scroll', debounce(function(event) {
|
||||||
|
binding.value(event, el)
|
||||||
|
}, 200))
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="d-flex" :class="{'feed-selected': feedSelected !== null, 'item-selected': itemSelected !== null}">
|
<div class="d-flex" :class="{'feed-selected': feedSelected !== null, 'item-selected': itemSelected !== null}">
|
||||||
<!-- feed list -->
|
<!-- feed list -->
|
||||||
<div id="col-feed-list" class="vh-100 position-relative d-flex flex-column border-right flex-shrink-0" :style="{width: feedListWidth+'px'}">
|
<div id="col-feed-list" class="vh-100 position-relative d-flex flex-column border-right flex-shrink-0" :style="{width: feedListWidth+'px'}">
|
||||||
<drag :width="feedListWidth" @resize="resizeFeedList"></drag>
|
<v-drag :width="feedListWidth" @resize="resizeFeedList"></v-drag>
|
||||||
<div class="p-2 toolbar d-flex align-items-center">
|
<div class="p-2 toolbar d-flex align-items-center">
|
||||||
<v-icon class="mx-2" name="anchor" />
|
<v-icon class="mx-2" name="anchor" />
|
||||||
<div class="flex-grow-1"></div>
|
<div class="flex-grow-1"></div>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<v-icon name="assorted" />
|
<v-icon name="assorted" />
|
||||||
</button>
|
</button>
|
||||||
<div class="flex-grow-1"></div>
|
<div class="flex-grow-1"></div>
|
||||||
<dropdown class="settings-dropdown" toggle-class="btn btn-link toolbar-item px-2" ref="menuDropdown" drop="right" :title="$t('settings')">
|
<v-dropdown class="settings-dropdown" toggle-class="btn btn-link toolbar-item px-2" ref="menuDropdown" drop="right" :title="$t('settings')">
|
||||||
<template v-slot:button>
|
<template v-slot:button>
|
||||||
<v-icon name="more-horizontal" />
|
<v-icon name="more-horizontal" />
|
||||||
</template>
|
</template>
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
<v-icon class="mr-1" name="log-out" />
|
<v-icon class="mr-1" name="log-out" />
|
||||||
{{ $t('log_out') }}
|
{{ $t('log_out') }}
|
||||||
</button>
|
</button>
|
||||||
</dropdown>
|
</v-dropdown>
|
||||||
</div>
|
</div>
|
||||||
<div id="feed-list-scroll" class="p-2 overflow-auto scroll-touch border-top flex-grow-1">
|
<div id="feed-list-scroll" class="p-2 overflow-auto scroll-touch border-top flex-grow-1">
|
||||||
<label class="selectgroup">
|
<label class="selectgroup">
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- item list -->
|
<!-- item list -->
|
||||||
<div id="col-item-list" class="vh-100 position-relative d-flex flex-column border-right flex-shrink-0" :style="{width: itemListWidth+'px'}">
|
<div id="col-item-list" class="vh-100 position-relative d-flex flex-column border-right flex-shrink-0" :style="{width: itemListWidth+'px'}">
|
||||||
<drag :width="itemListWidth" @resize="resizeItemList"></drag>
|
<v-drag :width="itemListWidth" @resize="resizeItemList"></v-drag>
|
||||||
<div class="px-2 toolbar d-flex align-items-center">
|
<div class="px-2 toolbar d-flex align-items-center">
|
||||||
<button class="toolbar-item mr-2 d-block d-md-none"
|
<button class="toolbar-item mr-2 d-block d-md-none"
|
||||||
@click="feedSelected = null"
|
@click="feedSelected = null"
|
||||||
@@ -197,7 +197,7 @@
|
|||||||
<button class="btn btn-link toolbar-item px-2 ml-2" v-if="!current.type" disabled>
|
<button class="btn btn-link toolbar-item px-2 ml-2" v-if="!current.type" disabled>
|
||||||
<v-icon name="more-horizontal" />
|
<v-icon name="more-horizontal" />
|
||||||
</button>
|
</button>
|
||||||
<dropdown class="settings-dropdown"
|
<v-dropdown class="settings-dropdown"
|
||||||
toggle-class="btn btn-link toolbar-item px-2 ml-2"
|
toggle-class="btn btn-link toolbar-item px-2 ml-2"
|
||||||
drop="right"
|
drop="right"
|
||||||
:title="$t('feed_settings')"
|
:title="$t('feed_settings')"
|
||||||
@@ -245,8 +245,8 @@
|
|||||||
<v-icon class="mr-1" name="trash" />
|
<v-icon class="mr-1" name="trash" />
|
||||||
{{ $t('delete') }}
|
{{ $t('delete') }}
|
||||||
</button>
|
</button>
|
||||||
</dropdown>
|
</v-dropdown>
|
||||||
<dropdown class="settings-dropdown"
|
<v-dropdown class="settings-dropdown"
|
||||||
toggle-class="btn btn-link toolbar-item px-2 ml-2"
|
toggle-class="btn btn-link toolbar-item px-2 ml-2"
|
||||||
:title="$t('folder_settings')"
|
:title="$t('folder_settings')"
|
||||||
drop="right"
|
drop="right"
|
||||||
@@ -264,7 +264,7 @@
|
|||||||
<v-icon class="mr-1" name="trash" />
|
<v-icon class="mr-1" name="trash" />
|
||||||
{{ $t('delete') }}
|
{{ $t('delete') }}
|
||||||
</button>
|
</button>
|
||||||
</dropdown>
|
</v-dropdown>
|
||||||
</div>
|
</div>
|
||||||
<div id="item-list-scroll" class="p-2 overflow-auto scroll-touch border-top flex-grow-1" v-scroll="loadMoreItems" ref="itemlist">
|
<div id="item-list-scroll" class="p-2 overflow-auto scroll-touch border-top flex-grow-1" v-scroll="loadMoreItems" ref="itemlist">
|
||||||
<label v-for="item in items" :key="item.id"
|
<label v-for="item in items" :key="item.id"
|
||||||
@@ -279,7 +279,7 @@
|
|||||||
<small class="flex-fill text-truncate mr-1">
|
<small class="flex-fill text-truncate mr-1">
|
||||||
{{ (feedsById[item.feed_id] || {}).title }}
|
{{ (feedsById[item.feed_id] || {}).title }}
|
||||||
</small>
|
</small>
|
||||||
<small class="flex-shrink-0"><relative-time v-bind:title="formatDate(item.date)" :val="item.date"/></small>
|
<small class="flex-shrink-0"><v-relative-time v-bind:title="formatDate(item.date)" :val="item.date"/></small>
|
||||||
</div>
|
</div>
|
||||||
<div>{{ item.title || $t('untitled') }}</div>
|
<div>{{ item.title || $t('untitled') }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -305,7 +305,7 @@
|
|||||||
<v-icon name="circle-full" v-if="itemSelectedDetails.status=='unread'" />
|
<v-icon name="circle-full" v-if="itemSelectedDetails.status=='unread'" />
|
||||||
<v-icon name="circle" v-if="itemSelectedDetails.status!='unread'" />
|
<v-icon name="circle" v-if="itemSelectedDetails.status!='unread'" />
|
||||||
</button>
|
</button>
|
||||||
<dropdown class="settings-dropdown" toggle-class="toolbar-item px-2" drop="center" :title="$t('appearance')">
|
<v-dropdown class="settings-dropdown" toggle-class="toolbar-item px-2" drop="center" :title="$t('appearance')">
|
||||||
<template v-slot:button>
|
<template v-slot:button>
|
||||||
<v-icon name="sliders" />
|
<v-icon name="sliders" />
|
||||||
</template>
|
</template>
|
||||||
@@ -318,7 +318,7 @@
|
|||||||
<button class="dropdown-item" style="font-size: 0.8rem" @click.stop="incrFont(-1)">A</button>
|
<button class="dropdown-item" style="font-size: 0.8rem" @click.stop="incrFont(-1)">A</button>
|
||||||
<button class="dropdown-item" style="font-size: 1.2rem" @click.stop="incrFont(1)">A</button>
|
<button class="dropdown-item" style="font-size: 1.2rem" @click.stop="incrFont(1)">A</button>
|
||||||
</div>
|
</div>
|
||||||
</dropdown>
|
</v-dropdown>
|
||||||
<button class="toolbar-item"
|
<button class="toolbar-item"
|
||||||
:class="{active: itemSelectedReadability}"
|
:class="{active: itemSelectedReadability}"
|
||||||
@click="toggleReadability()"
|
@click="toggleReadability()"
|
||||||
@@ -369,7 +369,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<modal :open="!!settings" @hide="settings = ''">
|
<v-modal :open="!!settings" @hide="settings = ''">
|
||||||
<button class="btn btn-link outline-none float-right p-2 mr-n2 mt-n2" style="line-height: 1" @click="settings = ''">
|
<button class="btn btn-link outline-none float-right p-2 mr-n2 mt-n2" style="line-height: 1" @click="settings = ''">
|
||||||
<v-icon name="x" />
|
<v-icon name="x" />
|
||||||
</button>
|
</button>
|
||||||
@@ -425,5 +425,5 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</v-modal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
49
src/frontend/js/utils.ts
Normal file
49
src/frontend/js/utils.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
export function scrollto(target, scroll) {
|
||||||
|
var padding = 10
|
||||||
|
var targetRect = target.getBoundingClientRect()
|
||||||
|
var scrollRect = scroll.getBoundingClientRect()
|
||||||
|
|
||||||
|
// target
|
||||||
|
var relativeOffset = targetRect.y - scrollRect.y
|
||||||
|
var absoluteOffset = relativeOffset + scroll.scrollTop
|
||||||
|
|
||||||
|
if (padding <= relativeOffset && relativeOffset + targetRect.height <= scrollRect.height - padding) return
|
||||||
|
|
||||||
|
var newPos = scroll.scrollTop
|
||||||
|
if (relativeOffset < padding) {
|
||||||
|
newPos = absoluteOffset - padding
|
||||||
|
} else {
|
||||||
|
newPos = absoluteOffset - scrollRect.height + targetRect.height + padding
|
||||||
|
}
|
||||||
|
scroll.scrollTop = Math.round(newPos)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debounce(callback, wait) {
|
||||||
|
var timeout
|
||||||
|
return function() {
|
||||||
|
var ctx = this, args = arguments
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = setTimeout(function() {
|
||||||
|
callback.apply(ctx, args)
|
||||||
|
}, wait)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dateRepr(d) {
|
||||||
|
var sec = (new Date().getTime() - d.getTime()) / 1000
|
||||||
|
var neg = sec < 0
|
||||||
|
var out = ''
|
||||||
|
|
||||||
|
sec = Math.abs(sec)
|
||||||
|
if (sec < 2700) // less than 45 minutes
|
||||||
|
out = Math.round(sec / 60) + 'm'
|
||||||
|
else if (sec < 86400) // less than 24 hours
|
||||||
|
out = Math.round(sec / 3600) + 'h'
|
||||||
|
else if (sec < 604800) // less than a week
|
||||||
|
out = Math.round(sec / 86400) + 'd'
|
||||||
|
else
|
||||||
|
out = d.toLocaleDateString(undefined, {year: "numeric", month: "long", day: "numeric"})
|
||||||
|
|
||||||
|
if (neg) return '-' + out
|
||||||
|
return out
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user