Skip to content
Browse files

Boatloads of changes

  • Loading branch information...
1 parent 99a066d commit 161b8df2d0c30473a52e7940b39020b7f70f8764 @jounik committed
View
5 .gitignore
@@ -1,4 +1,3 @@
+.DS_Store
-Seep.esproj/user.jouni.espressostorage
-
-Seep.esproj/Project.espressostorage
+node_modules
View
21 LICENSE
@@ -0,0 +1,21 @@
+MIT LICENSE
+
+Copyright (C) 2009-2011 Jouni Koivuviita
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
View
47 README.md
@@ -0,0 +1,47 @@
+# Seep.js
+### A web application UI library/framework for Node.js
+
+Seep provides user interface controls for your Node.js web applications. The UI logic runs in the server, but you can expose parts of it by "seeping" some of the logic to the client (browser) as well.
+
+## Installation
+
+npm install seep
+
+## Hello World
+
+my-app.js
+ var seep = require("seep")
+
+ var app = seep.Application.extend({
+
+ start: function() {
+ this.add(new seep.Text("Hello World!"))
+ }
+
+ })
+
+ seep start my-app.js
+
+## License
+
+MIT LICENSE
+
+Copyright (C) 2009-2011 Jouni Koivuviita
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
View
21 client/default.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <title>Loading...</title>
+ <link rel="stylesheet" type="text/css" href="/themes/alta.css">
+ </head>
+ <body>
+
+ <!-- TODO package all these dependencies to seep.js -->
+ <script src="/ender.min.js" type="text/javascript"></script>
+ <script src="/LAB.js" type="text/javascript"></script>
+ <script src="/socket.io/socket.io.js" type=text/javascript></script>
+ <script src="/seep.js" type="text/javascript"></script>
+ <script type="text/javascript">
+ seep.standalone = "SERVER_ADDR"
+ seep.init("APP_URL")
+ </script>
+
+ </body>
+</html>
View
14 client/seep.js
@@ -42,7 +42,8 @@ var seep = (function(){
new seep.application(appPath, appId);
}, 10)
- conn = io.connect(document.location)
+ seep.serverAddr = seep.standalone!=undefined? document.location.origin + seep.standalone : document.location
+ conn = io.connect(seep.serverAddr)
conn.on(settings.MESSAGE_UPDATE, function(data) {
if(data.sid) {
console.log("New session id", data.sid)
@@ -123,6 +124,7 @@ seep.application = function(appPath, elementId) {
this.rootElement = elementId? document.getElementById(elementId) : document.body
this.rootElement.className += (this.rootElement.className.length>0? " " : "") + "seep-app"
+ !elementId? document.documentElement.style.minHeight = "100%" : null
this.path = appPath
var self = this
@@ -132,10 +134,10 @@ seep.application = function(appPath, elementId) {
this.start = function(sid) {
// Namespace the connection to this application
- self.conn = io.connect(document.location + appPath + "_" + sid)
+ self.conn = io.connect(seep.serverAddr + appPath + "_" + sid)
self.conn.on("connect", function() {
- console.log("Application connected ("+document.location + appPath + "_" + sid +")")
+ console.log("Application connected ("+seep.serverAddr + appPath + "_" + sid +")")
})
self.conn.on('update', function(data) {
@@ -358,6 +360,12 @@ seep.widget = function(json) {
}
return val
})
+
+ if(json.focusable) {
+ this.focus = function() {
+ self.element.focus()
+ }
+ }
}
seep.widget.prototype.sync = function() {
View
27 client/themes/alta.css
@@ -1,5 +1,5 @@
body {
- font-family: "Droid Sans", sans-serif;
+ font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
color: #2c3b42;
font-size: 14px;
line-height: 19px;
@@ -34,6 +34,13 @@ body {
position: relative;
}
+body.seep-app {
+ margin: 0;
+ padding: 20px;
+ min-height: 100%;
+ position: static;
+ }
+
.seep-app .disconnected {
position: absolute;
top: 0;
@@ -80,6 +87,15 @@ body {
box-shadow: 0 1px 0 rgba(255,255,255,.6), inset 0 2px 0 rgba(255,255,255,.7), inset 0 0 1px rgba(255,255,255,1);
}
+.s-button:focus,
+.s-input:focus {
+ outline: none;
+ -webkit-box-shadow: 0 0 3px #0060D6;
+ -moz-box-shadow: 0 0 3px #0060D6;
+ box-shadow: 0 0 3px #0060D6;
+ border-color: #4E95E0;
+ }
+
.s-button.down {
background: #CCCECE; /* old browsers */
background: -moz-linear-gradient(top, #CCCECE 6%, #C0C3C4 100%); /* firefox */
@@ -94,15 +110,6 @@ body {
box-shadow: 0 1px 0 rgba(255,255,255,.6), inset 0 0 2px rgba(0,0,0,.3);
}
-.s-button:focus,
-.s-input:focus {
- outline: none;
- -webkit-box-shadow: 0 0 3px #0060D6;
- -moz-box-shadow: 0 0 3px #0060D6;
- box-shadow: 0 0 3px #0060D6;
- border-color: #4E95E0;
- }
-
.s-button::-moz-focus-inner {
border: 0;
}
View
37 client/widgets/button.js
@@ -1,11 +1,10 @@
-seep.button = function(json) {
+var button = function(json, test) {
if(!json)
return
-
if(!json.elementType)
json.elementType = "button"
- seep.field.call(this, json)
+ seep.layout.flow.call(this, json)
$(this.element).bind("mousedown", function(event) {
if(event.which == 1) {
@@ -27,22 +26,40 @@ seep.button = function(json) {
$(this).removeClass("down")
})
+ var self = $(this)
var downHandler = function(event) {
- var self = $(this)
- self.addClass("down");
+ self.addClass("down")
setTimeout(function() {self.removeClass("down")}, 120)
}
$(this.element).bind("down", downHandler)
$(this.element).bind("keypress", function(event) {
- if(event.keyCode==13)
- downHandler.call(this, event)
+ if(event.keyCode==13) {
+ event.preventDefault()
+ event.stopPropagation()
+ }
+ })
+ $(this.element).bind("keydown", function(event) {
+ if(event.keyCode==13) {
+ $(this).addClass("down")
+ event.preventDefault()
+ event.stopPropagation()
+ }
+ })
+ $(this.element).bind("keyup", function(event) {
+ if(event.keyCode==13) {
+ $(this).removeClass("down")
+ $(this).trigger("click")
+ event.preventDefault()
+ event.stopPropagation()
+ }
})
}
-seep.button.inherit(seep.field)
+button.inherit(seep.layout.flow)
+seep.button = seep.field.make(button)
-seep.button.prototype.click = function() {
+button.prototype.click = function() {
$(this.element).trigger("down")
$(this.element).trigger("click")
-}
+}
View
10 client/widgets/checkbox.js
@@ -5,7 +5,7 @@ seep.checkbox = function(json) {
if(!json.elementType)
json.elementType = "label"
- seep.field.call(this, json)
+ seep.text.call(this, json)
this.checkbox = document.createElement("input")
this.checkbox.type = "checkbox"
@@ -37,10 +37,10 @@ seep.checkbox = function(json) {
})
}
-seep.checkbox.inherit(seep.field)
+seep.checkbox.inherit(seep.text)
seep.checkbox.prototype.update = function(json) {
- seep.field.prototype.update.call(this, json);
+ seep.text.prototype.update.call(this, json);
this.sync(false)
if(typeof json.checked != "undefined") {
this.checked = json.checked
@@ -50,4 +50,6 @@ seep.checkbox.prototype.update = function(json) {
seep.checkbox.prototype.focus = function(json) {
this.checkbox.focus()
-}
+}
+
+seep.checkbox = seep.field.make(seep.checkbox)
View
100 client/widgets/field.js
@@ -1,55 +1,53 @@
-seep.field = function(json) {
- if(!json)
- return
-
- seep.text.call(this, json)
-
- var self = this
- this.watch("disabled", function(prop, old, val) {
- if(self.type == "checkbox")
- self.checkbox.disabled = val
- self.element.disabled = val
- if(val) $(self.element).addClass("s-disabled")
- else $(self.element).removeClass("s-disabled")
- self.sync(prop, old, val)
- return val
- })
-
- this.watch("readOnly", function(prop, old, val) {
- if(self.type == "checkbox")
- self.checkbox.disabled = val
- self.element.readOnly = val
- self.sync(prop, old, val)
- return val
- })
-
- this.watch("tabIndex", function(prop, old, val) {
- if(self.type == "checkbox")
- self.checkbox.tabIndex = val
- self.element.tabIndex = val
- self.sync(prop, old, val)
- return val
- })
-
-}
-
-seep.field.inherit(seep.text)
+seep.field = {}
-seep.field.prototype.update = function(json) {
- seep.text.prototype.update.call(this, json)
- this.sync(false)
- if(typeof json.disabled != "undefined") {
- this.disabled = json.disabled
+seep.field.make = function(targetClass) {
+ var field = function(json) {
+ targetClass.call(this, json)
+
+ var self = this
+ this.watch("disabled", function(prop, old, val) {
+ if(self.type == "checkbox")
+ self.checkbox.disabled = val
+ self.element.disabled = val
+ if(val) $(self.element).addClass("s-disabled")
+ else $(self.element).removeClass("s-disabled")
+ self.sync(prop, old, val)
+ return val
+ })
+
+ this.watch("readOnly", function(prop, old, val) {
+ if(self.type == "checkbox")
+ self.checkbox.disabled = val
+ self.element.readOnly = val
+ self.sync(prop, old, val)
+ return val
+ })
+
+ this.watch("tabIndex", function(prop, old, val) {
+ if(self.type == "checkbox")
+ self.checkbox.tabIndex = val
+ self.element.tabIndex = val
+ self.sync(prop, old, val)
+ return val
+ })
}
- if(typeof json.readOnly != "undefined") {
- this.readOnly = json.readOnly
- }
- if(typeof json.tabIndex != "undefined") {
- this.tabIndex = json.tabIndex
+
+ field.inherit(targetClass)
+
+ field.prototype.update = function(json) {
+ targetClass.prototype.update.call(this, json)
+ this.sync(false)
+ if(typeof json.disabled != "undefined") {
+ this.disabled = json.disabled
+ }
+ if(typeof json.readOnly != "undefined") {
+ this.readOnly = json.readOnly
+ }
+ if(typeof json.tabIndex != "undefined") {
+ this.tabIndex = json.tabIndex
+ }
+ this.sync(true)
}
- this.sync(true)
-}
-
-seep.field.prototype.focus = function(json) {
- this.element.focus()
+
+ return field
}
View
29 client/widgets/input.js
@@ -1,11 +1,11 @@
-seep.input = function(json) {
+var input = function(json) {
if(!json)
return
if(!json.elementType)
json.elementType = json.multiline? "textarea" : "input"
- seep.field.call(this, json)
+ seep.text.call(this, json)
if(!json.multiline)
this.element.type = "text"
@@ -14,31 +14,36 @@ seep.input = function(json) {
$(this.element).change(function(e) {
if(self.__cancelEvent)
return true
- //self._preventDomUpdate = true
+ self._preventDomUpdate = true
self.text = this.value
- //self._preventDomUpdate = false
+ self._preventDomUpdate = false
e.stopPropagation()
e.preventDefault()
})
- $(this.element).keydown(function(e) {
+ function keyHandler(e) {
if(self.text != this.value) {
self._preventDomUpdate = true
self.text = this.value
self._preventDomUpdate = false
}
- })
+ }
+
+ $(this.element).keyup(keyHandler)
+ $(this.element).keydown(keyHandler)
}
-seep.input.inherit(seep.field)
+input.inherit(seep.text)
-seep.input.prototype.update = function(json) {
- seep.field.prototype.update.call(this, json)
+input.prototype.update = function(json) {
+ seep.text.prototype.update.call(this, json)
if(json.placeholder)
this.element.placeholder = json.placeholder
}
-seep.input.prototype.focus = function(json) {
- seep.field.prototype.focus.call(this, json)
+input.prototype.focus = function(json) {
+ seep.widget.prototype.focus.call(this, json)
this.element.select()
-}
+}
+
+seep.input = seep.field.make(input)
View
21 client/widgets/layout/flow.js
@@ -22,7 +22,7 @@ seep.layout.flow.prototype.update = function(json) {
for(var i=0; i < json.widgets.length; i++) {
var widgetJson = json.widgets[i]
- if(widgetJson.remove) {
+ if(widgetJson.remove) {
var w = this.application.getWidgetById(widgetJson.id)
removed[w.id] = w
var el = this.wrap? w.element.parentNode: w.element
@@ -35,17 +35,24 @@ seep.layout.flow.prototype.update = function(json) {
continue
}
- // No changes for this slot
- if(!widgetJson.w)
- continue
+ var widget = this.application.getWidgetById(widgetJson.w.id)
- // Widget moved to this slot
- if(removed[widgetJson.w.id]) {
+ if(widget && !removed[widget.id]) {
+ // 'k' is the element index in this layout
+ var k=0, e = this.wrap? widget.element.parentNode : widget.element
+ while (e = e.previousSibling) { ++k;}
+ if(k == widgetJson.i) {
+ // Widget is already in the same location
+ widget.update(widgetJson.w)
+ continue
+ }
+ } else if(removed[widgetJson.w.id]) {
+ // Widget was moved to this slot
var widget = removed[widgetJson.w.id]
removed[widgetJson.w.id] = null
delete removed[widgetJson.w.id]
} else {
- // New widget
+ // Totally new widget for this layout
var widget = this.application.getWidget(widgetJson.w);
if(!widget)
continue
View
63 index.js
@@ -21,6 +21,9 @@ exports.layout = require("./lib/widgets/layout") // All layout widgets
var connect = require("connect")
, socket_io = require("socket.io")
, path = require("path")
+ , fs = require("fs")
+ , log4js = require("log4js")
+ , logger = log4js.getLogger("Seep")
@@ -42,7 +45,10 @@ var sessions = require("./lib/sessions")
* return true on success, false on fail
*/
exports.registerApp = function(filename) {
- return sessions.appRegistry.registerApp(filename);
+ return sessions.appRegistry.registerApp(filename)
+}
+exports.availableApps = function() {
+ return sessions.appRegistry.availableApps()
}
@@ -54,24 +60,50 @@ exports.registerApp = function(filename) {
* returns null
*/
exports.start = function(port, folder) {
- connect.session.ignore.push('/', '/seep.js', '/socket.io/socket.io.js', '/widgets', '/ender.min.js', 'LAB.js');
- var server = connect.createServer(
+
+
+
+ // Don't let the browser to cache these files
+ // TODO different settings for production
+ connect.session.ignore.push('/', '/seep.js', '/socket.io/socket.io.js', '/widgets', '/ender.min.js', 'LAB.js');
+
+ // Create the server
+ var server = connect.createServer(
connect.cookieParser()
// FIXME specify session timeout and do proper cleanup when session ends
, connect.session({ secret: "TODO-generate-key" })
, connect.favicon()
, connect.static(__dirname + "/client")
- , connect.static(folder + "public")
+ , connect.static(folder + "/public")
+
+ // Custom handling for direct application URL's
+ , connect.router(function(app){
+ app.get('/*', function(req, res, next){
+ var path = req.url.substr(1)
+ if(sessions.appRegistry.availableApps().indexOf(path) > -1) {
+ fs.readFile(__dirname + "/client/default.html", "utf-8", function(err, file) {
+ if(err) {
+ logger.error(err)
+ next()
+ return
+ }
+ // TODO if not in root context, set the server_addr
+ res.end(file.replace("APP_URL", path).replace("SERVER_ADDR", "/"))
+ })
+ } else {
+ next()
+ }
+ })
+ })
)
- server.listen(port)
- console.info("Seep server running at port " + port)
- var io = socket_io.listen(server, {"log level": 1})
+ // Start listening requests
+ server.listen(port)
- /*io.configure(function () {
- io.set('transports', ['flashsocket', 'htmlfile', 'xhr-polling', 'jsonp-polling']);
- });*/
+ // Derive Socket.io log level from the log4js level
+ var logLevel = Math.ceil(4 - (logger.level.level/10000))
+ var io = socket_io.listen(server, {"log level": logLevel})
io.sockets.on("connection", function (socket) {
socket.on(settings.MESSAGE_INIT, function(data, callback) {
@@ -81,13 +113,16 @@ exports.start = function(port, folder) {
}
var app = sessions.getApp(data.path, data.sid)
if(app) {
- io.of("/" + data.path + "_" + data.sid).on("connection", function(socket) {
- app.setConnection(socket)
- })
+ // Only initiate the connection once
+ if(!app.connection) {
+ io.of("/" + data.path + "_" + data.sid).on("connection", function(socket) {
+ app.setConnection(socket)
+ })
+ }
callback(data.sid)
}
else
- console.error("No application found for client/path", data.sid, data.path)
+ logger.error("No application found for client/path", data.sid, data.path)
})
})
View
22 lib/app-registry.js
@@ -5,7 +5,9 @@
/***********************************************************************
* External dependencies
***********************************************************************/
-var path = require("path");
+var path = require("path")
+ , log4js = require("log4js")
+ , logger = log4js.getLogger("Seep")
/***********************************************************************
* Map holding references from path (URL) to Application prototype
@@ -28,7 +30,7 @@ exports.registerApp = function(filename) {
if(targetFolder.indexOf(seepTestFolder) === -1
&& targetFolder.indexOf(seepDemoFolder) ===-1
&& targetFolder.indexOf(seepFolder) === 0) {
- console.error("ERROR: Can't deploy files from inside Seep module"
+ logger.error("Can't deploy files from inside Seep module"
+ " folder (" + path.normalize(filename) + ")")
return false
}
@@ -50,15 +52,16 @@ exports.registerApp = function(filename) {
}
if(app == null) {
- console.warn("WARNING: Only Seep Application instances can be added."
+ logger.warn("Only Seep Application instances can be added."
+ " No proper applications candidates found (tried to"
+ " deploy file '" + filename + "')")
return false
}
var appPath = path.basename(filename, ".js")
pathToApp[appPath] = app
- console.info("INFO: Added new Seep application prototype to path '"
+ logger.trace("Added new Seep application prototype to path '"
+ appPath + "'")
+ return true
}
@@ -70,6 +73,13 @@ exports.getAppForPath = function(path) {
if(pathToApp[path]) {
return pathToApp[path]
}
- console.warn("WARNING: No application prototype found for path " + path);
- return null;
+ logger.warn("No application prototype found for path " + path)
+ return null
+}
+
+exports.availableApps = function() {
+ var ret = []
+ for(var uri in pathToApp)
+ ret.push(uri)
+ return ret
}
View
25 lib/application.js
@@ -1,6 +1,8 @@
var Class = require("./class").Class
, Settings = require("./settings")
, util = require("./util")
+ , log4js = require("log4js")
+ , logger = log4js.getLogger("Seep")
// appId & widgetId are global to this server, i.e. no app or widget has ever the same id number
var appId = 0;
@@ -21,9 +23,11 @@ exports.Application = Class.extend({
this.connection = null;
},
- start: function() {
+ start: function(name) {
// Just a stub the application is supposed to override
- console.error("No start method found for application. Did you forget to implement it in your application class?")
+ logger.warn("No start method found for application. Did you forget to implement it in your application class?")
+ this.name = name
+ this.sendName = true
},
getName: function() {
@@ -96,7 +100,7 @@ exports.Application = Class.extend({
var self = this
conn.on("update", function(data) {
- console.log(new Date(data.time), "- App("+self.id+") received an update", data)
+ logger.debug(new Date(data.time), "- App("+self.id+") received an update", data)
self.processUpdate(data)
})
@@ -113,6 +117,7 @@ exports.Application = Class.extend({
for(var i=0; i < this.widgets.length; i++) {
this.widgets[i].repaint(recurse);
}
+ this.sendName = true
}
this.pushChanges()
},
@@ -142,8 +147,10 @@ exports.Application = Class.extend({
serialize: function() {
var out = {}
out.id = this.id
- if(typeof this.name != 'undefined')
+ if(this.sendName) {
out.name = this.name
+ delete this.sendName
+ }
if(this.widgetsToSend) {
out.types = []
@@ -191,12 +198,12 @@ exports.Application = Class.extend({
var timer = delay || this.pushBufferTimeout
var self = this
this._pushTimer = setTimeout(function() {
- console.log("Pushing update (id, connection id) (", self.id, ",", self.connection.id, ")")
+ logger.debug("Pushing update (id, connection id) (", self.id, ",", self.connection.id, ")")
self.connection.emit("update", self.serialize())
clearTimeout(self._pushTimer)
}, timer)
} else {
- console.warn("Trying to push changes for application with no connection", this.id)
+ logger.warn("Trying to push changes for application with no connection", this.id)
}
},
@@ -211,7 +218,7 @@ exports.Application = Class.extend({
for(var prop in props) {
var pullProps = {"pixelWidth": true, "pixelHeight": true}
if(widget && (pullProps[prop] || widget.syncProps[prop])) {
- console.log("Synching property for widget", id, ":", prop, ":", props[prop], "for application id", this.id)
+ logger.debug("Synching property for widget", id, ":", prop, ":", props[prop], "for application id", this.id)
widget.sync(false)
if(prop=="styles") {
widget.styles = props[prop].split(" ")
@@ -220,7 +227,7 @@ exports.Application = Class.extend({
}
widget.sync(true)
} else {
- console.error("Widget tried to sync a property that is not allowed to sync ("+prop+")")
+ logger.warn("Widget tried to sync a property that is not allowed to sync ("+prop+")")
}
}
}
@@ -229,7 +236,7 @@ exports.Application = Class.extend({
if(messages.events) {
for(var i=0; i < messages.events.length; i++) {
var event = messages.events[i]
- console.log("Event received for widget", event.id, ":", event.type, "for application id", this.id)
+ logger.debug("Event received for widget", event.id, ":", event.type, "for application id", this.id)
var widget = this.getWidgetById(event.id)
// FIXME make sure the widget is active and attached and make some other save-guard checks
if(widget) {
View
4 lib/sessions.js
@@ -3,6 +3,8 @@
***********************************************************************/
// Handles available applications on this server
var appRegistry = require("./app-registry")
+ , log4js = require("log4js")
+ , logger = log4js.getLogger("Seep")
exports.appRegistry = appRegistry
@@ -23,7 +25,7 @@ exports.getApp = function(appPath, sid) {
// Check if we already have an instance running for this
// session and path. If not, create new from the prototype
if(typeof sessions[sid][appPath] == "undefined") {
- console.log("\nCreating a new app for path '" + appPath + "'")
+ logger.debug("Starting a new app for path '" + appPath + "'")
var newApp = new (appRegistry.getAppForPath(appPath))()
newApp.setPath(appPath)
newApp.start()
View
125 lib/start.js 100644 → 100755
@@ -1,55 +1,108 @@
+#!/usr/bin/env node
+
var fs = require("fs")
, path = require("path")
, seep = require("../index")
-
+ , program = require("commander")
+ , log4js = require("log4js")
+ , logger = log4js.getLogger("Seep")
+
var DEPLOY_INTERVAL = 10000
+ , DEFAULT_PORT = 8000
+ , DEFAULT_LOG_LEVEL = "INFO"
+
+
-var PORT = process.argv[3]? process.argv[3].trim() : 8000
+var watched = []
+ , exclude = []
+// Get the version information from the package.json
+var packageJSON = JSON.parse(fs.readFileSync(__dirname + '/../package.json', 'utf-8'))
+program
+ .version(packageJSON.version)
+ .option('-p, --port <number>', 'use a custom http port (default is 8000)')
+ .option('-l, --log <level>', 'set the logging level (TRACE, DEBUG, INFO, WARN, ERROR)')
-// Path given from command line. Monitor this folder for changes and deploy all .js files
-var deploy_dir = process.argv[2].trim()
-
-// Always include a backslash as the last character in the path
-if(deploy_dir.lastIndexOf("/") != deploy_dir.length-1) {
- deploy_dir += "/"
-}
+/* Maybe later (initialize a dummy Seep project)
+program
+ .command('init [directory]')
+ .description('Initialize a new project and listen for connections.')
+ .action(function(dir){checkForDependencies(function(){
+ project.chdir(dir)
+ project.init()
+ project.start()
+ server.listen(program.port || process.env.PORT || 8123)
+ })})
+*/
-deploy_dir = path.normalize(deploy_dir);
-console.log("Seep is watching the deploy directory '" + deploy_dir + "'")
+program
+ .command('start <file>')
+ .description('Deploy the application described by the specified file. If it is a directory, all suitable applications inside that directory are deployed.')
+ .action(function(file){
+
+ program.port = program.port || DEFAULT_PORT
+ logger.setLevel(program.log || DEFAULT_LOG_LEVEL)
+
+ // Is this a relative path?
+ if(file.indexOf("/") !== 0) {
+ file = process.cwd() + "/" + file
+ }
+ file = path.normalize(file)
+
+ // Does it exist?
+ if(!path.existsSync(file)) {
+ logger.error("The specified target '"+file+"' doesn't exist.")
+ return
+ }
+
+ // Is this a file or a folder? Only accept .js as the file ending
+ if(path.extname(file) == ".js") {
+ if(!seep.registerApp(file)) {
+ logger.error("The file doesn't export any Seep application prototype.")
+ }
+ var folder = path.dirname(file)
+ } else {
+ watch_dir(file)
+ var folder = file
+ }
+
+ // Start the server
+ seep.start(program.port, folder)
+ console.log("Seep server running at http://localhost:"+program.port)
+
+ // Give additional info of the available apps
+ var apps = "The following applications are available:\n"
+ seep.availableApps().forEach(function(uri) {
+ apps += "\thttp://localhost:"+program.port+"/"+uri+"\n"
+ })
+ logger.info(apps)
+ })
-var watched = [];
-var exclude = [];
+// Parse the commands
+program.parse(process.argv);
function watch_dir(dir) {
- fs.readdir(dir, function(err, files) {
- if(err) {
- console.log(err);
- return;
- }
- for(var i=0; i<files.length; i++) {
- if(files[i].substr(files[i].length-3, files[i].length) == ".js") {
- var file = path.basename(files[i]);
- if(watched.indexOf(file) == -1 && exclude.indexOf(file) == -1) {
- var added = seep.registerApp(deploy_dir + file);
- if(added) {
- watched.push(file);
- } else {
- exclude.push(file);
- }
- }
- }
- }
- });
+ var files = fs.readdirSync(dir)
+ for(var i=0; i < files.length; i++) {
+ if(path.extname(files[i]) == ".js") {
+ var file = path.basename(files[i])
+ if(watched.indexOf(file) == -1 && exclude.indexOf(file) == -1) {
+ var added = seep.registerApp(dir + "/" + file)
+ if(added) {
+ watched.push(file)
+ } else {
+ exclude.push(file)
+ }
+ }
+ }
+ }
}
-watch_dir(deploy_dir);
-
+/* TODO maybe later (allow runtime addition of apps and update already deployed apps)
fs.watchFile(deploy_dir, {persistent: true, interval: DEPLOY_INTERVAL}, function(curr, prev) {
if(curr.mtime.getTime() != prev.mtime.getTime())
watch_dir(deploy_dir);
});
-
-seep.start(PORT, deploy_dir);
+*/
View
34 lib/widgets/button.js
@@ -1,16 +1,24 @@
-var Field = require("./field").Field
+var Field = require("./field")
+ , Focusable = require("./focusable")
+ , Flow = require("./layout").Flow
+ , Text = require("./text").Text
-exports.Button = Field.extend({
+// Extend from the layout class so we can have more versatile buttons (icons etc.)
+exports.Button = Focusable.make(Field.make(
+ Flow.extend({
- init: function(text) {
- this._super(text)
- this.setType(__filename)
- this.removeStyle("s-field")
- this.addStyle("s-button")
- },
-
- click: function() {
- this.fireEvent({type:"click"})
- }
+ init: function(text) {
+ this._super()
+ this.setType(__filename)
+ this.removeStyle("s-flow")
+ this.addStyle("s-button")
+ this.caption = new Text(text)
+ this.add(this.caption)
+ },
+
+ click: function() {
+ this.fireEvent({type:"click"})
+ }
-});
+ })
+))
View
52 lib/widgets/checkbox.js
@@ -1,28 +1,32 @@
-var Field = require("./field").Field
+var Field = require("./field")
+ , Focusable = require("./focusable")
+ , Text = require("./text").Text
-exports.Checkbox = Field.extend({
+exports.Checkbox = Focusable.make(Field.make(
+ Text.extend({
- init: function(text, checked) {
- this._super(text)
- this.syncProp("checked")
- this.setType(__filename)
- this.removeStyle("s-field")
- this.addStyle("s-checkbox")
- this.checked = checked
+ init: function(text, checked) {
+ this._super(text)
+ this.syncProp("checked")
+ this.setType(__filename)
+ this.removeStyle("s-text")
+ this.addStyle("s-checkbox")
+ this.checked = checked
+
+ var self = this
+ this.watch("checked", function(prop, old, val) {
+ if(old != val && this.synching) {
+ self.outBuffer[prop] = val
+ self.repaint()
+ setTimeout(function() {self.fireEvent({type:"change"})}, 0)
+ }
+ return val
+ })
+ },
- var self = this
- this.watch("checked", function(prop, old, val) {
- if(old != val && this.synching) {
- self.outBuffer[prop] = val
- self.repaint()
- setTimeout(function() {self.fireEvent({type:"change"})}, 0)
- }
- return val
- })
- },
-
- fireEvent: function(event) {
- this._super(event)
- }
+ fireEvent: function(event) {
+ this._super(event)
+ }
-});
+ })
+))
View
38 lib/widgets/field.js
@@ -1,31 +1,13 @@
-var Text = require("./text").Text
+exports.make = function(targetClass) {
+ return targetClass.extend({
-exports.Field = Text.extend({
-
- init: function(text) {
- this.pushProp("disabled")
- this.pushProp("readOnly")
- this.pushProp("tabIndex")
- this._super(text)
- this.setType(__filename)
- this.removeStyle("s-text")
- this.addStyle("s-field")
- },
-
- focus: function() {
- if(this.application) {
- this.application.focus(this)
- } else {
- this._pendingFocus = true
+ init: function(props) {
+ this.dependsOn("field")
+ this._super(props)
+ this.pushProp("disabled")
+ this.pushProp("readOnly")
+ this.pushProp("tabIndex")
}
- },
- setApplication: function(app) {
- this._super(app)
- if(this._pendingFocus) {
- app.focus(this)
- this._pendingFocus = false
- }
- }
-
-});
+ })
+}
View
28 lib/widgets/focusable.js
@@ -0,0 +1,28 @@
+exports.make = function(targetClass) {
+
+ return targetClass.extend({
+
+ init: function(props) {
+ this._super(props)
+ this.outBuffer.focusable = true
+ },
+
+ focus: function() {
+ if(this.application) {
+ this.application.focus(this)
+ } else {
+ this._pendingFocus = true
+ }
+ },
+
+ setApplication: function(app) {
+ this._super(app)
+ if(this._pendingFocus) {
+ app.focus(this)
+ this._pendingFocus = false
+ }
+ }
+
+ })
+
+}
View
34 lib/widgets/input.js
@@ -1,19 +1,23 @@
-var Field = require("./field").Field
+var Field = require("./field")
+ , Focusable = require("./focusable")
+ , Text = require("./text").Text
-exports.Input = Field.extend({
+exports.Input = Focusable.make(Field.make(
+ Text.extend({
- init: function(text, props) {
- var props = props || {}
- if(props.multiline) {
- this.pushProp("multiline")
+ init: function(text, props) {
+ var props = props || {}
+ if(props.multiline) {
+ this.pushProp("multiline")
+ }
+ this.pushProp("placeholder")
+ this._super(text)
+ this.setType(__filename)
+ this.multiline = props.multiline || false
+ this.syncProp("text")
+ this.removeStyle("s-text")
+ this.addStyle("s-input")
}
- this.pushProp("placeholder")
- this._super(text)
- this.setType(__filename)
- this.multiline = props.multiline || false
- this.syncProp("text")
- this.removeStyle("s-field")
- this.addStyle("s-input")
- }
-});
+ })
+))
View
32 lib/widgets/layout/flow.js
@@ -35,7 +35,7 @@ exports.Flow = Widget.extend({
if(!this.outBuffer.widgets)
this.outBuffer.widgets = []
- var out = {i: index, w: widget}
+ var out = {i: index, w: widget, dirty: true}
if(props.animate)
out.anim = true
@@ -110,36 +110,50 @@ exports.Flow = Widget.extend({
for(var i=0; i < this.widgets.length; i++) {
var widget = this.widgets[i]
widget.repaint(recurse)
- this.outBuffer.widgets.push({i: i, w: widget})
+ this.outBuffer.widgets.push({i: i, w: widget, dirty: true})
}
} else {
- for(var i=0; i < this.widgets.length; i++) {
+ /*for(var i=0; i < this.widgets.length; i++) {
var widget = this.widgets[i]
if(widget.needsRepaint()) {
if(!this.outBuffer.widgets)
this.outBuffer.widgets = []
- this.outBuffer.widgets.push({i: i, w: widget})
+ this.outBuffer.widgets.push({i: i, w: widget, dirty: true} )
}
- }
+ }*/
}
this._super(recurse)
},
serialize: function() {
+ var processedWidgets = []
if(this.outBuffer.widgets) {
for(var i=0; i < this.outBuffer.widgets.length; i++) {
var widget = this.outBuffer.widgets[i].w
var index = this.outBuffer.widgets[i].i
+ var remove = this.outBuffer.widgets[i].remove
var anim = this.outBuffer.widgets[i].anim
- if(widget) {
+ var dirty = this.outBuffer.widgets[i].dirty
+ if(widget && !remove && dirty) {
this.outBuffer.widgets[i] = null
widget = widget.serialize()
this.outBuffer.widgets[i] = {i: index, w: widget}
- }
- if(anim)
- this.outBuffer.widgets[i].anim = true
+ if(anim)
+ this.outBuffer.widgets[i].anim = true
+ processedWidgets.push(widget.id)
+ } else if(widget)
+ processedWidgets.push(widget)
}
}
+ for(var i=0; i < this.widgets.length; i++) {
+ var widget = this.widgets[i]
+ if(widget.needsRepaint() && processedWidgets.indexOf(widget.id) === -1) {
+ widget = widget.serialize()
+ if(!this.outBuffer.widgets)
+ this.outBuffer.widgets = []
+ this.outBuffer.widgets.push({i: i, w: widget})
+ }
+ }
return this._super()
}
View
3 lib/widgets/widget.js
@@ -11,7 +11,6 @@ var Widget = Class.extend({
this.idCounter = 0
this.parent = null
this.basePath = path.dirname(__filename)
- this.dependencies = null
this.styles = null
this.dirty = true
this.synching = true
@@ -76,7 +75,7 @@ var Widget = Class.extend({
setType: function(filename) {
// Add dependency on the parent type
if(this.type && this.type != "widget")
- this.dependsOn(this.type)
+ this.dependsOn(this.type)
// Derive widget type from the filename passed over from inheriting class
this.type = path.basename(filename.substr(this.basePath.length+1).replace("/","."), ".js");
View
10 package.json
@@ -7,17 +7,19 @@
, "url" : "http://jounikoivuviita.com"
}
, "homepage" : "http://seepjs.org"
-, "version" : "0.0.1a"
+, "version" : "0.0.2a"
, "main" : "./index"
, "repository": {
"type": "git"
, "url": "https://github.com/jounikoivuviita/Seep.git"
}
, "dependencies" : {
- "connect" : "1.7.x"
- , "socket.io" : "0.7.x"
+ "connect" : ">1.7.x"
+ , "socket.io" : ">0.7.x"
+ , "log4js" : ">0.4.x"
+ , "commander" : "0.5.0"
}
, "bin" : {
- "seep" : "./bin/seep"
+ "seep" : "./lib/start.js"
}
}

0 comments on commit 161b8df

Please sign in to comment.
Something went wrong with that request. Please try again.