Refactor 4 with bugfixes
This commit is contained in:
@@ -516,6 +516,10 @@
|
||||
class="chat-input-wrapper relative"
|
||||
(mouseenter)="inputHovered.set(true)"
|
||||
(mouseleave)="inputHovered.set(false)"
|
||||
(dragenter)="onDragEnter($event)"
|
||||
(dragover)="onDragOver($event)"
|
||||
(dragleave)="onDragLeave($event)"
|
||||
(drop)="onDrop($event)"
|
||||
>
|
||||
<textarea
|
||||
#messageInputRef
|
||||
|
||||
@@ -187,6 +187,7 @@ export class ChatMessagesComponent implements AfterViewChecked, OnInit, OnDestro
|
||||
private toolbarHovering = false;
|
||||
inlineCodeToken = '`';
|
||||
dragActive = signal(false);
|
||||
private dragDepth = 0;
|
||||
inputHovered = signal(false);
|
||||
ctrlHeld = signal(false);
|
||||
private boundCtrlDown: ((e: KeyboardEvent) => void) | null = null;
|
||||
@@ -766,49 +767,140 @@ export class ChatMessagesComponent implements AfterViewChecked, OnInit, OnDestro
|
||||
/** Handle drag-enter to activate the drop zone overlay. */
|
||||
// Attachments: drag/drop and rendering
|
||||
onDragEnter(evt: DragEvent): void {
|
||||
if (!this.hasPotentialFilePayload(evt))
|
||||
return;
|
||||
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this.dragDepth++;
|
||||
this.dragActive.set(true);
|
||||
}
|
||||
|
||||
/** Keep the drop zone active while dragging over. */
|
||||
onDragOver(evt: DragEvent): void {
|
||||
if (!this.hasPotentialFilePayload(evt))
|
||||
return;
|
||||
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
if (evt.dataTransfer) {
|
||||
evt.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
|
||||
this.dragActive.set(true);
|
||||
}
|
||||
|
||||
/** Deactivate the drop zone when dragging leaves. */
|
||||
onDragLeave(evt: DragEvent): void {
|
||||
if (!this.dragActive())
|
||||
return;
|
||||
|
||||
evt.preventDefault();
|
||||
this.dragActive.set(false);
|
||||
evt.stopPropagation();
|
||||
this.dragDepth = Math.max(0, this.dragDepth - 1);
|
||||
|
||||
if (this.dragDepth === 0) {
|
||||
this.dragActive.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
/** Handle dropped files, adding them to the pending upload queue. */
|
||||
onDrop(evt: DragEvent): void {
|
||||
evt.preventDefault();
|
||||
const files: File[] = [];
|
||||
const items = evt.dataTransfer?.items;
|
||||
evt.stopPropagation();
|
||||
this.dragDepth = 0;
|
||||
const droppedFiles = this.extractDroppedFiles(evt);
|
||||
|
||||
if (droppedFiles.length === 0) {
|
||||
this.dragActive.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.pendingFiles.push(...droppedFiles);
|
||||
// Keep toolbar visible so user sees options
|
||||
this.toolbarVisible.set(true);
|
||||
this.dragActive.set(false);
|
||||
}
|
||||
|
||||
private hasPotentialFilePayload(evt: DragEvent): boolean {
|
||||
const dataTransfer = evt.dataTransfer;
|
||||
|
||||
if (!dataTransfer)
|
||||
return false;
|
||||
|
||||
if (dataTransfer.files?.length)
|
||||
return true;
|
||||
|
||||
const items = dataTransfer.items;
|
||||
|
||||
if (items?.length) {
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
if (items[itemIndex].kind === 'file') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const types = evt.dataTransfer?.types;
|
||||
|
||||
if (!types || types.length === 0)
|
||||
// Some desktop-to-browser drags expose no types until drop.
|
||||
return true;
|
||||
|
||||
for (let typeIndex = 0; typeIndex < types.length; typeIndex++) {
|
||||
const type = types[typeIndex];
|
||||
|
||||
if (
|
||||
type === 'Files' ||
|
||||
type === 'application/x-moz-file' ||
|
||||
type === 'public.file-url' ||
|
||||
type === 'text/uri-list'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private extractDroppedFiles(evt: DragEvent): File[] {
|
||||
const droppedFiles: File[] = [];
|
||||
const items = evt.dataTransfer?.items ?? null;
|
||||
|
||||
if (items && items.length) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
const item = items[itemIndex];
|
||||
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile();
|
||||
|
||||
if (file)
|
||||
files.push(file);
|
||||
droppedFiles.push(file);
|
||||
}
|
||||
}
|
||||
} else if (evt.dataTransfer?.files?.length) {
|
||||
for (let i = 0; i < evt.dataTransfer.files.length; i++) {
|
||||
files.push(evt.dataTransfer.files[i]);
|
||||
}
|
||||
}
|
||||
|
||||
files.forEach((file) => this.pendingFiles.push(file));
|
||||
// Keep toolbar visible so user sees options
|
||||
this.toolbarVisible.set(true);
|
||||
this.dragActive.set(false);
|
||||
const files = evt.dataTransfer?.files;
|
||||
|
||||
if (!files?.length)
|
||||
return droppedFiles;
|
||||
|
||||
for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
|
||||
const file = files[fileIndex];
|
||||
const exists = droppedFiles.some((existing) =>
|
||||
existing.name === file.name &&
|
||||
existing.size === file.size &&
|
||||
existing.type === file.type &&
|
||||
existing.lastModified === file.lastModified
|
||||
);
|
||||
|
||||
if (!exists) {
|
||||
droppedFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
return droppedFiles;
|
||||
}
|
||||
|
||||
/** Return all file attachments associated with a message. */
|
||||
|
||||
Reference in New Issue
Block a user