1
0
mirror of https://github.com/chinchang/web-maker.git synced 2025-07-28 09:10:17 +02:00
Files
php-web-maker/src/components/SidePane.jsx
2019-03-18 10:15:52 +05:30

287 lines
7.7 KiB
JavaScript

import { h, Component } from 'preact';
import { FileIcon } from './FileIcon';
import { getParentPath, getFileFromPath } from '../fileUtils';
import { trackEvent } from '../analytics';
const ENTER_KEY = 13;
const ESCAPE_KEY = 27;
function File({
file,
selectedFile,
fileBeingRenamed,
onFileSelect,
onRenameBtnClick,
onRemoveBtnClick,
onNameInputBlur,
onNameInputKeyUp,
onFileDrop
}) {
function focusInput(el) {
el &&
setTimeout(() => {
el.focus();
}, 1);
}
function dragStartHandler(e) {
e.dataTransfer.setData('text/plain', file.path);
}
function dragOverHandler(e) {
if (file.isFolder) {
e.preventDefault();
// e.stopPropagation();
e.currentTarget.classList.add('is-being-dragged-over');
e.currentTarget.style.outline = '1px dashed';
}
}
function dragLeaveHandler(e) {
if (file.isFolder) {
e.preventDefault();
e.currentTarget.style.outline = null;
}
}
function dropHandler(e) {
e.stopPropagation();
if (file.isFolder) {
e.preventDefault();
onFileDrop(e.dataTransfer.getData('text/plain'), file);
e.currentTarget.style.outline = null;
}
}
return (
<div
onDragOver={dragOverHandler}
onDragLeave={dragLeaveHandler}
onDrop={dropHandler}
>
{file === fileBeingRenamed ? (
<input
type="text"
ref={focusInput}
value={file.name}
onBlur={onNameInputBlur}
onKeyUp={onNameInputKeyUp}
/>
) : (
<button
class={`sidebar__file ${selectedFile === file ? 'selected' : ''}`}
type="button"
draggable={file.name !== 'index.html'}
onDragStart={dragStartHandler}
onClick={onFileSelect.bind(null, file)}
>
<div class="flex flex-v-center ov-h">
<FileIcon file={file} />
<div class="sidebar__file-name">{file.name}</div>
</div>
<div class="sidebar__file-options">
<button
type="button"
class="btn btn--dark btn--chromeless"
onClick={onRenameBtnClick.bind(null, file)}
>
<svg viewBox="0 0 24 24">
<path d="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z" />
</svg>
</button>
<button
type="button"
class="btn btn--dark btn--chromeless"
onClick={onRemoveBtnClick.bind(null, file)}
>
<svg viewBox="0 0 24 24">
<path d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z" />
</svg>
</button>
</div>
</button>
)}
</div>
);
}
function Folder(props) {
return (
<div>
<File {...props} file={props.file} />
<div class="sidebar__folder-wrap" data-collapsed={props.file.isCollapsed}>
{props.file.children.map(childFile =>
childFile.isFolder ? (
<Folder {...props} file={childFile} />
) : (
<File {...props} file={childFile} />
)
)}
</div>
</div>
);
}
export class SidePane extends Component {
addFileButtonClickHandler() {
this.setState({ isAddingFile: true });
trackEvent('ui', 'fileAddBtnClick');
}
addFolderButtonClickHandler() {
this.setState({ isAddingFolder: true });
trackEvent('ui', 'folderAddBtnClick');
}
/**
* Checks if the passed filename already exists and if so, warns the user.
* Also it passes false if the validation fails.
* @param {string} newFileName New file name to validate
*/
warnForExistingFileName(newFileName) {
// We also check for fileBeingRenamed !== file because when a file being renamed is
// asked to be set as same name, then that should not match and warn here.
let newPath = this.state.fileBeingRenamed
? `${getParentPath(this.state.fileBeingRenamed.path)}/${newFileName}`
: newFileName;
// remove first slash
newPath = newPath.replace(/^\//, '');
const match = getFileFromPath(this.props.files, newPath);
if (match.file && this.state.fileBeingRenamed !== match.file) {
alert(`A file with name ${newFileName} already exists.`);
return false;
}
return true;
}
addFile(e) {
// This gets called twice when enter is pressed, because blur also fires.
// So check `isAddingFile` before proceeding.
if (!this.state.isAddingFile && !this.state.isAddingFolder) {
return;
}
const newFileName = e.target.value;
if (!this.warnForExistingFileName(newFileName)) {
return;
}
if (newFileName) {
this.props.onAddFile(newFileName, this.state.isAddingFolder);
}
this.setState({ isAddingFile: false, isAddingFolder: false });
}
newFileNameInputKeyDownHandler(e) {
if (e.which === ENTER_KEY) {
this.addFile(e);
} else if (e.which === ESCAPE_KEY) {
this.setState({ isAddingFile: false, isAddingFolder: false });
}
}
removeFileClickHandler(file, e) {
e.stopPropagation();
const answer = confirm(`Are you sure you want to delete "${file.name}"?`);
if (answer) {
this.props.onRemoveFile(file.path);
}
trackEvent('ui', 'fileRemoveBtnClick');
}
renameFile(e) {
// This gets called twice when enter is pressed, because blur also fires.
if (!e.target || !this.state.fileBeingRenamed) {
return;
}
const newFileName = e.target.value;
if (!this.warnForExistingFileName(newFileName)) {
return;
}
if (newFileName) {
this.props.onRenameFile(this.state.fileBeingRenamed.path, newFileName);
}
this.setState({ fileBeingRenamed: null });
}
renameFileNameInputKeyUpHandler(e) {
if (e.which === ENTER_KEY) {
this.renameFile(e);
} else if (e.which === ESCAPE_KEY) {
this.setState({ fileBeingRenamed: null });
}
}
renameFileClickHandler(file, e) {
e.stopPropagation();
this.setState({
fileBeingRenamed: file
});
trackEvent('ui', 'fileRenameBtnClick');
}
dragOverHandler(e) {
e.preventDefault();
}
dropHandler(e) {
e.preventDefault();
// Object with `children` key is to simulate a folder structure for root folder
this.props.onFileDrop(e.dataTransfer.getData('text/plain'), {
children: this.props.files
});
// e.currentTarget.style.outline = null;
}
render() {
const { files, onFileSelect, selectedFile, onRemoveFile } = this.props;
const moreProps = {
onRemoveBtnClick: this.removeFileClickHandler.bind(this),
onRenameBtnClick: this.renameFileClickHandler.bind(this),
onNameInputBlur: this.renameFile.bind(this),
onNameInputKeyUp: this.renameFileNameInputKeyUpHandler.bind(this),
fileBeingRenamed: this.state.fileBeingRenamed
};
return (
<div
class="sidebar"
onDragOver={this.dragOverHandler.bind(this)}
onDrop={this.dropHandler.bind(this)}
>
<div class="d-f jc-sb ai-c" style="padding: 5px 10px">
Files
<div class="flex ai-c">
<button
type="button"
class="btn btn--dark"
onClick={this.addFileButtonClickHandler.bind(this)}
>
<svg viewBox="0 0 24 24">
<path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M11,15V12H9V15H6V17H9V20H11V17H14V15H11Z" />
</svg>
</button>
<button
type="button"
class="btn btn--dark"
onClick={this.addFolderButtonClickHandler.bind(this)}
>
<svg viewBox="0 0 24 24">
<path d="M10,4L12,6H20A2,2 0 0,1 22,8V18A2,2 0 0,1 20,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10M15,9V12H12V14H15V17H17V14H20V12H17V9H15Z" />
</svg>
</button>
</div>
</div>
{this.state.isAddingFile || this.state.isAddingFolder ? (
<div>
<input
type="text"
ref={el => {
el &&
setTimeout(() => {
el.focus();
}, 1);
}}
onBlur={this.addFile.bind(this)}
onKeyUp={this.newFileNameInputKeyDownHandler.bind(this)}
/>
</div>
) : null}
{files.map(file =>
file.isFolder ? (
<Folder {...this.props} {...moreProps} file={file} />
) : (
<File {...this.props} {...moreProps} file={file} />
)
)}
</div>
);
}
}