Drag and drop w/ folders support in chat
This commit is contained in:
parent
f00be24bb2
commit
f593dc1bc1
86
src/components/chat/dragAndDrop.ts
Normal file
86
src/components/chat/dragAndDrop.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { generatePathData } from "../../helpers/dom";
|
||||
|
||||
export default class ChatDragAndDrop {
|
||||
container: HTMLDivElement;
|
||||
svg: SVGSVGElement;
|
||||
outlineWrapper: HTMLDivElement;
|
||||
path: SVGPathElement;
|
||||
|
||||
constructor(appendTo: HTMLElement, private options: {
|
||||
icon: string,
|
||||
header: string,
|
||||
subtitle: string,
|
||||
onDrop: (e: DragEvent) => void
|
||||
}) {
|
||||
this.container = document.createElement('div');
|
||||
this.container.classList.add('drop', 'z-depth-1');
|
||||
|
||||
this.outlineWrapper = document.createElement('div');
|
||||
this.outlineWrapper.classList.add('drop-outline-wrapper');
|
||||
|
||||
this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
this.svg.classList.add('drop-outline');
|
||||
|
||||
this.path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
this.path.classList.add('drop-outline-path');
|
||||
|
||||
const dropIcon = document.createElement('div');
|
||||
dropIcon.classList.add('drop-icon', 'tgico-' + options.icon);
|
||||
|
||||
const dropHeader = document.createElement('div');
|
||||
dropHeader.classList.add('drop-header');
|
||||
dropHeader.innerHTML = options.header;//'Drop files here to send them';
|
||||
|
||||
const dropSubtitle = document.createElement('div');
|
||||
dropSubtitle.classList.add('drop-subtitle');
|
||||
dropSubtitle.innerHTML = options.subtitle;//'without compression';
|
||||
|
||||
this.svg.append(this.path);
|
||||
this.outlineWrapper.append(this.svg);
|
||||
|
||||
this.container.append(this.outlineWrapper, dropIcon, dropHeader, dropSubtitle);
|
||||
appendTo.append(this.container);
|
||||
|
||||
this.container.addEventListener('dragover', this.onDragOver);
|
||||
this.container.addEventListener('dragleave', this.onDragLeave);
|
||||
this.container.addEventListener('drop', this.onDrop);
|
||||
}
|
||||
|
||||
onDragOver = (e: DragEvent) => {
|
||||
this.container.classList.add('is-dragover');
|
||||
//SetTransition(this.container, 'is-dragover', true, 500);
|
||||
};
|
||||
|
||||
onDragLeave = (e: DragEvent) => {
|
||||
this.container.classList.remove('is-dragover');
|
||||
//SetTransition(this.container, 'is-dragover', false, 500);
|
||||
};
|
||||
|
||||
onDrop = (e: DragEvent) => {
|
||||
this.options.onDrop(e);
|
||||
};
|
||||
|
||||
destroy() {
|
||||
delete this.options;
|
||||
this.container.remove();
|
||||
this.container.removeEventListener('dragover', this.onDragOver);
|
||||
this.container.removeEventListener('dragleave', this.onDragLeave);
|
||||
this.container.removeEventListener('drop', this.onDrop);
|
||||
}
|
||||
|
||||
setPath() {
|
||||
const rect = this.outlineWrapper.getBoundingClientRect();
|
||||
this.svg.setAttributeNS(null, 'preserveAspectRatio', 'none');
|
||||
this.svg.setAttributeNS(null, 'viewBox', `0 0 ${rect.width} ${rect.height}`);
|
||||
this.svg.setAttributeNS(null, 'width', `${rect.width}`);
|
||||
this.svg.setAttributeNS(null, 'height', `${rect.height}`);
|
||||
|
||||
const radius = 10;
|
||||
//const strokeWidth = 2;
|
||||
const sizeX = rect.width - radius;
|
||||
const sizeY = rect.height - radius;
|
||||
const pos = radius / 2;
|
||||
const d = generatePathData(pos, pos, sizeX, sizeY, radius, radius, radius, radius);
|
||||
this.path.setAttributeNS(null, 'd', d);
|
||||
}
|
||||
}
|
@ -321,6 +321,8 @@ export function generatePathData(x: number, y: number, width: number, height: nu
|
||||
return data.join(' ');
|
||||
};
|
||||
|
||||
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.generatePathData = generatePathData);
|
||||
|
||||
//export function findUpClassName<T>(el: any, className: string): T;
|
||||
export function findUpClassName(el: any, className: string): HTMLElement {
|
||||
return el.closest('.' + className);
|
||||
@ -590,3 +592,65 @@ export const getElementByPoint = (container: HTMLElement, verticalSide: 'top' |
|
||||
const y = verticalSide == 'bottom' ? Math.floor(rect.top + rect.height - 1) : Math.ceil(rect.top + 1);
|
||||
return document.elementFromPoint(x, y) as any;
|
||||
};
|
||||
|
||||
export async function getFilesFromEvent(e: ClipboardEvent | DragEvent, onlyTypes = false): Promise<any[]> {
|
||||
const files: any[] = [];
|
||||
|
||||
const scanFiles = async(item: any) => {
|
||||
if(item.isDirectory) {
|
||||
const directoryReader = item.createReader();
|
||||
await new Promise((resolve, reject) => {
|
||||
directoryReader.readEntries(async(entries: any) => {
|
||||
for(const entry of entries) {
|
||||
await scanFiles(entry);
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} else if(item) {
|
||||
if(onlyTypes) {
|
||||
files.push(item.type);
|
||||
} else {
|
||||
const file = item instanceof File ?
|
||||
item :
|
||||
(
|
||||
item instanceof DataTransferItem ?
|
||||
item.getAsFile() :
|
||||
await new Promise((resolve, reject) => item.file(resolve, reject))
|
||||
);
|
||||
|
||||
/* if(!onlyTypes) {
|
||||
console.log('getFilesFromEvent: got file', item, file);
|
||||
} */
|
||||
|
||||
if(!file) return;
|
||||
files.push(file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if(e instanceof DragEvent && e.dataTransfer.files && !e.dataTransfer.items) {
|
||||
for(let i = 0; i < e.dataTransfer.files.length; i++) {
|
||||
const file = e.dataTransfer.files[i];
|
||||
files.push(onlyTypes ? file.type : file);
|
||||
}
|
||||
} else {
|
||||
// @ts-ignore
|
||||
const items = (e.dataTransfer || e.clipboardData || e.originalEvent.clipboardData).items;
|
||||
|
||||
for(let i = 0; i < items.length; ++i) {
|
||||
const item: DataTransferItem = items[i];
|
||||
if(item.kind === 'file') {
|
||||
const entry = onlyTypes ? item : item.webkitGetAsEntry() || item.getAsFile();
|
||||
await scanFiles(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* if(!onlyTypes) {
|
||||
console.log('getFilesFromEvent: got files:', e, files);
|
||||
} */
|
||||
|
||||
return files;
|
||||
}
|
||||
|
@ -20,13 +20,16 @@ import appPhotosManager from './appPhotosManager';
|
||||
import appProfileManager from './appProfileManager';
|
||||
import appStickersManager from './appStickersManager';
|
||||
import appWebPagesManager from './appWebPagesManager';
|
||||
import { cancelEvent, placeCaretAtEnd } from '../../helpers/dom';
|
||||
import { cancelEvent, findUpClassName, generatePathData, getFilesFromEvent, placeCaretAtEnd } from '../../helpers/dom';
|
||||
import PopupNewMedia from '../../components/popupNewMedia';
|
||||
import { TransitionSlider } from '../../components/transition';
|
||||
import { numberWithCommas } from '../../helpers/number';
|
||||
import MarkupTooltip from '../../components/chat/markupTooltip';
|
||||
import { isTouchSupported } from '../../helpers/touchSupport';
|
||||
import appPollsManager from './appPollsManager';
|
||||
import SetTransition from '../../components/singleTransition';
|
||||
import { isSafari } from '../../helpers/userAgent';
|
||||
import ChatDragAndDrop from '../../components/chat/dragAndDrop';
|
||||
|
||||
//console.log('appImManager included33!');
|
||||
|
||||
@ -199,40 +202,134 @@ export class AppImManager {
|
||||
|
||||
document.body.addEventListener('keydown', onKeyDown);
|
||||
|
||||
if(!isTouchSupported) {
|
||||
this.attachDragAndDropListeners();
|
||||
}
|
||||
|
||||
if(!isTouchSupported) {
|
||||
this.markupTooltip = new MarkupTooltip(this);
|
||||
this.markupTooltip.handleSelection();
|
||||
}
|
||||
}
|
||||
|
||||
private onDocumentPaste = (e: ClipboardEvent) => {
|
||||
private attachDragAndDropListeners() {
|
||||
const drops: ChatDragAndDrop[] = [];
|
||||
let mounted = false;
|
||||
const toggle = async(e: DragEvent, mount: boolean) => {
|
||||
if(mount == mounted) return;
|
||||
|
||||
if(mount && !drops.length) {
|
||||
const types: string[] = await getFilesFromEvent(e, true)
|
||||
const isFiles = e.dataTransfer.types[0] === 'Files' && !types.length; // * can't get file items not from 'drop' on Safari
|
||||
|
||||
const foundMedia = types.filter(t => ['image', 'video'].includes(t.split('/')[0])).length;
|
||||
const foundDocuments = types.length - foundMedia;
|
||||
|
||||
this.log('drag files', types);
|
||||
|
||||
if(types.length || isFiles) {
|
||||
drops.push(new ChatDragAndDrop(dropsContainer, {
|
||||
icon: 'dragfiles',
|
||||
header: 'Drop files here to send them',
|
||||
subtitle: 'without compression',
|
||||
onDrop: (e: DragEvent) => {
|
||||
toggle(e, false);
|
||||
appImManager.log('drop', e);
|
||||
appImManager.onDocumentPaste(e, 'document');
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if((foundMedia && !foundDocuments) || isFiles) {
|
||||
drops.push(new ChatDragAndDrop(dropsContainer, {
|
||||
icon: 'dragmedia',
|
||||
header: 'Drop files here to send them',
|
||||
subtitle: 'in a quick way',
|
||||
onDrop: (e: DragEvent) => {
|
||||
toggle(e, false);
|
||||
appImManager.log('drop', e);
|
||||
appImManager.onDocumentPaste(e, 'media');
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
this.chat.container.append(dropsContainer);
|
||||
}
|
||||
|
||||
//if(!mount) return;
|
||||
|
||||
SetTransition(dropsContainer, 'is-visible', mount, 200, () => {
|
||||
if(!mount) {
|
||||
drops.forEach(drop => {
|
||||
drop.destroy();
|
||||
});
|
||||
|
||||
drops.length = 0;
|
||||
}
|
||||
});
|
||||
|
||||
if(mount) {
|
||||
drops.forEach(drop => {
|
||||
drop.setPath();
|
||||
});
|
||||
} else {
|
||||
counter = 0;
|
||||
}
|
||||
|
||||
document.body.classList.toggle('is-dragging', mount);
|
||||
mounted = mount;
|
||||
};
|
||||
|
||||
/* document.body.addEventListener('dragover', (e) => {
|
||||
cancelEvent(e);
|
||||
}); */
|
||||
|
||||
let counter = 0;
|
||||
document.body.addEventListener('dragenter', (e) => {
|
||||
counter++;
|
||||
});
|
||||
|
||||
document.body.addEventListener('dragover', (e) => {
|
||||
//this.log('dragover', e/* , e.dataTransfer.types[0] */);
|
||||
toggle(e, true);
|
||||
cancelEvent(e);
|
||||
});
|
||||
|
||||
document.body.addEventListener('dragleave', (e) => {
|
||||
//this.log('dragleave', e, counter);
|
||||
//if((e.pageX <= 0 || e.pageX >= appPhotosManager.windowW) || (e.pageY <= 0 || e.pageY >= appPhotosManager.windowH)) {
|
||||
counter--;
|
||||
if(counter === 0) {
|
||||
//if(!findUpClassName(e.target, 'drops-container')) {
|
||||
toggle(e, false);
|
||||
}
|
||||
});
|
||||
|
||||
const dropsContainer = document.createElement('div');
|
||||
dropsContainer.classList.add('drops-container');
|
||||
}
|
||||
|
||||
private onDocumentPaste = (e: ClipboardEvent | DragEvent, attachType?: 'media' | 'document') => {
|
||||
const peerId = this.chat?.peerId;
|
||||
if(!peerId || rootScope.overlayIsActive || (peerId < 0 && !appChatsManager.hasRights(peerId, 'send', 'send_media'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
//console.log('document paste');
|
||||
|
||||
// @ts-ignore
|
||||
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
|
||||
//console.log('item', event.clipboardData.getData());
|
||||
//let foundFile = false;
|
||||
const chatInput = this.chat.input;
|
||||
for(let i = 0; i < items.length; ++i) {
|
||||
if(items[i].kind == 'file') {
|
||||
e.preventDefault()
|
||||
e.cancelBubble = true;
|
||||
e.stopPropagation();
|
||||
//foundFile = true;
|
||||
|
||||
let file = items[i].getAsFile();
|
||||
//console.log(items[i], file);
|
||||
if(!file) continue;
|
||||
|
||||
chatInput.willAttachType = file.type.indexOf('image/') === 0 ? 'media' : "document";
|
||||
new PopupNewMedia([file], chatInput.willAttachType);
|
||||
cancelEvent(e);
|
||||
getFilesFromEvent(e).then((files: File[]) => {
|
||||
if(files.length) {
|
||||
if(attachType == 'media' && files.find(file => !['image', 'video'].includes(file.type.split('/')[0]))) {
|
||||
attachType = 'document';
|
||||
}
|
||||
|
||||
const chatInput = this.chat.input;
|
||||
chatInput.willAttachType = attachType || (files[0].type.indexOf('image/') === 0 ? 'media' : "document");
|
||||
new PopupNewMedia(files, chatInput.willAttachType);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
public selectTab(id: number) {
|
||||
|
123
src/scss/partials/_chatDrop.scss
Normal file
123
src/scss/partials/_chatDrop.scss
Normal file
@ -0,0 +1,123 @@
|
||||
.drops-container {
|
||||
--padding: 20px;
|
||||
--pinned-floating-height: 0px;
|
||||
position: absolute !important;
|
||||
z-index: 3;
|
||||
top: calc(56px + var(--pinned-floating-height) + var(--padding));
|
||||
right: var(--padding);
|
||||
bottom: var(--padding);
|
||||
left: var(--padding);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
width: auto !important;
|
||||
|
||||
@include respond-to(medium-screens) {
|
||||
//transition: transform var(--layer-transition);
|
||||
|
||||
body.is-right-column-shown & {
|
||||
//left: calc(var(--right-column-width) / -2);
|
||||
right: calc(var(--right-column-width));
|
||||
}
|
||||
}
|
||||
|
||||
@include respond-to(handhelds) {
|
||||
--padding: 10px;
|
||||
}
|
||||
|
||||
&:not(.is-visible) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.is-visible {
|
||||
animation: fade-in-opacity .2s linear forwards;
|
||||
|
||||
&.backwards {
|
||||
animation: fade-in-backwards-opacity .2s linear forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drop {
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
//height: 100%;
|
||||
border-radius: $border-radius-big;
|
||||
width: 100%;
|
||||
max-width: 696px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #a2acb4;
|
||||
transition: color .2s ease-in-out;
|
||||
pointer-events: all;
|
||||
flex: 1 1 auto;
|
||||
|
||||
&-outline {
|
||||
&-wrapper {
|
||||
position: absolute;
|
||||
top: 19px;
|
||||
right: 19px;
|
||||
bottom: 19px;
|
||||
left: 19px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&-path {
|
||||
fill: none;
|
||||
stroke-dasharray: 13.5, 11;
|
||||
stroke: #a2acb4;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
transition: stroke .2s ease-in-out;
|
||||
stroke-dashoffset: 0;
|
||||
|
||||
.drop.is-dragover & {
|
||||
animation: drop-outline-move .5s linear infinite;
|
||||
stroke: $color-blue;
|
||||
}
|
||||
|
||||
/* .drop.is-dragover.backwards & {
|
||||
//animation: drop-outline-backwards-move .5s linear forwards;
|
||||
animation-direction: reverse;
|
||||
animation-fill-mode: forwards;
|
||||
} */
|
||||
}
|
||||
}
|
||||
|
||||
&-icon {
|
||||
font-size: 6rem;
|
||||
}
|
||||
|
||||
&-header {
|
||||
font-weight: 500;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
&.is-dragover {
|
||||
color: $color-blue;
|
||||
}
|
||||
|
||||
& + & {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes drop-outline-move {
|
||||
0% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dashoffset: -24.5;
|
||||
}
|
||||
}
|
||||
|
||||
body.is-dragging {
|
||||
.page-chats {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
@ -14,6 +14,9 @@
|
||||
/* & + .bubbles {
|
||||
margin-top: 52px;
|
||||
} */
|
||||
& ~ .drops-container {
|
||||
--pinned-floating-height: 52px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-pinned-message-shown:not(.hide-pinned):not(.is-pinned-audio-shown) {
|
||||
|
@ -124,6 +124,7 @@ $chat-padding-handhelds: .5rem;
|
||||
@import "partials/chatPinned";
|
||||
@import "partials/chatMarkupTooltip";
|
||||
@import "partials/chatStickersHelper";
|
||||
@import "partials/chatDrop";
|
||||
@import "partials/sidebar";
|
||||
@import "partials/leftSidebar";
|
||||
@import "partials/rightSidebar";
|
||||
|
Loading…
x
Reference in New Issue
Block a user