Commit e3f65fbb authored by jdurrant's avatar jdurrant
Browse files

Added FileLoaderSystem.

parent 5ac46e88
// Released under the Apache 2.0 License. See
// LICENSE.md or go to https://opensource.org/licenses/Apache-2.0 for full
// details. Copyright 2021 Jacob D. Durrant.
import { addCSS } from "../Utils";
declare var Vue;
/** An object containing the vue-component computed functions. */
let computedFunctions = {
/**
* Determines whether this component has a label.
* @returns boolean True if it does, false otherwise.
*/
hasLabel(): boolean {
return this["label"] !== "" && this["label"] !== undefined;
},
/**
* Determines if label should be placed to the left or above. Number of
* columns for the label width 'xs' screens and up. Always 0 (labele above
* on small screens).
* @returns number 0
*/
"labelCols"(): number {
return 0;
// Used to return 2, but now I think it's good to have label on top if
// there isn't plenty of room.
// return ((this.hasLabel === true) && (this["labelToLeft"] === true)) ? 2 : 0;
},
/**
* Determines if label should be placed to the left or above. Number of
* columns for the label width 'md' screens and up.
* @returns number Returns 2 if it has a label, 0 otherwise.
*/
"labelColsMd"(): number {
return ((this.hasLabel === true) && (this["labelToLeft"] === true)) ? 2 : 0;
}
}
/**
* Setup the file-loader-form-group Vue commponent.
* @returns void
*/
export function setupFileLoaderFormGroup(): void {
Vue.component('file-loader-form-group', {
/**
* Get the data associated with this component.
* @returns any The data.
*/
"data": function() {
return {}
},
"computed": computedFunctions,
"template": /* html */ `
<span class="file-loader-form-group">
<!-- :label-cols="labelCols"
:label-cols-lg="labelColsMd" -->
<b-form-group
v-if="formGroupWrapper"
:label="label"
:label-for="id"
:id="'input-group-' + id"
:style="styl"
:label-cols="0"
:label-cols-md="labelColsMd"
>
<slot></slot>
<small
tabindex="-1"
:id="'input-group-input-group-' + id + '__BV_description_'"
class="form-text text-muted" style="display:inline;"
v-html="description">
</small>
<small class="form-text text-muted" style="display:inline;">
<slot name="extraDescription"></slot>
</small>
</b-form-group>
<div v-else>
<slot></slot>
</div>
</span>
`,
"props": {
"label": String,
"id": String,
"styl": String,
"description": String,
"formGroupWrapper": {
"type": Boolean,
"default": true
},
"labelToLeft": {
"type": Boolean,
"default": true
}
},
"methods": {},
"mounted"() {
addCSS(`.file-loader-form-group .col-form-label { hyphens: auto; max-width: 100px !important; }`);
}
})
}
You can omit this if this component is registered globally (lots of other
components you use might already be using this one).
\ No newline at end of file
// Released under the Apache 2.0 License. See LICENSE.md or go to
// https://opensource.org/licenses/Apache-2.0 for full details. Copyright 2021
// Jacob D. Durrant.
declare var Vue;
/**
* Setup the file-loader-text-input Vue commponent.
* @returns void
*/
export function setupFileLoaderTextInput(): void {
Vue.component("file-loader-text-input", {
/**
* Get the data associated with this component.
* @returns any The data.
*/
"data"(): any {
return {
"localValue": ""
};
},
"methods": {
"onLoad"(): void {
this.$emit("onLoad", this["value"]);
},
"keydown"(e: KeyboardEvent): void {
if (e.key === "Enter") {
this["onLoad"]();
}
},
"keyup"(e: KeyboardEvent) : void {
this.$emit("input", this["localValue"]);
}
// "clearText"(): void {
// this["val"] = "";
// }
},
"template": /*html*/ `
<b-input-group>
<b-form-input
v-model="localValue"
style="border-top-left-radius:4px; border-bottom-left-radius:4px;"
:placeholder="placeholder"
:formatter="formatter"
@keydown="keydown"
@keyup="keyup"
:state="valid"
></b-form-input>
<b-input-group-append>
<b-button
style="background-color:#e9ecef; color:#4a5056; border:1px solid #ced4da; border-top-right-radius:4px; border-bottom-right-radius:4px;"
variant="outline-primary"
@click="onLoad"
:disabled="btnDisabledFunc(value)"
>
Load
</b-button>
</b-input-group-append>
</b-input-group>`,
"props": {
"placeholder": {
"type": String,
"default": "Type here...",
},
"formatter": {
"type": Function,
"default": (t) => {return t;}
},
"btnDisabledFunc": {
"type": Function,
"default": () => {return false;}
},
"valid": {
"type": Boolean,
"default": true
},
"value": {
"type": String,
"default": ""
}
},
"computed": {},
/**
* Runs when the vue component is mounted.
* @returns void
*/
"mounted": () => {},
});
}
// export interface IInputFileName {
// type: string;
// filename: string;
// }
export interface IVueXVar {
name: string;
val: any;
}
export interface IConvert extends IFileInfo{
onConvertDone: Function;
onConvertCancel: Function;
}
export interface IFileInfo {
filename: string;
fileContents: string;
// onConvertDone: IConvert;
// convertedResolveFunc?: Function;
// convertedRejectFunc?: Function;
// id?: string; // associated component id
}
export interface IFileLoadError {
title: string;
body: string;
}
// export interface IFileFromTextField {
// placeholder: string;
// tabName: string;
// loadFunc: Function
// onSuccess: Function;
// onError: Function;
// }
export interface IAllFiles {
selectedFilename: string;
allFiles: {[key: string]: string}; // filename => contents
}
export interface IResidueInfo {
residueId: string[],
residuePdbLines: string
}
\ No newline at end of file
import { IFileInfo, IFileLoadError } from "./Interfaces";
export function extsStrToList(exts: string): string[] {
return exts
.toLowerCase()
.split(/,/g)
.map(
(e) =>
e.replace(/ /g, "").replace(/\./, "")
);
}
export function getExt(filename: string): string {
let fileNameParts = filename.toLowerCase().split(/\./g);
let ext = fileNameParts[fileNameParts.length - 1];
return ext;
}
/**
* Given a file object, returns a promise that resolves the text
* in that file.
* @param {*} fileObj The file object.
* @returns Promise
*/
export function getFileObjContents(fileObj): Promise<any> {
return new Promise((resolve, reject) => {
var fr = new FileReader();
fr.onload = () => {
// @ts-ignore: Not sure why this causes Typescript problems.
var data = new Uint8Array(fr.result);
resolve(new TextDecoder("utf-8").decode(data));
};
fr.readAsArrayBuffer(fileObj);
// Reset the show non-protein atom's link.
// if (this["id"] === "receptor") {
// this.$store.commit("setVar", {
// name: "showKeepProteinOnlyLink",
// val: true,
// });
// }
});
}
export function loadRemote(url: string, vueComp: any): Promise<boolean> {
let urlUpper = url.toUpperCase();
if (
(urlUpper.slice(0, 7) !== "HTTP://") &&
(urlUpper.slice(0, 8) !== "HTTPS://")
) {
vueComp.onError({
title: "Bad URL",
body: `The URL should start with http:// or https://.`
} as IFileLoadError);
return Promise.resolve(false);
}
return new Promise((resolve, reject) => {
fetch(url)
.then(response => {
if (!response.ok) {
vueComp.onError({
title: "Bad URL",
body: `Could not load the URL ${url}. Status ` + response.status.toString() + ": " + response.statusText
} as IFileLoadError);
resolve(false);
} else {
return response.text()
}
})
.then(text => {
let flnm = url.split("/").pop();
let filesInfo: IFileInfo[] = [{
filename: flnm,
fileContents: text
} as IFileInfo]
let allFilesLoaded = vueComp.onFilesLoaded(filesInfo);
// false if invalid files or something.
resolve(allFilesLoaded);
})
.catch((err) => {
vueComp.onError({
title: "Bad URL",
body: `Could not load the URL ${url}: ` + err.message
} as IFileLoadError);
resolve(false);
});
})
}
export function deepCopy(obj: any): any {
return JSON.parse(JSON.stringify(obj));
}
export function addCSS(css: string): void {
document.head.appendChild(Object.assign(
document.createElement("style"), {
textContent: css
}));
}
export function slugify(complexString: string): string {
// With help from codex
var slug = complexString.toLowerCase().replace(/[^a-z0-9]+/g, '-');
return slug;
}
\ No newline at end of file
// import { dbVueFuncs } from "../Common/DB.VueFuncs";
// import { clearAllInDatabase } from "../DataBase";
// import { removeFileFromDatabase } from "../DataBase/Internal";
import { IFileInfo } from "../Common/Interfaces";
import { deepCopy } from "../Common/Utils";
/** An object containing the vue-component methods functions. */
export let fileLoaderFileListMethodsFunctions = {
"fileDismissed"(filename) {
// removeFileFromDatabase(this["id"], filename, this["associatedFileLoaderComponent"])
let files = deepCopy(this["value"]);
let keys = Object.keys(files);
let idx = keys.indexOf(filename);
let newIdx = (idx === 0) ? idx + 1 : idx - 1;
let newFilename = keys[newIdx];
this["fileNameClicked"](newFilename);
delete files[filename];
this.$emit("input", files);
this.$emit("onSelectedFilenameChange", newFilename);
// this.$nextTick(() => {
// this.$emit("onRequestRemoveFile", filename);
// });
},
"clearAll"(): void {
// Clears all entries in the list.
// this.$emit("onRequestRemoveAllFiles");
this.$emit("input", {});
this.$emit("onSelectedFilenameChange", "");
// clearAllInDatabase(this["id"]);
// clearAllInDatabase();
},
// ...dbVueFuncs,
"fileNameClicked"(filename: string): void {
// this.loadSingleFileFromIndexedDB(filename, this["associatedFileLoaderComponent"]);
this["currentlySelectedFilenameToUse"] = filename;
this.$nextTick(() => {
this.$emit("onSelectedFilenameChange", filename);
// this.$emit("onSelectFile", {
// filename: filename,
// fileContents: this["files"][filename]
// } as IFileInfo);
});
},
"scrollToBottom"(): void {
setTimeout(() => {
let div = (this.$refs["filesDiv"] as HTMLDivElement);
div.scrollTo({
top: div.clientHeight,
left: 0,
behavior: 'smooth'
});
}, 500);
},
};
// Released under the Apache 2.0 License. See LICENSE.md or go to
// https://opensource.org/licenses/Apache-2.0 for full details. Copyright 2021
// Jacob D. Durrant.
import { addCSS } from "../Common/Utils";
import { fileLoaderFileListMethodsFunctions } from "./Methods.VueFuncs";
declare var Vue;
/** An object containing the vue-component computed functions. */
let computedFunctions = {
"filenames"(): string[] {
return Object.keys(this["value"]);
}
};
/**
* The vue-component mounted function.
* @returns void
*/
function mountedFunction(): void {
// Add some CSS
addCSS(`.alert-dismissible { padding: 0.15rem !important;} .alert-dismissible .close {padding-top: 4px; padding-right: 8px; padding-left: 8px; padding-bottom: 0;}`);
}
/**
* Setup the file-list Vue commponent.
* @returns void
*/
export function setupFileList(): void {
Vue.component("file-list", {
/**
* Get the data associated with this component.
* @returns any The data.
*/
"data"(): any {
return {
"currentlySelectedFilenameToUse": ""
};
},
"watch": {
"selectedFilename"(newVal: string, oldVal: string): void {
// this["currentlySelectedFilenameToUse"] = newVal;
this["fileNameClicked"](newVal);
}
},
"methods": fileLoaderFileListMethodsFunctions,
"template": /*html*/ `
<div>
<div
ref="filesDiv"
style="max-height:135px; overflow-y:scroll;"
:class="filenames.length > 0 ? 'mt-2' : ''"
>
<!-- <div style="margin-bottom:15px;margin-top:15px;">{{filenames}}</div> -->
<b-alert
v-for="filename in filenames"
:key="filename"
class="mb-1 p-3 py-3"
dismissible
fade
show
:variant="(filename === currentlySelectedFilenameToUse) ? 'primary' : 'secondary'"
@dismissed="fileDismissed(filename)"
>
<div style="cursor:pointer; position:relative; top:1px; left:4px;" @click="fileNameClicked(filename)">
&#128206; &nbsp; {{filename}}
</div>
</b-alert>
</div>
<!-- style="padding-top:3px; float:right; cursor:pointer;" -->
<div
v-if="filenames.length > 1"
style="height:22.5px;"
>
<b-form-tag
style="padding-top:3px; cursor:pointer; position:relative; top:1px; float:right;"
:no-remove="true"
:pill="true"
@click.native="clearAll"
>Clear All</b-form-tag>
</div>
<!-- variant="secondary" -->
<!-- <span style="clear:both;"></span> -->
</div>
`,
"props": {
"selectedFilename": {
"type": String,
"default": ""
},
// "database": {
// "type": Object,
// "default": undefined
// },
"id": {
"type": String,
"default": undefined
},
// for v-model
"value": {
"type": Object,
"default": {}
}
// "associatedFileLoaderComponent": {
// "type": Object,
// "default": undefined
// }
},
"computed": computedFunctions,
/**
* Runs when the vue component is mounted.
* @returns void
*/
"mounted": mountedFunction,
});
}
import { IAllFiles, IConvert, IFileInfo, IFileLoadError } from "../../Common/Interfaces";
import { deepCopy } from "../../Common/Utils";
// Note that plugin emits (with same name) are present in PluginParent.ts. These
// emit from the encapsulating FileLoader itself.
// These functions are called when plugin children emit data. They process that
// data a bit if necessary and emit it to the encompassing component
// (FileLoaderWrapper). Like relay functions.
export let fileLoaderEmitFunctions = {
// Start converting files that need to be converted.
"onStartConvertFiles": function(val: IConvert): void {
this.$emit("onStartConvertFiles", val);
},
// When an error occurs, handle that as well. No need to process. Just pass
// up the chain.
"onError": function(val: IFileLoadError): void {
this.$emit("onError", val);
},
// When the file is completely ready, after any conversion, error handling,
// etc. Fires for every file loaded.
"onFileReady": function(val: IFileInfo): void {
let files = {};
// If multiple not files allowed, copy current files.
if (this["multipleFiles"] !== false) {
files = deepCopy(this["value"]);
}
// Add this file to the object containing all files
files[val.filename] = val.fileContents;
// this["selectedFilename"] = val.filename;
// Send all the data up the chain (via v-bind).
this.$emit("input", files);
this.