Implement drag selection (#28)
parent
60501b5d58
commit
604b5e54b7
|
@ -65,6 +65,8 @@ import {
|
||||||
import { getCurrentUser } from "@nextcloud/auth";
|
import { getCurrentUser } from "@nextcloud/auth";
|
||||||
|
|
||||||
import * as dav from "../services/DavRequests";
|
import * as dav from "../services/DavRequests";
|
||||||
|
import * as utils from "../services/Utils";
|
||||||
|
|
||||||
import EditDate from "./modal/EditDate.vue";
|
import EditDate from "./modal/EditDate.vue";
|
||||||
import FaceMoveModal from "./modal/FaceMoveModal.vue";
|
import FaceMoveModal from "./modal/FaceMoveModal.vue";
|
||||||
import AddToAlbumModal from "./modal/AddToAlbumModal.vue";
|
import AddToAlbumModal from "./modal/AddToAlbumModal.vue";
|
||||||
|
@ -97,6 +99,9 @@ type Selection = Map<number, IPhoto>;
|
||||||
export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
@Prop() public heads: { [dayid: number]: IHeadRow };
|
@Prop() public heads: { [dayid: number]: IHeadRow };
|
||||||
|
|
||||||
|
/** List of rows for multi selection */
|
||||||
|
@Prop() public rows: IRow[];
|
||||||
|
|
||||||
/** Rows are in ascending order (desc is normal) */
|
/** Rows are in ascending order (desc is normal) */
|
||||||
@Prop() public isreverse: boolean;
|
@Prop() public isreverse: boolean;
|
||||||
|
|
||||||
|
@ -105,6 +110,11 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
private readonly selection!: Selection;
|
private readonly selection!: Selection;
|
||||||
private readonly defaultActions: ISelectionAction[];
|
private readonly defaultActions: ISelectionAction[];
|
||||||
|
|
||||||
|
private touchAnchor: IPhoto = null;
|
||||||
|
private touchTimer: number = 0;
|
||||||
|
private touchPrevSel!: Selection;
|
||||||
|
private prevOver!: IPhoto;
|
||||||
|
|
||||||
@Emit("refresh")
|
@Emit("refresh")
|
||||||
refresh() {}
|
refresh() {}
|
||||||
|
|
||||||
|
@ -197,6 +207,26 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Archive is not allowed only on folder routes */
|
||||||
|
private allowArchive() {
|
||||||
|
return this.$route.name !== "folders";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Is archive route */
|
||||||
|
private routeIsArchive() {
|
||||||
|
return this.$route.name === "archive";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Is album route */
|
||||||
|
private routeIsAlbum() {
|
||||||
|
return this.config_albumsEnabled && this.$route.name === "albums";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Public route that can't modify anything */
|
||||||
|
private routeIsPublic() {
|
||||||
|
return this.$route.name === "folder-share";
|
||||||
|
}
|
||||||
|
|
||||||
@Watch("show")
|
@Watch("show")
|
||||||
onShowChange() {
|
onShowChange() {
|
||||||
const klass = "has-top-bar";
|
const klass = "has-top-bar";
|
||||||
|
@ -207,6 +237,7 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Trigger to update props from selection set */
|
||||||
private selectionChanged() {
|
private selectionChanged() {
|
||||||
this.show = this.selection.size > 0;
|
this.show = this.selection.size > 0;
|
||||||
this.size = this.selection.size;
|
this.size = this.selection.size;
|
||||||
|
@ -220,37 +251,11 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
return this.selection.has(fileid);
|
return this.selection.has(fileid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Restore selections from new day object */
|
/** Get the actions list */
|
||||||
public restoreDay(day: IDay) {
|
private getActions(): ISelectionAction[] {
|
||||||
if (!this.has()) {
|
return this.defaultActions.filter(
|
||||||
return;
|
(a) => (!a.if || a.if(this)) && (!this.routeIsPublic() || a.allowPublic)
|
||||||
}
|
);
|
||||||
|
|
||||||
// FileID => Photo for new day
|
|
||||||
const dayMap = new Map<number, IPhoto>();
|
|
||||||
day.detail.forEach((photo) => {
|
|
||||||
dayMap.set(photo.fileid, photo);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.selection.forEach((photo, fileid) => {
|
|
||||||
// Process this day only
|
|
||||||
if (photo.dayid !== day.dayid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove all selections that are not in the new day
|
|
||||||
if (!dayMap.has(fileid)) {
|
|
||||||
this.selection.delete(fileid);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the photo object
|
|
||||||
const newPhoto = dayMap.get(fileid);
|
|
||||||
this.selection.set(fileid, newPhoto);
|
|
||||||
newPhoto.flag |= this.c.FLAG_SELECTED;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.selectionChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Click on an action */
|
/** Click on an action */
|
||||||
|
@ -265,43 +270,145 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get the actions list */
|
/** Clicking on photo */
|
||||||
private getActions(): ISelectionAction[] {
|
public clickPhoto(photo: IPhoto, event: any, rowIdx: number) {
|
||||||
return this.defaultActions.filter(
|
if (photo.flag & this.c.FLAG_PLACEHOLDER) return;
|
||||||
(a) => (!a.if || a.if(this)) && (!this.routeIsPublic() || a.allowPublic)
|
|
||||||
);
|
if (this.has()) {
|
||||||
|
if (event.shiftKey) {
|
||||||
|
this.selectMulti(photo, this.rows, rowIdx);
|
||||||
|
} else {
|
||||||
|
this.selectPhoto(photo);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.openViewer(photo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tap on */
|
||||||
|
public touchstartPhoto(photo: IPhoto, event: any, rowIdx: number) {
|
||||||
|
if (photo.flag & this.c.FLAG_PLACEHOLDER) return;
|
||||||
|
|
||||||
|
this.touchAnchor = photo;
|
||||||
|
this.prevOver = photo;
|
||||||
|
this.touchPrevSel = new Map(this.selection);
|
||||||
|
this.touchTimer = window.setTimeout(() => {
|
||||||
|
if (this.touchAnchor === photo) {
|
||||||
|
this.selectPhoto(photo, true);
|
||||||
|
}
|
||||||
|
this.touchTimer = 0;
|
||||||
|
}, 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tap off */
|
||||||
|
public touchendPhoto(photo: IPhoto, event: any, rowIdx: number) {
|
||||||
|
if (photo.flag & this.c.FLAG_PLACEHOLDER) return;
|
||||||
|
window.clearTimeout(this.touchTimer);
|
||||||
|
this.touchTimer = 0;
|
||||||
|
this.touchAnchor = null;
|
||||||
|
this.prevOver = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tap over
|
||||||
|
* photo and rowIdx are that of the *anchor*
|
||||||
|
*/
|
||||||
|
public touchmovePhoto(anchor: IPhoto, event: any, rowIdx: number) {
|
||||||
|
if (anchor.flag & this.c.FLAG_PLACEHOLDER) return;
|
||||||
|
|
||||||
|
if (this.touchTimer) {
|
||||||
|
// Touch is not held, just cancel
|
||||||
|
window.clearTimeout(this.touchTimer);
|
||||||
|
this.touchTimer = 0;
|
||||||
|
this.touchAnchor = null;
|
||||||
|
return;
|
||||||
|
} else if (!this.touchAnchor) {
|
||||||
|
// Touch was previously cancelled
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent scrolling
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Use first touch -- can't do much better yet
|
||||||
|
const touch: Touch = event.touches[0];
|
||||||
|
if (!touch) return;
|
||||||
|
|
||||||
|
// Which photo is the cursor over, if any
|
||||||
|
const elems = document.elementsFromPoint(touch.clientX, touch.clientY);
|
||||||
|
const photoComp: any = elems.find((e) => e.classList.contains("p-outer"));
|
||||||
|
let overPhoto: IPhoto = photoComp?.__vue__?.data;
|
||||||
|
if (overPhoto && overPhoto.flag & this.c.FLAG_PLACEHOLDER) overPhoto = null;
|
||||||
|
|
||||||
|
// Do multi-selection "till" overPhoto "from" anchor
|
||||||
|
// This logic is completely different from the desktop because of the
|
||||||
|
// existence of a definitive "anchor" element. We just need to find
|
||||||
|
// rverything between the anchor and the current photo
|
||||||
|
if (overPhoto && this.prevOver !== overPhoto) {
|
||||||
|
this.prevOver = overPhoto;
|
||||||
|
|
||||||
|
// days reverse XOR rows reverse
|
||||||
|
let reverse: boolean;
|
||||||
|
if (overPhoto.dayid === this.touchAnchor.dayid) {
|
||||||
|
const l = overPhoto.d.detail;
|
||||||
|
const ai = l.indexOf(this.touchAnchor);
|
||||||
|
const oi = l.indexOf(overPhoto);
|
||||||
|
if (ai === -1 || oi === -1) return; // Shouldn't happen
|
||||||
|
reverse = ai > oi;
|
||||||
|
} else {
|
||||||
|
reverse = overPhoto.dayid > this.touchAnchor.dayid != this.isreverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSelection = new Map(this.touchPrevSel);
|
||||||
|
const updatedDays = new Set<number>();
|
||||||
|
|
||||||
|
// Walk over rows
|
||||||
|
let i = rowIdx;
|
||||||
|
let j = this.rows[i].photos.indexOf(this.touchAnchor);
|
||||||
|
while (true) {
|
||||||
|
let p = this.rows[i]?.photos?.[j];
|
||||||
|
if (!p) break; // shouldn't happen, ever
|
||||||
|
|
||||||
|
j += reverse ? -1 : 1;
|
||||||
|
if (j < 0) {
|
||||||
|
while (!this.rows[--i].photos);
|
||||||
|
j = this.rows[i].photos.length - 1;
|
||||||
|
} else if (j >= this.rows[i].photos.length) {
|
||||||
|
while (!this.rows[++i].photos);
|
||||||
|
j = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is there now
|
||||||
|
newSelection.set(p.fileid, p);
|
||||||
|
|
||||||
|
// Perf: only update heads if not selected
|
||||||
|
if (!(p.flag & this.c.FLAG_SELECTED)) {
|
||||||
|
this.selectPhoto(p, true, true);
|
||||||
|
updatedDays.add(p.dayid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're trying to update too much -- something went wrong
|
||||||
|
if (newSelection.size - this.selection.size > 50) break;
|
||||||
|
|
||||||
|
// Check goal
|
||||||
|
if (p === overPhoto) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove unselected
|
||||||
|
for (const [fileid, p] of this.selection) {
|
||||||
|
if (!newSelection.has(fileid)) {
|
||||||
|
this.selectPhoto(p, false, true);
|
||||||
|
updatedDays.add(p.dayid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update heads
|
||||||
|
for (const dayid of updatedDays) {
|
||||||
|
this.updateHeadSelected(this.heads[dayid]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clear all selected photos */
|
|
||||||
public clearSelection(only?: IPhoto[]) {
|
|
||||||
const heads = new Set<IHeadRow>();
|
|
||||||
const toClear = only || this.selection.values();
|
|
||||||
Array.from(toClear).forEach((photo: IPhoto) => {
|
|
||||||
photo.flag &= ~this.c.FLAG_SELECTED;
|
|
||||||
heads.add(this.heads[photo.d.dayid]);
|
|
||||||
this.selection.delete(photo.fileid);
|
|
||||||
this.selectionChanged();
|
|
||||||
});
|
|
||||||
heads.forEach(this.updateHeadSelected);
|
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check if the day for a photo is selected entirely */
|
|
||||||
private updateHeadSelected(head: IHeadRow) {
|
|
||||||
let selected = true;
|
|
||||||
|
|
||||||
// Check if all photos are selected
|
|
||||||
for (const row of head.day.rows) {
|
|
||||||
for (const photo of row.photos) {
|
|
||||||
if (!(photo.flag & this.c.FLAG_SELECTED)) {
|
|
||||||
selected = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update head
|
|
||||||
head.selected = selected;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Add a photo to selection list */
|
/** Add a photo to selection list */
|
||||||
|
@ -339,7 +446,6 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
|
|
||||||
/** Multi-select */
|
/** Multi-select */
|
||||||
public selectMulti(photo: IPhoto, rows: IRow[], rowIdx: number) {
|
public selectMulti(photo: IPhoto, rows: IRow[], rowIdx: number) {
|
||||||
console.log("selectMulti", photo, rows, rowIdx);
|
|
||||||
const pRow = rows[rowIdx];
|
const pRow = rows[rowIdx];
|
||||||
const pIdx = pRow.photos.indexOf(photo);
|
const pIdx = pRow.photos.indexOf(photo);
|
||||||
if (pIdx === -1) return;
|
if (pIdx === -1) return;
|
||||||
|
@ -407,6 +513,71 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Check if the day for a photo is selected entirely */
|
||||||
|
private updateHeadSelected(head: IHeadRow) {
|
||||||
|
let selected = true;
|
||||||
|
|
||||||
|
// Check if all photos are selected
|
||||||
|
for (const row of head.day.rows) {
|
||||||
|
for (const photo of row.photos) {
|
||||||
|
if (!(photo.flag & this.c.FLAG_SELECTED)) {
|
||||||
|
selected = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update head
|
||||||
|
head.selected = selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clear all selected photos */
|
||||||
|
public clearSelection(only?: IPhoto[]) {
|
||||||
|
const heads = new Set<IHeadRow>();
|
||||||
|
const toClear = only || this.selection.values();
|
||||||
|
Array.from(toClear).forEach((photo: IPhoto) => {
|
||||||
|
photo.flag &= ~this.c.FLAG_SELECTED;
|
||||||
|
heads.add(this.heads[photo.d.dayid]);
|
||||||
|
this.selection.delete(photo.fileid);
|
||||||
|
this.selectionChanged();
|
||||||
|
});
|
||||||
|
heads.forEach(this.updateHeadSelected);
|
||||||
|
this.$forceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Restore selections from new day object */
|
||||||
|
public restoreDay(day: IDay) {
|
||||||
|
if (!this.has()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileID => Photo for new day
|
||||||
|
const dayMap = new Map<number, IPhoto>();
|
||||||
|
day.detail.forEach((photo) => {
|
||||||
|
dayMap.set(photo.fileid, photo);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.selection.forEach((photo, fileid) => {
|
||||||
|
// Process this day only
|
||||||
|
if (photo.dayid !== day.dayid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all selections that are not in the new day
|
||||||
|
if (!dayMap.has(fileid)) {
|
||||||
|
this.selection.delete(fileid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the photo object
|
||||||
|
const newPhoto = dayMap.get(fileid);
|
||||||
|
this.selection.set(fileid, newPhoto);
|
||||||
|
newPhoto.flag |= this.c.FLAG_SELECTED;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.selectionChanged();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download the currently selected files
|
* Download the currently selected files
|
||||||
*/
|
*/
|
||||||
|
@ -521,26 +692,6 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Archive is not allowed only on folder routes */
|
|
||||||
private allowArchive() {
|
|
||||||
return this.$route.name !== "folders";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Is archive route */
|
|
||||||
private routeIsArchive() {
|
|
||||||
return this.$route.name === "archive";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Is album route */
|
|
||||||
private routeIsAlbum() {
|
|
||||||
return this.config_albumsEnabled && this.$route.name === "albums";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Public route that can't modify anything */
|
|
||||||
private routeIsPublic() {
|
|
||||||
return this.$route.name === "folder-share";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move selected photos to album
|
* Move selected photos to album
|
||||||
*/
|
*/
|
||||||
|
@ -623,6 +774,14 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
this.deletePhotos(delPhotos);
|
this.deletePhotos(delPhotos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Open viewer with given photo */
|
||||||
|
private openViewer(photo: IPhoto) {
|
||||||
|
this.$router.push({
|
||||||
|
...this.$route,
|
||||||
|
hash: utils.getViewerHash(photo),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -98,7 +98,12 @@
|
||||||
:day="item.day"
|
:day="item.day"
|
||||||
:key="photo.fileid"
|
:key="photo.fileid"
|
||||||
@select="selectionManager.selectPhoto"
|
@select="selectionManager.selectPhoto"
|
||||||
@click="clickPhoto(photo, $event, index)"
|
@mousedown="selectionManager.clickPhoto(photo, $event, index)"
|
||||||
|
@touchstart="
|
||||||
|
selectionManager.touchstartPhoto(photo, $event, index)
|
||||||
|
"
|
||||||
|
@touchend="selectionManager.touchendPhoto(photo, $event, index)"
|
||||||
|
@touchmove="selectionManager.touchmovePhoto(photo, $event, index)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -117,6 +122,7 @@
|
||||||
<SelectionManager
|
<SelectionManager
|
||||||
ref="selectionManager"
|
ref="selectionManager"
|
||||||
:heads="heads"
|
:heads="heads"
|
||||||
|
:rows="list"
|
||||||
:isreverse="isMonthView"
|
:isreverse="isMonthView"
|
||||||
@refresh="softRefresh"
|
@refresh="softRefresh"
|
||||||
@delete="deleteFromViewWithAnimation"
|
@delete="deleteFromViewWithAnimation"
|
||||||
|
@ -1193,24 +1199,6 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clicking on photo */
|
|
||||||
clickPhoto(photo: IPhoto, event: any, rowIdx: number) {
|
|
||||||
if (photo.flag & this.c.FLAG_PLACEHOLDER) return;
|
|
||||||
|
|
||||||
if (this.selectionManager.has()) {
|
|
||||||
if (event.shiftKey) {
|
|
||||||
this.selectionManager.selectMulti(photo, this.list, rowIdx);
|
|
||||||
} else {
|
|
||||||
this.selectionManager.selectPhoto(photo);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.$router.push({
|
|
||||||
...this.$route,
|
|
||||||
hash: utils.getViewerHash(photo),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete elements from main view with some animation
|
* Delete elements from main view with some animation
|
||||||
*
|
*
|
||||||
|
|
|
@ -28,11 +28,11 @@
|
||||||
<div
|
<div
|
||||||
class="img-outer fill-block"
|
class="img-outer fill-block"
|
||||||
@contextmenu="contextmenu"
|
@contextmenu="contextmenu"
|
||||||
@mousedown.passive="emitClick"
|
@mousedown="$emit('mousedown', $event)"
|
||||||
@touchstart.passive="touchstart"
|
@touchstart.passive="$emit('touchstart', $event)"
|
||||||
@touchmove.passive="touchend"
|
@touchmove="$emit('touchmove', $event)"
|
||||||
@touchend.passive="touchend"
|
@touchend.passive="$emit('touchend', $event)"
|
||||||
@touchcancel.passive="touchend"
|
@touchcancel.passive="$emit('touchend', $event)"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
ref="img"
|
ref="img"
|
||||||
|
@ -78,7 +78,6 @@ export default class Photo extends Mixins(GlobalMixin) {
|
||||||
@Prop() day: IDay;
|
@Prop() day: IDay;
|
||||||
|
|
||||||
@Emit("select") emitSelect(data: IPhoto) {}
|
@Emit("select") emitSelect(data: IPhoto) {}
|
||||||
@Emit("click") emitClick() {}
|
|
||||||
|
|
||||||
@Watch("data")
|
@Watch("data")
|
||||||
onDataChange(newData: IPhoto, oldData: IPhoto) {
|
onDataChange(newData: IPhoto, oldData: IPhoto) {
|
||||||
|
@ -210,10 +209,10 @@ export default class Photo extends Mixins(GlobalMixin) {
|
||||||
|
|
||||||
contextmenu(e: Event) {
|
contextmenu(e: Event) {
|
||||||
// on mobile only
|
// on mobile only
|
||||||
if (this.hasTouch) {
|
// if (this.hasTouch) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
touchend() {
|
touchend() {
|
||||||
|
|
Loading…
Reference in New Issue