UNPKG

ares-ide

Version:

A browser-based code editor and UI designer for Enyo 2 projects

1,531 lines (1,366 loc) 54.3 kB
/*global Ares, ares, enyo, $L */ /** * Represents a file tree made with {hermes.Node} * * @class HermesFileTree */ enyo.kind({ name: "HermesFileTree", kind: "FittableRows", events: { onError: "", onFileClick: "", onFolderClick: "", onFileOpenRequest: "", onFileRemoved: "", onFolderChanged: "", onTreeChanged: "", onPathChecked: "", onShowWaitPopup: "", onHideWaitPopup: "" }, handlers: { onItemDown: "itemDown", onItemDragstart: "itemDragstart", onItemDragenter: "itemDragenter", onItemDragover: "itemDragover", onItemDragleave: "itemDragleave", onItemUp: "itemUp", onItemDrop: "itemDrop", onItemDragend: "itemDragend", onNodeDblClick: "nodeDblClick", onAdjustScroll: "adjustScroll" }, published: { serverName: "", // allows filetree to have draggable subnodes or not (not per default). dragAllowed: false, menuAllowed: false }, fit:true, components: [ // Hermes contextual menu {kind: "onyx.MenuDecorator", name: "hermesMenu", classes: "hermesMenu", onSelect: "hermesMenuItemSelected", components: [ {kind: "onyx.Menu", name: "hermesMenuList", classes: "hermesMenu-list", maxHeight: "100%", components: [ {name: "newFolderItem", value: "newFolderClick", classes: "hermesMenu-button", components: [ {kind: "onyx.IconButton", src: "$harmonia/images/folder_new_16.png"}, {content: $L("New Folder...")} ]}, {name: "newFileDivider", classes: "onyx-menu-divider hermesMenu-button"}, {name: "newFileItem", value: "newFileClick", classes: "hermesMenu-button", components: [ {kind: "onyx.IconButton", src: "$harmonia/images/document_new_16.png"}, {content: $L("New File...")} ]}, {name: "nodeDivider", classes: "onyx-menu-divider hermesMenu-button"}, {name: "renameItem", value: "renameClick", classes: "hermesMenu-button", components: [ {kind: "onyx.IconButton", src: "$harmonia/images/document_edit_16.png"}, {content: $L("Rename...")} ]}, {name: "copyItem", value: "copyClick", classes: "hermesMenu-button", components: [ {kind: "onyx.IconButton", src: "$harmonia/images/copy_16.png"}, {content: $L("Copy...")} ]}, {name: "deleteItem", value: "deleteClick", classes: "hermesMenu-button", components: [ {kind: "onyx.IconButton", src: "$harmonia/images/document_delete_16.png"}, {content: $L("Delete...")} ]} ]} ]}, {kind: "onyx.Toolbar", name: "hermesToolbar", classes:"ares-small-toolbar title-gradient", components: [ {name: "newFolder", kind: "onyx.TooltipDecorator", components: [ {name: "newFolderButton", kind: "onyx.IconButton", src: "$harmonia/images/folder_new.png", ontap: "newFolderClick"}, {kind: "onyx.Tooltip", content: $L("New Folder...")} ]}, {name: "reloadAll", kind: "onyx.TooltipDecorator", components: [ {kind: "onyx.IconButton", src: "$harmonia/images/folder_reload.png", ontap: "reloadClick"}, {kind: "onyx.Tooltip", content: $L("Reload...")} ]}, {name: "newFile", kind: "onyx.TooltipDecorator", components: [ {name: "newFileButton", kind: "onyx.IconButton", src: "$harmonia/images/document_new.png", ontap: "newFileClick"}, {kind: "onyx.Tooltip", content: $L("New File...")} ]}, {name: "renameFile", kind: "onyx.TooltipDecorator", components: [ {name: "renameFileButton", kind: "onyx.IconButton", src: "$harmonia/images/document_edit.png", ontap: "renameClick"}, {kind: "onyx.Tooltip", content: $L("Rename...")} ]}, {name: "copyFile", kind: "onyx.TooltipDecorator", components: [ {name: "copyFileButton", kind: "onyx.IconButton", src: "$harmonia/images/copy.png", ontap: "copyClick"}, {kind: "onyx.Tooltip", content: $L("Copy...")} ]}, {name: "deleteFile", kind: "onyx.TooltipDecorator", components: [ {name: "deleteFileButton", kind: "onyx.IconButton", src: "$harmonia/images/document_delete.png", ontap: "deleteClick"}, {kind: "onyx.Tooltip", content: $L("Delete...")} ]}, {name: "revertMove", kind: "onyx.TooltipDecorator", components: [ {name: "revertMoveButton", kind: "onyx.IconButton", src: "$harmonia/images/undo.png", ontap: "revertClick"}, {kind: "onyx.Tooltip", classes:"ares-tooltip-last", content: $L("Revert move...")} ]} ]}, // Hermes tree, "serverNode" component will be added as HermesFileTree is created {kind: "Scroller", fit:"true", classes:"enyo-document-fit" }, // track selection of nodes. here, selection Key is file or folderId. // Selection value is the node object. Is an Enyo kind {kind: "Selection", onSelect: "select", onDeselect: "deselect"}, // service provide connection to file storage {name: "service", kind: "FileSystemService"}, // Hermes popups {name: "errorPopup", kind: "Ares.ErrorPopup", msg: "Service returned an error"}, {name: "nameFilePopup", kind: "NamePopup", type: "file", fileName:"", placeHolder: $L("File Name"), onCancel: "newFileCancel", onConfirm: "newFileConfirm"}, {name: "nameFolderPopup", kind: "NamePopup", type: "folder", fileName: "", placeHolder: $L("Folder Name"), onCancel: "newFolderCancel", onConfirm: "newFolderConfirm"}, {name: "nameCopyPopup", kind: "NamePopup", title: $L("Name for copy of"), fileName: $L("Copy of foo.js"), onCancel: "copyFileCancel", onConfirm: "copyFileConfirm"}, {name: "deletePopup", kind: "DeletePopup", onCancel: "deleteCancel", onConfirm: "deleteConfirm"}, {name: "renamePopup", kind: "RenamePopup", title: $L("New name for "), fileName: "foo.js", onCancel: "renameCancel", onConfirm: "renameConfirm"}, {name: "revertPopup", kind: "RevertPopup", title: $L("Revert node moving"), fileName: "foo.js", onCancel: "revertCancel", onConfirm: "revertConfirm"} ], // warning: this variable duplicates an information otherwise stored in this.$.selection // BUT, retrieving it through this.$.selection.getSelected is not handy as this function // return an object (hash) which needs to be scanned to retrieve the selected value selectedNode: null, debug: false, // when set, deactivate Hermes right-click menu and allow the browser's menu debugContextMenu: false, packages: false, draggedNode: null, targetNode: null, movedNode: null, originNode: null, revertMove: false, holdoverTimeout: null, holdoverTimeoutMS: 1000, create: function() { ares.setupTraceLogger(this); // Setup this.trace() function according to this.debug value this.inherited(arguments); this.enableDisableButtons(); this.createComponent( {name: "serverNode", container: this.$.scroller, kind: "hermes.Node", classes: "enyo-unselectable hermesFileTree-root", showing: false, content: "server", icon: "$services/assets/images/antenna.png", expandable: true, expanded: true, collapsible: false, dragAllowed: this.dragAllowed } ); this.menuAllowedChanged(); }, menuAllowedChanged: function(oldValue) { this.trace(oldValue, "=>", this.menuAllowed); if (this.menuAllowed) { enyo.dispatcher.listen(document, "contextmenu", enyo.bind(this, "contextMenu")); } else { enyo.dispatcher.stopListening(document, "contextmenu", enyo.bind(this, "contextMenu")); } }, contextMenu: function(inEvent) { this.trace("inEvent", inEvent); var target = enyo.dispatcher.findDispatchTarget(inEvent.target), control = target; if (control.name !== "caption" || control.owner.kind !== "hermes.Node") { return true; } while (control.kind !== "HermesFileTree") { if (control.owner === undefined) { return true; } control = control.owner; } if (control.get("menuAllowed") && !control.get("debugContextMenu")) { inEvent.preventDefault(); this.nodeRightClick(target, inEvent); return true; } return true; }, /** @private */ itemDown: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); return true; }, /** @private */ itemDragstart: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); // get the related hermes.Node this.draggedNode = inEvent.originator; if (this.draggedNode.kind !== "hermes.Node") { this.draggedNode = this.draggedNode.parent; } this.targetNode = this.draggedNode; if (this.draggedNode.content == "package.js") { inEvent.dataTransfer.effectAllowed = "none"; } else { inEvent.dataTransfer.effectAllowed = "linkMove"; } var data = {}; data.kind = this.draggedNode.kind; data.file = this.draggedNode.file; data.file.service.owner = null; var dataText = enyo.json.stringify(data); inEvent.dataTransfer.setData("Text", dataText); return true; }, /** @private */ itemDragenter: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); // look for the related hermes.Node var tempNode = inEvent.originator; if (tempNode.kind !== "hermes.Node") { tempNode = tempNode.parent; } // FIXME: ENYO-2786 MSIE 10 workaround to avoid parent of Control object that is a Control object too and produce an error in the console // BTW, objects dragged from elsewhere than HermesFileTree are discarded if (tempNode.kind !== "hermes.Node") { return true; } if (!tempNode.file.isDir) { tempNode = tempNode.container; } if (this.targetNode === tempNode) { return true; } if (this.targetNode !== null) { if (this.targetNode.file.isDir && this.targetNode.expanded) { this.targetNode.removeClass("hermesFileTree-folder-highlight"); this.$.selection.deselect(this.targetNode.file.id, this.targetNode); } } this.resetHoldoverTimeout(); // targetNode update this.targetNode = tempNode; if (this.targetNode.file.isDir && this.targetNode.expanded) { this.targetNode.addClass("hermesFileTree-folder-highlight"); } this.setHoldoverTimeout(this.targetNode); return true; }, /** @private */ itemDragover: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); // discard any object that are not coming HermesFileTree if (!this.draggedNode) { return true; } if (this.draggedNode && this.draggedNode.content != "package.js") { if (this.isValidDropTarget(this.targetNode)) { inEvent.dataTransfer.dropEffect = "link"; } else { inEvent.dataTransfer.dropEffect = "move"; } inEvent.preventDefault(); } return true; }, /** @private */ itemDragleave: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); return true; }, /** @private */ itemUp: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); return true; }, /** @private */ itemDrop: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); var dataText = inEvent.dataTransfer.getData("Text"); if (dataText === "") { return true; } var data = enyo.json.parse(dataText); if (data.kind !== "hermes.Node") { return true; } var draggedNodeId = data.file.id; this.trace('node dropped', draggedNodeId); if (!this.isValidDropTarget(this.targetNode)) { this.trace("target not valid"); } else { if (this.draggedNode.content != "package.js") { var oldParentNode=this.draggedNode.container, newParentNode=this.targetNode; var nodeMoving = this.moveNode(this.draggedNode, this.targetNode); nodeMoving.response(this, function(inSender, inNodeFile) { var nodesUpdating = newParentNode.updateNodes(); nodesUpdating.response(this, function(inSender, inNodes) { this.movedNode=newParentNode.getNodeWithId(inNodeFile.id); this.originNode=oldParentNode; this.revertMove=true; this.showRevertMoveButton(); }); nodesUpdating.error(this, function() { this.warn("error retrieving related node children"); }); }); } else { this.trace("'package.js' files cannot be moved"); } } inEvent.dataTransfer.clearData(); return true; }, /** @private */ itemDragend: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); if (this.targetNode.file.isDir && this.targetNode.expanded) { this.targetNode.removeClass("hermesFileTree-folder-highlight"); this.$.selection.deselect(this.targetNode.file.id, this.targetNode); } this.resetHoldoverTimeout(); this.draggedNode = null; this.targetNode = null; return true; }, /** @private */ setHoldoverTimeout: function (inTarget) { this.holdoverTimeout = setTimeout(enyo.bind(this, function() { this.holdOver(inTarget); }), this.holdoverTimeoutMS); }, /** @private */ resetHoldoverTimeout: function() { clearTimeout(this.holdoverTimeout); this.holdoverTimeout = null; }, /** @private */ holdOver: function (inTargetNode) { this.trace("inTargetNode=", inTargetNode); // expanding closed folder node... if (inTargetNode != this.draggedNode && inTargetNode.file.isDir && !inTargetNode.expanded) { this.$.selection.select(inTargetNode.file.id, inTargetNode); inTargetNode.setExpanded(true); // update icon for expanded state inTargetNode.setIcon("$services/assets/images/arrowDown.png"); inTargetNode.addClass("hermesFileTree-folder-highlight"); // handle lazy-load when expanding inTargetNode.updateNodes(). response(this, function() { inTargetNode.effectExpanded(); }); } }, /** @private */ isValidDropTarget: function(inNode) { this.trace("inNode=", inNode); var draggedFile = this.draggedNode.file, inFile = inNode.file; if (draggedFile != inFile) { if (inFile.isDir) { if (this.draggedNode.container.file.id != inFile.id) { if (!draggedFile.isDir || inFile.isServer || inFile.dir.indexOf(draggedFile.dir) == -1) { this.trace("target node"); return true; } else { this.trace("target node is a child node"); } } else { this.trace("target node is its own parent node"); } } else { this.trace("target node is a file"); } } else { this.trace("target node is itself"); } return false; }, /** @public */ connectService: function(inService, next) { this.trace("connect to service: ", inService); this.projectUrlReady = false; // Reset the project information this.clear() ; this.$.service.connect(inService, enyo.bind(this, (function(err) { if (err) { if (next) { next(err); } } else { this.$.serverNode.file = this.$.service.getRootNode(); this.$.serverNode.file.isServer = true; this.$.serverNode.setContent(this.$.serverNode.file.name); this.$.serverNode.setService(inService); if (next) { next(); } } }))); return this ; }, /** * @public * @param {Object} inFsService a FileSystemService implementation, as listed in ProviderList */ connectProject: function(inProjectData, next) { this.trace("config:", inProjectData); this.projectData = inProjectData; this.trace("HFT:connecting project " + inProjectData.getName()); var serverNode = this.$.serverNode; var nodeName = inProjectData.getName(); var folderId = inProjectData.getFolderId(); var service = inProjectData.getService(); var that = this ; serverNode.hide(); // connects to a service that provides access to a // (possibly remote & always asynchronous) file system this.connectService(service, enyo.bind(this, (function(inError) { if (inError) { this.showErrorPopup(this.$LS("Internal Error (#{error}) from filesystem service", {error: inError.toString()})); } else { this.trace("HFT: service is now connected for project" + inProjectData.getName() + '. Requesting project URL'); if (this.selectedNode) { this.deselect(null, {data: this.selectedNode}); } this.$.selection.clear(); this.selectedNode = null; this.enableDisableButtons(); // Get extra info such as project URL var rootFinding = this.$.service.propfind(folderId, 0); rootFinding.response(this, function(inSender, inValue) { var projectUrl = service.getConfig().origin + service.getConfig().pathname + "/file" + inValue.path; this.trace("HFT: service reply: project" + inProjectData.getName() + ' URL is ' + projectUrl + '. UPdating projectData'); this.projectData.setProjectUrl(projectUrl); this.trace("HFT: projet data update done"); this.projectUrlReady = true; // serverNode.file = inValue; serverNode.file.isServer = true; serverNode.setContent(nodeName); this.refreshFileTree( function() { that.trace("HFT: refreshFileTree done"); if (next) {next();} else {that.$.selection.select( serverNode.file.id, serverNode );} }); }); rootFinding.error(this, function(inSender, inError) { this.projectData.setProjectUrl(""); this.showErrorPopup(this.$LS("Internal Error (#{error}) from filesystem service", {error: inError.toString()})); }); } }))); this.packages = true; return this; }, /** @public */ disconnect: function() { this.trace("disconnect..."); this.$.selection.clear(); this.selectedNode = null; this.projectUrlReady = false; // Reset the project information this.clear() ; this.$.serverNode.file = null; return this ; }, hideFileOpButtons: function() { this.$.newFolder.hide(); this.$.newFile.hide(); this.$.reloadAll.hide(); this.$.renameFile.hide(); this.$.copyFile.hide(); this.$.deleteFile.hide(); this.$.revertMove.hide(); return this ; }, showNewFolderButton: function() { this.$.newFolder.show(); return this ; }, showFileOpButtons: function() { this.$.newFolder.show(); this.$.reloadAll.show(); this.$.newFile.show(); this.$.renameFile.show(); this.$.copyFile.show(); this.$.deleteFile.show(); return this ; }, showToolbar: function(show) { this.$.hermesToolbar.setShowing(show); return this ; }, showRevertMoveButton: function() { this.$.revertMove.show(); return this ; }, hideRevertMoveButton: function() { this.$.revertMove.hide(); return this ; }, /** @private */ clear: function() { var server = this.$.serverNode; this.trace("clearing serverNode") ; enyo.forEach( server.getNodeFiles() , function(n){ n.destroy(); } ) ; server.hide(); return this ; }, reset: function() { this.$.serverNode.hide(); if (this.$.service.isOk()) { this.trace("reseting serverNode") ; this.updateNodes(this.$.serverNode); } return this ; }, /** @private */ adjustScroll: function (inSender, inEvent) { this.trace(inSender, "=>", inEvent); var node = inEvent.originator; // FIXME: Due to unexpected UI behaviour ENYO-2575, scrollIntoView method is overridden... //this.$.scroller.scrollIntoView(node, true); this.scrollToHermesNode(node, true); return true; }, /** * scrollToHermesNode: Scroll an Hermes node into scroller view * @private * @param {hermes.Node} inNode: Hermes node to scroll to * @param {Boolean} inAlignWithTop: if true, node is aligned to its top, if false (default), to its bottom */ scrollToHermesNode: function (inNode, inAlignWithTop) { this.trace("inNode", inNode, "inAlignWithTop", inAlignWithTop); var scrollerBounds = this.$.scroller.getScrollBounds(); var scrollNode = inNode; var scrollBounds = {height: 0, width: 0, top: 0, left: 0}; var first = true; var height = 0; while (scrollNode && scrollNode.container && scrollNode.container.id != this.$.scroller.id) { // get the offset of the node related to the hermes.Node var node = scrollNode.hasNode(); var nodeBounds = {height: node.offsetHeight, width: node.offsetWidth, top: node.offsetTop, left: node.offsetLeft}; // get the height took by the whole sibling hermes nodes var siblingNode = scrollNode.node; var siblingBounds = {height: 0, width: 0, top: 0, left: 0}; while (siblingNode) { siblingBounds = {height: siblingNode.offsetHeight, width: siblingNode.offsetWidth, top: siblingNode.offsetTop, left: siblingNode.offsetLeft}; siblingNode = siblingNode.nextSibling; } // get the height of the container var scrollNodeContainer = scrollNode.container; var containerNode = scrollNodeContainer.hasNode(); var containerBounds = {height: 0, width: 0, top: 0, left: 0}; if (scrollNodeContainer.id != this.$.scroller.id) { containerBounds = {height: containerNode.offsetHeight, width: containerNode.offsetWidth, top: containerNode.offsetTop, left: containerNode.offsetLeft}; } // keep the height of the hermes.Node to scroll to if (first) { height = nodeBounds.height; first = false; } // keep the add up of the offsets between the Control to scroll to and the Scroller Control scrollBounds.height = node.offsetHeight; scrollBounds.width = node.offsetWidth; scrollBounds.top += (containerBounds.height - siblingBounds.top - siblingBounds.height + nodeBounds.top); scrollBounds.left += (- siblingBounds.left + nodeBounds.left); // go to parent hermes.Node scrollNode = scrollNode.container; } // By default, the hermes.Node is scrolled to align with the top of the scroll area. this.$.scroller.setScrollTop(Math.min(scrollerBounds.maxTop, inAlignWithTop === false ? scrollerBounds.maxTop - (scrollerBounds.height - scrollBounds.top) + height : scrollBounds.top)); this.$.scroller.setScrollLeft(Math.min(scrollerBounds.maxLeft, inAlignWithTop === false ? scrollBounds.left - scrollerBounds.clientWidth + scrollBounds.width : scrollBounds.left)); }, nodeTap: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); var node = inEvent.originator; this.$.selection.select(node.file.id, node); if (!node.file.isDir) { this.doFileClick({file: node.file}); } else { this.doFolderClick({file: node.file}); } // handled here (don't bubble) return true; }, /** @private */ nodeDblClick: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); var node = inEvent.originator; if (!node.file.isDir && !node.file.isServer) { this.doFileOpenRequest({ file: node.file, projectData: this.projectData }); } else { if (node.file.isDir) { node.set("expanded", !node.get("expanded")); } } // handled here (don't bubble) return true; }, /** @private */ nodeRightClick: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); if (!this.menuAllowed) { return true; } var node = inSender; // activate contextual menu only on hermesNode caption if (node.name !== "caption") { return true; } // look for related hermesNode if (node.kind !== "hermes.Node") { node = node.parent; } if (node.kind !== "hermes.Node") { return true; } node.doNodeTap(); // determine the shift between harmonia and ares var LeftPanelTranslation = 0; var regexp = /translateX\((.*)px\)/; var t; if (enyo.platform.firefox) { t = this.owner.node.style["transform"]; } else { t = this.owner.node.style["webkitTransform"]; } var results = regexp.exec(t); if (results) { LeftPanelTranslation = results[1]; } // menu must be opened first to get its bounds this.$.hermesMenu.requestShowMenu(); // calculate a specific offset to shift the menu when the menu // reaches the bottom or the right side of hermesFileTree. // Without this offset the popup menu can be shown *under* the // editor.designer panel. This offset avoids but does not fix // the z-index issue, var bounds = this.$.hermesMenuList.getBounds(), backOffsetLeft = 0, backOffsetTop = 0; if (inEvent.clientX - this.node.offsetLeft - LeftPanelTranslation + bounds.width > this.node.clientWidth) { backOffsetLeft = inEvent.clientX - this.node.offsetLeft - LeftPanelTranslation + bounds.width - this.node.clientWidth; } if (inEvent.clientY - this.node.offsetTop + bounds.height > this.node.clientHeight) { backOffsetTop = bounds.height; } this.$.hermesMenuList.showAtEvent(inEvent, {left: - this.node.offsetLeft - LeftPanelTranslation - backOffsetLeft, top: - this.node.offsetTop - backOffsetTop}); this.$.hermesMenuList.updatePosition(); // handled here (don't bubble) return true; }, /** * Generic event handler * @private */ hermesMenuItemSelected: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); var fn = inEvent && inEvent.selected && inEvent.selected.value; if (typeof this[fn] === 'function') { this[fn](); } else { this.trace("*** BUG: '", fn, "' is not a known function"); } // handled here (don't bubble) return true; }, select: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); this.selectedNode=inEvent.data; inEvent.data.$.caption.addClass("hermesFileTree-select-highlight"); this.enableDisableButtons(); // handled here (don't bubble) return true; }, deselect: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); if (inEvent.data && inEvent.data.$.caption) { inEvent.data.$.caption.removeClass("hermesFileTree-select-highlight"); } this.selectedNode=null; this.enableDisableButtons(); // handled here (don't bubble) return true; }, copyName: function(inName) { return "Copy of "+inName; }, reloadClick: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); this.refreshFileTree(); }, asyncTracker: function(callBack) { var count = 0 ; var that = this ; this.trace("tracker created") ; return { inc: function() { count ++ ; that.trace("tracker inc ", count) ; return count; }, dec: function( val ){ count-- ; that.trace("tracker dec", count) ; if (count === 0 && callBack) { that.trace("running tracker call-back") ; callBack() ; } } }; }, /** * Refresh the current {HermesFileTree} view, if applicable * @param {Object} changedFile */ refreshFile: function(changedFile) { // FIXME: not cleanly implemented: should check wether // a refresh is necessary first. this.refreshFileTree(); }, /** * * @ public * All parameters are optional. * - callBack is optional. Will be called when the refresh is completely done, * i.e. when the aync events fireworks are finished * - toSelectId: when set, will force an entry to be selected. Use an id as returned * by fsService (or any other service) */ refreshFileTree: function(callBack, toSelectId, oldCallBack) { // deprecation warning if (oldCallBack) { this.warn("deprecated refreshFileTree signature. Callback is now the first parameter"); callBack = oldCallBack ; } if (toSelectId && toSelectId.match(/[^\da-f]/) ) { this.warn("refreshFileTree: internal error, toSelectId ", toSelectId, " does not look like an Id"); } var tracker = this.asyncTracker( function() { this.$.serverNode.render() ; if (callBack) { callBack() ; } }.bind(this) ) ; this.trace("refreshFileTree called. will select ",toSelectId) ; this.$.serverNode.refreshTree(tracker,0, toSelectId) ; this.trace("refreshFileTree done") ; }, /** * @public * Returns selected dir or container dir data */ getFolder: function() { var node = this.getFolderOfSelectedNode() ; return node ? node.file : null ; }, /** * @public * @returns selected folder {hermes.Node} or containing folder {hermes.Node} */ getFolderOfSelectedNode: function() { var node = this.selectedNode; return node && !node.file.isDir ? this.selectedNode.container : node; }, /** * @public * @returns a node structure for the parent node of the currently selected node */ getParentNodeOfSelected: function() { return this.selectedNode && this.selectedNode.container ; }, /** * @public * @returns a file data structure for the parent node of the currently selected node */ getParentOfSelected: function() { return this.selectedNode && this.selectedNode.container && this.selectedNode.container.file; }, /** @private */ // User Interaction for New Folder op newFolderClick: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); var folder = this.getFolder(); this.trace("on folder ",folder); if (this.$.serverNode && this.$.serverNode.file) { if (folder && folder.isDir) { this.$.nameFolderPopup.setFileName(""); this.$.nameFolderPopup.setFolderId(folder.id); this.$.nameFolderPopup.setPath(folder.path); this.$.nameFolderPopup.show(); } else { this.showErrorPopup($L("Select a parent folder first")); } } else { this.showErrorPopup($L("Select a file system first")); } }, /** @private */ newFolderCancel: function(inSender, inEvent) { this.trace("inSender:", inSender, "inEvent:", inEvent); }, /** @private */ newFolderConfirm: function(inSender, inEvent) { this.trace("inSender:", inSender, "inEvent:", inEvent); var folderId = inEvent.folderId; var name = inSender.fileName.trim(); if (!this.checkedPath(name)) { return true; } this.trace("Creating new folder ", name," into folderId=", folderId); var msgForCreatedItem = "Creating new folder "+name; this.doShowWaitPopup({msg: msgForCreatedItem}); var folderCreation = this.$.service.createFolder(folderId, name, { overwrite: false }); folderCreation.response(this, function(inSender, inFolder) { this.trace("inFolder: ", inFolder); var parentNode = this.getFolderOfSelectedNode(), pkgNode = parentNode.getNodeNamed('package.js'); if (!parentNode.expanded) { parentNode.setExpanded(true); // update icon for expanded state parentNode.setIcon("$services/assets/images/arrowDown.png"); // handle lazy-load when expanding parentNode.updateNodes(). response(this, function() { parentNode.effectExpanded(); }); } if (this.packages) { this.doTreeChanged({ add: { service: this.$.service, parentNode: parentNode && parentNode.file, pkgNode: pkgNode && pkgNode.file, node: inFolder } }); } /* cancel any move reverting */ this.resetRevert(); if (this.findNodeExtension(name) !== null) { this.refreshFileTree( function() {parentNode.getNodeWithId(inFolder.id).doAdjustScroll(); }, inFolder.id /*selectId*/ ); } else { this.showWarningPopup(this.$LS("Folder named '#{name}' is an hidden one. It won't be shown in the file tree and will be empty.", {name: name})); this.refreshFileTree(); } this.doHideWaitPopup(); }); folderCreation.error(this, function() { this.doHideWaitPopup(); this._handleXhrError.bind(this, "Unable to create folder '" + name + "'", null /*next*/); }); }, /** * Shared enyo.Ajax error handler * @private */ _handleXhrError: function(message, next, inSender, inError) { var response = inSender.xhrResponse, contentType, html, text; if (response) { contentType = response.headers['content-type']; if (contentType) { if (contentType.match('^text/plain')) { text = response.body; } if (contentType.match('^text/html')) { html = response.body; } } } var err = new Error(message + " (" + inError.toString() + ")"); err.html = html; err.text = text; err.status = response.status; this.doError({ msg: message, err: err}); if (typeof next === 'function') { next(err); } }, /** @private */ // User Interaction for New File op newFileClick: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); var folder = this.getFolder(); if (folder && folder.isDir) { this.$.nameFilePopup.setFileName(""); this.$.nameFilePopup.setFolderId(folder.id); this.$.nameFilePopup.setPath(folder.path); this.$.nameFilePopup.show(); } else { this.showErrorPopup($L("Select a parent folder first")); } }, /** @private */ newFileCancel: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); this.trace("New File canceled."); }, /** @private */ newFileConfirm: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); var folderId = inEvent.folderId; var name = inEvent.name.trim(); if (!this.checkedPath(name)) { return true; } var msgForCreatedItem = "Creating " +name; this.doShowWaitPopup({msg: msgForCreatedItem}); var nameStem = name.substring(0, name.lastIndexOf(".")); // aka basename var type = this.findNodeExtension(name); var templatePath; var location = window.location.toString(); var prefix = location.substring(0, location.lastIndexOf("/")+1); var folder = this.getFolderOfSelectedNode(); var nodeUpdating = folder.updateNodes(); nodeUpdating.response(this, function() { var matchFileName = function(node){ return (node.content === name ) ; }; var matchingNodes = folder.getNodeFiles().filter(matchFileName) ; if (matchingNodes.length !== 0) { this.doHideWaitPopup(); this.showErrorPopup(this.$LS("File '#{name}' already exists", {name: name})); return true; } if (name === "package.js") { templatePath = prefix+"../templates/package.js"; } else { templatePath = prefix+"../templates/template."+type; } var options = { url: templatePath, cacheBust: false, handleAs: "text" }; var replacements = { "$NAME": nameStem, "$YEAR": new Date().getFullYear() }; // retrieve template from server var r = new enyo.Ajax(options); r.response(this, function(inSender, inResponse) { this.trace("newFileConfirm response: ", inResponse); for (var n in replacements) { inResponse = inResponse.replace(n, replacements[n]); } this.createFile(name, folderId, inResponse); /* cancel any move reverting */ this.resetRevert(); }); r.error(this, function(inSender, error) { if (error === 404){ this.createFile(name, folderId); if (type === null) { this.showWarningPopup(this.$LS("File named '#{name}' is an hidden one. It won't be shown in the file tree and will be empty.", {name: name})); } else { this.showWarningPopup(this.$LS("No template found for '.#{extension}' files. Created an empty one.", {extension: type})); } } else { this.warn("error while fetching ", templatePath, ': ', error); } }); r.go(); this.doHideWaitPopup(); }); nodeUpdating.error(this, function() { this.doHideWaitPopup(); this.showErrorPopup($L("Cannot reach filesystem")); return true; }); }, /** @private */ // User Interaction for Copy File/Folder op copyClick: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); if (this.selectedNode) { this.$.nameCopyPopup.setType(this.selectedNode.file.type); this.$.nameCopyPopup.setFileName(this.copyName(this.selectedNode.file.name)); this.$.nameCopyPopup.setPath(this.selectedNode.file.path); this.$.nameCopyPopup.setFolderId(this.selectedNode.file.id); this.$.nameCopyPopup.show(); } else { this.showErrorPopup($L("First select a file or folder to copy")); } }, /** @private */ copyFileCancel: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); }, /** @private */ copyFileConfirm: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); var oldName = this.selectedNode.file.name; var newName = inSender.fileName.trim(); if (!this.checkedPath(newName)) { return true; } var type = this.findNodeExtension(newName), oldType; // Warning about file extension modification if (this.selectedNode.file.isDir) { if (type === null) { this.showWarningPopup(this.$LS("The new folder '#{newFolder}' will be hidden.", {newFolder: newName})); } else { oldType = this.findNodeExtension(this.selectedNode.file.name); if (oldType === null) { this.showWarningPopup(this.$LS("The new folder '#{newFolder}' will be no more hidden.", {newFolder: newName})); } } } else { if (type === null) { this.showWarningPopup(this.$LS("The new file '#{newFile}' will be hidden.", {newFile: newName})); } else { oldType = this.findNodeExtension(this.selectedNode.file.name); if (oldType === null) { this.showWarningPopup(this.$LS("The new file '#{newFile}' will be no more hidden.", {newFile: newName})); } else { if (type !== 'js' && type !== 'txt' && type !== 'md' && type !== 'png' && type !== 'jpg' && type !== 'json' && type !== 'yml') { this.showWarningPopup(this.$LS("Unknown '.#{extension}' file.", {extension: type})); } else { if (oldType !== 'js' && type === 'js') { this.showWarningPopup(this.$LS("The new file '#{newFile}' will be added to related 'package.js' file.", {newFile: newName})); } } } } } this.trace("Creating new file ", newName, " as copy of", oldName); var msgForCopiedItem = "Creating new file " + newName +" as copy of " + oldName; this.doShowWaitPopup({msg: msgForCopiedItem}); var nodeCopying = this.$.service.copy(this.selectedNode.file.id, { name: newName, overwrite: false }); nodeCopying.response(this, function(inSender, inFsNode) { this.trace("inNode: ", inFsNode); var parentNode = this.getParentNodeOfSelected(), pkgNode = parentNode.getNodeNamed('package.js'); this.doTreeChanged({ add: { service: this.$.service, parentNode: parentNode && parentNode.file, pkgNode: pkgNode && pkgNode.file, node: inFsNode } }); /* cancel any move reverting */ this.resetRevert(); if (type !== null) { this.refreshFileTree( function() { parentNode.getNodeWithId(inFsNode.id).doAdjustScroll(); }, inFsNode.id /*selectId*/ ); } else { this.showWarningPopup(this.$LS("Node named '#{newName}' is an hidden one. It won't be shown in the file tree and will be empty.", {name: newName})); this.refreshFileTree(); } this.doHideWaitPopup(); }); nodeCopying.error(this, function(inSender, inError) { this.doHideWaitPopup(); this.warn("Unable to copy:", this.selectedNode.file, "as", newName, inError); this.showErrorPopup(this.$LS("Creating file '#{copyName}' as copy of '#{name}' failed: #{error}", {copyName: newName, name: this.selectedNode.file.name, error: inError.toString()})); }); }, /** @private */ // User Interaction for Rename File/Folder op renameClick: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); if (this.selectedNode) { this.$.renamePopup.setType(this.selectedNode.file.type); this.$.renamePopup.setFileName(this.selectedNode.file.name); this.$.renamePopup.setFolderId(this.selectedNode.file.id); this.$.renamePopup.setPath(this.selectedNode.file.path); this.$.renamePopup.show(); } else { this.showErrorPopup($L("Select a file or folder to rename first")); } }, /** @private */ renameCancel: function(inSender, inEvent) { this.trace("inSender:", inSender, "inEvent:", inEvent); }, /** @private */ renameConfirm: function(inSender, inEvent) { this.trace("inSender:", inSender, "inEvent:", inEvent); var newName = inSender.fileName.trim(); if (!this.checkedPath(newName)) { return true; } var type = this.findNodeExtension(newName), oldType; // Warning about file extension modification if (!this.selectedNode.file.isServer) { if (this.selectedNode.file.isDir) { if (type === null) { this.showWarningPopup(this.$LS("Folder '#{newFolder}' is now hidden.", {newFolder: newName})); } else { oldType = this.findNodeExtension(this.selectedNode.file.name); if (oldType === null) { this.showWarningPopup(this.$LS("Folder '#{newFolder}' is no more hidden.", {newFolder: newName})); } } } else { if (type === null) { this.showWarningPopup(this.$LS("File '#{newFile}' is now hidden.", {newFile: newName})); } else { oldType = this.findNodeExtension(this.selectedNode.file.name); if (oldType === null) { this.showWarningPopup(this.$LS("File '#{newFile}' is no more hidden.", {newFile: newName})); } else { if (type !== 'js') { if (oldType === 'js') { if (type !== 'txt' && type !== 'md' && type !== 'png' && type !== 'jpg' && type !== 'json' && type !== 'yml') { this.showWarningPopup(this.$LS("Unknown '.#{extension}' file. File '#{oldFile}' will be removed from related 'package.js' file.", {extension: type, oldFile: this.selectedNode.file.name})); } else { this.showWarningPopup(this.$LS("File '#{oldFile}' will be removed from related 'package.js' file.", {oldFile: this.selectedNode.file.name})); } } else { if (type !== 'txt' && type !== 'md' && type !== 'png' && type !== 'jpg' && type !== 'json' && type !== 'yml') { this.showWarningPopup(this.$LS("Unknown '.#{extension}' file.", {extension: type})); } } } else { if (oldType !== 'js') { this.showWarningPopup(this.$LS("File '#{newFile}' will be added to related 'package.js' file.", {newFile: newName})); } } } } } } this.trace("Renaming '", this.selectedNode.file, "' as '", newName, "'"); var msgForRenamedItem = "Renaming " + this.selectedNode.file.name + " as " + newName; this.doShowWaitPopup({msg: msgForRenamedItem}); var nodeRenaming = this.$.service.rename(this.selectedNode.file.id, { name: newName, overwrite: false }); nodeRenaming.response(this, function(inSender, inNode) { this.trace("inNode: ",inNode); var parentNode = this.getParentNodeOfSelected(), pkgNode = parentNode.getNodeNamed('package.js'); if (!this.selectedNode.file.isServer && this.projectUrlReady) { if (!this.selectedNode.file.isDir) { this.doFileRemoved({id: Ares.Workspace.files.computeId(this.selectedNode.file)}); inNode.service = this.$.service ; this.doFileOpenRequest({ file: inNode, // need to add service projectData: this.projectData }); } else { this.doFolderChanged({file: this.selectedNode.file, projectData: this.projectData}); } } if (type !== null) { this.doTreeChanged({ remove: { service: this.$.service, parentNode: parentNode && parentNode.file, pkgNode: pkgNode && pkgNode.file, node: this.selectedNode.file }, add: { service: this.$.service, parentNode: parentNode && parentNode.file, pkgNode: pkgNode && pkgNode.file, node: inNode } }); this.refreshFileTree( function() { parentNode.getNodeWithId(inNode.id).doAdjustScroll(); }, inNode.id /*selectId*/ ); } else { // node is now an hidden one: ie. ".node" this.doTreeChanged({ remove: { service: this.$.service, parentNode: parentNode && parentNode.file, pkgNode: pkgNode && pkgNode.file, node: this.selectedNode.file } }); this.refreshFileTree(); } /* cancel any move reverting */ this.resetRevert(); this.doHideWaitPopup(); }); nodeRenaming.error(this, function(inSender, inError) { this.doHideWaitPopup(); this.warn("Unable to rename:", this.selectedNode.file, "into", newName, inError); this.showErrorPopup(this.$LS("Renaming file '#{oldName}' as '#{newName}' failed", {oldName: this.selectedNode.file.name, newName: newName})); }); }, /** @private */ // User Interaction for Delete File/Folder op deleteClick: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); if (this.selectedNode) { this.$.deletePopup.setType(this.selectedNode.file.isDir ? $L("folder") : $L("file")); this.$.deletePopup.setName(this.selectedNode.file.name); this.$.deletePopup.setPath(this.selectedNode.file.path); this.$.deletePopup.show(); } else { this.showErrorPopup($L("Select a file or folder to delete first")); } }, /** @private */ deleteCancel: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); }, /** @private */ deleteConfirm: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); var serverNode = this.$.serverNode; var parentNode = this.getParentNodeOfSelected(), pkgNode = parentNode.getNodeNamed('package.js'); var msgForDeletedItem = "Deleting " + (this.selectedNode.file.isDir ? "folder " : "file ") + this.selectedNode.file.name; this.doShowWaitPopup({msg: msgForDeletedItem}); var nodeRemoving = this.$.service.remove(this.selectedNode.file.id); nodeRemoving.response(this, function(inSender, inParentFolder) { this.trace("inParentFolder: ", inParentFolder); if (!this.selectedNode.file.isServer && this.projectUrlReady) { if (!this.selectedNode.file.isDir) { this.doFileRemoved({id: Ares.Workspace.files.computeId(this.selectedNode.file)}); } else { this.doFolderChanged({file: this.selectedNode.file, projectData: this.projectData}); } } this.doTreeChanged({ remove: { service: this.$.service, parentNode: parentNode && parentNode.file, pkgNode: pkgNode && pkgNode.file, node: this.selectedNode.file } }); /* cancel any move reverting */ this.resetRevert(); serverNode.resized(); this.refreshFileTree( function() { parentNode.doAdjustScroll(); }, parentNode.file.id /*selectId*/ ); if (parentNode === serverNode) { this.$.selection.select(serverNode.file.id, serverNode); } this.doHideWaitPopup(); }); nodeRemoving.error(this, function(inSender, inError) { this.warn("Unable to delete:", this.selectedNode.file, inError); this.showErrorPopup(this.$LS("Deleting '#{name}' failed", {name: this.selectedNode.file.name})); this.doHideWaitPopup(); }); }, /** @private */ // User Interaction for Revert File/Folder moving op revertClick: function(inSender, inEvent) { this.trace(inSender, "=>", inEvent); if (this.revertMove) { this.$.revertPopup.setType(this.movedNode.file.isDir ? $L("folder") : $L("file")); this.$.revertPopup.setName(this.movedNode.file.name); this.$.revertPopup.setPath(this.originNode.file.path); this.$.revertPopup.show(); } else { this.showErrorPopup($L("No more node moving to revert")); } }, /** @private */ revertCancel: function(inSender, inEvent) { this.trace("inSender:", inSender, "inEvent:", inEvent); }, /** @private */ revertConfirm: function(inSender, inEvent) { this.trace("inSender:", inSender, "inEvent:", inEvent); this.trace("Reverting '", this.movedNode.file.name, "' into '", this.originNode.file.path, "'"); var msgForRevertedItem = "Reverting " + this.movedNode.file.name + " into " + this.originNode.file.path; this.doShowWaitPopup({msg: msgForRevertedItem}); var nodeMoving = this.moveNode(this.movedNode, this.originNode); nodeMoving.response(this, function(inSender, inNodeFile) { /* cancel any move reverting */ this.resetRevert(); this.doHideWaitPopup(); }); nodeMoving.error(this, function(inSender, inError) { this.doHideWaitPopup(); this.warn("Unable to revert:", this.movedNode.file.name, "into", this.originNode.file.path, inError); this.showErrorPopup($L("Reverting '{name}' to '{oldpath}' failed", {name: this.movedNode.file.name, oldpath: this.originNode.file.path})); }); }, showErrorPopup : function(msg) { this.$.errorPopup.setErrorMsg(msg); this.$.errorPopup.show(); }, showWarningPopup : function(msg) { this.$.errorPopup.set("title", $L("warning")); this.showErrorPopup(msg); }, enableDisableButtons: function() { if (this.selectedNode) { this.$.deleteFileButton.setDisabled(this.selectedNode.file.isServer); this.$.copyFileButton.setDisabled(this.selectedNode.file.isServer); this.$.renameFileButton.setDisabled(this.selectedNode.file.isServer); if (this.selectedNode.file.isServer) { this.$.nodeDivider.hide(); this.$.deleteItem.hide(); this.$.copyItem.hide(); this.$.renameItem.hide(); } else { this.$.nodeDivider.show(); this.$.deleteItem.show(); this.$.copyItem.show(); this.$.renameItem.show(); } } else { this.$.copyFileButton.setDisabled(true); this.$.deleteFileButton.setDisabled(true); this.$.renameFileButton.setDisabled(true); this.$.nodeDivider.hide(); this.$.deleteItem.hide(); this.$.copyItem.hide(); this.$.renameItem.hide(); } }, delayedRefresh: function(msg, forceSelect) { var onDone = new enyo.Async() ; onDone.response(this, function(inSender, toSelectId) { var select = forceSelect || toSelectId ; this.trace("delayed refresh after ", msg, ' on ', select) ; this.refreshFileTree(null, select); }) ; return onDone ; }, createFile: function(name, folderId, content) { this.trace("Creating new file ",name," into folderId=",folderId); var fileCreation = this.$.service.createFile(folderId, name, content, { overwrite: false }); fileCreation.response(this, function(inSender, inNodes) { this.trace("inNodes: ",inNodes); var parentNode = this.getFolderOfSelectedNode(), pkgNode = parentNode.getNodeNamed('package.js'); if (!parentNode.expanded) { parentNode.setExpanded(true); // update icon for expanded state parentNode.setIcon("$services/assets/images/arrowDown.png"); // handle lazy-load when expanding parentNode.updateNodes(). response(this, function() { parentNode.effectExpanded(); }); } this.doTreeChanged({ add: { service: this.$.service, parentNode: parentNode && parentNode.file, pkgNode: pkgNode && pkgNode.file, node: inNodes[0] } }); /* cancel any move reverting */ this.resetRevert(); if (this.findNodeExtension(name) !== null) { this.refreshFileTree( function() { var newNode = parentNode.getNodeWithId(inNodes[0].id); // scroll adjustment newNode.doAdjustScroll(); // opens the created file: after tree refresh done if (!newNode.file.isServer && !newNode.file.isDir && this.projectUrlReady) { this.doFileOpenRequest({ file: newNode.file, projectData: this.projectData }); } }.bind(this), inNodes[0].id ); } }); fileCreation.error(this, this._handleXhrError.bind(this, "Unable to create file '" + name + "'", null /*next*/)); }, /** @private */ resetRevert: function() { this.movedNode=null; this.originNode=null; this.revertMove=false; this.hideRevertMoveButton(); }, /** * moveNode * @pu