diff --git a/src/frontend/js/app.ts b/src/frontend/js/app.ts
index dbc77a4..f154d61 100644
--- a/src/frontend/js/app.ts
+++ b/src/frontend/js/app.ts
@@ -4,222 +4,33 @@ import api from './api'
import template from './templates/index.html' with {type: 'text'}
import icons from './icons'
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 vm
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: '
',
- 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: `
-
-
-
-
- `,
- 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: `
-
- `,
- 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: '',
- 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: '',
- computed: {
- content: function () { return icons[this.name] || '' }
- }
-})
-
export default {
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() {
vm = this
this.refreshStats()
diff --git a/src/frontend/js/components/drag.ts b/src/frontend/js/components/drag.ts
new file mode 100644
index 0000000..2da6095
--- /dev/null
+++ b/src/frontend/js/components/drag.ts
@@ -0,0 +1,24 @@
+export default {
+ props: ['width'],
+ template: '',
+ 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)
+ })
+ },
+}
diff --git a/src/frontend/js/components/dropdown.ts b/src/frontend/js/components/dropdown.ts
new file mode 100644
index 0000000..c20a5e3
--- /dev/null
+++ b/src/frontend/js/components/dropdown.ts
@@ -0,0 +1,52 @@
+export default {
+ props: ['toggle-class', 'drop', 'title'],
+ data: function() {
+ return {open: false}
+ },
+ template: `
+
+
+
+
+ `,
+ 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()
+ }
+ },
+}
diff --git a/src/frontend/js/components/icon.ts b/src/frontend/js/components/icon.ts
new file mode 100644
index 0000000..3dd86c3
--- /dev/null
+++ b/src/frontend/js/components/icon.ts
@@ -0,0 +1,9 @@
+import icons from '../icons'
+
+export default {
+ props: ['name'],
+ template: '',
+ computed: {
+ content: function () { return icons[this.name] || '' }
+ }
+}
diff --git a/src/frontend/js/components/modal.ts b/src/frontend/js/components/modal.ts
new file mode 100644
index 0000000..e5ec6d1
--- /dev/null
+++ b/src/frontend/js/components/modal.ts
@@ -0,0 +1,36 @@
+export default {
+ props: ['open'],
+ template: `
+
+ `,
+ 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')
+ },
+ },
+}
diff --git a/src/frontend/js/components/relative-time.ts b/src/frontend/js/components/relative-time.ts
new file mode 100644
index 0000000..176e1ec
--- /dev/null
+++ b/src/frontend/js/components/relative-time.ts
@@ -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: '',
+ mounted: function() {
+ this.interval = setInterval(function() {
+ this.formatted = dateRepr(this.date)
+ }.bind(this), 600000) // every 10 minutes
+ },
+ destroyed: function() {
+ clearInterval(this.interval)
+ },
+}
diff --git a/src/frontend/js/directives/focus.ts b/src/frontend/js/directives/focus.ts
new file mode 100644
index 0000000..2208314
--- /dev/null
+++ b/src/frontend/js/directives/focus.ts
@@ -0,0 +1,5 @@
+export default {
+ inserted: function(el) {
+ el.focus()
+ }
+}
diff --git a/src/frontend/js/directives/scroll.ts b/src/frontend/js/directives/scroll.ts
new file mode 100644
index 0000000..963fe66
--- /dev/null
+++ b/src/frontend/js/directives/scroll.ts
@@ -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))
+ },
+}
diff --git a/src/frontend/js/templates/index.html b/src/frontend/js/templates/index.html
index a70b358..e63d2c5 100644
--- a/src/frontend/js/templates/index.html
+++ b/src/frontend/js/templates/index.html
@@ -1,7 +1,7 @@
-
+
-
+
-
{{ $t('delete') }}
-
-
+
{{ $t('delete') }}
-
+
{{ item.title || $t('untitled') }}
@@ -305,7 +305,7 @@
-
+
@@ -318,7 +318,7 @@
-
+
-
+
@@ -425,5 +425,5 @@
-
+
diff --git a/src/frontend/js/utils.ts b/src/frontend/js/utils.ts
new file mode 100644
index 0000000..835ccf9
--- /dev/null
+++ b/src/frontend/js/utils.ts
@@ -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
+}