UNPKG

fire-ide

Version:

Web-based Integrated Development Environment for fire.js

1,844 lines (1,668 loc) 51.4 kB
$.ajaxSetup ({ cache: false }); function EditorNodeTypeEnum() {} var EditorNodeType = createEnum(EditorNodeTypeEnum, ['root', 'null', 'hash', 'array', 'string', 'true', 'false', 'expression', 'key', 'expressionBlock', 'index' , 'number']) function EditorNodeModelCollection() {} EditorNodeModelCollection.prototype.add = function(modelInfo) { modelInfo.parentNodeTypes = modelInfo.parentNodeTypes || [] modelInfo.canEditTitle = modelInfo.canEditTitle !== undefined ? modelInfo.canEditTitle : true modelInfo.inlineSingleChild = modelInfo.inlineSingleChild !== undefined ? modelInfo.inlineSingleChild : false modelInfo.removable = modelInfo.removable !== undefined ? modelInfo.removable : true modelInfo.multiChild = modelInfo.multiChild !== undefined ? modelInfo.multiChild : false modelInfo.defaultChildType = modelInfo.defaultChildType !== undefined ? modelInfo.defaultChildType : EditorNodeType.null() modelInfo.convertFromString = modelInfo.convertFromString || function(node, strVal, hint) { return strVal; } if(!modelInfo.createDefaultNode) { modelInfo.createDefaultNode = function(parentNode) { return parentNode.createNewChild(modelInfo.defaultValue, modelInfo.type) } } modelInfo.serialize = modelInfo.serialize || function(node) { return node.getValue(); } this[modelInfo.type] = modelInfo } EditorNodeModelCollection.prototype.getModel = function(nodeType) { if(!nodeType) { throw "nodeType argument is required" } var model = this[nodeType] if(!model) { throw "Could't find a model for type '" + nodeType.toString() + "'" } return model } var EditorNodeModels = new EditorNodeModelCollection() // root EditorNodeModels.add({ type: EditorNodeType.root(), title: "Root", childs: true, inlineSingleChild: true, removable: false, decorate: function(node) { return { title: node.editor.rootNodeTitle || "=", class: "root" } }, defaultValue: "ROOT" }) var LITERAL_NODE_PARENT_TYPES = [ EditorNodeType.root(), EditorNodeType.key(), EditorNodeType.expression(), EditorNodeType.index()] // null EditorNodeModels.add({ type: EditorNodeType.null(), title: "null", childs: false, removable: true, decorate: function(node) { return { title: "null", class: "null" } }, defaultValue: null, canEditTitle: false, parentNodeTypes: LITERAL_NODE_PARENT_TYPES }) // true EditorNodeModels.add({ type: EditorNodeType.true(), title: "true", childs: false, removable: true, decorate: function(node) { return { title: "true", class: "bool" } }, defaultValue: true, canEditTitle: false, parentNodeTypes: LITERAL_NODE_PARENT_TYPES }) // false EditorNodeModels.add({ type: EditorNodeType.false(), title: "false", childs: false, removable: true, decorate: function(node) { return { title: "false", class: "bool" } }, defaultValue: true, canEditTitle: false, parentNodeTypes: LITERAL_NODE_PARENT_TYPES }) // string EditorNodeModels.add({ type: EditorNodeType.string(), title: "String", childs: false, removable: true, decorate: function(node) { return { title: node.getValue(), class: "str" } }, defaultValue: "lorem ipsum dolor sit amet", parentNodeTypes: LITERAL_NODE_PARENT_TYPES }) // expression block EditorNodeModels.add({ type: EditorNodeType.expressionBlock(), title: "Code", childs: true, multiChild: true, removable: true, decorate: function(node) { return { title: "{", class: "exp-block" } }, defaultValue: {}, canEditTitle: false, parentNodeTypes: LITERAL_NODE_PARENT_TYPES, defaultChildType: EditorNodeType.expression(), serialize: function(node) { var result = {} var keys = {} for(var i = 0;i < node.children.length;i++) { var child = node.children[i] var expValue = trimString(child.getValue()) if(!keys[expValue]) { keys[expValue] = 1 } else { keys[expValue]++ var prefix = '' var x = keys[expValue] while(x > 0) { prefix+=' ' x-- } expValue = prefix + expValue } result[expValue] = child.serialize() } return result } }) var ExpressionTags = [] function refreshExpressionTags() { $.get('/expressions', function(data) { ExpressionTags = [] data.response.forEach(function(item) { ExpressionTags.push(item.name) }) }) } refreshExpressionTags() // expression EditorNodeModels.add({ type: EditorNodeType.expression(), title: "Expression", childs: true, removable: true, decorate: function(node) { return { title: node.getExpressionName(), hint: getHint(node.getValue()), class: "exp", autoComplete: ExpressionTags } }, defaultValue: "@return", parentNodeTypes: [EditorNodeType.expressionBlock()], convertFromString: function(node, str, hint) { if(!hint) { hint = '' } if(hint && hint != '') { return "@" + str + "("+ hint +")" } return "@" + str }, serialize: function(node) { return node.getFirstChild().serialize() } }) // hash EditorNodeModels.add({ type: EditorNodeType.hash(), title: "Hash", childs: true, multiChild: true, removable: true, decorate: function(node) { return { title: "{", class: "hash" } }, defaultValue: {}, canEditTitle: false, parentNodeTypes: LITERAL_NODE_PARENT_TYPES, defaultChildType: null, serialize: function(node) { var result = {} for(var i = 0;i < node.children.length;i++) { var child = node.children[i] result[child.getValue()] = child.serialize() } return result } }) // property EditorNodeModels.add({ type: EditorNodeType.key(), title: "Property", childs: true, removable: true, decorate: function(node) { return { title: node.getValue(), class: "key" } }, defaultValue: "name", parentNodeTypes: [EditorNodeType.hash()], inlineSingleChild: true, serialize: function(node) { return node.getFirstChild().serialize() } }) // array EditorNodeModels.add({ type: EditorNodeType.array(), title: "Array", childs: true, multiChild: true, removable: true, decorate: function(node) { return { title: "[", class: "array" } }, defaultValue: {}, canEditTitle: false, parentNodeTypes: LITERAL_NODE_PARENT_TYPES, defaultChildType: null, assignChildValue: function(child) { var index = child.parent.children.indexOf(child) child.setValue(index) }, serialize: function(node) { var result = [] for(var i = 0;i < node.children.length;i++) { var child = node.children[i] result[i] = child.serialize() } return result } }) // index EditorNodeModels.add({ type: EditorNodeType.index(), title: "Index", childs: true, removable: true, decorate: function(node) { return { title: node.getValue(), class: "index" } }, defaultValue: 0, canEditTitle: false, parentNodeTypes: [EditorNodeType.array()], inlineSingleChild: true, serialize: function(node) { return node.getFirstChild().serialize() } }) var FIRE_NUMBER_DEFAULT_VALUE = 0 // number EditorNodeModels.add({ type: EditorNodeType.number(), title: "Number", childs: false, removable: true, decorate: function(node) { return { title: node.getValue(), class: "number" } }, defaultValue: FIRE_NUMBER_DEFAULT_VALUE, parentNodeTypes: LITERAL_NODE_PARENT_TYPES, convertFromString: function(node, str) { var number = parseFloat(str) if(isNaN(number)) return FIRE_NUMBER_DEFAULT_VALUE return number } }) function getModelForNodeType(nodeType) { var modelNames = Object.keys(EditorNodeModels) for(var i = 0;i < modelNames.length;i++) { var nodeModel = EditorNodeModels[modelNames[i]] if(nodeType == nodeModel.type) return nodeModel } return null } function SelectionGroup() { this.currentTarget = null } SelectionGroup.prototype.setSelectedTarget = function(target) { if(this.currentTarget) { this.currentTarget.deselect() } this.currentTarget = target || null } function FireEditor(visualContainer, selectionGroup, rootNodeTitle) { this.selectionGroup = selectionGroup || new SelectionGroup() this.rootVisual = visualContainer this.rootVisual.empty() this.visualContainer = $('<ul>').appendTo(this.rootVisual) this.rootEditorNode = null this.selectedNode = null this.rootNodeTitle= rootNodeTitle || null } FireEditor.prototype.loadDocument = function(jsonDocument) { this._preLoad() this.rootEditorNode.createChild(jsonDocument) this._postLoad() } FireEditor.prototype.load = function() { this._preLoad() this.rootEditorNode.ensureDefaultChild() this._postLoad() } FireEditor.prototype.serialize = function() { return this.rootEditorNode.children[0].serialize() } FireEditor.prototype._preLoad = function() { this.setSelectedNode(null) this.rootEditorNode = new EditorNode(this, null, null, EditorNodeType.root()) } FireEditor.prototype._postLoad = function() { this.rootEditorNode.appendToHtml(this.visualContainer) } FireEditor.prototype.setSelectedNode = function(node) { this.selectionGroup.setSelectedTarget(node) } var NODE_EDIT_EVENT = "dblclick" function createEnum(enumType, items) { items.forEach(function(type) { enumType.prototype[type] = function() { return enumType["_" + type] } }) enumType.prototype.toString = function() { return this.val } items.forEach(function(type) { var enumMember = new enumType() enumMember.val = type enumType["_" + type] = enumMember }) return new enumType() } function EditorNode(editor, value, parent, type) { var self = this this.editor = editor if(!editor || !(editor instanceof FireEditor)) throw "invalid editor instance" this.element = jQuery("<li>").addClass('node') this.element.click(function(event) { self.select() event.preventDefault() event.stopImmediatePropagation() }) this.element.editorNode = this this.subNodesList = jQuery("<ul>") this.children = [] this.titleElement = jQuery('<span>').hide().appendTo(this.element) this.titleElement.addClass("title").addClass('name') this.hintElement = jQuery('<span>').hide().appendTo(this.element) this.hintElement.addClass("title").addClass('hint') this.toolbarElement = jQuery('<span>').hide().addClass('toolbar').appendTo(this.element) this.toolbarElement.click(function(event) { event.preventDefault() event.stopImmediatePropagation() }) this.subNodesList.appendTo(this.element) this.staticType = type this.dynamicType = null this.model = null this.setValue(value) this.reparent(parent) if(this.parent) { this.parent._addChild(this) this.appendToHtml(this.parent.subNodesList) } } EditorNode.prototype.reparent = function(parent) { this.parent = parent || null } EditorNode.prototype.getFirstChild = function() { return this.children.length > 0 ? this.children[0] : null } EditorNode.prototype.serialize = function() { return this.model.serialize(this) } EditorNode.prototype._addChild = function(node) { this.children.push(node) this._ensureChildValues() return node } EditorNode.prototype.getChildIndex = function(node) { return this.children.indexOf(node) } EditorNode.prototype.getType = function() { return this.staticType || this.dynamicType } EditorNode.prototype._removeChild = function(child) { var i = this.children.indexOf(child) if(i > -1) { this.children.splice(i, 1) } this._ensureChildValues() } EditorNode.prototype.remove = function() { this.deselect() this.element.remove() if(this.parent) { this.parent._removeChild(this) } this.editor = null this.parent = null } /* Removes the current node ensuring the parent keeps it's default structure */ EditorNode.prototype.safeRemove = function() { var parent = this.parent || null this.remove() if(parent) { parent.ensureDefaultChild() } } EditorNode.prototype.canRemove = function() { return this.model.removable } EditorNode.prototype.copy = function() { window.localFireIdeClipboard = this._copyCore() return true } EditorNode.prototype._copyCore = function() { return { nodeType: this.getType(), value: this.getValue(), child: this.serialize() } } EditorNode.prototype.canPaste = function() { if(!window.localFireIdeClipboard ) return false var pasteNodeModel = getModelForNodeType(window.localFireIdeClipboard.nodeType) if(!pasteNodeModel) { throw "pasting unsupported node type" // say what??? } return pasteNodeModel.parentNodeTypes.indexOf(this.getType()) != -1 } EditorNode.prototype.paste = function() { if(window.localFireIdeClipboard ) { this._pasteCore(window.localFireIdeClipboard) } window.localFireIdeClipboard = null return true } EditorNode.prototype._pasteCore = function(clipboard) { if(clipboard) { this.willAddChild() var pastedNode = this.createChild(clipboard.value, clipboard.nodeType) //if(typeof(index) === 'undefined') { // console.log("pasting at last element") /* } else { console.log("pasting at index", index) this.insertChild(pastedNode, index) }*/ if(pastedNode.model.childs) { pastedNode.createChild(clipboard.child) } } } EditorNode.prototype.cut = function() { this.copy() this.safeRemove() return true } EditorNode.prototype.canReorderToPrevious = function() { if(!this.parent) return false if(this.parent.getChildIndex(this) < 1) return false return true } EditorNode.prototype.clearChildren = function() { while(this.children.length > 0) { this.children.pop().remove() } this.children = [] this.subNodesList.empty() } EditorNode.prototype._ensureChildValues = function() { if(this.model.assignChildValue) { for(var i = 0;i < this.children.length;i++) { var child = this.children[i] this.model.assignChildValue(child) } } } EditorNode.prototype.reorderToPrevious = function() { if(!this.canReorderToPrevious()) return false var oldIndex = this.parent.getChildIndex(this) var oldParent = this.parent var newIndex = oldIndex -1 var childrenCopy = [] for(var i = 0;i < oldParent.children.length;i++) { var child = oldParent.children[i] childrenCopy[i] = child } childrenCopy.splice(oldIndex, 1) // Remove myself childrenCopy.splice(newIndex, 0, this) // Insert me in the position var newChildren = [] for(var i = 0;i < childrenCopy.length;i++) { var child = childrenCopy[i] newChildren.push(child._copyCore()) } oldParent.clearChildren() for(var i = 0;i < newChildren.length;i++) { var child = newChildren[i] oldParent._pasteCore(child) } } EditorNode.prototype._createToolbarButtons = function(buttons) { var self = this if(this.canRemove()) { buttons.push({ tooltip: 'Remove the Value', id: 'remove', text: '[ Delete ]', execute: function() { self.safeRemove() } }) } buttons.push({ tooltip: 'Copy', id: 'copy', text: "[ Copy ]", execute: function() { self.deselect() self.copy() } }) buttons.push({ tooltip: 'Cut', id: 'cut', text: "[ Cut ]", execute: function() { self.deselect() self.cut() } }) if(this.canPaste()) { buttons.push({ tooltip: 'Paste', id: 'paste', text: "[ Paste ]", execute: function() { self.deselect() self.paste() } }) } if(this.canReorderToPrevious()) { buttons.push({ tooltip: 'Reorder to the previous position', id: 'reorder', text: "[ Reorder ]", execute: function() { self.deselect() self.reorderToPrevious() } }) } var childModels = [] Object.keys(EditorNodeModels).forEach(function(nodeTypeName) { var model = EditorNodeModels[nodeTypeName] if(model.parentNodeTypes.indexOf(this.model.type) > -1) { childModels.push(model) } }, this) childModels.forEach(function(childModel) { buttons.push({ tooltip: '', id: childModel.type.toString(), text: childModel.title, execute: function() { self.deselect() self.willAddChild() childModel.createDefaultNode(self).select().ensureDefaultChild() } }) }) } EditorNode.prototype.toggleToolbar = function() { var self = this if(this.isSelected()) { var commands = [] this._createToolbarButtons(commands) this.toolbarElement.empty() commands.forEach(function(cmdInfo) { var btn = $('<a>').attr('href','#' + cmdInfo.id) if(cmdInfo.icon) { $('<img>').attr('src', cmdInfo.icon).appendTo(btn) } else { //$('<span>').addClass(cmdInfo.class).text(cmdInfo.text).appendTo(btn) btn.text(cmdInfo.text) } btn.click(function(event) { event.preventDefault() event.stopImmediatePropagation() if(cmdInfo.execute){ cmdInfo.execute() } }) this.toolbarElement.append(btn) }, this) this.toolbarElement.fadeIn() } else { this.toolbarElement.hide() } } EditorNode.prototype.isSelected = function(val) { return $(this.element).hasClass('selected') } EditorNode.prototype.select = function() { if(!this.isSelected()) { $(this.element).addClass('selected') this.editor.setSelectedNode(this) this.toggleToolbar() } return this } EditorNode.prototype.focus = function() { this.element.focus() $('html, body').animate({ scrollTop: this.element.offset().top }, 500); return this } EditorNode.prototype.editTitleMode = function() { this.titleElement.trigger(NODE_EDIT_EVENT); return this } EditorNode.prototype.deselect = function(val) { if(this.isSelected()) { $(this.element).removeClass('selected') this.editor.setSelectedNode(null) this.toggleToolbar() } return this } EditorNode.prototype.toggleSelection = function(val) { if(this.isSelected()) { this.deselect() } else { this.select() } return this } EditorNode.prototype.setValue = function(val) { this._value = val this._ensureDynamicType() this.model = EditorNodeModels.getModel(this.getType()) this._parseChildren() this.updateUI() } EditorNode.prototype.setHint = function(hint) { if(this.getType() != EditorNodeType.expression()) { throw "setHint only works on expression nodes" } this.replaceValueFromString(this.getExpressionName(), hint) } EditorNode.prototype.replaceValueFromString = function(strVal, hint) { this.setValue(this.model.convertFromString(this, strVal, hint)) } EditorNode.prototype.getValue = function() { return this._value } EditorNode.prototype.updateUI = function() { this._updateTitleElement() this._updateSubNodesElement() } EditorNode.prototype._ensureDynamicType = function() { if(this.staticType) return var type = null //console.log("Value= '", this.getValue(), "' type = ", typeof(this.getValue())) if(this.getValue() === null) { type = EditorNodeType.null() } else if(typeof(this.getValue()) == 'string' && (isSpecialKey(this.getValue()))) { type = EditorNodeType.expression() } else if(this.getValue() && typeof(this.getValue()) == 'object' && hasSpecialKeys(this.getValue())) { type = EditorNodeType.expressionBlock() } else if(this.getValue() && (typeof(this.getValue()) == 'object' && !(this.getValue() instanceof Array))) { type = EditorNodeType.hash() } else if(this.getValue() && (typeof(this.getValue()) == 'object' && (this.getValue() instanceof Array))) { type = EditorNodeType.array() } else if(typeof(this.getValue()) == 'string') { type = EditorNodeType.string() } else if(this.getValue() === true) { type = EditorNodeType.true() } else if(this.getValue() === false) { type = EditorNodeType.false() } else if(typeof(this.getValue()) == 'number') { type = EditorNodeType.number() } if(!type) throw "failed to infer the Dynamic Type" this.dynamicType = type } EditorNode.prototype._updateSubNodesElement = function() { if(this.model.inlineSingleChild) { if(this.children.length > 1) { if(!this.subNodesList.hasClass('multi')) { this.subNodesList.addClass('multi') } } else { this.subNodesList.removeClass('multi') this.subNodesList.addClass('single') } } else { if(!this.subNodesList.hasClass('multi')) { this.subNodesList.addClass('multi') } } } $.editable.addInputType('autocomplete', { element : $.editable.types.text.element, plugin : function(settings, original) { $('input', this).autocomplete({ autoFocus: true, source: settings.autocomplete.data, delay: 50 }); focusEdit(this) } }); function focusEdit(target) { $('input', target).select() $('input', target).animate({ "min-width":150 }, 200) } $.editable.addInputType('autoSelectText', { element : $.editable.types.text.element, plugin : function(settings, original) { focusEdit(this) } }); EditorNode.prototype._updateTitleElement = function() { var self = this // Remove current class name if necessary if(this.currentTitleElementClass) { this.titleElement.removeClass(this.currentTitleElementClass) } this.currentTitleElementClass = null var nodeModel = EditorNodeModels.getModel(this.getType()) var decoration = nodeModel.decorate(this) var newText = decoration.title var newHint = decoration.hint this.currentTitleElementClass = decoration.class // Update Class if(this.currentTitleElementClass) { this.titleElement.addClass(this.currentTitleElementClass) } // Update Text if(newText !== undefined) { this.titleElement.fadeIn() this.titleElement.text(newText) if(this.canEditTitle()) { if(decoration.autoComplete) { $(this.titleElement).editable(function(value, settings) { self.replaceValueFromString(value) return nodeModel.decorate(self).title; }, { type : 'autocomplete', event: NODE_EDIT_EVENT, tooltip: '', placeholder: '', autocomplete: { data: decoration.autoComplete, target: this.titleElement } }); } else { $(this.titleElement).editable(function(value, settings) { self.replaceValueFromString(value) return nodeModel.decorate(self).title; }, { type : 'autoSelectText', event: NODE_EDIT_EVENT, tooltip: '', placeholder: '' }); } } else { $(this.titleElement).editable('disable') } } else { this.titleElement.hide() this.titleElement.text('') } // Update Hint if(newHint !== undefined) { this.hintElement.fadeIn() this.hintElement.text(newHint) $(this.hintElement).editable(function(value, settings) { self.setHint(value) return value; }, { type : 'autoSelectText', event: NODE_EDIT_EVENT, tooltip: '', placeholder: '' }); } else { this.hintElement.hide() this.hintElement.text('') } } EditorNode.prototype.canEditTitle = function() { return EditorNodeModels.getModel(this.getType()).canEditTitle } EditorNode.prototype.appendToHtml = function(targetList) { if(!targetList) throw "invalid targetList" targetList.append(this.element) this.updateUI() return this } EditorNode.prototype.createChild = function(value, staticType) { return new EditorNode(this.editor, value, this, staticType) } EditorNode.prototype.createNewChild = function(value, staticType, autoEdit) { if(autoEdit === undefined) { autoEdit = true } var newChild = this.createChild(value, staticType) if(autoEdit) { if(newChild.canEditTitle()) { newChild.editTitleMode() } } newChild.resetDefaultChild() return newChild } EditorNode.prototype.removeChildren = function() { while(this.children.length != 0) { this.children[0].remove() } } EditorNode.prototype.willAddChild = function() { if(!this.model.multiChild) { this.removeChildren() } } EditorNode.prototype.ensureDefaultChild = function(force) { if(force) { this.removeChildren() } if(this.children.length == 0){ this.resetDefaultChild() } return this } EditorNode.prototype.resetDefaultChild = function() { if(this.model.childs && this.model.defaultChildType){ EditorNodeModels.getModel(this.model.defaultChildType).createDefaultNode(this).ensureDefaultChild() } return this } EditorNode.prototype._parseChildren = function() { var val = this.getValue() if(this.getType() == EditorNodeType.expressionBlock()) { Object.keys(val).forEach(function(propName) { var propVal = val[propName] this.createChild(propName, EditorNodeType.expression()).createChild(propVal) }, this) } else if(this.getType() == EditorNodeType.hash()) { Object.keys(val).forEach(function(propName) { var propVal = val[propName] this.createChild(propName, EditorNodeType.key()).createChild(propVal) }, this) } else if(this.getType() == EditorNodeType.array()) { if(val) { for(var i = 0;i < val.length;i++) { var indexValue = val[i] this.createChild(i, EditorNodeType.index()).createChild(indexValue) } } } } EditorNode.prototype.createEditorForNode = function(fireNode) { if(!(fireNode instanceof FireNode)) throw "FireNode instance expected" var editorNode = new EditorNode(fireNode, this) editorNode.appendToHtml(this.subNodesList) } function hasSpecialKeys(obj) { var props = Object.keys(obj) for(var i = 0;i < props.length;i++) { if(isSpecialKey(props[i])) return true } return false } EditorNode.prototype.getExpressionName = function() { var propName = this.getValue() if(!propName) throw "Can't get the expression name from root" if(!isSpecialKey(propName)){ throw "Asked to extract special from a key that is not a special key, the key was '" + propName + "'" } propName = trimString(propName) if(keyHasHint(propName)) { propName = propName.substring(0,propName.indexOf(HINT_START_SYMBOL)) } return propName.substring(1,propName.length) } EditorNode.prototype.getHint = function() { var val = this.getValue() if(typeof(val) != 'string') return '' return getHint(val) } var HINT_START_SYMBOL = "(" var HINT_END_SYMBOL = ")" var SPECIAL_KEY_SYMBOL = "@" function keyHasHint(propName) { return propName.indexOf(HINT_START_SYMBOL) > -1 } function trimString(str) { if(str == null || str == undefined) return str return str.replace(/^\s\s*/,'').replace(/\s\s*$/,'') } function isSpecialKey(propName) { var n = trimString(propName) return n.indexOf(SPECIAL_KEY_SYMBOL) == 0 } function getHint(propName) { if(!keyHasHint(propName)) return ''; var hintEndIndex = propName.indexOf(HINT_END_SYMBOL) if(hintEndIndex > 1) { return propName.substring(propName.indexOf(HINT_START_SYMBOL) + 1,hintEndIndex) } return propName.substring(propName.indexOf(HINT_START_SYMBOL) + 1,propName.length) } function FireFileView(visualContainer, fileSelectionCallback) { this.fileSelectionCallback = fileSelectionCallback this.visualContainer = visualContainer this.list = $('ul:first', visualContainer) var self = this this.refreshBtn = $('a.btn.refresh', this.visualContainer).click(function(event) { event.stopImmediatePropagation() event.preventDefault() self.refresh() }) this.backBtn = $('a.btn.back', this.visualContainer).click(function(event) { event.stopImmediatePropagation() event.preventDefault() self.goBack() }).hide() this.pathLabel = $('.path:first', this.visualContainer) this.paths = [''] this.navigateDir('.') } FireFileView.prototype.goBack = function() { this.paths.pop() this.refresh() } FireFileView.prototype.getCurrentPath = function() { return this.paths[this.paths.length -1] } FireFileView.prototype.navigateDir = function(name) { this.paths.push(name + "/") this.refresh() } FireFileView.prototype.refresh = function() { var self = this if(this.paths.length > 2) { this.backBtn.fadeIn() } else { this.backBtn.fadeOut() } this.selectedListItem = null this.pathLabel.text(this.getCurrentPath()) this.refreshBtn.attr('disabled','disabled') var currentHeight = this.list.height() this.list.css("min-height", currentHeight) $.get('/fs/' + this.getCurrentPath(), function(data, status, xhr) { self.refreshBtn.removeAttr('disabled') if(status == 'success') { self.list.empty() data.response.forEach(function(file) { var listItem = $('<li>').appendTo(self.list) listItem.attr('title', file.path) $('<a>').attr('href',"/fs/" + file.path).text(file.path).click(function(event) { event.preventDefault() event.stopImmediatePropagation() if(!file.isDirectory && self.fileSelectionCallback) { if(self.selectedListItem) { self.selectedListItem.removeClass('selected') } self.selectedListItem = listItem self.selectedListItem.addClass('selected') self.fileSelectionCallback(file.path) } else { self.navigateDir(file.path) } }).hide().appendTo(listItem).fadeIn() }) self.list.animate({ "min-height": null }) } else { console.error('Fire IDE file view can not retrieve the list of files') } }) } function FireIde(filesPlaceholder, editorContainer) { var socket = io.connect(window.location.origin); window.fireIdeSocket = socket var self = this this.editorContainer = editorContainer this.editorPlaceholder = $('div.editor_placeholder', editorContainer) this.editorTopbar = $('div.topbar', editorContainer) this.runBtn = $('a.btn.run') this.runBtn.click(function(event) { event.stopImmediatePropagation() event.preventDefault() var interval = null var reloadStream = function() { $.get('/run/current/stdout', function(data){ var textArea = $('.stdout .stream', self.editorPlaceholder) textArea.val(data) textArea.scrollTop(textArea[0].scrollHeight); }) $.get('/run/current/stderr', function(data){ var textArea = $('.stderr .stream', self.editorPlaceholder) textArea.val(data) textArea.scrollTop(textArea[0].scrollHeight); }) } var refreshRun = function() { if(self.currentRunInfo && self.currentRunInfo.running) { $('.pid', self.editorPlaceholder).text('PID: ' + (self.currentRunInfo.pid || 'Stopped')) $('.stderr .path', self.editorPlaceholder).text(self.currentRunInfo.stderr) $('.stdout .path', self.editorPlaceholder).text(self.currentRunInfo.stdout) $('a.btn.run', self.editorPlaceholder).text('Stop') } else { $('.pid', self.editorPlaceholder).text('Not Running') $('.stderr .path', self.editorPlaceholder).text('') $('.stdout .path', self.editorPlaceholder).text('') $('a.btn.run', self.editorPlaceholder).text('Start') } } self.currentEditor = null self._updateEditorUI() var reloadCurrentRun = function() { $.get('/run/current', function(data, status) { self.currentRunInfo = data.response refreshRun() reloadStream() clearInterval(interval) if(self.currentRunInfo && self.currentRunInfo.running) { interval = setInterval(function(){ reloadStream() }, DELAY) } }) } var DELAY = 500 self.editorPlaceholder.load('/run', function(data, status) { var stdoutText = $('.stdout .stream', self.editorPlaceholder) $('a.btn.run', self.editorPlaceholder).click(function(event) { event.stopImmediatePropagation() event.preventDefault() if(self.currentRunInfo && self.currentRunInfo.running) { // Stop $.ajax({ url: '/run/current', type: "DELETE", complete: function(jqXHR, status) { if(status == 'success') { reloadCurrentRun() } else { console.log(status) } } }) } else { // run $.post('/run', function(data, status) { if(status == 'success') { stdoutText.val('') reloadCurrentRun() } else { console.log(status) } }) } }) socket.on('stdout', function (data) { //console.log("stdout", data) stdoutText.val(stdoutText.val() + data) stdoutText.scrollTop(stdoutText[0].scrollHeight); }); socket.on('exit', function (data) { reloadCurrentRun() }); refreshRun() }) }) this.deployBtn = $('a.btn.deploy') this.deployBtn.click(function(event) { event.stopImmediatePropagation() event.preventDefault() self.currentEditor = null self._updateEditorUI() self.editorPlaceholder.load('/deploy', function(data, status) { }) }) this.filesPlaceholder = filesPlaceholder || null this.currentEditor = null this.filesView = new FireFileView(this.filesPlaceholder, function(filePath){ var editorRefreshFinished = function() { } var fileType = null if(self.currentEditor && (self.currentEditor.editorProvider.getFileType(filePath) == self.currentEditor.fileType)) { self.currentEditor.loadFile(filePath, editorRefreshFinished) self._updateEditorUI() } else { self._clearCurrentEditor() var editorProvider = null for(var i = 0;i < self.editorProviders.length; i++) { var provider = self.editorProviders[i] if(fileType = provider.getFileType(filePath)) { editorProvider = provider break } } if(!editorProvider) { editorProvider = new FireUnsupportedEditorProvider() } var editor = new editorProvider.editorType(self.editorPlaceholder) editor.editorProvider = editorProvider editor.fileType = fileType editor.loadFile(filePath, editorRefreshFinished) if(editorProvider instanceof FireUnsupportedEditorProvider) { self.currentEditor = null } else { self.currentEditor = editor } self._updateEditorUI() } }) this.editorProviders = [ new FireJSONEditorProvider(), new JSONFileEditorProvider(), new JavascriptFileEditorProvider() ] for(var i = 0; i < this.editorProviders.length; i++) { this.editorProviders[i].ide = this } this.editorSaveButton = $('a.btn.save', this.editorContainer) this.editorDeleteButton = $('a.btn.delete', this.editorContainer) this.editorPathLabel = $('span.path', this.editorContainer) this.editorSaveButton.click(function(event) { event.stopImmediatePropagation() event.preventDefault() self.currentEditor.save() }) // Delete File Dialog $( "div.delete-file-dialog" ).dialog({ autoOpen: false, height: "auto", width: 350, modal: true, buttons: { "No": function() { $( this ).dialog( "close" ); }, "Yes": function() { deleteFileFromDialog() } } }); this.editorDeleteButton.click(function(event) { event.stopImmediatePropagation() event.preventDefault() $('div.delete-file-dialog span.filename').text(self.currentEditor.filePath) $('div.delete-file-dialog').dialog('open') }) var createFileFromDialog = function() { $( "div.create-file-dialog" ).dialog( "close" ); self.createFile(self._getSelectedFileTypeDialog(), self.fileNameInput.val()) } var deleteFileFromDialog = function() { $( "div.delete-file-dialog" ).dialog( "close" ); self.currentEditor.delete(function() { self.filesView.refresh() }) self.currentEditor = null self._updateEditorUI() } // Create File Dialog $( "div.create-file-dialog" ).dialog({ autoOpen: false, height: 250, width: 350, modal: true, buttons: { "Cancel": function() { $( this ).dialog( "close" ); }, "Create": function() { createFileFromDialog() } } }); $( "div.create-file-dialog" ).keydown(function(e){ if (e.keyCode == 13) { e.stopPropagation() e.preventDefault() createFileFromDialog() } }); this.fileTypeCombo = $( "div.create-file-dialog select#file-type" ) this.fileTypes = [] this.editorProviders.forEach(function(provider) { for(var i = 0; i < provider.fileTypes.length; i++) { self.fileTypes.push(provider.fileTypes[i]) } }) var providerId = 0 this.fileTypes.forEach(function(fileType) { self.fileTypeCombo.append($('<option>').attr('value', providerId).text(fileType.description)) providerId++ }) self.fileTypeCombo.change(function() { var selectedFileType = self._getSelectedFileTypeDialog() if(selectedFileType && selectedFileType.fixedFileName) { self.fileNameInput.attr('disabled', 'disabled') self.fileNameInput.val(selectedFileType.fixedFileName) } else { self.fileNameInput.removeAttr('disabled', 'disabled') } }) this.fileNameInput = $( "div.create-file-dialog input#file-name" ) // Create File Button this.createFileButton = $('a.btn.create', this.filesPlaceholder) this.createFileButton.click(function(event) { event.stopImmediatePropagation() event.preventDefault() self.fileNameInput.val('') $('div.create-file-dialog').dialog('open') }) // Finish Initialization self._updateEditorUI() } FireIde.prototype._clearCurrentEditor = function() { this.editorPlaceholder.empty() } FireIde.prototype._getSelectedFileTypeDialog = function() { return this.fileTypes[parseInt(this.fileTypeCombo.val())] } FireIde.prototype.createFile = function(fileType, fileName) { this._clearCurrentEditor() var provider = fileType.provider var self = this var editor = new provider.editorType(this.editorPlaceholder) editor.editorProvider = provider editor.newFile(fileType.addExtensionToFileName(this.filesView.getCurrentPath() + fileName), fileName, fileType, function() { self.currentEditor = editor self._updateEditorUI() self.filesView.refresh() }) } FireIde.prototype._updateEditorUI = function() { if(this.currentEditor) { this.editorPathLabel.fadeIn() this.editorPathLabel.text(this.currentEditor.filePath) this.editorSaveButton.fadeIn() this.editorDeleteButton.fadeIn() } else { this._clearCurrentEditor() this.editorPathLabel.hide() this.editorSaveButton.hide() this.editorDeleteButton.hide() } } function FireIdeEditorProvider() { this.fileTypes = [] } FireIdeEditorProvider.prototype.addFileType = function(fileType) { if(!(fileType instanceof FireIdeEditorFileType)) { throw "fileType must be instance of FireIdeEditorFileType" } fileType.provider = this this.fileTypes.push(fileType) } FireIdeEditorProvider.prototype.getFileType = function(filePath) { for(var i = 0; i < this.fileTypes.length; i++) { var fileType = this.fileTypes[i] var canEdit = fileType.canEdit(filePath) if(canEdit) return fileType } return null } FireIdeEditorProvider.prototype.canEdit = function(filePath) { return this.getFileType(filePath) != null } FireIdeEditorProvider.prototype.createEditor = function(editorPlaceholder) { throw "createEditor, missing implementation" } function FireIdeEditorFileType(description, extension, fixedFileName) { this.extensionRegex = new RegExp("(\\.(" + extension + ")$)", 'i') this.extension = extension this.description = description this.provider = null this.fixedFileName = fixedFileName } FireIdeEditorFileType.prototype.addExtensionToFileName = function(fileName) { if(this.fixedFileName) { return fileName } return fileName + "." + this.extension } FireIdeEditorFileType.prototype.canEdit = function(filePath) { return this.extensionRegex.test(filePath) } FireIdeEditorFileType.prototype.createNewFileContent = function(fileName) { throw "FireIdeEditorFileType.createNewFileContent, missing implementation" } function FireIdeEditor(editorPlaceholder) { this.editorPlaceholder = editorPlaceholder || null this.filePath = null this.fileType = null this.content = null } FireIdeEditor.prototype.loadFile = function(filePath, refreshFinishedCallback) { this.filePath = filePath if(!this.fileType) { this.fileType = this.editorProvider.getFileType(this.filePath) } this.reload(refreshFinishedCallback) } FireIdeEditor.prototype.reload = function(refreshFinishedCallback) { var self = this $.get('/fs/' + this.filePath, function(response, status, xhr) { self.content = response.response self.refresh(refreshFinishedCallback) }); } FireIdeEditor.prototype.newFile = function(newFilePath, fileName, fileType, finishedCallback) { this.filePath = newFilePath this.fileType = this.editorProvider.getFileType(this.filePath) var self = this this.content = fileType.createNewFileContent(fileName) self.refresh(function() { self.save(finishedCallback) }) } FireIdeEditor.prototype.save = function(saveFinishedCallback) { this.beforeSave() var self = this $.ajax({ url: '/fs/' + this.filePath, type: "POST", data: { content: this.content }, complete: function(response, status, xhr) { refreshExpressionTags() if(saveFinishedCallback) { saveFinishedCallback() } } }); } FireIdeEditor.prototype.delete = function(finishedCallback) { $.ajax({ url: '/fs/' + this.filePath, type: "DELETE", complete: function(jqXHR, status) { finishedCallback() } }) } FireIdeEditor.prototype.beforeSave = function() { } FireIdeEditor.prototype.refresh = function() { throw "FireIdeEditor.refresh, missing implementation" } /* * FireUnsupportedEditorProvider */ function FireUnsupportedEditorProvider() { } FireUnsupportedEditorProvider.prototype = new FireIdeEditorProvider() FireUnsupportedEditorProvider.prototype.canEdit = function(fileName) { return true } FireUnsupportedEditorProvider.prototype.editorType = FireUnsupportedEditor function FireUnsupportedEditor(editorPlaceholder) { FireIdeEditor.apply(this, arguments) this._warningContainer = $('<div>').append($('<p>').text('Sorry, there is no editor available for this kind of file')).addClass('unsupported-editor').appendTo(this.editorPlaceholder) } FireUnsupportedEditor.prototype = new FireIdeEditor() FireUnsupportedEditor.prototype.refresh = function(refreshFinishedCallback) { // booh! refreshFinishedCallback() } function FireIdeToJSONString(value) { return JSON.stringify(value, null, 2) } /* * FireJSONEditorProvider */ function FireJSONEditorProvider() { var fireJSONFile = new FireIdeEditorFileType("Fire.js JSON Expression(.fjson)", "fjson") fireJSONFile.createNewFileContent = function(fileName) { return FireIdeToJSONString({ name: fileName, json: null }) } this.addFileType(fireJSONFile) } FireJSONEditorProvider.prototype = new FireIdeEditorProvider() FireJSONEditorProvider.prototype.editorType = FireJSONEditor function FireJSONEditor(editorPlaceholder) { FireIdeEditor.apply(this, arguments) this.attributesEditorContainer = $('<div>').addClass('attributes').append((this.attributesEditorElement = $('<div>').addClass('editor'))).appendTo(this.editorPlaceholder) this.bodyEditorContainer = $('<div>').addClass('editor_body').append((this.bodyEditorElement = $('<div>').addClass('editor'))).appendTo(this.editorPlaceholder) } FireJSONEditor.prototype = new FireIdeEditor() FireJSONEditor.prototype.getDocumentParts = function(doc) { if(doc && typeof(doc) == 'object' && (!(doc instanceof Array))) { var parts = { attributes: {}, body: doc.json || null } Object.keys(doc).forEach(function(prop) { if(prop != 'json') { parts.attributes[prop] = doc[prop] } }) return parts } else { return { attributes: { name: this.filePath }, body: null } } } FireJSONEditor.prototype.beforeSave = function() { var finalDoc = {} var attributes = this.attributesEditor.serialize() Object.keys(attributes).forEach(function(prop) { if(prop != 'json') { finalDoc[prop] = attributes[prop] } }) finalDoc.json = this.bodyEditor.serialize() this.content = FireIdeToJSONString(finalDoc) } FireJSONEditor.prototype.refresh = function(refreshFinishedCallback) { var docParts = this.getDocumentParts(JSON.parse(this.content)) var editorSelection = new SelectionGroup() this.attributesEditor = new FireEditor(this.attributesEditorElement, editorSelection, "Attributes") this.attributesEditorContainer.css("min-height", this.attributesEditorContainer.height()) this.attributesEditor.loadDocument(docParts.attributes) this.attributesEditorContainer.animate({ "min-height": null }) this.bodyEditor = new FireEditor(this.bodyEditorElement, editorSelection, "Body") this.bodyEditorContainer.css("min-height", this.bodyEditorContainer.height()) this.bodyEditor.loadDocument(docParts.body) this.bodyEditorContainer.animate({ "min-height": null }) refreshFinishedCallback() } /* * JSONFileEditorProvider */ function JSONFileEditorProvider() { var self = this var packageFileType = new FireIdeEditorFileType("package.json", "package.json", "package.json") packageFileType.extensionRegex = /package.json$/i packageFileType.createNewFileContent = function(fileName) { return FireIdeToJSONString({ name: "MyApp", version: "1.0.0" }) } packageFileType.createHeaderView = function(editor, elementContainer) { var packageNameInput = null var packageInfoContainer = null var buttons = null var loaderIcon = null var npmSearchElementContainer = $('<div>').addClass('npm-search-container').appendTo(elementContainer) var npmSearchElement = $('<div>').addClass('npm-search') npmSearchElement.append($('<p>').addClass('title').text('NPM Search')).append(packageNameInput = $('<input type="text">')).append(loaderIcon = $('<img class="loader" src="/images/ajax-loader.gif">').hide()).append(packageInfoContainer = $('<div>').hide()).append(buttons = $('<div class="buttons">').hide()) npmSearchElement.appendTo(npmSearchElementContainer) $('<span>').addClass('attribute-name').text('Description:').appendTo(packageInfoContainer) var packageDescriptionLabel = $('<span>').addClass('attribute-value').appendTo(packageInfoContainer) $('<span>').addClass('attribute-name').text('Version:').appendTo(packageInfoContainer) var packageVersionLabel = $('<span>').addClass('attribute-value').appendTo(packageInfoContainer) $('<span>').addClass('attribute-name').text('Author:').appendTo(packageInfoContainer) var packageAuthorLabel = $('<span>').addClass('attribute-value').appendTo(packageInfoContainer) $('<span>').addClass('attribute-name').text('fire.js Enabled:').appendTo(packageInfoContainer) var packageEnabledLabel = $('<span>').addClass('attribute-value').appendTo(packageInfoContainer) var cancelButton = $('<a class="btn" href="#cancel">').text('Cancel').appendTo(buttons) var addDependencyButton = $('<a class="btn" href="#addDependency">').text('Add Dependency').appendTo(buttons) var npmSearchButtons = $('<div class="buttons">').appendTo(npmSearchElementContainer) var installButton = $('<a class="btn" href="#install">').text('Update Dependencies').appendTo(npmSearchButtons) cancelButton.click(function(event) { event.stopPropagation() event.preventDefault() packageInfoContainer.hide() buttons.hide() packageNameInput.val('') }) installButton.click(function(event) { loaderIcon.fadeIn() event.stopPropagation() event.preventDefault() $.get('/npm/install/', function(data) { editor.reload() loaderIcon.fadeOut() refreshExpressionTags() self.ide.filesView.refresh() }) }) addDependencyButton.click(function(event) { loaderIcon.fadeIn() event.stopPropagation() event.preventDefault() $.post('/npm/dependencies',{ name: packageNameInput.val() }, function(data) { editor.reload() loaderIcon.fadeOut() refreshExpressionTags() self.ide.filesView.refresh() }) }) packageNameInput.change(function() { packageInfoContainer.hide() buttons.fadeOut() loaderIcon.show() $.get('/npm/info/' + $(this).val(), function(data) { loaderIcon.hide() if(!data.response) { } else { var packageInfo = data.response[Object.keys(data.response)[0]] packageDescriptionLabel.text(packageInfo.description) packageVersionLabel.text(packageInfo.version) packageAuthorLabel.text(packageInfo.author) var isEnabled = packageInfo.keywords && packageInfo.keywords.indexOf('ignitable') != -1 packageEnabledLabel.text(isEnabled ? "Yes!" : "No") if(isEnabled) { packageEnabledLabel.addClass("highlight-value") } else { packageEnabledLabel.removeClass("highlight-value") } packageInfoContainer.fadeIn() buttons.fadeIn() } }) }) } this.addFileType(packageFileType) var manifestFile = new FireIdeEditorFileType("Fire.js Ignition Manifest", ".manifest.json", "ignition.manifest.json") manifestFile.createNewFileContent = function(fileName) { return FireIdeToJSONString({ modules: [] }) } this.addFileType(manifestFile) var JSONFile = new FireIdeEditorFileType("Plain JSON File(.json)", "json") JSONFile.createNewFileContent = function(fileName) { return FireIdeToJSONString({}) } this.addFileType(JSONFile) } JSONFileEditorProvider.prototype = new FireIdeEditorProvider() JSONFileEditorProvider.prototype.editorType = JSONFileEditor function JSONFileEditor(editorPlaceholder) { FireIdeEditor.apply(this, arguments) this.bodyEditorContainer = $('<div>').addClass('editor_body').append((this.headerEditorContainer = $('<div>').addClass('editor'))).append((this.bodyEditorElement = $('<div>').addClass('editor'))).appendTo(this.editorPlaceholder) this.headerEditorContainer.hide() } JSONFileEditor.prototype = new FireIdeEditor() JSONFileEditor.prototype.beforeSave = function() { this.content = FireIdeToJSONString(this.bodyEditor.serialize(),null, 2) } JSONFileEditor.prototype.refresh = function(refreshFinishedCallback) { var doc = JSON.parse(this.content) var editorSelection = new SelectionGroup() this.bodyEditor = new FireEditor(this.bodyEditorElement, editorSelection) this.bodyEditorContainer.css("min-height", this.bodyEditorContainer.height()) this.bodyEditor.loadDocument(doc) this.bodyEditorContainer.animate({ "min-height": null }) if(this.fileType.createHeaderView) { this.headerEditorContainer.empty() this.headerEditorContainer.show() this.fileType.createHeaderView(this, this.headerEditorContainer) } else { this.headerEditorContainer.empty() this.headerEditorContainer.hide() } if(refreshFinishedCallback) { refreshFinishedCallback() } } /* * JavascriptFileEditorProvider */ function JavascriptFileEditorProvider() { var JSFile = new FireIdeEditorFileType("Plain Javascript File(.js)", "js") JSFile.createNewFileContent = function(fileName) { return 'console.log("Hello There: ' + fileName + '")' } this.addFileType(JSFile) var FireInitFile = new FireIdeEditorFileType("Fire.js Bootstraper index.js", "js", "index.js") FireInitFile.createNewFileContent = function(fileName) { return 'var fire = require("fire")\n\nfire.executeApplication(["package.json"].concat(process.argv.slice(2)))\n\n' } this.addFileType(FireInitFile) var CustomExpressionFile = new FireIdeEditorFileType("Fire.js Javascript Expression(.fjs)", "fjs") CustomExpressionFile.createNewFileContent = function(fileName) { var ExpName = fileName.replace(/\./ig, "_") + "_Expression" return 'var fire =