vercengen/components/ComponentFileExplorer/ComponentFileExplorer.js

/**
 * Refer to <span color = "yellow">{@link ve.Component}</span> for methods or fields inherited from this Component's parent such as `.options.attributes` or `.element`.
 * 
 * File explorer used either for navigation (i.e. saving/loading) or as a generic file explorer primitive.
 * - Functional binding: <span color=00ffff>veFileExplorer</span>().
 * 
 * ##### Constructor:
 * - `arg0_value`: {@link string} - The file path in which the FileExplorer should be initialised.
 * - `arg1_options`: {@link Object}
 *   - `.actions_components_obj`: {@link Object}<{@link ve.Component}> - Appended components in the topbar action meny, usually at the end.
 *   - `.file_components_obj={select: ...}`: {@link Object}<{@link ve.Component}>
 *   - `.file_icon="<icon>description</icon>"`: {@link string}
 *   - `.file_options`: {@link Object}
 *   - `.folder_components_obj={select: ...}`: {@link Object}<{@link ve.Component}>
 *   - `.folder_icon="<icon>folder</icon>"`: {@link string}
 *   - `.folder_options`: {@link Object}
 *   - `.name`: {@link string}
 *   - `.navigation_only=false`: {@link boolean}
 *   - `.onrefresh`: {@link function}(v:{@link ve.FileExplorer.v}, arg1_e:{@link ve.FileExplorer})
 *   - 
 *   - `.load_function`: {@link function}(arg0_data:{@link string}, arg1_file_path:{@link string}) - Automatically loads the text content of a valid extension into this function.
 *   - `.save_extension`: {@link Array}<{@link string}>|{@link string} - The save dot extension that files can be loaded from. `.*` refers to all file extensions.
 *   - `.save_function`: {@link function}(arg0_save_file_name:{@link string}) - Returns the value of the current savedata state.
 *   
 * ##### Instance:
 * - `.clipboard`: {@link Array}<{@link string}> - The list of full file paths currently stored in the clipboard.
 * - `.selected`: {@link Array}<{@link string}> - The list of selected file paths.
 * - `.v`: {@link string} - Accessor. The current file path.
 * 
 * ##### Methods:
 * - <span color=00ffff>{@link ve.FileExplorer.clearClipboard|clearClipboard}</span>()
 * - <span color=00ffff>{@link ve.FileExplorer.deselect|deselect}</span>(arg0_file_path:{@link string}, arg1_options:{do_not_modify_classes: {@link boolean}}) | {@link Array}<{@link string}>
 * - <span color=00ffff>{@link ve.FileExplorer.deselectAll|deselectAll}</span>()
 * - <span color=00ffff>{@link ve.FileExplorer.fireSelectToggle|fireSelectToggle}(v:{@link boolean}, e:{@link ve.Toggle}) - Internal handler for toggling file/folder selection.
 * - <span color=00ffff>{@link ve.FileExplorer.select|select}</span>(arg0_file_path:{@link string}) | {@link Array}<{@link string}>
 * - <span color=00ffff>{@link ve.FileExplorer.selectAll|selectAll}</span>()
 * - <span color=00ffff>{@link ve.FileExplorer.setClipboard|setClipboard}</span>() - Sets the clipboard to currently selected file paths.
 * - <span color=00ffff>{@link ve.FileExplorer.refresh|refresh}</span>() - Refreshes the current ve.FileExplorer display. Handled automatically.
 * - 
 * - <span color=00ffff>{@link ve.FileExplorer.getFiles|getFiles}</span>(arg0_file_paths:{@link Array}<{@link string}>) | {@link Array}<{@link string}> - Returns all the subpaths in the set of file/folder paths given.
 * - <span color=00ffff>{@link ve.FileExplorer.copy|copy}</span>(arg0_file_paths:{@link Array}<{@link string}>, arg1_file_path:{@link string}, arg2_function:{@link function}) - Opens a copy files modal with a callback function.
 * - <span color=00ffff>{@link ve.FileExplorer.delete|delete}</span>(arg0_file_paths:{@link Array}<{@link string}>, arg1_file_path:{@link string}, arg2_function:{@link function}) - Opens a delete files modal with a callback function.
 * - <span color=00ffff>{@link ve.FileExplorer.move|move}</span>(arg0_file_paths:{@link Array}<{@link string}>, arg1_file_path:{@link string}, arg2_function:{@link function}) - Opens a move files modal with a callback function.
 * - <span color=00ffff>{@link ve.FileExplorer.rename|rename}</span>(arg0_file_path:{@link string}, arg1_function:{@link function}) - Opens a rename file modal with a callback function.
 *
 * @augments ve.Component
 * @memberof ve.Component
 * @type {ve.FileExplorer}
 */
ve.FileExplorer = class extends ve.Component {
	constructor (arg0_value, arg1_options) {
		//Convert from parameters
		let value = (arg0_value) ? arg0_value : process.cwd();
			if (!fs.existsSync(value)) value = process.cwd();
		let options = (arg1_options) ? arg1_options : {};
			super(options);
			
		//Initialise options
		options.attributes = (options.attributes) ? options.attributes : {};
		options.file_components_obj = {
			select: new ve.Toggle(false, {
				off_name: `<icon>check_box_outline_blank</icon>`,
				on_name: `<icon>check_box</icon>`,
				onchange: (v, e) => this.fireSelectToggle(v, e),
				tooltip: loc("ve.registry.localisation.FileExplorer_select"),
				
				style: {
					zIndex: 99
				},
			}),
			...options.file_components_obj
		};
		options.file_icon = (options.file_icon) ? options.file_icon : "<icon>description</icon>";
		options.file_options = (options.file_options) ? options.file_options : {};
		options.folder_components_obj = {
			select: new ve.Toggle(false, {
				off_name: `<icon>check_box_outline_blank</icon>`,
				on_name: `<icon>indeterminate_check_box</icon>`,
				onchange: (v, e) => this.fireSelectToggle(v, e),
				tooltip: loc("ve.registry.localisation.FileExplorer_select"),
				
				style: {
					zIndex: 99
				},
			}),
			...options.folder_components_obj
		};
		options.folder_icon = (options.folder_icon) ? options.folder_icon : "<icon>folder</icon>";
		options.folder_options = (options.folder_options) ? options.folder_options : {};
		options.name = (options.name) ? options.name : "";
		if (options.save_extension) 
			options.save_extension = Array.toArray(options.save_extension);
		
		//options.navigation_only override
		if (options.navigation_only)
			options = {
				...options,
				disable_actions: true,
				file_components_obj: {},
				folder_components_obj: {},
				onload: (v, e) => e.name = ""
			};
		
		//Declare local instance variables
		this.element = document.createElement("div");
			this.element.setAttribute("component", "ve-file-explorer");
			Object.iterate(options.attributes, (local_key, local_value) => {
				this.element.setAttribute(local_key, local_value.toString());
			});
			this.element.instance = this;
			
		//Format html_string
		let html_string = [];
		
		html_string.push(`<span id = "name"></span>`);
		html_string.push(`<div id = "file-explorer-body"></div>`);
		this.element.innerHTML = html_string.join("");
		
		/**
		 * Contains all file paths currently in the clipboard.
		 * - Field of: {@link ve.FileExplorer}
		 * @type {string[]}
		 */
		this.clipboard = []; //Array<String> containing file paths currently in clipboard
		
		/**
		 * Contains all currently selected file paths.
		 * - Field of: {@link ve.FileExplorer}
		 * @type {string[]}
		 */
		this.selected = []; //Array<String> containing file paths that are currently selected
		
		//Refresh file explorer display
		this.options = options;
		this.name = this.options.name;
		this.value = value;
		this.refresh();
		
		this.logic_loop = setInterval(() => {
			let current_files = fs.readdirSync(this.v).join("");
			
			if (this._checksum === undefined) {
				this._checksum = current_files;
			} else if (current_files !== this._checksum) {
				this.refresh();
				this._checksum = current_files;
				this.fireFromBinding();
			}
		}, 100);
	}
	
	/**
	 * Returns the present file path.
	 * - Accessor of {@link ve.FileExplorer} 
	 * 
	 * @alias v
	 * @memberof ve.Component.ve.FileExplorer
	 * @type {string}
	 */
	get v () {
		//Return statement
		return this.value;
	}
	
	/**
	 * Sets the present file path.
	 * - Accessor of {@link ve.FileExplorer}
	 * 
	 * @alias v
	 * @memberof ve.Component.ve.FileExplorer
	 * @param arg0_value {string}
	 */
	set v (arg0_value) {
		//Convert from parameters
		let value = arg0_value;
		
		//Set new folder path before refreshing display
		this.deselectAll();
		this.value = value;
		this.refresh();
		this.fireFromBinding();
	}
	
	/**
	 * Clears the present keyboard.
	 * - Method of {@link ve.FileExplorer}
	 * 
	 * @alias clearClipboard
	 * @memberof ve.Component.ve.FileExplorer
	 */
	clearClipboard () {
		this.clipboard = [];
	}
	
	/**
	 * Deselects a specific file path and removes it from {@link this.selected}.
	 * - Method of {@link ve.FileExplorer}
	 * 
	 * @alias deselect
	 * @memberof ve.Component.ve.FileExplorer
	 * 
	 * @param {string} arg0_file_path
	 * @param {Object} [arg1_options]
	 *  @param {boolean} [arg1_options.do_not_modify_classes=false]
	 * 
	 * @returns string[]
	 */
	deselect (arg0_file_path, arg1_options) {
		//Convert from parameters
		let file_path = arg0_file_path;
		let options = (arg1_options) ? arg1_options : {};
		
		//Iterate over all this.selected and splice
		for (let i = 0; i < this.selected.length; i++)
			if (this.selected[i] === file_path) {
				//Try to find the element in question and remove .selected
				if (!options.do_not_modify_classes)
					Object.iterate(this.hierarchy.components_obj, (local_key, local_value) => {
						if (local_key === file_path)
							local_value.element.classList.remove("selected");
					});
				this.selected.splice(i, 1);
			}
		
		//Return statement
		return this.selected;
	}
	
	/**
	 * Deselects all file paths, clearing {@link this.selected}.
	 * - Method of {@link ve.FileExplorer}
	 *
	 * @alias deselectAll
	 * @memberof ve.Component.ve.FileExplorer
	 */
	deselectAll () {
		//Iterate over all this.selected and deselect them
		for (let i = this.selected.length - 1; i >= 0; i--)
			this.deselect(this.selected[i], { do_not_modify_classes: true });
		//Iterate over all this.hierarchy.components_obj and update their classes to be deselected
		Object.iterate(this.hierarchy.components_obj, (local_key, local_value) => {
			local_value.element.classList.remove("selected");
		});
	}
	
	/**
	 * Fires a select toggle element for the full path.
	 * - Method of: {@link ve.FileExplorer}
	 *
	 * @alias fireSelectToggle
	 * @memberof ve.Component.ve.FileExplorer
	 * 
	 * @param {string} v
	 * @param {ve.FileExplorer} e
	 */
	fireSelectToggle (v, e) {
		if (e.owners)
			for (let i = e.owners.length - 1; i >= 0; i--)
				if (e.owners[i] instanceof ve.HierarchyDatatype) try {
					let full_path = e.owners[i].element.getAttribute("data-path");
					(v) ? this.select(full_path) : this.deselect(full_path);
					break;
				} catch (e) { console.error(e); }
	}
	
	/**
	 * Refreshes the current file explorer path, rerendering the display for folders and files within the Component.
	 * - Method of: {@link ve.FileExplorer}
	 *
	 * @alias refresh
	 * @memberof ve.Component.ve.FileExplorer
	 */
	refresh () {
		//Declare local instance variables
		let all_files_in_directory = fs.readdirSync(this.value, { withFileTypes: true });
		let hierarchy_obj = {};
		
		//Add item button to move up one folder at the top
		let actions_components_obj = (this.options.actions_components_obj) ? 
			this.options.actions_components_obj : {};
		let previous_folder_path = path.join(this.value, "..");
		
		hierarchy_obj.file_path = new ve.HierarchyDatatype({
			information: new ve.HTML(() => this.v, {
				style: {
					whiteSpace: "break-spaces",
					wordBreak: "break-all",
					width: "100%"
				}
			})
		}, { disabled: true });
		if (!this.options.disable_actions)
			hierarchy_obj.selection = new ve.HierarchyDatatype({
				information: new ve.HTML((e) => `${(this.clipboard.length > 0) ? `${loc("ve.registry.localisation.FileExplorer_clipboard")} (${String.formatNumber(this.clipboard.length)})` : loc("ve.registry.localisation.FileExplorer_clipboard_is_empty")} &nbsp; | &nbsp; ${(this.selected.length > 0) ? `${loc("ve.registry.localisation.FileExplorer_elements_selected", String.formatNumber(this.selected.length))}` : ""}
				`, { style: { marginRight: "auto", padding: 0 }}),
				actions_menu: new ve.RawInterface({
					copy_button: new ve.Button((e) => {
						if (this.selected.length === 0) return; //Internal guard clause if nothing is selected
						this.setClipboard();
						new ve.Toast(loc("ve.registry.localisation.FileExplorer_copied_elements_to_clipboard", String.formatNumber(this.clipboard.length)));
					}, { name: "<icon>copy</icon>", limit: () => this.selected.length, tooltip: loc("ve.registry.localisation.FileExplorer_copy_selected") }),
					cut_button: new ve.Button((e) => {
						//This has to use a new file explorer in a modal with .options.disable_actions=true, since it would be fatal otherwise
						if (this.selected.length === 0) return; //Internal guard clause if nothing is selected
						
						//Declare local instance variables
						let modal = new ve.Modal({
							file_explorer: new ve.FileExplorer(this.v, { navigation_only: true }),
							confirm_button: new ve.Button((e) => {
								modal.close();
								ve.FileExplorer.move(this.selected, modal.components_obj.file_explorer.v, () => {
									this.refresh();
									this.deselectAll();
								});
							}, { name: loc("ve.registry.localisation.FileExplorer_confirm") })
						}, { 
							name: loc("ve.registry.localisation.FileExplorer_cut_paste_files", String.formatNumber(this.selected.length)),
							draggable: true, 
							resizeable: true, 
							width: "24rem" 
						});
					}, { name: "<icon>cut</icon>", limit: () => this.selected.length, tooltip: loc("ve.registry.localisation.FileExplorer_cut_selected") }),
					paste_button: new ve.Button((e) => {
						let confirm = new ve.Confirm(loc("ve.registry.localisation.FileExplorer_are_you_sure_copy_paste", String.formatNumber(this.clipboard.length), this.v), {
							name: loc("ve.registry.localisation.FileExplorer_paste_files", String.formatNumber(this.clipboard.length)),
							special_function: () => {
								confirm.close();
								ve.FileExplorer.copy(this.clipboard, this.v, () => this.refresh());
							}
						});
					}, { name: "<icon>paste</icon>", limit: () => this.clipboard.length, tooltip: loc("ve.registry.localisation.FileExplorer_paste_clipboard") }),
					
					//clear_clipboard
					clear_clipboard: new ve.Button((e) => {
						this.clipboard = [];
					}, { name: "<icon>content_paste_off</icon>", limit: () => this.clipboard.length, tooltip: loc("ve.registry.localisation.FileExplorer_clear_clipboard") }),
					
					//move_button, delete_button
					move_button: new ve.Button((e) => {
						let modal = new ve.Modal({
							file_explorer: new ve.FileExplorer(this.v, { navigation_only: true }),
							confirm_button: new ve.Button((e) => {
								modal.close();
								ve.FileExplorer.move(this.selected, modal.components_obj.file_explorer.v, () => {
									this.refresh();
									this.deselectAll();
								});
							}, { name: loc("ve.registry.localisation.FileExplorer_confirm") })
						}, { 
							name: loc("ve.registry.localisation.FileExplorer_move_files", String.formatNumber(this.selected.length)),
							draggable: true, 
							resizeable: true, 
							width: "24rem"
						});
					}, { name: "<icon>arrow_forward</icon>", limit: () => this.selected.length, tooltip: loc("ve.registry.localisation.move_selected") }),
					delete_button: new ve.Button((e) => {
						let confirm = new ve.Confirm(loc("ve.registry.localisation.FileExplorer_are_you_sure_delete", this.selected.join(", ")), {
							name: loc("ve.registry.localisation.FileExplorer_delete_files", String.formatNumber(this.selected.length)),
							special_function: () => {
								confirm.close();
								ve.FileExplorer.delete(this.selected, () => this.refresh());
							}
						});
					}, { name: "<icon>delete</icon>", limit: () => this.selected.length, tooltip: loc("ve.registry.localisation.FileExplorer_delete_selected") }),
					
					new_folder_button: new ve.Button((e) => {
						let local_modal = new ve.Window({
							html: new ve.HTML(loc("ve.registry.localisation.FileExplorer_create_a_new_folder")),
							new_folder_name: new ve.Text("",  { name: " " }),
							confirm_button: new ve.Button((e) => {
								let new_folder_path = path.join(this.v, local_modal.components_obj.new_folder_name.v);
								
								if (local_modal.components_obj.new_folder_name.v.length > 0) {
									fs.mkdirSync(new_folder_path, { recursive: true });
									this.refresh();
								} else {
									new ve.Toast(loc("ve.registry.localisation.FileExplorer_cannot_create_unnamed_folder"));
								}
							})
						}, { can_rename: false, name: loc("ve.registry.localisation.FileExplorer_create_new_folder") })
					}, { name: "<icon>create_new_folder</icon>", tooltip: loc("ve.registry.localisation.FileExplorer_create_new_folder") }),
					new_file_button: new ve.Button((e) => {
						let local_modal = new ve.Window({
							html: new ve.HTML(loc("ve.registry.localisation.FileExplorer_create_a_new_file")),
							new_file_name: new ve.Text("", { name: " " }),
							confirm_button: new ve.Button((e) => {
								let new_file_path = path.join(this.v, local_modal.components_obj.new_file_name.v);
								
								if (!fs.existsSync()) {
									fs.closeSync(fs.openSync(new_file_path, "w"));
									this.refresh();
									new ve.Toast(loc("ve.registry.localisation.FileExplorer_created_empty_text_file", new_file_path));
								} else {
									new ve.Toast(loc("ve.registry.localisation.FileExplorer_specified_file_path_already_exists"));
								}
							})
						}, { can_rename: false, name: loc("ve.registry.localisation.FileExplorer_create_new_file") });
					}, { name: "<icon>note_add</icon>", tooltip: loc("ve.registry.localisation.FileExplorer_create_new_file") } ),
					...actions_components_obj
				}, {
					style: { marginLeft: "auto", order: 99, marginTop: "var(--cell-padding)", padding: 0 }
				}),
			}, {
				attributes: { "data-ve-is-information": true },
				disabled: true,
				style: {
					".nst-content": { 
						display: "flex", 
						flexDirection: "column",
						paddingBottom: `var(--cell-padding)`,
						paddingTop: `var(--cell-padding)`
					},
				}
			});
		
		//options.save_function
		if (this.options.save_function) {
			hierarchy_obj.save_button = new ve.HierarchyDatatype({
				save_as_icon: new ve.HTML(`<icon>save_as</icon>`),
				save_name: new ve.HTML(loc("ve.registry.localisation.FileExplorer_save_file"))
			}, { disabled: true });
			hierarchy_obj.save_button.element.onclick = () => {
				let local_modal = new ve.Window({
					html: new ve.HTML(loc("ve.registry.localisation.FileExplorer_save_file_as")),
					new_file_name: new ve.Text(`autosave${(this.options?.save_extension?.[0]) ? this.options.save_extension[0] : ""}`, { name: " " }),
					confirm_button: new ve.Button((e) => {
						let save_file_name = path.join(this.v, local_modal.new_file_name.v);
						
						if (this.options.save_function)
							try {
								let save_string;
								let save_value = this.options.save_function(save_file_name);
								
								if (typeof save_value === "object") {
									save_string = JSON.stringify(save_value);
								} else {
									save_string = save_value.toString();
								}
								
								//Check if file already exists, if so send a confirmation prompt
								let save_function = () => {
									fs.writeFile(save_file_name, save_string, () => {
										veToast(loc("ve.registry.localisation.FileExplorer_successfully_saved_file_as", save_file_name));
										local_modal.remove();
										this.refresh();
									});
								};
								
								(!fs.existsSync(save_file_name)) ?
									save_function() :
									veConfirm(loc("ve.registry.localisation.FileExplorer_do_you_want_to_overwrite_file"), {
										special_function: () => save_function()
									});
							} catch (e) {
								veToast(e.toString());
								console.error(e);
							}
					})
				}, { can_rename: false, name: loc("ve.registry.localisation.FileExplorer_save_file") });
			};
		}
		
		hierarchy_obj[previous_folder_path] = new ve.HierarchyDatatype({
			up_icon: new ve.HTML(`<icon>subdirectory_arrow_left</icon>`),
			two_dots: new ve.HTML(loc("ve.registry.localisation.FileExplorer_back"))
		}, { disabled: true, limit: () => !File.isDrive(this.v) });
		
		let previous_folder_obj = hierarchy_obj[previous_folder_path];
		previous_folder_obj.element.ondblclick = (e) => {
			this.from_binding_fire_silently = true;
			this.v = previous_folder_path;
			this.from_binding_fire_silently = false;
			this.fireToBinding();
		};
		
		//Special handling for drive switching
		if (File.isDrive(this.v)) {
			let all_drives = File.getAllDrives();
			
			for (let i = 0; i < all_drives.length; i++) {
				if (path.resolve(this.v) === path.resolve(all_drives[i])) continue; //Internal guard clause if paths are the same
				hierarchy_obj[all_drives[i]] = new ve.HierarchyDatatype({
					drive_icon: new ve.HTML(`<icon>storage</icon>`, { style: { padding: 0 } })
				}, {
					attributes: {
						"data-folder": true,
						"data-path": all_drives[i]
					},
					name: all_drives[i],
					disabled: true
				});
				hierarchy_obj[all_drives[i]].element.ondblclick = () => {
					this.from_binding_fire_silently = true;
					this.v = all_drives[i];
					this.from_binding_fire_silently = false;
					this.fireToBinding();
				};
			}
		}
		
		//Iterate over all files and folders in the current directory
		for (let i = 0; i < all_files_in_directory.length; i++) {
			let local_full_path = path.join(this.value, all_files_in_directory[i].name);
			
			//Check to make sure local_full_path is a directory
			if (all_files_in_directory[i].isDirectory()) {
				hierarchy_obj[local_full_path] = new ve.HierarchyDatatype(
					{
						folder_icon: new ve.HTML(this.options.folder_icon, {
							attributes: {
								"data-folder-icon": true
							},
							tooltip: `/${all_files_in_directory[i].name}/`
						}),
						actions_menu: new ve.RawInterface({
							rename: new ve.Button((e) => {
								ve.FileExplorer.rename(local_full_path, () => this.refresh());
							}, {
								name: `<icon>drive_file_rename_outline</icon>`,
								limit: () => (!this.options.navigation_only),
								tooltip: loc("ve.registry.localisation.FileExplorer_rename"),
								style: { padding: `var(--cell-padding)` }
							}),
							...Object.fromEntries(
								Object.entries(this.options.folder_components_obj).map(([local_key, local_component]) => {
									return [local_key, local_component.clone ?
										local_component.clone() :
										new local_component.constructor(local_component.value, local_component.options)];
								})
							)
						}, {
							attributes: { "data-ve-is-actions-menu": true },
							style: { display: "flex", marginLeft: "auto", order: 99, padding: 0 },
							...this.options.folder_options
						})
					}, {
						attributes: {
							"data-folder": true,
							"data-path": local_full_path
						},
						name: all_files_in_directory[i].name,
						disabled: true,
						...this.options.folder_options
					}
				);
				
				//Add onclick event handler to hierarchy_obj[local_full_path] since we need navigation to work into a folder
				let local_folder_obj = hierarchy_obj[local_full_path];
				//local_folder_obj.setOwner(this.owner);
				local_folder_obj.element.ondblclick = (e) => {
					//Internal guard clause for protected elements
					if (e.target.closest(`button, input, .tippy-arrow, .tippy-box, .tippy-content`)) return;
					
					this.from_binding_fire_silently = true;
					this.v = local_full_path;
					this.from_binding_fire_silently = false;
					this.fireToBinding();
				};
			}
		}
		for (let i = 0; i < all_files_in_directory.length; i++) {
			let local_full_path = path.join(this.value, all_files_in_directory[i].name);
			
			//Check to make sure local_full_path is a directory is a file
			if (all_files_in_directory[i].isFile()) {
				hierarchy_obj[local_full_path] = new ve.HierarchyDatatype(
					{
						file_icon: new ve.HTML(this.options.file_icon, {
							attributes: {
								"data-file-icon": true
							},
							style: { opacity: 0.6 },
							tooltip: all_files_in_directory[i].name
						}),
						actions_menu: new ve.RawInterface({
							rename: new ve.Button((e) => {
								ve.FileExplorer.rename(local_full_path, () => this.refresh());
							}, {
								name: `<icon>drive_file_rename_outline</icon>`,
								limit: () => (this.options.navigation_only && this.options?.save_extension?.includes(path.extname(local_full_path))) || !this.options.navigation_only,
								tooltip: loc("ve.registry.localisation.FileExplorer_rename"),
								style: { padding: `var(--cell-padding)` }
							}),
							load_file: new ve.Button((e) => {
								veConfirm(loc("ve.registry.localisation.FileExplorer_are_you_sure_load", local_full_path), {
									special_function: () => {
										fs.readFile(local_full_path, "utf8", (err, data) => {
											if (err) {
												veToast(loc("ve.registry.localisation.FileExplorer_error_load", local_full_path, e));
												return;
											}
											if (this.options.load_function)
												try {
													this.options.load_function(data, local_full_path);
													veToast(loc("ve.registry.localisation.FileExplorer_loaded_save_file", local_full_path));
												} catch (e) {
													veWindow(`${loc("ve.registry.localisation.FileExplorer_error_load_save_file")}: ${e}`, { name: loc("ve.registry.localisation.FileExplorer_error_load_save_file") });
													console.error(e);
												}
										});
									}
								});
							}, {
								name: `<icon>sync_arrow_down</icon>`,
								tooltip: loc("ve.registry.localisation.FileExplorer_load_save_file"),
								limit: () => this.options.load_function && (this.options.save_extension === undefined || this.options.save_extension.includes(path.extname(local_full_path)) || this.options.save_extension.includes(".*")),
								style: {
									"#name icon": { 
										justifyContent: "normal",
										transform: "translateX(0.25rem)"
									},
									padding: `var(--cell-padding)` 
								}
							}),
							...Object.fromEntries(
								Object.entries(this.options.file_components_obj).map(([local_key, local_component]) => {
									return [local_key, local_component.clone ?
										local_component.clone() :
										new local_component.constructor(local_component.value, local_component.options)];
								})
							)
						}, {
							style: { display: "flex", marginLeft: "auto", order: 99, padding: 0 },
							...this.options.folder_options
						})
					}, {
						attributes: {
							"data-file": true,
							"data-path": local_full_path
						},
						name: all_files_in_directory[i].name,
						disabled: true,
						...this.options.file_options
					}
				);
			}
		}
		
		//Set hierarchy depending on whether it already exists or not
		let file_explorer_el = this.element.querySelector(`#file-explorer-body`);
		
		this.hierarchy = new ve.Hierarchy(hierarchy_obj, { disable_searchbar: true });
		file_explorer_el.innerHTML = "";
		file_explorer_el.appendChild(this.hierarchy.element);
		
		setTimeout(() => {
			this.hierarchy.setOwner(this.owner, [this.owner]);
		});
		if (this.options.onrefresh)
			this.options.onrefresh(this.v, this);
	}
	
	/**
	 * Selects a specific file path, then returns {@link this.selected}.
	 * - Method of {@link ve.FileExplorer}
	 *
	 * @alias select
	 * @memberof ve.Component.ve.FileExplorer
	 * 
	 * @param {string} arg0_file_path
	 * 
	 * @returns string[]
	 */
	select (arg0_file_path) {
		//Convert from parameters
		let file_path = arg0_file_path;
		
		//Push to this.selected if not already selected
		if (!this.selected.includes(file_path)) {
			this.selected.push(file_path);
			if (this.hierarchy.components_obj[file_path])
				this.hierarchy.components_obj[file_path].element.classList.add("selected");
		}
		
		//Return statement
		return this.selected;
	}
	
	/**
	 * Selects all file paths in the current folder being navigated.
	 * - Method of {@link ve.FileExplorer}
	 *
	 * @alias selectAll
	 * @memberof ve.Component.ve.FileExplorer
	 */
	selectAll () {
		//Declare local instance variables
		let all_files_in_directory = fs.readdirSync(this.value, { withFileTypes: true });
		this.deselectAll(); //Reset this.selected
		
		//Iterate over all_files_in_directory and select them
		for (let i = 0; i < all_files_in_directory.length; i++) {
			let local_full_path = path.join(this.value, all_files_in_directory[i].name);
			this.select(local_full_path);
		}
	}
	
	/**
	 * Sets the clipboard to the current selection.
	 * - Method of {@link ve.FileExplorer}
	 *
	 * @alias setClipboard
	 * @memberof ve.Component.ve.FileExplorer
	 */
	setClipboard () {
		this.clipboard = structuredClone(this.selected);
	}
};

//Functional binding

/**
 * @returns {ve.FileExplorer}
 */
veFileExplorer = function () {
	//Return statement
	return new ve.FileExplorer(...arguments);
};