Skip to content

Commit

Permalink
Merge pull request #64 from LuchoTurtle/responsive-and-ios-fix
Browse files Browse the repository at this point in the history
[PR] Refactoring and fixing iOS Safari issue
  • Loading branch information
LuchoTurtle authored May 9, 2024
2 parents 548e114 + 890b78e commit 50df977
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 183 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tweakpane-plugin-file-import",
"version": "1.0.1",
"version": "1.0.2",
"description": "A general-purpose and simple file input Tweakpane plugin",
"homepage": "https://github.com/LuchoTurtle/tweakpane-plugin-file-import",
"main": "dist/tweakpane-plugin-file-import.js",
Expand Down
248 changes: 174 additions & 74 deletions src/controller/controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import {
Controller,
PointerHandler,
PointerHandlerEvent,
Value,
ViewProps,
} from '@tweakpane/core';
import {Controller, Value, ViewProps} from '@tweakpane/core';

import {FilePluginView} from '../view/view.js';

Expand All @@ -18,101 +12,207 @@ interface Config {

export class FilePluginController implements Controller<FilePluginView> {
public readonly value: Value<File | null>;
private readonly filetypes?: string[];

public readonly view: FilePluginView;
public readonly viewProps: ViewProps;

constructor(doc: Document, config: Config) {
// Binding click event handlers
this.onContainerClick_ = this.onContainerClick_.bind(this);
this.onButtonClick_ = this.onButtonClick_.bind(this);
private readonly config: Config;

// Receive the bound value from the plugin
constructor(doc: Document, config: Config) {
this.value = config.value;

// Get filetypes
this.filetypes = config.filetypes;

// and also view props
this.viewProps = config.viewProps;
this.viewProps.handleDispose(() => {
// Called when the controller is disposing
});

// Create a custom view
this.view = new FilePluginView(doc, {
value: this.value,
viewProps: this.viewProps,
value: config.value,
lineCount: config.lineCount,
filetypes: config.filetypes,
});
this.config = config;

// Bind event handlers
this.onFile = this.onFile.bind(this);
this.onDrop = this.onDrop.bind(this);
this.onDragOver = this.onDragOver.bind(this);
this.onDragLeave = this.onDragLeave.bind(this);
this.onDeleteClick = this.onDeleteClick.bind(this);

this.view.input.addEventListener('change', this.onFile);
this.view.element.addEventListener('drop', this.onDrop);
this.view.element.addEventListener('dragover', this.onDragOver);
this.view.element.addEventListener('dragleave', this.onDragLeave);
this.view.deleteButton.addEventListener('click', this.onDeleteClick);

// You can use `PointerHandler` to handle pointer events in the same way as Tweakpane do
const containerPtHandler = new PointerHandler(this.view.container);
containerPtHandler.emitter.on('down', this.onContainerClick_);
this.value.emitter.on('change', () => this.handleValueChange());

const buttonPtHandler = new PointerHandler(this.view.button);
buttonPtHandler.emitter.on('down', this.onButtonClick_);
// Dispose event handlers
this.viewProps.handleDispose(() => {
this.view.input.removeEventListener('change', this.onFile);
this.view.element.removeEventListener('drop', this.onDrop);
this.view.element.removeEventListener('dragover', this.onDragOver);
this.view.element.removeEventListener('dragleave', this.onDragLeave);
this.view.deleteButton.removeEventListener('click', this.onDeleteClick);
});
}

/**
* Event handler when the container HTML element is clicked.
* It checks if the filetype is valid and if the user has chosen a file.
* If the file is valid, the `rawValue` of the controller is set.
* @param ev Pointer event.
* Called when the value of the input changes.
* @param event change event.
*/
private onContainerClick_(_ev: PointerHandlerEvent) {
// Accepted filetypes
const filetypes = this.filetypes;

// Creates hidden `input` and mimicks click to open file explorer
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.style.opacity = '0';
input.style.position = 'fixed';
input.style.pointerEvents = 'none';
document.body.appendChild(input);

// Adds event listener when user chooses file
input.addEventListener(
'input',
(_ev) => {
// Check if user has chosen a file
if (input.files && input.files.length > 0) {
const file = input.files[0];
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();

// Check if filetype is allowed
if (
filetypes &&
filetypes.length > 0 &&
!filetypes.includes(fileExtension) &&
fileExtension
) {
return;
} else {
this.value.setRawValue(file);
}
}
},
{once: true},
);
private onFile(_event: Event): void {
const input = this.view.input;

// Check if user has chosen a file.
// If it's valid, we update the value. Otherwise, show warning.
if (input.files && input.files.length > 0) {
const file = input.files[0];
if (!this.isFileValid(file)) {
this.showWarning();
} else {
this.value.setRawValue(file);
}
}
}

// Click hidden input to open file explorer and remove it
input.click();
document.body.removeChild(input);
/**
* Shows warning text for 5 seconds.
*/
private showWarning() {
this.view.warning.style.display = 'block';
this.view.warning.innerHTML = 'Unaccepted file type.';
setTimeout(() => {
// Resetting warning text
this.view.warning.innerHTML = '';
this.view.warning.style.display = 'none';
}, 5000);
}

/**
* Checks if the file is valid with the given filetypes.
* @param file File object
* @returns true if the file is valid.
*/
private isFileValid(file: File): boolean {
const filetypes = this.config.filetypes;
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
return !(
filetypes &&
filetypes.length > 0 &&
!filetypes.includes(fileExtension) &&
fileExtension
);
}

/**
* Event handler when the delete HTML button is clicked.
* It resets the `rawValue` of the controller.
* @param ev Pointer event.
*/
private onButtonClick_(_ev: PointerHandlerEvent) {
private onDeleteClick() {
const file = this.value.rawValue;

if (file) {
// Resetting the value
this.value.setRawValue(null);

// Resetting the input
this.view.input.value = '';

// Resetting the warning text
this.view.warning.innerHTML = '';
this.view.warning.style.display = 'none';
}
}

/**
* Called when the user drags over a file.
* Updates the style of the container.
* @param event drag event.
*/
private onDragOver(event: Event) {
event.preventDefault();
this.view.changeDraggingState(true);
}

/**
* Called when the user leaves the container while dragging.
* Updates the style of the container.
*/
private onDragLeave() {
this.view.changeDraggingState(false);
}

/**
* Called when the user drops a file in the container.
* Either shows a warning if it's invalid or updates the value if it's valid.
* @param ev drag event.
*/
private onDrop(ev: DragEvent) {
if (ev instanceof DragEvent) {
// Prevent default behavior (Prevent file from being opened)
ev.preventDefault();

if (ev.dataTransfer) {
if (ev.dataTransfer.files) {
// We only change the value if the user has dropped a single file
const filesArray = [ev.dataTransfer.files][0];
if (filesArray.length == 1) {
const file = filesArray.item(0);
if (file) {
if (!this.isFileValid(file)) {
this.showWarning();
} else {
this.value.setRawValue(file);
}
}
}
}
}
}
this.view.changeDraggingState(false);
}

/**
* Called when the value (bound to the controller) changes (e.g. when the file is selected).
*/
private handleValueChange() {
const fileObj = this.value.rawValue;

const containerEl = this.view.container;
const textEl = this.view.text;
const fileIconEl = this.view.fileIcon;
const deleteButton = this.view.deleteButton;

if (fileObj) {
// Setting the text of the file to the element
textEl.textContent = fileObj.name;

// Removing icon and adding text
containerEl.appendChild(textEl);
if (containerEl.contains(fileIconEl)) {
containerEl.removeChild(fileIconEl);
}

// Resetting warning text
this.view.warning.innerHTML = '';
this.view.warning.style.display = 'none';

// Adding button to delete
deleteButton.style.display = 'block';
containerEl.style.border = 'unset';
} else {
// Setting the text of the file to the element
textEl.textContent = '';

// Removing text and adding icon
containerEl.appendChild(fileIconEl);
containerEl.removeChild(textEl);

// Resetting warning text
this.view.warning.innerHTML = '';
this.view.warning.style.display = 'none';

// Hiding button and resetting border
deleteButton.style.display = 'none';
containerEl.style.border = '1px dashed #717070';
}
}
}
32 changes: 31 additions & 1 deletion src/sass/plugin.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
overflow: hidden;
position: relative;

height: 100%;
width: 100%;

border: 1px dashed #717070;
-webkit-border-radius: 5px;
border-radius: 5px;
Expand All @@ -21,6 +24,28 @@
opacity: 0.5;
}

&_input_area_dragging {
border: 1px dashed rgb(103 116 255);
background-color: #5858b93b;
}

&_warning {
color: tp.cssVar('input-fg');
bottom: 2px;
display: inline-block;
font-size: 0.9em;
height: max-content;
line-height: 0.9;
margin: 0.2rem;
opacity: 0.5;
white-space: normal;
width: max-content;
word-wrap: break-word;
text-align: right;
width: 100%;
margin-top: var(--cnt-vp);
}

&_text {
// You can use CSS variables for styling. See declarations for details:
// ../../node_modules/@tweakpane/core/lib/sass/common/_defs.scss
Expand Down Expand Up @@ -57,7 +82,7 @@
// https://css.gg/software-upload
&_icon {
box-sizing: border-box;
position: relative;
position: absolute;
display: block;
transform: scale(var(--ggs, 1));
width: 16px;
Expand Down Expand Up @@ -102,3 +127,8 @@
.#{tp.$prefix}-btnv_b {
margin-top: 10px;
}

// Styles for the input
.#{tp.$prefix}-inputv {
opacity: 0;
}
Loading

0 comments on commit 50df977

Please sign in to comment.