<commit_before>'use strict';

window.allFavorites = JSON.parse(localStorage.getItem('savedFavorites'));

function displayFavorites() {

	JSON.parse(localStorage.getItem('savedFavorites')).forEach(function (element, index, array) {

		var favoritesRow =
			$('
' +
				'' + element.title + '' +
				'' +
				'
' +
				element.date +
			'
');

		var deleteIcon = favoritesRow.find('.glyphicon-remove');
		deleteIcon.data({ id: element.id });					//attach the ID again so that the "deletedStory" variable further below can access

		deleteIcon.click(function() {
			var thisIcon = $(this);
			$('#dialog-confirm').dialog("open");
		});

		$('.favorites').append(favoritesRow);
	});
}

function clickSortByDate() {
	allFavorites.sort(function(a,b){
		var c = new Date(a.date);
		var d = new Date(b.date);
		return d-c;
	});

	localStorage.setItem('savedFavorites', JSON.stringify(allFavorites));
	$('.favorite-story').remove();
	displayFavorites();
}

function clickSortByTitle() {
	allFavorites.sort(function(a,b) {
		var titleA = a.title.toUpperCase(); // ignore upper and lowercase
		var titleB = b.title.toUpperCase(); // ignore upper and lowercase
		return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
	});

	localStorage.setItem('savedFavorites', JSON.stringify(allFavorites));
	$('.favorite-story').remove();
	displayFavorites();
}

function deleteStory() {
	var deletedStory = thisIcon.data().id;

	for (var i = 0; i < allFavorites.length; i++) {
		if (allFavorites[i].id === deletedStory) {
			var index = i;
			break;
		}
	}

	allFavorites.splice(index, 1);
	localStorage.setItem('savedFavorites', JSON.stringify(allFavorites));
	var htmlToRemove = thisIcon.closest('.favorite-story');
	htmlToRemove.remove();
}

$('.sort-date').click(clickSortByDate);
$('.sort-title').click(clickSortByTitle);

$( "#dialog-confirm" ).dialog({
	autoOpen: false,
	resizable: false,
	height: "auto",
	width: 400,
	modal: true,
	buttons: {
		Delete: function() {
     		$( this ).dialog( "close" );
			deleteStory();
    	},
		Cancel: function() {
			$( this ).dialog( "close" );
		}
	}
});

displayFavorites();
<commit_msg>Fixed issue where clicking Delete would not delete a story.<commit_after>'use strict';

window.allFavorites = JSON.parse(localStorage.getItem('savedFavorites'));
var thisIcon;												//have to declare this in global scope so that its assignment can be passed into deleteStory()

function displayFavorites() {

	JSON.parse(localStorage.getItem('savedFavorites')).forEach(function (element, index, array) {

		var favoritesRow =
			$('
' +
				'' + element.title + '' +
				'' +
				'
' +
				element.date +
			'
');

		var deleteIcon = favoritesRow.find('.glyphicon-remove');
		deleteIcon.data({ id: element.id });					//attach the ID again so that the "deletedStory" variable further below can access

		deleteIcon.click(function() {
			thisIcon = $(this);
			$('#dialog-confirm').dialog("open");
		});

		$('.favorites').append(favoritesRow);
	});
}

function clickSortByDate() {
	allFavorites.sort(function(a,b){
		var c = new Date(a.date);
		var d = new Date(b.date);
		return d-c;
	});

	localStorage.setItem('savedFavorites', JSON.stringify(allFavorites));
	$('.favorite-story').remove();
	displayFavorites();
}

function clickSortByTitle() {
	allFavorites.sort(function(a,b) {
		var titleA = a.title.toUpperCase(); // ignore upper and lowercase
		var titleB = b.title.toUpperCase(); // ignore upper and lowercase
		return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
	});

	localStorage.setItem('savedFavorites', JSON.stringify(allFavorites));
	$('.favorite-story').remove();
	displayFavorites();
}

function deleteStory() {
	var deletedStory = thisIcon.data().id;

	for (var i = 0; i < allFavorites.length; i++) {
		if (allFavorites[i].id === deletedStory) {
			var index = i;
			break;
		}
	}

	allFavorites.splice(index, 1);
	localStorage.setItem('savedFavorites', JSON.stringify(allFavorites));
	var htmlToRemove = thisIcon.closest('.favorite-story');
	htmlToRemove.remove();
}

$('.sort-date').click(clickSortByDate);
$('.sort-title').click(clickSortByTitle);

$( "#dialog-confirm" ).dialog({
	autoOpen: false,
	resizable: false,
	draggable: false,
	height: "auto",
	width: 400,
	modal: true,
	buttons: {
		Delete: function() {
     		$(this).dialog( "close" );
     		deleteStory();
    	},
		Cancel: function() {
			$(this).dialog( "close" );
		}
	}
});

displayFavorites();
<|endoftext|>


<commit_before>var fs      = require('fs');
var path    = require('path');
var http    = require('http');
var https   = require('https');
var mzfs    = require('mz/fs');
var mkdirp  = require('mkdirp');
var koa     = require('koa');
// 參考：http://codeforgeek.com/2014/09/handle-get-post-request-express-4/
var bodyParser = require("koa-bodyparser");
var session = require('koa-session');
var router  = require('koa-router')();
var co      = require('co');
var parse   = require('co-busboy');
var saveTo  = require('save-to');
var mongodb = require('mongodb');
var app = koa();

function comkdir(path) {
	var dir = path.split("/");
	dir.pop();
	console.log("comkdir=", dir);
  return function (callback) {
    mkdirp(path, callback);
  };
}

function loadSysFile(file) {
	var filepath = path.join(process.cwd(), file);
	if (!mzfs.existsSync(filepath)) {
		filepath = path.join(__dirname, file);
	}
	return mzfs.readFileSync(filepath, 'utf-8');
}

var setting = JSON.parse(loadSysFile('setting.json'));
var passwords = setting.passwords;

var db = {
	db : null,
	tableMap:{},
}

db.table=function(tableName) {
  var table = db.tableMap[tableName];
  if (typeof table === 'undefined') {
		table = db.tableMap[tableName] = db.db.collection(tableName);
	}
	return table;
}

mongodb.MongoClient.connect(setting.mongodb.dburl, function(err, db1){
	if (err)
		console.error("db connect fail");
	else
		console.log('Connect to', setting.mongodb.dburl, 'success!');
	db.db=db1;
});

var fdbserver = {
	app:app,
	router:router,
	loadSysFile:loadSysFile,
	run:function() { this.app.run(); }
}

var filedir = process.cwd()+"/file/";
if (!fs.existsSync(filedir)) {
	fs.mkdir(filedir);
}

app.keys = [setting.key];
app.use(session(app));
app.use(bodyParser({formLimit:5*1000*1000, jsonLimit:5*1000*1000}));

function response(res, code, msg) {
  res.status = code;
  res.set({'Content-Length':''+msg.length,'Content-Type':'text/plain'});
  res.body = msg;
	if (code !== 200) console.log('response error : ', code, msg);
}

fdbserver.response = response;

fdbserver.doAfterFilePost=function*(path, self) {
	yield Promise.resolve(false);
}

fdbserver.doBeforeFileGet=function*(path, self) {
	// yield false; 在 co 裡面，這會導致錯誤 : TypeError: You may only yield a function, promise, generator, array, or object
	yield Promise.resolve(false);
}

function isPass(req) {
  if (setting.loginToSave === false)
    return true;
  return typeof(req.session.user)!=='undefined';
}

var dbOp=function*(table, op, body, self) {
  var req = self.request, res = self.response;
  if (!isPass(self)) {
    response(res, 401, 'Please login to save!');
    return;
  }
	if (!db.db) response(res, 404, 'db error!');
	var results=null;
	if (op === 'find') {
    // 範例: http://localhost/db?table=filelog&filter={"path":"/db.filelog"}
		var filter=JSON.parse(body.filter);
		results = yield db.table(table).find(filter).toArray();
	} else if (op === 'insert') {
		var records = JSON.parse(body.records);
		results = yield db.table(table).insertMany(records);
	} else if (op === 'update') {
		var filter = JSON.parse(body.filter);
		var update = JSON.parse(body.update);
		results = yield db.table(table).updateMany(filter, update);
	} else if (op === 'delete') {
		var filter = JSON.parse(body.filter);
		results = yield db.table(table).deleteMany(filter);
	}
	if (results) {
		self.body = results;
	} else {
		response(res, 404, 'db error');
	}
	fdbserver.doAfterPostDb(table, op, body);
}

router
 .post('/upload/', function*(next) {
  var domain = this.request.url.split("/").pop();
  if (!this.request.header["content-type"].startsWith("multipart/form-data;")) return yield next;
  var part, parts = parse(this);
  var files = [], file;
  while (part = yield parts) {
    if (typeof part.filename !== 'undefined') {
      files.push(file = path.join(filedir, domain, part.filename));
      yield saveTo(part, file);
    }
  }
  this.body = files;
 })
 .post('/db/:table/:op', function*(next){
	yield dbOp(this.params.table, this.params.op, this.request.body, this);
 })
 .get('/db/:table/:op',  function*(next){
	yield dbOp(this.params.table, this.params.op, this.request.query, this);
 })
 .post("/login", function*(next) {
  var req = this.request, res = this.response;
  var p = this.request.body;
  if (req.protocol !== 'https') {
    response(res, 401, p.user+":login fail!");
    return;
  }
  if (p.user in passwords && passwords[p.user].toUpperCase() === p.password.toUpperCase()) {
    this.session.user = p.user;
    response(res, 200, p.user+":login success!");
  } else {
    response(res, 401, p.user+":login fail!");
  }
 })
 .post("/logout", function*(next) {
  var req = this.request, res = this.response;
  this.session = null;
  response(res, 200, "logout success!");
 })
 .post(/\/file\/.*/, function*(next) {
  var req = this.request, res = this.response;
  var text = this.request.body.text;
  if (!isPass(this)) {
    response(res, 401, 'Please login to save!');
    return;
  }
  console.log('post %s', this.path)
	yield comkdir(this.path);
  yield mzfs.writeFile(process.cwd()+this.path, text).then(function() {
    response(res, 200, 'write success!');
  }).catch(function() {
    response(res, 403, 'write fail!'); // 403: Forbidden
  });
	fdbserver.doAfterFilePost(this.path, this)
 })
 .get(/.*/, function *(next) {
	if (db.db) {
		yield db.table('filelog').insert({path:this.path, time:new Date()});
	}
	if (yield *fdbserver.doBeforeFileGet(this.path, this))
		return;
	if (this.path==="/")
		this.redirect(setting.redirect);
	console.log('get %s', this.path);
	var root = process.cwd();
	var tpath = path.join(root, this.path);
	var tstat = yield mzfs.stat(tpath);
	if (tstat.isDirectory()) {
		var files = yield mzfs.readdir(tpath);
		this.type = 'json';
		this.body = {type:"directory", "files":files};
	} else if (tstat.isFile()) {
		var ext = path.extname(tpath)
		this.type = ([".wd",".md"].indexOf(ext)>=0)?'.txt':ext;
		this.body = mzfs.createReadStream(tpath);
	}
 });

app.run=function() {
	app.use(router.routes());
	app.use(router.allowedMethods());

	var port = setting.port || 80; // process.env.PORT for Heroku
	console.log('Server started: http://localhost:'+port);
	http.createServer(app.callback()).listen(port);

  // https version : in self signed certification
  // You can save & modify in SSL mode, no edit allowed in HTTP mode.
	var sslPort = setting.portSsl || 443;
	https.createServer({
		key: loadSysFile('key.pem'),
		cert: loadSysFile('cert.pem'),
		// The folowing is for self signed certification.
		requestCert: true,
		ca: [ loadSysFile('csr.pem') ]
	}, app.callback()).listen(sslPort);
	console.log('Ssl Server started: https://localhost:'+sslPort);
}

module.exports = fdbserver;

if (!module.parent) {
  app.run();
  console.log('app running');
}
<commit_msg>protect setting.json<commit_after>var fs      = require('fs');
var path    = require('path');
var http    = require('http');
var https   = require('https');
var mzfs    = require('mz/fs');
var mkdirp  = require('mkdirp');
var koa     = require('koa');
// 參考：http://codeforgeek.com/2014/09/handle-get-post-request-express-4/
var bodyParser = require("koa-bodyparser");
var session = require('koa-session');
var router  = require('koa-router')();
var co      = require('co');
var parse   = require('co-busboy');
var saveTo  = require('save-to');
var mongodb = require('mongodb');
var app = koa();

function comkdir(path) {
	var dir = path.split("/");
	dir.pop();
	console.log("comkdir=", dir);
  return function (callback) {
    mkdirp(path, callback);
  };
}

function loadSysFile(file) {
	var filepath = path.join(process.cwd(), file);
	if (!mzfs.existsSync(filepath)) {
		filepath = path.join(__dirname, file);
	}
	return mzfs.readFileSync(filepath, 'utf-8');
}

var setting = JSON.parse(loadSysFile('setting.json'));
var passwords = setting.passwords;

var db = {
	db : null,
	tableMap:{},
}

db.table=function(tableName) {
  var table = db.tableMap[tableName];
  if (typeof table === 'undefined') {
		table = db.tableMap[tableName] = db.db.collection(tableName);
	}
	return table;
}

mongodb.MongoClient.connect(setting.mongodb.dburl, function(err, db1){
	if (err)
		console.error("db connect fail");
	else
		console.log('Connect to', setting.mongodb.dburl, 'success!');
	db.db=db1;
});

var fdbserver = {
	app:app,
	router:router,
	loadSysFile:loadSysFile,
	run:function() { this.app.run(); }
}

var filedir = process.cwd()+"/file/";
if (!fs.existsSync(filedir)) {
	fs.mkdir(filedir);
}

app.keys = [setting.key];
app.use(session(app));
app.use(bodyParser({formLimit:5*1000*1000, jsonLimit:5*1000*1000}));

function response(res, code, msg) {
  res.status = code;
  res.set({'Content-Length':''+msg.length,'Content-Type':'text/plain'});
  res.body = msg;
	if (code !== 200) console.log('response error : ', code, msg);
}

fdbserver.response = response;

fdbserver.doAfterFilePost=function*(path, self) {
	yield Promise.resolve(false);
}

fdbserver.doBeforeFileGet=function*(path, self) {
	// yield false; 在 co 裡面，這會導致錯誤 : TypeError: You may only yield a function, promise, generator, array, or object
	yield Promise.resolve(false);
}

function isPass(req) {
  if (setting.loginToSave === false)
    return true;
  return typeof(req.session.user)!=='undefined';
}

var dbOp=function*(table, op, body, self) {
  var req = self.request, res = self.response;
  if (!isPass(self)) {
    response(res, 401, 'Please login to save!');
    return;
  }
	if (!db.db) response(res, 404, 'db error!');
	var results=null;
	if (op === 'find') {
    // 範例: http://localhost/db?table=filelog&filter={"path":"/db.filelog"}
		var filter=JSON.parse(body.filter);
		results = yield db.table(table).find(filter).toArray();
	} else if (op === 'insert') {
		var records = JSON.parse(body.records);
		results = yield db.table(table).insertMany(records);
	} else if (op === 'update') {
		var filter = JSON.parse(body.filter);
		var update = JSON.parse(body.update);
		results = yield db.table(table).updateMany(filter, update);
	} else if (op === 'delete') {
		var filter = JSON.parse(body.filter);
		results = yield db.table(table).deleteMany(filter);
	}
	if (results) {
		self.body = results;
	} else {
		response(res, 404, 'db error');
	}
	fdbserver.doAfterPostDb(table, op, body);
}

router
 .post('/upload/', function*(next) {
  var domain = this.request.url.split("/").pop();
  if (!this.request.header["content-type"].startsWith("multipart/form-data;")) return yield next;
  var part, parts = parse(this);
  var files = [], file;
  while (part = yield parts) {
    if (typeof part.filename !== 'undefined') {
      files.push(file = path.join(filedir, domain, part.filename));
      yield saveTo(part, file);
    }
  }
  this.body = files;
 })
 .post('/db/:table/:op', function*(next){
	yield dbOp(this.params.table, this.params.op, this.request.body, this);
 })
 .get('/db/:table/:op',  function*(next){
	yield dbOp(this.params.table, this.params.op, this.request.query, this);
 })
 .post("/login", function*(next) {
  var req = this.request, res = this.response;
  var p = this.request.body;
  if (req.protocol !== 'https') {
    response(res, 401, p.user+":login fail!");
    return;
  }
  if (p.user in passwords && passwords[p.user].toUpperCase() === p.password.toUpperCase()) {
    this.session.user = p.user;
    response(res, 200, p.user+":login success!");
  } else {
    response(res, 401, p.user+":login fail!");
  }
 })
 .post("/logout", function*(next) {
  var req = this.request, res = this.response;
  this.session = null;
  response(res, 200, "logout success!");
 })
 .post(/\/file\/.*/, function*(next) {
  var req = this.request, res = this.response;
  var text = this.request.body.text;
  if (!isPass(this)) {
    response(res, 401, 'Please login to save!');
    return;
  }
  console.log('post %s', this.path)
	yield comkdir(this.path);
  yield mzfs.writeFile(process.cwd()+this.path, text).then(function() {
    response(res, 200, 'write success!');
  }).catch(function() {
    response(res, 403, 'write fail!'); // 403: Forbidden
  });
	fdbserver.doAfterFilePost(this.path, this)
 })
 .get(/.*/, function *(next) {
	if (this.path==="/setting.json") return;
	if (db.db) {
		yield db.table('filelog').insert({path:this.path, time:new Date()});
	}
	if (yield *fdbserver.doBeforeFileGet(this.path, this))
		return;
	if (this.path==="/")
		this.redirect(setting.redirect);
	console.log('get %s', this.path);
	var root = process.cwd();
	var tpath = path.join(root, this.path);
	var tstat = yield mzfs.stat(tpath);
	if (tstat.isDirectory()) {
		var files = yield mzfs.readdir(tpath);
		this.type = 'json';
		this.body = {type:"directory", "files":files};
	} else if (tstat.isFile()) {
		var ext = path.extname(tpath)
		this.type = ([".wd",".md"].indexOf(ext)>=0)?'.txt':ext;
		this.body = mzfs.createReadStream(tpath);
	}
 });

app.run=function() {
	app.use(router.routes());
	app.use(router.allowedMethods());

	var port = setting.port || 80; // process.env.PORT for Heroku
	console.log('Server started: http://localhost:'+port);
	http.createServer(app.callback()).listen(port);

  // https version : in self signed certification
  // You can save & modify in SSL mode, no edit allowed in HTTP mode.
	var sslPort = setting.portSsl || 443;
	https.createServer({
		key: loadSysFile('key.pem'),
		cert: loadSysFile('cert.pem'),
		// The folowing is for self signed certification.
		requestCert: true,
		ca: [ loadSysFile('csr.pem') ]
	}, app.callback()).listen(sslPort);
	console.log('Ssl Server started: https://localhost:'+sslPort);
}

module.exports = fdbserver;

if (!module.parent) {
  app.run();
  console.log('app running');
}
<|endoftext|>


<commit_before>// TODO: scrolling, file i/o
'use strict';
var c=document.getElementById('c').getContext('2d'),
    p=document.getElementById('p').getContext('2d'),
    MODE='normal', // Vim modes: normal, insert, visual[-block, -line]
    ESC_FD=0, // 'fd' escape sequence
    KEYQ=[{mods:[false,false,false,false],k:''}], // lightens duties for key event handler
    Buffer=()=>({// a string with a cursor
        p:0,a:'',changed:false,
        searchf(r){},
        searchb(r){},
        ins(ch){// insert ch chars to right of p
            this.changed=true;
            if(this.p==this.a.length){this.a=this.a+ch;}
            else{this.a=this.a.slice(0,this.p)+ch+this.a.slice(this.p);}
            this.mov(ch.length);
        },
        del(n){// delete n chars to right of p (or left if n<0)
            if(n==0||n+this.p<0){return;}
            var bz=n<0?n:0, fz=n<0?0:n;
            this.changed=true;
            this.a=this.a.slice(0,this.p+bz)+this.a.slice(this.p+fz);
            this.mov(bz);
        },
        mov(n=1){// move the cursor
            this.p=this.p+n;
            if(this.p<0){this.p=0;}
            else if(this.p>this.a.length){this.p=this.a.length;}
        },
    }),
    buf=Buffer();

// udpate : [RawKey] -> BufferAction
var update=(rks)=>{
    while(rks.length){ // consume KEYQ, dispatch event handlers
        var dec=decode(rks.shift()); // behead queue
        if(MODE=='normal'){
            switch(dec.code){
            case'i':MODE='insert';break;
            case'a':MODE='insert';buf.mov(1);break;
            case'h':buf.mov(-1);break;
            case'l':buf.mov(1);break;
            case' ':console.log('SPC-');break;// hmm...
            }
        }else if(MODE=='insert'){
            switch(dec.type){
            case'escape':MODE='normal';break;
            case'print':
                buf.ins(dec.code);
                if(dec.code=='f'){ESC_FD=-performance.now();}
                if(dec.code=='d'&&ESC_FD<0&&performance.now()+ESC_FD<500){
                    MODE='normal'; buf.del(-2);}break;
            case'edit':buf.del(dec.code=='B'?-1:1);break;
            }
        }
        if(dec.type=='arrow'){
            switch(dec.code){
            case'L':buf.mov(-1);break;
            case'R':buf.mov(1);break;
            }
        }
    }
};

var spot={x:0,y:0};

var render_cursor=(t)=>{
    p.clearRect(0,0,p.canvas.width,p.canvas.height);
    var clr=Math.abs(Math.cos(t/500));
    p.fillStyle='rgba(20,255,255,'+clr+')';
    var ht=p.measureText('W').width;// cursor height
    var wd=p.measureText(buf.a.slice(0,buf.p)).width;// string width upto cursor
    var xt=p.measureText(buf.a[buf.p]).width;
    p.fillRect(wd+spot.x,spot.y-ht,1,ht);
};

var render_text=(t)=>{
    c.clearRect(0,0,c.canvas.width,c.canvas.height);
    c.fillStyle='#cadaba';
    var w=c.measureText(buf.a).width;
    spot.x=(c.canvas.width-w)/2;
    spot.y=c.canvas.height/2;
    c.fillText(buf.a,spot.x,spot.y);
};

var gameloop=(now,resiz)=>{
    requestAnimationFrame((now)=>gameloop(now,true));
    render_cursor(now);
    update(KEYQ);
    if(buf.changed||resiz){
        render_text(now);
        buf.changed=false;
    }
};
requestAnimationFrame((now)=>gameloop(now,true));

var rsz=()=>{
    c.canvas.width=c.canvas.clientWidth;
    c.canvas.height=c.canvas.clientHeight;
    c.font='24px Sans-Serif';
    p.canvas.width=p.canvas.clientWidth;
    p.canvas.height=p.canvas.clientHeight;
    p.font='24px Sans-Serif';
};

window.onload=rsz;
window.onresize=rsz;
window.onkeydown=(k)=>{
    if(k.type=='keydown'){// push incoming events to a queue as they occur
        k.preventDefault();
        KEYQ.push({mods:[k.altKey,k.ctrlKey,k.metaKey,k.shiftKey], k:k.code});
    }
};

buf.ins('a test');

<commit_msg>render_text has newlines<commit_after>// TODO: scrolling, file i/o
'use strict';
var c=document.getElementById('c').getContext('2d'),
    p=document.getElementById('p').getContext('2d'),
    MODE='normal', // Vim modes: normal, insert, visual[-block, -line]
    ESC_FD=0, // 'fd' escape sequence
    KEYQ=[{mods:[false,false,false,false],k:''}], // lightens duties for key event handler
    Buffer=()=>({// a string with a cursor
        p:0,a:'',changed:false,
        nl:[0],//newline indexes, prepopulated with 1 at start
        load(txt){// load and initialize a buffer
            this.ins(txt);
            for(var i=0;i<txt.length;++i){
                if(txt[i]=='\n'){
                    this.nl.push(i);//get newlines
                }
            }
        },
        ins(ch){// insert ch chars to right of p
            this.changed=true;
            if(this.p==this.a.length){
                for(var i=0;i<ch.length;++i){if(ch[i]=='\n'){this.nl.push[this.p+i];}}
                this.a=this.a+ch;
            }
            else{
                for(var i=0;i<ch.length;++i){if(ch[i]=='\n'){this.nl.push[this.p+i];nl.sort();}}
                this.a=this.a.slice(0,this.p)+ch+this.a.slice(this.p);
            }
            this.mov(ch.length);
        },
        del(n){// delete n chars to right of p (or left if n<0)
            if(n==0||n+this.p<0){return;}
            var bz=n<0?n:0, fz=n<0?0:n;
            this.changed=true;
            this.a=this.a.slice(0,this.p+bz)+this.a.slice(this.p+fz);
            this.mov(bz);
        },
        mov(n=1){// move the cursor
            this.p=this.p+n;
            if(this.p<0){this.p=0;}
            else if(this.p>this.a.length){this.p=this.a.length;}
        },
    }),
    buf=Buffer();

// udpate : [RawKey] -> BufferAction
var update=(rks)=>{
    while(rks.length){ // consume KEYQ, dispatch event handlers
        var dec=decode(rks.shift()); // behead queue
        if(MODE=='normal'){
            switch(dec.code){
            case'i':MODE='insert';break;
            case'a':MODE='insert';buf.mov(1);break;
            case'h':buf.mov(-1);break;
            case'l':buf.mov(1);break;
            case' ':console.log('SPC-');break;// hmm...
            }
        }else if(MODE=='insert'){
            switch(dec.type){
            case'escape':MODE='normal';break;
            case'print':
                buf.ins(dec.code);
                if(dec.code=='f'){ESC_FD=-performance.now();}
                if(dec.code=='d'&&ESC_FD<0&&performance.now()+ESC_FD<500){
                    MODE='normal'; buf.del(-2);}break;
            case'edit':buf.del(dec.code=='B'?-1:1);break;
            }
        }
        if(dec.type=='arrow'){
            switch(dec.code){
            case'L':buf.mov(-1);break;
            case'R':buf.mov(1);break;
            }
        }
    }
};

var spot={x:20,y:0};

var render_cursor=(t)=>{
    p.clearRect(0,0,p.canvas.width,p.canvas.height);
    var clr=Math.abs(Math.cos(t/500));
    p.fillStyle='rgba(20,255,255,'+clr+')';
    var h=p.measureText('W').width;//cursor as tall as a W is wide
    var w=p.measureText(buf.a.slice(0,buf.p)).width;//string width upto cursor
    p.fillRect(w+spot.x,spot.y-h,1,h);
};

var render_text=(t)=>{
    c.clearRect(0,0,c.canvas.width,c.canvas.height);
    c.fillStyle='#cadaba';
    var h=c.measureText('W').width;
    spot.y=h+20;
    var lines=buf.a.split('\n');
    for(var i=0;i<lines.length;++i){
        c.fillText(lines[i],spot.x,spot.y);
        spot.y+=h;
    }
    //spot.x=(c.canvas.width-w)/2;//center horizontally
    //spot.y=c.canvas.height/2;//center vertically
    //c.fillText(buf.a,spot.x,spot.y);
};

var gameloop=(now,resiz)=>{
    requestAnimationFrame((now)=>gameloop(now,true));
    render_cursor(now);
    update(KEYQ);
    if(buf.changed||resiz){
        render_text(now);
        buf.changed=false;
    }
};
requestAnimationFrame((now)=>gameloop(now,true));

var rsz=()=>{
    c.canvas.width=c.canvas.clientWidth;
    c.canvas.height=c.canvas.clientHeight;
    c.font='24px Sans-Serif';
    p.canvas.width=p.canvas.clientWidth;
    p.canvas.height=p.canvas.clientHeight;
    p.font='24px Sans-Serif';
};

window.onload=rsz;
window.onresize=rsz;
window.onkeydown=(k)=>{
    if(k.type=='keydown'){// push incoming events to a queue as they occur
        k.preventDefault();
        KEYQ.push({mods:[k.altKey,k.ctrlKey,k.metaKey,k.shiftKey], k:k.code});
    }
};

buf.load('a test\nwith a newline');

<|endoftext|>


<commit_before>require('babel-register')
var skiff = require('./lib/skiff') // this must be required first
var moment = require('moment-timezone')
var rollbar = skiff.rollbar
var sails = skiff.sails
var digest2 = require('./lib/community/digest2')
var Promise = require('bluebird')

require('colors')

const sendAndLogDigests = type =>
  digest2.sendAllDigests(type).tap(results => sails.log.debug(results))

var jobs = {
  daily: function () {
    var now = moment.tz('America/Los_Angeles')
    var tasks = []

    sails.log.debug('Removing old kue jobs')
    tasks.push(Queue.removeOldCompletedJobs(20000))

    switch (now.day()) {
      case 3:
        sails.log.debug('Sending weekly digests')
        tasks.push(sendAndLogDigests('weekly'))
        break
    }
    return Promise.all(tasks)
  },

  hourly: function () {
    var now = moment.tz('America/Los_Angeles')
    var tasks = []

    switch (now.hour()) {
      case 12:
        sails.log.debug('Sending daily digests')
        tasks.push(sendAndLogDigests('daily'))
        break
      default:
        if (process.env.SERENDIPITY_ENABLED) {
          tasks.push(Relevance.cron(1, 'hour'))
        }
    }

    return Promise.all(tasks)
  },

  every10minutes: function () {
    sails.log.debug('Refreshing full-text search index')
    return FullTextSearch.refreshView()
  }
}

var runJob = Promise.method(function (name) {
  var job = jobs[name]
  if (typeof (job) !== 'function') {
    throw new Error(format('Unknown job name: "%s"', name))
  }
  sails.log.debug(format('Running %s job', name))
  return job()
})

skiff.lift({
  start: function (argv) {
    runJob(argv.interval)
    .then(function () {
      skiff.lower()
    })
    .catch(function (err) {
      sails.log.error(err.message.red)
      rollbar.handleError(err, () => skiff.lower())
    })
  }
})
<commit_msg>tweak digest cron logging<commit_after>require('babel-register')
var skiff = require('./lib/skiff') // this must be required first
var moment = require('moment-timezone')
var rollbar = skiff.rollbar
var sails = skiff.sails
var digest2 = require('./lib/community/digest2')
var Promise = require('bluebird')

require('colors')

const sendAndLogDigests = type =>
  digest2.sendAllDigests(type)
  .tap(results => sails.log.debug(`Sent digests to: ${results}`))

var jobs = {
  daily: function () {
    var now = moment.tz('America/Los_Angeles')
    var tasks = []

    sails.log.debug('Removing old kue jobs')
    tasks.push(Queue.removeOldCompletedJobs(20000))

    switch (now.day()) {
      case 3:
        sails.log.debug('Sending weekly digests')
        tasks.push(sendAndLogDigests('weekly'))
        break
    }
    return Promise.all(tasks)
  },

  hourly: function () {
    var now = moment.tz('America/Los_Angeles')
    var tasks = []

    switch (now.hour()) {
      case 12:
        sails.log.debug('Sending daily digests')
        tasks.push(sendAndLogDigests('daily'))
        break
      default:
        if (process.env.SERENDIPITY_ENABLED) {
          tasks.push(Relevance.cron(1, 'hour'))
        }
    }

    return Promise.all(tasks)
  },

  every10minutes: function () {
    sails.log.debug('Refreshing full-text search index')
    return FullTextSearch.refreshView()
  }
}

var runJob = Promise.method(function (name) {
  var job = jobs[name]
  if (typeof (job) !== 'function') {
    throw new Error(format('Unknown job name: "%s"', name))
  }
  sails.log.debug(format('Running %s job', name))
  return job()
})

skiff.lift({
  start: function (argv) {
    runJob(argv.interval)
    .then(function () {
      skiff.lower()
    })
    .catch(function (err) {
      sails.log.error(err.message.red)
      rollbar.handleError(err, () => skiff.lower())
    })
  }
})
<|endoftext|>


<commit_before>/*jslint node: true*/
"use strict";

var fs = require('fs');
var path = require('path');
var S = require('string');

var options = require('./options.js');
var logging = require('./logging.js');

module.exports.uriToFilename = function(uri) {
    var filename = path.join(options.fileBase, uri);
    // Make sure filename ends with '/'  if filename exists and is a directory.
    try {
        var fileStats = fs.statSync(filename);
        if (fileStats.isDirectory() && !S(filename).endsWith('/')) {
            filename += '/';
        } else if (fileStats.isFile() && S(filename).endsWith('/')) {
            filename = S(filename).chompRight('/').s;
        }
    } catch (err) {}
    return filename;
};

module.exports.uriToRelativeFilename = function(uri) {
    var filename = this.uriToFilename(uri);
    var relative = path.relative(options.fileBase, filename);
    return relative;
};

module.exports.filenameToBaseUri = function(filename) {
    var uriPath = S(filename).strip(options.fileBase).toString();
    return path.join(options.uriBase, uriPath);
};
<commit_msg>Fixed bug in URI generation<commit_after>/*jslint node: true*/
"use strict";

var fs = require('fs');
var path = require('path');
var S = require('string');

var options = require('./options.js');
var logging = require('./logging.js');

module.exports.uriToFilename = function(uri) {
    var filename = path.join(options.fileBase, uri);
    // Make sure filename ends with '/'  if filename exists and is a directory.
    try {
        var fileStats = fs.statSync(filename);
        if (fileStats.isDirectory() && !S(filename).endsWith('/')) {
            filename += '/';
        } else if (fileStats.isFile() && S(filename).endsWith('/')) {
            filename = S(filename).chompRight('/').s;
        }
    } catch (err) {}
    return filename;
};

module.exports.uriToRelativeFilename = function(uri) {
    var filename = this.uriToFilename(uri);
    var relative = path.relative(options.fileBase, filename);
    return relative;
};

module.exports.filenameToBaseUri = function(filename) {
    var uriPath = S(filename).strip(options.fileBase).toString();
    return options.uriBase + uriPath;
};
<|endoftext|>


<commit_before>'use strict';

import Sprite from './sprite';

export default class SpriteSheet {
	constructor(texture, width, height) {
		this.texture = texture;
		this.width = width;
		this.height = height;
		this.spriteCoordinates = {};
	}
	register(name, x, y, width, height) {
		this.spriteCoordinates[name] = {
			x: x,
			y: y,
			width: width,
			height: height
		};
	}
	registerMany(map) {
		for (var name in map) {
			var coords = map[name];
			this.register(name, coords[0], coords[1], coords[2], coords[3]);
		}
	}
	createSprite(name) {
		let info = this.spriteCoordinates[name];
		let sprite = new Sprite(info.width, info.height, this.texture);
		sprite.setTextureCoordinates(
			info.x / this.width,
			info.y / this.height,
			(info.x + info.width) / this.width,
			(info.x + info.height) / this.height
		);
		return sprite;
	}
}
<commit_msg>Fix an invalid coordinate in SpriteSheet<commit_after>'use strict';

import Sprite from './sprite';

export default class SpriteSheet {
	constructor(texture, width, height) {
		this.texture = texture;
		this.width = width;
		this.height = height;
		this.spriteCoordinates = {};
	}
	register(name, x, y, width, height) {
		this.spriteCoordinates[name] = {
			x: x,
			y: y,
			width: width,
			height: height
		};
	}
	registerMany(map) {
		for (var name in map) {
			var coords = map[name];
			this.register(name, coords[0], coords[1], coords[2], coords[3]);
		}
	}
	createSprite(name) {
		let info = this.spriteCoordinates[name];
		let sprite = new Sprite(info.width, info.height, this.texture);
		sprite.setTextureCoordinates(
			info.x / this.width,
			info.y / this.height,
			(info.x + info.width) / this.width,
			(info.y + info.height) / this.height
		);
		return sprite;
	}
}
<|endoftext|>


<commit_before>/** @namespace sputils.lib */

/**
* "taps" a function to produce a side effect
* but wrap it in an identity fn.
*
* @function sputils.lib.tap
**/
var tap = function (fn) {
  return function (value) {
    fn();
    return value;
  };
};

/**
* Get a value deeply from an object without crashing on nulls.
*
* This function is dynamic, so can be bound or just assigned to an
* object.
*
* Can be used to "index" arrays.
*
* @function sputils.lib.getval
* @param {string} subscript - the subscript string, i.e. 'a.b.c.prop'
* @param {Optional<object>} root - the root object. Defaults to this.
* @returns {string} the value of the prop, if exists else undefined
* @example
* var obj = {a:{b:{c:{}}}}
* var c = sputils.lib.getval('a.b.c', obj);
* c === obj.a.b.c;
* var none = sputils.lib.getval('a.b.1.notHere', obj);
* none === void 0;
*
* // dynamic binding
* testObjects.getval = sputils.lib.getval;
* expect(testObjects.getval('a.b')).to.equal(testObjects.a.b);
*
* // any kind of property name is allowed, excepting period.
* var getval = sputils.lib.getval;
* var res = getval('a.b.d.2.long prop name', testObjects);
* expect(res).to.equal(testObjects.a.b.d[2]['long prop name']);
**/
var getval = function recur(subscript, root) {
  if (this === sputils.lib) {
    return recur.call(global, subscript, root);
  }
  root = (root || this) || global;

  // if subscript already is an array, just use it.
  var parts = subscript.constructor === ([]).constructor ?
    subscript : subscript.split('.');
  var nextProp = parts[0];
  var next = root[nextProp];
  if (next !== void 0) {
    if (parts.length > 1) {
      return recur(parts.slice(1), next);
    }

    return next;
  }

  return void 0;
};

sputils.lib = {
  getval: getval,
  tap: tap
};
<commit_msg>pass parameter to the function supplied to `tap`<commit_after>/** @namespace sputils.lib */

/**
* "taps" a function to produce a side effect
* but wrap it in an identity fn.
*
* @function sputils.lib.tap
**/
var tap = function (fn) {
  return function (value) {
    fn(value);
    return value;
  };
};

/**
* Get a value deeply from an object without crashing on nulls.
*
* This function is dynamic, so can be bound or just assigned to an
* object.
*
* Can be used to "index" arrays.
*
* @function sputils.lib.getval
* @param {string} subscript - the subscript string, i.e. 'a.b.c.prop'
* @param {Optional<object>} root - the root object. Defaults to this.
* @returns {string} the value of the prop, if exists else undefined
* @example
* var obj = {a:{b:{c:{}}}}
* var c = sputils.lib.getval('a.b.c', obj);
* c === obj.a.b.c;
* var none = sputils.lib.getval('a.b.1.notHere', obj);
* none === void 0;
*
* // dynamic binding
* testObjects.getval = sputils.lib.getval;
* expect(testObjects.getval('a.b')).to.equal(testObjects.a.b);
*
* // any kind of property name is allowed, excepting period.
* var getval = sputils.lib.getval;
* var res = getval('a.b.d.2.long prop name', testObjects);
* expect(res).to.equal(testObjects.a.b.d[2]['long prop name']);
**/
var getval = function recur(subscript, root) {
  if (this === sputils.lib) {
    return recur.call(global, subscript, root);
  }
  root = (root || this) || global;

  // if subscript already is an array, just use it.
  var parts = subscript.constructor === ([]).constructor ?
    subscript : subscript.split('.');
  var nextProp = parts[0];
  var next = root[nextProp];
  if (next !== void 0) {
    if (parts.length > 1) {
      return recur(parts.slice(1), next);
    }

    return next;
  }

  return void 0;
};

sputils.lib = {
  getval: getval,
  tap: tap
};
<|endoftext|>


<commit_before>'use strict'
const config = require(__dirname + '/config.test.js');
const Twit = require('twit');
const HC = require('node-hipchat');

let twitter = new Twit({
  consumer_key: config['twitter_consumer_key'],
  consumer_secret: config['twitter_consumer_secret'],
  access_token: config['twitter_access_token'],
  access_token_secret: config['twitter_access_token_secret'],
  timeout_ms: 60*1000  // optional HTTP request timeout to apply to all requests.
});

let hipchat = new HC(config['hipchat_key']);
const params = {
  'room': config['hipchat_room'],
  'from': config['hipchat_bot'],
  'color': 'gray',
  'notify': 0
};

// Listen to TEA twitter accounts
let stream = twitter.stream('statuses/filter', { track: config['twitter_keywords'] });

console.log('Listening to Twitter, tracking ' + config['twitter_keywords'].join(', ') + '.');
stream.on('tweet', function (tweet) {
  // posting to HipChat
  let content = params;
  content.message = tweet.text;
  hipchat.postMessage(params, null);
});
<commit_msg>Fix: config file path<commit_after>'use strict'
const config = require(__dirname + '/config.js');
const Twit = require('twit');
const HC = require('node-hipchat');

let twitter = new Twit({
  consumer_key: config['twitter_consumer_key'],
  consumer_secret: config['twitter_consumer_secret'],
  access_token: config['twitter_access_token'],
  access_token_secret: config['twitter_access_token_secret'],
  timeout_ms: 60*1000  // optional HTTP request timeout to apply to all requests.
});

let hipchat = new HC(config['hipchat_key']);
const params = {
  'room': config['hipchat_room'],
  'from': config['hipchat_bot'],
  'color': 'gray',
  'notify': 0
};

// Listen to TEA twitter accounts
let stream = twitter.stream('statuses/filter', { track: config['twitter_keywords'] });

console.log('Listening to Twitter, tracking ' + config['twitter_keywords'].join(', ') + '.');
stream.on('tweet', function (tweet) {
  // posting to HipChat
  let content = params;
  content.message = tweet.text;
  hipchat.postMessage(params, null);
});
<|endoftext|>


<commit_before>;(function (thisplay, d3) {
  'use strict';

  function Stack(target) {
    var svg = d3.select(target).append("g")
      .attr("class", "thisplay-stack")
      .attr("transform", "translate(25, 25)");

    var width = 500;
    var height = 300;
    var data = [];
    var lineData;
    var zoom = d3.zoom()
      .scaleExtent([0.1, 10])
      .on("zoom", function () {
        svg.attr("transform", d3.event.transform);
      });

    d3.select(target).call(zoom);

    this.svg = svg;
    this.width = width;
    this.height = height;
    this.data = data;
    this.rectHeight = 30;
    this.rectWidth = 70;
    this.padding = this.rectHeight / 10;
    this.top = -1;
    lineData = [
      {"x" : (width-this.rectWidth-4*this.padding)/2, "y" : this.padding},
      {"x" : (width-this.rectWidth-4*this.padding)/2, "y" : height/2},
      {"x" : (width-this.rectWidth-4*this.padding)/2, "y" : height},
      {"x" : (width-this.rectWidth-4*this.padding)/2, "y" : height+this.padding},
      {"x" : (width+this.rectWidth+4*this.padding)/2, "y" : height+this.padding},
      {"x" : (width+this.rectWidth+4*this.padding)/2, "y" : height},
      {"x" : (width+this.rectWidth+4*this.padding)/2, "y" : height/2},
      {"x" : (width+this.rectWidth+4*this.padding)/2, "y" : this.padding}
    ];

    var lineFunc = d3.line().x(function(d){return d.x;}).y(function(d){return d.y;}).curve(d3.curveCatmullRom,1.0);

    svg.append("path")
    .attr("d",lineFunc(lineData))
    .attr("stroke","#C6B2BB")
    .attr("stroke-width","2")
    .attr("fill","none")
    .attr("id","stackLine");

    var lineLength = d3.select("#stackLine").node().getTotalLength();

     d3.select("#stackLine")
      .attr("stroke-dasharray", lineLength)
      .attr("stroke-dashoffset", lineLength)
      .transition()
      .duration(1000)
      .attr("stroke-dashoffset", 0);

    var that = this;

    this.mouseOver = function(d,i){
      d3.select("#elemIdx_"+i)
        //.attr("temp",function(){console.log(i);})
        .attr("fill","#9598AB")
        .attr("width",that.rectWidth*1.1)
        .attr("height",that.rectHeight*1.1)
        .attr("transform","translate("+(-that.rectWidth*0.05)+","+(-that.rectHeight*0.05)+")");

      that.svg.append("text")
      .text("←")
      .attr("font-family","Arial")
      .attr("font-size", (that.rectHeight / 2)+"px")
      .attr("fill",that.data[i].color)
      .attr("text-anchor","middle")
      .attr("id","arrow")
      .attr("x", that.width/2+that.rectWidth/2 + 10)
      .attr("y",that.height-(that.rectHeight+that.padding)*(i+1)+that.rectHeight/3*2);

      that.svg.append("text")
          .text(function(){return "stack["+i+"] = " + that.data[i].text;})
          .attr("x", that.width/2+ that.rectWidth/2 + 10 + that.rectHeight / 2)
          .attr("y",that.height-(that.rectHeight+that.padding)*(i+1)+that.rectHeight/3*2)
          .attr("font-family","Arial")
          .attr("font-size",(that.rectHeight / 2)+"px")
          .attr("fill",that.data[i].color)
          //.attr("text-anchor","middle")
          .attr("id","stackInfo");
    };

    this.mouseOut = function(d,i){

      d3.select(this)
         .attr("fill",that.data[i].background)
         .attr("width",that.rectWidth)
         .attr("height",that.rectHeight)
         .attr("transform","translate(0,0)");

      d3.select("#arrow").remove();
      d3.select("#stackInfo").remove();
    };

  }

  Stack.prototype.init = function(size){
    this.top = -1;
   // this.rectWidth = Math.ceil(this.width/10);
    this.rectHeight = Math.ceil(this.height/(size+size/10));

    this.padding = this.rectHeight/10;

  };

  Stack.prototype.redrawStack = function () {
    var that = this;
    this.svg.selectAll(".stack").remove();

    var rectWidth = this.rectWidth;
    var rectHeight = this.rectHeight;

    var ele = this.svg.append('g')
    .attr("class", "stack");

    var cells = ele.selectAll(".elem")
      .data(this.data)
      .enter()
      .append("g")
      //.attr("id", function (d, i) { return "elemIdx_" + i;})
      .attr("class", "elem");

    cells.append("rect")
      .attr("fill", function (d) { return d.background;})
      .attr("x", that.width/2-rectWidth/2)
      .attr("y", function(d,i){return that.height-(rectHeight+that.padding)*(i+1);})
      .attr("width", rectWidth)
      .attr("height", rectHeight)
      .attr("rx",2)
      .attr("ry",2)
      .attr("id", function (d, i) { return "elemIdx_" + i;})
      .on("mouseover",this.mouseOver)
      .on("mouseout",this.mouseOut);

      console.log(rectHeight/3);
      cells.append("text")
      .attr("x", that.width/2)
      .attr("y", function(d,i){return that.height-(rectHeight+that.padding)*(i+1)+rectHeight/3*2;})
      .attr("text-anchor", "middle")
      .attr("font-family","Arial")
      .attr("font-size",function(d){return rectHeight / 3 +"px";})
      .attr("fill", function (d) { return d.color; })
      .text(function (d) {  return d.text; });
  };

  Stack.prototype.push = function(val) {
    this.top++;
    this.data[this.top] = {
        text:val,
        color:"#4F474A",
        background:"#FFA4A7"
      };

    this.svg.select("#elemIdx_" + this.top + " text").text(val);
    var distance = (this.height-(this.rectHeight+this.padding)*(this.top+1))-this.rectHeight;
    var newElem = this.svg.append("g");
    var that = this;
    var fontSize = that.rectHeight / 3;

    newElem.append("rect")
            .attr("x",this.width/2-this.rectWidth/2)
            .attr("y",this.rectHeight)
            .attr("width",this.rectWidth)
            .attr("height",this.rectHeight)
            .attr("fill","white")
            .attr("rx",2)
            .attr("ry",2)
            .attr("id","newElemRect");

    newElem.append("text")
            .text(val)
            .attr("x",that.width/2)
            .attr("y",that.rectHeight/3*2 + that.rectHeight)
            .attr("text-anchor", "middle")
            .attr("font-family","Arial")
            .attr("font-size", (that.rectHeight / 3)+"px" )
            .attr("fill",this.data[this.top].color)
            .attr("id","newElemText");

    d3.select("#newElemRect")
            .transition()
            .attr("fill",this.data[this.top].background)
            .duration(300);

    d3.select("#newElemText")
            .transition()
            .attr("fill",this.data[this.top].color)
            .duration(300);

    newElem.transition()
            .attr("transform","translate(0,"+distance+")").duration(500).delay(300).ease(d3.easeSinOut);

    setTimeout(function(){
      newElem.remove().exit();
      that.redrawStack();
    },600);
  };

  Stack.prototype.pop = function(){
    var top = this.top;
    if (top < 0) return;
    var val = this.data[top];
    var newElem = this.svg.append("g");
    var that = this;
    var distance = that.rectHeight-(that.height-(that.rectHeight+that.padding)*top);
    this.data.pop();
    this.top--;
    console.log("top="+top);
    console.log("thistop="+this.top);
    this.redrawStack();
    newElem.append("rect")
            .attr("x",this.width/2-this.rectWidth/2)
            .attr("y",that.height-(that.rectHeight+that.padding)*(top+1))
            .attr("width",this.rectWidth)
            .attr("height",this.rectHeight)
            .attr("fill",val.background)
            .attr("rx",2)
            .attr("ry",2);

    newElem.append("text")
            .text(val.text)
            .attr("x",that.width/2)
            .attr("y",that.rectHeight/3*2 + that.height-(that.rectHeight+that.padding)*(top+1))
            .attr("text-anchor", "middle")
            .attr("font-family","Arial")
            .attr("font-size", (that.rectHeight / 3 )+"px")
            .attr("fill",val.color);

    newElem.transition()
           .attr("transform","translate(0,"+distance+")").duration(500).ease(d3.easeSinOut);

    setTimeout(function(){
      newElem.remove().exit();
    },300);
  };

  Stack.prototype.clear = function () {
    this.svg.selectAll(".stack").remove();
    this.top = -1;
    this.data = [];
  };

  Stack.prototype.setData = function (idx, val) {
    this.data[idx].text = val;
    this.svg.select(
    "#elemIdx_" + idx +
    " text").text(val);
  };

  Stack.prototype.highlight = function (row, col, color) {
    this.data[row][col].background = color;
    this.svg.select(
      "#rowIdx_" + row +
      " #colIdx_" + col +
      " rect")
      .transition()
      .attr("fill", color);
  };

  Stack.prototype.unhighlight = function (row, col){
    this.data[row][col].background = "#f2f2f2";
    this.svg.select(
      "#rowIdx_" + row +
      " #colIdx_" + col +
      " rect")
      .attr("fill", "#f2f2f2");
  };

  Stack.prototype.unhighlightAll = function (){
    this.svg.selectAll(".cell rect")
      .attr("fill", "#f2f2f2");
  };

  Stack.prototype.unhighlightColor = function (color) {
    this.svg.selectAll(".cell rect")
      .attr("fill", function (d, i) {
        if (d.background === color) {
          d.background = "#f2f2f2";
        }
        return d.background;
      });
  };

  thisplay.Stack = Stack;

})(thisplay, d3);
<commit_msg>push할 때 newElem의 text 색을 "white"로 조정하지 않아 나타난 animation error 수정<commit_after>;(function (thisplay, d3) {
  'use strict';

  function Stack(target) {
    var svg = d3.select(target).append("g")
      .attr("class", "thisplay-stack")
      .attr("transform", "translate(25, 25)");

    var width = 500;
    var height = 300;
    var data = [];
    var lineData;
    var zoom = d3.zoom()
      .scaleExtent([0.1, 10])
      .on("zoom", function () {
        svg.attr("transform", d3.event.transform);
      });

    d3.select(target).call(zoom);

    this.svg = svg;
    this.width = width;
    this.height = height;
    this.data = data;
    this.rectHeight = 30;
    this.rectWidth = 70;
    this.padding = this.rectHeight / 10;
    this.top = -1;
    lineData = [
      {"x" : (width-this.rectWidth-4*this.padding)/2, "y" : this.padding},
      {"x" : (width-this.rectWidth-4*this.padding)/2, "y" : height/2},
      {"x" : (width-this.rectWidth-4*this.padding)/2, "y" : height},
      {"x" : (width-this.rectWidth-4*this.padding)/2, "y" : height+this.padding},
      {"x" : (width+this.rectWidth+4*this.padding)/2, "y" : height+this.padding},
      {"x" : (width+this.rectWidth+4*this.padding)/2, "y" : height},
      {"x" : (width+this.rectWidth+4*this.padding)/2, "y" : height/2},
      {"x" : (width+this.rectWidth+4*this.padding)/2, "y" : this.padding}
    ];

    var lineFunc = d3.line().x(function(d){return d.x;}).y(function(d){return d.y;}).curve(d3.curveCatmullRom,1.0);

    svg.append("path")
    .attr("d",lineFunc(lineData))
    .attr("stroke","#C6B2BB")
    .attr("stroke-width","2")
    .attr("fill","none")
    .attr("id","stackLine");

    var lineLength = d3.select("#stackLine").node().getTotalLength();

     d3.select("#stackLine")
      .attr("stroke-dasharray", lineLength)
      .attr("stroke-dashoffset", lineLength)
      .transition()
      .duration(1000)
      .attr("stroke-dashoffset", 0);

    var that = this;

    this.mouseOver = function(d,i){
      d3.select("#elemIdx_"+i)
        //.attr("temp",function(){console.log(i);})
        .attr("fill","#9598AB")
        .attr("width",that.rectWidth*1.1)
        .attr("height",that.rectHeight*1.1)
        .attr("transform","translate("+(-that.rectWidth*0.05)+","+(-that.rectHeight*0.05)+")");

      that.svg.append("text")
      .text("←")
      .attr("font-family","Arial")
      .attr("font-size", (that.rectHeight / 2)+"px")
      .attr("fill",that.data[i].color)
      .attr("text-anchor","middle")
      .attr("id","arrow")
      .attr("x", that.width/2+that.rectWidth/2 + 10)
      .attr("y",that.height-(that.rectHeight+that.padding)*(i+1)+that.rectHeight/3*2);

      that.svg.append("text")
          .text(function(){return "stack["+i+"] = " + that.data[i].text;})
          .attr("x", that.width/2+ that.rectWidth/2 + 10 + that.rectHeight / 2)
          .attr("y",that.height-(that.rectHeight+that.padding)*(i+1)+that.rectHeight/3*2)
          .attr("font-family","Arial")
          .attr("font-size",(that.rectHeight / 2)+"px")
          .attr("fill",that.data[i].color)
          //.attr("text-anchor","middle")
          .attr("id","stackInfo");
    };

    this.mouseOut = function(d,i){

      d3.select(this)
         .attr("fill",that.data[i].background)
         .attr("width",that.rectWidth)
         .attr("height",that.rectHeight)
         .attr("transform","translate(0,0)");

      d3.select("#arrow").remove();
      d3.select("#stackInfo").remove();
    };

  }

  Stack.prototype.init = function(size){
    this.clear();
    if (!size) return;
    this.top = -1;
   // this.rectWidth = Math.ceil(this.width/10);
    this.rectHeight = Math.ceil(this.height/(size+size/10));

    this.padding = this.rectHeight/10;

  };

  Stack.prototype.redrawStack = function () {
    var that = this;
    this.svg.selectAll(".stack").remove();

    var rectWidth = this.rectWidth;
    var rectHeight = this.rectHeight;

    var ele = this.svg.append('g')
    .attr("class", "stack");

    var cells = ele.selectAll(".elem")
      .data(this.data)
      .enter()
      .append("g")
      //.attr("id", function (d, i) { return "elemIdx_" + i;})
      .attr("class", "elem");

    cells.append("rect")
      .attr("fill", function (d) { return d.background;})
      .attr("x", that.width/2-rectWidth/2)
      .attr("y", function(d,i){return that.height-(rectHeight+that.padding)*(i+1);})
      .attr("width", rectWidth)
      .attr("height", rectHeight)
      .attr("rx",2)
      .attr("ry",2)
      .attr("id", function (d, i) { return "elemIdx_" + i;})
      .on("mouseover",this.mouseOver)
      .on("mouseout",this.mouseOut);

      console.log(rectHeight/3);
      cells.append("text")
      .attr("x", that.width/2)
      .attr("y", function(d,i){return that.height-(rectHeight+that.padding)*(i+1)+rectHeight/3*2;})
      .attr("text-anchor", "middle")
      .attr("font-family","Arial")
      .attr("font-size",function(d){return rectHeight / 3 +"px";})
      .attr("fill", function (d) { return d.color; })
      .text(function (d) {  return d.text; });
  };

  Stack.prototype.push = function(val) {
    this.top++;
    this.data[this.top] = {
        text:val,
        color:"#4F474A",
        background:"#FFA4A7"
      };

    this.svg.select("#elemIdx_" + this.top + " text").text(val);
    var distance = (this.height-(this.rectHeight+this.padding)*(this.top+1))-this.rectHeight;
    var newElem = this.svg.append("g");
    var that = this;
    var fontSize = that.rectHeight / 3;

    newElem.append("rect")
            .attr("x",this.width/2-this.rectWidth/2)
            .attr("y",this.rectHeight)
            .attr("width",this.rectWidth)
            .attr("height",this.rectHeight)
            .attr("fill","white")
            .attr("rx",2)
            .attr("ry",2)
            .attr("id","newElemRect");

    newElem.append("text")
            .text(val)
            .attr("x",that.width/2)
            .attr("y",that.rectHeight/3*2 + that.rectHeight)
            .attr("text-anchor", "middle")
            .attr("font-family","Arial")
            .attr("font-size", (that.rectHeight / 3)+"px" )
            .attr("fill","white")
            .attr("id","newElemText");

    d3.select("#newElemRect")
            .transition()
            .attr("fill",this.data[this.top].background)
            .duration(300);

    d3.select("#newElemText")
            .transition()
            .attr("fill",this.data[this.top].color)
            .duration(300);

    newElem.transition()
            .attr("transform","translate(0,"+distance+")").duration(500).delay(300).ease(d3.easeSinOut);

    setTimeout(function(){
      newElem.remove().exit();
      that.redrawStack();
    },600);
  };

  Stack.prototype.pop = function(){
    var top = this.top;
    if (top < 0) return;
    var val = this.data[top];
    var newElem = this.svg.append("g");
    var that = this;
    var distance = that.rectHeight-(that.height-(that.rectHeight+that.padding)*top);
    this.data.pop();
    this.top--;
    console.log("top="+top);
    console.log("thistop="+this.top);
    this.redrawStack();
    newElem.append("rect")
            .attr("x",this.width/2-this.rectWidth/2)
            .attr("y",that.height-(that.rectHeight+that.padding)*(top+1))
            .attr("width",this.rectWidth)
            .attr("height",this.rectHeight)
            .attr("fill",val.background)
            .attr("rx",2)
            .attr("ry",2);

    newElem.append("text")
            .text(val.text)
            .attr("x",that.width/2)
            .attr("y",that.rectHeight/3*2 + that.height-(that.rectHeight+that.padding)*(top+1))
            .attr("text-anchor", "middle")
            .attr("font-family","Arial")
            .attr("font-size", (that.rectHeight / 3 )+"px")
            .attr("fill",val.color);

    newElem.transition()
           .attr("transform","translate(0,"+distance+")").duration(500).ease(d3.easeSinOut);

    setTimeout(function(){
      newElem.remove().exit();
    },300);
  };

  Stack.prototype.clear = function () {
    this.svg.selectAll(".stack").remove();
    this.top = -1;
    this.data = [];
  };

  Stack.prototype.setData = function (idx, val) {
    this.data[idx].text = val;
    this.svg.select(
    "#elemIdx_" + idx +
    " text").text(val);
  };

  Stack.prototype.highlight = function (row, col, color) {
    this.data[row][col].background = color;
    this.svg.select(
      "#rowIdx_" + row +
      " #colIdx_" + col +
      " rect")
      .transition()
      .attr("fill", color);
  };

  Stack.prototype.unhighlight = function (row, col){
    this.data[row][col].background = "#f2f2f2";
    this.svg.select(
      "#rowIdx_" + row +
      " #colIdx_" + col +
      " rect")
      .attr("fill", "#f2f2f2");
  };

  Stack.prototype.unhighlightAll = function (){
    this.svg.selectAll(".cell rect")
      .attr("fill", "#f2f2f2");
  };

  Stack.prototype.unhighlightColor = function (color) {
    this.svg.selectAll(".cell rect")
      .attr("fill", function (d, i) {
        if (d.background === color) {
          d.background = "#f2f2f2";
        }
        return d.background;
      });
  };

  thisplay.Stack = Stack;

})(thisplay, d3);
<|endoftext|>


<commit_before>Handler = {
    /**
     * Display a user's message
     */
    event_message: function handle_event_message(e){
        var rooms = JSON.parse($('#rooms').val());
        var data = JSON.parse(e.data);

        /**
         * don't display the message if the user is not on the room
         * where the message comes from
         */
        if(-1 === rooms.indexOf(data['room'])){
            return;
        }

        var lineDiv = document.createElement('div');

        var timeSpan = document.createElement('span');
        var nickSpan = document.createElement('span');
        var msgSpan = document.createElement('span');

        timeSpan.innerHTML = get_current_time() + ' ';
        timeSpan.className = 'time';

        nickSpan.innerHTML = data['nick'] + ': ';
        nickSpan.className = 'nick';

        msgSpan.innerHTML = data['message'] + ' ';
        msgSpan.className = 'msg';

        if(is_mention(data['message'])){
            lineDiv.className = 'line mention';
        }
        else{
            lineDiv.className = 'line';
        }

        lineDiv.appendChild(timeSpan);
        lineDiv.appendChild(nickSpan);
        lineDiv.appendChild(msgSpan);

        $('#' + data['room']).append(lineDiv);

        notify_activity(data['room']);

        //TODO: scroll the div to the bottom of the page when the content is larger
        //than the div
    },

    /**
     * Update the user list when someone joins the chat
     */
    event_users: function handle_event_users(e){
        var users = $.parseJSON(e.data);
        var usersDiv = $('#user-list')[0];

        usersDiv.innerHTML = '';

        for(i=0; i<users.length-1; i++){
            usersDiv.innerHTML += users[i] + ', ';
        }

        usersDiv.innerHTML += users[users.length-1];
    },

    /**
     * Callback to respond to ping events via AJAX
     */
    event_ping: function handle_event_ping(e){
        $.post($SCRIPT_ROOT + '/_pong', 'PONG!');
    },

    /**
     * Callback that handles message sending errors
     */
    publish_error: function handle_publish_error(e){
        var lineDiv = document.createElement('div');

        lineDiv.className = 'line error';
        lineDiv.innerHTML = e.responseText;

        $('.tab-pane.active').append(lineDiv);
    },

    /**
     * Callback for handling SSE errors
     */
    event_error: function handle_event_error(e){
        this.close(); //here, `this` refers to `stream`
        console.log('err:', e);
        //TODO: display some error to the user here (some kind of pop up) with
        //e.data as the err msg, also show a reconnect btn with a timer for
        //auto-reconnect
    },

    /**
     * Callback that handles the join errors
     */
    join_error: function handle_join_error(e){
        var lineDiv = document.createElement('div');

        lineDiv.className = 'join error';
        lineDiv.innerHTML = e.responseText;

        $('.tab-pane.active').append(lineDiv);
    },

    /**
     * Callback for handling the leave error (these occur when leaving rooms)
     */
    leave_room_error: function handle_leave_room_error(e){
        //if the status is 404 then the user closed his last room, so we logged
        //him out
        if(404 === e.status){
            window.location.replace($SCRIPT_ROOT);
        }

        var lineDiv = document.createElement('div');

        lineDiv.className = 'leave error';
        lineDiv.innerHTML = e.responseText;

        $('.tab-pane.active').append(lineDiv);
    },
}

/**
 * Create an AJAX request that will publish the message the user wants to
 * transmit
 */
function publish_message(e){
    e.preventDefault();
    $.post($SCRIPT_ROOT + '/_publish_message', {'message': $('#text').val(),
        'room': $('.tab-pane.active').attr('id')}).fail(Handler.publish_error);
    $('#text').val('');
}

/**
 * Callback for successfully joining or leaving rooms after login
 */
function update_rooms(e){
    $('#rooms').val(e);
    display_rooms();
}

/**
 * Leave a room
 *
 * An AJAX request is made to the server which will respond with the new room
 * list, also the tabs will be updated
 *
 * @param string room_name room's name, this parameter is taken from the parent of
 * the button (the anchor) from th href attribute
 *
 * @param string active_room the room currently active
 */
function leave_room(room_name, active_room){
    var active_room_name = active_room.children().attr("href").slice(1);

    if(active_room_name == room_name){
        var next_room = active_room.next();
        if(next_room.length != 0){ //the current tab is not the last, go right
            active_room.next().attr('class', 'active');
        }
        else{ //current tab is the last, move to left
            active_room.prev().attr('class', 'active');
        }
    }
    else{
        active_room.attr('class', 'active');
    }

    $.post($SCRIPT_ROOT + '/_leave_room', {'room': room_name}).fail(Handler.leave_room_error)
        .success(update_rooms);
}

/**
 * Display the rooms as tabs
 *
 * This function is able to decide whether it needs to create all the tabs or
 * just some of the tabs by array difference and if the user left or joined
 * rooms
 */
function display_rooms(){
    var left = false; //assume that the user joined some rooms
    var rooms = JSON.parse($('#rooms').val());

    var close_btn = $('<button>').attr({
        class: 'close',
        onclick: 'leave_room($(this).parent().attr("href").slice(1), $("li.active"))'
    }).html('×');

    /**
     * starting from one when joining rooms at login,
     * because the first room is displayed outside the for loop
     * since I need to set some classes to "active"
     */
    var pos = 1;

    if($('.nav.nav-tabs').length == 0){ //the user joins the rooms at login
        $('#chat').append(
            $('<ul>').attr({class: 'nav nav-tabs'}).append(
                $('<li>').attr({class: 'active'}).append(
                    $('<a>').attr({
                        href: '#' + rooms[0],
                        'data-toggle': 'tab',
                    }).html(rooms[0] + close_btn.prop('outerHTML'))
                )
            )
        );

        $('#chat').append(
            $('<div>').attr({class: 'tab-content'}).append(
                $('<div>').attr({
                    class: 'tab-pane active',
                    id: rooms[0],
                })
            )
        );
    }
    else{ //the user joins the rooms after he logged in, so we display only the
          //new rooms since the rest are already displayed
        var current_rooms = get_current_rooms();

        var initial_rooms = rooms; //in case the user left rooms

        rooms = rooms.filter(function(i){
            return current_rooms.indexOf(i) < 0;
        });

        /**
         * the user left rooms, didn't join, so the list is smaller
         * and tabs should be removed, not added
         */
        if(rooms.length == 0){
            rooms = current_rooms.filter(function(i){
                return initial_rooms.indexOf(i) < 0;
            });

            left = true;
        }

        /**
         * every room should be processed because if we join or leave we don;t
         * have to change the active room
         */
        pos = 0;
    }

    for(i=pos; i < rooms.length; i++){
        if(left){
            $('a[href="#' + rooms[i] + '"]').parent().remove();
            $('#' + rooms[i]).remove();
        }
        else{
            $('.nav.nav-tabs').append(
                $('<li>').append(
                    $('<a>').attr({
                        href: '#' + rooms[i],
                        'data-toggle': 'tab',
                    }).html(rooms[i] + close_btn.prop('outerHTML'))
                )
            );

            $('.tab-content').append(
                $('<div>').attr({
                    class: 'tab-pane',
                    id: rooms[i],
                })
            );
        }
    }
}

/**
 * Get the currently joined rooms
 *
 * @return array of room names
 */
function get_current_rooms(){
    var current_rooms = [];
    $('.tab-pane').each(function(){
        current_rooms.push($(this).attr('id'))
    });

    return current_rooms;
}

/**
 * Create an AJAX request to join more rooms after logging in
 */
function join_rooms(e){
    e.preventDefault();
    $.post($SCRIPT_ROOT + '/_join_rooms', {'join_rooms': $('#join_rooms').val()})
        .fail(Handler.join_error).success(update_rooms);
    $('#join_rooms').val('');
}

/**
 * Initialize the elements that compose the chat, the EventSource (for the Server Sent
 * Events) and display the rooms the user joined
 */
function load_chat(){
    var stream = new EventSource($SCRIPT_ROOT + '/_sse_stream');

    stream.onmessage = Handler.event_message;
    stream.onerror = Handler.event_error;

    stream.addEventListener('users', Handler.event_users);
    stream.addEventListener('ping', Handler.event_ping);

    display_rooms();
}

/**
 * Get the current time
 *
 * @return string the current time in HH:MM format
 */
function get_current_time(){
    var currentTime = new Date();

    var h = currentTime.getHours();
    var m = currentTime.getMinutes();

    if(h < 10){
        h = '0' + h;
    }

    if(m < 10){
        m = '0' + m;
    }

    return h + ':' + m;
}

/**
 * Check if the user is mentioned in the message
 *
 * A user is mentioned when his nick appears as a word in a message:
 * "hello @nick"
 * "nick, hello"
 * "nick: hi"
 * "how are you nick?"
 *
 * The nick may be surrounded by punctuation, symbols or spaces, but not other
 * letters, so for the following message this function will return false:
 * "hello foonick"
 *
 * @param message the message that is checked for mentions
 *
 * @return bool true if the user is mentioned, else false
 */
function is_mention(message){
    var nick = $('label[for="text"]').html().slice(0, -1);
    var pos = message.indexOf(nick);

    while(-1 !== pos){
        if((pos === 0 || (pos - 1 >= 0 && !is_letter(message[pos-1]))) &&
           (pos+nick.length == message.length ||
                (pos+nick.length < message.length &&
                !is_letter(message[pos+nick.length])
                )
           )){
            return true;
        }

        message = message.slice(pos+nick.length);
        pos = message.indexOf(nick);
    }

    return false;
}

/**
 * Check if a character is a letter
 *
 * @param l string this should be a single character to be checked
 *
 * @return bool true if l is a letter, else false
 */
function is_letter(l){
    if((l >= 'a' && l <= 'z') || (l >= 'A' && l <= 'Z')){
        return true;
    }

    return false;
}

/**
 * Notify the user that there is activity on the chat
 *
 * @param string room_name room's name where is activity
 */
function notify_activity(room_name){
    //create an activity notice if the tab is not focused
    var a = $('a[href="#' + room_name + '"]');
    if('active' !== a.parent().attr('class')){
        if(0 === $('#icon-' + room_name).length){ //prepend just one icon
            a.prepend($('<i>').attr({
                class: 'icon-comment',
                id: 'icon-' + room_name,
            }));
        }
    }

    if(away){
        Notificon("!", {
            font: '10px arial',
            color: '#ffffff',
            stroke: 'rgba(240, 61, 37, 1)',
            width: 7,
        });
    }
}

//Global initializations
load_chat();
$('[name="send"]').click(publish_message);
$('[name="join"]').click(join_rooms);

//clear the activity notice upon clicking on the tab
$('a[data-toggle="tab"]').on('show', function (e) {
    $('#icon-' + $(e.target).attr('href').slice(1)).remove();
});

//if the browser or the browser's tab is not focused display a Notificon
var away = false;

$(window).focus(function() {
    away = false;
    Notificon('');
});

$(window).blur(function() {
    away = true;
});
<commit_msg>Fix #25<commit_after>Handler = {
    /**
     * Display a user's message
     */
    event_message: function handle_event_message(e){
        var rooms = JSON.parse($('#rooms').val());
        var data = JSON.parse(e.data);

        /**
         * don't display the message if the user is not on the room
         * where the message comes from
         */
        if(-1 === rooms.indexOf(data['room'])){
            return;
        }

        var lineDiv = document.createElement('div');

        var timeSpan = document.createElement('span');
        var nickSpan = document.createElement('span');
        var msgSpan = document.createElement('span');

        timeSpan.innerHTML = get_current_time() + ' ';
        timeSpan.className = 'time';

        nickSpan.innerHTML = data['nick'] + ': ';
        nickSpan.className = 'nick';

        msgSpan.innerHTML = data['message'] + ' ';
        msgSpan.className = 'msg';

        if(is_mention(data['message'])){
            lineDiv.className = 'line mention';
        }
        else{
            lineDiv.className = 'line';
        }

        lineDiv.appendChild(timeSpan);
        lineDiv.appendChild(nickSpan);
        lineDiv.appendChild(msgSpan);

        $('#' + data['room']).append(lineDiv);

        notify_activity(data['room']);

        //TODO: scroll the div to the bottom of the page when the content is larger
        //than the div
    },

    /**
     * Update the user list when someone joins the chat
     */
    event_users: function handle_event_users(e){
        var users = $.parseJSON(e.data);
        var usersDiv = $('#user-list')[0];

        usersDiv.innerHTML = '';

        for(i=0; i<users.length-1; i++){
            usersDiv.innerHTML += users[i] + ', ';
        }

        usersDiv.innerHTML += users[users.length-1];
    },

    /**
     * Callback to respond to ping events via AJAX
     */
    event_ping: function handle_event_ping(e){
        $.post($SCRIPT_ROOT + '/_pong', 'PONG!');
    },

    /**
     * Callback that handles message sending errors
     */
    publish_error: function handle_publish_error(e){
        var lineDiv = document.createElement('div');

        lineDiv.className = 'line error';
        lineDiv.innerHTML = e.responseText;

        $('.tab-pane.active').append(lineDiv);
    },

    /**
     * Callback for handling SSE errors
     */
    event_error: function handle_event_error(e){
        this.close(); //here, `this` refers to `stream`
        console.log('err:', e);
        //TODO: display some error to the user here (some kind of pop up) with
        //e.data as the err msg, also show a reconnect btn with a timer for
        //auto-reconnect
    },

    /**
     * Callback that handles the join errors
     */
    join_error: function handle_join_error(e){
        var lineDiv = document.createElement('div');

        lineDiv.className = 'join error';
        lineDiv.innerHTML = e.responseText;

        $('.tab-pane.active').append(lineDiv);
    },

    /**
     * Callback for handling the leave error (these occur when leaving rooms)
     */
    leave_room_error: function handle_leave_room_error(e){
        //if the status is 404 then the user closed his last room, so we logged
        //him out
        if(404 === e.status){
            window.location.replace($SCRIPT_ROOT);
        }

        var lineDiv = document.createElement('div');

        lineDiv.className = 'leave error';
        lineDiv.innerHTML = e.responseText;

        $('.tab-pane.active').append(lineDiv);
    },
}

/**
 * Create an AJAX request that will publish the message the user wants to
 * transmit
 */
function publish_message(e){
    e.preventDefault();
    $.post($SCRIPT_ROOT + '/_publish_message', {'message': $('#text').val(),
        'room': $('.tab-pane.active').attr('id')}).fail(Handler.publish_error);
    $('#text').val('');
}

/**
 * Callback for successfully joining or leaving rooms after login
 */
function update_rooms(e){
    $('#rooms').val(e);
    display_rooms();
}

/**
 * Leave a room
 *
 * An AJAX request is made to the server which will respond with the new room
 * list, also the tabs will be updated
 *
 * @param string room_name room's name, this parameter is taken from the parent of
 * the button (the anchor) from th href attribute
 *
 * @param string active_room the room currently active
 */
function leave_room(room_name, active_room){
    var active_room_name = active_room.children().attr("href").slice(1);

    if(active_room_name == room_name){
        var next_room = active_room.next();
        if(next_room.length != 0){ //the current tab is not the last, go right
            active_room.next().attr('class', 'active');
        }
        else{ //current tab is the last, move to left
            active_room.prev().attr('class', 'active');
        }
    }
    else{
        active_room.attr('class', 'active');
    }

    $.post($SCRIPT_ROOT + '/_leave_room', {'room': room_name}).fail(Handler.leave_room_error)
        .success(update_rooms);
}

/**
 * Display the rooms as tabs
 *
 * This function is able to decide whether it needs to create all the tabs or
 * just some of the tabs by array difference and if the user left or joined
 * rooms
 */
function display_rooms(){
    var left = false; //assume that the user joined some rooms
    var rooms = JSON.parse($('#rooms').val());

    var close_btn = $('<button>').attr({
        class: 'close',
        onclick: 'leave_room($(this).parent().attr("href").slice(1), $("li.active"))'
    }).html('×');

    /**
     * starting from one when joining rooms at login,
     * because the first room is displayed outside the for loop
     * since I need to set some classes to "active"
     */
    var pos = 1;

    if($('.nav.nav-tabs').length == 0){ //the user joins the rooms at login
        $('#chat').append(
            $('<ul>').attr({class: 'nav nav-tabs'}).append(
                $('<li>').attr({class: 'active'}).append(
                    $('<a>').attr({
                        href: '#' + rooms[0],
                        'data-toggle': 'tab',
                    }).html(rooms[0] + close_btn.prop('outerHTML'))
                )
            )
        );

        $('#chat').append(
            $('<div>').attr({class: 'tab-content'}).append(
                $('<div>').attr({
                    class: 'tab-pane active',
                    id: rooms[0],
                })
            )
        );
    }
    else{ //the user joins the rooms after he logged in, so we display only the
          //new rooms since the rest are already displayed
        var current_rooms = get_current_rooms();

        var initial_rooms = rooms; //in case the user left rooms

        rooms = rooms.filter(function(i){
            return current_rooms.indexOf(i) < 0;
        });

        /**
         * the user left rooms, didn't join, so the list is smaller
         * and tabs should be removed, not added
         */
        if(rooms.length == 0){
            rooms = current_rooms.filter(function(i){
                return initial_rooms.indexOf(i) < 0;
            });

            left = true;
        }

        /**
         * every room should be processed because if we join or leave we don;t
         * have to change the active room
         */
        pos = 0;
    }

    for(i=pos; i < rooms.length; i++){
        if(left){
            $('a[href="#' + rooms[i] + '"]').parent().remove();
            $('#' + rooms[i]).remove();
        }
        else{
            $('.nav.nav-tabs').append(
                $('<li>').append(
                    $('<a>').attr({
                        href: '#' + rooms[i],
                        'data-toggle': 'tab',
                    }).html(rooms[i] + close_btn.prop('outerHTML'))
                )
            );

            $('.tab-content').append(
                $('<div>').attr({
                    class: 'tab-pane',
                    id: rooms[i],
                })
            );
        }
    }
}

/**
 * Get the currently joined rooms
 *
 * @return array of room names
 */
function get_current_rooms(){
    var current_rooms = [];
    $('.tab-pane').each(function(){
        current_rooms.push($(this).attr('id'))
    });

    return current_rooms;
}

/**
 * Create an AJAX request to join more rooms after logging in
 */
function join_rooms(e){
    e.preventDefault();
    $.post($SCRIPT_ROOT + '/_join_rooms', {'join_rooms': $('#join_rooms').val()})
        .fail(Handler.join_error).success(update_rooms);
    $('#join_rooms').val('');
}

/**
 * Initialize the elements that compose the chat, the EventSource (for the Server Sent
 * Events) and display the rooms the user joined
 */
function load_chat(){
    var stream = new EventSource($SCRIPT_ROOT + '/_sse_stream');

    stream.onmessage = Handler.event_message;
    stream.onerror = Handler.event_error;

    stream.addEventListener('users', Handler.event_users);
    stream.addEventListener('ping', Handler.event_ping);

    display_rooms();
}

/**
 * Get the current time
 *
 * @return string the current time in HH:MM format
 */
function get_current_time(){
    var currentTime = new Date();

    var h = currentTime.getHours();
    var m = currentTime.getMinutes();

    if(h < 10){
        h = '0' + h;
    }

    if(m < 10){
        m = '0' + m;
    }

    return h + ':' + m;
}

/**
 * Check if the user is mentioned in the message
 *
 * A user is mentioned when his nick appears as a word in a message:
 * "hello @nick"
 * "nick, hello"
 * "nick: hi"
 * "how are you nick?"
 *
 * The nick may be surrounded by punctuation, symbols or spaces, but not other
 * letters, so for the following message this function will return false:
 * "hello foonick"
 *
 * @param message the message that is checked for mentions
 *
 * @return bool true if the user is mentioned, else false
 */
function is_mention(message){
    var nick = $('label[for="text"]').html().slice(0, -1);
    var pos = message.indexOf(nick);

    while(-1 !== pos){
        if((pos === 0 || (pos - 1 >= 0 && !is_letter(message[pos-1]))) &&
           (pos+nick.length == message.length ||
                (pos+nick.length < message.length &&
                !is_letter(message[pos+nick.length])
                )
           )){
            return true;
        }

        message = message.slice(pos+nick.length);
        pos = message.indexOf(nick);
    }

    return false;
}

/**
 * Check if a character is a letter
 *
 * @param l string this should be a single character to be checked
 *
 * @return bool true if l is a letter, else false
 */
function is_letter(l){
    if((l >= 'a' && l <= 'z') || (l >= 'A' && l <= 'Z')){
        return true;
    }

    return false;
}

/**
 * Notify the user that there is activity on the chat
 *
 * @param string room_name room's name where is activity
 */
function notify_activity(room_name){
    //create an activity notice if the tab is not focused
    var a = $('a[href="#' + room_name + '"]');
    if('active' !== a.parent().attr('class')){
        if(0 === $('#icon-' + room_name).length){ //prepend just one icon
            a.prepend($('<i>').attr({
                class: 'icon-comment',
                id: 'icon-' + room_name,
            }));
        }
    }

    if(away){
        Notificon("!", {
            font: '10px arial',
            color: '#ffffff',
            stroke: 'rgba(240, 61, 37, 1)',
            width: 7,
        });
    }
}

/**
 * Add horizontal rules to each object in an array
 *
 * If there are existing <hr> elements they are removed and a new one is added
 *
 * @param array obj array of objects to add a horizontal rule to
 */
function add_hr(obj){
    for(i=0; i<obj.length; i++){
        obj[i].innerHTML = obj[i].innerHTML.split('<hr>').join('') + '<hr>';
    }
}

//Global initializations
load_chat();
$('[name="send"]').click(publish_message);
$('[name="join"]').click(join_rooms);

//clear the activity notice upon clicking on the tab
$('a[data-toggle="tab"]').on('show', function (e) {
    $('#icon-' + $(e.target).attr('href').slice(1)).remove();

    //add a <hr> to the last tab in order to mark the activity on that room
    //since the user moved away
    if(typeof e.relatedTarget !== 'undefined'){
        add_hr($('div' + $(e.relatedTarget).attr('href')));
    }
});

//if the browser or the browser's tab is not focused display a Notificon
var away = false;

$(window).focus(function() {
    away = false;
    Notificon('');
});

$(window).blur(function() {
    away = true;

    add_hr($('div.tab-pane'));
});
<|endoftext|>