Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: capture and store file's full path while getting drag'n'dropped #536

Merged
merged 1 commit into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions abstract/UploaderBlock.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { Data } from '@symbiotejs/symbiote';
import { calculateMaxCenteredCropFrame } from '../blocks/CloudImageEditor/src/crop-utils.js';
import { parseCropPreset } from '../blocks/CloudImageEditor/src/lib/parseCropPreset.js';
import { Modal } from '../blocks/Modal/Modal.js';

Check warning on line 7 in abstract/UploaderBlock.js

View workflow job for this annotation

GitHub Actions / build

'Modal' is defined but never used
import { UploadSource } from '../blocks/utils/UploadSource.js';
import { debounce } from '../blocks/utils/debounce.js';
import { customUserAgent } from '../blocks/utils/userAgent.js';
Expand Down Expand Up @@ -165,9 +165,9 @@

/**
* @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 @@
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);
}
Loading