Skip to content

Commit

Permalink
fix: capture and store file's full path while getting drag'n'dropped (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
nd0ut authored Oct 10, 2023
1 parent 8b7bce9 commit 3ba168e
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 67 deletions.
5 changes: 3 additions & 2 deletions abstract/UploaderBlock.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,9 @@ export class UploaderBlock extends ActivityBlock {

/**
* @param {File} file
* @param {{ silent?: boolean; fileName?: string; source?: string }} [options]
* @param {{ silent?: boolean; fileName?: string; source?: string; fullPath?: string }} [options]
*/
addFileFromObject(file, { silent, fileName, source } = {}) {
addFileFromObject(file, { silent, fileName, source, fullPath } = {}) {
this.uploadCollection.add({
file,
isImage: fileIsImage(file),
Expand All @@ -176,6 +176,7 @@ export class UploaderBlock extends ActivityBlock {
fileSize: file.size,
silentUpload: silent ?? false,
source: source ?? UploadSource.API,
fullPath,
});
}

Expand Down
5 changes: 5 additions & 0 deletions abstract/uploadEntrySchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,9 @@ export const uploadEntrySchema = Object.freeze({
value: false,
nullable: true,
},
fullPath: {
type: String,
value: null,
nullable: true,
},
});
108 changes: 68 additions & 40 deletions blocks/DropArea/DropArea.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import { UploaderBlock } from '../../abstract/UploaderBlock.js';
// @ts-check

import { ActivityBlock } from '../../abstract/ActivityBlock.js';
import { DropzoneState, addDropzone } from './addDropzone.js';
import { Modal } from '../Modal/Modal.js';
import { UploaderBlock } from '../../abstract/UploaderBlock.js';
import { stringToArray } from '../../utils/stringToArray.js';
import { UploadSource } from '../utils/UploadSource.js';
import { asBoolean } from '../Config/normalizeConfigValue.js';
import { UploadSource } from '../utils/UploadSource.js';
import { DropzoneState, addDropzone } from './addDropzone.js';

export class DropArea extends UploaderBlock {
init$ = {
...this.init$,
state: DropzoneState.INACTIVE,
withIcon: false,
isClickable: false,
isFullscreen: false,
isEnabled: true,
isVisible: true,
text: this.l10n('drop-files-here'),
'lr-drop-area/targets': null,
};
constructor() {
super();

this.init$ = {
...this.init$,
state: DropzoneState.INACTIVE,
withIcon: false,
isClickable: false,
isFullscreen: false,
isEnabled: true,
isVisible: true,
text: this.l10n('drop-files-here'),
'lr-drop-area/targets': null,
};
}

isActive() {
if (!this.$.isEnabled) {
Expand All @@ -36,6 +41,7 @@ export class DropArea extends UploaderBlock {

return hasSize && visible && isInViewport;
}

initCallback() {
super.initCallback();

Expand All @@ -44,46 +50,62 @@ export class DropArea extends UploaderBlock {
}
this.$['lr-drop-area/targets'].add(this);

this.defineAccessor('disabled', (value) => {
this.set$({ isEnabled: !asBoolean(value) });
});
this.defineAccessor('clickable', (value) => {
this.set$({ isClickable: asBoolean(value) });
});
this.defineAccessor('with-icon', (value) => {
this.set$({ withIcon: asBoolean(value) });
});
this.defineAccessor('fullscreen', (value) => {
this.set$({ isFullscreen: asBoolean(value) });
});

this.defineAccessor('text', (value) => {
if (value) {
this.set$({ text: this.l10n(value) || value });
} else {
this.set$({ text: this.l10n('drop-files-here') });
this.defineAccessor(
'disabled',
/** @param {unknown} value */ (value) => {
this.set$({ isEnabled: !asBoolean(value) });
}
});
);
this.defineAccessor(
'clickable',
/** @param {unknown} value */ (value) => {
this.set$({ isClickable: asBoolean(value) });
}
);
this.defineAccessor(
'with-icon',
/** @param {unknown} value */ (value) => {
this.set$({ withIcon: asBoolean(value) });
}
);
this.defineAccessor(
'fullscreen',
/** @param {unknown} value */ (value) => {
this.set$({ isFullscreen: asBoolean(value) });
}
);

this.defineAccessor(
'text',
/** @param {unknown} value */ (value) => {
if (typeof value === 'string') {
this.set$({ text: this.l10n(value) || value });
} else {
this.set$({ text: this.l10n('drop-files-here') });
}
}
);

/** @private */
this._destroyDropzone = addDropzone({
element: this,
shouldIgnore: () => this._shouldIgnore(),
/** @param {DropzoneState} state */
onChange: (state) => {
this.$.state = state;
},
/** @param {(File | String)[]} items */
/** @param {import('./getDropItems.js').DropItem[]} items */
onItems: (items) => {
if (!items.length) {
return;
}

items.forEach((/** @type {File | String} */ item) => {
if (typeof item === 'string') {
this.addFileFromUrl(item, { source: UploadSource.DROP_AREA });
return;
items.forEach((/** @type {import('./getDropItems.js').DropItem} */ item) => {
if (item.type === 'url') {
this.addFileFromUrl(item.url, { source: UploadSource.DROP_AREA });
} else if (item.type === 'file') {
this.addFileFromObject(item.file, { source: UploadSource.DROP_AREA, fullPath: item.fullPath });
}
this.addFileFromObject(item, { source: UploadSource.DROP_AREA });
});
if (this.uploadCollection.size) {
this.set$({
Expand All @@ -98,6 +120,7 @@ export class DropArea extends UploaderBlock {
if (contentWrapperEl) {
this._destroyContentWrapperDropzone = addDropzone({
element: contentWrapperEl,
/** @param {DropzoneState} state */
onChange: (state) => {
const stateText = Object.entries(DropzoneState)
.find(([, value]) => value === state)?.[0]
Expand Down Expand Up @@ -206,9 +229,14 @@ DropArea.template = /* HTML */ `
`;

DropArea.bindAttributes({
// @ts-expect-error TODO: fix types inside symbiote
'with-icon': null,
// @ts-expect-error TODO: fix types inside symbiote
clickable: null,
// @ts-expect-error TODO: fix types inside symbiote
text: null,
// @ts-expect-error TODO: fix types inside symbiote
fullscreen: null,
// @ts-expect-error TODO: fix types inside symbiote
disabled: null,
});
87 changes: 62 additions & 25 deletions blocks/DropArea/getDropItems.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// @ts-check

/**
* @typedef {| {
* type: 'file';
* file: File;
* fullPath?: string;
* }
* | {
* type: 'url';
* url: string;
* }} DropItem
*/

/**
* @param {File} file
* @returns {Promise<boolean>}
Expand All @@ -13,6 +27,7 @@ function checkIsDirectory(file) {
reader.onerror = () => {
resolve(true);
};
/** @param {Event} e */
let onLoad = (e) => {
if (e.type !== 'loadend') {
reader.abort();
Expand All @@ -29,33 +44,45 @@ function checkIsDirectory(file) {
});
}

/**
* @param {FileSystemEntry} webkitEntry
* @param {string} dataTransferItemType
* @returns {Promise<DropItem[] | null>}
*/
function readEntryContentAsync(webkitEntry, dataTransferItemType) {
return new Promise((resolve) => {
let reading = 0;
let contents = [];
/** @type {DropItem[]} */
const dropItems = [];

let readEntry = (entry) => {
/** @param {FileSystemEntry} entry */
const readEntry = (entry) => {
if (!entry) {
console.warn('Unexpectedly received empty content entry', { scope: 'drag-and-drop' });
resolve(null);
}
if (entry.isFile) {
reading++;
entry.file((file) => {
/** @type {FileSystemFileEntry} */ (entry).file((file) => {
reading--;
// webkitGetAsEntry don't provide type for HEIC images at least, so we use type value from dataTransferItem
const clonedFile = new File([file], file.name, { type: file.type || dataTransferItemType });
contents.push(clonedFile);
dropItems.push({
type: 'file',
file: clonedFile,
fullPath: entry.fullPath,
});

if (reading === 0) {
resolve(contents);
resolve(dropItems);
}
});
} else if (entry.isDirectory) {
readReaderContent(entry.createReader());
readReaderContent(/** @type {FileSystemDirectoryEntry} */ (entry).createReader());
}
};

/** @param {FileSystemDirectoryReader} reader */
let readReaderContent = (reader) => {
reading++;

Expand All @@ -66,7 +93,7 @@ function readEntryContentAsync(webkitEntry, dataTransferItemType) {
}

if (reading === 0) {
resolve(contents);
resolve(dropItems);
}
});
};
Expand All @@ -79,11 +106,12 @@ function readEntryContentAsync(webkitEntry, dataTransferItemType) {
* Note: dataTransfer will be destroyed outside of the call stack. So, do not try to process it asynchronous.
*
* @param {DataTransfer} dataTransfer
* @returns {Promise<(File | String)[]>}
* @returns {Promise<DropItem[]>}
*/
export function getDropItems(dataTransfer) {
let files = [];
let promises = [];
/** @type {DropItem[]} */
const dropItems = [];
const promises = [];
for (let i = 0; i < dataTransfer.items.length; i++) {
let item = dataTransfer.items[i];
if (!item) {
Expand All @@ -97,34 +125,43 @@ export function getDropItems(dataTransfer) {
? item.webkitGetAsEntry()
: /** @type {any} */ (item).getAsEntry();
promises.push(
readEntryContentAsync(entry, itemType).then((entryContent) => {
files.push(...entryContent);
readEntryContentAsync(entry, itemType).then((items) => {
if (items) {
dropItems.push(...items);
}
})
);
continue;
}

let file = item.getAsFile();
promises.push(
checkIsDirectory(file).then((isDirectory) => {
if (isDirectory) {
// we can't get directory files, so we'll skip it
} else {
files.push(file);
}
})
);
const file = item.getAsFile();
file &&
promises.push(
checkIsDirectory(file).then((isDirectory) => {
if (isDirectory) {
// we can't get directory files, so we'll skip it
} else {
dropItems.push({
type: 'file',
file,
});
}
})
);
} else if (item.kind === 'string' && item.type.match('^text/uri-list')) {
promises.push(
new Promise((resolve) => {
item.getAsString((value) => {
files.push(value);
resolve();
dropItems.push({
type: 'url',
url: value,
});
resolve(undefined);
});
})
);
}
}

return Promise.all(promises).then(() => files);
return Promise.all(promises).then(() => dropItems);
}

0 comments on commit 3ba168e

Please sign in to comment.