Marked as 0.2.9
This commit is contained in:
parent
4e64583a0b
commit
f7f8c901f4
33 changed files with 2972 additions and 242 deletions
45
README.md
45
README.md
|
@ -1,11 +1,26 @@
|
|||
HackMD 0.2.8
|
||||
HackMD 0.2.9
|
||||
===
|
||||
|
||||
This is a realtime collaborative markdown notes on all platforms.
|
||||
But still in early stage, feel free to fork or contribute to it.
|
||||
HackMD is a realtime collaborative markdown notes on all platforms.
|
||||
Inspired by Hackpad, but more focusing on speed and flexibility.
|
||||
Still in early stage, feel free to fork or contribute to this.
|
||||
|
||||
Thanks for your using!
|
||||
Thanks for your using! :smile:
|
||||
|
||||
Dependency
|
||||
---
|
||||
- PostgreSQL 9.3.6 or 9.4.1
|
||||
- MongoDB 3.0.2
|
||||
|
||||
Import db schema
|
||||
---
|
||||
The notes are store in PostgreSQL, the schema is in the `hackmd_schema.sql`
|
||||
To import the sql file in PostgreSQL, type `psql -i hackmd_schema.sql`
|
||||
|
||||
The users, temps and sessions are store in MongoDB, which don't need schema, so just make sure you have the correct connection string.
|
||||
|
||||
Config
|
||||
---
|
||||
There are some config you need to change in below files
|
||||
```
|
||||
./run.sh
|
||||
|
@ -13,13 +28,25 @@ There are some config you need to change in below files
|
|||
./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.
|
||||
The script `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.
|
||||
You can use SSL to encrypt your site by passing certificate path in the `config.js` and set `usessl=true`
|
||||
|
||||
Run a server
|
||||
---
|
||||
To run the server, type `bash run.sh`
|
||||
Log will be at `~/.forever/hackmd.log`
|
||||
|
||||
Stop a server
|
||||
---
|
||||
To stop the server, simply type `forever stop hackmd`
|
||||
|
||||
Backup db
|
||||
---
|
||||
To backup the db, type `bash backup.sh`
|
||||
Backup files will be at `./backups/`
|
||||
|
||||
|
||||
**License under MIT.**
|
40
app.js
40
app.js
|
@ -5,6 +5,7 @@ var toobusy = require('toobusy-js');
|
|||
var ejs = require('ejs');
|
||||
var passport = require('passport');
|
||||
var methodOverride = require('method-override');
|
||||
var cookieParser = require('cookie-parser');
|
||||
var bodyParser = require('body-parser');
|
||||
var mongoose = require('mongoose');
|
||||
var compression = require('compression')
|
||||
|
@ -14,9 +15,12 @@ var fs = require('fs');
|
|||
var shortid = require('shortid');
|
||||
var imgur = require('imgur');
|
||||
var formidable = require('formidable');
|
||||
var morgan = require('morgan');
|
||||
var passportSocketIo = require("passport.socketio");
|
||||
|
||||
//core
|
||||
var config = require("./config.js");
|
||||
var logger = require("./lib/logger.js");
|
||||
var User = require("./lib/user.js");
|
||||
var Temp = require("./lib/temp.js");
|
||||
var auth = require("./lib/auth.js");
|
||||
|
@ -45,7 +49,12 @@ if (config.usessl) {
|
|||
var app = express();
|
||||
var server = require('http').createServer(app);
|
||||
}
|
||||
//socket io listen
|
||||
var io = require('socket.io').listen(server);
|
||||
//logger
|
||||
app.use(morgan('combined', {
|
||||
"stream": logger.stream
|
||||
}));
|
||||
|
||||
// connect to the mongodb
|
||||
mongoose.connect(process.env.MONGOLAB_URI || config.mongodbstring);
|
||||
|
@ -65,6 +74,15 @@ var urlencodedParser = bodyParser.urlencoded({
|
|||
extended: false
|
||||
});
|
||||
|
||||
//session store
|
||||
var sessionStore = new MongoStore({
|
||||
mongooseConnection: mongoose.connection,
|
||||
touchAfter: config.sessiontouch
|
||||
},
|
||||
function (err) {
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
//compression
|
||||
app.use(compression());
|
||||
|
||||
|
@ -79,13 +97,7 @@ app.use(session({
|
|||
expires: new Date(Date.now() + config.sessionlife),
|
||||
},
|
||||
maxAge: new Date(Date.now() + config.sessionlife),
|
||||
store: new MongoStore({
|
||||
mongooseConnection: mongoose.connection,
|
||||
touchAfter: config.sessiontouch
|
||||
},
|
||||
function (err) {
|
||||
console.log(err);
|
||||
})
|
||||
store: sessionStore
|
||||
}));
|
||||
|
||||
//middleware which blocks requests when we're too busy
|
||||
|
@ -293,6 +305,7 @@ app.get('/me', function (req, res) {
|
|||
var profile = JSON.parse(user.profile);
|
||||
res.send({
|
||||
status: 'ok',
|
||||
id: req.session.passport.user,
|
||||
name: profile.displayName || profile.username
|
||||
});
|
||||
}
|
||||
|
@ -317,7 +330,9 @@ app.post('/uploadimage', function (req, res) {
|
|||
.then(function (json) {
|
||||
if (config.debug)
|
||||
console.log('SERVER uploadimage success: ' + JSON.stringify(json));
|
||||
res.send({link:json.data.link});
|
||||
res.send({
|
||||
link: json.data.link
|
||||
});
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error(err);
|
||||
|
@ -337,6 +352,15 @@ app.get("/:noteId/:action", response.noteActions);
|
|||
|
||||
//socket.io secure
|
||||
io.use(realtime.secure);
|
||||
//socket.io auth
|
||||
io.use(passportSocketIo.authorize({
|
||||
cookieParser: cookieParser,
|
||||
key: config.sessionname,
|
||||
secret: config.sessionsecret,
|
||||
store: sessionStore,
|
||||
success: realtime.onAuthorizeSuccess,
|
||||
fail: realtime.onAuthorizeFail
|
||||
}));
|
||||
//socket.io heartbeat
|
||||
io.set('heartbeat interval', config.heartbeatinterval);
|
||||
io.set('heartbeat timeout', config.heartbeattimeout);
|
||||
|
|
7
backup.sh
Normal file
7
backup.sh
Normal file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
path=./backups
|
||||
today=$(date +"%Y%m%d")
|
||||
timestamp=$(date +"%Y%m%d%H%M%S")
|
||||
mkdir -p $path/$today
|
||||
pg_dump hackmd > $path/$today/postgresql_$timestamp
|
||||
mongodump -d hackmd -o $path/$today/mongodb_$timestamp
|
21
lib/logger.js
Normal file
21
lib/logger.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
var winston = require('winston');
|
||||
winston.emitErrs = true;
|
||||
|
||||
var logger = new winston.Logger({
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
level: 'debug',
|
||||
handleExceptions: true,
|
||||
json: false,
|
||||
colorize: true
|
||||
})
|
||||
],
|
||||
exitOnError: false
|
||||
});
|
||||
|
||||
module.exports = logger;
|
||||
module.exports.stream = {
|
||||
write: function(message, encoding){
|
||||
logger.info(message);
|
||||
}
|
||||
};
|
193
lib/realtime.js
193
lib/realtime.js
|
@ -7,6 +7,8 @@ var async = require('async');
|
|||
var LZString = require('lz-string');
|
||||
var shortId = require('shortid');
|
||||
var randomcolor = require("randomcolor");
|
||||
var Chance = require('chance'),
|
||||
chance = new Chance();
|
||||
|
||||
//core
|
||||
var config = require("../config.js");
|
||||
|
@ -18,11 +20,22 @@ var User = require("./user.js");
|
|||
|
||||
//public
|
||||
var realtime = {
|
||||
onAuthorizeSuccess: onAuthorizeSuccess,
|
||||
onAuthorizeFail: onAuthorizeFail,
|
||||
secure: secure,
|
||||
connection: connection,
|
||||
getStatus: getStatus
|
||||
};
|
||||
|
||||
function onAuthorizeSuccess(data, accept) {
|
||||
accept(null, true);
|
||||
}
|
||||
|
||||
function onAuthorizeFail(data, message, error, accept) {
|
||||
if (error) throw new Error(message);
|
||||
accept(null, true);
|
||||
}
|
||||
|
||||
function secure(socket, next) {
|
||||
try {
|
||||
var handshakeData = socket.request;
|
||||
|
@ -53,8 +66,10 @@ var updater = setInterval(function () {
|
|||
if (note.isDirty) {
|
||||
if (config.debug)
|
||||
console.log("updater found dirty note: " + key);
|
||||
var title = Note.getNoteTitle(LZString.decompressFromBase64(note.body));
|
||||
db.saveToDB(key, title, note.body,
|
||||
var body = LZString.decompressFromUTF16(note.body);
|
||||
var title = Note.getNoteTitle(body);
|
||||
body = LZString.compressToBase64(body);
|
||||
db.saveToDB(key, title, body,
|
||||
function (err, result) {});
|
||||
note.isDirty = false;
|
||||
}
|
||||
|
@ -72,7 +87,7 @@ function getStatus(callback) {
|
|||
var distinctaddresses = [];
|
||||
Object.keys(users).forEach(function (key) {
|
||||
var value = users[key];
|
||||
if(value.login)
|
||||
if (value.login)
|
||||
regusers++;
|
||||
var found = false;
|
||||
for (var i = 0; i < distinctaddresses.length; i++) {
|
||||
|
@ -83,9 +98,9 @@ function getStatus(callback) {
|
|||
}
|
||||
if (!found) {
|
||||
distinctaddresses.push(value.address);
|
||||
if(value.login)
|
||||
if (value.login)
|
||||
distinctregusers++;
|
||||
}
|
||||
}
|
||||
});
|
||||
User.getUserCount(function (err, regcount) {
|
||||
if (err) {
|
||||
|
@ -129,17 +144,25 @@ function emitOnlineUsers(socket) {
|
|||
Object.keys(notes[notename].users).forEach(function (key) {
|
||||
var user = notes[notename].users[key];
|
||||
if (user)
|
||||
users.push({
|
||||
id: user.id,
|
||||
color: user.color,
|
||||
cursor: user.cursor
|
||||
});
|
||||
users.push(buildUserOutData(user));
|
||||
});
|
||||
notes[notename].socks.forEach(function (sock) {
|
||||
sock.emit('online users', {
|
||||
count: notes[notename].socks.length,
|
||||
var out = {
|
||||
users: users
|
||||
});
|
||||
};
|
||||
out = LZString.compressToUTF16(JSON.stringify(out));
|
||||
sock.emit('online users', out);
|
||||
});
|
||||
}
|
||||
|
||||
function emitUserStatus(socket) {
|
||||
var notename = getNotenameFromSocket(socket);
|
||||
if (!notename || !notes[notename]) return;
|
||||
notes[notename].socks.forEach(function (sock) {
|
||||
if (sock != socket) {
|
||||
var out = buildUserOutData(users[socket.id]);
|
||||
sock.emit('user status', out);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -198,7 +221,8 @@ function startConnection(socket) {
|
|||
isConnectionBusy = false;
|
||||
return console.error(err);
|
||||
}
|
||||
var body = data.rows[0].content;
|
||||
var body = LZString.decompressFromBase64(data.rows[0].content);
|
||||
body = LZString.compressToUTF16(body);
|
||||
notes[notename] = {
|
||||
socks: [],
|
||||
body: body,
|
||||
|
@ -232,8 +256,10 @@ function disconnect(socket) {
|
|||
notes[notename].socks.splice(index, 1);
|
||||
}
|
||||
if (Object.keys(notes[notename].users).length <= 0) {
|
||||
var title = Note.getNoteTitle(LZString.decompressFromBase64(notes[notename].body));
|
||||
db.saveToDB(notename, title, notes[notename].body,
|
||||
var body = LZString.decompressFromUTF16(notes[notename].body);
|
||||
var title = Note.getNoteTitle(body);
|
||||
body = LZString.compressToBase64(body);
|
||||
db.saveToDB(notename, title, body,
|
||||
function (err, result) {
|
||||
delete notes[notename];
|
||||
if (config.debug) {
|
||||
|
@ -265,20 +291,80 @@ function disconnect(socket) {
|
|||
}
|
||||
}
|
||||
|
||||
function buildUserOutData(user) {
|
||||
var out = {
|
||||
id: user.id,
|
||||
login: user.login,
|
||||
userid: user.userid,
|
||||
color: user.color,
|
||||
cursor: user.cursor,
|
||||
name: user.name,
|
||||
idle: user.idle,
|
||||
type: user.type
|
||||
};
|
||||
return out;
|
||||
}
|
||||
|
||||
function updateUserData(socket, user) {
|
||||
//retrieve user data from passport
|
||||
if (socket.request.user && socket.request.user.logged_in) {
|
||||
var profile = JSON.parse(socket.request.user.profile);
|
||||
user.name = profile.displayName || profile.username;
|
||||
user.userid = socket.request.user._id;
|
||||
user.login = true;
|
||||
} else {
|
||||
user.userid = null;
|
||||
user.name = 'Guest ' + chance.last();
|
||||
user.login = false;
|
||||
}
|
||||
}
|
||||
|
||||
function connection(socket) {
|
||||
//split notename from socket
|
||||
var notename = getNotenameFromSocket(socket);
|
||||
|
||||
//initialize user data
|
||||
//random color
|
||||
var color = randomcolor({
|
||||
luminosity: 'light'
|
||||
});
|
||||
//make sure color not duplicated or reach max random count
|
||||
if (notename && notes[notename]) {
|
||||
var randomcount = 0;
|
||||
var maxrandomcount = 5;
|
||||
var found = false;
|
||||
do {
|
||||
Object.keys(notes[notename].users).forEach(function (user) {
|
||||
if (user.color == color) {
|
||||
found = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
if (found) {
|
||||
color = randomcolor({
|
||||
luminosity: 'light'
|
||||
});
|
||||
randomcount++;
|
||||
}
|
||||
} while (found && randomcount < maxrandomcount);
|
||||
}
|
||||
//create user data
|
||||
users[socket.id] = {
|
||||
id: socket.id,
|
||||
address: socket.handshake.address,
|
||||
'user-agent': socket.handshake.headers['user-agent'],
|
||||
otk: shortId.generate(),
|
||||
color: randomcolor({
|
||||
luminosity: 'light'
|
||||
}),
|
||||
color: color,
|
||||
cursor: null,
|
||||
login: false
|
||||
login: false,
|
||||
userid: null,
|
||||
name: null,
|
||||
idle: false,
|
||||
type: null
|
||||
};
|
||||
updateUserData(socket, users[socket.id]);
|
||||
|
||||
//start connection
|
||||
connectionSocketQueue.push(socket);
|
||||
startConnection(socket);
|
||||
|
||||
|
@ -293,61 +379,88 @@ function connection(socket) {
|
|||
notes[notename].isDirty = true;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
//received user status
|
||||
socket.on('user status', function (data) {
|
||||
if(data)
|
||||
users[socket.id].login = data.login;
|
||||
var notename = getNotenameFromSocket(socket);
|
||||
if (!notename) return;
|
||||
if (config.debug)
|
||||
console.log('SERVER received [' + notename + '] user status from [' + socket.id + ']: ' + JSON.stringify(data));
|
||||
if (data) {
|
||||
var user = users[socket.id];
|
||||
user.idle = data.idle;
|
||||
user.type = data.type;
|
||||
}
|
||||
emitUserStatus(socket);
|
||||
});
|
||||
|
||||
socket.on('online users', function () {
|
||||
//reveiced when user logout or changed
|
||||
socket.on('user changed', function () {
|
||||
console.log('user changed');
|
||||
var notename = getNotenameFromSocket(socket);
|
||||
if (!notename || !notes[notename]) return;
|
||||
updateUserData(socket, notes[notename].users[socket.id]);
|
||||
emitOnlineUsers(socket);
|
||||
});
|
||||
|
||||
//received sync of online users request
|
||||
socket.on('online users', function () {
|
||||
var notename = getNotenameFromSocket(socket);
|
||||
if (!notename || !notes[notename]) return;
|
||||
var users = [];
|
||||
Object.keys(notes[notename].users).forEach(function (key) {
|
||||
var user = notes[notename].users[key];
|
||||
if (user)
|
||||
users.push(buildUserOutData(user));
|
||||
});
|
||||
var out = {
|
||||
users: users
|
||||
};
|
||||
out = LZString.compressToUTF16(JSON.stringify(out));
|
||||
socket.emit('online users', out);
|
||||
});
|
||||
|
||||
//check version
|
||||
socket.on('version', function () {
|
||||
socket.emit('version', config.version);
|
||||
});
|
||||
|
||||
//received cursor focus
|
||||
socket.on('cursor focus', function (data) {
|
||||
var notename = getNotenameFromSocket(socket);
|
||||
if (!notename || !notes[notename]) return;
|
||||
users[socket.id].cursor = data;
|
||||
var out = buildUserOutData(users[socket.id]);
|
||||
notes[notename].socks.forEach(function (sock) {
|
||||
if (sock != socket) {
|
||||
var out = {
|
||||
id: socket.id,
|
||||
color: users[socket.id].color,
|
||||
cursor: data
|
||||
};
|
||||
sock.emit('cursor focus', out);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//received cursor activity
|
||||
socket.on('cursor activity', function (data) {
|
||||
var notename = getNotenameFromSocket(socket);
|
||||
if (!notename || !notes[notename]) return;
|
||||
users[socket.id].cursor = data;
|
||||
var out = buildUserOutData(users[socket.id]);
|
||||
notes[notename].socks.forEach(function (sock) {
|
||||
if (sock != socket) {
|
||||
var out = {
|
||||
id: socket.id,
|
||||
color: users[socket.id].color,
|
||||
cursor: data
|
||||
};
|
||||
sock.emit('cursor activity', out);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//received cursor blur
|
||||
socket.on('cursor blur', function () {
|
||||
var notename = getNotenameFromSocket(socket);
|
||||
if (!notename || !notes[notename]) return;
|
||||
users[socket.id].cursor = null;
|
||||
var out = {
|
||||
id: socket.id
|
||||
};
|
||||
notes[notename].socks.forEach(function (sock) {
|
||||
if (sock != socket) {
|
||||
var out = {
|
||||
id: socket.id
|
||||
};
|
||||
if (sock != socket) {
|
||||
sock.emit('cursor blur', out);
|
||||
}
|
||||
|
@ -365,7 +478,7 @@ function connection(socket) {
|
|||
socket.on('change', function (op) {
|
||||
var notename = getNotenameFromSocket(socket);
|
||||
if (!notename) return;
|
||||
op = LZString.decompressFromBase64(op);
|
||||
op = LZString.decompressFromUTF16(op);
|
||||
if (op)
|
||||
op = JSON.parse(op);
|
||||
if (config.debug)
|
||||
|
@ -390,10 +503,12 @@ function connection(socket) {
|
|||
if (sock != socket) {
|
||||
if (config.debug)
|
||||
console.log('SERVER emit sync data out [' + notename + ']: ' + sock.id + ', op:' + JSON.stringify(op));
|
||||
sock.emit('change', LZString.compressToBase64(JSON.stringify(op)));
|
||||
sock.emit('change', LZString.compressToUTF16(JSON.stringify(op)));
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
console.log('SERVER received uncaught [' + notename + '] data changed: ' + socket.id + ', op:' + JSON.stringify(op));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "hackmd",
|
||||
"version": "0.2.8",
|
||||
"version": "0.2.9",
|
||||
"description": "Realtime collaborative markdown notes on all platforms.",
|
||||
"main": "app.js",
|
||||
"author": "jackycute",
|
||||
|
@ -9,6 +9,7 @@
|
|||
"dependencies": {
|
||||
"async": "^0.9.0",
|
||||
"body-parser": "^1.12.3",
|
||||
"chance": "^0.7.5",
|
||||
"cheerio": "^0.19.0",
|
||||
"compression": "^1.4.3",
|
||||
"connect-mongo": "^0.8.1",
|
||||
|
@ -27,17 +28,20 @@
|
|||
"marked": "^0.3.3",
|
||||
"method-override": "^2.3.2",
|
||||
"mongoose": "^4.0.2",
|
||||
"morgan": "^1.5.3",
|
||||
"node-uuid": "^1.4.3",
|
||||
"passport": "^0.2.1",
|
||||
"passport-dropbox-oauth2": "^0.1.6",
|
||||
"passport-facebook": "^2.0.0",
|
||||
"passport-github": "^0.1.5",
|
||||
"passport-twitter": "^1.0.3",
|
||||
"passport.socketio": "^3.5.1",
|
||||
"pg": "4.x",
|
||||
"randomcolor": "^0.2.0",
|
||||
"shortid": "2.1.3",
|
||||
"socket.io": "1.3.5",
|
||||
"toobusy-js": "^0.4.1"
|
||||
"toobusy-js": "^0.4.1",
|
||||
"winston": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "0.10.x"
|
||||
|
|
|
@ -20,7 +20,6 @@ form,
|
|||
font-family: 'Source Code Pro', Consolas, monaco, monospace;
|
||||
line-height: 18px;
|
||||
font-size: 16px;
|
||||
/*height: auto;*/
|
||||
min-height: 100%;
|
||||
overflow-y: hidden !important;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
@ -30,7 +29,7 @@ form,
|
|||
overflow-y: auto !important;
|
||||
}
|
||||
.CodeMirror-code {
|
||||
/*padding-bottom: 72px;*/
|
||||
padding-bottom: 36px;
|
||||
}
|
||||
.CodeMirror-linenumber {
|
||||
opacity: 0.5;
|
||||
|
@ -73,23 +72,82 @@ form,
|
|||
font-size: 14px;
|
||||
}
|
||||
.nav-mobile {
|
||||
position: relative;
|
||||
position: inherit;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.nav-mobile .dropdown-menu {
|
||||
left: 40%;
|
||||
right: 6px;
|
||||
top: 42px;
|
||||
}
|
||||
.nav-status {
|
||||
float: right !important;
|
||||
padding: 7px 8px;
|
||||
}
|
||||
.ui-status {
|
||||
cursor: auto !important;
|
||||
min-width: 120px;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.ui-status span {
|
||||
cursor: pointer;
|
||||
}
|
||||
.ui-short-status {
|
||||
cursor: pointer;
|
||||
min-width: 40px;
|
||||
}
|
||||
.ui-short-status:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.ui-user-item {
|
||||
/*na*/
|
||||
}
|
||||
.ui-user-name {
|
||||
margin-top: 2px;
|
||||
}
|
||||
.ui-user-icon {
|
||||
font-size: 20px;
|
||||
margin-top: 2px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.ui-user-status {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.ui-user-status-online {
|
||||
color: rgb(92,184,92);
|
||||
}
|
||||
.ui-user-status-idle {
|
||||
color: rgb(240,173,78);
|
||||
}
|
||||
.ui-user-status-offline {
|
||||
color: rgb(119,119,119);
|
||||
}
|
||||
.list > li > a {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
#short-online-user-list .list .name {
|
||||
max-width: 65%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
float: left;
|
||||
}
|
||||
#online-user-list .list .name {
|
||||
max-width: 110px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
float: left;
|
||||
}
|
||||
.navbar-right {
|
||||
margin-right: 0;
|
||||
}
|
||||
.navbar-nav > li > a {
|
||||
cursor: pointer;
|
||||
}
|
||||
.dropdown-menu > li > a {
|
||||
cursor: pointer;
|
||||
}
|
||||
.other-cursors {
|
||||
position:relative;
|
||||
z-index:3;
|
||||
|
@ -99,10 +157,63 @@ form,
|
|||
position: absolute;
|
||||
border-right: none;
|
||||
}
|
||||
.cursortag {
|
||||
cursor: pointer;
|
||||
background: black;
|
||||
position: absolute;
|
||||
padding: 2px 7px 2px 8px;
|
||||
font-size: 12px;
|
||||
max-width: 150px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-family: inherit;
|
||||
border-radius: .25em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.fixfixed .navbar-fixed-top {
|
||||
position: absolute !important;
|
||||
}
|
||||
div[contenteditable]:empty:not(:focus):before{
|
||||
content:attr(data-ph);
|
||||
color: gray;
|
||||
}
|
||||
.dropdown-menu {
|
||||
max-height: 80vh;
|
||||
overflow: auto;
|
||||
}
|
||||
.dropdown-menu::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.dropdown-menu .emoji {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.dropdown-menu.other-cursor {
|
||||
width: auto !important;
|
||||
}
|
||||
.CodeMirror-scrollbar-filler {
|
||||
background: inherit;
|
||||
}
|
||||
.unselectable {
|
||||
-moz-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.cm-trailing-space-a:before,
|
||||
.cm-trailing-space-b:before,
|
||||
.cm-trailing-space-new-line:before {
|
||||
font-weight: bold;
|
||||
color: hsl(30, 100%, 50%); /* a dark orange */
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.cm-trailing-space-a:before,
|
||||
.cm-trailing-space-b:before {
|
||||
content: '·';
|
||||
}
|
||||
|
||||
.cm-trailing-space-new-line:before {
|
||||
content: '↵';
|
||||
}
|
|
@ -81,8 +81,8 @@
|
|||
</div>
|
||||
<hr>
|
||||
<form class="form-inline">
|
||||
<div class="form-group">
|
||||
<input class="form-control ui-use-tags" style="width:172px;" />
|
||||
<div class="form-group" style="vertical-align: bottom;">
|
||||
<input class="form-control ui-use-tags" style="min-width:172px;max-width:344px;" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input class="search form-control" placeholder="Search anything..." />
|
||||
|
@ -188,6 +188,8 @@
|
|||
<!-- Placed at the end of the document so the pages load faster -->
|
||||
<script src="/js/fb.js" async defer></script>
|
||||
<script src="//code.jquery.com/jquery-1.11.3.min.js" defer></script>
|
||||
<script src="/vendor/greensock-js/TweenMax.min.js" defer></script>
|
||||
<script src="/vendor/greensock-js/jquery.gsap.min.js" defer></script>
|
||||
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js" defer></script>
|
||||
<script src="/vendor/select2/select2.min.js" defer></script>
|
||||
<script src="/vendor/js.cookie.js" defer></script>
|
||||
|
|
|
@ -3,38 +3,62 @@ var domain = 'change this';
|
|||
var checkAuth = false;
|
||||
var profile = null;
|
||||
var lastLoginState = getLoginState();
|
||||
var lastUserId = getUserId();
|
||||
var loginStateChangeEvent = null;
|
||||
|
||||
function resetCheckAuth() {
|
||||
checkAuth = false;
|
||||
}
|
||||
|
||||
function setLoginState(bool) {
|
||||
function setLoginState(bool, id) {
|
||||
Cookies.set('loginstate', bool, {
|
||||
expires: 14
|
||||
});
|
||||
if (loginStateChangeEvent && bool != lastLoginState)
|
||||
loginStateChangeEvent();
|
||||
if (id) {
|
||||
Cookies.set('userid', id, {
|
||||
expires: 14
|
||||
});
|
||||
} else {
|
||||
Cookies.remove('userid');
|
||||
}
|
||||
lastLoginState = bool;
|
||||
lastUserId = id;
|
||||
checkLoginStateChanged();
|
||||
}
|
||||
|
||||
function checkLoginStateChanged() {
|
||||
if (getLoginState() != lastLoginState || getUserId() != lastUserId) {
|
||||
if(loginStateChangeEvent)
|
||||
loginStateChangeEvent();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getLoginState() {
|
||||
return Cookies.get('loginstate') === "true";
|
||||
}
|
||||
|
||||
function getUserId() {
|
||||
return Cookies.get('userid');
|
||||
}
|
||||
|
||||
function clearLoginState() {
|
||||
Cookies.remove('loginstate');
|
||||
}
|
||||
|
||||
function checkIfAuth(yesCallback, noCallback) {
|
||||
var cookieLoginState = getLoginState();
|
||||
if (checkLoginStateChanged())
|
||||
checkAuth = false;
|
||||
if (!checkAuth || typeof cookieLoginState == 'undefined') {
|
||||
$.get('/me')
|
||||
.done(function (data) {
|
||||
if (data && data.status == 'ok') {
|
||||
profile = data;
|
||||
yesCallback(profile);
|
||||
setLoginState(true);
|
||||
setLoginState(true, data.id);
|
||||
} else {
|
||||
noCallback();
|
||||
setLoginState(false);
|
||||
|
@ -43,8 +67,10 @@ function checkIfAuth(yesCallback, noCallback) {
|
|||
.fail(function () {
|
||||
noCallback();
|
||||
setLoginState(false);
|
||||
})
|
||||
.always(function () {
|
||||
checkAuth = true;
|
||||
});
|
||||
checkAuth = true;
|
||||
} else if (cookieLoginState) {
|
||||
yesCallback(profile);
|
||||
} else {
|
||||
|
|
|
@ -229,6 +229,44 @@ var source = $("#template").html();
|
|||
var template = Handlebars.compile(source);
|
||||
var context = {
|
||||
release: [
|
||||
{
|
||||
version: "0.2.9",
|
||||
tag: "wildfire",
|
||||
date: moment("201505301400", 'YYYYMMDDhhmm').fromNow(),
|
||||
detail: [
|
||||
{
|
||||
title: "Features",
|
||||
item: [
|
||||
"+ Support text auto complete",
|
||||
"+ Support cursor tag and random last name",
|
||||
"+ Support online user list",
|
||||
"+ Support show user info in blockquote"
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Enhancements",
|
||||
item: [
|
||||
"* Added more code highlighting support",
|
||||
"* Added more continue list support",
|
||||
"* Adjust menu and history filter UI for better UX",
|
||||
"* Adjust sync scoll animte to gain performance",
|
||||
"* Change compression method of dynamic data",
|
||||
"* Optimized render script"
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Fixes",
|
||||
item: [
|
||||
"* Access history fallback might get wrong",
|
||||
"* Sync scroll not accurate",
|
||||
"* Sync scroll reach bottom range too much",
|
||||
"* Detect login state change not accurate",
|
||||
"* Detect editor focus not accurate",
|
||||
"* Server not handle some editor events"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "0.2.8",
|
||||
tag: "flame",
|
||||
|
|
|
@ -65,6 +65,7 @@ function finishView(view) {
|
|||
try {
|
||||
for (var i = 0; i < mathjaxdivs.length; i++) {
|
||||
MathJax.Hub.Queue(["Typeset", MathJax.Hub, mathjaxdivs[i].innerHTML]);
|
||||
MathJax.Hub.Queue(viewAjaxCallback);
|
||||
$(mathjaxdivs[i]).removeClass("mathjax");
|
||||
}
|
||||
} catch(err) {
|
||||
|
@ -101,6 +102,18 @@ function finishView(view) {
|
|||
//render title
|
||||
document.title = renderTitle(view);
|
||||
}
|
||||
|
||||
//regex for blockquote
|
||||
var spaceregex = /\s*/;
|
||||
var notinhtmltagregex = /(?![^<]*>|[^<>]*<\/)/;
|
||||
var coloregex = /\[color=([#|\(|\)|\s|\,|\w]*)\]/;
|
||||
coloregex = new RegExp(coloregex.source + notinhtmltagregex.source, "g");
|
||||
var nameregex = /\[name=([-|_|\s|\w]*)\]/;
|
||||
var timeregex = /\[time=([:|,|+|-|\(|\)|\s|\w]*)\]/;
|
||||
var nameandtimeregex = new RegExp(nameregex.source + spaceregex.source + timeregex.source + notinhtmltagregex.source, "g");
|
||||
nameregex = new RegExp(nameregex.source + notinhtmltagregex.source, "g");
|
||||
timeregex = new RegExp(timeregex.source + notinhtmltagregex.source, "g");
|
||||
|
||||
//only static transform should be here
|
||||
function postProcess(code) {
|
||||
var result = $('<div>' + code + '</div>');
|
||||
|
@ -121,6 +134,20 @@ function postProcess(code) {
|
|||
lis[i].setAttribute('class', 'task-list-item');
|
||||
}
|
||||
}
|
||||
//blockquote
|
||||
var blockquote = result.find("blockquote");
|
||||
blockquote.each(function (key, value) {
|
||||
var html = $(value).html();
|
||||
html = html.replace(coloregex, '<span class="color" data-color="$1"></span>');
|
||||
html = html.replace(nameandtimeregex, '<small><i class="fa fa-user"></i> $1 <i class="fa fa-clock-o"></i> $2</small>');
|
||||
html = html.replace(nameregex, '<small><i class="fa fa-user"></i> $1</small>');
|
||||
html = html.replace(timeregex, '<small><i class="fa fa-clock-o"></i> $1</small>');
|
||||
$(value).html(html);
|
||||
});
|
||||
var blockquotecolor = result.find("blockquote .color");
|
||||
blockquotecolor.each(function (key, value) {
|
||||
$(value).closest("blockquote").css('border-left-color', $(value).attr('data-color'));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -195,7 +222,7 @@ function highlightRender(code, lang) {
|
|||
if (/\=$/.test(lang)) {
|
||||
var lines = result.value.split('\n');
|
||||
var linenumbers = [];
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
for (var i = 0; i < lines.length - 1; i++) {
|
||||
linenumbers[i] = "<div class='linenumber'>" + (i + 1) + "</div>";
|
||||
}
|
||||
var linegutter = "<div class='gutter'>" + linenumbers.join('\n') + "</div>";
|
||||
|
|
|
@ -47,7 +47,7 @@ function saveHistoryToStorage(notehistory) {
|
|||
if (store.enabled)
|
||||
store.set('notehistory', JSON.stringify(notehistory));
|
||||
else
|
||||
saveHistoryToCookie(notehistory);
|
||||
saveHistoryToStorage(notehistory);
|
||||
}
|
||||
|
||||
function saveHistoryToCookie(notehistory) {
|
||||
|
@ -146,11 +146,14 @@ function writeHistoryToServer(view) {
|
|||
} catch (err) {
|
||||
var notehistory = [];
|
||||
}
|
||||
if(!notehistory)
|
||||
notehistory = [];
|
||||
|
||||
var newnotehistory = generateHistory(view, notehistory);
|
||||
saveHistoryToServer(newnotehistory);
|
||||
})
|
||||
.fail(function () {
|
||||
writeHistoryToCookie(view);
|
||||
writeHistoryToStorage(view);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -160,7 +163,9 @@ function writeHistoryToCookie(view) {
|
|||
} catch (err) {
|
||||
var notehistory = [];
|
||||
}
|
||||
|
||||
if(!notehistory)
|
||||
notehistory = [];
|
||||
|
||||
var newnotehistory = generateHistory(view, notehistory);
|
||||
saveHistoryToCookie(newnotehistory);
|
||||
}
|
||||
|
@ -174,6 +179,9 @@ function writeHistoryToStorage(view) {
|
|||
var notehistory = data;
|
||||
} else
|
||||
var notehistory = [];
|
||||
if(!notehistory)
|
||||
notehistory = [];
|
||||
|
||||
var newnotehistory = generateHistory(view, notehistory);
|
||||
saveHistoryToStorage(newnotehistory);
|
||||
} else {
|
||||
|
@ -241,7 +249,7 @@ function getServerHistory(callback) {
|
|||
}
|
||||
})
|
||||
.fail(function () {
|
||||
getCookieHistory(callback);
|
||||
getStorageHistory(callback);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -282,7 +290,7 @@ function parseServerToHistory(list, callback) {
|
|||
}
|
||||
})
|
||||
.fail(function () {
|
||||
parseCookieToHistory(list, callback);
|
||||
parseStorageToHistory(list, callback);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -144,10 +144,14 @@ md.renderer.rules.code = function (tokens, idx /*, options, env */ ) {
|
|||
return '<code>' + Remarkable.utils.escapeHtml(tokens[idx].content) + '</code>';
|
||||
};
|
||||
|
||||
//var editorScrollThrottle = 100;
|
||||
var buildMapThrottle = 100;
|
||||
|
||||
var viewScrolling = false;
|
||||
var viewScrollingDelay = 200;
|
||||
var viewScrollingTimer = null;
|
||||
|
||||
//editor.on('scroll', _.throttle(syncScrollToView, editorScrollThrottle));
|
||||
editor.on('scroll', syncScrollToView);
|
||||
ui.area.view.on('scroll', function () {
|
||||
viewScrolling = true;
|
||||
|
@ -168,10 +172,12 @@ function clearMap() {
|
|||
lineHeightMap = null;
|
||||
}
|
||||
|
||||
var buildMap = _.throttle(buildMapInner, buildMapThrottle);
|
||||
|
||||
// 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 buildMap() {
|
||||
function buildMapInner(syncBack) {
|
||||
var i, offset, nonEmptyList, pos, a, b, _lineHeightMap, linesCount,
|
||||
acc, sourceLikeDiv, textarea = ui.area.codemirror,
|
||||
wrap = $('.CodeMirror-wrap pre'),
|
||||
|
@ -182,8 +188,6 @@ function buildMap() {
|
|||
visibility: 'hidden',
|
||||
height: 'auto',
|
||||
width: wrap.width(),
|
||||
padding: wrap.css('padding'),
|
||||
margin: wrap.css('margin'),
|
||||
'font-size': textarea.css('font-size'),
|
||||
'font-family': textarea.css('font-family'),
|
||||
'line-height': textarea.css('line-height'),
|
||||
|
@ -198,21 +202,23 @@ function buildMap() {
|
|||
_lineHeightMap = [];
|
||||
|
||||
acc = 0;
|
||||
editor.getValue().split('\n').forEach(function (str) {
|
||||
var lines = editor.getValue().split('\n');
|
||||
for (i = 0; i < lines.length; i++) {
|
||||
var str = lines[i];
|
||||
var h, lh;
|
||||
|
||||
_lineHeightMap.push(acc);
|
||||
|
||||
if (str.length === 0) {
|
||||
acc++;
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -224,9 +230,10 @@ function buildMap() {
|
|||
nonEmptyList.push(0);
|
||||
_scrollMap[0] = 0;
|
||||
|
||||
ui.area.markdown.find('.part').each(function (n, el) {
|
||||
var $el = $(el),
|
||||
t = $el.data('startline') - 1;
|
||||
var parts = ui.area.markdown.find('.part').toArray();
|
||||
for (i = 0; i < parts.length; i++) {
|
||||
var $el = $(parts[i]),
|
||||
t = $el.attr('data-startline') - 1;
|
||||
if (t === '') {
|
||||
return;
|
||||
}
|
||||
|
@ -235,7 +242,7 @@ function buildMap() {
|
|||
nonEmptyList.push(t);
|
||||
}
|
||||
_scrollMap[t] = Math.round($el.offset().top + offset);
|
||||
});
|
||||
}
|
||||
|
||||
nonEmptyList.push(linesCount);
|
||||
_scrollMap[linesCount] = ui.area.view[0].scrollHeight;
|
||||
|
@ -256,6 +263,9 @@ function buildMap() {
|
|||
|
||||
scrollMap = _scrollMap;
|
||||
lineHeightMap = _lineHeightMap;
|
||||
|
||||
if(loaded && syncBack)
|
||||
syncScrollToView();
|
||||
}
|
||||
|
||||
function getPartByEditorLineNo(lineNo) {
|
||||
|
@ -290,20 +300,20 @@ function getEditorLineNoByTop(top) {
|
|||
return null;
|
||||
}
|
||||
|
||||
function syncScrollToView(_lineNo) {
|
||||
function syncScrollToView(event, _lineNo) {
|
||||
if (currentMode != modeType.both) return;
|
||||
var lineNo, posTo;
|
||||
var scrollInfo = editor.getScrollInfo();
|
||||
if (!scrollMap || !lineHeightMap) {
|
||||
buildMap();
|
||||
buildMap(true);
|
||||
return;
|
||||
}
|
||||
if (typeof _lineNo != "number") {
|
||||
if (!_lineNo) {
|
||||
var topDiffPercent, posToNextDiff;
|
||||
var textHeight = editor.defaultTextHeight();
|
||||
lineNo = Math.floor(scrollInfo.top / textHeight);
|
||||
var lineCount = editor.lineCount();
|
||||
var lastLineHeight = editor.getLineHandle(lineCount - 1).height;
|
||||
//if reach last line, then scroll to end
|
||||
if (scrollInfo.top + scrollInfo.clientHeight >= scrollInfo.height - lastLineHeight) {
|
||||
//if reach bottom, then scroll to end
|
||||
if (scrollInfo.top + scrollInfo.clientHeight >= scrollInfo.height - defaultTextHeight) {
|
||||
posTo = ui.area.view[0].scrollHeight - ui.area.view.height();
|
||||
} else {
|
||||
topDiffPercent = (scrollInfo.top % textHeight) / textHeight;
|
||||
|
@ -316,12 +326,18 @@ function syncScrollToView(_lineNo) {
|
|||
posTo = scrollMap[lineHeightMap[_lineNo]];
|
||||
}
|
||||
var posDiff = Math.abs(ui.area.view.scrollTop() - posTo);
|
||||
var duration = posDiff / 50;
|
||||
ui.area.view.stop(true, true).animate({
|
||||
scrollTop: posTo
|
||||
}, duration >= 100 ? duration : 100, "linear");
|
||||
/*
|
||||
if (posDiff > scrollInfo.clientHeight / 5) {
|
||||
var duration = posDiff / 50;
|
||||
ui.area.view.stop(true).animate({
|
||||
ui.area.view.stop(true, true).animate({
|
||||
scrollTop: posTo
|
||||
}, duration >= 50 ? duration : 100, "linear");
|
||||
}, duration >= 100 ? duration : 100, "linear");
|
||||
} else {
|
||||
ui.area.view.stop(true).scrollTop(posTo);
|
||||
ui.area.view.stop(true, true).scrollTop(posTo);
|
||||
}
|
||||
*/
|
||||
}
|
|
@ -63,7 +63,7 @@
|
|||
}
|
||||
for (var i = ranges.length - 1; i >= 0; i--) {
|
||||
var cur = ranges[i].head;
|
||||
cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1));
|
||||
cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,7 +79,7 @@
|
|||
if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass;
|
||||
}
|
||||
cm.operation(function() {
|
||||
cm.replaceSelection("\n\n", null);
|
||||
cm.replaceSelection("\n\n", null, "+input");
|
||||
cm.execCommand("goCharLeft");
|
||||
ranges = cm.listSelections();
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
|
@ -144,12 +144,12 @@
|
|||
var sels = cm.getSelections();
|
||||
for (var i = 0; i < sels.length; i++)
|
||||
sels[i] = left + sels[i] + right;
|
||||
cm.replaceSelections(sels, "around");
|
||||
cm.replaceSelections(sels, "around", "+input");
|
||||
} else if (type == "both") {
|
||||
cm.replaceSelection(left + right, null);
|
||||
cm.replaceSelection(left + right, null, "+input");
|
||||
cm.execCommand("goCharLeft");
|
||||
} else if (type == "addFour") {
|
||||
cm.replaceSelection(left + left + left + left, "before");
|
||||
cm.replaceSelection(left + left + left + left, "before", "+input");
|
||||
cm.execCommand("goCharRight");
|
||||
}
|
||||
});
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
var listRE = /^(\s*)(>[> ]*|[*+-]\s|(\d+)\.)(\s*)/,
|
||||
emptyListRE = /^(\s*)(>[> ]*|[*+-]|(\d+)\.)(\s*)$/,
|
||||
var listRE = /^(\s*)(>[> ]*|[*+-]\s|(\d+)\.)(\[\s\]\s|\[x\]\s|\s*)/,
|
||||
emptyListRE = /^(\s*)(>[> ]*|[*+-]\s|(\d+)\.)(\[\s\]\s*|\[x\]\s|\s*)$/,
|
||||
unorderedListRE = /[*+-]\s/;
|
||||
|
||||
CodeMirror.commands.newlineAndIndentContinueMarkdownList = function(cm) {
|
||||
|
|
23
public/vendor/codemirror/codemirror.min.js
vendored
23
public/vendor/codemirror/codemirror.min.js
vendored
File diff suppressed because one or more lines are too long
18
public/vendor/codemirror/compress.sh
vendored
18
public/vendor/codemirror/compress.sh
vendored
|
@ -1,6 +1,8 @@
|
|||
uglifyjs --compress --mangle --output codemirror.min.js \
|
||||
lib/codemirror.js \
|
||||
addon/mode/overlay.js \
|
||||
addon/mode/simple.js \
|
||||
addon/mode/multiplex.js \
|
||||
addon/selection/active-line.js \
|
||||
addon/search/searchcursor.js \
|
||||
addon/search/search.js \
|
||||
|
@ -14,6 +16,7 @@ addon/edit/matchtags.js \
|
|||
addon/edit/closetag.js \
|
||||
addon/edit/continuelist.js \
|
||||
addon/comment/comment.js \
|
||||
addon/comment/continuecomment.js \
|
||||
addon/wrap/hardwrap.js \
|
||||
addon/fold/foldcode.js \
|
||||
addon/fold/brace-fold.js \
|
||||
|
@ -26,7 +29,22 @@ mode/gfm/gfm.js \
|
|||
mode/javascript/javascript.js \
|
||||
mode/css/css.js \
|
||||
mode/htmlmixed/htmlmixed.js \
|
||||
mode/htmlembedded/htmlembedded.js \
|
||||
mode/clike/clike.js \
|
||||
mode/clojure/clojure.js \
|
||||
mode/ruby/ruby.js \
|
||||
mode/python/python.js \
|
||||
mode/shell/shell.js \
|
||||
mode/php/php.js \
|
||||
mode/sql/sql.js \
|
||||
mode/coffeescript/coffeescript.js \
|
||||
mode/yaml/yaml.js \
|
||||
mode/jade/jade.js \
|
||||
mode/lua/lua.js \
|
||||
mode/cmake/cmake.js \
|
||||
mode/nginx/nginx.js \
|
||||
mode/perl/perl.js \
|
||||
mode/sass/sass.js \
|
||||
mode/r/r.js \
|
||||
mode/dockerfile/dockerfile.js \
|
||||
keymap/sublime.js
|
12
public/vendor/codemirror/lib/codemirror.js
vendored
12
public/vendor/codemirror/lib/codemirror.js
vendored
|
@ -388,7 +388,7 @@
|
|||
viewWidth: d.wrapper.clientWidth,
|
||||
barLeft: cm.options.fixedGutter ? gutterW : 0,
|
||||
docHeight: docH,
|
||||
scrollHeight: docH + scrollGap(cm) + d.barHeight,
|
||||
scrollHeight: docH + scrollGap(cm) + d.barHeight + textHeight(cm.display),
|
||||
nativeBarWidth: d.nativeBarWidth,
|
||||
gutterWidth: gutterW
|
||||
};
|
||||
|
@ -2287,9 +2287,13 @@
|
|||
}
|
||||
|
||||
$('.other-cursor').each(function(key, value) {
|
||||
var coord = cm.charCoords({line:$(value).attr('data-line'), ch:$(value).attr('data-ch')}, 'windows');
|
||||
$(value)[0].style.left = coord.left + 'px';
|
||||
$(value)[0].style.top = coord.top + 'px';
|
||||
var line = parseInt($(value).attr('data-line'));
|
||||
var ch = parseInt($(value).attr('data-ch'));
|
||||
var offsetLeft = parseFloat($(value).attr('data-offset-left'));
|
||||
var offsetTop = parseFloat($(value).attr('data-offset-top'));
|
||||
var coord = cm.charCoords({line: line, ch: ch}, 'windows');
|
||||
$(value)[0].style.left = coord.left + offsetLeft + 'px';
|
||||
$(value)[0].style.top = coord.top + offsetTop + 'px';
|
||||
});
|
||||
}
|
||||
|
||||
|
|
17
public/vendor/greensock-js/TweenMax.min.js
vendored
Executable file
17
public/vendor/greensock-js/TweenMax.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
14
public/vendor/greensock-js/jquery.gsap.min.js
vendored
Executable file
14
public/vendor/greensock-js/jquery.gsap.min.js
vendored
Executable file
|
@ -0,0 +1,14 @@
|
|||
/*!
|
||||
* VERSION: 0.1.11
|
||||
* DATE: 2015-03-13
|
||||
* UPDATES AND DOCS AT: http://greensock.com/jquery-gsap-plugin/
|
||||
*
|
||||
* Requires TweenLite version 1.8.0 or higher and CSSPlugin.
|
||||
*
|
||||
* @license Copyright (c) 2013-2015, GreenSock. All rights reserved.
|
||||
* This work is subject to the terms at http://greensock.com/standard-license or for
|
||||