mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-16 02:29:50 +00:00
* Merging file drop * Fixing double drop
This commit is contained in:
committed by
Jake Archibald
parent
3035a68b90
commit
7c220b1a92
116
src/components/app/custom-els/FileDrop/index.ts
Normal file
116
src/components/app/custom-els/FileDrop/index.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { bind } from '../../../../lib/util';
|
||||
import './styles.css';
|
||||
|
||||
function firstMatchingItem(list: DataTransferItemList, acceptVal: string): DataTransferItem | undefined {
|
||||
// Split accepts values by ',' then by '/'. Trim everything & lowercase.
|
||||
const accepts = acceptVal.toLowerCase().split(',').map(accept => {
|
||||
return accept.trim().split('/').map(part => part.trim());
|
||||
}).filter(acceptParts => acceptParts.length === 2); // Filter invalid values
|
||||
|
||||
return Array.from(list).find(item => {
|
||||
if (item.kind !== 'file') return false;
|
||||
|
||||
// 'Parse' the type.
|
||||
const [typeMain, typeSub] = item.type.toLowerCase().split('/').map(s => s.trim());
|
||||
|
||||
for (const [acceptMain, acceptSub] of accepts) {
|
||||
// Look for an exact match, or a partial match if * is accepted, eg image/*.
|
||||
if (typeMain === acceptMain && (acceptSub === '*' || typeSub === acceptSub)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
interface FileDropEventInit extends EventInit {
|
||||
file: File;
|
||||
}
|
||||
|
||||
export class FileDropEvent extends Event {
|
||||
private _file: File;
|
||||
constructor(typeArg: string, eventInitDict: FileDropEventInit) {
|
||||
super(typeArg, eventInitDict);
|
||||
this._file = eventInitDict.file;
|
||||
}
|
||||
|
||||
get file(): File {
|
||||
return this._file;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Example Usage.
|
||||
<file-drop
|
||||
accept='image/*'
|
||||
class='drop-valid|drop-invalid'
|
||||
>
|
||||
[everything in here is a drop target.]
|
||||
</file-drop>
|
||||
|
||||
dropElement.addEventListner('dropfile', (event) => console.log(event.detail))
|
||||
*/
|
||||
export class FileDrop extends HTMLElement {
|
||||
|
||||
private _dragEnterCount = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener('dragover', (event) => event.preventDefault());
|
||||
this.addEventListener('drop', this._onDrop);
|
||||
this.addEventListener('dragenter', this._onDragEnter);
|
||||
this.addEventListener('dragend', () => this._reset());
|
||||
this.addEventListener('dragleave', this._onDragLeave);
|
||||
}
|
||||
|
||||
get accept() {
|
||||
return this.getAttribute('accept') || '';
|
||||
}
|
||||
|
||||
set accept(val: string) {
|
||||
this.setAttribute('accept', val);
|
||||
}
|
||||
|
||||
@bind
|
||||
private _onDragEnter(event: DragEvent) {
|
||||
this._dragEnterCount++;
|
||||
if (this._dragEnterCount > 1) return;
|
||||
|
||||
// We don't have data, attempt to get it and if it matches, set the correct state.
|
||||
const dragDataItem = firstMatchingItem(event.dataTransfer.items, this.accept);
|
||||
if (dragDataItem) {
|
||||
this.classList.add('drop-valid');
|
||||
} else {
|
||||
this.classList.add('drop-invalid');
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
private _onDragLeave() {
|
||||
this._dragEnterCount--;
|
||||
if (this._dragEnterCount === 0) {
|
||||
this._reset();
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
private _onDrop(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
this._reset();
|
||||
const dragDataItem = firstMatchingItem(event.dataTransfer.items, this.accept);
|
||||
if (!dragDataItem) return;
|
||||
|
||||
const file = dragDataItem.getAsFile();
|
||||
if (file === null) return;
|
||||
|
||||
this.dispatchEvent(new FileDropEvent('filedrop', { file }));
|
||||
}
|
||||
|
||||
private _reset() {
|
||||
this._dragEnterCount = 0;
|
||||
this.classList.remove('drop-valid');
|
||||
this.classList.remove('drop-invalid');
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('file-drop', FileDrop);
|
||||
20
src/components/app/custom-els/FileDrop/missing-types.d.ts
vendored
Normal file
20
src/components/app/custom-els/FileDrop/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
import { FileDropEvent, FileDrop } from ".";
|
||||
|
||||
|
||||
declare global {
|
||||
|
||||
interface HTMLElementEventMap {
|
||||
'filedrop': FileDropEvent;
|
||||
}
|
||||
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
'file-drop': FileDropAttributes;
|
||||
}
|
||||
|
||||
interface FileDropAttributes extends HTMLAttributes {
|
||||
accept?: string;
|
||||
onfiledrop?: ((this: FileDrop, ev: FileDropEvent) => any) | null;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
src/components/app/custom-els/FileDrop/styles.css
Normal file
3
src/components/app/custom-els/FileDrop/styles.css
Normal file
@@ -0,0 +1,3 @@
|
||||
file-drop {
|
||||
display: block;
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import { bind, bitmapToImageData } from '../../lib/util';
|
||||
import * as style from './style.scss';
|
||||
import Output from '../output';
|
||||
import Options from '../options';
|
||||
import { FileDropEvent } from './custom-els/FileDrop';
|
||||
import './custom-els/FileDrop';
|
||||
|
||||
import * as mozJPEG from '../../codecs/mozjpeg/encoder';
|
||||
import * as identity from '../../codecs/identity/encoder';
|
||||
@@ -104,6 +106,17 @@ export default class App extends Component<Props, State> {
|
||||
const fileInput = event.target as HTMLInputElement;
|
||||
const file = fileInput.files && fileInput.files[0];
|
||||
if (!file) return;
|
||||
await this.updateFile(file);
|
||||
}
|
||||
|
||||
@bind
|
||||
async onFileDrop(event: FileDropEvent) {
|
||||
const { file } = event;
|
||||
if (!file) return;
|
||||
await this.updateFile(file);
|
||||
}
|
||||
|
||||
async updateFile(file: File) {
|
||||
this.setState({ loading: true });
|
||||
try {
|
||||
const bmp = await createImageBitmap(file);
|
||||
@@ -112,7 +125,7 @@ export default class App extends Component<Props, State> {
|
||||
this.setState({
|
||||
source: { data, bmp, file },
|
||||
error: undefined,
|
||||
loading: false
|
||||
loading: false,
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({ error: 'IMAGE_INVALID', loading: false });
|
||||
@@ -173,29 +186,33 @@ export default class App extends Component<Props, State> {
|
||||
loading = loading || images.some(image => image.loading);
|
||||
|
||||
return (
|
||||
<div id="app" class={style.app}>
|
||||
{(leftImageBmp && rightImageBmp) ? (
|
||||
<Output leftImg={leftImageBmp} rightImg={rightImageBmp} />
|
||||
) : (
|
||||
<div class={style.welcome}>
|
||||
<h1>Select an image</h1>
|
||||
<input type="file" onChange={this.onFileChange} />
|
||||
</div>
|
||||
)}
|
||||
{images.map((image, index) => (
|
||||
<span class={index ? style.rightLabel : style.leftLabel}>{encoderMap[image.encoderState.type].label}</span>
|
||||
))}
|
||||
{images.map((image, index) => (
|
||||
<Options
|
||||
class={index ? style.rightOptions : style.leftOptions}
|
||||
encoderState={image.encoderState}
|
||||
onTypeChange={this.onEncoderChange.bind(this, index)}
|
||||
onOptionsChange={this.onOptionsChange.bind(this, index)}
|
||||
/>
|
||||
))}
|
||||
{loading && <span style={{ position: 'fixed', top: 0, left: 0 }}>Loading...</span>}
|
||||
{error && <span style={{ position: 'fixed', top: 0, left: 0 }}>Error: {error}</span>}
|
||||
</div>
|
||||
<file-drop accept="image/*" onfiledrop={this.onFileDrop}>
|
||||
<div id="app" class={style.app}>
|
||||
{(leftImageBmp && rightImageBmp) ? (
|
||||
<Output leftImg={leftImageBmp} rightImg={rightImageBmp} />
|
||||
) : (
|
||||
<div class={style.welcome}>
|
||||
<h1>Select an image</h1>
|
||||
<input type="file" onChange={this.onFileChange} />
|
||||
</div>
|
||||
)}
|
||||
{images.map((image, index) => (
|
||||
<span class={index ? style.rightLabel : style.leftLabel}>
|
||||
{encoderMap[image.encoderState.type].label}
|
||||
</span>
|
||||
))}
|
||||
{images.map((image, index) => (
|
||||
<Options
|
||||
class={index ? style.rightOptions : style.leftOptions}
|
||||
encoderState={image.encoderState}
|
||||
onTypeChange={this.onEncoderChange.bind(this, index)}
|
||||
onOptionsChange={this.onOptionsChange.bind(this, index)}
|
||||
/>
|
||||
))}
|
||||
{loading && <span style={{ position: 'fixed', top: 0, left: 0 }}>Loading...</span>}
|
||||
{error && <span style={{ position: 'fixed', top: 0, left: 0 }}>Error: {error}</span>}
|
||||
</div>
|
||||
</file-drop>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ Note: These styles are temporary. They will be replaced before going live.
|
||||
font-size: 150%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
input {
|
||||
display: inline-block;
|
||||
width: 16em;
|
||||
@@ -60,3 +60,24 @@ Note: These styles are temporary. They will be replaced before going live.
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
file-drop {
|
||||
overflow: hidden;
|
||||
touch-action: none;
|
||||
height:100%;
|
||||
width:100%;
|
||||
|
||||
&.drop-valid {
|
||||
transition: opacity 200ms ease-in-out, background-color 200ms;
|
||||
opacity: 0.5;
|
||||
background-color:green;
|
||||
}
|
||||
|
||||
&.drop-invalid {
|
||||
transition: opacity 200ms ease-in-out, background-color 200ms;
|
||||
opacity: 0.5;
|
||||
background-color:red;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,12 +30,12 @@ export default class Output extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps({ leftImg, rightImg }: Props) {
|
||||
if (leftImg !== this.props.leftImg && this.canvasLeft) {
|
||||
drawBitmapToCanvas(this.canvasLeft, leftImg);
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.leftImg !== this.props.leftImg && this.canvasLeft) {
|
||||
drawBitmapToCanvas(this.canvasLeft, this.props.leftImg);
|
||||
}
|
||||
if (rightImg !== this.props.rightImg && this.canvasRight) {
|
||||
drawBitmapToCanvas(this.canvasRight, rightImg);
|
||||
if (prevProps.rightImg !== this.props.rightImg && this.canvasRight) {
|
||||
drawBitmapToCanvas(this.canvasRight, this.props.rightImg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user