From 1c4751ee1c2080c6d4f70bfb17f8ee3ae1a07a16 Mon Sep 17 00:00:00 2001 From: Colton Mercurio Date: Fri, 1 Apr 2016 04:43:34 -0500 Subject: [PATCH 1/2] adding a file browser logic and tab UI --- bower.json | 5 +- gulp-tasks/build-js.js | 1 + src/app/require.config.js | 7 +- src/app/startup.js | 1 + src/app/sys-global-observables.js | 5 + src/components/editor/editor.js | 11 + src/components/file-browser/file-browser.html | 7 + src/components/file-browser/file-browser.js | 513 ++++++++++++++++++ .../playground-layout/playground-layout.js | 13 + src/styles/_file-browser.scss | 47 ++ src/styles/main.scss | 1 + 11 files changed, 609 insertions(+), 2 deletions(-) create mode 100644 src/components/file-browser/file-browser.html create mode 100644 src/components/file-browser/file-browser.js create mode 100644 src/styles/_file-browser.scss diff --git a/bower.json b/bower.json index a105e493..11ed9f66 100644 --- a/bower.json +++ b/bower.json @@ -25,7 +25,10 @@ "jquery-fullscreen": "~1.1.4", "cjs": "*", "ace": "~1.2.2", - "browserfs": "0.5.8" + "browserfs": "0.5.8", + "ace": "~1.2.2", + "bootstrap-contextmenu": "^0.3.4", + "bootbox.js": "^4.4.0" }, "devDependencies": {}, "resolutions": { diff --git a/gulp-tasks/build-js.js b/gulp-tasks/build-js.js index fe3db887..a280ea04 100644 --- a/gulp-tasks/build-js.js +++ b/gulp-tasks/build-js.js @@ -53,6 +53,7 @@ const requireJsOptimizerFilesConfig = [ 'components/vm-state-label/vm-state-label', 'components/compiler-state-label/compiler-state-label', 'components/not-found-page/not-found-page', + 'components/file-browser/file-browser', 'ace/mode/c_cpp', 'ace/theme/monokai', 'ace/theme/terminal', diff --git a/src/app/require.config.js b/src/app/require.config.js index 67d15f29..414b76e9 100644 --- a/src/app/require.config.js +++ b/src/app/require.config.js @@ -29,6 +29,9 @@ require.config({ "FileSaver": "bower_modules/FileSaver/FileSaver.min", // exports window global "saveAs" "Blob": "bower_modules/Blob/Blob", // exports window global "Blob" "browserfs": "bower_modules/browserfs/dist/browserfs.min", + "bootstrap-contextmenu":"bower_modules/bootstrap-contextmenu/bootstrap-contextmenu", + "bootbox": "bower_modules/bootbox.js/bootbox", + // Application-specific modules "app/config": "app/config/config.dev" // overridden to 'config.dist' in build config @@ -38,7 +41,9 @@ require.config({ "jquery-ui": { deps: ["jquery"] }, "jquery-ui-layout": { deps: ["jquery", "jquery-ui"] }, "jquery-fullscreen": { deps: ["jquery"] }, - "typeahead-jquery": { deps: ["jquery"] } + "typeahead-jquery": { deps: ["jquery"] }, + "bootbox": { deps: ["jquery"] }, + "bootstrap-contextmenu": { deps: ["bootstrap"] } }, packages: [ { diff --git a/src/app/startup.js b/src/app/startup.js index 84faf2b5..e4db642a 100644 --- a/src/app/startup.js +++ b/src/app/startup.js @@ -38,6 +38,7 @@ ko.components.register('playground-footer', { require: 'components/playground-fo ko.components.register('vm-state-label', { require: 'components/vm-state-label/vm-state-label' }); ko.components.register('compiler-state-label', { require: 'components/compiler-state-label/compiler-state-label' }); ko.components.register('not-found-page', { require: 'components/not-found-page/not-found-page' }); +ko.components.register('file-browser', { require: 'components/file-browser/file-browser' }); // [Scaffolded component registrations will be inserted here. To retain this feature, don't remove this comment.] // Start the application diff --git a/src/app/sys-global-observables.js b/src/app/sys-global-observables.js index 659f284f..a4b154a4 100644 --- a/src/app/sys-global-observables.js +++ b/src/app/sys-global-observables.js @@ -15,4 +15,9 @@ export var gccErrorCount = ko.observable(0); export var gccWarningCount = ko.observable(0); export var editorAnnotations = ko.observableArray([]); +export var currentFileName = ko.observable('untitled'); +export var currentFilePath = ko.observable(''); + export var projectLicense = ko.observable(''); + +export var Editor = {}; diff --git a/src/components/editor/editor.js b/src/components/editor/editor.js index 7f325253..0b2fe848 100644 --- a/src/components/editor/editor.js +++ b/src/components/editor/editor.js @@ -6,6 +6,7 @@ import 'bloodhound'; import TokenHighlighter from 'components/editor/token-highlighter'; import 'Blob'; import 'FileSaver'; +import * as SysGlobalObservables from 'app/sys-global-observables'; class Editor { constructor(params) { @@ -54,6 +55,8 @@ class Editor { $(window).resize(this.resize.bind(this)); params.editorTextGetter(this.getText.bind(this)); + + SysGlobalObservables.Editor = this; } initAce(editorDivId) { @@ -227,6 +230,14 @@ class Editor { return this.aceEditor.getSession().getValue(); } + setFile(path, filename, text) { + var session = this.aceEditor.getSession(); + + session.setValue(text); + + return; + } + resize() { // We resize after a timeout because when the window resize handler is called, // the window may not have resized completely, and hence the calculation below would diff --git a/src/components/file-browser/file-browser.html b/src/components/file-browser/file-browser.html new file mode 100644 index 00000000..0e44cafb --- /dev/null +++ b/src/components/file-browser/file-browser.html @@ -0,0 +1,7 @@ +
+
+ +
+
Loading...
+
\ No newline at end of file diff --git a/src/components/file-browser/file-browser.js b/src/components/file-browser/file-browser.js new file mode 100644 index 00000000..3044da11 --- /dev/null +++ b/src/components/file-browser/file-browser.js @@ -0,0 +1,513 @@ +import ko from 'knockout'; +import templateMarkup from 'text!./file-browser.html'; +import 'knockout-projections'; +import SysRuntime from 'app/sys-runtime'; +import SysFileSystem from 'app/sys-filesystem'; +import bootbox from 'bootbox'; +import 'bootstrap-contextmenu'; +import * as SysGlobalObservables from 'app/sys-global-observables'; + +var fbCalled = false; + +// notification options +var warningNotific8Options = { + life: 5000, + theme: 'ruby', + icon: 'exclamation-triangle' +}; + +var busyNotific8Options = { + life: 5000, + theme: 'lemon', + icon: 'info-circled' +}; + +var confirmNotific8Options = { + life: 5000, + theme: 'lime', + icon: 'check-mark-2' +}; + +class Filebrowser { + constructor() { + if (fbCalled) { + fbCalled = false; + return; + } + + fbCalled = true; + + var readyCallback = () => { + this.id = '#file-browser-body'; + var fs = this.fs = SysFileSystem; + + //TODO we are using the TokenHighligher to get a reference to the current Ace Editor... find a direct reference + this.editor = SysGlobalObservables.Editor; + + this.depth = -1; + this.directoryState = []; + this.activePath = ''; + + // refresh the file browser on file system changes + fs.addChangeListener(() => { + setTimeout(() => { + this.refresh(); + }, 300); + }); + + // double-click: load item to editor + $(this.id).on('dblclick', '.item', (e) => { + var itemId = $(e.currentTarget).data('id'); + var itemName = this.metaData[itemId].name; + + if (this.metaData[itemId].isDirectory) { + // do nothing + } + else { + try { + this.makeActive(null); + var content = fs.readFileSync(this.metaData[itemId].path).toString('binary'); + this.makeActive(this.metaData[itemId].path); + this.editor.setFile(self.metaData[itemId].path, self.metaData[itemId].name, content); + + + $.notific8('"' + itemName + '" is loaded', confirmNotific8Options); + $('span:contains("Code")').click().blur(); + } + catch (e) { + $.notific8('Cannot load "' + itemName + '"', warningNotific8Options); + } + } + }); + + // Save Hotkey + this.editor.addKeyboardCommand( + 'saveFile', + { + win: 'Ctrl-S', + mac: 'Command-S', + sender: 'editor|cli' + }, + function(env, args, request) { + self.saveActiveFile(); + } + ); + + var rightClickedItem; + var self = this; + $(this.id).contextmenu({ + target: '#file-browser-context-menu', + before: function(e, context) { + e.preventDefault(); + // execute code before context menu if shown + rightClickedItem = $(e.target); + var itemId = rightClickedItem.data('id'); + var menuContainer = this.getMenu().find('ul'); + var menuHtml = ''; + + if (self.metaData[itemId].isDirectory) { + menuHtml += '
  • New File
  • '; + menuHtml += '
  • New Directory
  • '; + } + menuHtml += '
  • Rename
  • '; + menuHtml += '
  • Delete
  • '; + + menuContainer.html(menuHtml); + }, + onItem: function(context, e) { + // execute on menu item selection + var $target = $(e.target); + var action = $target.data('action'); + var itemId = rightClickedItem.data('id'); + var itemName = self.metaData[itemId].name; + var itemPath = self.metaData[itemId].path; + var index; + + if (action === 'delete') { + bootbox.confirm('Are you sure you want to delete "' + itemName + '" ?', function (result) { + if (result) { + if (self.metaData[itemId].isDirectory) { + self.fs.removeDirectory(itemPath); + } + else { + self.fs.deleteFile(itemPath); + } + } + }); + } + else if (action === 'rename') { + bootbox.prompt('Insert new name', function (result) { + if (result === null || result.trim().length === 0) { + // do nothing + } + else { + index = itemPath.indexOf(itemName); + if (index === -1) { + return; + } + else { + self.fs.rename(itemPath, itemPath.slice(0, index) + result); + } + } + }); + } + else if (action === 'newDir') { + bootbox.prompt('New directory name', function (result) { + if (result === null || result.trim().length === 0) { + // do nothing + } + else { + self.fs.makeDirectory(itemPath + '/' + result); + } + }); + } + else if (action === 'newFile') { + bootbox.prompt('New file name', function (result) { + if (result === null || result.trim().length === 0) { + // do nothing + } + else { + try { + self.fs.readFileSync(itemPath + '/' + result).toString('binary'); + bootbox.alert('File already exists!'); + } + catch (e) { + self.fs.writeFile(itemPath + '/' + result, ''); + } + } + }); + } + } + }); + + // on right-click + $('#file-browser').on('contextmenu', (e) => { + e.preventDefault(); + return false; + }); + + // toggle directory + $(this.id).on('click', '.folder', (e) => { + var $curr = $(e.currentTarget); + var itemId = $curr.data('id'); + var data = this.metaData[itemId]; + var children; + + var iconClass = { + closed: 'glyphicon glyphicon-chevron-right', + opened: 'glyphicon glyphicon-chevron-down' + }; + + // open + if ($curr.data('status') === 'closed') { + $curr.data('status', 'opened'); + + if (this.directoryState.indexOf(data.path) === -1) { + this.directoryState.push(data.path); + this.directoryState.sort(); + } + + $curr + .find('i') + .removeClass(iconClass.closed) + .addClass(iconClass.opened); + + children = this.fs.getDirectoryChildren(data.path); + this.assignChildren(data, children, data.path); + + $curr.trigger('opened'); + } + // collapse + else { + $curr.data('status', 'closed'); + + this.directoryState = this._removeElemFromArray(this.directoryState, data.path).sort(); + + $curr + .find('i') + .removeClass(iconClass.opened) + .addClass(iconClass.closed); + + for (var i = 0; i < data.children.length; i++) { + this.cleanUp(data.children[i]); + } + + $curr.trigger('closed'); + } + }); + + + + // init + this.init(); + + // make program.c active + if (this.metaDataPathLookUp['/program.c']) { + this.makeActive(null); + var content = fs.readFileSync('/program.c').toString('binary'); + this.makeActive('/program.c'); + this.editor.setFile(self.metaDataPathLookUp['/program.c'].path, self.metaDataPathLookUp['/program.c'].name, content); + } + }; + + if (SysRuntime.ready()) { + window.setTimeout(() => { + readyCallback(); + }, 300); + } + + SysRuntime.addListener('ready', () => { + readyCallback(); + }); + } + + _removeElemFromArray(arr) { + var what, a = arguments, L = a.length, ax; + while (L > 1 && arr.length) { + what = a[--L]; + while ((ax= arr.indexOf(what)) !== -1) { + arr.splice(ax, 1); + } + } + return arr; + } + + init() { + var fs = this.fs; + + this.itemPrefix = 'fs-item-'; + this.indent = 30; // pixel + + // init + this.children = []; + this.metaData = {}; + this.metaDataPathLookUp = {}; + + this.itemCounter = 0; + + this.assignChildren(this, [{ + isDirectory: true, + name: 'home', + isRoot: true + }]); + + $(this.id).css('margin-bottom', '0px'); + this.draw(); + + // mimic the state + this.retrieveStates(); + } + + assignChildren(self, children, newPath) { + var i; + + // clean up existing children + for (i = 0; i < self.children.length; i++) { + this.cleanUp(self.children[i]); + } + self.children = []; + + // sort before assign + children.sort((a, b) => { + if (a.isDirectory && b.isDirectory) { + if (a.name > b.name) { + return -1; + } + if (b.name < a.name) { + return 1; + } + return 0; + } + else if (a.isDirectory && !b.isDirectory) { + return -1; + } + else if (!a.isDirectory && b.isDirectory) { + return 1; + } + else { + if (a.name > b.name) { + return -1; + } + if (b.name < a.name) { + return 1; + } + return 0; + } + }); + + // assign new children + var itemData; + var path; + var activeOneExists = false; + + for (i = 0; i < children.length; i++) { + /* + isDirectory: false + id: 'fs-item-0' + name: 'name' + parent: '/' + path: '/name' + depth: 0 + children: [] + */ + if (children[i].isRoot) { + path = ''; + newPath = ''; + } + else { + path = (newPath ? newPath : '') + '/' + children[i].name; + } + + itemData = { + isDirectory: children[i].isDirectory, + parentPath: newPath || '/', + path: path, + id: this.generateName(), + name: this._escape(children[i].name), + depth: self.depth + 1, + children: [] + }; + + self.children.push(itemData); + + this.metaData[itemData.id] = itemData; + this.metaDataPathLookUp[path] = this.metaData[itemData.id]; + + if (path === this.activePath) { + activeOneExists = true; + } + + this.itemCounter++; + } + + // apply to DOM + var str = ''; + + for (i = 0; i < self.children.length; i++) { + str += this.getItemDOM(self.children[i]); + } + + // replace or append + if (self.id.indexOf('#') === 0) { + $(self.id).html(str); + } else { + $('#' + self.id).after(str); + } + + if (activeOneExists) { + this.makeActive(this.activePath); + } + } + + cleanUp(data) { + $('#' + data.id).remove(); + + for (var i = 0; i < data.children.length; i++) { + this.cleanUp(data.children[i]); + } + + data.children = []; + + delete this.metaData[data.id]; + } + + saveActiveFile() { + var editorContent = this.editor.getText(); + var itemData = this.metaDataPathLookUp[this.activePath]; + + if (itemData) { + this.fs.writeFile(itemData.path, editorContent); + } else { + this.activePath = ''; + $.notific8('Error occurred while saving the file', warningNotific8Options); + } + } + + makeActive(itemPath) { + $(this.id).find('.active-item').removeClass('active-item'); + if (itemPath && this.metaDataPathLookUp[itemPath]) { + this.activePath = itemPath; + $('#' + this.metaDataPathLookUp[itemPath].id).addClass('active-item'); + SysGlobalObservables.currentFileName(this.metaDataPathLookUp[itemPath].name); + SysGlobalObservables.currentFilePath(this.metaDataPathLookUp[itemPath].path); + } + else { + this.activePath = ''; + } + } + + generateName() { + this.itemCounter++; + return this.itemPrefix + Date.now() + '-' + this.itemCounter; + } + + getId() { + return this.id; + } + + draw() { + var str = ''; + var activeFileData = this.metaDataPathLookUp[this.activePath]; + + if (this.activePath !== '' && activeFileData) { + var content = this.fs.readFileSync(activeFileData.path).toString('binary'); + this.editor.setFile(activeFileData.path, activeFileData.name, content); + //self.editor.getSession().setValue(content); + } + + for (var i = 0; i < this.children.length; i++) { + str += this.getItemDOM(this.children[i]); + } + + $(this.id).html(str); + } + + retrieveStates() { + var id; + + if (this.directoryState.length === 0) { + id = this.metaDataPathLookUp[''].id; + $('#' + id).trigger('click'); + } + else { + for (var i = 0; i < this.directoryState.length; i++) { + if (this.metaDataPathLookUp[this.directoryState[i]]) { + id = this.metaDataPathLookUp[this.directoryState[i]].id; + $('#' + id).trigger('click'); + } + } + } + } + + refresh() { + this.init(); + } + + _escape(unsafe) { + return unsafe + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\//g, '/'); + } + + getItemDOM(data) { + if (data.isDirectory) { + return '
    \ + ' + data.name + '
    '; + } + else { + return '
    \ + ' + data.name + '
    '; + } + } + + dispose() { + // This runs when the component is torn down. Put here any logic necessary to clean up, + // for example cancelling setTimeouts or disposing Knockout subscriptions/computeds. + } +} + +export default { viewModel: Filebrowser, template: templateMarkup }; \ No newline at end of file diff --git a/src/components/playground-layout/playground-layout.js b/src/components/playground-layout/playground-layout.js index ea0618f5..ba643c53 100644 --- a/src/components/playground-layout/playground-layout.js +++ b/src/components/playground-layout/playground-layout.js @@ -41,7 +41,9 @@ class PlaygroundLayout { this.createVideoSearchTab(); this.createEditorTab(editorParams, compilerParams); + this.createFileBrowserTab(); this.createManPageSearchTab(openManPageCallback); + } createEditorTab(editorParams, compilerParams) { @@ -82,6 +84,17 @@ class PlaygroundLayout { }); } + createFileBrowserTab() { + this.editorPaneTabs.push({ + title: 'File Browser', + icon: 'list-alt', + closable: false, + component: { + name: 'file-browser' + } + }); + } + // Callback used by the manpages-search-tab and editor components openManPage(manPage) { if (!manPage) diff --git a/src/styles/_file-browser.scss b/src/styles/_file-browser.scss new file mode 100644 index 00000000..1f23a75f --- /dev/null +++ b/src/styles/_file-browser.scss @@ -0,0 +1,47 @@ +/* file browser */ +#file-browser { + $file-browser-background-color: #232830; + + background-color: $file-browser-background-color; + color: lighten($file-browser-background-color, 50%); + user-select: none; + width: 100%; + height: 100%; + + .loading { + font-size: 16px; + font-weight: bold; + padding-top: 35%; + width: 100%; + display: block; + text-align: center; + } + .item { + font-size: 15px; + display: block; + &:hover { + color: lighten($file-browser-background-color, 75%); + cursor: pointer; + } + .item-icon { + display: inline-block; + min-width: 15px; + margin-right: 4px; + } + } + .active-item { + color: #F8BC00; + font-weight: bold; + } + #file-browser-context-menu { + .dropdown-menu { + a { + &:hover { + background: #396A8D; + color: #eee; + cursor: pointer; + } + } + } + } +} \ No newline at end of file diff --git a/src/styles/main.scss b/src/styles/main.scss index 1b3960c5..8e422c88 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -22,6 +22,7 @@ $icon-font-path: 'bower_modules/bootstrap-sass/assets/fonts/bootstrap/'; @import "styles/compiler-state-label"; @import "styles/vm-state-label"; @import "styles/footer"; +@import "styles/file-browser"; #page { margin-top: 80px; From b98a1e491544485b266d95f60a29d365afba418b Mon Sep 17 00:00:00 2001 From: Colton Mercurio Date: Fri, 1 Apr 2016 04:50:45 -0500 Subject: [PATCH 2/2] adding current file name to the UI and changing download file to have the correct name --- src/components/editor/editor.html | 6 +++++- src/components/editor/editor.js | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/editor/editor.html b/src/components/editor/editor.html index 4995d182..dd6a04ca 100644 --- a/src/components/editor/editor.html +++ b/src/components/editor/editor.html @@ -9,7 +9,11 @@ -
    +
    + +