Fix lint issues resulting from switching to airbnb (#94)

* Fix lint issues resulting from switching to airbnb.

* Case sensitivity change

* Fix lint script to actually lint tsx files
This commit is contained in:
Jason Miller
2018-07-10 09:01:09 -04:00
committed by Jake Archibald
parent 23ea9fad49
commit 835a537c55
19 changed files with 43 additions and 13 deletions

View File

@@ -0,0 +1,129 @@
import { bind } from '../../../../lib/util';
import './styles.css';
// tslint:disable-next-line:max-line-length
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;
}
// Safari and Edge don't quite support extending Event, this works around it.
function fixExtendedEvent(instance: Event, type: Function) {
if (!(instance instanceof type)) {
Object.setPrototypeOf(instance, type.prototype);
}
}
export class FileDropEvent extends Event {
private _file: File;
constructor(typeArg: string, eventInitDict: FileDropEventInit) {
super(typeArg, eventInitDict);
fixExtendedEvent(this, FileDropEvent);
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 += 1;
if (this._dragEnterCount > 1) return;
// We don't have data, attempt to get it and if it matches, set the correct state.
const validDrop: boolean = event.dataTransfer.items.length ?
!!firstMatchingItem(event.dataTransfer.items, this.accept) :
// Safari doesn't give file information on drag enter, so the best we can do is return valid.
true;
if (validDrop) {
this.classList.add('drop-valid');
} else {
this.classList.add('drop-invalid');
}
}
@bind
private _onDragLeave() {
this._dragEnterCount -= 1;
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);

View File

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

View File

@@ -0,0 +1,3 @@
file-drop {
display: block;
}

View File

@@ -0,0 +1,277 @@
import { h, Component } from 'preact';
import { partial } from 'filesize';
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';
import * as browserPNG from '../../codecs/browser-png/encoder';
import * as browserJPEG from '../../codecs/browser-jpeg/encoder';
import * as browserWebP from '../../codecs/browser-webp/encoder';
import * as browserGIF from '../../codecs/browser-gif/encoder';
import * as browserTIFF from '../../codecs/browser-tiff/encoder';
import * as browserJP2 from '../../codecs/browser-jp2/encoder';
import * as browserBMP from '../../codecs/browser-bmp/encoder';
import * as browserPDF from '../../codecs/browser-pdf/encoder';
import {
EncoderState,
EncoderType,
EncoderOptions,
encoderMap,
} from '../../codecs/encoders';
interface SourceImage {
file: File;
bmp: ImageBitmap;
data: ImageData;
}
interface EncodedImage {
bmp?: ImageBitmap;
file?: File;
downloadUrl?: string;
encoderState: EncoderState;
loading: boolean;
/** Counter of the latest bmp currently encoding */
loadingCounter: number;
/** Counter of the latest bmp encoded */
loadedCounter: number;
}
interface Props {}
interface State {
source?: SourceImage;
images: [EncodedImage, EncodedImage];
loading: boolean;
error?: string;
}
const filesize = partial({});
async function compressImage(
source: SourceImage,
encodeData: EncoderState,
): Promise<File> {
// Special case for identity
if (encodeData.type === identity.type) return source.file;
const compressedData = await (() => {
switch (encodeData.type) {
case mozJPEG.type: return mozJPEG.encode(source.data, encodeData.options);
case browserPNG.type: return browserPNG.encode(source.data, encodeData.options);
case browserJPEG.type: return browserJPEG.encode(source.data, encodeData.options);
case browserWebP.type: return browserWebP.encode(source.data, encodeData.options);
case browserGIF.type: return browserGIF.encode(source.data, encodeData.options);
case browserTIFF.type: return browserTIFF.encode(source.data, encodeData.options);
case browserJP2.type: return browserJP2.encode(source.data, encodeData.options);
case browserBMP.type: return browserBMP.encode(source.data, encodeData.options);
case browserPDF.type: return browserPDF.encode(source.data, encodeData.options);
default: throw Error(`Unexpected encoder ${JSON.stringify(encodeData)}`);
}
})();
const encoder = encoderMap[encodeData.type];
return new File(
[compressedData],
source.file.name.replace(/\..+$/, '.' + encoder.extension),
{ type: encoder.mimeType },
);
}
export default class App extends Component<Props, State> {
state: State = {
loading: false,
images: [
{
encoderState: { type: identity.type, options: identity.defaultOptions },
loadingCounter: 0,
loadedCounter: 0,
loading: false,
},
{
encoderState: { type: mozJPEG.type, options: mozJPEG.defaultOptions },
loadingCounter: 0,
loadedCounter: 0,
loading: false,
},
],
};
constructor() {
super();
// In development, persist application state across hot reloads:
if (process.env.NODE_ENV === 'development') {
this.setState(window.STATE);
const oldCDU = this.componentDidUpdate;
this.componentDidUpdate = (props, state) => {
if (oldCDU) oldCDU.call(this, props, state);
window.STATE = this.state;
};
}
}
onEncoderChange(index: 0 | 1, type: EncoderType, options?: EncoderOptions): void {
const images = this.state.images.slice() as [EncodedImage, EncodedImage];
const oldImage = images[index];
// Some type cheating here.
// encoderMap[type].defaultOptions is always safe.
// options should always be correct for the type, but TypeScript isn't smart enough.
const encoderState: EncoderState = {
type,
options: options ? options : encoderMap[type].defaultOptions,
} as EncoderState;
images[index] = {
...oldImage,
encoderState,
};
this.setState({ images });
}
onOptionsChange(index: 0 | 1, options: EncoderOptions): void {
this.onEncoderChange(index, this.state.images[index].encoderState.type, options);
}
componentDidUpdate(prevProps: Props, prevState: State): void {
const { source, images } = this.state;
for (const [i, image] of images.entries()) {
const prevImage = prevState.images[i];
// The image only needs updated if the encoder settings have changed, or the source has
// changed.
if (source !== prevState.source || image.encoderState !== prevImage.encoderState) {
if (prevImage.downloadUrl) URL.revokeObjectURL(prevImage.downloadUrl);
this.updateImage(i);
}
}
}
@bind
async onFileChange(event: Event): Promise<void> {
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);
// compute the corresponding ImageData once since it only changes when the file changes:
const data = await bitmapToImageData(bmp);
this.setState({
source: { data, bmp, file },
error: undefined,
loading: false,
});
} catch (err) {
this.setState({ error: 'IMAGE_INVALID', loading: false });
}
}
async updateImage(index: number): Promise<void> {
const { source } = this.state;
if (!source) return;
let images = this.state.images.slice() as [EncodedImage, EncodedImage];
// Each time we trigger an async encode, the counter changes.
const loadingCounter = images[index].loadingCounter + 1;
const image = images[index] = {
...images[index],
loadingCounter,
loading: true,
};
this.setState({ images });
let file;
try {
file = await compressImage(source, image.encoderState);
} catch (err) {
this.setState({ error: `Encoding error (type=${image.encoderState.type}): ${err}` });
throw err;
}
const latestImage = this.state.images[index];
// If a later encode has landed before this one, return.
if (loadingCounter < latestImage.loadedCounter) {
this.setState({ error: '' });
return;
}
const bmp = await createImageBitmap(file);
images = this.state.images.slice() as [EncodedImage, EncodedImage];
images[index] = {
...images[index],
file,
bmp,
downloadUrl: URL.createObjectURL(file),
loading: images[index].loadingCounter !== loadingCounter,
loadedCounter: loadingCounter,
};
this.setState({ images, error: '' });
}
render({ }: Props, { loading, error, images }: State) {
const [leftImageBmp, rightImageBmp] = images.map(i => i.bmp);
const anyLoading = loading || images.some(image => image.loading);
return (
<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}
{(image.downloadUrl && image.file) && (
<a href={image.downloadUrl} download={image.file.name}>🔻</a>
)}
{image.file && ` - ${filesize(image.file.size)}`}
</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)}
/>
))}
{anyLoading && <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>
);
}
}

View File

@@ -0,0 +1,83 @@
/*
Note: These styles are temporary. They will be replaced before going live.
*/
.app {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
contain: strict;
.leftLabel,
.rightLabel {
position: fixed;
bottom: 0;
padding: 5px 10px;
background: rgba(0,0,0,0.5);
color: #fff;
}
.leftLabel { left: 0; }
.rightLabel { right: 0; }
.leftOptions,
.rightOptions {
position: fixed;
bottom: 40px;
}
.leftOptions { left: 10px; }
.rightOptions { right: 10px; }
}
.welcome {
position: absolute;
display: inline-block;
left: 50%;
top: 50%;
padding: 20px;
transform: translate(-50%, -50%);
h1 {
font-weight: inherit;
font-size: 150%;
text-align: center;
}
input {
display: inline-block;
width: 16em;
padding: 5px;
margin: 0 auto;
-webkit-appearance: none;
border: 1px solid #b68c86;
background: #f0d3cf;
box-shadow: inset 0 0 1px #fff;
border-radius: 3px;
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;
}
}
}