Frontend: Add experimental "pull to refresh" component
This commit is contained in:
parent
6c6e20ec2a
commit
55ec4e5053
4 changed files with 257 additions and 13 deletions
|
@ -9,6 +9,7 @@ import PPhotoViewer from "./p-photo-viewer.vue";
|
|||
import PPhotoSearch from "./p-photo-search.vue";
|
||||
import PPhotoClipboard from "./p-photo-clipboard.vue";
|
||||
import PScrollTop from "./p-scroll-top.vue";
|
||||
import PPullRefresh from "./p-pull-refresh.vue";
|
||||
|
||||
const components = {};
|
||||
|
||||
|
@ -24,6 +25,7 @@ components.install = (Vue) => {
|
|||
Vue.component("p-photo-search", PPhotoSearch);
|
||||
Vue.component("p-photo-clipboard", PPhotoClipboard);
|
||||
Vue.component("p-scroll-top", PScrollTop);
|
||||
Vue.component("p-pull-refresh", PPullRefresh);
|
||||
};
|
||||
|
||||
export default components;
|
||||
|
|
207
frontend/src/component/p-pull-refresh.vue
Normal file
207
frontend/src/component/p-pull-refresh.vue
Normal file
|
@ -0,0 +1,207 @@
|
|||
<template>
|
||||
<div class="p-pull-refresh">
|
||||
<div class="pull-down-header" v-bind:style="{'height': pullDown.height + 'px'}">
|
||||
<div class="pull-down-content" :style="pullDownContentStyle">
|
||||
<i class="pull-down-icon material-icons">{{iconClass}}</i>
|
||||
</div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const STATUS_ERROR = -1;
|
||||
const STATUS_START = 0;
|
||||
const STATUS_READY = 1;
|
||||
const STATUS_REFRESH = 2;
|
||||
const ANIMATION = 'height .2s ease';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
onRefresh: {
|
||||
type: Function
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
config: {
|
||||
errorLabel: "Failed to refresh",
|
||||
startLabel: "Pull to refresh",
|
||||
readyLabel: "Release to refresh",
|
||||
loadingLabel: "Updating...",
|
||||
pullDownHeight: 60,
|
||||
},
|
||||
pullDown: {
|
||||
status: 0,
|
||||
height: 0,
|
||||
msg: ''
|
||||
},
|
||||
canPull: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
label() {
|
||||
// label of pull down
|
||||
if (this.pullDown.status === STATUS_ERROR) {
|
||||
return this.pullDown.msg;
|
||||
}
|
||||
return this.customLabels[this.pullDown.status + 1];
|
||||
},
|
||||
customLabels() {
|
||||
return [this.config.errorLabel, this.config.startLabel, this.config.readyLabel, this.config.loadingLabel];
|
||||
},
|
||||
iconClass() {
|
||||
// icon of pull down
|
||||
if (this.pullDown.status === STATUS_REFRESH) {
|
||||
return 'refresh';
|
||||
} else if (this.pullDown.status === STATUS_ERROR) {
|
||||
return 'error';
|
||||
}
|
||||
return 'refresh';
|
||||
},
|
||||
pullDownContentStyle() {
|
||||
return {
|
||||
bottom: (this.config.pullDownHeight - 40) / 2 + 'px'
|
||||
};
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
let el = this.$el;
|
||||
let pullDownHeader = el.querySelector('.pull-down-header');
|
||||
let icon = pullDownHeader.querySelector('.pull-down-icon');
|
||||
// set default pullDownHeight
|
||||
this.config.pullDownHeight = this.config.pullDownHeight || 60;
|
||||
/**
|
||||
* reset the status of pull down
|
||||
* @param {Object} pullDown the pull down
|
||||
* @param {Boolean} withAnimation whether add animation when pull up
|
||||
*/
|
||||
let resetPullDown = (pullDown, withAnimation) => {
|
||||
if (withAnimation) {
|
||||
pullDownHeader.style.transition = ANIMATION;
|
||||
}
|
||||
pullDown.height = 0;
|
||||
pullDown.status = STATUS_START;
|
||||
};
|
||||
// store of touch position, include start position and distance
|
||||
let touchPosition = {
|
||||
start: 0,
|
||||
distance: 0
|
||||
};
|
||||
|
||||
// @see https://www.chromestatus.com/feature/5745543795965952
|
||||
// Test via a getter in the options object to see if the passive property is accessed
|
||||
let supportsPassive = false;
|
||||
try {
|
||||
let opts = Object.defineProperty({}, 'passive', {
|
||||
get: function() {
|
||||
supportsPassive = true;
|
||||
}
|
||||
});
|
||||
/* global window */
|
||||
window.addEventListener("test", null, opts);
|
||||
} catch (e) {}
|
||||
|
||||
// bind touchstart event to store start position of touch
|
||||
el.addEventListener('touchstart', e => {
|
||||
if (el.scrollTop === 0) {
|
||||
this.canPull = true;
|
||||
} else {
|
||||
this.canPull = false;
|
||||
}
|
||||
touchPosition.start = e.touches.item(0).pageY;
|
||||
}, supportsPassive ? {passive: true} : false);
|
||||
|
||||
/**
|
||||
* bind touchmove event, do the following:
|
||||
* first, update the height of pull down
|
||||
* finally, update the status of pull down based on the distance
|
||||
*/
|
||||
el.addEventListener('touchmove', e => {
|
||||
if (!this.canPull) {
|
||||
return;
|
||||
}
|
||||
|
||||
let distance = e.touches.item(0).pageY - touchPosition.start;
|
||||
// limit the height of pull down to 180
|
||||
distance = distance > 180 ? 180 : distance;
|
||||
// prevent native scroll
|
||||
if (distance > 0) {
|
||||
el.style.overflow = 'hidden';
|
||||
}
|
||||
// update touchPosition and the height of pull down
|
||||
touchPosition.distance = distance;
|
||||
this.pullDown.height = distance;
|
||||
/**
|
||||
* if distance is bigger than the height of pull down
|
||||
* set the status of pull down to STATUS_READY
|
||||
*/
|
||||
if (distance > this.config.pullDownHeight) {
|
||||
this.pullDown.status = STATUS_READY;
|
||||
icon.style.transform = 'rotate(180deg)';
|
||||
} else {
|
||||
/**
|
||||
* else set the status of pull down to STATUS_START
|
||||
* and rotate the icon based on distance
|
||||
*/
|
||||
this.pullDown.status = STATUS_START;
|
||||
icon.style.transform = 'rotate(' + distance / this.config.pullDownHeight * 180 + 'deg)';
|
||||
}
|
||||
}, supportsPassive ? {passive: true} : false);
|
||||
|
||||
// bind touchend event
|
||||
el.addEventListener('touchend', e => {
|
||||
this.canPull = false;
|
||||
el.style.overflowY = 'auto';
|
||||
pullDownHeader.style.transition = ANIMATION;
|
||||
// reset icon rotate
|
||||
icon.style.transform = '';
|
||||
// if distance is bigger than 60
|
||||
if (touchPosition.distance - el.scrollTop > this.config.pullDownHeight) {
|
||||
el.scrollTop = 0;
|
||||
this.pullDown.height = this.config.pullDownHeight;
|
||||
this.pullDown.status = STATUS_REFRESH;
|
||||
// trigger refresh callback
|
||||
if (this.onRefresh && typeof this.onRefresh === 'function') {
|
||||
let res = this.onRefresh();
|
||||
// if onRefresh return promise
|
||||
if (res && res.then && typeof res.then === 'function') {
|
||||
res.then(result => {
|
||||
resetPullDown(this.pullDown, true);
|
||||
}, error => {
|
||||
// show error and hide the pull down after 1 second
|
||||
if (typeof error !== 'string') {
|
||||
error = false;
|
||||
}
|
||||
this.pullDown.msg = error || this.customLabels[0];
|
||||
this.pullDown.status = STATUS_ERROR;
|
||||
setTimeout(() => {
|
||||
resetPullDown(this.pullDown, true);
|
||||
}, 1000);
|
||||
});
|
||||
} else {
|
||||
resetPullDown(this.pullDown);
|
||||
}
|
||||
} else {
|
||||
resetPullDown(this.pullDown);
|
||||
console.warn('please use :on-refresh to pass onRefresh callback');
|
||||
}
|
||||
} else {
|
||||
resetPullDown(this.pullDown);
|
||||
}
|
||||
// reset touchPosition
|
||||
touchPosition.distance = 0;
|
||||
touchPosition.start = 0;
|
||||
});
|
||||
// remove transition when transitionend
|
||||
pullDownHeader.addEventListener('transitionend', () => {
|
||||
pullDownHeader.style.transition = '';
|
||||
});
|
||||
pullDownHeader.addEventListener('webkitTransitionEnd', () => {
|
||||
pullDownHeader.style.transition = '';
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -66,3 +66,36 @@ main {
|
|||
#photoprism .p-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.p-pull-refresh {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.p-pull-refresh .pull-down-header {
|
||||
width: 100%;
|
||||
height: 0px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background-color: #757575;
|
||||
}
|
||||
|
||||
.p-pull-refresh .pull-down-content {
|
||||
position: absolute;
|
||||
max-width: 90%;
|
||||
bottom: 10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
height: 40px;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
border-left: 20px solid transparent;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.p-pull-refresh .pull-down-icon {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<div class="p-page p-page-photos" v-infinite-scroll="loadMore" :infinite-scroll-disabled="scrollDisabled"
|
||||
:infinite-scroll-distance="10" :infinite-scroll-listen-for-event="'scrollRefresh'">
|
||||
|
||||
<p-pull-refresh :on-refresh="refresh">
|
||||
<p-photo-search :settings="settings" :filter="filter" :filter-change="updateQuery"
|
||||
:refresh="refresh"></p-photo-search>
|
||||
|
||||
|
@ -18,6 +19,7 @@
|
|||
:open-photo="openPhoto" :open-location="openLocation"></p-photo-details>
|
||||
<p-photo-tiles v-else :photos="results" :selection="selection" :open-photo="openPhoto"></p-photo-tiles>
|
||||
</v-container>
|
||||
</p-pull-refresh>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
Loading…
Reference in a new issue