Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/components/B4ACloudCodeView/B4ACloudCodeView.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ import B4aCodeEditor from '../CodeEditor/B4aCodeEditor.react';
import { getExtension } from '../B4ACodeTree/B4ATreeActions';

export default class B4ACloudCodeView extends React.Component {
constructor(props) {
super(props);
this.editorRef = React.createRef();
}

// Selects the whole file in the editor. Returns false while the editor is
// still loading.
selectAll() {
return this.editorRef.current ? this.editorRef.current.selectAll() : false;
}

extensionDecoder() {
if (this.props.fileName && typeof this.props.fileName === 'string') {
return getExtension(this.props.fileName);
Expand All @@ -14,8 +25,11 @@ export default class B4ACloudCodeView extends React.Component {
return (
<div style={{ height: 'calc(100% - 40px)' }}>
<B4aCodeEditor
ref={this.editorRef}
fontSize={13}
fileName={this.props.fileName}
documentKey={this.props.fileId}
focusToken={this.props.focusToken}
code={this.props.source}
onCodeChange={value => this.props.onCodeChange(value)}
mode={this.extensionDecoder()}
Expand Down
97 changes: 85 additions & 12 deletions src/components/B4ACodeTree/B4ACodeTree.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ const getCloudFolderPlaceholder = () =>

const publicFolderPlaceholder = 'Public folder can be used to deploy public static content as html, images, css, etc.\n'

const modifiedFileIcon = require('./icons/file.png');

const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || '');

let cloudFolderPlaceholder;

const swalWithBootstrapButtons = Swal.mixin({
Expand Down Expand Up @@ -120,6 +124,9 @@ export default class B4ACodeTree extends React.Component {
// jstree's `changed.jstree` event in selectNode().
selectedTreePath: '',
showUploadMenu: false,
// Bumped every time the user opens a file, so the editor takes focus and
// typing/pasting right after picking a file lands in it.
editorFocusToken: 0,
}

// Used to track the latest file load request
Expand All @@ -128,6 +135,8 @@ export default class B4ACodeTree extends React.Component {
this.fileInputRef = React.createRef();
this.folderInputRef = React.createRef();
this.uploadMenuRef = React.createRef();
this.containerRef = React.createRef();
this.codeViewRef = React.createRef();
}

// Read the current tree state from jstree and mirror it into React state so
Expand Down Expand Up @@ -194,9 +203,53 @@ export default class B4ACodeTree extends React.Component {
if (!nodeId) {
return;
}
if (node.type === 'folder' || node.type === 'new-folder') {
B4ATreeActions.selectFileOnTree(nodeId);
} else {
this.openFileInEditor(nodeId);
}
}

openFileInEditor(nodeId) {
B4ATreeActions.selectFileOnTree(nodeId);
this.focusEditor();
}

focusEditor() {
this.setState(state => ({ editorFocusToken: state.editorFocusToken + 1 }));
}

// SweetAlert hands focus back to whatever had it before the dialog, and it
// does so after its promise has resolved. The editor can only take focus
// once that is done.
focusEditorAfterDialog = () => {
if (this.focusEditorOnDialogClose) {
this.focusEditorOnDialogClose = false;
this.focusEditor();
}
};

// Outside a text field, Cmd/Ctrl+A selects the whole page. The editor only
// renders the lines on screen, so copying that selection grabs a fragment
// of the file. Send the shortcut to the editor instead.
handleSelectAllShortcut = e => {
const modifier = isMac ? e.metaKey && !e.ctrlKey : e.ctrlKey && !e.metaKey;
if (!modifier || e.altKey || e.shiftKey || (e.key || '').toLowerCase() !== 'a') {
return;
}
const target = e.target;
const container = this.containerRef.current;
if (!container || !target || (target !== document.body && !container.contains(target))) {
return;
}
if (target.closest && target.closest('input, textarea, select, [contenteditable], .monaco-editor')) {
return;
}
if (this.codeViewRef.current && this.codeViewRef.current.selectAll()) {
e.preventDefault();
}
};

handleContextAction(action, node, path, newName) {
if (this.props.hideControls) {
return;
Expand Down Expand Up @@ -256,13 +309,15 @@ export default class B4ACodeTree extends React.Component {
confirmButtonText: 'Create file',
buttonsStyling: false,
showCloseButton: true,
allowOutsideClick: () => !Swal.isLoading()
allowOutsideClick: () => !Swal.isLoading(),
didClose: this.focusEditorAfterDialog
}).then(({ value }) => {
if (value) {
value = B4ATreeActions.sanitizeHTML(value);
const parent = parentNodeId ? [parentNodeId] : B4ATreeActions.getSelectedParent();
const newNodeId = B4ATreeActions.addFileOnSelectedNode(value, parent[0]);
B4ATreeActions.selectFileOnTree(newNodeId);
this.focusEditorOnDialogClose = true;
this.setState({ files: $('#tree').jstree(true).get_json() });
}
});
Expand Down Expand Up @@ -594,20 +649,31 @@ export default class B4ACodeTree extends React.Component {
return this.props.parentState({ unsavedChanges: true })
}

async updateSelectedFileContent(value) {
// Runs on every keystroke and paste, so it stays synchronous (the node and
// `source` are never behind the editor) and only touches the tree the first
// time a file is modified.
updateSelectedFileContent(value) {
if (this.props.hideControls) { return; }
const ecodedValue = await B4ATreeActions.encodeFile(value, 'data:plain/text;base64');
this.setState({ source: value });
const tree = $('#tree').jstree(true);
const selectedNode = tree && tree.get_selected(true).pop();
if (!selectedNode) { return; }

this.state.selectedNodeData?.instance.set_icon(this.state.selectedNodeData.node, require('./icons/file.png'));
selectedNode.data.code = B4ATreeActions.encodeFile(value, 'data:plain/text;base64');
this.setState({ source: value });

$('#tree').jstree('get_selected', true).pop().data.code = ecodedValue;
$('#tree').jstree().redraw(true);
if (selectedNode.icon !== modifiedFileIcon) {
tree.set_icon(selectedNode, modifiedFileIcon);
tree.redraw(true);
}

// set updated files.
let cloneUpdatedFiles = [...this.props.updatedFiles];
if(!cloneUpdatedFiles.includes('j1_mainJS') && !cloneUpdatedFiles.includes('j1_indexHTML')){
this.props.cloudCodeChanges.addFile($('#tree').jstree('get_selected', true).pop().id);
const updatedFiles = this.props.updatedFiles;
if (
!updatedFiles.includes('j1_mainJS') &&
!updatedFiles.includes('j1_indexHTML') &&
!this.props.cloudCodeChanges.getFiles().includes(selectedNode.id)
) {
this.props.cloudCodeChanges.addFile(selectedNode.id);
this.props.setUpdatedFile(this.props.cloudCodeChanges.getFiles());
}
}
Expand Down Expand Up @@ -705,6 +771,7 @@ export default class B4ACodeTree extends React.Component {
}
$('#tree').jstree(config);
this.watchSelectedNode();
document.addEventListener('keydown', this.handleSelectAllShortcut);

// Mirror jstree's data into React state on every mutation so
// <B4aFileTree> stays in sync with the source of truth.
Expand Down Expand Up @@ -750,6 +817,7 @@ export default class B4ACodeTree extends React.Component {
}

componentWillUnmount() {
document.removeEventListener('keydown', this.handleSelectAllShortcut);
$('#tree').off(
'changed.jstree create_node.jstree delete_node.jstree rename_node.jstree move_node.jstree set_text.jstree refresh.jstree ready.jstree'
);
Expand Down Expand Up @@ -806,11 +874,14 @@ export default class B4ACodeTree extends React.Component {
)}
</div>
<B4ACloudCodeView
ref={this.codeViewRef}
isFolderSelected={this.state.isFolderSelected}
onCodeChange={value => this.updateSelectedFileContent(value)}
source={this.state.source}
extension={this.state.extension}
fileName={this.state.selectedFile}
fileId={this.state.nodeId}
focusToken={this.state.editorFocusToken}
readOnly={!!this.props.hideControls}
/>
</div>;
Expand Down Expand Up @@ -838,20 +909,22 @@ export default class B4ACodeTree extends React.Component {
confirmButtonText: 'Create file',
buttonsStyling: false,
showCloseButton: true,
allowOutsideClick: () => !Swal.isLoading()
allowOutsideClick: () => !Swal.isLoading(),
didClose: this.focusEditorAfterDialog
}).then(({ value }) => {
if (value) {
value = B4ATreeActions.sanitizeHTML(value);
const parent = B4ATreeActions.getSelectedParent();
const newNodeId = B4ATreeActions.addFileOnSelectedNode(value, parent[0]);
B4ATreeActions.selectFileOnTree(newNodeId);
this.focusEditorOnDialogClose = true;
this.setState({ files: $('#tree').jstree(true).get_json() });
}
});
};

return (
<div className={styles.codeContainer} style={this.props.style ? this.props.style : {}} id="codeContainer">
<div ref={this.containerRef} className={styles.codeContainer} style={this.props.style ? this.props.style : {}} id="codeContainer">
<div className={styles.fileSelector}>
<div className={styles.vscodeSidebar}>
<div className={styles.vscodeHeader}>
Expand Down
2 changes: 1 addition & 1 deletion src/components/B4ACodeTree/B4ATreeActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ const decodeFile = (code) => {
return Base64.decode(code.split(',')[1]);
}

const encodeFile = async (code, extension) => {
const encodeFile = (code, extension) => {
return extension + ',' + Base64.encode(code);
}

Expand Down
Loading
Loading