Browse Source

Drag and drop w/ folders support in chat

master
Eduard Kuzmenko 4 years ago
parent
commit
f593dc1bc1
  1. 86
      src/components/chat/dragAndDrop.ts
  2. 64
      src/helpers/dom.ts
  3. 139
      src/lib/appManagers/appImManager.ts
  4. 123
      src/scss/partials/_chatDrop.scss
  5. 3
      src/scss/partials/_chatTopbar.scss
  6. 1
      src/scss/style.scss

86
src/components/chat/dragAndDrop.ts

@ -0,0 +1,86 @@ @@ -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);
}
}

64
src/helpers/dom.ts

@ -321,6 +321,8 @@ export function generatePathData(x: number, y: number, width: number, height: nu @@ -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' | @@ -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;
}

139
src/lib/appManagers/appImManager.ts

@ -20,13 +20,16 @@ import appPhotosManager from './appPhotosManager'; @@ -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 { @@ -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

@ -0,0 +1,123 @@ @@ -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;
}
}

3
src/scss/partials/_chatTopbar.scss

@ -14,6 +14,9 @@ @@ -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) {

1
src/scss/style.scss

@ -124,6 +124,7 @@ $chat-padding-handhelds: .5rem; @@ -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…
Cancel
Save