parent
0b6579f444
commit
3550cda699
@ -0,0 +1,135 @@ |
||||
import React, { useRef, useEffect } from 'react' // eslint-disable-line
|
||||
import { action, FileExplorerContextMenuProps } from '../types' |
||||
|
||||
import './css/file-explorer-context-menu.css' |
||||
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel' |
||||
|
||||
declare global { |
||||
interface Window { |
||||
_paq: any |
||||
} |
||||
} |
||||
const _paq = window._paq = window._paq || [] //eslint-disable-line
|
||||
|
||||
export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => { |
||||
const { actions, createNewFile, createNewFolder, deletePath, renamePath, hideContextMenu, pushChangesToGist, publishFileToGist, publishFolderToGist, copy, paste, runScript, emit, pageX, pageY, path, type, focus, ...otherProps } = props |
||||
const contextMenuRef = useRef(null) |
||||
useEffect(() => { |
||||
contextMenuRef.current.focus() |
||||
}, []) |
||||
|
||||
useEffect(() => { |
||||
const menuItemsContainer = contextMenuRef.current |
||||
const boundary = menuItemsContainer.getBoundingClientRect() |
||||
|
||||
if (boundary.bottom > (window.innerHeight || document.documentElement.clientHeight)) { |
||||
menuItemsContainer.style.position = 'fixed' |
||||
menuItemsContainer.style.bottom = '10px' |
||||
menuItemsContainer.style.top = null |
||||
} |
||||
}, [pageX, pageY]) |
||||
|
||||
const filterItem = (item: action) => { |
||||
/** |
||||
* if there are multiple elements focused we need to take this and all conditions must be met |
||||
* for example : 'downloadAsZip' with type ['file','folder'] will work on files and folders when multiple are selected |
||||
**/ |
||||
const nonRootFocus = focus.filter((el) => { return !(el.key === '' && el.type === 'folder') }) |
||||
if (nonRootFocus.length > 1) { |
||||
for (const element of nonRootFocus) { |
||||
if (!itemMatchesCondition(item, element.type, element.key)) return false |
||||
} |
||||
return true |
||||
} else { |
||||
return itemMatchesCondition(item, type, path) |
||||
} |
||||
} |
||||
|
||||
const itemMatchesCondition = (item: action, itemType: string, itemPath: string) => { |
||||
if (item.type && Array.isArray(item.type) && (item.type.findIndex(name => name === itemType) !== -1)) return true |
||||
else if (item.path && Array.isArray(item.path) && (item.path.findIndex(key => key === itemPath) !== -1)) return true |
||||
else if (item.extension && Array.isArray(item.extension) && (item.extension.findIndex(ext => itemPath.endsWith(ext)) !== -1)) return true |
||||
else if (item.pattern && Array.isArray(item.pattern) && (item.pattern.filter(value => itemPath.match(new RegExp(value))).length > 0)) return true |
||||
else return false |
||||
} |
||||
|
||||
const getPath = () => { |
||||
if (focus.length > 1) { |
||||
return focus.map((element) => element.key) |
||||
} else { |
||||
return path |
||||
} |
||||
} |
||||
|
||||
const menu = () => { |
||||
return actions.filter(item => filterItem(item)).map((item, index) => { |
||||
return <li |
||||
id={`menuitem${item.name.toLowerCase()}`} |
||||
key={index} |
||||
className='remixui_liitem' |
||||
onClick={(e) => { |
||||
e.stopPropagation() |
||||
switch (item.name) { |
||||
case 'New File': |
||||
createNewFile(path) |
||||
break |
||||
case 'New Folder': |
||||
createNewFolder(path) |
||||
break |
||||
case 'Rename': |
||||
renamePath(path, type) |
||||
break |
||||
case 'Delete': |
||||
deletePath(getPath()) |
||||
break |
||||
case 'Push changes to gist': |
||||
_paq.push(['trackEvent', 'fileExplorer', 'pushToChangesoGist']) |
||||
pushChangesToGist(path, type) |
||||
break |
||||
case 'Publish folder to gist': |
||||
_paq.push(['trackEvent', 'fileExplorer', 'publishFolderToGist']) |
||||
publishFolderToGist(path, type) |
||||
break |
||||
case 'Publish file to gist': |
||||
_paq.push(['trackEvent', 'fileExplorer', 'publishFileToGist']) |
||||
publishFileToGist(path, type) |
||||
break |
||||
case 'Run': |
||||
_paq.push(['trackEvent', 'fileExplorer', 'runScript']) |
||||
runScript(path) |
||||
break |
||||
case 'Copy': |
||||
copy(path, type) |
||||
break |
||||
case 'Paste': |
||||
paste(path, type) |
||||
break |
||||
case 'Delete All': |
||||
deletePath(getPath()) |
||||
break |
||||
default: |
||||
_paq.push(['trackEvent', 'fileExplorer', 'customAction', `${item.id}/${item.name}`]) |
||||
emit && emit({ ...item, path: [path] } as customAction) |
||||
break |
||||
} |
||||
hideContextMenu() |
||||
}}>{item.label || item.name}</li> |
||||
}) |
||||
} |
||||
|
||||
return ( |
||||
<div |
||||
id="menuItemsContainer" |
||||
className="p-1 remixui_contextContainer bg-light shadow border" |
||||
style={{ left: pageX, top: pageY }} |
||||
ref={contextMenuRef} |
||||
onBlur={hideContextMenu} |
||||
tabIndex={500} |
||||
{...otherProps} |
||||
> |
||||
<ul id='remixui_menuitems'>{menu()}</ul> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
export default FileExplorerContextMenu |
@ -0,0 +1,98 @@ |
||||
import React, { useState, useEffect } from 'react' //eslint-disable-line
|
||||
import { FileExplorerMenuProps } from '../types' |
||||
|
||||
export const FileExplorerMenu = (props: FileExplorerMenuProps) => { |
||||
const [state, setState] = useState({ |
||||
menuItems: [ |
||||
{ |
||||
action: 'createNewFile', |
||||
title: 'Create New File', |
||||
icon: 'far fa-file' |
||||
}, |
||||
{ |
||||
action: 'createNewFolder', |
||||
title: 'Create New Folder', |
||||
icon: 'far fa-folder' |
||||
}, |
||||
{ |
||||
action: 'publishToGist', |
||||
title: 'Publish all the current workspace files (only root) to a github gist', |
||||
icon: 'fab fa-github' |
||||
}, |
||||
{ |
||||
action: 'uploadFile', |
||||
title: 'Load a local file into current workspace', |
||||
icon: 'fa fa-upload' |
||||
}, |
||||
{ |
||||
action: 'updateGist', |
||||
title: 'Update the current [gist] explorer', |
||||
icon: 'fab fa-github' |
||||
} |
||||
].filter(item => props.menuItems && props.menuItems.find((name) => { return name === item.action })), |
||||
actions: {} |
||||
}) |
||||
|
||||
useEffect(() => { |
||||
const actions = { |
||||
updateGist: () => {} |
||||
} |
||||
|
||||
setState(prevState => { |
||||
return { ...prevState, actions } |
||||
}) |
||||
}, []) |
||||
|
||||
return ( |
||||
<> |
||||
<span className='remixui_label' title={props.title} data-path={props.title} style={{ fontWeight: 'bold' }}>{ props.title }</span> |
||||
<span className="pl-2">{ |
||||
state.menuItems.map(({ action, title, icon }, index) => { |
||||
if (action === 'uploadFile') { |
||||
return ( |
||||
<label |
||||
id={action} |
||||
data-id={'fileExplorerUploadFile' + action } |
||||
className={icon + ' mb-0 remixui_newFile'} |
||||
title={title} |
||||
key={index} |
||||
> |
||||
<input id="fileUpload" data-id="fileExplorerFileUpload" type="file" onChange={(e) => { |
||||
e.stopPropagation() |
||||
props.uploadFile(e.target) |
||||
e.target.value = null |
||||
}} |
||||
multiple /> |
||||
</label> |
||||
) |
||||
} else { |
||||
return ( |
||||
<span |
||||
id={action} |
||||
data-id={'fileExplorerNewFile' + action} |
||||
onClick={(e) => { |
||||
e.stopPropagation() |
||||
if (action === 'createNewFile') { |
||||
props.createNewFile() |
||||
} else if (action === 'createNewFolder') { |
||||
props.createNewFolder() |
||||
} else if (action === 'publishToGist') { |
||||
props.publishToGist() |
||||
} else { |
||||
state.actions[action]() |
||||
} |
||||
}} |
||||
className={'newFile ' + icon + ' remixui_newFile'} |
||||
title={title} |
||||
key={index} |
||||
> |
||||
</span> |
||||
) |
||||
} |
||||
})} |
||||
</span> |
||||
</> |
||||
) |
||||
} |
||||
|
||||
export default FileExplorerMenu |
@ -0,0 +1,654 @@ |
||||
import React, { useEffect, useState, useRef, useContext } from 'react' // eslint-disable-line
|
||||
// import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' // eslint-disable-line
|
||||
import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line
|
||||
import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line
|
||||
import { FileExplorerContextMenu } from './file-explorer-context-menu' // eslint-disable-line
|
||||
import { FileExplorerProps, File, MenuItems, FileExplorerState } from '../types' |
||||
import { FileSystemContext } from '@remix-ui/workspace' |
||||
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel' |
||||
import { contextMenuActions } from '../utils' |
||||
|
||||
import './css/file-explorer.css' |
||||
import { checkSpecialChars, extractParentFromKey, getPathIcon, joinPath } from '@remix-ui/helper' |
||||
|
||||
export const FileExplorer = (props: FileExplorerProps) => { |
||||
const { name, focusRoot, contextMenuItems, externalUploads, removedContextMenuItems, resetFocus, files } = props |
||||
const [state, setState] = useState<FileExplorerState>({ |
||||
ctrlKey: false, |
||||
newFileName: '', |
||||
actions: contextMenuActions, |
||||
focusContext: { |
||||
element: null, |
||||
x: null, |
||||
y: null, |
||||
type: '' |
||||
}, |
||||
focusEdit: { |
||||
element: null, |
||||
type: '', |
||||
isNew: false, |
||||
lastEdit: '' |
||||
}, |
||||
expandPath: [name], |
||||
mouseOverElement: null, |
||||
showContextMenu: false, |
||||
reservedKeywords: [name, 'gist-'], |
||||
copyElement: [] |
||||
}) |
||||
const [canPaste, setCanPaste] = useState(false) |
||||
const editRef = useRef(null) |
||||
const global = useContext(FileSystemContext) |
||||
|
||||
useEffect(() => { |
||||
if (global.fs.mode === 'browser') { |
||||
setState(prevState => { |
||||
return { ...prevState, expandPath: [...new Set([...prevState.expandPath, ...global.fs.browser.expandPath])] } |
||||
}) |
||||
} else if (global.fs.mode === 'localhost') { |
||||
setState(prevState => { |
||||
return { ...prevState, expandPath: [...new Set([...prevState.expandPath, ...global.fs.localhost.expandPath])] } |
||||
}) |
||||
} |
||||
}, [global.fs.browser.expandPath, global.fs.localhost.expandPath]) |
||||
|
||||
useEffect(() => { |
||||
if (state.focusEdit.element) { |
||||
setTimeout(() => { |
||||
if (editRef && editRef.current) { |
||||
editRef.current.focus() |
||||
} |
||||
}, 0) |
||||
} |
||||
}, [state.focusEdit.element]) |
||||
|
||||
useEffect(() => { |
||||
if (focusRoot) { |
||||
global.dispatchSetFocusElement([{ key: '', type: 'folder' }]) |
||||
resetFocus(false) |
||||
} |
||||
}, [focusRoot]) |
||||
|
||||
useEffect(() => { |
||||
if (contextMenuItems) { |
||||
addMenuItems(contextMenuItems) |
||||
} |
||||
}, [contextMenuItems]) |
||||
|
||||
useEffect(() => { |
||||
if (removedContextMenuItems) { |
||||
removeMenuItems(removedContextMenuItems) |
||||
} |
||||
}, [contextMenuItems]) |
||||
|
||||
useEffect(() => { |
||||
if (global.fs.focusEdit) { |
||||
setState(prevState => { |
||||
return { ...prevState, focusEdit: { element: global.fs.focusEdit, type: 'file', isNew: true, lastEdit: null } } |
||||
}) |
||||
} |
||||
}, [global.fs.focusEdit]) |
||||
|
||||
useEffect(() => { |
||||
if (externalUploads) { |
||||
uploadFile(externalUploads) |
||||
} |
||||
}, [externalUploads]) |
||||
|
||||
useEffect(() => { |
||||
const keyPressHandler = (e: KeyboardEvent) => { |
||||
if (e.shiftKey) { |
||||
setState(prevState => { |
||||
return { ...prevState, ctrlKey: true } |
||||
}) |
||||
} |
||||
} |
||||
|
||||
const keyUpHandler = (e: KeyboardEvent) => { |
||||
if (!e.shiftKey) { |
||||
setState(prevState => { |
||||
return { ...prevState, ctrlKey: false } |
||||
}) |
||||
} |
||||
} |
||||
|
||||
document.addEventListener('keydown', keyPressHandler) |
||||
document.addEventListener('keyup', keyUpHandler) |
||||
return () => { |
||||
document.removeEventListener('keydown', keyPressHandler) |
||||
document.removeEventListener('keyup', keyUpHandler) |
||||
} |
||||
}, []) |
||||
|
||||
useEffect(() => { |
||||
if (canPaste) { |
||||
addMenuItems([{ |
||||
id: 'paste', |
||||
name: 'Paste', |
||||
type: ['folder', 'file'], |
||||
path: [], |
||||
extension: [], |
||||
pattern: [], |
||||
multiselect: false, |
||||
label: '' |
||||
}]) |
||||
} else { |
||||
removeMenuItems([{ |
||||
id: 'paste', |
||||
name: 'Paste', |
||||
type: ['folder', 'file'], |
||||
path: [], |
||||
extension: [], |
||||
pattern: [], |
||||
multiselect: false, |
||||
label: '' |
||||
}]) |
||||
} |
||||
}, [canPaste]) |
||||
|
||||
const addMenuItems = (items: MenuItems) => { |
||||
setState(prevState => { |
||||
// filter duplicate items
|
||||
const actions = items.filter(({ name }) => prevState.actions.findIndex(action => action.name === name) === -1) |
||||
|
||||
return { ...prevState, actions: [...prevState.actions, ...actions] } |
||||
}) |
||||
} |
||||
|
||||
const removeMenuItems = (items: MenuItems) => { |
||||
setState(prevState => { |
||||
const actions = prevState.actions.filter(({ id, name }) => items.findIndex(item => id === item.id && name === item.name) === -1) |
||||
return { ...prevState, actions } |
||||
}) |
||||
} |
||||
|
||||
const extractNameFromKey = (key: string):string => { |
||||
const keyPath = key.split('/') |
||||
|
||||
return keyPath[keyPath.length - 1] |
||||
} |
||||
|
||||
const hasReservedKeyword = (content: string): boolean => { |
||||
if (state.reservedKeywords.findIndex(value => content.startsWith(value)) !== -1) return true |
||||
else return false |
||||
} |
||||
|
||||
const getFocusedFolder = () => { |
||||
if (global.fs.focusElement[0]) { |
||||
if (global.fs.focusElement[0].type === 'folder' && global.fs.focusElement[0].key) return global.fs.focusElement[0].key |
||||
else if (global.fs.focusElement[0].type === 'gist' && global.fs.focusElement[0].key) return global.fs.focusElement[0].key |
||||
else if (global.fs.focusElement[0].type === 'file' && global.fs.focusElement[0].key) return extractParentFromKey(global.fs.focusElement[0].key) ? extractParentFromKey(global.fs.focusElement[0].key) : name |
||||
else return name |
||||
} |
||||
} |
||||
|
||||
const createNewFile = async (newFilePath: string) => { |
||||
try { |
||||
global.dispatchCreateNewFile(newFilePath, props.name) |
||||
} catch (error) { |
||||
return global.modal('File Creation Failed', typeof error === 'string' ? error : error.message, 'Close', async () => {}) |
||||
} |
||||
} |
||||
|
||||
const createNewFolder = async (newFolderPath: string) => { |
||||
try { |
||||
global.dispatchCreateNewFolder(newFolderPath, props.name) |
||||
} catch (e) { |
||||
return global.modal('Folder Creation Failed', typeof e === 'string' ? e : e.message, 'Close', async () => {}) |
||||
} |
||||
} |
||||
|
||||
const deletePath = async (path: string[]) => { |
||||
if (global.fs.readonly) return global.toast('cannot delete file. ' + name + ' is a read only explorer') |
||||
if (!Array.isArray(path)) path = [path] |
||||
|
||||
global.modal(`Delete ${path.length > 1 ? 'items' : 'item'}`, deleteMessage(path), 'OK', () => { global.dispatchDeletePath(path) }, 'Cancel', () => {}) |
||||
} |
||||
|
||||
const renamePath = async (oldPath: string, newPath: string) => { |
||||
try { |
||||
global.dispatchRenamePath(oldPath, newPath) |
||||
} catch (error) { |
||||
global.modal('Rename File Failed', 'Unexpected error while renaming: ' + typeof error === 'string' ? error : error.message, 'Close', async () => {}) |
||||
} |
||||
} |
||||
|
||||
const uploadFile = (target) => { |
||||
const parentFolder = getFocusedFolder() |
||||
const expandPath = [...new Set([...state.expandPath, parentFolder])] |
||||
|
||||
setState(prevState => { |
||||
return { ...prevState, expandPath } |
||||
}) |
||||
global.dispatchUploadFile(target, parentFolder) |
||||
} |
||||
|
||||
const copyFile = (src: string, dest: string) => { |
||||
try { |
||||
global.dispatchCopyFile(src, dest) |
||||
} catch (error) { |
||||
global.modal('Copy File Failed', 'Unexpected error while copying file: ' + src, 'Close', async () => {}) |
||||
} |
||||
} |
||||
|
||||
const copyFolder = (src: string, dest: string) => { |
||||
try { |
||||
global.dispatchCopyFolder(src, dest) |
||||
} catch (error) { |
||||
global.modal('Copy Folder Failed', 'Unexpected error while copying folder: ' + src, 'Close', async () => {}) |
||||
} |
||||
} |
||||
|
||||
const publishToGist = (path?: string, type?: string) => { |
||||
global.modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${name} workspace as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {}) |
||||
} |
||||
|
||||
const pushChangesToGist = (path?: string, type?: string) => { |
||||
global.modal('Create a public gist', 'Are you sure you want to push changes to remote gist file on github.com?', 'OK', () => toGist(path, type), 'Cancel', () => {}) |
||||
} |
||||
|
||||
const publishFolderToGist = (path?: string, type?: string) => { |
||||
global.modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${path} folder as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {}) |
||||
} |
||||
|
||||
const publishFileToGist = (path?: string, type?: string) => { |
||||
global.modal('Create a public gist', `Are you sure you want to anonymously publish ${path} file as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {}) |
||||
} |
||||
|
||||
const toGist = (path?: string, type?: string) => { |
||||
global.dispatchPublishToGist(path, type) |
||||
} |
||||
|
||||
const runScript = async (path: string) => { |
||||
try { |
||||
global.dispatchRunScript(path) |
||||
} catch (error) { |
||||
global.toast('Run script failed') |
||||
} |
||||
} |
||||
|
||||
const emitContextMenuEvent = (cmd: customAction) => { |
||||
try { |
||||
global.dispatchEmitContextMenuEvent(cmd) |
||||
} catch (error) { |
||||
global.toast(error) |
||||
} |
||||
} |
||||
|
||||
const handleClickFile = (path: string, type: 'folder' | 'file' | 'gist') => { |
||||
path = path.indexOf(props.name + '/') === 0 ? path.replace(props.name + '/', '') : path |
||||
if (!state.ctrlKey) { |
||||
global.dispatchHandleClickFile(path, type) |
||||
} else { |
||||
if (global.fs.focusElement.findIndex(item => item.key === path) !== -1) { |
||||
const focusElement = global.fs.focusElement.filter(item => item.key !== path) |
||||
|
||||
global.dispatchSetFocusElement(focusElement) |
||||
} else { |
||||
const nonRootFocus = global.fs.focusElement.filter((el) => { return !(el.key === '' && el.type === 'folder') }) |
||||
|
||||
nonRootFocus.push({ key: path, type }) |
||||
global.dispatchSetFocusElement(nonRootFocus) |
||||
} |
||||
} |
||||
} |
||||
|
||||
const handleClickFolder = async (path: string, type: 'folder' | 'file' | 'gist') => { |
||||
if (state.ctrlKey) { |
||||
if (global.fs.focusElement.findIndex(item => item.key === path) !== -1) { |
||||
const focusElement = global.fs.focusElement.filter(item => item.key !== path) |
||||
|
||||
global.dispatchSetFocusElement(focusElement) |
||||
} else { |
||||
const nonRootFocus = global.fs.focusElement.filter((el) => { return !(el.key === '' && el.type === 'folder') }) |
||||
|
||||
nonRootFocus.push({ key: path, type }) |
||||
global.dispatchSetFocusElement(nonRootFocus) |
||||
} |
||||
} else { |
||||
let expandPath = [] |
||||
|
||||
if (!state.expandPath.includes(path)) { |
||||
expandPath = [...new Set([...state.expandPath, path])] |
||||
global.dispatchFetchDirectory(path) |
||||
} else { |
||||
expandPath = [...new Set(state.expandPath.filter(key => key && (typeof key === 'string') && !key.startsWith(path)))] |
||||
} |
||||
|
||||
global.dispatchSetFocusElement([{ key: path, type }]) |
||||
setState(prevState => { |
||||
return { ...prevState, expandPath } |
||||
}) |
||||
} |
||||
} |
||||
|
||||
const handleContextMenuFile = (pageX: number, pageY: number, path: string, content: string, type: string) => { |
||||
if (!content) return |
||||
setState(prevState => { |
||||
return { ...prevState, focusContext: { element: path, x: pageX, y: pageY, type }, focusEdit: { ...prevState.focusEdit, lastEdit: content }, showContextMenu: prevState.focusEdit.element !== path } |
||||
}) |
||||
} |
||||
|
||||
const handleContextMenuFolder = (pageX: number, pageY: number, path: string, content: string, type: string) => { |
||||
if (!content) return |
||||
setState(prevState => { |
||||
return { ...prevState, focusContext: { element: path, x: pageX, y: pageY, type }, focusEdit: { ...prevState.focusEdit, lastEdit: content }, showContextMenu: prevState.focusEdit.element !== path } |
||||
}) |
||||
} |
||||
|
||||
const hideContextMenu = () => { |
||||
setState(prevState => { |
||||
return { ...prevState, focusContext: { element: null, x: 0, y: 0, type: '' }, showContextMenu: false } |
||||
}) |
||||
} |
||||
|
||||
const editModeOn = (path: string, type: string, isNew: boolean = false) => { |
||||
if (global.fs.readonly) return |
||||
setState(prevState => { |
||||
return { ...prevState, focusEdit: { ...prevState.focusEdit, element: path, isNew, type } } |
||||
}) |
||||
} |
||||
|
||||
const editModeOff = async (content: string) => { |
||||
if (typeof content === 'string') content = content.trim() |
||||
const parentFolder = extractParentFromKey(state.focusEdit.element) |
||||
|
||||
if (!content || (content.trim() === '')) { |
||||
if (state.focusEdit.isNew) { |
||||
global.dispatchRemoveInputField(parentFolder) |
||||
setState(prevState => { |
||||
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } } |
||||
}) |
||||
} else { |
||||
editRef.current.textContent = state.focusEdit.lastEdit |
||||
setState(prevState => { |
||||
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } } |
||||
}) |
||||
} |
||||
} else { |
||||
if (state.focusEdit.lastEdit === content) { |
||||
editRef.current.textContent = content |
||||
return setState(prevState => { |
||||
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } } |
||||
}) |
||||
} |
||||
if (checkSpecialChars(content)) { |
||||
global.modal('Validation Error', 'Special characters are not allowed', 'OK', () => {}) |
||||
} else { |
||||
if (state.focusEdit.isNew) { |
||||
if (hasReservedKeyword(content)) { |
||||
global.dispatchRemoveInputField(parentFolder) |
||||
global.modal('Reserved Keyword', `File name contains remix reserved keywords. '${content}'`, 'Close', () => {}) |
||||
} else { |
||||
state.focusEdit.type === 'file' ? createNewFile(joinPath(parentFolder, content)) : createNewFolder(joinPath(parentFolder, content)) |
||||
global.dispatchRemoveInputField(parentFolder) |
||||
} |
||||
} else { |
||||
if (hasReservedKeyword(content)) { |
||||
editRef.current.textContent = state.focusEdit.lastEdit |
||||
global.modal('Reserved Keyword', `File name contains remix reserved keywords. '${content}'`, 'Close', () => {}) |
||||
} else { |
||||
const oldPath: string = state.focusEdit.element |
||||
const oldName = extractNameFromKey(oldPath) |
||||
const newPath = oldPath.replace(oldName, content) |
||||
|
||||
editRef.current.textContent = extractNameFromKey(oldPath) |
||||
renamePath(oldPath, newPath) |
||||
} |
||||
} |
||||
setState(prevState => { |
||||
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } } |
||||
}) |
||||
} |
||||
} |
||||
} |
||||
|
||||
const handleNewFileInput = async (parentFolder?: string) => { |
||||
if (!parentFolder) parentFolder = getFocusedFolder() |
||||
const expandPath = [...new Set([...state.expandPath, parentFolder])] |
||||
|
||||
await global.dispatchAddInputField(parentFolder, 'file') |
||||
setState(prevState => { |
||||
return { ...prevState, expandPath } |
||||
}) |
||||
editModeOn(parentFolder + '/blank', 'file', true) |
||||
} |
||||
|
||||
const handleNewFolderInput = async (parentFolder?: string) => { |
||||
if (!parentFolder) parentFolder = getFocusedFolder() |
||||
else if ((parentFolder.indexOf('.sol') !== -1) || (parentFolder.indexOf('.js') !== -1)) parentFolder = extractParentFromKey(parentFolder) |
||||
const expandPath = [...new Set([...state.expandPath, parentFolder])] |
||||
|
||||
await global.dispatchAddInputField(parentFolder, 'folder') |
||||
setState(prevState => { |
||||
return { ...prevState, expandPath } |
||||
}) |
||||
editModeOn(parentFolder + '/blank', 'folder', true) |
||||
} |
||||
|
||||
const handleEditInput = (event) => { |
||||
if (event.which === 13) { |
||||
event.preventDefault() |
||||
editModeOff(editRef.current.innerText) |
||||
} |
||||
} |
||||
|
||||
const handleMouseOver = (path: string) => { |
||||
setState(prevState => { |
||||
return { ...prevState, mouseOverElement: path } |
||||
}) |
||||
} |
||||
|
||||
const handleMouseOut = () => { |
||||
setState(prevState => { |
||||
return { ...prevState, mouseOverElement: null } |
||||
}) |
||||
} |
||||
|
||||
const handleCopyClick = (path: string, type: 'folder' | 'gist' | 'file') => { |
||||
setState(prevState => { |
||||
return { ...prevState, copyElement: [{ key: path, type }] } |
||||
}) |
||||
setCanPaste(true) |
||||
global.toast(`Copied to clipboard ${path}`) |
||||
} |
||||
|
||||
const handlePasteClick = (dest: string, destType: string) => { |
||||
dest = destType === 'file' ? extractParentFromKey(dest) || props.name : dest |
||||
state.copyElement.map(({ key, type }) => { |
||||
type === 'file' ? copyFile(key, dest) : copyFolder(key, dest) |
||||
}) |
||||
} |
||||
|
||||
const deleteMessage = (path: string[]) => { |
||||
return ( |
||||
<div> |
||||
<div>Are you sure you want to delete {path.length > 1 ? 'these items' : 'this item'}?</div> |
||||
{ |
||||
path.map((item, i) => (<li key={i}>{item}</li>)) |
||||
} |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
const label = (file: File) => { |
||||
const isEditable = (state.focusEdit.element === file.path) || (global.fs.focusEdit === file.path) |
||||
|
||||
return ( |
||||
<div |
||||
className='remixui_items d-inline-block w-100' |
||||
ref={ isEditable ? editRef : null} |
||||
suppressContentEditableWarning={true} |
||||
contentEditable={isEditable} |
||||
onKeyDown={handleEditInput} |
||||
onBlur={(e) => { |
||||
e.stopPropagation() |
||||
editModeOff(editRef.current.innerText) |
||||
}} |
||||
> |
||||
<span |
||||
title={file.path} |
||||
className={'remixui_label ' + (file.isDirectory ? 'folder' : 'remixui_leaf')} |
||||
data-path={file.path} |
||||
> |
||||
{ file.name } |
||||
</span> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
const renderFiles = (file: File, index: number) => { |
||||
if (!file || !file.path || typeof file === 'string' || typeof file === 'number' || typeof file === 'boolean') return |
||||
const labelClass = state.focusEdit.element === file.path |
||||
? 'bg-light' : global.fs.focusElement.findIndex(item => item.key === file.path) !== -1 |
||||
? 'bg-secondary' : state.mouseOverElement === file.path |
||||
? 'bg-light border' : (state.focusContext.element === file.path) && (state.focusEdit.element !== file.path) |
||||
? 'bg-light border' : '' |
||||
const icon = getPathIcon(file.path) |
||||
const spreadProps = { |
||||
onClick: (e) => e.stopPropagation() |
||||
} |
||||
|
||||
if (file.isDirectory) { |
||||
return ( |
||||
<TreeViewItem |
||||
id={`treeViewItem${file.path}`} |
||||
iconX='pr-3 fa fa-folder' |
||||
iconY='pr-3 fa fa-folder-open' |
||||
key={`${file.path + index}`} |
||||
label={label(file)} |
||||
onClick={(e) => { |
||||
e.stopPropagation() |
||||
if (state.focusEdit.element !== file.path) handleClickFolder(file.path, file.type) |
||||
}} |
||||
onContextMenu={(e) => { |
||||
e.preventDefault() |
||||
e.stopPropagation() |
||||
handleContextMenuFolder(e.pageX, e.pageY, file.path, e.target.textContent, file.type) |
||||
}} |
||||
labelClass={labelClass} |
||||
controlBehaviour={ state.ctrlKey } |
||||
expand={state.expandPath.includes(file.path)} |
||||
onMouseOver={(e) => { |
||||
e.stopPropagation() |
||||
handleMouseOver(file.path) |
||||
}} |
||||
onMouseOut={(e) => { |
||||
e.stopPropagation() |
||||
if (state.mouseOverElement === file.path) handleMouseOut() |
||||
}} |
||||
> |
||||
{ |
||||
file.child ? <TreeView id={`treeView${file.path}`} key={`treeView${file.path}`} {...spreadProps }>{ |
||||
Object.keys(file.child).map((key, index) => { |
||||
return renderFiles(file.child[key], index) |
||||
}) |
||||
} |
||||
</TreeView> : <TreeView id={`treeView${file.path}`} key={`treeView${file.path}`} {...spreadProps }/> |
||||
} |
||||
</TreeViewItem> |
||||
) |
||||
} else { |
||||
return ( |
||||
<TreeViewItem |
||||
id={`treeViewItem${file.path}`} |
||||
key={`treeView${file.path}`} |
||||
label={label(file)} |
||||
onClick={(e) => { |
||||
e.stopPropagation() |
||||
if (state.focusEdit.element !== file.path) handleClickFile(file.path, file.type) |
||||
}} |
||||
onContextMenu={(e) => { |
||||
e.preventDefault() |
||||
e.stopPropagation() |
||||
handleContextMenuFile(e.pageX, e.pageY, file.path, e.target.textContent, file.type) |
||||
}} |
||||
icon={icon} |
||||
labelClass={labelClass} |
||||
onMouseOver={(e) => { |
||||
e.stopPropagation() |
||||
handleMouseOver(file.path) |
||||
}} |
||||
onMouseOut={(e) => { |
||||
e.stopPropagation() |
||||
if (state.mouseOverElement === file.path) handleMouseOut() |
||||
}} |
||||
/> |
||||
) |
||||
} |
||||
} |
||||
|
||||
return ( |
||||
<div> |
||||
<TreeView id='treeView'> |
||||
<TreeViewItem id="treeViewItem" |
||||
controlBehaviour={true} |
||||
label={ |
||||
<div onClick={(e) => { |
||||
e.stopPropagation() |
||||
if (e && (e.target as any).getAttribute('data-id') === 'fileExplorerUploadFileuploadFile') return // we don't want to let propagate the input of type file
|
||||
if (e && (e.target as any).getAttribute('data-id') === 'fileExplorerFileUpload') return // we don't want to let propagate the input of type file
|
||||
let expandPath = [] |
||||
|
||||
if (!state.expandPath.includes(props.name)) { |
||||
expandPath = [props.name, ...new Set([...state.expandPath])] |
||||
} else { |
||||
expandPath = [...new Set(state.expandPath.filter(key => key && (typeof key === 'string') && !key.startsWith(props.name)))] |
||||
} |
||||
setState(prevState => { |
||||
return { ...prevState, expandPath } |
||||
}) |
||||
resetFocus(true) |
||||
}}> |
||||
<FileExplorerMenu |
||||
title={''} |
||||
menuItems={props.menuItems} |
||||
createNewFile={handleNewFileInput} |
||||
createNewFolder={handleNewFolderInput} |
||||
publishToGist={publishToGist} |
||||
uploadFile={uploadFile} |
||||
/> |
||||
</div> |
||||
} |
||||
expand={true}> |
||||
<div className='pb-2'> |
||||
<TreeView id='treeViewMenu'> |
||||
{ |
||||
files[props.name] && Object.keys(files[props.name]).map((key, index) => { |
||||
return renderFiles(files[props.name][key], index) |
||||
}) |
||||
} |
||||
</TreeView> |
||||
</div> |
||||
</TreeViewItem> |
||||
</TreeView> |
||||
{ state.showContextMenu && |
||||
<FileExplorerContextMenu |
||||
actions={global.fs.focusElement.length > 1 ? state.actions.filter(item => item.multiselect) : state.actions.filter(item => !item.multiselect)} |
||||
hideContextMenu={hideContextMenu} |
||||
createNewFile={handleNewFileInput} |
||||
createNewFolder={handleNewFolderInput} |
||||
deletePath={deletePath} |
||||
renamePath={editModeOn} |
||||
runScript={runScript} |
||||
copy={handleCopyClick} |
||||
paste={handlePasteClick} |
||||
emit={emitContextMenuEvent} |
||||
pageX={state.focusContext.x} |
||||
pageY={state.focusContext.y} |
||||
path={state.focusContext.element} |
||||
type={state.focusContext.type} |
||||
focus={global.fs.focusElement} |
||||
onMouseOver={(e) => { |
||||
e.stopPropagation() |
||||
handleMouseOver(state.focusContext.element) |
||||
}} |
||||
pushChangesToGist={pushChangesToGist} |
||||
publishFolderToGist={publishFolderToGist} |
||||
publishFileToGist={publishFileToGist} |
||||
/> |
||||
} |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
export default FileExplorer |
@ -0,0 +1,28 @@ |
||||
.remixui_contextContainer |
||||
{ |
||||
display: block; |
||||
position: fixed; |
||||
border-radius: 2px; |
||||
z-index: 1000; |
||||
box-shadow: 0 0 4px var(--dark); |
||||
} |
||||
.remixui_contextContainer:focus { |
||||
outline: none; |
||||
} |
||||
.remixui_liitem |
||||
{ |
||||
padding: 2px; |
||||
padding-left: 6px; |
||||
cursor: pointer; |
||||
color: var(--text-dark); |
||||
background-color: var(--light); |
||||
} |
||||
.remixui_liitem:hover |
||||
{ |
||||
background-color: var(--secondary); |
||||
} |
||||
#remixui_menuitems |
||||
{ |
||||
list-style: none; |
||||
margin: 0px; |
||||
} |
@ -0,0 +1,56 @@ |
||||
.remixui_label { |
||||
margin-top : 4px; |
||||
} |
||||
.remixui_leaf { |
||||
overflow : hidden; |
||||
text-overflow : ellipsis; |
||||
width : 90%; |
||||
margin-bottom : 0px; |
||||
} |
||||
.remixui_fileexplorer { |
||||
box-sizing : border-box; |
||||
user-select : none; |
||||
} |
||||
input[type="file"] { |
||||
display: none; |
||||
} |
||||
.remixui_folder, |
||||
.remixui_file { |
||||
font-size : 14px; |
||||
cursor : pointer; |
||||
} |
||||
.remixui_file { |
||||
padding : 4px; |
||||
} |
||||
.remixui_newFile { |
||||
padding-right : 10px; |
||||
} |
||||
.remixui_newFile i { |
||||
cursor : pointer; |
||||
} |
||||
.remixui_newFile:hover { |
||||
transform : scale(1.3); |
||||
} |
||||
.remixui_menu { |
||||
margin-left : 20px; |
||||
} |
||||
.remixui_items { |
||||
display : inline |
||||
} |
||||
.remixui_remove { |
||||
margin-left : auto; |
||||
padding-left : 5px; |
||||
padding-right : 5px; |
||||
} |
||||
.remixui_activeMode { |
||||
display : flex; |
||||
width : 100%; |
||||
margin-right : 10px; |
||||
padding-right : 19px; |
||||
} |
||||
.remixui_activeMode > div { |
||||
min-width : 10px; |
||||
} |
||||
ul { |
||||
padding : 0; |
||||
} |
@ -0,0 +1,61 @@ |
||||
.remixui_container { |
||||
display : flex; |
||||
flex-direction : row; |
||||
width : 100%; |
||||
height : 100%; |
||||
box-sizing : border-box; |
||||
} |
||||
.remixui_fileexplorer { |
||||
display : flex; |
||||
flex-direction : column; |
||||
position : relative; |
||||
width : 100%; |
||||
padding-left : 6px; |
||||
padding-right : 6px; |
||||
padding-top : 6px; |
||||
} |
||||
.remixui_fileExplorerTree { |
||||
cursor : default; |
||||
} |
||||
.remixui_gist { |
||||
padding : 10px; |
||||
} |
||||
.remixui_gist i { |
||||
cursor : pointer; |
||||
} |
||||
.remixui_gist i:hover { |
||||
color : orange; |
||||
} |
||||
.remixui_connectToLocalhost { |
||||
padding : 10px; |
||||
} |
||||
.remixui_connectToLocalhost i { |
||||
cursor : pointer; |
||||
} |
||||
.remixui_connectToLocalhost i:hover { |
||||
color : var(--secondary) |
||||
} |
||||
.remixui_uploadFile { |
||||
padding : 10px; |
||||
} |
||||
.remixui_uploadFile label:hover { |
||||
color : var(--secondary) |
||||
} |
||||
.remixui_uploadFile label { |
||||
cursor : pointer; |
||||
} |
||||
.remixui_treeview { |
||||
overflow-y : auto; |
||||
} |
||||
.remixui_dialog { |
||||
display: flex; |
||||
flex-direction: column; |
||||
} |
||||
.remixui_dialogParagraph { |
||||
margin-bottom: 2em; |
||||
word-break: break-word; |
||||
} |
||||
.remixui_menuicon { |
||||
padding-right : 10px; |
||||
} |
||||
|
@ -0,0 +1,61 @@ |
||||
export const contextMenuActions: MenuItems = [{ |
||||
id: 'newFile', |
||||
name: 'New File', |
||||
type: ['folder', 'gist'], |
||||
multiselect: false, |
||||
label: '' |
||||
}, { |
||||
id: 'newFolder', |
||||
name: 'New Folder', |
||||
type: ['folder', 'gist'], |
||||
multiselect: false, |
||||
label: '' |
||||
}, { |
||||
id: 'rename', |
||||
name: 'Rename', |
||||
type: ['file', 'folder'], |
||||
multiselect: false, |
||||
label: '' |
||||
}, { |
||||
id: 'delete', |
||||
name: 'Delete', |
||||
type: ['file', 'folder', 'gist'], |
||||
multiselect: false, |
||||
label: '' |
||||
}, { |
||||
id: 'run', |
||||
name: 'Run', |
||||
extension: ['.js'], |
||||
multiselect: false, |
||||
label: '' |
||||
}, { |
||||
id: 'pushChangesToGist', |
||||
name: 'Push changes to gist', |
||||
type: ['gist'], |
||||
multiselect: false, |
||||
label: '' |
||||
}, { |
||||
id: 'publishFolderToGist', |
||||
name: 'Publish folder to gist', |
||||
type: ['folder'], |
||||
multiselect: false, |
||||
label: '' |
||||
}, { |
||||
id: 'publishFileToGist', |
||||
name: 'Publish file to gist', |
||||
type: ['file'], |
||||
multiselect: false, |
||||
label: '' |
||||
}, { |
||||
id: 'copy', |
||||
name: 'Copy', |
||||
type: ['folder', 'file'], |
||||
multiselect: false, |
||||
label: '' |
||||
}, { |
||||
id: 'deleteAll', |
||||
name: 'Delete All', |
||||
type: ['folder', 'file'], |
||||
multiselect: true, |
||||
label: '' |
||||
}] |
Loading…
Reference in new issue