commit ea4a65ddc9edf0fd608bdb77b4e9b3d514082130 Author: Curtis SerVaas Date: Thu Jul 24 15:22:08 2014 -0400 Clean start diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a7732e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ + +node_modules/ +lib/db.js + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5c93f45 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/README.md b/README.md new file mode 100644 index 0000000..97d56c9 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +**Live Demo:** http://glacial-island-2506.herokuapp.com/ + +Features ndentJS has that workflowy doesn't have: +-------- + - Live googleDocs-esque editing. + + +Pending features that workflowy has: +-------- + - Several small bug fixes. + - A better UI + - Hashtags + - Search + - Import/Export + + +Pending features that Workflowy doesn't have: +------- + - Split Screen: + - Revision Control. + - Transclusion/Aliasing/Graph-structure. + - Latex Editor. + + + +Installation and Usage +====================== + + - A throwaway database account is provided. But, you can edit /lib/db.js with your own credentials. + - `npm install` + - `node app.js` \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..d995fca --- /dev/null +++ b/app.js @@ -0,0 +1,55 @@ + +/** + * Module dependencies. + */ + +var express = require('express'); +// var routes = require('./routes/routes.js'); +var fs = require('fs'); +var http = require('http'); +var path = require('path'); +var crypto = require('crypto'); +var db = require('./lib/db'); +var helperLib = require('./lib/helperLib.js'); + +var app = express() +var server = http.Server(app); +helperLib.createSocket(server); +server.listen(process.env.PORT || 3000); + + + +// all environments +app.set('port', process.env.PORT || 3000); +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); +app.use(express.favicon()); +app.use(express.logger('dev')); +app.use(express.json()); +app.use(express.urlencoded()); +app.use(express.methodOverride()); +app.use(app.router); +app.use(express.static(path.join(__dirname, 'public'))); + +app.use(express.session({secret: 'secretpasswordforsessions', store: helperLib.getSessionStore()})); + +app.configure(function () { + app.use(express.bodyParser()); //not sure... + app.set('views', __dirname + '/views'); + app.set('view engine', 'ejs'); + app.use(express.static(__dirname + '/public')); //ALREADY USING IT. +}); +app.set('view options', { + layout: false +}); + +app.get('/',function(req,res){ + console.log("\n\nrenderingIndex\n") + res.render('index'); +}); + +if(process.argv[2] == "restart"){ + console.log("restarting"); +helperLib.setUpDB(); +} + diff --git a/lib/db.js b/lib/db.js new file mode 100644 index 0000000..4963d13 --- /dev/null +++ b/lib/db.js @@ -0,0 +1,25 @@ +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; +module.exports.mongoose = mongoose; +module.exports.Schema = Schema; + +// Connect to cloud database +//https://mongolab.com/ +var username = "throwaway" +var password = "throwaway1";// +var address = '@ds037637.mongolab.com:37637/throwaway_db'; +connect(); + + +// Connect to mongo +function connect() { + + var url = 'mongodb://' + username + ':' + password + address; + try { mongoose.connect(url); } + catch(err) { console.log("Error: Sign In to MongoLab") } + console.log("error caught"); + +} +function disconnect() { + mongoose.disconnect() +} diff --git a/lib/helperLib.js b/lib/helperLib.js new file mode 100644 index 0000000..dd1b51a --- /dev/null +++ b/lib/helperLib.js @@ -0,0 +1,172 @@ +var //cookie = require('cookie'), + crypto = require('crypto') + , db = require('./db') + //, exchange = require('./exchange') + , express = require('express') + , http = require('http') + , MemoryStore = express.session.MemoryStore + , ObjectID = require('mongodb').ObjectID, + cookie = require('cookie'); + +var User = require('../models/User.js'); +var Node = require('../models/Node.js'); +var sessionStore = new MemoryStore(); +var io; +var online = []; +var lastExchangeData = {}; + + +console.log("\n\nLOOK HERE!!") +// console.log(revAlg); + +// var revAlg = require('./revControlAlg.js'); +// var getTimeHash = revAlg.getTimeHash; +console.log("\n\n"); + + +module.exports = { + // createUser: function(username, email, password, callback) { + // var user = {username: username, email: email + // , password: encryptPassword(password)}; + // db.insertOne('users', user, callback); + // }, + createUser: function(username, password, callback){ + User.addUser(username, password, callback); + }, + + getNodes: function(){ + return Node.findNodes(); + }, + + getSessionStore: function(){ + return sessionStore; + }, + + createSocket: function(server) { + io = require('socket.io').listen(server); + io.sockets.on('connection', function(socket){ + + //socket.emit('news', {hello: "world"}); + socket.on('nodeRequest', function(data){ + var nodes = Node.findNodes(socket); //finds, then sends through socket. + //var nodes = {'keep': 'calm'}; + //socket.emit('nodeData', nodes); (emit is in findNodes); + }) + + socket.on("edit", function(data){ + var id = data[0]; + var newText = data[1]; + console.log("\n\n\n\n edit received:" + newText); + Node.updateText(id, newText); + socket.broadcast.emit("edit", [id, newText]); + + }); + + //This works for all ids except negative ids. + socket.on("editing", function(id){ + socket.broadcast.emit("editing", id); + }); + + + socket.on("blurred", function(data){ + console.log('\nBLURRED\n') + socket.broadcast.emit("blurred", data); + var id = data[0]; + var text = data[1]; + Node.updateText(id, text); + }); + + //I'm pretty sure i don't use this anywhere. + socket.on('nodeInsert', function(data){ + Node.addNode("testText", [], [], function(){}); + }); + + socket.on("transclude", function(data){ + var parId = data[0]; + var transcludeId = data[2]; + var newIndex = data[1]; + var now = Date.now(); + Node.updateParent(parId, transcludeId ,newIndex, now ); + }) + + + + socket.on("newNode", function(data){ + //data[0] = [parID, newindex] . data[1] = + var modelJson = data[1]; //(includes the negative ID to find later); + var parId = data[0][0]; + var newIndex = data[0][1]; + + var now = Date.now(); + var callback = function(err, instance, now){ + socket.emit("updateReceived", [modelJson._id ,instance, data[0]]); + socket.broadcast.emit("newNode", [ data[0] ,instance]); + //io.sockets.emit("newNode", [data[0], instance]) + //I'm going to have to update the parent Model on this as well... + //(and therefore, broadcast the array...) + Node.updateParent(parId, instance._id ,newIndex, now ); + } + Node.addNode(modelJson.text, modelJson.children, modelJson.parents, callback); + + }); + + socket.on("removeNode", function(data){ + var thisId = data[0]; + var thisIndex = data[1]; + var parId = data[2]; + socket.broadcast.emit("removeNode", data); + Node.removeNode(thisId, thisIndex, parId); + + }); + + socket.on("movedNode", function(data){ + // var ids = [thisModel.get("_id"), oldParModel.get("_id"), newParModel.get("_id")]; + // var arrays = [thisModel.get("parents"), oldParModel.get("children"), newParModel.get("children")]; + // var data = [ids, arrays]; + Node.moveNode(data[0], data[1]); + socket.broadcast.emit("movedNode", [data[0], data[2]]); + + + + }); + + socket.on("getTimeHash", function(){ + var timeHash = getTimeHash(); + + socket.emit("timeHash", timeHash); + }) + + + }); + // io.configure(function (){ + // io.set('authorization', function (handshakeData, callback) { + // if (handshakeData.headers.cookie) { + // handshakeData.cookie = cookie.parse(decodeURIComponent(handshakeData.headers.cookie)); + // handshakeData.sessionID = handshakeData.cookie['connect.sid']; + // sessionStore.get(handshakeData.sessionID, function (err, session) { + // if (err || !session) { + // return callback(null, false); + // } else { + // handshakeData.session = session; + // console.log('session data', session); + // return callback(null, true); + // } + // }); + // } + // else { + // return callback(null, false); + // } + // }); + // }); + }, + + setUpDB: function(){ + Node.setUpDB(); + //getTimeHash(); + } + + + + +} + diff --git a/models/Node.js b/models/Node.js new file mode 100644 index 0000000..e5a9056 --- /dev/null +++ b/models/Node.js @@ -0,0 +1,220 @@ +var db = require('../lib/db'); + +var NodeSchema = new db.Schema({ + text: {type: String}, + children: {type: Array}, + parents: {type: Array} +}) + +var SnapSchema = new db.Schema({ + text: {type: String}, + children: {type: Array}, + parents: {type: Array}, + timestamp: {type: Number}, + cur_id: {type: String} +}); + + + +var rootID; + +var MySnap = db.mongoose.model('snaps', SnapSchema); +var MyNode = db.mongoose.model('nodes', NodeSchema); + +// Exports +module.exports.addNode = addNode; +module.exports.findNodes = findNodes; +module.exports.updateParent = updateParent; +module.exports.updateText = updateText; +module.exports.setUpDB = setUpDB; +module.exports.removeNode = removeNode; +module.exports.moveNode = moveNode; +module.exports.rootNodeId = rootNodeId; +module.exports.MySnap = MySnap; + +function rootNodeId(){ + return rootID; +} + +function moveNode(ids, arrays){ + var thisId = ids[0]; //draggedId + var now = Date.now(); + MyNode.findById(thisId, null, function(err, node){ + addSnap(node, now); + node.parents = arrays[0]; + node.save(); + }); + var oldParId = ids[1]; + MyNode.findById(oldParId, null, function(err, node){ + addSnap(node, now); + node.children = arrays[1]; + node.save(); + }); + var newParId = ids[2]; + MyNode.findById(newParId, null, function(err, node){ + addSnap(node, now); + node.children = arrays[2]; + node.save(); + }); +} + +function addSnap(node, now, callback){ + var instance = new MySnap(); + instance.text = node.text; + instance.children = node.children; + instance.parents = node.parents; + instance.cur_id = node._id; + instance.timestamp = now; + instance.save(function(err){ + if(err){ + //callback(err); + } + else{ + //callback(null, instance); + } + }); + +} + + + + + + + + + + + + + +//this is technically not good asynchronous code. +//this coulde be made into a recursive function which takes an array of todo-strings. +function addListOfNodes(){ + addNode("0root", [], [], function(err, rootNode){ + rootID = rootNodeId._id; + var parId = rootNode._id; + addNode("Todos", [], [parId], function(err, vadar){ //called vadar because it is the parent/father + var vadarId = vadar._id; + var childArray = []; + addNode("Revision Control", [], [vadarId], function(err, child){ + childArray.push(child._id); + }); + addNode("LatexEditor", [], [vadarId], function(err, child){ + childArray.push(child._id); + }); + addNode("Lots of other stuff", [], [vadarId], function(err, child){ + childArray.push(child._id); + vadar.children = childArray; + vadar.save(); + }); + rootNode.children = [vadarId]; + rootNode.save(); + }); + }); +} + + +function setUpDB(){ + MyNode.remove({}, function(err) { console.log('collection removed') }); + MySnap.remove({}, function(err) { console.log('collection removed') }); + addListOfNodes(); +} + +function removeNode(thisId, thisIndex, parId){ + var now = Date.now(); + MyNode.findById(parId, null, function(err, parNode){ + if(err || parNode == null){ + return; + } + addSnap(parNode, now); + var temp = parNode.children; + console.log(temp.splice(thisIndex, 1)); + console.log(temp); + parNode.children = temp; + + parNode.save(); + }); + MyNode.findById(thisId, null, function(err, delNode){ + if(err || delNode == null){ + return; + } + addSnap(delNode, now); + if(delNode.parents.length ==1 ){ + delNode.remove(); + } + else{ //this is the condition that we'll have to take care of if there are dups. + delNode.parents = _.without(parents, parId); + delNode.save(); + } + }) +} + +function updateText(id, newText){ + console.log("\nUPDATE TEXT\n" + id + newText); + var now = Date.now(); + MyNode.findById(id, null, function(err, node){ + if(err || node == null){ + return; + } + addSnap(node, now); + node.text = newText; + node.save(); + }); +} + +function updateParent(parId, newId ,newIndex, now){ + MyNode.findById(parId, null, function(err, parNode){ + if(err || parNode == null){ + return; + } + addSnap(parNode, now); + parNode.children.insert(newIndex, newId); + parNode.save(); + }) +} + +//add Node to the DB. +function addNode(text, children, parents, callback){ + var now = Date.now(); + var instance = new MyNode(); + instance.text = text; + instance.children = children; + instance.parents = parents; + addSnap(instance, now); + + instance.save(function (err) { + if (err) { + callback(err); + } + else { + callback(null, instance, now); + } + }); +} + + +function findNodes(socket){ + var nodes = {'keeping': 'calm'} + MyNode.find(function(err, nodes){ + if(!err){ + socket.emit('nodeData', nodes) + return {'hell': 'yes'} + }else{ + socket.emit('nodeData', "error!!!") + return {'hell': 'no'} + } + + + }); +} + +Array.prototype.insert = function (index, item) { + this.splice(index, 0, item); +}; + +Array.prototype.remove = function(from, to) { + var rest = this.slice((to || from) + 1 || this.length); + this.length = from < 0 ? this.length + from : from; + return this.push.apply(this, rest); +}; diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..e9f187d --- /dev/null +++ b/models/User.js @@ -0,0 +1,29 @@ +//(not being used yet) + +var crypto = require('crypto') +var db = require('../lib/db'); +var UserSchema = new db.Schema({ + username : {type: String, unique: true} + , password : String +}) +var MyUser = db.mongoose.model('User', UserSchema); +// Exports +module.exports.addUser = addUser; +// Add user to database +function addUser(username, password, callback) { + var instance = new MyUser(); + instance.username = username; + instance.password = encryptPassword(password); + instance.save(function (err) { + if (err) { + callback(err); +} + else { + callback(null, instance); + } + }); +} + +function encryptPassword(plainText) { + return crypto.createHash('md5').update(plainText).digest('hex'); +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..fe1ffa2 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "ndent", + "version": "0.0.1-2", + "private": true, + "scripts": { + "start": "node app.js" + }, + "dependencies": { + "express": "3.1.0", + "jade": "0.26.1", + "mongoose": "2.6.5", + "ejs": "0.8.3", + "mongodb": "^1.1.7", + "socket.io": "0.9.13", + "cookie": "0.0.4", + "underscore": "1.5.2" + }, + "subdomain": "ndent", + "engines": { + "node": "0.10.x" + } +} diff --git a/public/img/bullet-8.png b/public/img/bullet-8.png new file mode 100644 index 0000000..0a99d26 Binary files /dev/null and b/public/img/bullet-8.png differ diff --git a/public/img/bullet-combined.png b/public/img/bullet-combined.png new file mode 100644 index 0000000..9bfa638 Binary files /dev/null and b/public/img/bullet-combined.png differ diff --git a/public/img/bullet-outline.png b/public/img/bullet-outline.png new file mode 100644 index 0000000..9b4d886 Binary files /dev/null and b/public/img/bullet-outline.png differ diff --git a/public/img/bullet.png b/public/img/bullet.png new file mode 100644 index 0000000..0020d2a Binary files /dev/null and b/public/img/bullet.png differ diff --git a/public/img/glyphicons-halflings.png b/public/img/glyphicons-halflings.png new file mode 100644 index 0000000..a996999 Binary files /dev/null and b/public/img/glyphicons-halflings.png differ diff --git a/public/img/logo.png b/public/img/logo.png new file mode 100644 index 0000000..d8127d6 Binary files /dev/null and b/public/img/logo.png differ diff --git a/public/img/redo.png b/public/img/redo.png new file mode 100644 index 0000000..7a921c8 Binary files /dev/null and b/public/img/redo.png differ diff --git a/public/img/search.png b/public/img/search.png new file mode 100644 index 0000000..a18cffd Binary files /dev/null and b/public/img/search.png differ diff --git a/public/img/smiley.png b/public/img/smiley.png new file mode 100644 index 0000000..09feba7 Binary files /dev/null and b/public/img/smiley.png differ diff --git a/public/img/splitscreen.png b/public/img/splitscreen.png new file mode 100644 index 0000000..d0f435f Binary files /dev/null and b/public/img/splitscreen.png differ diff --git a/public/img/undo.png b/public/img/undo.png new file mode 100644 index 0000000..9cf4c68 Binary files /dev/null and b/public/img/undo.png differ diff --git a/public/js/app.js b/public/js/app.js new file mode 100644 index 0000000..a4b4a8a --- /dev/null +++ b/public/js/app.js @@ -0,0 +1,311 @@ +$(function(){ + //alert("jquery works"); + + $("body").on("mouseover", "a.expandCollapse", function(event){ + // console.log("bleh"); + // debugger; + $(event.target).find("a.expandCollapse").css("opacity", "1"); + $(event.target).closest("a.expandCollapse").css("opacity", "1"); + // $(event.target).siblings("a.expandCollapse").css("opacity", "1") + }); + $("body").on("mouseleave", "a.expandCollapse", function(event){ + // console.log("bleh"); + // debugger; + $(event.target).find("a.expandCollapse").css("opacity", ".001"); + $(event.target).closest("a.expandCollapse").css("opacity", ".001"); + // $(event.target).siblings("a.expandCollapse").css("opacity", ".001") + }); + + $("body").on("click", ".expandCollapse", function(event){ + var LI = $(event.target).parent(); + LI.children("ul").slideToggle(110); + LI.children(".zoomButton").toggleClass("collapsed"); + }); + $("body").on("click", ".splitScreen", function(){ + $(".main2").remove(); + }) + $("body").on("keydown", "textarea", keydownHandler); + $("body").on("focus", "textarea", function(event){ + var that = this; + voInitializer(that, event); + var id = $(event.target).closest("li").attr("data-id"); + console.log("ABOUT TO EMIT 'EDITING' "); + console.log(id); + socket.emit("editing", id); + }); + $("body").on("blur", "textarea", function(event){ + var thisLI = $(event.target).closest("li"); + var id = thisLI.attr("data-id"); + var text = thisLI.children().children("textarea").val(); + //console.log(id); + //alert("blurred ID" + id + text); + socket.emit("blurred", [id, text]); + $("textarea").textareaAutoExpand(); + }); + $("body").on("click", ".transclude", function(event){ + //alert(vo.thisId); + alert("Transclusion syncing with the server has not been implemented. KnownBugs:\n0.Don't make infinite loops.\n1."); + transclude(); + }); + + // AppRouter.initialize(); + myRouter = new AppRouter; + Backbone.history.start(); + + //console.log("typeOF FUNCTION"); + //console.log($.fn.textareaAutoExpand); +}); + + +function createPathMenu(event){ + //console.log($(event.target).attr("data-id")); + var pathDiv = $(event.target).parent().children("#pathDiv") + console.log(pathDiv); + if(pathDiv.length != 0){pathDiv.remove(); return; } + var curId = $(event.target).attr("data-id"); + var parents = nodesCollection.findWhere({_id:curId}).get("parents"); + var html = $(""); + console.log("parents="); + console.log(parents); + if(parents.length == 1){ + var parentId = parents[0]; + var grandParentId = nodesCollection.findWhere({_id:parentId}).get("parents"); + html.append($("
  • "+parentId+"
  • ")); + html.append($("
  • "+grandParentId+"
  • ")); + } + console.log(html); + // alert("something"); + $(event.target).parent().append(html); + +} + + + +Array.prototype.insert = function (index, item) { + this.splice(index, 0, item); +}; + +//http://stackoverflow.com/questions/500606/javascript-array-delete-elements +Array.prototype.remove = function(from, to) { + var rest = this.slice((to || from) + 1 || this.length); + this.length = from < 0 ? this.length + from : from; + return this.push.apply(this, rest); +}; + +function hasDuplicates(array) { + var valuesSoFar = {}; + for (var i = 0; i < array.length; ++i) { + var value = array[i]; + if (Object.prototype.hasOwnProperty.call(valuesSoFar, value)) { + return true; + } + valuesSoFar[value] = true; + } + return false; +} + +voInitializer = function(that, event){ + //var that = this; + vo = {}; + vo.hitEnter = (event.which == 13); + vo.hitTab = (event.which ==9); + vo.atEnd = ( $(that).getSelection().end == $(that).val().length); + vo.atBeg = ( $(that).getSelection().start == 0); + //cursor = $(this).getSelection().start; + vo.hitBack = (event.which ==8); + vo.empty = ($(that).val().length ==0); + //vo.thisLen = $() + + vo.rootLevel = $(that).closest("ul").is(".root") + vo.lastBullet = ( $(that).closest("li").is(":first-child") && vo.rootLevel); + vo.thisLI = $(event.target).closest("li"); + vo.thisId = vo.thisLI.attr("data-id"); //data-id. + vo.thisIndex = vo.thisLI.index(); //returns -1 if there's no match. + vo.thisModel = nodesCollection.findWhere({_id: vo.thisId}); + + //alert(thisIndex) + //thisModel = nodesCollection.get(thisId); + vo.siblingLI = vo.thisLI.prev(); + vo.siblingIndex = vo.siblingLI.index(); + vo.siblingId = vo.siblingLI.attr("data-id"); + vo.siblingModel = nodesCollection.findWhere({_id: vo.siblingId}); + + console.log(nodesCollection); + console.log(vo.thisModel); + + + + + if(vo.rootLevel){ + vo.parentLI = undefined; + vo.parentId = (vo.thisLI.closest("ul").attr("data-id")) + vo.grandParentId = undefined; // won't matter since outTab prevents it. //unless programattic. + } + else{ //not root level. + //debugger; + vo.parentLI = vo.thisLI.parent().closest("li"); + vo.parentId = (vo.parentLI.attr("data-id")); + if(vo.parentLI.attr("data-depth") == 0){ //could test this another way. + vo.grandParentId = vo.parentLI.closest("ul").attr("data-id"); + //console.log("grandParentId" + grandParentId) + } + else{ + vo.grandParentId = (vo.parentLI.parent().closest("li").attr('data-id')); + //console.log("grandParentId" + grandParentId) + } + } + vo.grandParentModel = nodesCollection.findWhere({_id: vo.grandParentId}); + vo.parentModel = nodesCollection.findWhere({_id: vo.parentId}); +} + + +keydownHandler = function(event){ //the entire body is wrapped in this. + var that = this; + // + if(event.which == undefined){ return; } + + voInitializer(that, event); + + //event.preventDefault(); + //http://stackoverflow.com/questions/20964729/run-keydown-event-handler-after-the-value-of-a-textarea-has-been-changed + //keyupp fixes this, but causes other problems. + if( !(vo.hitTab || vo.hitEnter || (vo.hitBack && vo.empty))){ + //The reason this isn't syncing perfectly between bullets is that... + //... the text-area val hasn't updated at this point. + vo.thisModel.set("text", $(that).val()); + _.each(vo.thisModel.get("views"), function(view){ + view.updateText() + }); + } + + if(vo.hitEnter){ + event.preventDefault(); + + if(event.shiftKey){ + _.each(vo.thisModel.get("views"), function(view){ + view.collapse(); + }); + event.preventDefault(); + alert("Temporarily is used for expand/collapse (instead of clicking)") + } + + if(!event.shiftKey){ + event.preventDefault(); + if(vo.empty){ + addNode(""); + return; + }//if(empty) + else{//!empty + if(vo.atEnd){ + //alert("CORRECT!") + addNode(""); + return; + }// + if(!vo.atEnd && !vo.atBeg){ //split bullet + var topStr = ''; + var botStr = ''; + splitText(that, botStr, topStr, addNode); + return; + //addNode(botStr, topStr); + } + if(!vo.atEnd && vo.atBeg){//addNode (before). (equivalent to splitting bullet) + var topStr = ''; + var botStr = ''; + splitText(that, botStr, topStr, addNode); + return; + //addNode(botStr, topStr); + } + }//!empty + + } + } //hitEnter + + if(vo.hitBack && vo.empty){ + event.preventDefault(); + removeNode(vo); + + }//hitBack. + + // // START ON HIT TAB +// if (vo.hitTab) { + +// event.preventDefault(); + +// if (event.shiftKey) { +// event.preventDefault(); + + +// if (($(this).parent().parent().parent().hasClass("root"))) { +// // do nothing. //alert() //IT USED TO BE ID = 'ROOT' // we use 'root SubList' because it has two classes. +// } +// else { // OUTDENT!! +// var newIndex = $(this).parent().parent().parent().closest("li").index(); +// debugger; +// moveNode(vo.thisModel, vo.thisIndex, vo.parentModel, vo.grandParentModel, newIndex+1, true); +// } +// } +// var hasAboveSibling = (vo.thisIndex != 0); +// if(!event.shiftKey && (hasAboveSibling) ){ +// var newIndex = vo.siblingModel.get("children").length; // no need for a + 1, because 0 index + insert (duh) +// moveNode(vo.thisModel, vo.thisIndex, vo.parentModel, vo.siblingModel, newIndex, true); +// } +// }// END ON HIT TAB +if((vo.hitTab && !event.shiftKey) || (event.keyCode == 39 && event.shiftKey)){ //INDENT + event.preventDefault(); + var hasAboveSibling = (vo.thisIndex != 0); + if(hasAboveSibling){ + var newIndex = vo.siblingModel.get("children").length; // no need for a + 1, because 0 index + insert (duh) + moveNode(vo.thisModel, vo.thisIndex, vo.parentModel, vo.siblingModel, newIndex, true); + } +} +if((vo.hitTab && event.shiftKey) || (event.keyCode == 37 && event.shiftKey)){// OUTDENT!! + if (($(this).parent().parent().parent().hasClass("root"))) { + // do nothing. //alert() //IT USED TO BE ID = 'ROOT' // we use 'root SubList' because it has two classes. + } + else { + + var newSiblingUL = $(this).parent().parent().parent(); + var newIndex = newSiblingUL.closest("li").index(); + moveNode(vo.thisModel, vo.thisIndex, vo.parentModel, vo.grandParentModel, newIndex+1, true); + setTimeout(function(){ //(MoveNode is asynchronous, so you need to wait a little bit.). + newSiblingUL.parent().next().children().children("textarea").focus(); + }, 100); + + + } +} + + if(event.keyCode == 38 && event.shiftKey && !vo.thisLI.is(":first-child")) { + moveNode(vo.thisModel, vo.thisIndex, vo.parentModel, vo.parentModel, vo.thisIndex-1, true); + return; + } + if(event.keyCode == 40 && event.shiftKey && !vo.thisLI.is(":last-child")) { + moveNode(vo.thisModel, vo.thisIndex, vo.parentModel, vo.parentModel, vo.thisIndex+1, true); + return; + } + + + +} + + + +Array.prototype.removeOne = function(parId){ + var parIndex = this.indexOf(parId); + this.remove(parIndex); +} + + + + + + + + + + + + + + + diff --git a/public/js/collections/nodesCollection.js b/public/js/collections/nodesCollection.js new file mode 100644 index 0000000..52674b8 --- /dev/null +++ b/public/js/collections/nodesCollection.js @@ -0,0 +1,5 @@ +var nodesCollection = Backbone.Collection.extend({ + + model: NodeModel + +}); \ No newline at end of file diff --git a/public/js/libs/externalLibs/backbone-min.js b/public/js/libs/externalLibs/backbone-min.js new file mode 100644 index 0000000..d82f45b --- /dev/null +++ b/public/js/libs/externalLibs/backbone-min.js @@ -0,0 +1,2 @@ +(function(t,e){if(typeof exports!=="undefined"){e(t,exports,require("underscore"))}else if(typeof define==="function"&&define.amd){define(["underscore","jquery","exports"],function(i,r,s){t.Backbone=e(t,s,i,r)})}else{t.Backbone=e(t,{},t._,t.jQuery||t.Zepto||t.ender||t.$)}})(this,function(t,e,i,r){var s=t.Backbone;var n=[];var a=n.push;var o=n.slice;var h=n.splice;e.VERSION="1.1.0";e.$=r;e.noConflict=function(){t.Backbone=s;return this};e.emulateHTTP=false;e.emulateJSON=false;var u=e.Events={on:function(t,e,i){if(!c(this,"on",t,[e,i])||!e)return this;this._events||(this._events={});var r=this._events[t]||(this._events[t]=[]);r.push({callback:e,context:i,ctx:i||this});return this},once:function(t,e,r){if(!c(this,"once",t,[e,r])||!e)return this;var s=this;var n=i.once(function(){s.off(t,n);e.apply(this,arguments)});n._callback=e;return this.on(t,n,r)},off:function(t,e,r){var s,n,a,o,h,u,l,f;if(!this._events||!c(this,"off",t,[e,r]))return this;if(!t&&!e&&!r){this._events={};return this}o=t?[t]:i.keys(this._events);for(h=0,u=o.length;h").attr(t);this.setElement(r,false)}else{this.setElement(i.result(this,"el"),false)}}});e.sync=function(t,r,s){var n=T[t];i.defaults(s||(s={}),{emulateHTTP:e.emulateHTTP,emulateJSON:e.emulateJSON});var a={type:n,dataType:"json"};if(!s.url){a.url=i.result(r,"url")||M()}if(s.data==null&&r&&(t==="create"||t==="update"||t==="patch")){a.contentType="application/json";a.data=JSON.stringify(s.attrs||r.toJSON(s))}if(s.emulateJSON){a.contentType="application/x-www-form-urlencoded";a.data=a.data?{model:a.data}:{}}if(s.emulateHTTP&&(n==="PUT"||n==="DELETE"||n==="PATCH")){a.type="POST";if(s.emulateJSON)a.data._method=n;var o=s.beforeSend;s.beforeSend=function(t){t.setRequestHeader("X-HTTP-Method-Override",n);if(o)return o.apply(this,arguments)}}if(a.type!=="GET"&&!s.emulateJSON){a.processData=false}if(a.type==="PATCH"&&k){a.xhr=function(){return new ActiveXObject("Microsoft.XMLHTTP")}}var h=s.xhr=e.ajax(i.extend(a,s));r.trigger("request",r,h,s);return h};var k=typeof window!=="undefined"&&!!window.ActiveXObject&&!(window.XMLHttpRequest&&(new XMLHttpRequest).dispatchEvent);var T={create:"POST",update:"PUT",patch:"PATCH","delete":"DELETE",read:"GET"};e.ajax=function(){return e.$.ajax.apply(e.$,arguments)};var S=e.Router=function(t){t||(t={});if(t.routes)this.routes=t.routes;this._bindRoutes();this.initialize.apply(this,arguments)};var $=/\((.*?)\)/g;var H=/(\(\?)?:\w+/g;var A=/\*\w+/g;var I=/[\-{}\[\]+?.,\\\^$|#\s]/g;i.extend(S.prototype,u,{initialize:function(){},route:function(t,r,s){if(!i.isRegExp(t))t=this._routeToRegExp(t);if(i.isFunction(r)){s=r;r=""}if(!s)s=this[r];var n=this;e.history.route(t,function(i){var a=n._extractParameters(t,i);s&&s.apply(n,a);n.trigger.apply(n,["route:"+r].concat(a));n.trigger("route",r,a);e.history.trigger("route",n,r,a)});return this},navigate:function(t,i){e.history.navigate(t,i);return this},_bindRoutes:function(){if(!this.routes)return;this.routes=i.result(this,"routes");var t,e=i.keys(this.routes);while((t=e.pop())!=null){this.route(t,this.routes[t])}},_routeToRegExp:function(t){t=t.replace(I,"\\$&").replace($,"(?:$1)?").replace(H,function(t,e){return e?t:"([^/]+)"}).replace(A,"(.*?)");return new RegExp("^"+t+"$")},_extractParameters:function(t,e){var r=t.exec(e).slice(1);return i.map(r,function(t){return t?decodeURIComponent(t):null})}});var N=e.History=function(){this.handlers=[];i.bindAll(this,"checkUrl");if(typeof window!=="undefined"){this.location=window.location;this.history=window.history}};var O=/^[#\/]|\s+$/g;var P=/^\/+|\/+$/g;var C=/msie [\w.]+/;var j=/\/$/;var R=/[?#].*$/;N.started=false;i.extend(N.prototype,u,{interval:50,getHash:function(t){var e=(t||this).location.href.match(/#(.*)$/);return e?e[1]:""},getFragment:function(t,e){if(t==null){if(this._hasPushState||!this._wantsHashChange||e){t=this.location.pathname;var i=this.root.replace(j,"");if(!t.indexOf(i))t=t.slice(i.length)}else{t=this.getHash()}}return t.replace(O,"")},start:function(t){if(N.started)throw new Error("Backbone.history has already been started");N.started=true;this.options=i.extend({root:"/"},this.options,t);this.root=this.options.root;this._wantsHashChange=this.options.hashChange!==false;this._wantsPushState=!!this.options.pushState;this._hasPushState=!!(this.options.pushState&&this.history&&this.history.pushState);var r=this.getFragment();var s=document.documentMode;var n=C.exec(navigator.userAgent.toLowerCase())&&(!s||s<=7);this.root=("/"+this.root+"/").replace(P,"/");if(n&&this._wantsHashChange){this.iframe=e.$('