diff --git a/README.md b/README.md index e687ed9..8accba9 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,25 @@ -HackMD 0.2.7 +HackMD 0.2.8 === This is a realtime collaborative markdown notes on all platforms. But still in early stage, feel free to fork or contribute to it. -Thanks for your using. +Thanks for your using! + +There are some config you need to change in below files +``` +./run.sh +./config.js +./public/js/common.js +``` + +You can use SSL to encrypt your site by passing certificate path in the `config.js` and set `usessl=true`. + +And there is a script called `run.sh`, it's for someone like me to run the server via npm package `forever`, and can passing environment variable to the server, like heroku does. + +To install `forever`, just type `npm install forever -g` The notes are store in PostgreSQL, and I provided the schema in the `hackmd_schema.sql`. The users and sessions are store in mongoDB, which don't need schema, so just connect it directly. -License under MIT. +**License under MIT.** \ No newline at end of file diff --git a/app.js b/app.js index 3f66b32..d398488 100644 --- a/app.js +++ b/app.js @@ -1,6 +1,5 @@ //app //external modules -var connect = require('connect'); var express = require('express'); var toobusy = require('toobusy-js'); var ejs = require('ejs'); @@ -11,24 +10,45 @@ var mongoose = require('mongoose'); var compression = require('compression') var session = require('express-session'); var MongoStore = require('connect-mongo')(session); +var fs = require('fs'); +var shortid = require('shortid'); +var imgur = require('imgur'); +var formidable = require('formidable'); //core var config = require("./config.js"); var User = require("./lib/user.js"); +var Temp = require("./lib/temp.js"); var auth = require("./lib/auth.js"); var response = require("./lib/response.js"); //server setup -var app = express(); -var server = require('http').createServer(app); +if (config.usessl) { + var ca = (function () { + var i, len, results; + results = []; + for (i = 0, len = config.sslcapath.length; i < len; i++) { + results.push(fs.readFileSync(config.sslcapath[i], 'utf8')); + } + return results; + })(); + var options = { + key: fs.readFileSync(config.sslkeypath, 'utf8'), + cert: fs.readFileSync(config.sslcertpath, 'utf8'), + ca: ca, + requestCert: false, + rejectUnauthorized: false + }; + var app = express(); + var server = require('https').createServer(options, app); +} else { + var app = express(); + var server = require('http').createServer(app); +} var io = require('socket.io').listen(server); -var port = process.env.PORT || config.testport; // connect to the mongodb -if (config.debug) - mongoose.connect(config.mongodbstring); -else - mongoose.connect(process.env.MONGOLAB_URI); +mongoose.connect(process.env.MONGOLAB_URI || config.mongodbstring); //others var db = require("./lib/db.js"); @@ -53,7 +73,7 @@ app.use(session({ name: config.sessionname, secret: config.sessionsecret, resave: false, //don't save session if unmodified - saveUninitialized: true, //don't create session until something stored + saveUninitialized: false, //don't create session until something stored cookie: { maxAge: new Date(Date.now() + config.sessionlife), expires: new Date(Date.now() + config.sessionlife), @@ -111,6 +131,59 @@ app.get("/status", function (req, res, next) { res.end(JSON.stringify(data)); }); }); +//get status +app.get("/temp", function (req, res) { + var host = req.get('host'); + if (config.alloworigin.indexOf(host) == -1) + response.errorForbidden(res); + else { + var tempid = req.query.tempid; + if (!tempid) + response.errorForbidden(res); + else { + Temp.findTemp(tempid, function (err, temp) { + if (err || !temp) + response.errorForbidden(res); + else { + res.header("Access-Control-Allow-Origin", "*"); + res.send({ + temp: temp.data + }); + temp.remove(function (err) { + if (err) + console.log('remove temp failed: ' + err); + }); + } + }); + } + } +}); +//post status +app.post("/temp", urlencodedParser, function (req, res) { + var host = req.get('host'); + if (config.alloworigin.indexOf(host) == -1) + response.errorForbidden(res); + else { + var id = shortid.generate(); + var data = req.body.data; + if (!id || !data) + response.errorForbidden(res); + else { + if (config.debug) + console.log('SERVER received temp from [' + host + ']: ' + req.body.data); + Temp.newTemp(id, data, function (err, temp) { + if (!err && temp) { + res.header("Access-Control-Allow-Origin", "*"); + res.send({ + status: 'ok', + id: temp.id + }); + } else + response.errorInternalError(res); + }); + } + } +}); //facebook auth app.get('/auth/facebook', passport.authenticate('facebook'), @@ -230,6 +303,29 @@ app.get('/me', function (req, res) { }); } }); +//upload to imgur +app.post('/uploadimage', function (req, res) { + var form = new formidable.IncomingForm(); + form.parse(req, function (err, fields, files) { + if (err || !files.image || !files.image.path) { + response.errorForbidden(res); + } else { + if (config.debug) + console.log('SERVER received uploadimage: ' + JSON.stringify(files.image)); + imgur.setClientId(config.imgur.clientID); + imgur.uploadFile(files.image.path) + .then(function (json) { + if (config.debug) + console.log('SERVER uploadimage success: ' + JSON.stringify(json)); + res.send({link:json.data.link}); + }) + .catch(function (err) { + console.error(err); + res.send(err.message); + }); + } + }); +}); //get new note app.get("/new", response.newNote); //get features @@ -248,6 +344,12 @@ io.set('heartbeat timeout', config.heartbeattimeout); io.sockets.on('connection', realtime.connection); //listen -server.listen(port, function () { - console.log('Server listening at port %d', port); -}); \ No newline at end of file +if (config.usessl) { + server.listen(config.sslport, function () { + console.log('HTTPS Server listening at sslport %d', config.sslport); + }); +} else { + server.listen(config.port, function () { + console.log('HTTP Server listening at port %d', config.port); + }); +} \ No newline at end of file diff --git a/config.js b/config.js index 3a5c764..03764fe 100644 --- a/config.js +++ b/config.js @@ -1,11 +1,33 @@ //config var path = require('path'); +var domain = process.env.DOMAIN; +var testport = '3000'; +var testsslport = '3001'; +var port = process.env.PORT || testport; +var sslport = process.env.SSLPORT || testsslport; +var usessl = false; +var urladdport = true; //add port on getserverurl + var config = { debug: true, - version: '0.2.7', - domain: 'http://localhost:3000', - testport: '3000', + version: '0.2.8', + domain: domain, + alloworigin: ['add here to allow origin to cross'], + testport: testport, + testsslport: testsslport, + port: port, + sslport: sslport, + sslkeypath: 'change this', + sslcertpath: 'change this', + sslcapath: ['change this'], + usessl: usessl, + getserverurl: function() { + if(usessl) + return 'https://' + domain + (sslport == 443 || !urladdport ? '' : ':' + sslport); + else + return 'http://' + domain + (port == 80 || !urladdport ? '' : ':' + port); + }, //path tmppath: "./tmp/", defaultnotepath: path.join(__dirname, '/public', "default.md"), @@ -14,36 +36,39 @@ var config = { errorpath: path.join(__dirname, '/public/views', "error.ejs"), prettypath: path.join(__dirname, '/public/views', 'pretty.ejs'), //db string - postgresqlstring: "postgresql://localhost:5432/hackmd", - mongodbstring: "mongodb://localhost/hackmd", + postgresqlstring: "change this", + mongodbstring: "change this", //constants featuresnotename: "features", - sessionname: 'please set this', - sessionsecret: 'please set this', + sessionname: 'change this', + sessionsecret: 'change this', sessionlife: 14 * 24 * 60 * 60 * 1000, //14 days sessiontouch: 1 * 3600, //1 hour heartbeatinterval: 5000, heartbeattimeout: 10000, //auth facebook: { - clientID: 'get yourself one', - clientSecret: 'get yourself one', + clientID: 'change this', + clientSecret: 'change this', callbackPath: '/auth/facebook/callback' }, twitter: { - consumerKey: 'get yourself one', - consumerSecret: 'get yourself one', + consumerKey: 'change this', + consumerSecret: 'change this', callbackPath: '/auth/twitter/callback' }, github: { - clientID: 'get yourself one', - clientSecret: 'get yourself one', + clientID: 'change this', + clientSecret: 'change this', callbackPath: '/auth/github/callback' }, dropbox: { - clientID: 'get yourself one', - clientSecret: 'get yourself one', + clientID: 'change this', + clientSecret: 'change this', callbackPath: '/auth/dropbox/callback' + }, + imgur: { + clientID: 'change this' } }; diff --git a/lib/auth.js b/lib/auth.js index e7b0dc7..456c9df 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -27,23 +27,23 @@ function callback(accessToken, refreshToken, profile, done) { module.exports = passport.use(new FacebookStrategy({ clientID: config.facebook.clientID, clientSecret: config.facebook.clientSecret, - callbackURL: config.domain + config.facebook.callbackPath + callbackURL: config.getserverurl() + config.facebook.callbackPath }, callback)); //twitter passport.use(new TwitterStrategy({ consumerKey: config.twitter.consumerKey, consumerSecret: config.twitter.consumerSecret, - callbackURL: config.domain + config.twitter.callbackPath + callbackURL: config.getserverurl() + config.twitter.callbackPath }, callback)); //github passport.use(new GithubStrategy({ clientID: config.github.clientID, clientSecret: config.github.clientSecret, - callbackURL: config.domain + config.github.callbackPath + callbackURL: config.getserverurl() + config.github.callbackPath }, callback)); //dropbox passport.use(new DropboxStrategy({ clientID: config.dropbox.clientID, clientSecret: config.dropbox.clientSecret, - callbackURL: config.domain + config.dropbox.callbackPath + callbackURL: config.getserverurl() + config.dropbox.callbackPath }, callback)); \ No newline at end of file diff --git a/lib/db.js b/lib/db.js index d763eb8..d924cdf 100644 --- a/lib/db.js +++ b/lib/db.js @@ -18,10 +18,7 @@ var db = { }; function getDBClient() { - if (config.debug) - return new pg.Client(config.postgresqlstring); - else - return new pg.Client(process.env.DATABASE_URL); + return new pg.Client(process.env.DATABASE_URL || config.postgresqlstring); } function readFromFile(callback) { @@ -49,12 +46,14 @@ function newToDB(id, owner, body, callback) { var client = getDBClient(); client.connect(function (err) { if (err) { + client.end(); callback(err, null); return console.error('could not connect to postgres', err); } var newnotequery = util.format(insertquery, id, owner, body); //console.log(newnotequery); client.query(newnotequery, function (err, result) { + client.end(); if (err) { callback(err, null); return console.error("new note to db failed: " + err); @@ -62,7 +61,6 @@ function newToDB(id, owner, body, callback) { if (config.debug) console.log("new note to db success"); callback(null, result); - client.end(); } }); }); @@ -72,23 +70,25 @@ function readFromDB(id, callback) { var client = getDBClient(); client.connect(function (err) { if (err) { + client.end(); callback(err, null); return console.error('could not connect to postgres', err); } var readquery = util.format(selectquery, id); //console.log(readquery); client.query(readquery, function (err, result) { + client.end(); if (err) { callback(err, null); return console.error("read from db failed: " + err); } else { //console.log(result.rows); if (result.rows.length <= 0) { - callback("not found note in db", null); + callback("not found note in db: " + id, null); } else { - console.log("read from db success"); + if(config.debug) + console.log("read from db success"); callback(null, result); - client.end(); } } }); @@ -99,12 +99,14 @@ function saveToDB(id, title, data, callback) { var client = getDBClient(); client.connect(function (err) { if (err) { + client.end(); callback(err, null); return console.error('could not connect to postgres', err); } var savequery = util.format(updatequery, title, data, id); //console.log(savequery); client.query(savequery, function (err, result) { + client.end(); if (err) { callback(err, null); return console.error("save to db failed: " + err); @@ -112,7 +114,6 @@ function saveToDB(id, title, data, callback) { if (config.debug) console.log("save to db success"); callback(null, result); - client.end(); } }); }); @@ -122,10 +123,12 @@ function countFromDB(callback) { var client = getDBClient(); client.connect(function (err) { if (err) { + client.end(); callback(err, null); return console.error('could not connect to postgres', err); } client.query(countquery, function (err, result) { + client.end(); if (err) { callback(err, null); return console.error("count from db failed: " + err); @@ -134,9 +137,9 @@ function countFromDB(callback) { if (result.rows.length <= 0) { callback("not found note in db", null); } else { - console.log("count from db success"); + if(config.debug) + console.log("count from db success"); callback(null, result); - client.end(); } } }); diff --git a/lib/realtime.js b/lib/realtime.js index 303eb6a..e380027 100644 --- a/lib/realtime.js +++ b/lib/realtime.js @@ -81,10 +81,11 @@ function getStatus(callback) { break; } } - if (!found) + if (!found) { distinctaddresses.push(value.address); - if(!found && value.login) - distinctregusers++; + if(value.login) + distinctregusers++; + } }); User.getUserCount(function (err, regcount) { if (err) { @@ -372,11 +373,19 @@ function connection(socket) { switch (op.origin) { case '+input': case '+delete': + case '+transpose': case 'paste': case 'cut': case 'undo': case 'redo': case 'drag': + case '*compose': + case 'case': + case '+insertLine': + case '+swapLine': + case '+joinLines': + case '+duplicateLine': + case '+sortLines': notes[notename].socks.forEach(function (sock) { if (sock != socket) { if (config.debug) diff --git a/lib/response.js b/lib/response.js index 458ed01..a7dcc02 100644 --- a/lib/response.js +++ b/lib/response.js @@ -17,16 +17,16 @@ var Note = require("./note.js"); //public var response = { errorForbidden: function (res) { - res.status(403).send("Forbidden, oh no.") + res.status(403).send("Forbidden, oh no."); }, errorNotFound: function (res) { - responseError(res, "404", "Not Found", "oops.") + responseError(res, "404", "Not Found", "oops."); }, errorInternalError: function (res) { - responseError(res, "500", "Internal Error", "wtf.") + responseError(res, "500", "Internal Error", "wtf."); }, errorServiceUnavailable: function (res) { - res.status(503).send("I'm busy right now, try again later.") + res.status(503).send("I'm busy right now, try again later."); }, newNote: newNote, showFeatures: showFeatures, diff --git a/lib/temp.js b/lib/temp.js new file mode 100644 index 0000000..90d9343 --- /dev/null +++ b/lib/temp.js @@ -0,0 +1,83 @@ +//temp +//external modules +var mongoose = require('mongoose'); + +//core +var config = require("../config.js"); + +// create a temp model +var model = mongoose.model('temp', { + id: String, + data: String, + created: Date +}); + +//public +var temp = { + model: model, + findTemp: findTemp, + newTemp: newTemp, + removeTemp: removeTemp, + getTempCount: getTempCount +}; + +function getTempCount(callback) { + model.count(function(err, count){ + if(err) callback(err, null); + else callback(null, count); + }); +} + +function findTemp(id, callback) { + model.findOne({ + id: id + }, function (err, temp) { + if (err) { + console.log('find temp failed: ' + err); + callback(err, null); + } + if (!err && temp) { + callback(null, temp); + } else { + console.log('find temp failed: ' + err); + callback(err, null); + }; + }); +} + +function newTemp(id, data, callback) { + var temp = new model({ + id: id, + data: data, + created: Date.now() + }); + temp.save(function (err) { + if (err) { + console.log('new temp failed: ' + err); + callback(err, null); + } else { + console.log("new temp success: " + temp.id); + callback(null, temp); + }; + }); +} + +function removeTemp(id, callback) { + findTemp(id, function(err, temp) { + if(!err && temp) { + temp.remove(function(err) { + if(err) { + console.log('remove temp failed: ' + err); + callback(err, null); + } else { + callback(null, null); + } + }); + } else { + console.log('remove temp failed: ' + err); + callback(err, null); + } + }); +} + +module.exports = temp; \ No newline at end of file diff --git a/lib/user.js b/lib/user.js index 1d7f11d..1106450 100644 --- a/lib/user.js +++ b/lib/user.js @@ -37,7 +37,7 @@ function findUser(id, callback) { console.log('find user failed: ' + err); callback(err, null); } - if (!err && user != null) { + if (!err && user) { callback(null, user); } else { console.log('find user failed: ' + err); @@ -65,7 +65,7 @@ function newUser(id, profile, callback) { function findOrNewUser(id, profile, callback) { findUser(id, function(err, user) { - if(err || user == null) { + if(err || !user) { newUser(id, profile, function(err, user) { if(err) { console.log('find or new user failed: ' + err); diff --git a/package.json b/package.json index 7c38fec..2d2d462 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "hackmd", - "version": "0.2.7", + "version": "0.2.8", "description": "Realtime collaborative markdown notes on all platforms.", - "main": "server.js", - "author": "jackymaxj", + "main": "app.js", + "author": "jackycute", "private": true, "license": "MIT", "dependencies": { @@ -11,7 +11,6 @@ "body-parser": "^1.12.3", "cheerio": "^0.19.0", "compression": "^1.4.3", - "connect": "3.x", "connect-mongo": "^0.8.1", "cookie": "0.1.2", "cookie-parser": "1.3.3", @@ -19,8 +18,9 @@ "emojify.js": "^1.0.1", "express": "4.x", "express-session": "^1.11.1", + "formidable": "^1.0.17", "highlight.js": "^8.4.0", - "html": "0.0.7", + "imgur": "^0.1.5", "jsdom-nogyp": "^0.8.3", "lz-string": "1.3.6", "markdown-pdf": "^5.2.0", diff --git a/public/css/cover.css b/public/css/cover.css index da4207b..30d54d3 100644 --- a/public/css/cover.css +++ b/public/css/cover.css @@ -23,13 +23,15 @@ a:hover { /* * Base structure */ - +html { + height: 100%; +} html, body { - height: 100%; background-color: #333; } body { + min-height: 100%; color: #fff; text-align: center; text-shadow: 0 1px 3px rgba(0, 0, 0, .5); @@ -40,7 +42,7 @@ body { padding: 10px; display: table; width: 100%; - height: 100%; + height: 100vh; /* For at least Firefox */ min-height: 100%; -webkit-box-shadow: inset 0 0 100px rgba(0, 0, 0, .5); @@ -156,7 +158,7 @@ body { .masthead, .mastfoot, .cover-container { - width: 700px; + width: 1000px; } } .section ul { @@ -168,41 +170,14 @@ html, body { overflow-x: hidden; } -.select2-selection, -.select2-search__field { - outline: 0; -} -.select2-search__field:hover { - border: 1px solid #b9b9b9 !important; - border-top-color: #a0a0a0 !important; -} -.select2-search__field:focus { - border: 1px solid #4d90fe !important; -} input { color: black; } .mastfoot { position: relative; } -.select2 { - width: 100% !important; - max-width: 400px; -} -.select2-selection { - height: 32px !important; -} -.select2-selection__rendered { - line-height: 32px !important; -} -.select2-selection__arrow { - height: 30px !important; -} -.select2-selection__rendered, -.select2-selection__placeholder, -.select2-results__option { - color: #000; - text-shadow: none; +.select2-container { + margin: 0 auto !important; } .list { width: 100%; @@ -275,4 +250,24 @@ input { .modal-title { text-align: left; color: black; +} + +.btn-file { + position: relative; + overflow: hidden; +} +.btn-file input[type=file] { + position: absolute; + top: 0; + right: 0; + min-width: 100%; + min-height: 100%; + font-size: 100px; + text-align: right; + filter: alpha(opacity=0); + opacity: 0; + outline: none; + background: white; + cursor: inherit; + display: block; } \ No newline at end of file diff --git a/public/css/github-extract.css b/public/css/github-extract.css index 201aebc..67ceb41 100644 --- a/public/css/github-extract.css +++ b/public/css/github-extract.css @@ -1,6 +1,5 @@ .markdown-body { overflow: hidden; - font-family: "Helvetica Neue", Helvetica, "Segoe UI", Arial, freesans, sans-serif; font-size: 16px; line-height: 1.6; word-wrap: break-word; @@ -339,7 +338,7 @@ } .task-list-item-checkbox { float: left; - margin: 0.4em 0 0.2em -1.3em !important; + margin: 0.31em 0 0.2em -1.3em !important; vertical-align: middle; cursor: default !important; } \ No newline at end of file diff --git a/public/css/index.css b/public/css/index.css index 6cfbeab..b3a6bae 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -20,7 +20,7 @@ form, font-family: 'Source Code Pro', Consolas, monaco, monospace; line-height: 18px; font-size: 16px; - height: auto; + /*height: auto;*/ min-height: 100%; overflow-y: hidden !important; -webkit-overflow-scrolling: touch; @@ -30,7 +30,7 @@ form, overflow-y: auto !important; } .CodeMirror-code { - padding-bottom: 100px; + /*padding-bottom: 72px;*/ } .CodeMirror-linenumber { opacity: 0.5; @@ -43,7 +43,7 @@ form, .CodeMirror-foldmarker { color: #d0d0d0; text-shadow: none; - font-family: arial; + font-family: Arial; line-height: .3; cursor: pointer; margin: 2px; diff --git a/public/css/site.css b/public/css/site.css index b50a1f9..eed8b95 100644 --- a/public/css/site.css +++ b/public/css/site.css @@ -6,6 +6,7 @@ body { text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.004); /*text-rendering: optimizeLegibility;*/ -webkit-overflow-scrolling: touch; + font-family: "Helvetica Neue", Helvetica, Arial, "Microsoft JhengHei", sans-serif !important; } :focus { outline: none !important; diff --git a/public/index.html b/public/index.html index ab844ab..c52b8ec 100644 --- a/public/index.html +++ b/public/index.html @@ -10,13 +10,6 @@ - - - - - - - HackMD - Collaborative notes @@ -24,7 +17,8 @@ - + + @@ -56,9 +50,7 @@

HackMD

- Realtime collaborate notes. -
Using markdown syntax. -
On all platforms. + Realtime collaborative markdown notes on all platforms.

@@ -76,12 +68,12 @@

Sign In to get own history!

-

Below are history from cookie

+

Below are history from browser


+
+ +
- - Sort by title + + Title - - Sort by time + + Time + +
-

+

- + - +
- - - - - - - - - - - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/js/common.js b/public/js/common.js new file mode 100644 index 0000000..37591d3 --- /dev/null +++ b/public/js/common.js @@ -0,0 +1,53 @@ +//common +var domain = 'change this'; +var checkAuth = false; +var profile = null; +var lastLoginState = getLoginState(); +var loginStateChangeEvent = null; + +function resetCheckAuth() { + checkAuth = false; +} + +function setLoginState(bool) { + Cookies.set('loginstate', bool, { + expires: 14 + }); + if (loginStateChangeEvent && bool != lastLoginState) + loginStateChangeEvent(); + lastLoginState = bool; +} + +function getLoginState() { + return Cookies.get('loginstate') === "true"; +} + +function clearLoginState() { + Cookies.remove('loginstate'); +} + +function checkIfAuth(yesCallback, noCallback) { + var cookieLoginState = getLoginState(); + if (!checkAuth || typeof cookieLoginState == 'undefined') { + $.get('/me') + .done(function (data) { + if (data && data.status == 'ok') { + profile = data; + yesCallback(profile); + setLoginState(true); + } else { + noCallback(); + setLoginState(false); + } + }) + .fail(function () { + noCallback(); + setLoginState(false); + }); + checkAuth = true; + } else if (cookieLoginState) { + yesCallback(profile); + } else { + noCallback(); + } +} \ No newline at end of file diff --git a/public/js/cover.js b/public/js/cover.js index fea51f6..24ba605 100644 --- a/public/js/cover.js +++ b/public/js/cover.js @@ -1,3 +1,47 @@ +var options = { + valueNames: ['id', 'text', 'timestamp', 'fromNow', 'time', 'tags'], + item: '
  • \ + \ + \ +
    \ +
    \ +

    \ +

    \ +
    \ +

    \ +

    \ +
    \ +
    \ +
  • ' +}; +var historyList = new List('history', options); + +migrateHistoryFromTempCallback = pageInit; +loginStateChangeEvent = pageInit; +pageInit(); + +function pageInit() { + checkIfAuth( + function (data) { + $('.ui-signin').hide(); + $('.ui-or').hide(); + $('.ui-welcome').show(); + $('.ui-name').html(data.name); + $('.ui-signout').show(); + $(".ui-history").click(); + parseServerToHistory(historyList, parseHistoryCallback); + }, + function () { + $('.ui-signin').slideDown(); + $('.ui-or').slideDown(); + $('.ui-welcome').hide(); + $('.ui-name').html(''); + $('.ui-signout').hide(); + parseStorageToHistory(historyList, parseHistoryCallback); + } + ); +} + $(".masthead-nav li").click(function () { $(this).siblings().removeClass("active"); $(this).addClass("active"); @@ -19,63 +63,202 @@ $(".ui-releasenotes").click(function () { }); function checkHistoryList() { - if ($("#history-list").children().length > 0) + if ($("#history-list").children().length > 0) { $(".ui-nohistory").hide(); - else if ($("#history-list").children().length == 0) { + $(".ui-import-from-browser").hide(); + } else if ($("#history-list").children().length == 0) { $(".ui-nohistory").slideDown(); - var cookienotehistory = JSON.parse($.cookie('notehistory')); - if (login && cookienotehistory && cookienotehistory.length > 0) { - $(".ui-import-from-cookie").slideDown(); - } + getStorageHistory(function (data) { + if (data && data.length > 0 && getLoginState() && historyList.items.length == 0) { + $(".ui-import-from-browser").slideDown(); + } + }); } } -function parseHistoryCallback() { +function parseHistoryCallback(list, notehistory) { checkHistoryList(); + list.sort('timestamp', { + order: "desc" + }); + var filtertags = []; + $(".item").each(function (key, value) { + var a = $(this).closest("a"); + var id = a.siblings("span").html(); + var tagsEl = $(this).find(".tags"); + var item = historyList.get('id', id); + if (item.length > 0 && item[0]) { + var values = item[0].values(); + //parse link to element a + a.attr('href', '/' + values.id); + //parse tags + if (values.tags) { + var tags = values.tags; + if (tags.length > 0) { + var labels = []; + for (var j = 0; j < tags.length; j++) { + //push info filtertags if not found + var found = false; + if (filtertags.indexOf(tags[j]) != -1) + found = true; + if (!found) + filtertags.push(tags[j]); + //push into the item label + labels.push("" + tags[j] + ""); + } + tagsEl.html(labels.join(' ')); + } + } + } + }); $(".ui-history-close").click(function (e) { e.preventDefault(); - var id = $(this).closest("a").attr("href").split('/')[1]; + var id = $(this).closest("a").siblings("span").html(); getHistory(function (notehistory) { var newnotehistory = removeHistory(id, notehistory); saveHistory(newnotehistory); }); - $(this).closest("li").remove(); + list.remove('id', id); checkHistoryList(); }); + buildTagsFilter(filtertags); } -var login = false; - -checkIfAuth( - function (data) { - $('.ui-signin').hide(); - $('.ui-or').hide(); - $('.ui-welcome').show(); - $('.ui-name').html(data.name); - $('.ui-signout').show(); - $(".ui-history").click(); - login = true; - }, - function () { - $('.ui-signin').slideDown(); - $('.ui-or').slideDown(); - login = false; - } -); - -parseHistory(parseHistoryCallback); - -$(".ui-import-from-cookie").click(function () { - saveCookieHistoryToServer(function() { - parseCookieToHistory(parseHistoryCallback); - $(".ui-import-from-cookie").hide(); +$(".ui-import-from-browser").click(function () { + saveStorageHistoryToServer(function () { + parseStorageToHistory(historyList, parseHistoryCallback); }); }); +$(".ui-save-history").click(function () { + getHistory(function (data) { + var history = JSON.stringify(data); + var blob = new Blob([history], { + type: "application/json;charset=utf-8" + }); + saveAs(blob, 'hackmd_history_' + moment().format('YYYYMMDDHHmmss')); + }); +}); + +$(".ui-open-history").bind("change", function (e) { + var files = e.target.files || e.dataTransfer.files; + var file = files[0]; + var reader = new FileReader(); + reader.onload = function () { + var notehistory = JSON.parse(reader.result); + //console.log(notehistory); + if (!reader.result) return; + getHistory(function (data) { + var mergedata = data.concat(notehistory); + mergedata = clearDuplicatedHistory(mergedata); + saveHistory(mergedata); + parseHistory(historyList, parseHistoryCallback); + }); + $(".ui-open-history").replaceWith($(".ui-open-history").val('').clone(true)); + }; + reader.readAsText(file); +}); + +$(".ui-clear-history").click(function () { + saveHistory([]); + historyList.clear(); + checkHistoryList(); +}); + +$(".ui-refresh-history").click(function () { + resetCheckAuth(); + historyList.clear(); + parseHistory(historyList, parseHistoryCallback); +}); + +$(".ui-logout").click(function () { + clearLoginState(); + location.href = '/logout'; +}); + +var filtertags = []; +$(".ui-use-tags").select2({ + placeholder: 'Use tags...', + multiple: true, + data: function () { + return { + results: filtertags + }; + } +}); +$('.select2-input').css('width', 'inherit'); +buildTagsFilter([]); + +function buildTagsFilter(tags) { + for (var i = 0; i < tags.length; i++) + tags[i] = { + id: i, + text: tags[i] + }; + filtertags = tags; +} +$(".ui-use-tags").on('change', function () { + var tags = []; + var data = $(this).select2('data'); + for (var i = 0; i < data.length; i++) + tags.push(data[i].text); + if (tags.length > 0) { + historyList.filter(function (item) { + var values = item.values(); + if (!values.tags) return false; + var found = false; + for (var i = 0; i < tags.length; i++) { + if (values.tags.indexOf(tags[i]) != -1) { + found = true; + break; + } + } + return found; + }); + } else { + historyList.filter(); + } + checkHistoryList(); +}); + +$('.search').keyup(function () { + checkHistoryList(); +}); + var source = $("#template").html(); var template = Handlebars.compile(source); var context = { release: [ + { + version: "0.2.8", + tag: "flame", + date: moment("201505151200", 'YYYYMMDDhhmm').fromNow(), + detail: [ + { + title: "Features", + item: [ + "+ Support drag-n-drop(exclude firefox) and paste image inline", + "+ Support tags filter in history", + "+ Support sublime-like shortcut keys" + ] + }, + { + title: "Enhancements", + item: [ + "* Adjust index description", + "* Adjust toolbar ui and view font", + "* Remove scroll sync delay and gain accuracy" + ] + }, + { + title: "Fixes", + item: [ + "* Partial update in the front and the end might not render properly", + "* Server not handle some editor events" + ] + } + ] + }, { version: "0.2.7", tag: "fuel", diff --git a/public/js/extra.js b/public/js/extra.js index 45833c8..05fa470 100644 --- a/public/js/extra.js +++ b/public/js/extra.js @@ -28,6 +28,8 @@ function renderFilename(view) { return filename; } +var viewAjaxCallback = null; + //dynamic event or object binding here function finishView(view) { //youtube @@ -42,7 +44,7 @@ function finishView(view) { .each(function (key, value) { $.ajax({ type: 'GET', - url: 'http://vimeo.com/api/v2/video/' + $(value).attr('videoid') + '.json', + url: '//vimeo.com/api/v2/video/' + $(value).attr('videoid') + '.json', jsonp: 'callback', dataType: 'jsonp', success: function (data) { @@ -54,7 +56,7 @@ function finishView(view) { //gist view.find("code[data-gist-id]").each(function(key, value) { if($(value).children().length == 0) - $(value).gist(); + $(value).gist(viewAjaxCallback); }); //emojify emojify.run(view[0]); diff --git a/public/js/fb.js b/public/js/fb.js new file mode 100644 index 0000000..0bb7a46 --- /dev/null +++ b/public/js/fb.js @@ -0,0 +1,8 @@ +(function (d, s, id) { + var js, fjs = d.getElementsByTagName(s)[0]; + if (d.getElementById(id)) return; + js = d.createElement(s); + js.id = id; + js.src = "//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.3&appId=1436904003272070"; + fjs.parentNode.insertBefore(js, fjs); +}(document, 'script', 'facebook-jssdk')); \ No newline at end of file diff --git a/public/js/ga.js b/public/js/ga.js deleted file mode 100644 index 0fda241..0000000 --- a/public/js/ga.js +++ /dev/null @@ -1,14 +0,0 @@ -(function (i, s, o, g, r, a, m) { - i['GoogleAnalyticsObject'] = r; - i[r] = i[r] || function () { - (i[r].q = i[r].q || []).push(arguments) - }, i[r].l = 1 * new Date(); - a = s.createElement(o), - m = s.getElementsByTagName(o)[0]; - a.async = 1; - a.src = g; - m.parentNode.insertBefore(a, m) -})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga'); - -ga('create', 'get your self one', 'auto'); -ga('send', 'pageview'); \ No newline at end of file diff --git a/public/js/history.js b/public/js/history.js index c6deaa5..717a7ca 100644 --- a/public/js/history.js +++ b/public/js/history.js @@ -1,16 +1,35 @@ -//common -function checkIfAuth(yesCallback, noCallback) { - $.get('/me') - .done(function (data) { - if (data && data.status == 'ok') { - yesCallback(data); - } else { - noCallback(); - } - }) - .fail(function () { - noCallback(); - }); +var migrateHistoryFromTempCallback = null; + +migrateHistoryFromTemp(); + +function migrateHistoryFromTemp() { + if (url('#tempid')) { + $.get('/temp', { + tempid: url('#tempid') + }) + .done(function (data) { + if (data && data.temp) { + getStorageHistory(function (olddata) { + if (!olddata || olddata.length == 0) { + saveHistoryToStorage(JSON.parse(data.temp)); + } + }); + } + }) + .always(function () { + var hash = location.hash.split('#')[1]; + hash = hash.split('&'); + for (var i = 0; i < hash.length; i++) + if (hash[i].indexOf('tempid') == 0) { + hash.splice(i, 1); + i--; + } + hash = hash.join('&'); + location.hash = hash; + if (migrateHistoryFromTempCallback) + migrateHistoryFromTempCallback(); + }); + } } function saveHistory(notehistory) { @@ -19,13 +38,20 @@ function saveHistory(notehistory) { saveHistoryToServer(notehistory); }, function () { - saveHistoryToCookie(notehistory); + saveHistoryToStorage(notehistory); } ); } +function saveHistoryToStorage(notehistory) { + if (store.enabled) + store.set('notehistory', JSON.stringify(notehistory)); + else + saveHistoryToCookie(notehistory); +} + function saveHistoryToCookie(notehistory) { - $.cookie('notehistory', JSON.stringify(notehistory), { + Cookies.set('notehistory', notehistory, { expires: 365 }); } @@ -36,12 +62,29 @@ function saveHistoryToServer(notehistory) { }); } +function saveCookieHistoryToStorage(callback) { + store.set('notehistory', Cookies.get('notehistory')); + callback(); +} + +function saveStorageHistoryToServer(callback) { + var data = store.get('notehistory'); + if (data) { + $.post('/history', { + history: data + }) + .done(function (data) { + callback(data); + }); + } +} + function saveCookieHistoryToServer(callback) { $.post('/history', { - history: $.cookie('notehistory') + history: Cookies.get('notehistory') }) .done(function (data) { - callback(); + callback(data); }); } @@ -58,7 +101,7 @@ function clearDuplicatedHistory(notehistory) { if (!found) newnotehistory.push(notehistory[i]); } - return notehistory; + return newnotehistory; } function addHistory(id, text, time, tags, notehistory) { @@ -86,7 +129,7 @@ function writeHistory(view) { writeHistoryToServer(view); }, function () { - writeHistoryToCookie(view); + writeHistoryToStorage(view); } ); } @@ -113,7 +156,7 @@ function writeHistoryToServer(view) { function writeHistoryToCookie(view) { try { - var notehistory = JSON.parse($.cookie('notehistory')); + var notehistory = Cookies.getJSON('notehistory'); } catch (err) { var notehistory = []; } @@ -122,6 +165,22 @@ function writeHistoryToCookie(view) { saveHistoryToCookie(newnotehistory); } +function writeHistoryToStorage(view) { + if (store.enabled) { + var data = store.get('notehistory'); + if (data) { + if (typeof data == "string") + data = JSON.parse(data); + var notehistory = data; + } else + var notehistory = []; + var newnotehistory = generateHistory(view, notehistory); + saveHistoryToStorage(newnotehistory); + } else { + writeHistoryToCookie(view); + } +} + function renderHistory(view) { var title = renderFilename(view); @@ -169,7 +228,7 @@ function getHistory(callback) { getServerHistory(callback); }, function () { - getCookieHistory(callback); + getStorageHistory(callback); } ); } @@ -187,70 +246,76 @@ function getServerHistory(callback) { } function getCookieHistory(callback) { - callback(JSON.parse($.cookie('notehistory'))); + callback(Cookies.getJSON('notehistory')); } -function parseHistory(callback) { +function getStorageHistory(callback) { + if (store.enabled) { + var data = store.get('notehistory'); + if (data) { + if (typeof data == "string") + data = JSON.parse(data); + callback(data); + } else + getCookieHistory(callback); + } else { + getCookieHistory(callback); + } +} + +function parseHistory(list, callback) { checkIfAuth( function () { - parseServerToHistory(callback); + parseServerToHistory(list, callback); }, function () { - parseCookieToHistory(callback); + parseStorageToHistory(list, callback); } ); } -function parseServerToHistory(callback) { +function parseServerToHistory(list, callback) { $.get('/history') .done(function (data) { if (data.history) { - //console.log(data.history); - parseToHistory(data.history, callback); + parseToHistory(list, data.history, callback); } }) .fail(function () { - parseCookieToHistory(callback); + parseCookieToHistory(list, callback); }); } -function parseCookieToHistory(callback) { - var notehistory = JSON.parse($.cookie('notehistory')); - parseToHistory(notehistory, callback); +function parseCookieToHistory(list, callback) { + var notehistory = Cookies.getJSON('notehistory'); + parseToHistory(list, notehistory, callback); } -function parseToHistory(notehistory, callback) { - if (notehistory && notehistory.length > 0) { - //console.log(notehistory); +function parseStorageToHistory(list, callback) { + if (store.enabled) { + var data = store.get('notehistory'); + if (data) { + if (typeof data == "string") + data = JSON.parse(data); + parseToHistory(list, data, callback); + } else + parseCookieToHistory(list, callback); + } else { + parseCookieToHistory(list, callback); + } +} + +function parseToHistory(list, notehistory, callback) { + if (!callback) return; + else if (!list || !notehistory) callback(list, notehistory); + else if (notehistory && notehistory.length > 0) { for (var i = 0; i < notehistory.length; i++) { + //parse time to timestamp and fromNow notehistory[i].timestamp = moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a').unix(); notehistory[i].fromNow = moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a').fromNow(); + if (list.get('id', notehistory[i].id).length == 0) + list.add(notehistory[i]); } - $(notehistory).each(function (key, value) { - var close = "
    "; - var text = "

    " + value.text + "

    "; - var timestamp = ""; - var fromNow = " " + value.fromNow + ""; - var time = "" + value.time + ""; - var tags = ""; - if (value.tags) { - var labels = []; - for (var j = 0; j < value.tags.length; j++) - labels.push("" + value.tags[j] + ""); - tags = "

    " + labels.join(" ") + "

    "; - } - var li = "
  • " + close + text + '

    ' + fromNow + '
    ' + timestamp + time + '

    ' + tags + "
  • " - //console.debug(li); - $("#history-list").append(li); - }); } - - var options = { - valueNames: ['text', 'timestamp', 'fromNow', 'time', 'tags'] - }; - var historyList = new List('history', options); - historyList.sort('timestamp', { - order: "desc" - }); - callback(); + callback(list, notehistory); } \ No newline at end of file diff --git a/public/js/index.js b/public/js/index.js index 73b4e59..331251c 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -1,12 +1,10 @@ //constant vars //settings -var debug = false; -var version = '0.2.7'; +var debug = true; +var version = '0.2.8'; var doneTypingDelay = 400; var finishChangeDelay = 400; var cursorActivityDelay = 50; -var syncScrollDelay = 50; -var scrollAnimatePeriod = 100; var cursorAnimatePeriod = 100; var modeType = { edit: {}, @@ -67,15 +65,20 @@ var lastInfo = { }; //editor settings -var editor = CodeMirror.fromTextArea(document.getElementById("textit"), { +var textit = document.getElementById("textit"); +if (!textit) throw new Error("There was no textit area!"); +var editor = CodeMirror.fromTextArea(textit, { mode: 'gfm', + keyMap: "sublime", viewportMargin: 20, styleActiveLine: true, lineNumbers: true, lineWrapping: true, + showCursorWhenSelecting: true, theme: "monokai", autofocus: true, inputStyle: "textarea", + scrollbarStyle: "overlay", matchBrackets: true, autoCloseBrackets: true, matchTags: { @@ -89,6 +92,7 @@ var editor = CodeMirror.fromTextArea(document.getElementById("textit"), { }, readOnly: true }); +inlineAttachment.editors.codemirror4.attach(editor); //ui vars var ui = { @@ -148,23 +152,33 @@ $(document).ready(function () { changeMode(currentMode); /* we need this only on touch devices */ if (isTouchDevice) { - /* cache dom references */ - var $body = jQuery('body'); + /* cache dom references */ + var $body = jQuery('body'); /* bind events */ $(document) - .on('focus', 'textarea, input', function() { - $body.addClass('fixfixed'); - }) - .on('blur', 'textarea, input', function() { - $body.removeClass('fixfixed'); - }); + .on('focus', 'textarea, input', function () { + $body.addClass('fixfixed'); + }) + .on('blur', 'textarea, input', function () { + $body.removeClass('fixfixed'); + }); } }); //when page resize +var windowResizeDelay = 200; +var windowResizeTimer = null; $(window).resize(function () { - checkResponsive(); + clearTimeout(windowResizeTimer); + windowResizeTimer = setTimeout(function () { + windowResize(); + }, windowResizeDelay); }); +function windowResize() { + checkResponsive(); + clearMap(); + syncScrollToView(); +} //768-792px have a gap function checkResponsive() { visibleXS = $(".visible-xs").is(":visible"); @@ -176,6 +190,10 @@ function checkResponsive() { changeMode(modeType.edit); else changeMode(modeType.view); + if (visibleXS) + $('.CodeMirror').css('height', 'auto'); + else + $('.CodeMirror').css('height', ''); } function showStatus(type, num) { @@ -220,7 +238,7 @@ function showStatus(type, num) { } function toggleMode() { - switch(currentMode) { + switch (currentMode) { case modeType.edit: changeMode(modeType.view); break; @@ -297,26 +315,31 @@ var url = window.location.origin + '/' + noteId; ui.toolbar.pretty.attr("href", url + "/pretty"); //download //markdown -ui.toolbar.download.markdown.click(function() { +ui.toolbar.download.markdown.click(function () { var filename = renderFilename(ui.area.markdown) + '.md'; var markdown = editor.getValue(); - var blob = new Blob([markdown], {type: "text/markdown;charset=utf-8"}); + var blob = new Blob([markdown], { + type: "text/markdown;charset=utf-8" + }); saveAs(blob, filename); }); //save to dropbox -ui.toolbar.save.dropbox.click(function() { +ui.toolbar.save.dropbox.click(function () { var filename = renderFilename(ui.area.markdown) + '.md'; var options = { files: [ - {'url': url + "/download", 'filename': filename} + { + 'url': url + "/download", + 'filename': filename + } ] }; Dropbox.save(options); }); //import from dropbox -ui.toolbar.import.dropbox.click(function() { +ui.toolbar.import.dropbox.click(function () { var options = { - success: function(files) { + success: function (files) { ui.spinner.show(); var url = files[0].link; importFromUrl(url); @@ -328,64 +351,73 @@ ui.toolbar.import.dropbox.click(function() { Dropbox.choose(options); }); //import from clipboard -ui.toolbar.import.clipboard.click(function() { +ui.toolbar.import.clipboard.click(function () { //na }); //fix for wrong autofocus -$('#clipboardModal').on('shown.bs.modal', function() { +$('#clipboardModal').on('shown.bs.modal', function () { $('#clipboardModal').blur(); }); -$("#clipboardModalClear").click(function() { +$("#clipboardModalClear").click(function () { $("#clipboardModalContent").html(''); }); -$("#clipboardModalConfirm").click(function() { +$("#clipboardModalConfirm").click(function () { var data = $("#clipboardModalContent").html(); - if(data) { + if (data) { parseToEditor(data); $('#clipboardModal').modal('hide'); $("#clipboardModalContent").html(''); } }); + function parseToEditor(data) { var parsed = toMarkdown(data); - if(parsed) - editor.replaceRange(parsed, {line:0, ch:0}, {line:editor.lastLine(), ch:editor.lastLine().length}, '+input'); + if (parsed) + editor.replaceRange(parsed, { + line: 0, + ch: 0 + }, { + line: editor.lastLine(), + ch: editor.lastLine().length + }, '+input'); } + function importFromUrl(url) { //console.log(url); - if(url == null) return; - if(!isValidURL(url)) { + if (url == null) return; + if (!isValidURL(url)) { alert('Not valid URL :('); return; } $.ajax({ method: "GET", url: url, - success: function(data) { + success: function (data) { parseToEditor(data); }, - error: function() { + error: function () { alert('Import failed :('); }, - complete: function() { + complete: function () { ui.spinner.hide(); } }); } + function isValidURL(str) { - var pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol - '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name - '((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address - '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path - '(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string - '(\\#[-a-z\\d_]*)?$','i'); // fragment locator - if(!pattern.test(str)) { - return false; - } else { - return true; + var pattern = new RegExp('^(https?:\\/\\/)?' + // protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name + '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path + '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string + '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator + if (!pattern.test(str)) { + return false; + } else { + return true; + } } -} -//mode + //mode ui.toolbar.mode.click(function () { toggleMode(); }); @@ -427,7 +459,7 @@ socket.on('version', function (data) { }); socket.on('refresh', function (data) { saveInfo(); - + var body = data.body; body = LZString.decompressFromBase64(body); if (body) @@ -455,7 +487,7 @@ socket.on('refresh', function (data) { if (editor.getOption('readOnly')) editor.setOption('readOnly', false); - + restoreInfo(); }); socket.on('change', function (data) { @@ -470,51 +502,65 @@ socket.on('online users', function (data) { if (debug) console.debug(data); showStatus(statusType.online, data.count); - $('.other-cursors').html(''); - for(var i = 0; i < data.users.length; i++) { + $('.other-cursors').children().each(function (key, value) { + var found = false; + for (var i = 0; i < data.users.length; i++) { + var user = data.users[i]; + if ($(this).attr('id') == user.id) + found = true; + } + if (!found) + $(this).remove(); + }); + for (var i = 0; i < data.users.length; i++) { var user = data.users[i]; - if(user.id != socket.id) + if (user.id != socket.id) buildCursor(user.id, user.color, user.cursor); } }); socket.on('cursor focus', function (data) { - if(debug) + if (debug) console.debug(data); var cursor = $('#' + data.id); - if(cursor.length > 0) { + if (cursor.length > 0) { cursor.fadeIn(); } else { - if(data.id != socket.id) + if (data.id != socket.id) buildCursor(data.id, data.color, data.cursor); } }); socket.on('cursor activity', function (data) { - if(debug) + if (debug) console.debug(data); - if(data.id != socket.id) + if (data.id != socket.id) buildCursor(data.id, data.color, data.cursor); }); socket.on('cursor blur', function (data) { - if(debug) + if (debug) console.debug(data); var cursor = $('#' + data.id); - if(cursor.length > 0) { + if (cursor.length > 0) { cursor.fadeOut(); } }); + function emitUserStatus() { checkIfAuth( function (data) { - socket.emit('user status', {login:true}); + socket.emit('user status', { + login: true + }); }, function () { - socket.emit('user status', {login:false}); + socket.emit('user status', { + login: false + }); } ); } function buildCursor(id, color, pos) { - if(!pos) return; + if (!pos) return; if ($('.other-cursors').length <= 0) { $("
    ").insertAfter('.CodeMirror-cursors'); } @@ -535,7 +581,10 @@ function buildCursor(id, color, pos) { cursor.attr('data-line', pos.line); cursor.attr('data-ch', pos.ch); var coord = editor.charCoords(pos, 'windows'); - cursor.stop(true).css('opacity', 1).animate({"left":coord.left, "top":coord.top}, cursorAnimatePeriod); + cursor.stop(true).css('opacity', 1).animate({ + "left": coord.left, + "top": coord.top + }, cursorAnimatePeriod); //cursor[0].style.left = coord.left + 'px'; //cursor[0].style.top = coord.top + 'px'; cursor[0].style.height = '18px'; @@ -566,6 +615,7 @@ editor.on('cursorActivity', function (cm) { clearTimeout(cursorActivityTimer); cursorActivityTimer = setTimeout(cursorActivity, cursorActivityDelay); }); + function cursorActivity() { socket.emit('cursor activity', editor.getCursor()); } @@ -578,8 +628,9 @@ function saveInfo() { var top = $(document.body).scrollTop(); switch (currentMode) { case modeType.edit: - lastInfo.edit.scroll.left = left; - lastInfo.edit.scroll.top = top; + //lastInfo.edit.scroll.left = left; + //lastInfo.edit.scroll.top = top; + lastInfo.edit.scroll = editor.getScrollInfo(); break; case modeType.view: lastInfo.view.scroll.left = left; @@ -603,8 +654,12 @@ function restoreInfo() { switch (currentMode) { case modeType.edit: - $(document.body).scrollLeft(lastInfo.edit.scroll.left); - $(document.body).scrollTop(lastInfo.edit.scroll.top); + //$(document.body).scrollLeft(lastInfo.edit.scroll.left); + //$(document.body).scrollTop(lastInfo.edit.scroll.top); + var left = lastInfo.edit.scroll.left; + var top = lastInfo.edit.scroll.top; + editor.scrollIntoView(); + editor.scrollTo(left, top); break; case modeType.view: $(document.body).scrollLeft(lastInfo.view.scroll.left); @@ -652,9 +707,8 @@ function updateView() { finishView(ui.area.view); writeHistory(ui.area.markdown); isDirty = false; - // reset lines mapping cache on content update - scrollMap = null; emitUserStatus(); + clearMap(); } function partialUpdate(src, tar, des) { @@ -702,7 +756,7 @@ function partialUpdate(src, tar, des) { } } //tar end - for (var i = 1; i <= tar.length; i++) { + for (var i = 1; i <= tar.length + 1; i++) { var srcLength = src.length; var tarLength = tar.length; copyAttribute(src[srcLength - i], des[srcLength - i], 'data-startline'); @@ -715,7 +769,7 @@ function partialUpdate(src, tar, des) { } } //src end - for (var i = 1; i <= src.length; i++) { + for (var i = 1; i <= src.length + 1; i++) { var srcLength = src.length; var tarLength = tar.length; copyAttribute(src[srcLength - i], des[srcLength - i], 'data-startline'); @@ -730,56 +784,75 @@ function partialUpdate(src, tar, des) { //check if tar end overlap tar start var overlap = 0; for (var i = start; i >= 0; i--) { - var rawTarStart = cloneAndRemoveDataAttr(tar[i-1]); - var rawTarEnd = cloneAndRemoveDataAttr(tar[tarEnd+1+start-i]); - if(rawTarStart && rawTarEnd && rawTarStart.outerHTML == rawTarEnd.outerHTML) + var rawTarStart = cloneAndRemoveDataAttr(tar[i - 1]); + var rawTarEnd = cloneAndRemoveDataAttr(tar[tarEnd + 1 + start - i]); + if (rawTarStart && rawTarEnd && rawTarStart.outerHTML == rawTarEnd.outerHTML) overlap++; else break; } - if(debug) + if (debug) console.log('overlap:' + overlap); //show diff content - if(debug) { + if (debug) { console.log('start:' + start); console.log('tarEnd:' + tarEnd); console.log('srcEnd:' + srcEnd); - console.log('des[start]:' + des[start]); } tarEnd += overlap; srcEnd += overlap; - //add new element - var newElements = ""; - for (var j = start; j <= srcEnd; j++) { - if(debug) - srcChanged += src[j].outerHTML; - newElements += src[j].outerHTML; + var repeatAdd = (start - srcEnd) < (start - tarEnd); + var repeatDiff = Math.abs(srcEnd - tarEnd) - 1; + //push new elements + var newElements = []; + if(srcEnd >= start) { + for (var j = start; j <= srcEnd; j++) { + if (!src[j]) continue; + newElements.push(src[j].outerHTML); + } + } else if(repeatAdd) { + for (var j = srcEnd - repeatDiff; j <= srcEnd; j++) { + if (!des[j]) continue; + newElements.push(des[j].outerHTML); + } } - if(newElements && des[start]) { - $(newElements).insertBefore(des[start]); - } else { - $(newElements).insertAfter(des[des.length-1]); + //push remove elements + var removeElements = []; + if(tarEnd >= start) { + for (var j = start; j <= tarEnd; j++) { + if (!des[j]) continue; + removeElements.push(des[j]); + } + } else if(!repeatAdd) { + for (var j = start; j <= start + repeatDiff; j++) { + if (!des[j]) continue; + removeElements.push(des[j]); + } } - if(debug) - console.log(srcChanged); - //remove old element - if(debug) - var tarChanged = ""; - for (var j = start; j <= tarEnd; j++) { - if(debug) - tarChanged += tar[j].outerHTML; - if(des[j]) - des[j].remove(); + //add elements + if (debug) { + console.log('ADD ELEMENTS'); + console.log(newElements.join('\n')); } - if(debug) { - console.log(tarChanged); - var srcChanged = ""; + if (des[start]) + $(newElements.join('')).insertBefore(des[start]); + else + $(newElements.join('')).insertAfter(des[start - 1]); + //remove elements + if (debug) + console.log('REMOVE ELEMENTS'); + for (var j = 0; j < removeElements.length; j++) { + if (debug) { + console.log(removeElements[j].outerHTML); + } + if (removeElements[j]) + removeElements[j].remove(); } } } function cloneAndRemoveDataAttr(el) { - if(!el) return; + if (!el) return; var rawEl = $(el).clone(true)[0]; rawEl.removeAttribute('data-startline'); rawEl.removeAttribute('data-endline'); @@ -789,152 +862,4 @@ function cloneAndRemoveDataAttr(el) { function copyAttribute(src, des, attr) { if (src && src.getAttribute(attr) && des) des.setAttribute(attr, src.getAttribute(attr)); -} - -// -// Inject line numbers for sync scroll. Notes: -// -// - We track only headings and paragraphs on first level. That's enougth. -// - Footnotes content causes jumps. Level limit filter it automatically. -// -md.renderer.rules.paragraph_open = function (tokens, idx) { - var line; - if (tokens[idx].lines && tokens[idx].level === 0) { - var startline = tokens[idx].lines[0] + 1; - var endline = tokens[idx].lines[1]; - return '

    '; - } - return ''; -}; - -md.renderer.rules.heading_open = function (tokens, idx) { - var line; - if (tokens[idx].lines && tokens[idx].level === 0) { - var startline = tokens[idx].lines[0] + 1; - var endline = tokens[idx].lines[1]; - return ''; - } - return ''; -}; - -editor.on('scroll', _.debounce(syncScrollToView, syncScrollDelay)); -//ui.area.view.on('scroll', _.debounce(syncScrollToEdit, 50)); -var scrollMap; -// Build offsets for each line (lines can be wrapped) -// That's a bit dirty to process each line everytime, but ok for demo. -// Optimizations are required only for big texts. -function buildScrollMap() { - var i, offset, nonEmptyList, pos, a, b, lineHeightMap, linesCount, - acc, sourceLikeDiv, textarea = ui.area.codemirror, - _scrollMap; - - sourceLikeDiv = $('

    ').css({ - position: 'absolute', - visibility: 'hidden', - height: 'auto', - width: editor.getScrollInfo().clientWidth, - 'font-size': textarea.css('font-size'), - 'font-family': textarea.css('font-family'), - 'line-height': textarea.css('line-height'), - 'white-space': textarea.css('white-space') - }).appendTo('body'); - - offset = ui.area.view.scrollTop() - ui.area.view.offset().top; - _scrollMap = []; - nonEmptyList = []; - lineHeightMap = []; - - acc = 0; - editor.getValue().split('\n').forEach(function (str) { - var h, lh; - - lineHeightMap.push(acc); - - if (str.length === 0) { - acc++; - return; - } - - sourceLikeDiv.text(str); - h = parseFloat(sourceLikeDiv.css('height')); - lh = parseFloat(sourceLikeDiv.css('line-height')); - acc += Math.round(h / lh); - }); - sourceLikeDiv.remove(); - lineHeightMap.push(acc); - linesCount = acc; - - for (i = 0; i < linesCount; i++) { - _scrollMap.push(-1); - } - - nonEmptyList.push(0); - _scrollMap[0] = 0; - - ui.area.markdown.find('.part').each(function (n, el) { - var $el = $(el), - t = $el.data('startline'); - if (t === '') { - return; - } - t = lineHeightMap[t]; - if (t !== 0) { - nonEmptyList.push(t); - } - _scrollMap[t] = Math.round($el.offset().top + offset); - }); - - nonEmptyList.push(linesCount); - _scrollMap[linesCount] = ui.area.view[0].scrollHeight; - - pos = 0; - for (i = 1; i < linesCount; i++) { - if (_scrollMap[i] !== -1) { - pos++; - continue; - } - - a = nonEmptyList[pos]; - b = nonEmptyList[pos + 1]; - _scrollMap[i] = Math.round((_scrollMap[b] * (i - a) + _scrollMap[a] * (b - i)) / (b - a)); - } - - return _scrollMap; -} - -function syncScrollToView() { - var lineNo, posTo; - var scrollInfo = editor.getScrollInfo(); - if (!scrollMap) { - scrollMap = buildScrollMap(); - } - lineNo = Math.floor(scrollInfo.top / editor.defaultTextHeight()); - posTo = scrollMap[lineNo]; - ui.area.view.stop(true).animate({scrollTop: posTo}, scrollAnimatePeriod); -} - -function syncScrollToEdit() { - var lineNo, posTo; - if (!scrollMap) { - scrollMap = buildScrollMap(); - } - var top = ui.area.view.scrollTop(); - lineNo = closestIndex(top, scrollMap); - posTo = lineNo * editor.defaultTextHeight(); - editor.scrollTo(0, posTo); -} - -function closestIndex(num, arr) { - var curr = arr[0]; - var index = 0; - var diff = Math.abs(num - curr); - for (var val = 0; val < arr.length; val++) { - var newdiff = Math.abs(num - arr[val]); - if (newdiff < diff) { - diff = newdiff; - curr = arr[val]; - index = val; - } - } - return index; } \ No newline at end of file diff --git a/public/js/pretty.js b/public/js/pretty.js new file mode 100644 index 0000000..33b9780 --- /dev/null +++ b/public/js/pretty.js @@ -0,0 +1,9 @@ +var raw = $(".markdown-body").text(); +var markdown = LZString.decompressFromBase64(raw); +var result = postProcess(md.render(markdown)); +var markdown = $(".markdown-body"); +markdown.html(result); +markdown.show(); +finishView(markdown); +autoLinkify(markdown); +scrollToHash(); \ No newline at end of file diff --git a/public/js/syncscroll.js b/public/js/syncscroll.js new file mode 100644 index 0000000..3d97324 --- /dev/null +++ b/public/js/syncscroll.js @@ -0,0 +1,327 @@ +// +// Inject line numbers for sync scroll. Notes: +// +// - We track only headings and paragraphs on first level. That's enougth. +// - Footnotes content causes jumps. Level limit filter it automatically. +// +md.renderer.rules.blockquote_open = function (tokens, idx /*, options, env */ ) { + if (tokens[idx].lines && tokens[idx].level === 0) { + var startline = tokens[idx].lines[0] + 1; + var endline = tokens[idx].lines[1]; + return '
    \n'; + } + return '
    \n'; +}; + +md.renderer.rules.table_open = function (tokens, idx /*, options, env */ ) { + if (tokens[idx].lines && tokens[idx].level === 0) { + var startline = tokens[idx].lines[0] + 1; + var endline = tokens[idx].lines[1]; + return '\n'; + } + return '
    \n'; +}; + +md.renderer.rules.bullet_list_open = function (tokens, idx /*, options, env */ ) { + if (tokens[idx].lines && tokens[idx].level === 0) { + var startline = tokens[idx].lines[0] + 1; + var endline = tokens[idx].lines[1]; + return '