From: Jack Allnutt Date: Sun, 22 Jan 2012 18:50:06 +0000 (+0000) Subject: Starting to separate model from view. X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=f7a9a13c5295a2afa71eae01ca8d0bfcfbe84f37;p=KiwiIRC.git Starting to separate model from view. Now has backbone.js as a client-side dependency. Only UI change so far is a duplication of channel tabs, but the foundations are there. --- diff --git a/client/index.html.jade b/client/index.html.jade old mode 100644 new mode 100755 index d1ce2c3..d4c3f25 --- a/client/index.html.jade +++ b/client/index.html.jade @@ -110,7 +110,10 @@ html(lang="en-gb") - if (debug) script(type="text/javascript", src="/js/underscore.min.js") script(type="text/javascript", src="/js/util.js") + script(type="text/javascript", src="/js/backbone-0.5.3-min.js"); script(type="text/javascript", src="/js/gateway.js") + script(type="text/javascript", src="/js/model.js") + script(type="text/javascript", src="/js/view.js") script(type="text/javascript", src="/js/front.js") script(type="text/javascript", src="/js/front.events.js") script(type="text/javascript", src="/js/front.ui.js") diff --git a/client/js/backbone-0.5.3-min.js b/client/js/backbone-0.5.3-min.js new file mode 100644 index 0000000..08623c0 --- /dev/null +++ b/client/js/backbone-0.5.3-min.js @@ -0,0 +1,34 @@ +// Backbone.js 0.5.3 +// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://documentcloud.github.com/backbone +(function(){var h=this,p=h.Backbone,e;e=typeof exports!=="undefined"?exports:h.Backbone={};e.VERSION="0.5.3";var f=h._;if(!f&&typeof require!=="undefined")f=require("underscore")._;var g=h.jQuery||h.Zepto;e.noConflict=function(){h.Backbone=p;return this};e.emulateHTTP=!1;e.emulateJSON=!1;e.Events={bind:function(a,b,c){var d=this._callbacks||(this._callbacks={});(d[a]||(d[a]=[])).push([b,c]);return this},unbind:function(a,b){var c;if(a){if(c=this._callbacks)if(b){c=c[a];if(!c)return this;for(var d= +0,e=c.length;d/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")},has:function(a){return this.attributes[a]!=null},set:function(a,b){b||(b={});if(!a)return this;if(a.attributes)a=a.attributes;var c=this.attributes,d=this._escapedAttributes;if(!b.silent&&this.validate&&!this._performValidation(a,b))return!1;if(this.idAttribute in a)this.id=a[this.idAttribute]; +var e=this._changing;this._changing=!0;for(var g in a){var h=a[g];if(!f.isEqual(c[g],h))c[g]=h,delete d[g],this._changed=!0,b.silent||this.trigger("change:"+g,this,h,b)}!e&&!b.silent&&this._changed&&this.change(b);this._changing=!1;return this},unset:function(a,b){if(!(a in this.attributes))return this;b||(b={});var c={};c[a]=void 0;if(!b.silent&&this.validate&&!this._performValidation(c,b))return!1;delete this.attributes[a];delete this._escapedAttributes[a];a==this.idAttribute&&delete this.id;this._changed= +!0;b.silent||(this.trigger("change:"+a,this,void 0,b),this.change(b));return this},clear:function(a){a||(a={});var b,c=this.attributes,d={};for(b in c)d[b]=void 0;if(!a.silent&&this.validate&&!this._performValidation(d,a))return!1;this.attributes={};this._escapedAttributes={};this._changed=!0;if(!a.silent){for(b in c)this.trigger("change:"+b,this,void 0,a);this.change(a)}return this},fetch:function(a){a||(a={});var b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&& +c(b,d)};a.error=i(a.error,b,a);return(this.sync||e.sync).call(this,"read",this,a)},save:function(a,b){b||(b={});if(a&&!this.set(a,b))return!1;var c=this,d=b.success;b.success=function(a,e,f){if(!c.set(c.parse(a,f),b))return!1;d&&d(c,a,f)};b.error=i(b.error,c,b);var f=this.isNew()?"create":"update";return(this.sync||e.sync).call(this,f,this,b)},destroy:function(a){a||(a={});if(this.isNew())return this.trigger("destroy",this,this.collection,a);var b=this,c=a.success;a.success=function(d){b.trigger("destroy", +b,b.collection,a);c&&c(b,d)};a.error=i(a.error,b,a);return(this.sync||e.sync).call(this,"delete",this,a)},url:function(){var a=k(this.collection)||this.urlRoot||l();if(this.isNew())return a;return a+(a.charAt(a.length-1)=="/"?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this)},isNew:function(){return this.id==null},change:function(a){this.trigger("change",this,a);this._previousAttributes=f.clone(this.attributes);this._changed=!1},hasChanged:function(a){if(a)return this._previousAttributes[a]!= +this.attributes[a];return this._changed},changedAttributes:function(a){a||(a=this.attributes);var b=this._previousAttributes,c=!1,d;for(d in a)f.isEqual(b[d],a[d])||(c=c||{},c[d]=a[d]);return c},previous:function(a){if(!a||!this._previousAttributes)return null;return this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},_performValidation:function(a,b){var c=this.validate(a);if(c)return b.error?b.error(this,c,b):this.trigger("error",this,c,b),!1;return!0}}); +e.Collection=function(a,b){b||(b={});if(b.comparator)this.comparator=b.comparator;f.bindAll(this,"_onModelEvent","_removeReference");this._reset();a&&this.reset(a,{silent:!0});this.initialize.apply(this,arguments)};f.extend(e.Collection.prototype,e.Events,{model:e.Model,initialize:function(){},toJSON:function(){return this.map(function(a){return a.toJSON()})},add:function(a,b){if(f.isArray(a))for(var c=0,d=a.length;c').hide().appendTo("body")[0].contentWindow,this.navigate(a); +this._hasPushState?g(window).bind("popstate",this.checkUrl):"onhashchange"in window&&!b?g(window).bind("hashchange",this.checkUrl):setInterval(this.checkUrl,this.interval);this.fragment=a;m=!0;a=window.location;b=a.pathname==this.options.root;if(this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),window.location.replace(this.options.root+"#"+this.fragment),!0;else if(this._wantsPushState&&this._hasPushState&&b&&a.hash)this.fragment=a.hash.replace(j,""),window.history.replaceState({}, +document.title,a.protocol+"//"+a.host+this.options.root+this.fragment);if(!this.options.silent)return this.loadUrl()},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&&this.iframe&&(a=this.getFragment(this.iframe.location.hash));if(a==this.fragment||a==decodeURIComponent(this.fragment))return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(window.location.hash)},loadUrl:function(a){var b=this.fragment=this.getFragment(a); +return f.any(this.handlers,function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){var c=(a||"").replace(j,"");if(!(this.fragment==c||this.fragment==decodeURIComponent(c))){if(this._hasPushState){var d=window.location;c.indexOf(this.options.root)!=0&&(c=this.options.root+c);this.fragment=c;window.history.pushState({},document.title,d.protocol+"//"+d.host+c)}else if(window.location.hash=this.fragment=c,this.iframe&&c!=this.getFragment(this.iframe.location.hash))this.iframe.document.open().close(), +this.iframe.location.hash=c;b&&this.loadUrl(a)}}});e.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.delegateEvents();this.initialize.apply(this,arguments)};var u=/^(\S+)\s*(.*)$/,n=["model","collection","el","id","attributes","className","tagName"];f.extend(e.View.prototype,e.Events,{tagName:"div",$:function(a){return g(a,this.el)},initialize:function(){},render:function(){return this},remove:function(){g(this.el).remove();return this},make:function(a, +b,c){a=document.createElement(a);b&&g(a).attr(b);c&&g(a).html(c);return a},delegateEvents:function(a){if(a||(a=this.events))for(var b in f.isFunction(a)&&(a=a.call(this)),g(this.el).unbind(".delegateEvents"+this.cid),a){var c=this[a[b]];if(!c)throw Error('Event "'+a[b]+'" does not exist');var d=b.match(u),e=d[1];d=d[2];c=f.bind(c,this);e+=".delegateEvents"+this.cid;d===""?g(this.el).bind(e,c):g(this.el).delegate(d,e,c)}},_configure:function(a){this.options&&(a=f.extend({},this.options,a));for(var b= +0,c=n.length;b ' + data.nick + ' [' + data.ident + '@' + data.hostname + '] has joined', 'action join', 'color:#009900;'); + var c = new kiwi.model.Channel({"name": data.channel.toLowerCase()}); + c.get("members").add(new kiwi.model.Member({"nick": data.nick, "modes": []})); + kiwi.bbchans.add(c); if (data.nick === kiwi.gateway.nick) { return; // Not needed as it's already in nicklist } diff --git a/client/js/front.js b/client/js/front.js old mode 100644 new mode 100755 index 8928632..99c5164 --- a/client/js/front.js +++ b/client/js/front.js @@ -168,6 +168,10 @@ kiwi.front = { kiwi.front.ui.doLayout(); kiwi.front.ui.barsHide(); + kiwi.bbchans = new kiwi.model.ChannelList(); + kiwi.bbtabs = new kiwi.view.Tabs({"el": $('#kiwi .windowlist ul')[0], "model": kiwi.bbchans}); + + server_tabview = new Tabview('server'); server_tabview.userlist.setWidth(0); // Disable the userlist server_tabview.setIcon('/img/app_menu.png'); @@ -748,7 +752,6 @@ var ChannelList = function () { }; }; - /** * @constructor * @param {String} name The name of the UserList @@ -1353,6 +1356,7 @@ var Tabview = function (v_name) { $('#kiwi .windows .scroller').append('
'); // Create the window tab + tmp_tab = $('
  • '); $('span', tmp_tab).text(v_name); $('#kiwi .windowlist ul').append(tmp_tab); diff --git a/client/js/model.js b/client/js/model.js new file mode 100755 index 0000000..15443a2 --- /dev/null +++ b/client/js/model.js @@ -0,0 +1,174 @@ +/*jslint white:true, regexp: true, nomen: true, devel: true, undef: true, browser: true, continue: true, sloppy: true, forin: true, newcap: true, plusplus: true, maxerr: 50, indent: 4 */ +/*global kiwi */ +kiwi.model = {}; + +kiwi.model.MemberList = Backbone.Collection.extend({ + model: kiwi.model.Member, + comparator: function (a, b) { + var i, a_modes, b_modes, a_idx, b_idx, a_nick, b_nick; + a_modes = a.get("modes"); + b_modes = b.get("modes"); + // Try to sort by modes first + if (a_modes.length > 0) { + // a has modes, but b doesn't so a should appear first + if (b_modes.length === 0) { + return -1; + } + a_idx = b_idx = -1; + // Compare the first (highest) mode + for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) { + if (kiwi.gateway.user_prefixes[i].mode === a_modes[0]) { + a_idx = i; + } + } + for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) { + if (kiwi.gateway.user_prefixes[i].mode === b_modes[0]) { + b_idx = i; + } + } + if (a_idx < b_idx) { + return -1; + } else if (a_idx > b_idx) { + return 1; + } + // If we get to here both a and b have the same highest mode so have to resort to lexicographical sorting + + } else if (b_modes.length > 0) { + // b has modes but a doesn't so b should appear first + return 1; + } + a_nick = a.get("nick").toLocaleUpperCase(); + b_nick = b.get("nick").toLocaleUpperCase(); + // Lexicographical sorting + if (a_nick < b_nick) { + return -1; + } else if (a_nick > b_nick) { + return 1; + } else { + // This should never happen; both users have the same nick. + console.log('Something\'s gone wrong somewhere - two users have the same nick!'); + return 0; + } + } +}); + +kiwi.model.Member = Backbone.Model.extend({ + sortModes: function (modes) { + return modes.sort(function (a, b) { + var a_idx, b_idx, i; + for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) { + if (kiwi.gateway.user_prefixes[i].mode === a) { + a_idx = i; + } + } + for (i = 0; i < kiwi.gateway.user_prefixes.length; i++) { + if (kiwi.gateway.user_prefixes[i].mode === b) { + b_idx = i; + } + } + if (a_idx < b_idx) { + return -1; + } else if (a_idx > b_idx) { + return 1; + } else { + return 0; + } + }); + }, + initialize: function (attributes) { + var nick, modes, prefix; + nick = this.stripPrefix(this.get("nick")); + + modes = this.get("modes"); + modes = modes || []; + modes.sort(this.sortModes); + + this.set({"nick": nick, "modes": modes, "prefix": this.getPrefix(modes)}, {silent: true}); + }, + addMode: function (mode) { + var modes, prefix; + modes = this.get("modes"); + modes.push(mode); + modes = this.sortModes(modes); + this.set({"prefix": this.getPrefix(modes), "modes": modes}); + }, + removeMode: function (mode) { + var modes, prefix; + modes = this.get("modes"); + modes = _.reject(modes, function(m) { + return m === mode; + }); + this.set({"prefix": this.getPrefix(modes), "modes": modes}); + }, + getPrefix: function (modes) { + var prefix = ''; + if (typeof modes[0] !== 'undefined') { + prefix = _.detect(kiwi.gateway.user_prefixes, function (prefix) { + return prefix.mode === modes[0]; + }); + prefix = (prefix) ? prefix.symbol : ''; + } + return prefix; + }, + stripPrefix: function (nick) { + var tmp = nick, i, j, k; + i = 0; + for (j = 0; j < nick.length; j++) { + for (k = 0; k < kiwi.gateway.user_prefixes.length; k++) { + if (nick.charAt(j) === kiwi.gateway.user_prefixes[k].symbol) { + i++; + break; + } + } + } + + return tmp.substr(i); + } +}); + +kiwi.model.ChannelList = Backbone.Collection.extend({ + model: kiwi.model.Channel, + comparator: function (chan) { + return chan.get("name"); + } +}); + +kiwi.model.Channel = Backbone.Model.extend({ + initialize: function (attributes) { + var name = this.get("name") || ""; + this.set({ + "members": new kiwi.model.MemberList(), + "name": name, + "backscroll": [] + }, {"silent": true}); + this.view = new kiwi.view.Channel({"model": this, "name": name}); + }, + addMsg: function (time, nick, msg, type, style) { + var tmp, bs; + + tmp = {"msg": msg, "time": time, "nick": nick, "chan": this.get("name")}; + tmp = kiwi.plugs.run('addmsg', tmp); + if (!tmp) { + return; + } + if (tmp.time === null) { + d = new Date(); + tmp.time = d.getHours().toString().lpad(2, "0") + ":" + d.getMinutes().toString().lpad(2, "0") + ":" + d.getSeconds().toString().lpad(2, "0"); + } + + // The CSS class (action, topic, notice, etc) + if (typeof tmp.type !== "string") { + tmp.type = ''; + } + + // Make sure we don't have NaN or something + if (typeof tmp.msg !== "string") { + tmp.msg = ''; + } + + bs = this.get("backscroll"); + bs.push(tmp) + this.set({"backscroll": bs}, {silent:true}); + this.trigger("msg", tmp); + } +}); \ No newline at end of file diff --git a/client/js/util.js b/client/js/util.js old mode 100644 new mode 100755 index 86f284c..fde8b60 --- a/client/js/util.js +++ b/client/js/util.js @@ -281,8 +281,8 @@ var plugins = [ } return false; - } - }, + } + }, */ { @@ -321,16 +321,16 @@ var plugins = [ } }, - { - name: "kiwitest", - oninit: function (event, opts) { - console.log('registering namespace'); - $(gateway).bind("kiwi.lol.browser", function (e, data) { - console.log('YAY kiwitest'); - console.log(data); - }); - } - } + { + name: "kiwitest", + oninit: function (event, opts) { + console.log('registering namespace'); + $(gateway).bind("kiwi.lol.browser", function (e, data) { + console.log('YAY kiwitest'); + console.log(data); + }); + } + } ]; diff --git a/client/js/view.js b/client/js/view.js new file mode 100755 index 0000000..27585a6 --- /dev/null +++ b/client/js/view.js @@ -0,0 +1,108 @@ +/*jslint white:true, regexp: true, nomen: true, devel: true, undef: true, browser: true, continue: true, sloppy: true, forin: true, newcap: true, plusplus: true, maxerr: 50, indent: 4 */ +/*global kiwi */ + +kiwi.view = {}; + +kiwi.view.MemberList = Backbone.View.extend({ + tagName: "ul", + events: { + "click .nick": "nickClick" + }, + initialize: function (options) { + $(this.el).attr("id", 'kiwi_userlist_' + options.name); + this.model.get("members").bind('change', this.render, this); + }, + render: function () { + var $this = $(this.el); + $this.empty(); + this.model.get("members").forEach(function (member) { + $this.append('
  • ' + user.prefix + user.nick + '
  • '); + }); + }, + nickClick: function (x) { + console.log(x); + } +}); + +kiwi.view.Channel = Backbone.View.extend({ + tagName: "div", + className: "messages", + events: { + "click .chan": "chanClick" + }, + initialize: function (options) { + this.htmlsafe_name = 'chan_' + randomString(15); + $(this.el).attr("id", 'kiwi_window_' + this.htmlsafe_name); + this.model.bind('msg', this.newMsg, this); + this.msg_count = 0; + this.model.set({"view": this}, {"silent": true}); + }, + render: function () { + var $this = $(this.el); + $this.empty(); + this.model.get("backscroll").forEach(this.newMsg); + }, + newMsg: function (msg) { + var re, line_msg, $this = $(this.el); + // Make the channels clickable + re = new RegExp('\\B(' + kiwi.gateway.channel_prefix + '[^ ,.\\007]+)', 'g'); + msg.msg = msg.msg.replace(re, function (match) { + return '' + match + ''; + }); + + msg.msg = kiwi.front.formatIRCMsg(msg.msg); + + // Build up and add the line + line_msg = $('
    ' + msg.time + '
    ' + msg.nick + '
    ' + msg.msg + '
    '); + $this.append(line_msg); + this.msg_count++; + if (this.msg_count > 250) { + $('.msg:first', this.div).remove(); + this.msg_count--; + } + }, + chanClick: function (x) { + console.log(x); + } +}); + +kiwi.view.Tabs = Backbone.View.extend({ + events: { + "click li": "tabClick" + }, + initialize: function () { + this.model.bind("add", this.addTab, this); + this.model.bind("remove", this.removeTab, this); + this.model.bind("reset", this.render, this); + }, + render: function () { + $this = $(this.el); + $this.empty(); + this.model.forEach(function (tab) { + var tabname = $(tab.get("view").el).attr("id"); + $this.append($('
  • ' + tab.get("name") + '
  • ')); + }); + }, + addTab: function (tab) { + var tabname = $(tab.get("view").el).attr("id"), + $this = $(this.el); + $this.append($('
  • ' + tab.get("name") + '
  • ')); + }, + removeTab: function (tab) { + $('#tab_' + $(tab.get("view").el).attr("id")).remove(); + }, + tabClick: function (x) { + console.log(x); + } +}); + + + + + + + + + + + diff --git a/server/app.js b/server/app.js old mode 100644 new mode 100755 index e49c0ee..cab69b5 --- a/server/app.js +++ b/server/app.js @@ -595,12 +595,15 @@ this.httpHandler = function (request, response) { min.underscore = fs.readFileSync(public_http_path + 'js/underscore.min.js'); min.util = fs.readFileSync(public_http_path + 'js/util.js'); + min.backbone = fs.readFileSync(public_http_path + 'js/backbone-0.5.3-min.js'); min.gateway = fs.readFileSync(public_http_path + 'js/gateway.js'); + min.model = fs.readFileSync(public_http_path + 'js/model.js'); + min.view = fs.readFileSync(public_http_path + 'js/view.js'); min.front = fs.readFileSync(public_http_path + 'js/front.js'); min.front_events = fs.readFileSync(public_http_path + 'js/front.events.js'); min.front_ui = fs.readFileSync(public_http_path + 'js/front.ui.js'); min.iscroll = fs.readFileSync(public_http_path + 'js/iscroll.js'); - min.ast = jsp.parse(min.underscore + min.util + min.gateway + min.front + min.front_events + min.front_ui + min.iscroll); + min.ast = jsp.parse(min.underscore + min.util + min.backbone + min.gateway + min.model + min.view + min.front + min.front_events + min.front_ui + min.iscroll); min.ast = pro.ast_mangle(min.ast); min.ast = pro.ast_squeeze(min.ast); min.final_code = pro.gen_code(min.ast);