UNPKG

@kitschpatrol/tweakpane-plugin-file-import

Version:

A fork of tweakpane-plugin-file-import with build optimizations.

297 lines (291 loc) 13.7 kB
import { ClassName, createPlugin, CompositeConstraint, parseRecord } from '@tweakpane/core'; // Create a class name generator from the view name // ClassName('tmp') will generate a CSS class name like `tp-tmpv` const containerClassName = ClassName('ctn'); const inputClassName = ClassName('input'); const deleteButtonClassName = ClassName('btn'); class FilePluginView { constructor(doc, config) { // Root this.element = doc.createElement('div'); // Container this.container = doc.createElement('div'); this.container.classList.add(containerClassName()); config.viewProps.bindClassModifiers(this.container); // File input field this.input = doc.createElement('input'); this.input.classList.add(inputClassName()); this.input.setAttribute('type', 'file'); this.input.setAttribute('accept', config.filetypes ? config.filetypes.join(',') : '*'); this.input.style.height = `calc(20px * ${config.lineCount})`; // Icon this.fileIcon = doc.createElement('div'); this.fileIcon.classList.add(containerClassName('icon')); // Text this.text = doc.createElement('span'); this.text.classList.add(containerClassName('text')); // Warning text this.warning = doc.createElement('span'); this.warning.classList.add(containerClassName('warning')); this.warning.innerHTML = config.invalidFiletypeMessage; this.warning.style.display = 'none'; // Delete button this.deleteButton = doc.createElement('button'); this.deleteButton.classList.add(deleteButtonClassName('b')); this.deleteButton.innerHTML = 'Delete'; this.deleteButton.style.display = 'none'; this.container.appendChild(this.input); this.container.appendChild(this.fileIcon); this.element.appendChild(this.container); this.element.appendChild(this.warning); this.element.appendChild(this.deleteButton); } /** * Changes the style of the container based on whether the user is dragging or not. * @param state if the user is dragging or not. */ changeDraggingState(state) { var _a, _b; if (state) { (_a = this.container) === null || _a === void 0 ? void 0 : _a.classList.add(containerClassName('input_area_dragging')); } else { (_b = this.container) === null || _b === void 0 ? void 0 : _b.classList.remove(containerClassName('input_area_dragging')); } } } class FilePluginController { constructor(doc, config) { this.value = config.value; this.viewProps = config.viewProps; this.view = new FilePluginView(doc, { viewProps: this.viewProps, value: config.value, invalidFiletypeMessage: config.invalidFiletypeMessage, 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); this.value.emitter.on('change', () => this.handleValueChange()); // 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); }); } /** * Called when the value of the input changes. * @param event change event. */ onFile(_event) { 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); } } } /** * Shows warning text for 5 seconds. */ showWarning() { this.view.warning.style.display = 'block'; setTimeout(() => { // Resetting warning text 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. */ isFileValid(file) { var _a; const filetypes = this.config.filetypes; const fileExtension = '.' + ((_a = file.name.split('.').pop()) === null || _a === void 0 ? void 0 : _a.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. */ 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.style.display = 'none'; } } /** * Called when the user drags over a file. * Updates the style of the container. * @param event drag event. */ onDragOver(event) { event.preventDefault(); this.view.changeDraggingState(true); } /** * Called when the user leaves the container while dragging. * Updates the style of the container. */ 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. */ onDrop(ev) { 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). */ 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.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.style.display = 'none'; // Hiding button and resetting border deleteButton.style.display = 'none'; containerEl.style.border = '1px dashed #717070'; } } } const TweakpaneFileInputPlugin = createPlugin({ id: 'file-input', // type: The plugin type. type: 'input', accept(exValue, params) { if (typeof exValue !== 'string') { // Return null to deny the user input return null; } // Parse parameters object const result = parseRecord(params, (p) => ({ // `view` option may be useful to provide a custom control for primitive values view: p.required.constant('file-input'), invalidFiletypeMessage: p.optional.string, lineCount: p.optional.number, filetypes: p.optional.array(p.required.string), })); if (!result) { return null; } // Return a typed value and params to accept the user input return { initialValue: exValue, params: result, }; }, binding: { reader(_args) { return (exValue) => { // Convert an external unknown value into the internal value return exValue instanceof File ? exValue : null; }; }, constraint(_args) { return new CompositeConstraint([]); }, writer(_args) { return (target, inValue) => { // Use `target.write()` to write the primitive value to the target, // or `target.writeProperty()` to write a property of the target target.write(inValue); }; }, }, controller(args) { var _a, _b; const defaultNumberOfLines = 3; const defaultFiletypeWarningText = 'Unaccepted file type.'; // Create a controller for the plugin return new FilePluginController(args.document, { value: args.value, viewProps: args.viewProps, invalidFiletypeMessage: (_a = args.params.invalidFiletypeMessage) !== null && _a !== void 0 ? _a : defaultFiletypeWarningText, lineCount: (_b = args.params.lineCount) !== null && _b !== void 0 ? _b : defaultNumberOfLines, filetypes: args.params.filetypes, }); }, }); // Export your plugin(s) as constant `plugins` const id = 'file-input'; const css = '.tp-ctnv{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:rgba(0,0,0,0);border-width:0;font-family:inherit;font-size:inherit;font-weight:inherit;margin:0;outline:none;padding:0}.tp-ctnv{background-color:var(--in-bg);border-radius:var(--bld-br);box-sizing:border-box;color:var(--in-fg);font-family:inherit;height:var(--cnt-usz);line-height:var(--cnt-usz);min-width:0;width:100%}.tp-ctnv:hover{background-color:var(--in-bg-h)}.tp-ctnv:focus{background-color:var(--in-bg-f)}.tp-ctnv:active{background-color:var(--in-bg-a)}.tp-ctnv:disabled{opacity:.5}.tp-ctnv{cursor:pointer;display:flex;justify-content:center;align-items:center;overflow:hidden;position:relative;height:100%;width:100%;border:1px dashed #717070;border-radius:5px}.tp-ctnv.tp-v-disabled{opacity:.5}.tp-ctnv_input_area_dragging{border:1px dashed #6774ff;background-color:rgba(88,88,185,.231372549)}.tp-ctnv_warning{color:var(--in-fg);bottom:2px;display:inline-block;font-size:.9em;height:-moz-max-content;height:max-content;line-height:1.5;opacity:.5;white-space:normal;width:-moz-max-content;width:max-content;word-wrap:break-word;text-align:right;width:100%;margin-top:var(--cnt-vp)}.tp-ctnv_text{color:var(--in-fg);bottom:2px;display:inline-block;font-size:.9em;height:-moz-max-content;height:max-content;line-height:.9;margin:.2rem;max-height:100%;max-width:100%;opacity:.5;position:absolute;right:2px;text-align:right;white-space:normal;width:-moz-max-content;width:max-content;word-wrap:break-word}.tp-ctnv_frac{background-color:var(--in-fg);border-radius:1px;height:2px;left:50%;margin-top:-1px;position:absolute;top:50%}.tp-ctnv_icon{box-sizing:border-box;position:absolute;display:block;transform:scale(var(--ggs, 1));width:16px;height:6px;border:2px solid;border-top:0;border-bottom-left-radius:2px;border-bottom-right-radius:2px;margin-top:8px;opacity:.5}.tp-ctnv_icon::after{content:"";display:block;box-sizing:border-box;position:absolute;width:8px;height:8px;border-left:2px solid;border-top:2px solid;transform:rotate(45deg);left:2px;bottom:4px}.tp-ctnv_icon::before{content:"";display:block;box-sizing:border-box;position:absolute;border-radius:3px;width:2px;height:10px;background:currentColor;left:5px;bottom:3px}.tp-btnv_b{margin-top:10px}.tp-inputv{opacity:0}'; const plugins = [TweakpaneFileInputPlugin]; export { css, id, plugins };