├── public
├── .gitignore
├── images
│ └── logo-square.png
├── icons
│ ├── livecalc.svg
│ └── livecalc-source.svg
├── logo
│ └── livecalc.svg
└── main.js
├── sass
├── dashboard.scss
├── livecalc-icons.scss
├── account.scss
├── signup.scss
├── palette.scss
├── sidebar.scss
├── landing.scss
├── pricing.scss
├── chat.scss
├── header.scss
├── base.scss
├── livecalc.scss
└── style.scss
├── views
├── sidebar.pug
├── palette-trigonometry.pug
├── dashboard.pug
├── login.pug
├── header.pug
├── palette-basics.pug
├── signup.pug
├── templates.pug
├── base.pug
├── palette-greek-letters.pug
├── landing.pug
├── chars.txt
├── page-content.pug
├── account.pug
├── livecalc-templates.pug
├── doc.pug
└── pricing.pug
├── tokens.js
├── user
├── cookie_utils.js
└── user_pages.js
├── chat_db.js
├── package.json
├── README.md
├── stats.js
├── Makefile
├── marketing
└── marketing.js
├── sheet_counter.js
├── Users.md
├── .gitignore
├── sheet_db.js
├── sheet_model.js
├── user_cache.js
├── user_db.js
├── index.js
├── cache_user_model.js
├── livecalc.js
└── LICENSE
/public/.gitignore:
--------------------------------------------------------------------------------
1 | lib/**
--------------------------------------------------------------------------------
/public/images/logo-square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gmx/livecalc.xyz/master/public/images/logo-square.png
--------------------------------------------------------------------------------
/sass/dashboard.scss:
--------------------------------------------------------------------------------
1 | .dashboard{
2 | padding:30px;
3 | background:#fff;
4 | margin:20px;
5 | }
6 |
7 | a.account-settings-link{
8 | margin-left:30px;
9 | }
--------------------------------------------------------------------------------
/sass/livecalc-icons.scss:
--------------------------------------------------------------------------------
1 | .icon.delete-icon{
2 | background:url(../icons/livecalc.svg);
3 | width:20px;
4 | height:20px;
5 | background-position:-20px 0px;
6 | }
--------------------------------------------------------------------------------
/views/sidebar.pug:
--------------------------------------------------------------------------------
1 | .palette
2 | .sidebar-header.text-center
3 | h3
4 | | Palette
5 | .content
6 | include ./palette-basics.pug
7 | include ./palette-trigonometry.pug
8 | include ./palette-greek-letters.pug
9 | .palette-bottom-spacer
10 | livechat
11 |
--------------------------------------------------------------------------------
/tokens.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
3 | /*
4 | Thank you, Stack Overflow:
5 | http://stackoverflow.com/questions/8855687/secure-random-token-in-node-jse
6 | */
7 | module.exports.generate_token = function(num){
8 | var num = num || 10;
9 | return require('crypto').randomBytes(num).toString('hex');
10 | }
11 |
--------------------------------------------------------------------------------
/sass/account.scss:
--------------------------------------------------------------------------------
1 | .account{
2 | background:#fff;
3 | }
4 |
5 | .account-content{
6 | margin: 20px;
7 | padding-top:20px;
8 | }
9 |
10 | .account fieldset{
11 | border: none;
12 | padding:0;
13 | }
14 |
15 | .account button[type='submit']{
16 | margin-top:10px;
17 | margin-bottom:10px;
18 | }
19 |
20 | .account h4{
21 | margin-top: 10px;
22 | margin-bottom: 10px;
23 | }
--------------------------------------------------------------------------------
/sass/signup.scss:
--------------------------------------------------------------------------------
1 | .why-account{
2 | max-width:700px;
3 | margin:auto;
4 | font-size:120%;
5 | font-weight:300;
6 | }
7 |
8 | .auth{
9 | max-width:800px;
10 | background:#fff;
11 | margin:auto;
12 | }
13 |
14 | .auth fieldset{
15 | border:none;
16 | padding:10px 0px;
17 | }
18 |
19 | .auth input{
20 | width:100%;
21 | }
22 |
23 | .auth .login-block{
24 | padding:20px;
25 | }
26 |
27 | .go-to-signup{
28 | margin-left:30px;
29 | }
--------------------------------------------------------------------------------
/user/cookie_utils.js:
--------------------------------------------------------------------------------
1 | var cookie = require('cookie');
2 |
3 | module.exports = {};
4 |
5 | module.exports.cookie_send_id = cookie_send_id;
6 |
7 | function cookie_send_id(res, id){
8 | res.setHeader(
9 | 'Set-Cookie',
10 | cookie.serialize('session_id', id, {
11 | httpOnly: true,
12 | maxAge: 60 * 60 * 24 // 1 days
13 | }));
14 | }
15 |
16 | module.exports.cookie_get_id = cookie_get_id;
17 |
18 | function cookie_get_id(req){
19 | return cookie.parse(req.headers.cookie || '').session_id;
20 | }
21 |
--------------------------------------------------------------------------------
/views/palette-trigonometry.pug:
--------------------------------------------------------------------------------
1 | .palette-trigonometry
2 | h4 Trigonometry
3 | p In degree
4 | button(data-replace-var="x") sin(x deg)
5 | button(data-replace-var="x") cos(x deg)
6 | button(data-replace-var="x") tan(x deg)
7 | p In radian
8 | button(data-replace-var="x") sin(x)
9 | button(data-replace-var="x") cos(x)
10 | button(data-replace-var="x") tan(x)
11 |
12 | p Inverse trigonometric functions
13 |
14 | button(data-replace-var="x") asin(x)
15 | button(data-replace-var="x") acos(x)
16 | button(data-replace-var="x") atan(x)
17 |
--------------------------------------------------------------------------------
/sass/palette.scss:
--------------------------------------------------------------------------------
1 |
2 | .palette{
3 | background:#eee;
4 | }
5 |
6 | .palette .content{
7 | margin-left:10px;
8 | max-height:100%;
9 | overflow-y: scroll;
10 | }
11 |
12 | .palette-bottom-spacer{
13 | height:60px;
14 | }
15 |
16 | .palette button{
17 | margin-top:5px;
18 | margin-bottom:0px;
19 | margin-right:2px;
20 | }
21 |
22 | .palette-greek-letters button{
23 | width:30px;
24 | }
25 |
26 | .palette h4{
27 | line-height:20px;
28 | margin:0;
29 | margin-top:10px;
30 | padding:4px;
31 | }
32 |
33 | .palette p{
34 | line-height:15px;
35 | margin-top:8px;
36 | margin-bottom:0;
37 | }
--------------------------------------------------------------------------------
/views/dashboard.pug:
--------------------------------------------------------------------------------
1 | .dashboard-content
2 | col-md-6
3 | h3 Dashboard
4 | h4 Recently visited sheets
5 | if recent_sheets.length == 0
6 | p (No recently visited sheets)
7 | .recent-sheet-list
8 | ul
9 | for sheet in recent_sheets
10 | -url = "/sheet/"+sheet
11 | li
12 | a(href=url)= url
13 | p
14 | a(href="/new")
15 | button
16 | | Create a new sheet
17 |
18 | col-md-6
19 | h4 Account Settings
20 | p
21 | | Change your username, password, email address & more :
22 | a(href="/account").account-settings-link
23 | button Account Settings
24 |
--------------------------------------------------------------------------------
/chat_db.js:
--------------------------------------------------------------------------------
1 | var redis = require("redis");
2 |
3 | var client = redis.createClient();
4 |
5 | module.exports = {};
6 |
7 | module.exports.add_message = function(id, data){
8 | var data = JSON.stringify(data);
9 | client.rpush(["livechat:"+id, data], function(err, reply){
10 | if(err != null){
11 | console.log(err);
12 | }
13 | });
14 | };
15 |
16 | module.exports.get_conv = function(id, callback){
17 | /* get 100 last messages */
18 | client.lrange("livechat:"+id, -100, -1, function(err, reply){
19 | if(err != null){
20 | console.log(err);
21 | }
22 | callback(reply);
23 | });
24 | };
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "livecalc.xyz",
3 | "version": "0.0.13",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "body-parser": "^1.15.1",
13 | "cookie": "^0.3.1",
14 | "cookie-session": "^2.0.0-alpha.1",
15 | "crypto": "0.0.3",
16 | "deepcopy": "^0.6.3",
17 | "email-validator": "^1.0.4",
18 | "express": "^4.14.0",
19 | "mongoose": "^4.5.3",
20 | "multer": "^1.1.0",
21 | "node-sass-middleware": "^0.9.8",
22 | "password-hash": "^1.2.2",
23 | "pug": "^2.0.0-beta3",
24 | "redis": "^2.6.2",
25 | "socket.io": "^1.4.6"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/views/login.pug:
--------------------------------------------------------------------------------
1 | h1.text-center Login
2 | .auth.login-form
3 | col-md-6
4 | .basic-auth.login-block
5 | p
6 | | Don't have an account?
7 | a(href="/signup").go-to-signup
8 | | sign up!
9 | form(action="/login", method="post")
10 | fieldset
11 | label Your email address
12 | br
13 | input(type="email", name="email", placeholder="email@email.com")
14 | fieldset
15 | label Password
16 | br
17 | input(type="password", name="password", placeholder="Password")
18 | br
19 | .text-center
20 | button(type="submit" value="submit")
21 | | Login
22 | col-md-6
23 | .facebook-login.login-block
24 | //h3
25 | // | Login with Facebook
26 | //p Not yet
27 |
--------------------------------------------------------------------------------
/views/header.pug:
--------------------------------------------------------------------------------
1 | header
2 | col-md-8
3 | h1.inline-block.mobile-center
4 | a(href='https://livecalc.xyz') livecalc.xyz
5 | p.inline-block.text-left.mobile-center
6 | | Github
7 | a(href='https://github.com/antoineMoPa/livecalc',
8 | target='_blank') project
9 | | |
10 | | Version 0.0.13
11 | col-md-4.text-right.mobile-center.links
12 | a.new-sheet-button(href='/new', target='_blank')
13 | button New Sheet
14 | nav
15 | ul
16 | li
17 | a.pricing-link(href='/pricing', target='_blank')
18 | | Pricing
19 |
20 | if !logged_in
21 | li
22 | a.login(href='/login')
23 | | Log in
24 |
25 | else
26 | li
27 | a.dashboard-link(href='/dashboard')
28 | | Dashboard
29 | li
30 | a(href="/logout") Log out
31 |
--------------------------------------------------------------------------------
/sass/sidebar.scss:
--------------------------------------------------------------------------------
1 | .sticky-sidebar{
2 | position:fixed;
3 | background:#f2f2f2;
4 | height:100%;
5 | top:0;
6 | right:0;
7 | bottom:0;
8 | z-index:1000;
9 | box-shadow:0 0 3px rgba(0,0,0,0.15);
10 | }
11 |
12 | .sidebar-header{
13 | background:$color_6;
14 | font-size:12px;
15 | padding:3px;
16 | text-align:left;
17 | }
18 |
19 | .sidebar-header h3{
20 | color:#fff;
21 | }
22 |
23 | @media all and (min-width:768px){
24 | .in-sheet header,
25 | .in-sheet nav{
26 | width: 75%;
27 | }
28 | }
29 |
30 | @media (max-width:768px){
31 | .sticky-sidebar{
32 | display:none;
33 | }
34 | }
35 |
36 | .sidebar-resize-header{
37 | cursor: ns-resize;
38 | }
39 |
40 | livechat{
41 | height:33%;
42 | }
43 |
44 | .palette{
45 | height:66%;
46 | }
47 |
48 | @import "palette.scss";
49 |
50 |
--------------------------------------------------------------------------------
/sass/landing.scss:
--------------------------------------------------------------------------------
1 | /* landing page */
2 |
3 | body.page-landing{
4 | background:$color_3;
5 | }
6 |
7 | .landing{
8 | max-width:100%;
9 | word-wrap: break-word;
10 | }
11 |
12 | .landing h1{
13 | line-height:80px;
14 | color:#fff;
15 | font-size:30px;
16 | font-family: 'Fira Sans';
17 | font-weight:300;
18 | }
19 |
20 | .landing p{
21 | line-height:150%;
22 | color:#fff;
23 | font-size:24px;
24 | font-family:sans-serif;
25 | font-family: 'Fira Sans';
26 | font-weight:300;
27 | }
28 |
29 | .landing a{
30 | color: $color_2;
31 | }
32 |
33 | .features{
34 | background:#eee;
35 | color:#333;
36 | margin-left:-20px;
37 | margin-right:-25px;
38 | padding:20px;
39 | border-bottom:5px solid $body_bg;
40 | }
41 |
42 | .features h3{
43 | text-shadow:none;
44 | }
45 |
46 | @media (min-width:1000px){
47 | .features .block-center{
48 | width:40%;
49 | margin:auto;
50 | }
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/views/palette-basics.pug:
--------------------------------------------------------------------------------
1 | .palette-basics
2 | h4 Basics
3 | button(title='Last result') ans
4 | button(title='Add',
5 | data-wrap-before="(",
6 | data-wrap-after="+[[cursor]])",
7 | ) +
8 | button(title='Substract',
9 | data-wrap-before="(",
10 | data-wrap-after="-[[cursor]])",
11 | ) -
12 | button(title='Multiply',
13 | data-wrap-before="(",
14 | data-wrap-after=")*[[cursor]]",
15 | ) *
16 | button(title='Divide',
17 | data-wrap-before="(",
18 | data-wrap-after=")*[[cursor]]",
19 | ) /
20 | button(title='Exponent',
21 | data-wrap-before="(",
22 | data-wrap-after=")^[[cursor]]",
23 | ) ^
24 | button(title='Square root',
25 | data-wrap-before="sqrt(",
26 | data-wrap-after="[[cursor]])",
27 | data-no-sel="sqrt([[cursor]])",
28 | ) sqrt
29 | button(title='π, The math constant Pi',
30 | data-no-sel="π[[cursor]]",
31 | ) π
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Code Status: No major problem.
2 |
3 | # livecalc.xyz
4 |
5 | livecalc.xyz is a multi-user math web app. The math is parsed & computed in the browser by [math.js](http://mathjs.org/).
6 |
7 | [Live demo](https://www.livecalc.xyz/sheet/demo)
8 |
9 | # Example
10 |
11 | sin(45 deg) => 0.7071067811865475
12 |
13 | # Code
14 |
15 | A node.js backend with socket.io is used for synchronisation of sheets between multiple users.
16 | The client UI is mostly written in vanilla JS. No jQuery, no angular, no react, no whatever (for now).
17 |
18 | # Install
19 |
20 | Clone:
21 |
22 | git clone https://github.com/antoineMoPa/livecalc.git
23 |
24 | Backend dependencies:
25 |
26 | npm install
27 |
28 | I was too hipster for browserify. So I decided to create a makefile to download frontend dependencies:
29 |
30 | make download
31 |
32 | If you want to use the fira firefox font:
33 |
34 | make fira
35 |
36 | Run
37 |
38 | nodejs index.js
39 |
40 | You can now visit [http://127.0.0.1:3000](http://127.0.0.1:3000).
41 |
42 | # Contributing
43 |
44 | You can submit issues.
45 |
46 | You can also clone and code then send a pull request.
47 |
--------------------------------------------------------------------------------
/views/signup.pug:
--------------------------------------------------------------------------------
1 | h1.text-center Create your account
2 |
3 | .why-account
4 | // hide for now
5 | //ul
6 | li Keep track of your sheets, lock and unlock them
7 | li Store up to 5 private sheets
8 | li Invite up to 5 people in a sheet at the same time
9 |
10 | .auth.signup-form
11 | col-md-6
12 | .basic-auth.login-block
13 | form(action="/signup", method="post")
14 | fieldset
15 | label User name (6-40 letters and numbers)
16 | br
17 | input(type="text", pattern="^[A-Za-z0-9]{6-40}$", name="username", placeholder="JohnSmith")
18 | fieldset
19 | label Your email address
20 | br
21 | input(type="email", name="email", placeholder="email@email.com")
22 | fieldset
23 | label Create a password (Minimum 6 characters)
24 | br
25 | input(type="password", name="password", pattern="^.{6,}$", placeholder="Password")
26 | br
27 | .text-center
28 | button(type="submit" value="submit")
29 | | Create my account
30 | col-md-6
31 | .facebook-login.login-block
32 | //h3
33 | // | Login with Facebook
34 | //p Not yet
35 |
--------------------------------------------------------------------------------
/stats.js:
--------------------------------------------------------------------------------
1 | var redis = require("redis");
2 |
3 | var client = redis.createClient()
4 |
5 | module.exports = {};
6 |
7 | module.exports.new_sheet = function(){
8 | client.incr("sheet_count", function(err, msg){
9 | // Log a each 5 new sheets
10 | if(msg % 5 == 0){
11 | console.log("total sheets: " + msg);
12 | }
13 | });
14 | }
15 |
16 | module.exports.log_visit = function(what){
17 | console.log("visit: " + what);
18 | }
19 |
20 | module.exports.new_sheet_visit = function(id){
21 | client.incr("visit_sheet:"+id, function(err, msg){
22 | if(err != null){
23 | console.log(err);
24 | }
25 | });
26 | }
27 |
28 | module.exports.get_sheet_visits = function(id, callback){
29 | var callback = callback || function(){};
30 | client.get("visit_sheet:"+id, function(err, msg){
31 | if(err != null){
32 | console.log(err);
33 | }
34 | callback(msg);
35 | });
36 | }
37 |
38 | /* Marketing */
39 |
40 | module.exports.newaccounts_newsletter_signup = function(email){
41 | client.lpush("newsaccounts_newsletter",email, function(err, msg){
42 | if(err != null){
43 | console.log(err);
44 | }
45 | });
46 | }
47 |
--------------------------------------------------------------------------------
/views/templates.pug:
--------------------------------------------------------------------------------
1 | template(name='livecalc-die')
2 | h3 Create a new sheet
3 | a(href='/new')
4 | button Create a new sheet
5 | h3 Go to homepage
6 | a(href='/')
7 | button Homepage
8 | template(name='livechat')
9 | .sidebar-header.sidebar-resize-header.text-center
10 | h3
11 | | Chat
12 | .message-log
13 | .message-input
14 | textarea(placeholder='Join the conversation')
15 | button
16 | | Send
17 | template(name='livechat-received-message')
18 | .livechat-received-message
19 | .content
20 | .sender
21 | template(name='livechat-sent-message')
22 | .livechat-sent-message
23 | .content
24 | .sender
25 | template(name='small-doc')
26 | .doc
27 | include ./doc.pug
28 | template(name='single-field', data-params='class,default,info')
29 | .single-field(class='{{class}}')
30 | p {{info}}
31 | input(value='{{default}}')
32 | button Confirm
33 | template(name='livecalc-wait-click')
34 | .livecalc-wait-click
35 | button.livecalc-wait-click-button View
36 | p This might take some time to compute.
37 | template(name='modal-overlay')
38 | .modal-overlay
39 | template(name='modal-inform')
40 | .modal
41 | .content
42 | p
43 | .buttons
44 | include ./livecalc-templates.pug
45 |
--------------------------------------------------------------------------------
/sass/pricing.scss:
--------------------------------------------------------------------------------
1 | /* pricing page */
2 |
3 | .pricing-table{
4 | margin:auto;
5 | font-size: 18px;
6 | background:#fff;
7 | padding:30px;
8 | }
9 |
10 | .pricing-table td{
11 | padding:10px;
12 | background:#efefef;
13 | }
14 |
15 | .pricing-table .col-feature{
16 | text-align: left;
17 | background: lighten($color_1,80%);
18 | }
19 |
20 | .pricing-table th{
21 | padding: 20px 30px;
22 | }
23 |
24 | .pricing-table .yes{
25 | background: lighten($color_3,20%);
26 | }
27 |
28 | .pricing p{
29 | font-size:120%;
30 | font-weight:300;
31 | }
32 |
33 | .pricing .coming-soon{
34 | font-weight:300;
35 | font-size:13px;
36 | }
37 |
38 | td.receive-alert{
39 | padding:30px;
40 | }
41 |
42 | @media all and (max-width:768px) {
43 | .pricing-table{
44 | font-size:10px;
45 | padding:5px;
46 | }
47 | .pricing-table tr{
48 | margin:0;
49 | }
50 | .pricing .coming-soon{
51 | font-size:8px;
52 | }
53 | .pricing-table td{
54 | padding:2px;
55 | }
56 | .pricing-table th{
57 | padding: 0px 3px;
58 | }
59 | .pricing-table input{
60 | width:100px;
61 | }
62 | .pricing-table button{
63 | font-size:10px;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | download:
2 | # math.js
3 | wget -N http://cdnjs.cloudflare.com/ajax/libs/mathjs/3.2.1/math.min.js -P public/lib/
4 | # socket.io
5 | wget -N https://raw.githubusercontent.com/socketio/socket.io-client/master/socket.io.js -P public/lib/
6 | # function-plot.js
7 | wget -N https://raw.githubusercontent.com/maurizzzio/function-plot/master/dist/function-plot.js -P public/lib/
8 | # d3.js
9 | wget -N https://github.com/d3/d3/releases/download/v3.5.17/d3.zip -P public/lib/
10 | unzip public/lib/d3.zip -d public/lib/d3
11 | mv -f public/lib/d3/d3.min.js public/lib/
12 | rm -f public/lib/d3/*
13 | rmdir public/lib/d3
14 | rm public/lib/d3.zip
15 | # diff.js
16 | # https://github.com/kpdecker/jsdiff
17 | wget -N https://cdnjs.cloudflare.com/ajax/libs/jsdiff/2.2.3/diff.min.js -P public/lib
18 |
19 | fira:
20 | wget -N https://github.com/mozilla/Fira/archive/4.202.tar.gz -P public/fonts/fira-download
21 | # Remove old folder
22 | rm -rf public/fonts/fira-extract/
23 | rm -rf public/fonts/fira/
24 | mkdir -p public/fonts/fira-extract/
25 | tar -zxvf public/fonts/fira-download/*.tar.gz -C public/fonts/fira-extract/ --exclude "source/*"
26 | mv public/fonts/fira-extract/* public/fonts/fira
27 | rm -rf public/fonts/fira-download public/fonts/fira-extract
28 | rm -rf public/fonts/fira/technical\ reports/
29 |
--------------------------------------------------------------------------------
/marketing/marketing.js:
--------------------------------------------------------------------------------
1 | /* handles pricing related pages */
2 |
3 | module.exports = function(app, stats){
4 | app.get('/pricing', function (req, res) {
5 | stats.log_visit("pricing-page");
6 | res.render('base',{page: "pricing"});
7 | });
8 |
9 | app.post('/marketing/newsletter_signup', function (req, res) {
10 | var email = req.body.email;
11 |
12 | var validator = require("email-validator");
13 |
14 | if(validator.validate(email)){
15 | stats.newaccounts_newsletter_signup(email);
16 | console.log("newsletter signup: ", email);
17 | res.render('base',{
18 | page: "pricing",
19 | positive_message:true,
20 | message:
21 | "Thank you for signing up to our newsletter! "+
22 | "You will be informed when new "+
23 | "user accounts are made available."
24 | });
25 | } else {
26 | res.render('base',{
27 | page: "pricing",
28 | negative_message:true,
29 | message:
30 | "We could not sign you up, "+
31 | "there seems to be a problem with your email address."
32 | });
33 | }
34 | });
35 | };
36 |
--------------------------------------------------------------------------------
/views/base.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | meta(charset='utf-8')
5 | meta
6 | // I'm so meta
7 | title livecalc.xyz Collaborative Calculator
8 | link(rel='stylesheet', href='/fonts/fira/fira.css')
9 | link(rel='stylesheet', href='/css/style.css')
10 | meta(name='viewport', content='width=device-width, initial-scale=1')
11 | meta(name='description', content='Multi-user calculator for all your projects that include math, plots & problems to solve.')
12 | meta(property='og:image', content='http://livecalc.xyz/images/logo-square.png')
13 | meta(property='og:title', content='livecalc.xyz open source collaborative calculator')
14 | meta(property='og:description', content='Multi-user calculator for all your projects that include math, plots & problems to solve.')
15 | body(class="page-"+(page || ""), class=in_sheet? "in-sheet": "")
16 | include ./header.pug
17 | if in_sheet
18 | col-md-9
19 | include ./page-content.pug
20 | col-md-3.sticky-sidebar
21 | include ./sidebar.pug
22 | else
23 | include ./page-content.pug
24 | include ./templates.pug
25 |
26 | script(src='/lib/math.min.js')
27 | script(src='/lib/d3.min.js', charset='utf-8')
28 | script(src='/lib/function-plot.js', charset='utf-8')
29 | script(src='/lib/socket.io.js')
30 | script(src='/lib/diff.min.js')
31 | script(src='/main.js')
32 |
--------------------------------------------------------------------------------
/sheet_counter.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
3 | module.exports.Counter = Counter;
4 |
5 | function Counter(){
6 | var exports = {};
7 |
8 | var data = {};
9 |
10 | exports.data = data;
11 |
12 | data.counters = {};
13 |
14 | var counters = data.counters;
15 | var types = [];
16 |
17 | exports.plus = function(type){
18 | if(counters[type] == undefined){
19 | counters[type] = 0;
20 | types.push(type);
21 | }
22 | data.counters[type]++;
23 | }
24 |
25 | exports.minus = function(type){
26 | if(counters[type] == undefined){
27 | console.log(
28 | "Error: Trying to decrement undefined counter");
29 | }
30 |
31 | if(counters[type] == 0){
32 | console.log(
33 | "Error: Trying to decrement counter = 0, type: "+type);
34 | }
35 |
36 | data.counters[type]--;
37 | }
38 |
39 | /*
40 | Returns total if type is null/undefined
41 | */
42 | exports.get = function(type){
43 | if(type == undefined || type == null){
44 | var tot = 0;
45 | for(t in types){
46 | tot += data.counters[t];
47 | }
48 | return tot;
49 | }
50 |
51 | return data.counters[type] || 0;
52 | }
53 |
54 | return exports;
55 | }
56 |
--------------------------------------------------------------------------------
/Users.md:
--------------------------------------------------------------------------------
1 | # Users, session, etc.
2 |
3 | ## login
4 |
5 | When a user
6 |
7 | ## session_id
8 |
9 | This is stored in a cookie and is currently valid for one login.
10 |
11 | We have to validate if the session really exists in redis at each request.
12 |
13 | ## public_id (sometimes user_id in frontend code)
14 |
15 | public_id in the server side is the same thing as user_id in the frontend side.
16 |
17 | It allows, for example, to identify who as sent what message, who is editing what cell, etc.
18 |
19 | Previously, public_id and user_id used to be updatable by frontend, but this should never be the case anymore.
20 |
21 | ## Ideas to prevent session hijacking
22 |
23 | Change the session_id at each page load.
24 |
25 | See: [this wikipedia article](https://en.wikipedia.org/wiki/Session_hijacking#Prevention)
26 |
27 | ## What happens when a user logs in
28 |
29 | Example of a successful login and sheet interaction.
30 |
31 | * User logins, password is matched to mongodb password
32 | * Session is created, cookie with id is sent
33 | * Data is fetched from mongo and put in redis
34 | * User goes to a sheet
35 | * Socket.io connection and cookie reception
36 | * Verify if session exists
37 | * Add user to memory
38 | * Send nickname and public_id to client
39 | * Past messages are loaded in chat, messages where
40 | `public id == current user id`
41 | are highlighted as own-message.
42 | * etc.
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | -*- mode: gitignore; -*-
2 | *~
3 | \#*\#
4 | /.emacs.desktop
5 | /.emacs.desktop.lock
6 | *.elc
7 | auto-save-list
8 | tramp
9 | .\#*
10 |
11 | # Org-mode
12 | .org-id-locations
13 | *_archive
14 |
15 | # flymake-mode
16 | *_flymake.*
17 |
18 | # eshell files
19 | /eshell/history
20 | /eshell/lastdir
21 |
22 | # elpa packages
23 | /elpa/
24 |
25 | # reftex files
26 | *.rel
27 |
28 | # AUCTeX auto folder
29 | /auto/
30 |
31 | # cask packages
32 | .cask/
33 | dist/
34 |
35 | # Flycheck
36 | flycheck_*.el
37 |
38 | # server auth directory
39 | /server/
40 |
41 | # projectiles files
42 | .projectile
43 |
44 | Logs
45 | logs
46 | *.log
47 | npm-debug.log*
48 |
49 | # Runtime data
50 | pids
51 | *.pid
52 | *.seed
53 |
54 | # Directory for instrumented libs generated by jscoverage/JSCover
55 | lib-cov
56 |
57 | # Coverage directory used by tools like istanbul
58 | coverage
59 |
60 | # nyc test coverage
61 | .nyc_output
62 |
63 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
64 | .grunt
65 |
66 | # node-waf configuration
67 | .lock-wscript
68 |
69 | # Compiled binary addons (http://nodejs.org/api/addons.html)
70 | build/Release
71 |
72 | # Dependency directories
73 | node_modules
74 | jspm_packages
75 |
76 | # Optional npm cache directory
77 | .npm
78 |
79 | # Optional REPL history
80 | .node_repl_history
81 |
82 | # Fonts folder
83 | public/fonts
84 |
85 | # Generated CSS
86 | public/css/**
--------------------------------------------------------------------------------
/views/palette-greek-letters.pug:
--------------------------------------------------------------------------------
1 | .palette-greek-letters
2 | h4 Greek Letters
3 | button(title='alpha') Α
4 | button(title='alpha') α
5 | button(title='beta') Β
6 | button(title='beta') β
7 | button(title='gamma') Γ
8 | button(title='gamma') γ
9 | button(title='delta') Δ
10 | button(title='delta') δ
11 | button(title='epsilon') Ε
12 | button(title='epsilon') ε
13 | button(title='zeta') Ζ
14 | button(title='zeta') ζ
15 | button(title='eta') Η
16 | button(title='eta') η
17 | button(title='theta') Θ
18 | button(title='theta') θ
19 | button(title='iota') Ι
20 | button(title='iota') ι
21 | button(title='kappa') Κ
22 | button(title='kappa') κ
23 | button(title='lambda') Λ
24 | button(title='lambda') λ
25 | button(title='mu') Μ
26 | button(title='mu') μ
27 | button(title='nu') Ν
28 | button(title='nu') ν
29 | button(title='xi') Ξ
30 | button(title='xi') ξ
31 | button(title='omicron') Ο
32 | button(title='omicron') ο
33 | button(title='pi') Π
34 | button(title='pi') π
35 | button(title='rho') Ρ
36 | button(title='rho') ρ
37 | button(title='sigma') Σ
38 | button(title='sigma') σ
39 | button(title='sigma') ς
40 | button(title='tau') Τ
41 | button(title='tau') τ
42 | button(title='upsilon') Υ
43 | button(title='upsilon') υ
44 | button(title='phi') Φ
45 | button(title='phi') φ
46 | button(title='chi') Χ
47 | button(title='chi') χ
48 | button(title='psi') Ψ
49 | button(title='psi') ψ
50 | button(title='omega') Ω
51 | button(title='omega') ω
52 |
--------------------------------------------------------------------------------
/sass/chat.scss:
--------------------------------------------------------------------------------
1 | /* Chat */
2 |
3 | livechat{
4 | background:#eee;
5 | position:relative;
6 | display:block;
7 | }
8 |
9 | .livechat-sent-message,
10 | .livechat-received-message{
11 | width:90%;
12 | word-wrap: break-word;
13 | margin-bottom:5px;
14 | margin-top:5px;
15 | padding:10px;
16 | border-radius:10px;
17 | }
18 |
19 | .livechat-received-message{
20 | color:$color_2;
21 | background:#fff;
22 | border:1px solid darken(#eee,4%);
23 | border-top-left-radius:0;
24 | margin-left:0;
25 | margin-right:auto;
26 | }
27 |
28 | .livechat-sent-message{
29 | color:$color_2;
30 | background:$color_5;
31 | border:1px solid darken($color_5,5%);
32 | border-bottom-right-radius:0;
33 | margin-left:auto;
34 | margin-right:0;
35 | }
36 |
37 | livechat .header{
38 | padding:5px;
39 | background: $color_3;
40 | }
41 |
42 | livechat .header h3{
43 | color:#fff;
44 | font-size:15px;
45 | }
46 |
47 | livechat button{
48 | position:absolute;
49 | bottom:0px;
50 | right:10px;
51 | }
52 |
53 | livechat textarea{
54 | position:absolute;
55 | bottom:0px;
56 | left:10px;
57 | height:50px;
58 | padding:5px;
59 | border:none;
60 | font-size:13px;
61 | background:#fff;
62 | resize:none;
63 | }
64 |
65 | livechat .message-log{
66 | overflow-y: scroll;
67 | overflow-x:hidden;
68 | max-height:100%;
69 | padding-left:5px;
70 | padding-right:15px;
71 | }
72 |
73 | livechat .sender{
74 | font-size:10px;
75 | text-align:right;
76 | }
77 |
--------------------------------------------------------------------------------
/views/landing.pug:
--------------------------------------------------------------------------------
1 | .landing.only-landing
2 | h1.text-center Solve problems together
3 | p.text-center
4 | | livecalc.xyz is a collaborative calculator
5 | br
6 | br
7 | | To use livecalc.xyz, create a
8 | a(href='/new', target='_blank') new sheet
9 | br
10 | | Up to 3 users can edit a sheet at the same time,
11 | br
12 | | so share the sheet URL to your friends/classmates/colleagues!
13 | br
14 | br
15 | | Have fun!
16 | br
17 | br
18 | .features
19 | .block-center
20 | h3 Features
21 | p
22 | ul
23 | li Play with numbers
24 | ul
25 | li a+b, a-b (addition, substraction)
26 | li a*b, a/b (multiplication, division)
27 | li a^b (exponent)
28 | li sqrt(a) (square root)
29 | li sin(x), cos(x), tan(x) (trigonometric functions in rad)
30 | li sin(45 deg) (same, in degree)
31 | li n! (factorial)
32 | li For the complete syntax documentation, see
33 | a(
34 | href="http://mathjs.org/docs/expressions/syntax.html",
35 | target="_blank"
36 | ) here
37 | li Plots Functions
38 | li Store values in variables
39 | li Convert units
40 | li Generate fractals with complex numbers
41 | li Chat with your teammates
42 | li Completely open source
43 | p.text-center
44 | | Contribute code & issues on
45 | a(href='https://github.com/antoineMoPa/livecalc', target='_blank') github
46 |
--------------------------------------------------------------------------------
/sheet_db.js:
--------------------------------------------------------------------------------
1 | var redis = require("redis");
2 |
3 | var client = redis.createClient();
4 |
5 | module.exports = {};
6 |
7 | function gen_id(id){
8 | var id = id.replace(/[^A-Za-z0-9_]/g,"");
9 | return "sheet:"+id;
10 | }
11 |
12 | test();
13 |
14 | function test(){
15 | if(gen_id("*a*") != "sheet:a"){
16 | console.log("Test failed "+gen_id("*a*"));
17 | }
18 | }
19 |
20 | module.exports.store_sheet = function(id, data){
21 | var data = JSON.stringify(data);
22 |
23 | var id = gen_id(id);
24 |
25 | if(id.length == 0){
26 | return;
27 | }
28 |
29 | client.set(id, data, function(err, reply){
30 | if(err != null){
31 | console.log("err: " + err);
32 | }
33 | });
34 | };
35 |
36 | /*
37 |
38 | callback(data)
39 |
40 | */
41 | module.exports.get_sheet = function(id, callback){
42 | var id = gen_id(id);
43 |
44 | if(id.length == 0){
45 | return;
46 | }
47 |
48 | client.get(id, function(err, reply){
49 | if(err != null){
50 | console.log("err: " + err);
51 | }
52 | callback(JSON.parse(reply));
53 | });
54 | };
55 |
56 | /*
57 |
58 | callback(bool: exists)
59 |
60 | */
61 | module.exports.exists = function(id, callback){
62 | var id = gen_id(id);
63 |
64 | if(id.length == 0){
65 | callback(false);
66 | return;
67 | }
68 |
69 | client.exists(id, function(err, exists){
70 | if(err != null){
71 | console.log("err: " + err);
72 | }
73 | callback(exists);
74 | });
75 | };
76 |
--------------------------------------------------------------------------------
/views/chars.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/views/page-content.pug:
--------------------------------------------------------------------------------
1 | .main-page
2 | if positive_message
3 | .server-message.positive
4 | p
5 | = message
6 | if negative_message
7 | .server-message.negative
8 | p
9 | = message
10 |
11 | if page != undefined
12 | .static
13 | if page == "landing"
14 | include ./landing.pug
15 | else if page == "pricing"
16 | .pricing
17 | include ./pricing.pug
18 | else if page == "signup"
19 | .signup
20 | include ./signup.pug
21 | else if page == "account"
22 | .account
23 | include ./account.pug
24 | else if page == "login"
25 | .login
26 | include ./login.pug
27 | else if page == "dashboard"
28 | .dashboard
29 | include ./dashboard.pug
30 | else if page == "sheet"
31 | col-md-6
32 | p To get started, scroll down and see the quick reference.
33 | col-md-6
34 | p.text-right.livecalc-sharing
35 | | Share this sheet:
36 | a(target="_blank", href="https://www.facebook.com/sharer/sharer.php?u=" + share_url)
37 | | facebook
38 | a(target="_blank", href=("https://twitter.com/intent/tweet?text=View%20this%20livecalc.xyz%20sheet:%20" + share_url))
39 | | twitter
40 | a(target="_blank",href=share_url).url-popup-modal
41 | | URL
42 | livecalc
43 | else
44 | p
45 | | Content not found
46 | br
47 | p.text-center
48 | | Licence:
49 | a(href='https://www.gnu.org/licenses/gpl-3.0.en.html',
50 | target='_blank')
51 | | GPLv3
52 | br
53 |
--------------------------------------------------------------------------------
/views/account.pug:
--------------------------------------------------------------------------------
1 | .account-content
2 | h3 Your Account
3 | col-md-6
4 | h4 Change your username
5 | form(action="/account-username", method="post")
6 | fieldset
7 | label User name (6-40 letters and numbers)
8 | br
9 | input(type="text", pattern="^[A-Za-z0-9]{6,40}$",
10 | name="username", placeholder="JohnSmith",
11 | value=user.get_username()
12 | )
13 | .text-left
14 | button(type="submit" value="submit")
15 | | Change username
16 | h4 Change email address
17 | form(action="/account-email", method="post")
18 | fieldset
19 | label New email address
20 | br
21 | input(
22 | type="email", name="email",
23 | placeholder="email@email.com",
24 | value=user.get_email()
25 | )
26 | .text-left
27 | button(type="submit" value="submit")
28 | | Change email address
29 | col-md-6
30 | h4 Password Change
31 | form(action="/account-password", method="post")
32 | fieldset
33 | label Current password
34 | br
35 | input(type="password", name="current_password", pattern="^.{6,}$", placeholder="Current password")
36 | br
37 | br
38 | label New password
39 | br
40 | input(type="password", name="new_password", pattern="^.{6,}$", placeholder="New password")
41 | br
42 | br
43 | label Retype new password
44 | br
45 | input(type="password", name="new_password_repeat", pattern="^.{6,}$", placeholder="Retype new password")
46 | .text-left
47 | button(type="submit" value="submit")
48 | | Change password
49 |
50 | br
51 | hr
52 | p.text-center
53 | a(href="/dashboard")
54 | button
55 | | Go back to my dashboard
56 |
--------------------------------------------------------------------------------
/sheet_model.js:
--------------------------------------------------------------------------------
1 | var deepcopy = require("deepcopy");
2 |
3 | var default_sheet = {
4 | "params":{
5 | "title":"Empty",
6 | "locked":false,
7 | },
8 | "cells":[
9 | ""
10 | ]
11 | };
12 |
13 | module.exports = {};
14 |
15 | module.exports.create = function(){
16 | var sheet = deepcopy(default_sheet);
17 |
18 | var exports = {};
19 |
20 | exports.edit = function(data){
21 | if(sheet.params.locked){
22 | return;
23 | }
24 |
25 | var number = parseInt(data.number);
26 | var content = data.content;
27 | var method = data.method;
28 |
29 | if(method == "insert"){
30 | sheet.cells.splice(number, 0, content);
31 | } else if(number >= 0){
32 | sheet.cells[number] = content;
33 | }
34 | }
35 |
36 | exports.remove = function(data){
37 | if(sheet.params.locked){
38 | return;
39 | }
40 |
41 | var number = data.number;
42 |
43 | var len = sheet.cells.length;
44 | if((number > 0 || len > 1) && number < len){
45 | sheet.cells.splice(number, 1);
46 | }
47 | }
48 |
49 | exports.lock = function(){
50 | sheet.params.locked = true;
51 | }
52 |
53 | exports.is_locked = function(){
54 | if(sheet.params.locked == undefined){
55 | sheet.params.locked = false;
56 | }
57 | return sheet.params.locked;
58 | }
59 |
60 | exports.set_sheet = function(data){
61 | if(sheet.params.locked){
62 | return sheet;
63 | }
64 |
65 | sheet = data;
66 | sheet.params = sheet.params || {};
67 | sheet.params.locked = sheet.params.locked || false;
68 |
69 | return sheet;
70 | }
71 |
72 | exports.get_sheet = function(){
73 | return sheet;
74 | }
75 |
76 | exports.get_length = function(){
77 | return sheet.cells.length + 1;
78 | }
79 |
80 | return exports;
81 | }
82 |
--------------------------------------------------------------------------------
/sass/header.scss:
--------------------------------------------------------------------------------
1 | header .links a{
2 | margin:0px 10px;
3 | }
4 |
5 | header h1{
6 | line-height:19px;
7 | font-family: 'Fira Sans';
8 | font-weight:300;
9 | }
10 |
11 | header p{
12 | margin:15px 20px;
13 | }
14 |
15 | @media (max-width:768px){
16 | header h1{
17 | text-align:center!important;
18 | display:block!important;
19 | }
20 | }
21 |
22 | header h1 a{
23 | color:#fff;
24 | }
25 |
26 | @media (max-width:500px){
27 | h1{
28 | margin:10px;
29 | margin-top:20px;
30 | display:block!important;
31 | text-align:center;
32 | }
33 | }
34 |
35 | header{
36 | background:$color_2;
37 | color:#fff;
38 | width:100%;
39 | display:block;
40 | padding-top:4px;
41 | padding-bottom:4px;
42 | position:relative;
43 | z-index:100;
44 | }
45 |
46 | @media (max-width:500px){
47 | header .col{
48 | display:block;
49 | width:100%;
50 | }
51 | }
52 |
53 | header a{
54 | color:$color_5;
55 | text-decoration:none;
56 | }
57 |
58 | header button{
59 | margin:0;
60 | padding:10px 12px;
61 | margin-top:6px;
62 | height:auto;
63 | border-radius:3px;
64 | border:2px solid $body_bg;
65 | }
66 |
67 | .new-sheet-button{
68 | color:#fff;
69 | display:inline-block;
70 | margin-right:20px;
71 | }
72 |
73 | @media (max-width:1250px){
74 | .new-sheet-button button{
75 | font-size:11px;
76 | margin-top:8px
77 | }
78 | }
79 |
80 | .new-sheet-button button{
81 | word-wrap: nowrap;
82 | }
83 |
84 | @media (max-width:768px){
85 | .new-sheet-button{
86 | margin-right:0;
87 | }
88 | header{
89 | padding-bottom:20px;
90 | }
91 | }
92 |
93 | nav{
94 | text-align:center;
95 | }
96 |
97 | nav ul{
98 | padding:0;
99 | margin:0;
100 | background:$color_5;
101 | }
102 |
103 | nav li{
104 | display: inline-block;
105 | padding:5px 20px;
106 | &:hover{
107 | background:darken($color_5, 10%);
108 | }
109 | }
110 |
111 | nav a{
112 | text-decoration: none;
113 | }
--------------------------------------------------------------------------------
/sass/base.scss:
--------------------------------------------------------------------------------
1 | /* general stuff that should be everywhere */
2 | .text-right{
3 | text-align:right;
4 | }
5 |
6 | .text-left{
7 | text-align:left;
8 | }
9 |
10 | .text-center{
11 | text-align:center;
12 | }
13 |
14 | .strong{
15 | font-weight:600;
16 | }
17 |
18 | .bigger{
19 | font-size:150%;
20 | }
21 |
22 | .smaller{
23 | font-size:80%!important;
24 | }
25 |
26 | @media (max-width:768px){
27 | .mobile-center{
28 | display:block!important;
29 | text-align:center;
30 | }
31 | }
32 |
33 | col-md-12,
34 | col-md-11,
35 | col-md-10,
36 | col-md-9,
37 | col-md-8,
38 | col-md-7,
39 | col-md-6,
40 | col-md-5,
41 | col-md-4,
42 | col-md-3,
43 | col-md-2,
44 | col-md-1{
45 | display:inline-block;
46 | vertical-align:top;
47 | }
48 |
49 | col-sm-12,
50 | col-sm-11,
51 | col-sm-10,
52 | col-sm-9,
53 | col-sm-8,
54 | col-sm-7,
55 | col-sm-6,
56 | col-sm-5,
57 | col-sm-4,
58 | col-sm-3,
59 | col-sm-2,
60 | col-sm-1{
61 | display:inline-block;
62 | vertical-align:top;
63 | }
64 |
65 | col-md-1{
66 | width:8%;
67 | }
68 |
69 | col-md-2{
70 | width:16%;
71 | }
72 |
73 | col-md-3{
74 | width:25%;
75 | }
76 |
77 | col-md-4{
78 | width:33%;
79 | margin: 0 -1px;
80 | }
81 |
82 | col-md-6{
83 | width:49.5%;
84 | }
85 |
86 | col-md-8{
87 | width:65%;
88 | }
89 |
90 | col-md-9{
91 | width:74%;
92 | }
93 |
94 | col-md-10{
95 | width:82%;
96 | }
97 |
98 | col-sm-6{
99 | width:49%;
100 | }
101 |
102 | @media (max-width:768px){
103 | col-md-1,
104 | col-md-2,
105 | col-md-3,
106 | col-md-4,
107 | col-md-5,
108 | col-md-6,
109 | col-md-7,
110 | col-md-8,
111 | col-md-9,
112 | col-md-10,
113 | col-md-11,
114 | col-md-12{
115 | display:block;
116 | width:100%;
117 | }
118 | }
119 |
120 | .hidden{
121 | display:none;
122 | }
123 |
124 | .flashing{
125 | background: $color_3 !important;
126 | color: #fff;
127 | }
128 |
129 | .inline-block{
130 | display:inline-block;
131 | vertical-align:top;
132 | }
133 |
134 | span.spacer{
135 | width:30px;
136 | }
--------------------------------------------------------------------------------
/views/livecalc-templates.pug:
--------------------------------------------------------------------------------
1 | template(name='livecalc')
2 | .livecalc
3 | .livecalc-header
4 | col-md-4.text-left.mobile-center
5 | .sheet-state
6 | col-md-4.text-center
7 | .user-count(title='(Including you)')
8 | col-md-4.text-right
9 | .user-info.mobile-center
10 | span.user-name
11 | | Anonymous
12 | if logged_in
13 | | -
14 | a(href="/dashboard", target="_blank") dashboard
15 | | -
16 | a(href="/logout") Log out
17 | else
18 | | -
19 | a(href="/login") Log in
20 | .livecalc-cells
21 | template-instance(data-name='sheet-panel')
22 | .panel
23 | h3 Settings
24 | template-instance(data-name='single-field',
25 | data-info='This name is used in the chat: '
26 | data-class='nickname',
27 | data-default='anonymous')
28 | template-instance(data-name='small-doc')
29 | template(name='sheet-panel')
30 | .sheet-panel.panel
31 | h3 Sheet Panel
32 | col-md-3
33 | | Lock sheet from further editing
34 | br
35 | button(name='lock-sheet')
36 | | Lock Sheet
37 | col-md-3
38 | | Open a copy
39 | br
40 | button(name='new-copy')
41 | | New Copy
42 | br
43 | h3 Stats
44 | p
45 | span.visit-count
46 | 0
47 | | visits.
48 | template(name='plot-interact-button')
49 | .text-center
50 | button.plot-interact
51 | | Interact with this plot
52 | template(name='fullscreen')
53 | .fullscreen
54 | .fullscreen-header
55 | col-sm-6
56 | h3.fullscreen-title
57 | col-sm-6
58 | .text-right
59 | button.close-button
60 | | Close
61 | .content
62 | template(name='livecalc-cell')
63 | .cell-and-button-wrapper
64 | .add-cell-button
65 | p.text-center.inner
66 | | +
67 | .livecalc-cell
68 | .delete-cell-button.icon.delete-icon
69 | .text-part
70 | .math-part
71 | fieldset
72 | div
73 | input.inline-block.livecalc-input(
74 | type='text',
75 | placeholder='Type math')
76 | button.inline-block.livecalc-go-button
77 | | Go
78 | pre.livecalc-output.
79 | pre.livecalc-secondary-output
80 | p.users-info
81 | .plot
82 |
--------------------------------------------------------------------------------
/views/doc.pug:
--------------------------------------------------------------------------------
1 | h2 livecalc.xyz Quick Reference
2 | col-md-6
3 | h3 Basics
4 | p
5 | | Type math:
6 | code 13*42*(3+32)/12
7 | br
8 | | Data is saved when you press GO/Enter
9 | br
10 | | Copy/Paste URL to share sheet
11 | br
12 | | Use backspace to delete an empty cell
13 | br
14 | | Use semicolon (;) to write what you
15 | | would write on many cells:
16 | br
17 | code a = 1; b=2; sin(a+b)
18 | br
19 | | Navigation: Tab/Shift+tab
20 | h3 Numbers
21 | p
22 | | You can use the electrical engineering
23 | | notation with numbers (n,u,m,k/K,meg/M,G)
24 | br
25 | | ex:
26 | code 1K
27 | | = 1000
28 | br
29 | h3 Functions
30 | p
31 | | You can define functions like that:
32 | code f(x) = x**2
33 | br
34 | | Then you can use f(x):
35 | code f(pi)
36 | h3 Constants
37 | p
38 | code pi
39 | | ,
40 | code e
41 | | (Euler),
42 | code i
43 | | (sqrt(-1)), etc.
44 | a(href='http://mathjs.org/docs/reference/constants.html',
45 | target='_blank') mathjs constants
46 | h3 Plots
47 | p
48 | | You can plot an expression:
49 | br
50 | code plot("sin(x+0.5)")
51 | br
52 | code plot("e**x")
53 | br
54 | | Plot more than one function at once:
55 | br
56 | code plot("e**x","sin(x)")
57 | h3 Fractals
58 | p
59 | | For 10 iterations
60 | | and a size of 140 pixels:
61 | br
62 | | Julia:
63 | code zfractal("z**2+0.4-0.05i",10,140)
64 | br
65 | | Mandelbrot:
66 | code zfractal("z**2+c",10,140)
67 | br
68 | | Cellular automaton:
69 | code rule(45)
70 | col-md-6
71 | h3 Math
72 | p
73 | code sin(3.14)
74 | br
75 | code sin(360deg)
76 | br
77 | code a = 30
78 | br
79 | code b = 3 * a
80 | br
81 | code b
82 | | (to show the value of 'b')
83 | br
84 | h3 Conversions
85 | p
86 | code 3 meters to inches
87 | br
88 | code 3 in to cm
89 | br
90 | code 1 gal to L
91 | br
92 | code 1 L to cm^3
93 | br
94 | code 4 bytes in bits
95 | br
96 | br
97 | h3 Comments
98 | p
99 | | Comments exist to let you explain what you are doing.
100 | br
101 | | Create text cells with this syntax:
102 | code // Comments can be used to describe what's next
103 | br
104 | | Once created, click a comment to edit it.
105 | br
106 | | Use comments at the end the line to describe
107 | | what you are doing:
108 | code 1+1 // Here is a simple addition
109 | h3 Bonus
110 | p
111 | code a=1K; b=2K; LL(a,b)
112 | | :
113 | | equivalent value of resistors in parallel.
114 |
--------------------------------------------------------------------------------
/user_cache.js:
--------------------------------------------------------------------------------
1 | var redis = require("redis");
2 |
3 | var client = redis.createClient();
4 |
5 | module.exports = {};
6 |
7 | function session_db_id(id){
8 | var id = id.replace(/[^A-Za-z0-9]/g,"");
9 | return "user_session:"+id;
10 | }
11 |
12 | module.exports.store_user = function(id, data){
13 | var data = JSON.stringify(data);
14 |
15 | var id = session_db_id(id);
16 |
17 | if(id.length == 0){
18 | return;
19 | }
20 |
21 | client.set(id, data, function(err, reply){
22 | if(err != null){
23 | console.log("err: " + err);
24 | }
25 | });
26 | };
27 |
28 | module.exports.logout = function(session_id){
29 | var id = session_db_id(session_id);
30 | client.del(id, function(err, reply){
31 | if(err != null){
32 | console.log("err: " + err);
33 | }
34 | });
35 | };
36 |
37 | /*
38 |
39 | Gets user from temp db
40 | callback(data)
41 |
42 | */
43 | module.exports.get_user = function(id, callback){
44 | var id = session_db_id(id);
45 |
46 | if(id.length == 0){
47 | return;
48 | }
49 |
50 | client.get(id, function(err, reply){
51 | if(err != null){
52 | console.log("err: " + err);
53 | }
54 | callback(JSON.parse(reply));
55 | });
56 | };
57 |
58 | /*
59 |
60 | Does a temp user exist in redis?
61 |
62 | callback(bool: exists)
63 |
64 | */
65 | module.exports.exists = function(id, callback){
66 | if(id == undefined || id == ""){
67 | return false;
68 | }
69 |
70 | var id = session_db_id(id);
71 |
72 | if(id.length == 0){
73 | callback(false);
74 | return;
75 | }
76 |
77 | client.exists(id, function(err, exists){
78 | if(err != null){
79 | console.log("err: " + err);
80 | }
81 | callback(exists);
82 | });
83 | };
84 |
85 | function visited_sheets_id(user_id){
86 | var visited_id = "user_sheet_visits:"+user_id+":";
87 |
88 | return visited_id;
89 | }
90 |
91 | /*
92 | Call whenever a sheet is visited by a user
93 | */
94 | module.exports.visit_sheet = function(user_id, sheet_ids){
95 | var visited_id = visited_sheets_id(user_id);
96 |
97 | client.lpush(visited_id, sheet_ids, function(err, reply){
98 | if(err != null){
99 | console.log("err: " + err);
100 | }
101 | });
102 | };
103 |
104 | /*
105 | Call whenever a sheet is visited by a user
106 | callback(sheets)
107 | */
108 | module.exports.recently_visited_sheets = function(user_id, callback){
109 | var visited_id = visited_sheets_id(user_id);
110 |
111 | client.lrange(visited_id, -10, -1, function(err, reply){
112 | if(err != null){
113 | console.log("err: " + err);
114 | } else {
115 | callback(reply);
116 | }
117 | });
118 | };
119 |
--------------------------------------------------------------------------------
/public/icons/livecalc.svg:
--------------------------------------------------------------------------------
1 |
2 |
81 |
--------------------------------------------------------------------------------
/public/logo/livecalc.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
88 |
--------------------------------------------------------------------------------
/sass/livecalc.scss:
--------------------------------------------------------------------------------
1 | .livecalc-cell{
2 | margin-top:5px;
3 | margin-bottom:5px;
4 | position:relative;
5 | }
6 |
7 | .cell-and-button-wrapper{
8 | .delete-cell-button{
9 | position:absolute;
10 | cursor:pointer;
11 | top:-10px;
12 | right:-10px;
13 | opacity:0.1;
14 | transition:all 0.3s;
15 | &:hover{
16 | opacity:1;
17 | transform:rotate(90deg); /* oh yeah! */
18 | }
19 | }
20 |
21 | /* last cell can't be deleted anyway */
22 | &:last-child .delete-cell-button{
23 | display:none;
24 | }
25 | }
26 |
27 | .livecalc-cell .text-part{
28 | background:#fff;
29 | color:#000;
30 | padding:10px;
31 | padding-left:20px;
32 | font-family:monospace;
33 | font-size:13px;
34 | cursor:pointer;
35 | }
36 |
37 | .livecalc-cell .math-part,
38 | .livecalc .livecalc-header{
39 | background:$color_4;
40 | padding-top:5px;
41 | padding-bottom:5px;
42 | }
43 |
44 | .livecalc fieldset{
45 | border:none;
46 | margin:0;
47 | padding:0;
48 | padding-top:5px;
49 | }
50 |
51 | .livecalc .livecalc-input{
52 | width: 100%;
53 | width: calc(100% - 150px);
54 | margin-left:10px;
55 | font-size:20px;
56 | margin-bottom:5px;
57 | color:#444;
58 | border:none;
59 | background:#fff;
60 | box-shadow:none;
61 | font-family:monospace;
62 | }
63 |
64 | .livecalc-secondary-output,
65 | .livecalc-output{
66 | margin-top:5px;
67 | margin-left:10px;
68 | margin-bottom:5px;
69 | margin-right:10px;
70 | min-height:20px;
71 | background:#fff;
72 | font-size:20px;
73 | padding:5px;
74 | padding-left:20px;
75 | border-radius: 2px;
76 | }
77 |
78 | .livecalc-secondary-output{
79 | min-height:0;
80 | font-size:10px;
81 | margin-top:10px;
82 | }
83 |
84 | .livecalc-go-button{
85 | height:32px;
86 | margin:0;
87 | margin-left:10px;
88 | width:100px;
89 | border-radius:0;
90 | border:none;
91 | padding:5px;
92 | background:$color_2;
93 | color:#eee;
94 | border-bottom:2px solid $body_bg;
95 | }
96 |
97 | .livecalc .users-info{
98 | text-align:right;
99 | font-size:11px;
100 | margin:5px;
101 | }
102 |
103 | .livecalc .plot{
104 | margin:10px 10px;
105 | }
106 |
107 | .livecalc .plot .livecalc-wait-click,
108 | .livecalc .plot svg,
109 | .livecalc .plot canvas{
110 | background:#fff;
111 | margin:auto;
112 | display:block;
113 | }
114 |
115 | .livecalc-wait-click{
116 | max-width:400px;
117 | text-align:center;
118 | padding:10px;
119 | padding-bottom:2px;
120 | border-radius:2px;
121 | }
122 |
123 | .livecalc-wait-click-button{
124 | margin-bottom:0;
125 | box-shadow: 0 0 5px rgba(0,0,0,0.1);
126 | }
127 |
128 | .livecalc-header{
129 | text-align:left;
130 | font-size:14px;
131 | color:darken($color_3, 20%);
132 | }
133 |
134 | .livecalc-header .sheet-state{
135 | margin-left:10px;
136 | }
137 |
138 | .livecalc-header .user-info{
139 | padding-right:10px;
140 | }
141 |
142 | livecalc{
143 | overflow-x:hidden;
144 | }
145 |
146 | .panel{
147 | background:$color_4;
148 | padding:10px;
149 | margin-top:10px;
150 | margin-bottom:10px;
151 | }
152 |
153 | .livecalc-sharing a{
154 | margin-right:10px;
155 | }
156 |
157 | .add-cell-button{
158 | margin-top:5px;
159 | .inner{
160 | background:darken($color_4,10%);
161 | border-radius:3px;
162 | width:20px;
163 | margin:auto;
164 | color:#fff;
165 | line-height:5px;
166 | cursor:pointer;
167 | transition: line-height 0.1s;
168 | &:hover{
169 | background:darken($color_4,20%);
170 | line-height:15px;
171 | }
172 | }
173 | }
--------------------------------------------------------------------------------
/public/icons/livecalc-source.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
118 |
--------------------------------------------------------------------------------
/user_db.js:
--------------------------------------------------------------------------------
1 | /* user_db.js */
2 | /*
3 | For the moment, this deals with both redis and mongo,
4 | this could be changed.
5 | */
6 |
7 | var password_hash = require("password-hash");
8 |
9 | module.exports = {};
10 |
11 | var mongoose = require('mongoose');
12 |
13 | mongoose.connect('mongodb://localhost/livecalc');
14 |
15 | var db = mongoose.connection;
16 |
17 | db.on('error', console.error.bind(console, 'connection error:'));
18 |
19 | db.once('open', function() {
20 | console.log("mongo connection");
21 | });
22 |
23 | var UserSchema = new mongoose.Schema({
24 | name: String,
25 | public_id: String, /* to know who sent what message, for example.
26 | Since it is visible to everybody, it must not
27 | allow to do dangerous things.
28 | */
29 | username: String,
30 | password: String,
31 | email: String,
32 | nickname: String,
33 | sheets: Array,
34 | recent_sheets: Array
35 | });
36 |
37 | UserSchema.methods.verify_password = function(given_password){
38 | if(password_hash.verify(given_password, this.password)){
39 | return true;
40 | }
41 |
42 | return false;
43 | };
44 |
45 | UserSchema.methods.update_password = function(new_password){
46 | this.password = hash_password(new_password);
47 | this.save();
48 | };
49 |
50 | module.exports.create = create;
51 |
52 | function create(data, callback){
53 | if(data.password == ""){
54 | console.log("Error: empty password got to user_db.create.");
55 | }
56 |
57 | var user = new User({
58 | name: data.name || "",
59 | username: data.username || "",
60 | email: data.email || "",
61 | nickname: data.nickname || "",
62 | password: hash_password(data.password)
63 | });
64 |
65 | user.save(function(err){
66 | if(err){
67 | console.log(err);
68 | } else {
69 | console.log("New user saved to mongo: " + user.username);
70 | }
71 | callback();
72 | });
73 | }
74 |
75 | /*
76 |
77 | Fetches data from mongodb
78 | Puts it in redis
79 |
80 | */
81 | UserSchema.methods.login = function(cache_user_model, session_id){
82 | var user = cache_user_model.create();
83 | var public_id = user.get_public_id();
84 |
85 | if(this.public_id == undefined || this.public_id == ""){
86 | // Set public id for the first time
87 | this.public_id = public_id;
88 | this.save();
89 | } else {
90 | // Use public id from db
91 | public_id = this.public_id;
92 | user.set_public_id(public_id);
93 | }
94 |
95 | user.set_permanent_user(this);
96 | user.set_nickname(this.nickname);
97 | user.set_session_id(session_id);
98 | user.set_public_id(public_id);
99 | user.save();
100 |
101 | return public_id;
102 | };
103 |
104 | var User = mongoose.model('User', UserSchema);
105 |
106 | /**
107 | This is supposed to hash passwords
108 | */
109 | function hash_password(password){
110 | var hashed = password_hash.generate(password);
111 | return hashed;
112 | }
113 |
114 | module.exports.exists_email = exists_email;
115 |
116 | /*
117 | Exists in mongo ?
118 | (Also gives the user if it exists)
119 | callback(exists: true | false, [user])
120 | */
121 | function exists_email(email, callback){
122 | User.find({ email: email }, function(err, user){
123 | if(err){console.log(err)};
124 |
125 | if(user.length > 0){
126 | callback(true, user[0]);
127 | } else {
128 | callback(false, user[0]);
129 | }
130 | });
131 | }
132 |
133 | module.exports.exists_username = exists_username;
134 |
135 | /*
136 | Exists in mongo ?
137 | callback(exists: true | false)
138 | */
139 | function exists_username(username, callback){
140 | User.find({ username: username }, function(err, user){
141 | if(err){console.log(err)};
142 | if(user.length > 0){
143 | callback(true);
144 | } else {
145 | callback(false);
146 | }
147 | });
148 | }
149 |
150 | module.exports.get_user_by_id = get_user_by_id;
151 |
152 | function get_user_by_id(id, callback){
153 | User.findById(id, function(err, user){
154 | if(err){console.log(err)};
155 | if(user != null){
156 | callback(user);
157 | } else {
158 | callback(null);
159 | }
160 | });
161 | }
162 |
--------------------------------------------------------------------------------
/views/pricing.pug:
--------------------------------------------------------------------------------
1 | .text-center
2 | h1
3 | | Pricing
4 | p
5 | | Registered, Premium and Teacher Premium accounts are not yet available, but here is what we plan to offer*:
6 | p
7 | | *The features may change, livecalc.xyz is still in heavy development.
8 | p
9 | | If you are interested in trying these future premium accounts, sign up below!
10 | p.smaller
11 | | (Hurry up! Our first clients will receive a couple of free premium months!)
12 | br
13 | .text-center
14 | table.pricing-table
15 | tr
16 | th
17 |
18 | th
19 | | Free
20 | th
21 | | Free Registered*
22 | br
23 | span.coming-soon *Coming soon
24 | th
25 | | Premium*
26 | br
27 | span.coming-soon *Coming soon
28 | th
29 | | Teacher Premium*
30 | br
31 | span.coming-soon *Coming soon
32 | tr
33 | td.col-feature
34 | | Create public math
35 | br
36 | | sheets
37 | td.yes
38 | | yes
39 | td.yes
40 | | yes
41 | td.yes
42 | | yes
43 | td.yes
44 | | yes
45 | tr
46 | td.col-feature
47 | | Use chat
48 | td.yes
49 | | yes
50 | td.yes
51 | | yes
52 | td.yes
53 | | yes
54 | td.yes
55 | | yes
56 | tr
57 | td.col-feature
58 | | Plot functions
59 | td.yes
60 | | yes
61 | td.yes
62 | | yes
63 | td.yes
64 | | yes
65 | td.yes
66 | | yes
67 | tr
68 | td.col-feature
69 | | Lock sheets
70 | td.yes
71 | | yes
72 | td.yes
73 | | yes
74 | td.yes
75 | | yes
76 | td.yes
77 | | yes
78 | tr
79 | td.col-feature
80 | | Unlock sheets
81 | td
82 | | no
83 | td.yes
84 | | yes
85 | td.yes
86 | | yes
87 | td.yes
88 | | yes
89 | tr
90 | td.col-feature
91 | | Import student accounts
92 | td
93 | | no
94 | td
95 | | no
96 | td
97 | | no
98 | td.yes
99 | | yes
100 | tr
101 | td.col-feature
102 | | Online users / sheet
103 | td
104 | | up to 3
105 | td
106 | | up to 5
107 | td
108 | | up to 8
109 | td
110 | | up to 32
111 | tr
112 | td.col-feature
113 | | Active sheet / user
114 | td
115 | | 2
116 | td
117 | | 4
118 | td
119 | | 8
120 | td
121 | | 4 / student
122 | br
123 | span.smaller 8 for you
124 | tr
125 | td.col-feature
126 | | Facebook notifications
127 | td
128 | | no
129 | td.yes
130 | | yes
131 | td.yes
132 | | yes
133 | td.yes
134 | | yes
135 | tr
136 | td.col-feature
137 | | Email notifications
138 | td
139 | | no
140 | td.yes
141 | | yes
142 | td.yes
143 | | yes
144 | td.yes
145 | | yes
146 | tr
147 | td.col-feature
148 | | Visited sheet list
149 | td
150 | | no
151 | td.yes
152 | | yes
153 | td.yes
154 | | yes
155 | td.yes
156 | | yes
157 | tr
158 | td.col-feature
159 | | Create public templates
160 | td
161 | | no
162 | td.yes
163 | | yes
164 | td.yes
165 | | yes
166 | td.yes
167 | | yes
168 | tr
169 | td.col-feature
170 | | Upload images
171 | td
172 | | no
173 | td.yes
174 | | yes
175 | td.yes
176 | | yes
177 | td.yes
178 | | yes
179 | tr
180 | td.col-feature
181 | | Private sheets
182 | td
183 | | none
184 | td
185 | | 5
186 | td.yes
187 | | unlimited
188 | td.yes
189 | | unlimited
190 | tr
191 | td.col-feature
192 | | Price
193 | td
194 | | Free
195 | td
196 | | Free
197 | td
198 | span.bigger.strong $3
199 | | /month
200 | td
201 | span.bigger.strong $12
202 | | /month
203 | tr
204 | td.col-feature
205 | |
206 | td
207 | a(href="/new")
208 | button
209 | | Create a new sheet!
210 | td.receive-alert(colspan=3)
211 | | Receive email
212 | | alerts when these become available:
213 | br
214 | br
215 | form(action="/marketing/newsletter_signup", method="post")
216 | input(type="email", name="email", placeholder="email@email.com")
217 | button(type="submit" value="submit")
218 | | Sign up
219 |
--------------------------------------------------------------------------------
/sass/style.scss:
--------------------------------------------------------------------------------
1 | $color_1: #3a260d;
2 | $color_2: #172a3a;
3 | $color_3: #729b79;
4 | $color_4: #dddddd;
5 | $color_5: #bacdb0;
6 | $color_6: #475b63;
7 | $color_7: #2e2c2f;
8 |
9 | $body_bg: #f3e8ee;
10 |
11 | $error_color: #733;
12 |
13 | @import 'base.scss';
14 |
15 | @import 'livecalc-icons.scss';
16 |
17 | @import 'pricing.scss';
18 |
19 | @import 'landing.scss';
20 |
21 | @import 'signup.scss';
22 |
23 | @import 'sidebar.scss';
24 |
25 | @import 'chat.scss';
26 |
27 | @import 'livecalc.scss';
28 |
29 | @import 'dashboard.scss';
30 |
31 | @import 'account.scss';
32 |
33 | /* static pages */
34 |
35 | .static h1{
36 | font-size:35px;
37 | line-height:50px;
38 | }
39 |
40 |
41 | /* server messages */
42 |
43 | .server-message{
44 | width:80%;
45 | margin:auto;
46 | padding:20px;
47 | margin-top:20px;
48 | }
49 |
50 | .server-message.positive{
51 | background:$color_3;
52 | color:#fff;
53 | }
54 |
55 | .server-message.negative{
56 | background:$error_color;
57 | color:#fff;
58 | }
59 |
60 |
61 | /* page-related stuff */
62 | body{
63 | background:$body_bg;
64 | font-family: sans-serif;
65 | font-family: 'Fira Sans';
66 | padding:0;
67 | margin:0;
68 | width:100%;
69 | height:100%;
70 | }
71 |
72 | body a{
73 | color:$color_2;
74 | }
75 |
76 | body,
77 | window{
78 | overflow-x:hidden;
79 | max-width:100%;
80 | }
81 |
82 | h1{
83 | font-size:25px;
84 | font-weight:300;
85 | margin-right:40px;
86 | margin-left:20px;
87 | }
88 |
89 | @import "header.scss";
90 |
91 | template{
92 | display:none;
93 | }
94 |
95 | button{
96 | min-height:32px;
97 | margin-bottom:20px;
98 | margin-top:5px;
99 | border-radius:0;
100 | border:none;
101 | padding:5px;
102 | background:$color_3;
103 | font-size:14px;
104 | color:#fff;
105 | border-radius:2px;
106 | cursor:pointer;
107 | word-wrap: break-word;
108 | transition:all 0.3s;
109 |
110 | &:hover{
111 | background:darken($color_3,10%);
112 | }
113 | }
114 |
115 | button:first-child{
116 | margin-left:0;
117 | }
118 |
119 | input{
120 | border:none;
121 | padding:0px 5px;
122 | background:#fff;
123 | box-shadow:0 0 3px rgba(0,0,0,0.2);
124 | height:30px;
125 | }
126 |
127 | h3{
128 | font-family: 'Fira Sans';
129 | font-weight:500;
130 | font-size:120%;
131 | margin-left:0;
132 | margin:5px 0;
133 | color:#000;
134 | }
135 |
136 | .doc{
137 | background:#fff;
138 | margin-top:10px;
139 | padding:20px;
140 | padding-top:20px;
141 | }
142 |
143 | .doc h3{
144 | margin-bottom:-10px;
145 | }
146 |
147 | h2{
148 | color:$color_2;
149 | margin-bottom:6px;
150 | margin-top:0px;
151 | }
152 |
153 | p{
154 | line-height:140%;
155 | }
156 |
157 | .doc p{
158 | line-height:150%;
159 | }
160 |
161 | .doc code{
162 | background:#eee;
163 | color:$color_1;
164 | display:inline-block;
165 | margin:5px 0;
166 | padding:0 5px;
167 | cursor:pointer;
168 | border-radius:3px;
169 | }
170 |
171 | .main-page{
172 | margin-left:10px;
173 | margin-right:10px;
174 | }
175 |
176 | /* modals */
177 |
178 | .modal-overlay{
179 | position:fixed;
180 | z-index:1000;
181 | top:0;
182 | left:0;
183 | width:100%;
184 | height:100%;
185 | background:#333;
186 | background:rgba(0,0,0,0.3);
187 | }
188 |
189 | .modal{
190 | width:400px;
191 | display:block;
192 | margin:auto;
193 | margin-top:100px;
194 | min-height:100px;
195 | background:#fff;
196 | color:#000;
197 | padding:20px;
198 | border:10px solid #eee;
199 | border-radius:3px;
200 | position:relative;
201 | z-index:1100;
202 | }
203 |
204 | @media all and (max-width:768px){
205 | .modal{
206 | width:90%;
207 | width:calc( 100% - 20px );
208 | border:0px;
209 | margin-top:20px;
210 | padding:10px;
211 | margin-left:0;
212 | margin-right:0;
213 | border:none;
214 | position:relative;
215 | z-index:1000;
216 | }
217 | }
218 |
219 | .modal .buttons{
220 | text-align:center;
221 | }
222 |
223 | .modal button{
224 | margin-right: 15px;
225 | }
226 |
227 | .fullscreen{
228 | background:lighten($body_bg, 10%);
229 | width:100%;
230 | height:100%;
231 | position:fixed;
232 | top:0;
233 | left:0;
234 | z-index:1000;
235 | text-align:center;
236 | }
237 |
238 | .fullscreen-header{
239 | background:#eee;
240 | margin-bottom:5px;
241 | }
242 |
243 | .fullscreen-title{
244 | margin-top:25px;
245 | text-align:left;
246 | margin-left:30px;
247 | }
248 |
249 | .fullscreen .close-button{
250 | margin-right:20px;
251 | margin-top:20px;
252 | }
253 |
254 | .function-plot {
255 | font-size: 10px;
256 | }
257 |
258 | .plot-interact{
259 | margin-top: 30px;
260 | }
261 |
262 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var express = require("express");
2 | var body_parser = require("body-parser");
3 | var app = express();
4 | var http = require("http").Server(app);
5 | var io = require("socket.io")(http);
6 | var sass_middleware = require("node-sass-middleware");
7 | var livecalc = require("./livecalc");
8 | var sheet_db = require("./sheet_db");
9 | var cache_user_model = require("./cache_user_model");
10 | var chat_db = require("./chat_db");
11 | var stats = require("./stats");
12 | var cookie_utils = require("./user/cookie_utils");
13 |
14 | livecalc.set_globals(io, sheet_db, chat_db, stats, cache_user_model);
15 |
16 | app.use(body_parser.json());
17 | app.use(body_parser.urlencoded({extended: true}));
18 |
19 | // Add some user info for render
20 | app.use(function(req, res, next){
21 | res.locals.page = "not-specified";
22 | get_user_model(req, function(user){
23 | res.locals.user = user || null;
24 | if(user == null){
25 | // not logged in
26 | res.locals.logged_in = false;
27 | } else {
28 | // logged in
29 | res.locals.logged_in = true;
30 | }
31 | next();
32 | });
33 | });
34 |
35 | /* Stylesheets */
36 | app.use(sass_middleware({
37 | src: "./sass",
38 | dest: "./public/css",
39 | debug: false,
40 | sourceMap: true,
41 | outputStyle: "compressed",
42 | prefix: "/css"
43 | }));
44 |
45 | // Serve static files
46 | app.use(express.static('public'));
47 |
48 | app.locals.pretty = true;
49 |
50 | // Views
51 | app.set('views', './views')
52 | app.set('view engine', 'pug');
53 |
54 | app.get('/', function (req, res) {
55 | stats.log_visit("landing-page");
56 | res.render('base',{page: "landing"});
57 | });
58 |
59 | // All the user pages (account, login, etc.)
60 | var user_pages = require("./user/user_pages")(app, cache_user_model)
61 |
62 | // All the marketing pages (pricing)
63 | var marketing_pages = require("./marketing/marketing")(app, stats)
64 |
65 | function is_logged_in(req, callback){
66 | var session_id = cookie_utils.cookie_get_id(req) || "";
67 |
68 | if(session_id == ""){
69 | callback(false);
70 | }
71 |
72 | cache_user_model.temp_exists(session_id, function(exists){
73 | if(exists){
74 | return callback(true);
75 | } else {
76 | return callback(false);
77 | }
78 | });
79 | }
80 |
81 | /*
82 | Make sure user is logged in before calling this.
83 | callback(cached_user)
84 | */
85 | function get_user_model(req, callback){
86 | var session_id = cookie_utils.cookie_get_id(req) || "";
87 |
88 | is_logged_in(req, function(logged_in){
89 | if(logged_in){
90 | user = cache_user_model.cached_user(session_id);
91 | user.fetch(function(){
92 | callback(user);
93 | });
94 | } else {
95 | callback(null);
96 | }
97 | });
98 | }
99 |
100 | app.get("/sheet/:id",function (req, res) {
101 | var sheet_id = req.params.id;
102 | var logged_in = res.locals.logged_in;
103 | var user = res.locals.user || null;
104 |
105 | // Todo: unhardcode domain and protocol
106 | var share_url =
107 | "https://livecalc.xyz/sheet/" +
108 | sheet_id;
109 |
110 | res.locals.share_url = share_url;
111 |
112 | // See if namespace is already in memory
113 | // Else we check in DB
114 | if(livecalc.namespaces.indexOf(sheet_id) <= -1){
115 | sheet_db.exists(sheet_id, function(exists){
116 | // Maybe then it exists in redis
117 | if(exists){
118 | livecalc.new_namespace(sheet_id);
119 | res.render('base',{
120 | in_sheet: true,
121 | page: "sheet"
122 | });
123 |
124 | // For "recently visited sheets"
125 | if(user != null){
126 | user.visit_sheets(sheet_id);
127 | }
128 | } else {
129 | // Else, sheet not found
130 | res.status(404).send('Not Found');
131 | }
132 | });
133 | } else {
134 | // Sheet exists
135 | res.render('base',{
136 | in_sheet: true,
137 | page: "sheet"
138 | });
139 | }
140 | });
141 |
142 | app.get("/copy/:id",function (req, res) {
143 | var sheet_id = req.params.id;
144 | var new_id = require("./tokens").generate_token();
145 |
146 | sheet_db.get_sheet(sheet_id, function(data){
147 | data.params.locked = false;
148 | sheet_db.store_sheet(new_id, data);
149 | res.redirect("/sheet/"+new_id);
150 | });
151 | });
152 |
153 | app.get("/new",function (req, res) {
154 | // Todo: verify if a sheet already has same token
155 | // Not so probable...
156 | // Nope, new sheet
157 | stats.new_sheet();
158 |
159 | var token = require("./tokens").generate_token();
160 |
161 | var model = require("./sheet_model").create();
162 |
163 | sheet_db.store_sheet(token, model.get_sheet());
164 |
165 | res.redirect("/sheet/"+token);
166 | });
167 |
168 | http.listen(3000);
169 |
--------------------------------------------------------------------------------
/cache_user_model.js:
--------------------------------------------------------------------------------
1 | /* cache_user_model.js */
2 |
3 | /* todo: untangle this and user_db.js */
4 |
5 | var db = require("./user_db");
6 | var user_cache = require("./user_cache");
7 |
8 | module.exports = {};
9 |
10 | module.exports.db = db;
11 |
12 | module.exports.cached_user = cached_user;
13 |
14 | function cached_user(session_id, public_id){
15 | var exports = {};
16 |
17 | var data = {
18 | public_id: public_id || "",
19 | session_id: session_id || "",
20 | nickname: "Anonymous",
21 | username: "",
22 | permanent_id: null,
23 | recent_sheets: []
24 | };
25 |
26 | var permanent_user = null;
27 |
28 | exports.get_permanent_user = function(){
29 | return permanent_user;
30 | };
31 |
32 | exports.set_permanent_user = set_permanent_user;
33 |
34 | function set_permanent_user(new_permanent_user){
35 | permanent_user = new_permanent_user;
36 | data.username = permanent_user.username;
37 | data.email = permanent_user.email;
38 | data.nickname = permanent_user.nickname;
39 | data.permanent_id = permanent_user.id;
40 | data.recent_sheets = permanent_user.recent_sheets;
41 | }
42 |
43 | exports.visit_sheets = visit_sheets;
44 |
45 | /*
46 | Can be an array or a single element
47 | */
48 | function visit_sheets(sheet_ids){
49 | user_cache.visit_sheet(data.permanent_id, sheet_ids);
50 | }
51 |
52 | exports.recently_visited_sheets = recently_visited_sheets;
53 |
54 | function recently_visited_sheets(callback){
55 | user_cache.recently_visited_sheets(
56 | data.permanent_id, callback
57 | );
58 | }
59 |
60 | exports.fetch_permanent_user = fetch_permanent_user;
61 |
62 | /*
63 | Find user in mongo
64 | to prepare for update
65 | */
66 | function fetch_permanent_user(callback){
67 | if(data.permanent_id == null){
68 | console.log("Error: cannot fetch permanent user. "+
69 | "permanent_id is not known.");
70 | }
71 |
72 | db.get_user_by_id(data.permanent_id, function(user){
73 | if(user == null){
74 | console.log("Error: permanent id not linked to a user.");
75 | return;
76 | }
77 | set_permanent_user(user);
78 | callback();
79 | });
80 | }
81 |
82 | /*
83 | Save in mongo
84 | */
85 | exports.save_permanent = function(){
86 | permanent_user.save();
87 | };
88 |
89 | /*
90 | Save in cache
91 | */
92 | exports.save = function(){
93 | user_cache.store_user(data.session_id, data);
94 | };
95 |
96 | /*
97 | Fetch from cache
98 | callback()
99 | */
100 | exports.fetch = function(callback){
101 | user_cache.get_user( data.session_id, function(from_cache){
102 | data = from_cache;
103 | callback();
104 | });
105 | };
106 |
107 | /*
108 | Should be only data safe for frontend
109 | */
110 | exports.get_public_data = function(){
111 | return {
112 | public_id: data.public_id,
113 | nickname: data.nickname,
114 | focus: -1
115 | }
116 | };
117 |
118 | exports.get_public_id = function(){
119 | return data.public_id;
120 | };
121 |
122 | exports.set_public_id = function(new_id){
123 | data.public_id = new_id;
124 | };
125 |
126 | exports.get_session_id = function(){
127 | return data.session_id;
128 | };
129 |
130 | exports.set_session_id = function(new_id){
131 | data.session_id = new_id;
132 | };
133 |
134 | exports.set_nickname = function(new_nickname){
135 | if(permanent_user != null){
136 | permanent_user.nickname = new_nickname;
137 | permanent_user.save();
138 | }
139 | data.nickname = new_nickname;
140 | };
141 |
142 | exports.get_nickname = function(){
143 | return data.nickname;
144 | };
145 |
146 | exports.set_username = function(new_username){
147 | if(permanent_user != null){
148 | permanent_user.username = new_username;
149 | permanent_user.save();
150 | }
151 | data.username = new_username;
152 | };
153 |
154 | exports.get_username = function(){
155 | return data.username;
156 | };
157 |
158 | exports.set_email = function(new_email){
159 | if(permanent_user != null){
160 | permanent_user.email = new_email;
161 | permanent_user.save();
162 | }
163 | data.email = new_email;
164 | };
165 |
166 | exports.get_email = function(){
167 | return data.email;
168 | };
169 |
170 | return exports;
171 | }
172 |
173 | module.exports.create = function(){
174 | var session_id = require("./tokens").generate_token(6);
175 | var public_id = require("./tokens").generate_token(6);
176 | return cached_user(session_id, public_id);
177 | };
178 |
179 | module.exports.temp_exists = function(id, callback){
180 | user_cache.exists(id, callback);
181 | };
182 |
183 | module.exports.logout = function(session_id){
184 | user_cache.logout(session_id);
185 | };
186 |
--------------------------------------------------------------------------------
/user/user_pages.js:
--------------------------------------------------------------------------------
1 | /* Handles request to user pages */
2 |
3 | var cookie_utils = require("./cookie_utils");
4 |
5 | module.exports = function(app, cache_user_model){
6 | app.get('/signup', function (req, res) {
7 | if(!res.locals.logged_in){
8 | // If not logged in
9 | res.render('base',{page: "signup"});
10 | } else {
11 | // If logged in, redirect to user dashboard
12 | res.redirect("/dashboard");
13 | }
14 | });
15 |
16 | app.get('/account', function (req, res) {
17 | if(res.locals.logged_in){
18 | // If logged in
19 | res.render('base',{page: "account"});
20 | } else {
21 | // Else, go to signup
22 | res.redirect("/signup");
23 | }
24 | });
25 |
26 | function get_ip(req){
27 | // http://stackoverflow.com/questions/10849687/express-js-how-to-get-remote-client-address
28 | var ip = req.headers['x-forwarded-for'] ||
29 | req.connection.remoteAddress;
30 |
31 | return ip;
32 | }
33 |
34 | app.get('/account-username',function(req, res){
35 | res.redirect("/account");
36 | });
37 |
38 | app.post('/account-username', function (req, res) {
39 | var user = res.locals.user || null;
40 |
41 | if(user == null){
42 | res.redirect("/signup");
43 | return;
44 | }
45 |
46 | user.fetch_permanent_user(function(){
47 | var username = req.body.username || "";
48 | var user_db = cache_user_model.db;
49 |
50 | // Until not true,
51 | var success = true;
52 | var errors = [];
53 |
54 | // Username length
55 | if(username.length < 6){
56 | success = false;
57 | errors.push("Username is too short.");
58 | }
59 |
60 | // Username length
61 | if(username.length > 40){
62 | success = false;
63 | errors.push("Username is too long.");
64 | }
65 |
66 | // Username format
67 | if(!username.match(/^[A-Za-z0-9]*$/)){
68 | success = false;
69 | errors.push("Username contains invalid characters.");
70 | }
71 |
72 | // Check username for existence
73 | user_db.exists_username(username, function(exists){
74 | if(exists){
75 | success = false;
76 | errors.push("This username is already used by someone.");
77 | }
78 |
79 | // If still successful
80 | if(success == true){
81 | // Save user to mongo db
82 | user.set_username(username);
83 | account_render(req, res, true, ["Your username was changed"]);
84 | } else {
85 | account_render(req, res, false, errors);
86 | }
87 | });
88 | });
89 | });
90 |
91 | app.get('/account-password',function(req, res){
92 | res.redirect("/account");
93 | });
94 |
95 | app.post('/account-password', function (req, res) {
96 | var user = res.locals.user || null;
97 | var current_password = req.body.current_password || "";
98 | var new_password = req.body.new_password || "";
99 | var new_password_repeat = req.body.new_password_repeat || "";
100 |
101 | if(user == null){
102 | res.redirect("/signup");
103 | return;
104 | }
105 |
106 | user.fetch_permanent_user(function(){
107 | var perm = user.get_permanent_user();
108 |
109 | if(!perm.verify_password(current_password)){
110 | account_render(req, res, false, ["Wrong password"]);
111 | return;
112 | }
113 |
114 | // Password length
115 | if(new_password.length < 6){
116 | account_render(req, res, false, ["Password is too short."]);
117 | return;
118 | }
119 |
120 | // Password & password repead match
121 | if(new_password_repeat != new_password){
122 | account_render(req, res, false,
123 | ["New passwords do not match."]);
124 | return;
125 | }
126 |
127 | perm.update_password(new_password);
128 | account_render(req, res, true, "Password updated!");
129 | });
130 | });
131 |
132 | app.get('/account-email',function(req, res){
133 | res.redirect("/account");
134 | });
135 |
136 | app.post('/account-email', function (req, res) {
137 | var user = res.locals.user || null;
138 |
139 | if(user == null){
140 | res.redirect("/signup");
141 | return;
142 | }
143 |
144 | var validator = require("email-validator");
145 | var email = req.body.email || "";
146 |
147 | // Email validation
148 | if(!validator.validate(email)){
149 | // Nope
150 | account_render(req, res, false, ["Email address is not valid."]);
151 | return;
152 | }
153 |
154 | user.fetch_permanent_user(function(){
155 | var user_db = cache_user_model.db;
156 |
157 | // Check email
158 | user_db.exists_email(email, function(exists){
159 | if(!exists){
160 | // Ok!
161 | user.set_email(email);
162 | account_render(req, res, true,
163 | "Your email address was updated.");
164 | } else {
165 | // Nope
166 | account_render(req, res, false, ["Email " +
167 | email
168 | + " already in use."]);
169 | return;
170 | }
171 | });
172 | });
173 | });
174 |
175 | function account_render(req, res, success, message){
176 | if(success != ""){
177 | res.render('base',{
178 | page: "account",
179 | positive_message: true,
180 | message: message
181 | });
182 | } else {
183 | var ip = get_ip(req);
184 | console.log("unsuccessful account operation: " + ip);
185 |
186 | res.render('base',{
187 | page: "account",
188 | negative_message: true,
189 | message: message.join(" ")
190 | });
191 | }
192 | }
193 |
194 | app.get('/login', function (req, res) {
195 | if(!res.locals.logged_in){
196 | // If not logged in
197 | res.render('base',{page: "login"});
198 | } else {
199 | // If logged in, redirect to user dashboard
200 | res.redirect("/dashboard");
201 | return;
202 | }
203 | });
204 |
205 |
206 | app.get('/logout', function (req, res){
207 | var session_id = cookie_utils.cookie_get_id(req);
208 | cookie_utils.cookie_send_id(res, "");
209 |
210 | // So that the frontend does not think user
211 | // is logged in.
212 | res.locals.user = undefined;
213 | res.locals.logged_in = false;
214 |
215 | cache_user_model.logout(session_id);
216 |
217 | res.render('base',{
218 | page: "login",
219 | positive_message: true,
220 | message: "You successfully logged out. Come again soon!"
221 | });
222 | });
223 |
224 | app.post('/login', function (req, res){
225 | if(!res.locals.logged_in){
226 | // If not logged in
227 | post_login_form(req, res);
228 | } else {
229 | // If logged in, redirect to user dashboard
230 | res.redirect("/dashboard");
231 | return;
232 | }
233 | });
234 |
235 | function post_login_form(req, res){
236 | var email = req.body.email || "";
237 | var password = req.body.password || "";
238 | var user_db = cache_user_model.db;
239 |
240 | // Check email
241 | user_db.exists_email(email, function(exists, user){
242 | if(!exists){
243 | // Nope!
244 | render(false);
245 | } else {
246 | if(user.verify_password(password)){
247 | // User exists, has good password
248 | var session_id = require("../tokens").generate_token(20);
249 | user.login(cache_user_model, session_id);
250 |
251 | cookie_utils.cookie_send_id(res, session_id);
252 | render(true);
253 | } else {
254 | // User exists, but wrong password
255 | render(false);
256 | }
257 | }
258 | });
259 |
260 | function render(success){
261 | if(success){
262 | res.redirect("/dashboard");
263 | return;
264 | } else {
265 | var ip = get_ip(req);
266 |
267 | console.log("unsuccessful login attempt: " + ip);
268 |
269 | res.render('base',{
270 | page: "login",
271 | negative_message: true,
272 | message: "Wrong username/password combination."
273 | });
274 | }
275 | }
276 | }
277 |
278 | app.get('/dashboard', function (req, res) {
279 | // Se if user is logged in and get data
280 | if(!res.locals.logged_in){
281 | // Not logged in
282 | res.redirect("/login");
283 | return;
284 | } else {
285 | var user = res.locals.user || null;
286 |
287 | // Find recently visited sheets
288 | user.recently_visited_sheets(function(sheets){
289 | // Render page
290 | res.render('base',{
291 | page: "dashboard",
292 | recent_sheets: sheets
293 | });
294 | });
295 | }
296 | });
297 |
298 | app.post('/signup', function (req, res) {
299 | var username = req.body.username || "";
300 | var email = req.body.email || "";
301 | var password = req.body.password || "";
302 | var user_db = cache_user_model.db;
303 |
304 | // Until not true,
305 | var success = true;
306 | var errors = [];
307 |
308 | var validator = require("email-validator");
309 |
310 | // Email validation
311 | if(!validator.validate(email)){
312 | success = false;
313 | errors.push("Email address is not valid.");
314 | }
315 |
316 | // Username length
317 | if(username.length < 6){
318 | success = false;
319 | errors.push("Username is too short.");
320 | }
321 |
322 | // Username length
323 | if(username.length > 40){
324 | success = false;
325 | errors.push("Username is too long.");
326 | }
327 |
328 | // Username format
329 | if(!username.match(/^[A-Za-z0-9]*$/)){
330 | success = false;
331 | errors.push("Username contains invalid characters.");
332 | }
333 |
334 | // Password length
335 | if(password.length < 6){
336 | success = false;
337 | errors.push("Password is too short.");
338 | }
339 |
340 | // Check email
341 | user_db.exists_email(email, function(exists){
342 | if(exists){
343 | success = false;
344 | errors.push("This email is already in use.");
345 | }
346 |
347 | // Check username
348 | user_db.exists_username(username, function(exists){
349 | if(exists){
350 | success = false;
351 | errors.push("This username is already used by someone.");
352 | }
353 |
354 | // If still successful
355 | if(success == true){
356 | // Save user to mongo db
357 | user_db.create({
358 | username: username,
359 | email: email,
360 | password: password
361 | }, function(){
362 | render(success, errors);
363 | });
364 | } else {
365 | render(success, errors);
366 | }
367 | });
368 | });
369 |
370 | function render(success, errors){
371 | if(success){
372 | res.redirect("/dashboard");
373 | return;
374 | } else {
375 | var ip = get_ip(req);
376 |
377 | console.log("unsuccessful account creation attempt: " + ip);
378 |
379 |
380 | res.render('base',{
381 | page: "signup",
382 | negative_message: true,
383 | message: errors.join(" ")
384 | });
385 | }
386 | }
387 | });
388 | };
389 |
--------------------------------------------------------------------------------
/livecalc.js:
--------------------------------------------------------------------------------
1 | /* livecalc.js */
2 |
3 | var cookie = require('cookie');
4 | var user_cache = require('./user_cache');
5 |
6 | var sheet_counter = require('./sheet_counter');
7 |
8 | var site_user_count = 0;
9 |
10 | /* old globals */
11 | var io, sheet_db, chat_db, stats, cache_user_model;
12 |
13 | var namespaces = [];
14 |
15 | module.exports = {};
16 |
17 | module.exports.namespaces = namespaces;
18 |
19 | /* Set socket.io et al. */
20 | module.exports.set_globals = function (
21 | new_io,
22 | new_sheet_db,
23 | new_chat_db,
24 | new_stats,
25 | new_cache_user_model ){
26 |
27 | io = new_io;
28 | sheet_db = new_sheet_db;
29 | chat_db = new_chat_db;
30 | stats = new_stats;
31 | cache_user_model = new_cache_user_model;
32 | };
33 |
34 |
35 | /**
36 | Callback is supposed to render page
37 | */
38 | module.exports.new_namespace = function(namespace){
39 | var nsp = io.of("/"+namespace);
40 |
41 | livecalc(namespace, nsp);
42 | }
43 |
44 | /**
45 | Sidebar chat
46 |
47 | This is the code that listens to everything related to the chat.
48 | */
49 | module.exports.livechat = livechat;
50 | function livechat(namespace, nsp, socket, user){
51 | var user;
52 |
53 | var exports = {};
54 |
55 | exports.set_user = function(new_user){
56 | user = new_user;
57 | };
58 |
59 | socket.on("load more messages",function(last_sent){
60 | chat_db.get_conv(namespace,function(data){
61 | socket.emit("past messages", data);
62 | });
63 | });
64 |
65 | socket.on("new message", function(data){
66 | if(user == undefined){
67 | return;
68 | }
69 |
70 | var data = {
71 | message: data.message,
72 | sender: user.get_nickname(),
73 | public_id: user.get_public_id()
74 | };
75 |
76 | chat_db.add_message(namespace, data);
77 |
78 | socket.broadcast.emit("new message", data);
79 | socket.emit("own message", data);
80 | });
81 |
82 | return exports;
83 | }
84 |
85 | /*
86 | callback(success)
87 | */
88 | module.exports.livecalc = livecalc;
89 | function livecalc(namespace, nsp){
90 | var model = require("./sheet_model").create();
91 | var counter = sheet_counter.Counter();
92 |
93 | namespaces.push(namespace);
94 |
95 | // Check if sheet exists
96 | // Then load it and serve normally
97 | sheet_db.exists(namespace, function(exists){
98 | if(exists){
99 | sheet_db.get_sheet(namespace, function(data){
100 | model.set_sheet(data);
101 | listen();
102 | })
103 | } else {
104 | console.log("Someone tries to access namespace: "+namespace+
105 | " but it does not exist. This should not happen.");
106 | }
107 | });
108 |
109 | function listen(){
110 | /*
111 | Array of users used to create focus index
112 |
113 | note: don't rely on a value being in there.
114 |
115 | for(i in users) == ok
116 | users[my_id] == not ok, don't do that!
117 | (it might not exist)
118 |
119 | */
120 | var users = {};
121 |
122 | nsp.on("connection", function(socket){
123 | var cookie_val = socket.handshake.headers['cookie'];
124 | var session_id = cookie.parse(cookie_val || '').session_id || "";
125 | var registered = false;
126 | var user;
127 | var public_id;
128 |
129 | function temp_user(){
130 | // Temporary user
131 | user = cache_user_model.create();
132 | public_id = user.get_public_id();
133 |
134 | // Generate a temporary session id
135 | // (That will not be saved, even to redis)
136 | session_id = user.get_session_id();
137 | users[session_id] = user.get_public_data();
138 | }
139 |
140 | if(session_id != ""){
141 | // User says "im logged in"
142 | // See if the user is actually in redis
143 | cache_user_model.temp_exists(session_id, function(exists){
144 | if(exists){
145 | // User actually logged in
146 | registered = true;
147 | user = cache_user_model.cached_user(session_id);
148 | user.fetch(function(){
149 | public_id = user.get_public_id();
150 | users[session_id] = user.get_public_data();
151 | chat.set_user(user);
152 | send_user_data();
153 | });
154 | } else {
155 | // User had a session_id, but it
156 | // is not in db. (expired/never existed)
157 | // TODO: inform user he is not connected.
158 | console.log(
159 | "Attempt to login with bad cookie token."
160 | );
161 |
162 | temp_user();
163 |
164 | // Send temp data
165 | send_user_data();
166 | }
167 | });
168 | } else {
169 | // User is not logged in
170 | temp_user();
171 | // Still send temp data
172 | send_user_data();
173 | }
174 |
175 | send_focus_index();
176 |
177 | // Rate limiting.
178 | // while registered users are not managed,
179 | // leave this to 5
180 | if(counter.get() >= 5){
181 | socket.emit("too many users");
182 | return;
183 | }
184 |
185 | stats.new_sheet_visit(namespace);
186 |
187 | stats.get_sheet_visits(namespace, function(num){
188 | nsp.emit("sheet visit count", num);
189 | });
190 |
191 |
192 | counter.plus("anon");
193 | site_user_count++;
194 |
195 | console.log(
196 | "connection - " +
197 | site_user_count +
198 | " users in site, " +
199 | counter.get("anon") +
200 | " users in sheet " +
201 | namespace
202 | );
203 |
204 | nsp.emit("user count", counter.get("anon"));
205 |
206 | if(users[session_id] != undefined){
207 | users[session_id].focus = -1;
208 | }
209 |
210 | var chat = livechat(namespace, nsp, socket, user);
211 |
212 | /*
213 | Build array containing array of nicknames
214 | of user focussing on each cell
215 |
216 | [
217 | ["Paul","Anonymous"],
218 | [],
219 | ["George"]
220 | ]
221 |
222 | Goal: show the users who's editing what.
223 |
224 | */
225 | function send_focus_index(){
226 | var fi = [];
227 |
228 | for(var i = 0; i < model.get_length(); i++){
229 | fi.push([]);
230 | }
231 |
232 | if(!model.is_locked()){
233 | for(var i in users){
234 | var user = users[i];
235 | if(user == undefined){
236 | continue;
237 | }
238 | if(user.focus != undefined && user.focus != -1){
239 | fi[user.focus].push(user.nickname);
240 | }
241 | }
242 | }
243 | nsp.emit("focus index", fi);
244 | }
245 |
246 | // Send sheet to user
247 | socket.emit("sheet", JSON.stringify(model.get_sheet()));
248 |
249 | socket.on("set nickname",function(data){
250 | // Prevent XSS
251 | // Prevent injection of whatever dom element here
252 | // by allowing only certain characters
253 | var nickname = data.nickname.replace(/[^A-Za-z0-9\-]/g,"");
254 |
255 | if(registered){
256 | user.fetch_permanent_user(function(){
257 | user.set_nickname(nickname);
258 | // Store in redis
259 | user.save();
260 | // And in mongo
261 | user.save_permanent();
262 |
263 | after();
264 | });
265 | } else {
266 | user.set_nickname(nickname);
267 | after();
268 | }
269 |
270 | function after(){
271 | users[session_id].nickname = nickname;
272 | send_user_data();
273 | send_focus_index();
274 | }
275 | });
276 |
277 | function send_user_data(){
278 | socket.emit("user data", user.get_public_data());
279 | }
280 |
281 | socket.on("set focus",function(data){
282 | if(model.is_locked()){
283 | return;
284 | }
285 |
286 | var index = data.index;
287 | users[session_id].focus = index;
288 |
289 | send_focus_index();
290 | });
291 |
292 |
293 | socket.on("lock sheet",function(data){
294 | // Don't lock demo
295 | if(namespace == "demo"){
296 | return;
297 | }
298 |
299 | // Already locked?
300 | if(model.is_locked()){
301 | return;
302 | }
303 |
304 | model.lock();
305 | save(true);
306 |
307 | send_focus_index();
308 |
309 | socket.emit("sheet locked", {
310 | initiator: users[session_id].nickname
311 | });
312 | });
313 |
314 | /*
315 | When a user submits a cell by pressing enter
316 | or "go", the model is updated and the info
317 | is sent to other users, overwriting current value
318 | for all users.
319 | */
320 | socket.on("definitive edit", function(data){
321 | if(!model.is_locked()){
322 | model.edit(data);
323 | socket.broadcast.emit("definitive edit", data);
324 | save();
325 | }
326 | });
327 |
328 | /*
329 | Live edits are not definitive
330 | They are just there to show what other users
331 | are typing
332 | */
333 | socket.on("live edit", function(data){
334 | if(!model.is_locked()){
335 | socket.broadcast.emit("live edit", data);
336 | }
337 | });
338 |
339 | socket.on("insert cell", function(data){
340 | if(!model.is_locked()){
341 | model.insert_cell(data);
342 | socket.broadcast.emit("insert cell", data);
343 | save();
344 | }
345 | });
346 |
347 | socket.on("delete cell", function(data){
348 | if(!model.is_locked()){
349 | model.remove(data);
350 |
351 | save();
352 | socket.broadcast.emit("delete cell", data);
353 | }
354 | });
355 |
356 | socket.on("disconnect",function(socket){
357 | counter.minus("anon");
358 | site_user_count--;
359 |
360 | console.log(
361 | "disconnection - " +
362 | site_user_count +
363 | " users in site, " +
364 | counter.get("anon") +
365 | " users in sheet " +
366 | namespace
367 | );
368 |
369 | nsp.emit("user count", counter.get("anon"));
370 |
371 | if(registered){
372 | // Save user in memory
373 | user.save();
374 | }
375 |
376 | /*
377 | Delete user from memory.
378 |
379 | What if user is in 2 tabs:
380 | If the user is there 2 times in 2 tabs,
381 | it will be recreated after the user sends back it's
382 | focus. So there is no problem in deleting.
383 |
384 | This is necessary to avoid ending up with enormous
385 | amounts of users in this array.
386 | */
387 | delete users[session_id];
388 | send_focus_index();
389 | });
390 | });
391 |
392 | function save(even_if_locked){
393 | var even_if_locked = even_if_locked || false;
394 | if(!model.is_locked() || even_if_locked){
395 | sheet_db.store_sheet(namespace, model.get_sheet());
396 | }
397 | }
398 | }
399 | }
400 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 | {one line to give the program's name and a brief idea of what it does.}
635 | Copyright (C) {year} {name of author}
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | {project} Copyright (C) {year} {fullname}
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/public/main.js:
--------------------------------------------------------------------------------
1 | /*
2 | Shortcut to document.querySelectorAll
3 | */
4 | function qsa(sel){
5 | return document.querySelectorAll(sel);
6 | }
7 |
8 | /*
9 | qsa, but on an element
10 | */
11 | function subqsa(el,sel){
12 | return el.querySelectorAll(sel);
13 | }
14 |
15 | /*
16 | Hide an element
17 | */
18 | function hide(el){
19 | el.classList.add("hidden");
20 | }
21 |
22 | /*
23 | Determine if hidden
24 | */
25 | function hidden(el){
26 | return el.classList.contains("hidden");
27 | }
28 |
29 |
30 | /*
31 | Show an element
32 | */
33 | function show(el){
34 | el.classList.remove("hidden");
35 | }
36 |
37 |
38 | /*
39 | Load a template
40 |
41 | returns the HTML
42 | */
43 | function load_template(name){
44 | var el = qsa("template[name="+name+"]")[0];
45 |
46 | if(el == undefined){
47 | console.error("Template "+name+" does not exist.");
48 | }
49 |
50 | var content = el.innerHTML;
51 |
52 | var params = el.getAttribute("data-params");
53 |
54 | if(params == "" || params == null){
55 | params = [];
56 | } else {
57 | params = params.split(",");
58 | }
59 |
60 | return {
61 | content: content,
62 | params: params
63 | };
64 | }
65 |
66 | /*
67 | finds template-instances
68 |
69 | replaces handlebars by data- attributes
70 |
71 | {{meow}} will be replaced by attribute data-meow
72 |
73 | (Sort of a preprocessor)
74 | */
75 | function instanciator(el){
76 | var instances = subqsa(el,"template-instance");
77 |
78 | for(var i = 0; i < instances.length; i++){
79 | var instance = instances[i];
80 | var name = instance.getAttribute("data-name");
81 | var template = load_template(name);
82 | var content = template.content;
83 | var params = template.params;
84 |
85 | for(var j = 0; j < params.length; j++){
86 | var attr = "data-"+params[j];
87 | var value = instance.getAttribute(attr);
88 |
89 | // Sanitize value to avoid XSS
90 | value = value.replace(/[^A-Za-z0-9\-\.\_\: ]/g,"");
91 | var attr = attr.replace(/^data-/,"")
92 | var handle = "{{"+attr+"}}";
93 |
94 | content = content.replace(handle,value);
95 | }
96 |
97 | instance.innerHTML = content;
98 | }
99 | }
100 |
101 | /*
102 | Create an instance of a template and put it in to_el,
103 | replacing the content
104 | Improvement idea: manage parameters and use this in instanciator
105 | instead of current code.
106 | */
107 | function render(template_name, to_el){
108 | var template = load_template(template_name).content;
109 |
110 | var to_el = to_el || dom("");
111 |
112 | to_el.innerHTML = template;
113 | instanciator(to_el);
114 |
115 | return to_el;
116 | }
117 |
118 | /*
119 | Load a script
120 | Returns the content
121 | */
122 | function load_script(name){
123 | var content = qsa("script[name="+name+"]")[0].innerHTML;
124 | return content;
125 | }
126 |
127 | /*
128 | Create a dom element
129 | */
130 | function dom(html){
131 | var node = document.createElement("div");
132 | node.innerHTML = html;
133 | return node.children[0];
134 | }
135 |
136 | /*
137 | Make something disappear "smoothly"
138 | */
139 | function disappear(el, effect){
140 | appear(el, effect, true);
141 | }
142 |
143 | /*
144 | Make something appear "smoothly"
145 | */
146 | function appear(el, effect, reverse){
147 | var reverse = reverse || false;
148 | var effect = effect || "scale up";
149 |
150 | var effects = {
151 | "scale up": {
152 | max: 3,
153 | begin: function(el){
154 | el.style.transform = "scale(0.0)";
155 | },
156 | end: function(el){
157 | el.style.transform = "";
158 | },
159 | step: function(el,step,max){
160 | var ratio = step / max;
161 |
162 | if(reverse){
163 | ratio = 1 - ratio;
164 | }
165 |
166 | el.style.opacity = "0.0";
167 | el.style.opacity = 1.0 - ratio;
168 | el.style.transform = "scale("+(1.0 - ratio)+")";
169 | }
170 | },
171 | "from top": {
172 | max: 6,
173 | begin: function(el){
174 | el.style.transform = "scale(1,0)";
175 | },
176 | end: function(el){
177 | el.style.transform = "";
178 | },
179 | step: function(el,step,max){
180 | var ratio = step / max;
181 |
182 | if(reverse){
183 | ratio = 1 - ratio;
184 | }
185 |
186 | el.style.opacity = "0.0";
187 | el.style.opacity = 1.0 - ratio;
188 | el.style.transformOrigin = "top";
189 | el.style.transform =
190 | "scale(1,"+(1.0 - ratio)+")";
191 | }
192 | }
193 | };
194 |
195 | if(effects[effect] == undefined){
196 | console.error("Animation '" + effect + "' does not exist.");
197 | return;
198 | }
199 |
200 | var effect = effects[effect];
201 |
202 | /* reverse begin and end */
203 | if(reverse == true){
204 | var tmp = effect.begin;
205 | effect.begin = effect.end;
206 | effect.end = function(el){
207 | // Hide before to prevent visual glitch
208 | hide(el);
209 | tmp(el);
210 | };
211 | }
212 |
213 | animate(el, effect);
214 | }
215 |
216 | /*
217 | Make something appear "smoothly"
218 | */
219 | function animated_remove(el, callback){
220 | var options = {
221 | max: 10,
222 | begin: function(el){
223 | el.style.position = "relative";
224 | el.style.opacity = "1.0";
225 | },
226 | end: function(el){
227 | el.style.position = "";
228 | el.parentNode.removeChild(el);
229 | callback();
230 | },
231 | step: function(el,step,max){
232 | var ratio = step / max;
233 | el.style.opacity = ratio;
234 | el.style.transform = "scale("+ratio+")";
235 | el.style.top = 100 * (1.0 - ratio) + "px";
236 | }
237 | };
238 | animate(el,options);
239 | }
240 |
241 | /*
242 | Make something flash
243 | */
244 | function flash(el,color,text_color){
245 | var original_color, original_text;
246 | var options = {
247 | max: 2,
248 | time_step: 300,
249 | begin: function(el){
250 | el.classList.add("flashing");
251 | },
252 | end: function(el){
253 | el.classList.remove("flashing");
254 | },
255 | step: function(el,step,max){
256 | }
257 | };
258 | animate(el,options);
259 | }
260 |
261 | function animate(el,options,step){
262 | max = options.max;
263 | time_step = options.time_step || 33;
264 | if(step == undefined){
265 | step = max;
266 | options.begin(el);
267 | }
268 | if(step < 0){
269 | options.end(el);
270 | return;
271 | }
272 |
273 | options.step(el, step, max);
274 |
275 | setTimeout(
276 | function(){
277 | animate(el, options, step - 1);
278 | },
279 | time_step
280 | );
281 | }
282 |
283 |
284 | /**
285 | Interface between the shell and the server.
286 | */
287 | function lc_network_engine(socket, shell){
288 | var exports = {};
289 |
290 | socket.on("too many users",function(sheet){
291 | shell.die("Too many users are editing this sheet right now."+
292 | " Try again later or create a new sheet.");
293 | });
294 |
295 | socket.on("user data", function(data){
296 | shell.on_user_data(data);
297 | });
298 |
299 | socket.on("sheet",function(sheet){
300 | shell.on_sheet(sheet);
301 | });
302 |
303 | socket.on("user count",function(count){
304 | shell.on_user_count(count);
305 | });
306 |
307 | socket.on("definitive edit",function(data){
308 | shell.on_edit_cell(data);
309 | });
310 |
311 | socket.on("live edit",function(data){
312 | shell.on_live_edit(data);
313 | });
314 |
315 | socket.on("sheet locked",function(data){
316 | shell.on_sheet_locked(data);
317 | });
318 |
319 | socket.on("focus index",function(data){
320 | shell.on_focus_index(data);
321 | });
322 |
323 | socket.on("delete cell",function(data){
324 | shell.on_delete_cell(data);
325 | });
326 |
327 | exports.close = function(){
328 | socket.close();
329 | };
330 |
331 | exports.send_nickname = function(nickname){
332 | socket.emit("set nickname", {
333 | nickname: nickname
334 | });
335 | };
336 |
337 | exports.lock_sheet = function(){
338 | socket.emit("lock sheet");
339 | };
340 |
341 | exports.delete_cell = function(index){
342 | socket.emit("delete cell", {
343 | number: index
344 | });
345 | };
346 |
347 | exports.send_focus = function(index){
348 | socket.emit("set focus", {
349 | index: index
350 | });
351 | };
352 |
353 | exports.edit_cell = function(index, value, method){
354 | socket.emit("definitive edit", {
355 | number: index,
356 | content: value,
357 | method: method
358 | });
359 | };
360 |
361 |
362 | exports.live_edit_cell = function(index, value){
363 | socket.emit("live edit", {
364 | number: index,
365 | content: value,
366 | });
367 | };
368 |
369 | exports.ask_user_id = function(){
370 | socket.emit("give user id");
371 | };
372 |
373 | exports.send_user_id = function(id){
374 | socket.emit("user id",{
375 | user_id: id
376 | });
377 | };
378 |
379 | /* Less essential functionnality after this point */
380 |
381 | // stats
382 |
383 | socket.on("sheet visit count", function(num){
384 | shell.on_visit_count(num);
385 | });
386 |
387 | return exports;
388 | }
389 |
390 | function mathjs_compute_engine(){
391 |
392 | }
393 |
394 | function livecalc(root_el, namespace, user){
395 | eeify_mathjs();
396 | var chat;
397 | var scope = default_scope();
398 | var current_focus = -1;
399 |
400 | function default_scope(){
401 | return {
402 | ans: undefined
403 | };
404 | }
405 |
406 | // Create template
407 | render("livecalc", root_el);
408 |
409 | var cells = subqsa(root_el,".livecalc-cells")[0];
410 | var cell_count;
411 | var exports = {};
412 | var currently_calculated_cell = null
413 | var socket = io("/" + namespace);
414 | var params = {};
415 |
416 | var net_engine = lc_network_engine(socket, exports);
417 |
418 | exports.el = root_el;
419 |
420 | exports.socket = socket;
421 |
422 | new_cell("", true, true);
423 |
424 | exports.on_sheet = function(sheet){
425 | load_json(sheet);
426 | };
427 |
428 | var user_count = subqsa(root_el, ".user-count")[0];
429 |
430 | if(user.has_id() == false){
431 | net_engine.ask_user_id();
432 | } else {
433 | net_engine.send_user_id(user.get_public_id());
434 | }
435 |
436 | {
437 | // share url activation
438 | var url_share_button = qsa(".url-popup-modal")[0];
439 |
440 | url_share_button.onclick = function(e){
441 | e.preventDefault();
442 | var link = url_share_button.href;
443 | modal_inform(link);
444 | }
445 | }
446 |
447 | exports.die = function(message){
448 | render("livecalc-die",root_el);
449 |
450 | if(chat != undefined){
451 | chat.die();
452 | }
453 |
454 | var buttons = [
455 | {
456 | text: "Create a new sheet",
457 | action: function(modal){
458 | window.location.href = "/new";
459 | modal.close();
460 | }
461 | },
462 | {
463 | text: "Go back to homepage",
464 | action: function(modal){
465 | window.location.href = "/";
466 | modal.close();
467 | }
468 | }
469 | ];
470 |
471 | modal(message, buttons);
472 | };
473 |
474 | exports.set_chat = function(c){
475 | chat = c;
476 | };
477 |
478 | exports.on_user_count = function(count){
479 | var plural = count > 1;
480 | var count = parseInt(count);
481 | user_count.innerHTML = count + " user" + (plural? "s": "");
482 | };
483 |
484 | exports.on_edit_cell = function(data){
485 | var number = data.number;
486 | var content = data.content;
487 | var method = data.method;
488 | edit_cell(number, content, method);
489 | };
490 |
491 | exports.on_sheet_locked = function(data){
492 | // data.initiator is normally sanited on server
493 | modal_inform( "Sheet was locked by \"" +
494 | data.initiator +
495 | "\". You can still open a copy."
496 | );
497 |
498 | params.locked = true;
499 |
500 | update_state();
501 | };
502 |
503 | exports.on_focus_index = function(data){
504 | for(var i = 0; i < data.length; i++){
505 | var cell = find_cell(i);
506 |
507 | if(cell == null){
508 | return;
509 | }
510 |
511 | var usersinfo = cell.usersinfo;
512 |
513 | if(Array.isArray(data[i]) && data[i].length > 0){
514 | usersinfo.textContent = data[i].join(", ") + " editing this cell...";
515 | } else {
516 | usersinfo.textContent = "";
517 | }
518 | }
519 | };
520 |
521 | exports.on_delete_cell = function(data){
522 | var number = data.number;
523 | delete_cell(number, true);
524 | }
525 |
526 | window.addEventListener("beforeunload", net_engine.close);
527 |
528 | var nickname = "";
529 |
530 | function init_user_data(){
531 | var nickname_input = subqsa(root_el, ".nickname input")[0];
532 | var nickname_button = subqsa(root_el, ".nickname button")[0];
533 | var username_field = qsa(".user-name")[0];
534 |
535 | exports.set_nickname = function(new_nickname){
536 | nickname_input.value = new_nickname;
537 | nickname = new_nickname;
538 | username_field.innerText = nickname;
539 | };
540 |
541 | exports.on_user_data = function(data){
542 | user.set_public_id(data.public_id);
543 | exports.set_nickname(data.nickname);
544 | chat.on_user_ready();
545 | };
546 |
547 | exports.send_nickname = function(){
548 | net_engine.send_nickname(nickname);
549 | }
550 |
551 | nickname_input.onkeydown = function(e){
552 | if(e.keyCode == 13){
553 | submit();
554 | }
555 | }
556 |
557 | nickname_button.addEventListener("click",function(){
558 | submit();
559 | });
560 |
561 | function submit(){
562 | nickname = nickname_input.value;
563 | flash(nickname_input,"#eee","#333");
564 | exports.send_nickname();
565 | }
566 | }
567 |
568 | init_user_data();
569 |
570 | function init_sheet_panel(){
571 | var panel = qsa(".sheet-panel")[0];
572 |
573 | var lock_sheet_button = subqsa(
574 | root_el,
575 | "button[name='lock-sheet']"
576 | )[0];
577 |
578 | lock_sheet_button.onclick = function(){
579 | if(namespace == "demo"){
580 | modal_inform("This sheet is the public demo, it can't be locked.");
581 | return;
582 | }
583 |
584 | modal_yesno(
585 | "This action cannot be undone. " +
586 | "Nobody will be able to modify this " +
587 | "sheet after you click \"yes\". " +
588 | "Do you really want to lock the sheet?",
589 | function(yes){
590 | if(yes){
591 | net_engine.lock_sheet();
592 | }
593 | });
594 | };
595 |
596 | var new_copy_button = subqsa(
597 | root_el,
598 | "button[name='new-copy']"
599 | )[0];
600 |
601 | new_copy_button.onclick = function(){
602 | window.location.href = "/copy/"+namespace;
603 | }
604 |
605 | var visit_count = subqsa(panel, ".visit-count")[0];
606 |
607 | exports.on_visit_count = function(count){
608 | visit_count.textContent = count;
609 | };
610 | }
611 |
612 | init_sheet_panel();
613 |
614 | /*
615 | Delete a cell. If remote, we don't send an event to the server.
616 | */
617 | function delete_cell(index, remote){
618 | var remote = remote || false;
619 |
620 | // Never delete last remaining cell
621 | var len = cells.children.length;
622 |
623 | if((index > 0 || len > 1) && index < len){
624 | var cell = find_cell(index).element;
625 |
626 | // Avoid deleting more than one time
627 | if(cell.getAttribute("data-deleting") == "true"){
628 | return;
629 | }
630 |
631 | cell.setAttribute("data-deleting","true");
632 |
633 | animated_remove(cell,function(){
634 | update_indices();
635 | focus(index-1);
636 | });
637 |
638 | if(!remote){
639 | net_engine.delete_cell(index);
640 | }
641 | }
642 | }
643 |
644 | function delete_all(){
645 | cells.innerHTML = "";
646 | }
647 |
648 | function re_run(){
649 | scope = default_scope();
650 | for(var i = 0; i < cells.children.length; i++){
651 | cells.children[i].calculate();
652 | }
653 | }
654 |
655 | exports.re_run = re_run;
656 |
657 | function load_json(data){
658 | var data = JSON.parse(data);
659 | var cells = data.cells;
660 | params = data.params;
661 |
662 | update_state();
663 |
664 | delete_all();
665 |
666 | for(var i = 0; i < cells.length; i++){
667 | new_cell(cells[i], true, false);
668 | }
669 | re_run();
670 | focus(0);
671 | }
672 |
673 | exports.load_json = load_json;
674 |
675 | var sheet_state = subqsa(root_el, ".sheet-state")[0];
676 |
677 | function update_state(){
678 | if(params.locked){
679 | sheet_state.textContent = "This sheet is locked.";
680 | sheet_state.setAttribute("title","Modifications will not be saved. You can still open a copy (See bottom of the page).");
681 | } else {
682 | sheet_state.textContent = "This sheet is public.";
683 | sheet_state.setAttribute("title","");
684 | }
685 | }
686 |
687 | update_state();
688 |
689 | function send_all(){
690 | for(var i = 0; i < cells.children.length; i++){
691 | send_value(i);
692 | }
693 | }
694 |
695 | exports.send_all = send_all;
696 |
697 | function focus(index){
698 | if(index >= cell_count || index < 0){
699 | return;
700 | }
701 |
702 | find_cell(index).input.focus();
703 | send_focus(index);
704 | }
705 |
706 | exports.focus = focus;
707 |
708 | function send_focus(index){
709 | if(index == null){
710 | index = -1;
711 | }
712 | current_focus = index;
713 | net_engine.send_focus(index);
714 | }
715 |
716 | function find_cell(index){
717 | var el = cells.children[index];
718 |
719 | if(el == undefined){
720 | return null;
721 | }
722 |
723 | return {
724 | element: el,
725 | input: subqsa(el, ".livecalc-input")[0],
726 | button: subqsa(el, ".livecalc-go-button")[0],
727 | output: subqsa(el,".livecalc-output")[0],
728 | secondary_output: subqsa(el,".livecalc-secondary-output")[0],
729 | usersinfo: subqsa(el,".users-info")[0],
730 | text_part: subqsa(el,".text-part")[0],
731 | math_part: subqsa(el,".math-part")[0],
732 | plot: subqsa(el,".plot")[0]
733 | };
734 | }
735 |
736 | exports.find_cell = find_cell;
737 |
738 | function update_indices(){
739 | var i = 0;
740 | for(i = 0; i < cells.children.length; i++){
741 | var cell = cells.children[i];
742 | cell.setAttribute("data-index", i);
743 | subqsa(cell,".livecalc-input")[0]
744 | .setAttribute("tabindex", i + 1);
745 | }
746 | cell_count = i;
747 | }
748 |
749 | function edit_cell(number, content, method){
750 | grow_to(number);
751 |
752 | if(method == "insert"){
753 | new_cell(content, false, false, number);
754 | } else {
755 | var field = find_cell(number).input;
756 | field.value = content;
757 | }
758 |
759 | calculate_cell(number);
760 | }
761 |
762 | /* To add cells if required*/
763 | function grow_to(number){
764 | var from = cells.children.length;
765 | var to = number;
766 |
767 | for(i = from; i <= to; i++){
768 | new_cell("", false, false);
769 | }
770 | }
771 |
772 | function insert_cell_at(index, send_data, callback){
773 | new_cell("", send_data, true, index);
774 | callback();
775 | }
776 |
777 | function new_cell(content, send_data, animate, at_index){
778 | var exports = {};
779 | var content = content || "";
780 | var method = "append";
781 |
782 | if(at_index == undefined || at_index < 0){
783 | at_index = -1;
784 | } else {
785 | method = "insert";
786 | }
787 |
788 | cell_count++;
789 | var cell = dom(load_template("livecalc-cell").content);
790 |
791 | if(at_index == -1){
792 | // Append at end
793 | cells.appendChild(cell);
794 | } else {
795 | // Append at index
796 | cells.insertBefore(
797 | cell, // Insert this
798 | find_cell(at_index).element // Before this cell
799 | );
800 | }
801 |
802 | // This should not be used anymore,
803 | // since it may change anytime.
804 | // use get_index()
805 | at_index = undefined;
806 |
807 | update_indices();
808 |
809 | var cell_data = find_cell(get_index());
810 | var input = cell_data.input;
811 | var button = cell_data.button;
812 | var output = cell_data.output;
813 | var text_part = cell_data.text_part;
814 | var math_part = cell_data.math_part;
815 | var secondary_output = cell_data.secondary_output;
816 |
817 | if(animate == true){
818 | appear(cell);
819 | }
820 |
821 | var add_cell_button = subqsa(cell, ".add-cell-button .inner")[0];
822 |
823 | add_cell_button.onclick = function(){
824 | var index = get_index();
825 | insert_cell_at(index, true,function(){
826 | focus(index);
827 | });
828 | }
829 |
830 | input.setAttribute("value", content);
831 |
832 | /* Make sure inputs are shown on mouse click */
833 | text_part.addEventListener("click",function(){
834 | if(hidden(math_part)){
835 | // Show math part for edition
836 | show(math_part);
837 | appear(math_part,"from top");
838 | input.focus();
839 | } else {
840 | // Hide
841 | hide(math_part);
842 | }
843 | });
844 |
845 | // Hide these at beginning
846 | hide(text_part);
847 | hide(secondary_output);
848 |
849 | function get_index(){
850 | return parseInt(cell.getAttribute("data-index"));
851 | }
852 |
853 | exports.get_index = get_index;
854 |
855 | function get_value(){
856 | return input.value;
857 | }
858 |
859 | function calculate(){
860 | calculate_cell(get_index());
861 | };
862 |
863 | input.addEventListener("click",function(){
864 | send_focus(get_index());
865 | });
866 |
867 | /* lost focus */
868 | input.addEventListener("blur",function(){
869 | send_focus(-1);
870 | });
871 |
872 | cell.calculate = calculate;
873 |
874 | {
875 | // manage delete cell button
876 | var delete_cell_button
877 | = subqsa(cell,".delete-cell-button")[0];
878 |
879 | delete_cell_button.onclick = function(){
880 | delete_cell(get_index());
881 | };
882 | }
883 |
884 | var operation_keys = ["+","-","*","/"];
885 |
886 | input.onkeydown = function(e){
887 | var key_num = e.keyCode || e.which;
888 | var has_live_edit = true;
889 |
890 | if(e.keyCode == 13 && !e.shiftKey){
891 | // Enter key
892 | e.preventDefault();
893 | if(get_value() != ""){
894 | send_value(get_index());
895 | go();
896 |
897 | // Edit will already be send
898 | has_live_edit = false;
899 | }
900 | } else if (e.keyCode == 38) {
901 | // Up arrow
902 | focus(get_index()-1);
903 | } else if (e.keyCode == 40) {
904 | // Down arrow
905 | focus(get_index()+1);
906 | } else if(e.code == "Backspace"){
907 | // Delete cell
908 | if(get_value() == ""){
909 | delete_cell(get_index());
910 |
911 | // delete_cell fires a delete event
912 | // sending edit would be a problem
913 | has_live_edit = false;
914 | }
915 | } else {
916 | // Detect if an operator
917 | // was inserted (+ - * /)
918 | // and wrap selected text
919 | for(var i in operation_keys){
920 | var op = operation_keys[i];
921 | if(e.key == op){
922 | operation_keydown(e, op);
923 | }
924 | }
925 | }
926 |
927 | if(has_live_edit){
928 | send_live_throttled();
929 | }
930 |
931 | var last_live_edit_send = time();
932 | var time_threshold = 350;
933 | var has_waiting_timeout = false;
934 |
935 | /* Send data, but not too often */
936 | function send_live_throttled(){
937 | if(time() - last_live_edit_send > time_threshold){
938 | send_live_edit(get_index());
939 | } else {
940 | if(!has_waiting_timeout){
941 | setTimeout(function(){
942 | send_live_throttled();
943 | has_waiting_timeout = false;
944 | },
945 | time() - time_threshold
946 | );
947 | has_waiting_timeout = true;
948 | }
949 | }
950 | }
951 |
952 | function time(){
953 | return (new Date()).getTime();
954 | }
955 | }
956 |
957 | /*
958 |
959 | Manage text selection smartly
960 | when user types an operator + / - / * / '/'
961 |
962 | And move cursor to a practical position.
963 |
964 | */
965 | function operation_keydown(e, operation){
966 | var start = input.selectionStart;
967 | var end = input.selectionEnd;
968 |
969 | if(start != end){
970 | e.preventDefault();
971 |
972 | // By default, place cursor after operator
973 | var inside = false;
974 |
975 | if( operation == "+" ||
976 | operation == "-"
977 | ){
978 | inside = true;
979 | }
980 |
981 | var after = ")" + operation + "[[cursor]]";
982 |
983 | if(inside){
984 | after = operation + "[[cursor]]" + ")";
985 | }
986 |
987 | selection_wrap(input, "(", after, operation);
988 | }
989 | }
990 |
991 | function go(){
992 | calculate();
993 |
994 | var index = get_index();
995 |
996 | // If last cell, add new cell
997 | if(index == cell_count - 1){
998 | new_cell("", true, true);
999 | }
1000 | }
1001 |
1002 | button.onclick = function(){
1003 | send_value(get_index());
1004 | go();
1005 | };
1006 |
1007 | exports.calculate = calculate;
1008 |
1009 | input.focus();
1010 |
1011 | if(send_data){
1012 | send_focus(get_index());
1013 | send_value(get_index(), method);
1014 | }
1015 |
1016 | return exports;
1017 | }
1018 |
1019 | /*
1020 | before_sel : something to place before selection
1021 | after_sel : idem, after
1022 |
1023 | puts selections at [[cursor]] ans [[cursor-end]]
1024 | */
1025 | function selection_wrap(input, before_sel, after_sel, fallback, no_selection){
1026 | var fallback = fallback || "";
1027 |
1028 | var start = input.selectionStart;
1029 | var end = input.selectionEnd;
1030 |
1031 | // if the browser does not support this feature,
1032 | // just add fallback at input end
1033 | if(input.selectionStart == undefined){
1034 | input.value += fallback;
1035 | return;
1036 | }
1037 |
1038 | // If there is no selection
1039 | // add the appropriate value (no_selection) if present
1040 | // or the fallback
1041 | if(start == end){
1042 | before_sel = "";
1043 | after_sel = no_selection || (fallback + "[[cursor]]");
1044 | }
1045 |
1046 | var val = input.value;
1047 | var before = val.substr(0,start);
1048 | var between = val.substr(start, end - start);
1049 | var after = val.substr(end,val.length - end);
1050 |
1051 | var new_val = before;
1052 | new_val += before_sel;
1053 | new_val += between;
1054 | new_val += after_sel;
1055 | new_val += after;
1056 |
1057 | var new_start = new_val.indexOf("[[cursor]]");
1058 | new_val = new_val.replace("[[cursor]]","");
1059 |
1060 | var new_end = new_val.indexOf("[[cursor-end]]");
1061 |
1062 | // Actually using [[cursor-end]]?
1063 | if(new_end != -1){
1064 | // Yes
1065 | new_val = new_val.replace("[[cursor-end]]","");
1066 | } else {
1067 | // Nope, just place cursor
1068 | new_end = new_start;
1069 | }
1070 |
1071 | input.value = new_val;
1072 |
1073 | input.selectionStart = new_start;
1074 | input.selectionEnd = new_end;
1075 | }
1076 |
1077 | exports.new_cell = new_cell;
1078 |
1079 | function send_live_edit(index){
1080 | var cell_data = find_cell(index);
1081 | var input = cell_data.input;
1082 | net_engine.live_edit_cell(index, input.value);
1083 | }
1084 |
1085 | exports.on_live_edit = on_live_edit;
1086 |
1087 | function on_live_edit(data){
1088 | var cell_data = find_cell(data.number);
1089 | var input = cell_data.input;
1090 | input.value = data.content;
1091 | }
1092 |
1093 | function send_value(index, method){
1094 | var method = method || "append";
1095 |
1096 | var cell_data = find_cell(index);
1097 |
1098 | var input = cell_data.input;
1099 |
1100 | net_engine.edit_cell(index, input.value, method);
1101 | }
1102 |
1103 | function calculate_cell(index){
1104 | var cell_data = find_cell(index);
1105 | currently_calculated_cell = cell_data;
1106 | var cell = cell_data.element;
1107 | var text_part = cell_data.text_part;
1108 | var math_part = cell_data.math_part;
1109 | var plot_el = cell_data.plot;
1110 | var input = cell_data.input;
1111 | var output = cell_data.output;
1112 | var secondary_output = cell_data.secondary_output;
1113 | var value = input.value;
1114 | var math_value = value;
1115 | var text_comment = "";
1116 |
1117 | plot_el.innerHTML = "";
1118 | show(math_part);
1119 | hide(secondary_output);
1120 |
1121 | // Extract comment
1122 | var comment_pos = value.indexOf("//");
1123 |
1124 | if(comment_pos != -1){
1125 | text_comment = value.substr(comment_pos+2,value.length);
1126 | math_value = value.substr(0,comment_pos);
1127 | }
1128 |
1129 | if(text_comment != "" && math_value == ""){
1130 | // Has comment but no math
1131 | // Show only text part
1132 | text_part.textContent = text_comment;
1133 | show(text_part);
1134 | hide(math_part);
1135 | return;
1136 | } else if (math_value != "" && text_comment == ""){
1137 | // Has math but no comment
1138 | // Show only math part
1139 | text_part.textContent = "";
1140 | hide(text_part);
1141 | show(math_part);
1142 | } else if (math_value != "" && text_comment != ""){
1143 | // Has math and comment
1144 | // Show math (includes comment anyway)
1145 | show(math_part);
1146 | hide(text_part);
1147 | } else {
1148 | // No value, no comment
1149 | // Show only math
1150 | show(math_part);
1151 | hide(text_part);
1152 | return;
1153 | }
1154 |
1155 | var text = ee_parse(math_value);
1156 |
1157 | // Evaluate and display errors/result
1158 | try{
1159 | var result = math.eval(text, scope);
1160 | scope["ans"] = result;
1161 | } catch (exception){
1162 | output.textContent = exception;
1163 | return;
1164 | }
1165 |
1166 | secondary_output.innerHTML = "";
1167 |
1168 | if(text == ""){
1169 | return;
1170 | } else if(result != undefined){
1171 | if(typeof result == "function"){
1172 | output.textContent = "[function]";
1173 | } else if (typeof result == "number"){
1174 | // Here, we will round values if needed
1175 | var rounded = parseFloat(result.toPrecision(10));
1176 | var final_output = "";
1177 |
1178 | // If we display a rounding, inform the user
1179 | // and also show the non-rounded value.
1180 | if(result != rounded){
1181 | final_output = rounded;
1182 | secondary_output.textContent =
1183 | "Raw float value: " +
1184 | result;
1185 | show(secondary_output);
1186 | } else {
1187 | final_output = result;
1188 | }
1189 |
1190 | output.textContent = final_output;
1191 | } else {
1192 | output.textContent = result;
1193 | }
1194 | } else {
1195 | output.textContent = "[undefined]";
1196 | return;
1197 | }
1198 |
1199 | flash(output,"#09bc8a","#ffffff");
1200 | }
1201 |
1202 | /*
1203 | Add some useful stuff to math.js
1204 | */
1205 | function eeify_mathjs(){
1206 | math.import({
1207 | /* Parallel resistors */
1208 | LL: function(a,b){
1209 | var num = 0;
1210 | for(i in arguments){
1211 | var arg = arguments[i];
1212 | num += 1/arg;
1213 | }
1214 | return 1 / num;
1215 | },
1216 | rule: function(number){
1217 | var cell = currently_calculated_cell;
1218 | wait_for_click(cell, function(){
1219 | rule(cell.plot, number)
1220 | });
1221 | return "";
1222 | },
1223 | plot: plot,
1224 | zfractal: function(e,i,s){
1225 | var cell = currently_calculated_cell;
1226 | wait_for_click(cell, function(){
1227 | zfractal(cell.plot, e, i, s);
1228 | });
1229 | return "";
1230 | },
1231 | "π": math.pi
1232 | });
1233 | }
1234 |
1235 | /*
1236 | Wait for user click before
1237 | calculating something potentially long
1238 | */
1239 | function wait_for_click(cell, callback){
1240 | render("livecalc-wait-click", cell.plot);
1241 |
1242 | var button = subqsa(cell.plot,"button")[0];
1243 | button.onclick = go;
1244 |
1245 | function go(){
1246 | button.innerHTML = "Computing...";
1247 | setTimeout(callback,100);
1248 | }
1249 | }
1250 |
1251 | /*
1252 | Cellular automata
1253 | */
1254 | function rule(plot_el, number){
1255 | var number = parseInt(number);
1256 |
1257 | plot_el.innerHTML = "";
1258 |
1259 | if(number < 0 || number > 255){
1260 | throw "Number should be between 0 and 255";
1261 | }
1262 |
1263 | var div_width = plot_el.clientWidth;
1264 |
1265 | var grid_size = 100;
1266 | var pixel_size = 4;
1267 |
1268 | var width = grid_size;
1269 | var height = grid_size;
1270 |
1271 | var can = dom("");
1272 | var ctx = can.getContext("2d");
1273 |
1274 | plot_el.appendChild(can);
1275 |
1276 | can.width = width * pixel_size;
1277 | can.height = height * pixel_size;
1278 |
1279 | var imgdata = ctx.createImageData(can.width, can.height);
1280 |
1281 | var line = new_line();
1282 |
1283 | line[parseInt(width/2)] = true;
1284 |
1285 | // This is a port of my GLSL code found
1286 | // [here](https://github.com/antoineMoPa/ogl-js/blob/master/tests/triangles/post-fragment.glsl)
1287 |
1288 | // Draw a pixel
1289 | // (That may be many pixel, depending on pixel_size)
1290 | function set_pixel(i,j,val){
1291 | var j = j * pixel_size;
1292 | var i = i * pixel_size;
1293 |
1294 | // repeat the pixel
1295 | for(var k = 0; k < pixel_size; k++){
1296 | for(var l = 0; l < pixel_size; l++){
1297 | set_one_pixel(i+k,j+l,val);
1298 | }
1299 | }
1300 | }
1301 |
1302 | // Draw one actual canvas pixel
1303 | function set_one_pixel(i,j,val){
1304 | var index = 4 * (j * can.width + i);
1305 | // Set value
1306 | imgdata.data[index + 0] = val;
1307 | imgdata.data[index + 1] = val;
1308 | imgdata.data[index + 2] = val;
1309 | imgdata.data[index + 3] = 255;
1310 | }
1311 |
1312 | // Parse screen and follow the rule to create new line
1313 | for(var j = 0; j < height; j++){
1314 | // Keep last line in memory
1315 | var last_line = copy_line(line);
1316 | for(var i = 0; i < width; i++){
1317 | // Drawing
1318 | var val = 230;
1319 |
1320 | if(line[i]){
1321 | val = 30;
1322 | }
1323 |
1324 | set_pixel(i, j, val);
1325 |
1326 | var cell_1 = last_line[i-1];
1327 | var cell_2 = last_line[i];
1328 | var cell_3 = last_line[i+1];
1329 |
1330 | var num = 0;
1331 |
1332 | if(cell_1){
1333 | num += 4;
1334 | }
1335 | if(cell_2){
1336 | num += 2;
1337 | }
1338 | if(cell_3){
1339 | num += 1;
1340 | }
1341 |
1342 | next = false;
1343 |
1344 | // `rule`: 8 bits
1345 | // `num`: 3 bits addressing `rule` bits
1346 | //
1347 | // `rule` indicates which cases of `num` will produce
1348 | // an open pixel
1349 | //
1350 | // bitwise or (&) operator example:
1351 | // 0010 0000 & 0010 0001 == 0010 0000
1352 | //
1353 | // Example with rule 3
1354 | // Rule 3 = 0000 0011
1355 | // So bits 1 and 2 are activated
1356 | // Which means 2^0 and 2^1 is activated
1357 | // 0000 0011 & 0000 0001 != 0 and
1358 | // 0000 0011 & 0000 0010 != 0
1359 | //
1360 | // In these cases, the next state of the pixel is `1`
1361 | //
1362 | if(number & parseInt(Math.pow(2,num))){
1363 | next = true;
1364 | }
1365 |
1366 | if(next){
1367 | line[i] = true;
1368 | } else {
1369 | line[i] = false;
1370 | }
1371 | }
1372 | }
1373 |
1374 | function new_line(){
1375 | var line = [];
1376 | for(var i = 0; i < width; i++){
1377 | line.push(false);
1378 | }
1379 | return line;
1380 | }
1381 |
1382 | function copy_line(old){
1383 | var line = [];
1384 | for(var i = 0; i < old.length; i++){
1385 | line.push(old[i]);
1386 | }
1387 | return line;
1388 | }
1389 |
1390 | ctx.putImageData(imgdata,0,0);
1391 |
1392 | // We must return a value
1393 | return "";
1394 | }
1395 |
1396 | function plot(){
1397 | var plot_el = currently_calculated_cell.plot;
1398 | var fullscreen_button = render("plot-interact-button");
1399 | var div_width = plot_el.clientWidth;
1400 |
1401 | var functions_data = [];
1402 |
1403 | for(i in arguments){
1404 | var expression = arguments[i];
1405 | functions_data.push({
1406 | sampler: 'builtIn', /* To use math.js */
1407 | graphType: 'polyline', /* To use math.js */
1408 | fn: expression
1409 | });
1410 | }
1411 |
1412 |
1413 | // For most screens: keep this width
1414 | // To optimize vertical space used +
1415 | // pragmatic aspect ratio
1416 | var width = 550;
1417 |
1418 | // Smaller screens limit widthx
1419 | if(div_width < 550){
1420 | width = div_width - 10;
1421 | }
1422 |
1423 | functionPlot({
1424 | target: plot_el,
1425 | width: width,
1426 | disableZoom: true,
1427 | data: functions_data,
1428 | grid: true
1429 | });
1430 |
1431 | plot_el.appendChild(fullscreen_button);
1432 |
1433 | fullscreen_button.onclick = function(){
1434 | fullscreen(expression, function(content){
1435 | functionPlot({
1436 | target: content,
1437 | width: window.innerWidth,
1438 | height: window.innerHeight - 100,
1439 | disableZoom: false,
1440 | data: functions_data,
1441 | grid: true
1442 | });
1443 | });
1444 |
1445 | /*
1446 | callback(content_dom_element)
1447 | */
1448 | function fullscreen(title_text, callback){
1449 | var fullscreen_el = render("fullscreen");
1450 | var close_button = subqsa(fullscreen_el, ".close-button")[0];
1451 | var content = subqsa(fullscreen_el, ".content")[0];
1452 | var title = subqsa(fullscreen_el, ".fullscreen-title")[0];
1453 |
1454 | title.textContent = title_text;
1455 |
1456 | close_button.onclick = function(){
1457 | fullscreen_el.parentNode.removeChild(fullscreen_el);
1458 | };
1459 |
1460 | document.body.appendChild(fullscreen_el);
1461 | callback(content);
1462 | }
1463 | }
1464 |
1465 | // We must return a value
1466 | return "";
1467 | }
1468 |
1469 | function zfractal(plot_el, expression, iterations, size){
1470 | var iterations = iterations || 10;
1471 | var exp = math.compile(expression);
1472 |
1473 | plot_el.innerHTML = "";
1474 |
1475 | var div_width = plot_el.clientWidth;
1476 | var pixel_ratio = 1;
1477 |
1478 | var size = size || 30;
1479 |
1480 | var width = size;
1481 | var height = size;
1482 |
1483 | var can = dom("");
1484 | var ctx = can.getContext("2d");
1485 |
1486 | plot_el.appendChild(can);
1487 |
1488 | // Make it square
1489 | can.width = width * pixel_ratio;
1490 | can.height = height * pixel_ratio;
1491 |
1492 | var data = ctx.createImageData(width, height);
1493 |
1494 | for(var i = 0; i < width; i++){
1495 | for(var j = 0; j < height; j++){
1496 | scope.c = math.complex(
1497 | 4.0 * (i/width - 0.5),
1498 | 4.0 * (j/height - 0.5)
1499 | );
1500 |
1501 | scope.z = math.complex(scope.c);
1502 |
1503 | var val = 255;
1504 |
1505 | for(var k = 0; k < iterations; k++){
1506 | scope.z = exp.eval(scope);
1507 | if(len(scope.z) > 2.0){
1508 | val = parseInt(((k/iterations) * 255));
1509 | break;
1510 | }
1511 | }
1512 |
1513 | var index = 4 * (j * width + i);
1514 |
1515 | data.data[index + 0] = val;
1516 | data.data[index + 1] = val;
1517 | data.data[index + 2] = val;
1518 | data.data[index + 3] = 255
1519 | }
1520 | }
1521 |
1522 | ctx.putImageData(data,0,0);
1523 |
1524 | function len(z){
1525 | return Math.sqrt(
1526 | Math.pow(z.re,2) +
1527 | Math.pow(z.im,2)
1528 | );
1529 | }
1530 | }
1531 |
1532 | var palette_el = qsa(".palette")[0];
1533 |
1534 | exports.palette = palette_el;
1535 |
1536 | if(palette_el){
1537 | init_palette(palette_el);
1538 | }
1539 |
1540 | function init_palette(palette){
1541 | palette_el.addEventListener("mousedown",function(event){
1542 | // Prevent focus loss to input
1543 | event.preventDefault();
1544 | event.stopPropagation();
1545 |
1546 | var el = event.target;
1547 |
1548 | // Is it a button ?
1549 | if(el.tagName.toLowerCase() == "button"){
1550 | var cell = find_cell(current_focus);
1551 |
1552 | if(cell != null){
1553 | var input = cell.input;
1554 | on_click(el, input);
1555 | } else if (chat.has_focus){
1556 | on_click(el, chat.textarea);
1557 | }
1558 | }
1559 | });
1560 |
1561 | function on_click(button, input){
1562 | var value = button.innerText;
1563 |
1564 | // Are we using selection_wrap?
1565 | if(button.hasAttribute("data-wrap-before")){
1566 | var before = button.getAttribute("data-wrap-before");
1567 | var after = "";
1568 | var fallback = button.innerText
1569 | var no_selection = fallback + "[[cursor]]";
1570 |
1571 | if(button.hasAttribute("data-wrap-after")){
1572 | after = button.getAttribute("data-wrap-after")
1573 | }
1574 |
1575 | if(button.hasAttribute("data-no-sel")){
1576 | no_selection = button.getAttribute("data-no-sel")
1577 | }
1578 |
1579 | selection_wrap(input, before, after, fallback, no_selection);
1580 |
1581 | return;
1582 | }
1583 |
1584 | if(input.selectionStart != undefined){
1585 | var offset = input.selectionStart;
1586 |
1587 | var curr_val = input.value; // Current input value
1588 | var input_begin = curr_val.substr(0,offset);
1589 | var input_end = curr_val.substr(offset,curr_val.length);
1590 |
1591 | if(offset != input.selectionEnd){
1592 | // If some text is selected, we'll replace it
1593 | // So erase that part
1594 |
1595 | input_end = curr_val.substr(
1596 | input.selectionEnd,
1597 | curr_val.length
1598 | );
1599 | }
1600 |
1601 | input.value =
1602 | input_begin +
1603 | value +
1604 | input_end;
1605 |
1606 | // Remove variable and place
1607 | // user cursor
1608 | var replace_var =
1609 | button.getAttribute("data-replace-var") || "";
1610 |
1611 | if(replace_var != ""){
1612 | var var_pos = value.indexOf(replace_var);
1613 | var var_len = replace_var.length;
1614 |
1615 | // Select variable
1616 | input.selectionStart =
1617 | input_begin.length + var_pos;
1618 | input.selectionEnd =
1619 | input_begin.length + var_pos + var_len;
1620 | } else {
1621 | // Place back mouse after what we inserted
1622 | var new_offset = input_begin.length +
1623 | value.length;
1624 |
1625 | input.selectionStart = new_offset;
1626 | input.selectionEnd = new_offset;
1627 | }
1628 | } else {
1629 | input.value += value;
1630 | }
1631 | }
1632 | }
1633 |
1634 | return exports;
1635 | }
1636 |
1637 | // Replace electrical engineering notation
1638 | function ee_parse(str){
1639 | str = str.replace(/([0-9]+)G/g, "$1E9");
1640 | str = str.replace(/([0-9]+)M/g, "$1E6");
1641 | str = str.replace(/([0-9]+)meg/g,"$1E6");
1642 | str = str.replace(/([0-9]+)K/g, "$1E3");
1643 | str = str.replace(/([0-9]+)k/g, "$1E3");
1644 | str = str.replace(/([0-9]+)m/g, "$1E-3");
1645 | str = str.replace(/([0-9]+)u/g, "$1E-6");
1646 | str = str.replace(/([0-9]+)n/g, "$1E-9");
1647 | str = str.replace(/([0-9]+)p/g, "$1E-12");
1648 | str = str.replace(/\*\*/g, "^");
1649 | return str;
1650 | }
1651 |
1652 | function init_doc(calc){
1653 | var codes = subqsa(calc.el, ".doc code");
1654 |
1655 | for(var i = 0; i < codes.length; i++){
1656 | var el = codes[i];
1657 | var content = el.innerHTML;
1658 |
1659 | init_click(el, content);
1660 | el.setAttribute("title","Click to add to sheet");
1661 | }
1662 |
1663 | function init_click(el, code){
1664 | el.onclick = function(){
1665 | var cell = calc.new_cell(code, true, true);
1666 | cell.calculate();
1667 | var dom_data = calc.find_cell(cell.get_index());
1668 | show(dom_data.math_part);
1669 | };
1670 | }
1671 | }
1672 |
1673 | function livechat(root_el, namespace, socket, user){
1674 | render("livechat", root_el);
1675 |
1676 | var log = subqsa(root_el, ".message-log")[0];
1677 | var textarea = subqsa(root_el, "textarea")[0];
1678 | var button = subqsa(root_el, "button")[0];
1679 | var exports = {};
1680 |
1681 | textarea.value = "";
1682 |
1683 | exports.root_el = root_el;
1684 |
1685 | exports.has_focus = false;
1686 | exports.textarea = textarea;
1687 |
1688 | textarea.addEventListener("focus",function(){
1689 | exports.has_focus = true;
1690 | });
1691 |
1692 | textarea.addEventListener("blur",function(){
1693 | exports.has_focus = false;
1694 | });
1695 |
1696 | exports.die = function(){
1697 | root_el.innerHTML = "";
1698 | };
1699 |
1700 | var past_messages_loaded = false;
1701 |
1702 | exports.on_user_ready = function(){
1703 | // Only do this once
1704 | if(past_messages_loaded == false){
1705 | // Now that we know the user id, we
1706 | // load the messages
1707 | // (and we will be able to mark "own" messages)
1708 | socket.emit("load more messages",0);
1709 | past_messages_loaded = true;
1710 | }
1711 | };
1712 |
1713 | function get_value(){
1714 | return textarea.value;
1715 | }
1716 |
1717 | function scroll_bottom(){
1718 | log.scrollTop = log.scrollHeight;
1719 | }
1720 |
1721 | textarea.onkeydown = function(e){
1722 | if(e.keyCode == 13 && !e.shiftKey){
1723 | e.preventDefault();
1724 | submit();
1725 | }
1726 | }
1727 |
1728 | exports.resize = resize;
1729 |
1730 | /*
1731 | Note: This is full of ugly hacks to position and size
1732 | The chat elements.
1733 | */
1734 | function resize(proportion){
1735 | var winw = window.innerWidth;
1736 | var winh = window.innerHeight;
1737 | var proportion = proportion || 1/3;
1738 | var scroll = window.scrollY || 0;
1739 |
1740 | /* set with to one column + margin */
1741 | var w = (parseInt(winw)/4) - 10;
1742 |
1743 | var button_width = button.clientWidth;
1744 |
1745 | var chat_header = 15;
1746 | var input_height = 40;
1747 | var input_width = w - button_width - 60;
1748 | var chat_height = parseInt(proportion * winh - input_height);
1749 |
1750 | textarea.style.width = (input_width)+"px";
1751 |
1752 | log.style.height = (
1753 | chat_height - input_height - chat_header - 10
1754 | ) + "px";
1755 | }
1756 |
1757 | resize();
1758 |
1759 | window.addEventListener("resize", resize);
1760 |
1761 | button.addEventListener("click",submit);
1762 |
1763 | socket.on("new message", function(data){
1764 | var el = render_message(data);
1765 | log.appendChild(el);
1766 | scroll_bottom();
1767 | });
1768 |
1769 | socket.on("own message", function(data){
1770 | var el = render_message(data,true);
1771 | log.appendChild(el);
1772 | scroll_bottom();
1773 | });
1774 |
1775 | socket.on("past messages", function(messages){
1776 | var user_id = user.get_public_id();
1777 |
1778 | for(var i = messages.length - 1; i >= 0; i--){
1779 | var data = JSON.parse(messages[i]);
1780 | var own = false;
1781 |
1782 | if(data.public_id == user_id){
1783 | own = true;
1784 | }
1785 |
1786 | var el = render_message(data, own);
1787 |
1788 | if(log.children[0] != undefined){
1789 | log.insertBefore(el, log.children[0]);
1790 | } else {
1791 | log.appendChild(el);
1792 | }
1793 | }
1794 |
1795 | // While this is used only at page load:
1796 | scroll_bottom();
1797 | });
1798 |
1799 | function render_message(data, own){
1800 | var el;
1801 | if(own){
1802 | el = render("livechat-sent-message");
1803 | } else {
1804 | el = render("livechat-received-message");
1805 | }
1806 |
1807 | // Set the content
1808 | var message = subqsa(el, ".content")[0];
1809 |
1810 | // Use textContent to avoid script injection
1811 | message.textContent = data.message;
1812 |
1813 | // Clickable links
1814 | message.innerHTML = message.innerHTML
1815 | .replace(/(https?\:\/\/[^\n ]*)/g,"$1");
1816 |
1817 | var sender = subqsa(el, ".sender")[0];
1818 |
1819 | // Put sender nickname
1820 | sender.textContent = data.sender;
1821 |
1822 | // Remove newline at begining and end of string
1823 | message.innerHTML = message.innerHTML.replace(/^[\s\n]*/g,"");
1824 | // String end
1825 | message.innerHTML = message.innerHTML.replace(/[\s\n]*$/g,"");
1826 | // Replace newline inside message to
1827 | message.innerHTML = message.innerHTML.replace(/\n/g,"
");
1828 |
1829 | return el;
1830 | }
1831 |
1832 | // Send chat message
1833 | function submit(){
1834 | var val = get_value();
1835 |
1836 | if(val != ""){
1837 |
1838 | // Only whitespace?
1839 | if(val.match(/[^\s\n]/) == null){
1840 | return;
1841 | }
1842 |
1843 | socket.emit("new message",{
1844 | message: val
1845 | });
1846 |
1847 | textarea.value = "";
1848 | }
1849 | }
1850 |
1851 | return exports;
1852 | }
1853 |
1854 | function User(){
1855 | var exports = {};
1856 |
1857 | var public_id = "";
1858 |
1859 | exports.has_id = function(){
1860 | if(public_id == ""){
1861 | return false;
1862 | }
1863 | if(public_id == ""){
1864 | return false;
1865 | }
1866 | return true;
1867 | }
1868 |
1869 | exports.get_public_id = function(){
1870 | return public_id;
1871 | }
1872 |
1873 | exports.set_public_id = function(id){
1874 | public_id = id;
1875 | }
1876 |
1877 | return exports;
1878 | }
1879 |
1880 | var href = window.location.href;
1881 |
1882 | var is_sheet = href.match(/\/sheet\/(.*)/);
1883 | var is_landing = qsa(".landing").length > 0? true: false;
1884 |
1885 | if(is_sheet){
1886 | // In a sheet
1887 | var namespace = /\/sheet\/(.*)/g.exec(href)[1];
1888 |
1889 | var user = User();
1890 |
1891 | // Start everything
1892 | // Start calculator
1893 | var calc = livecalc(qsa("livecalc")[0], namespace, user);
1894 | var chat = livechat(qsa("livechat")[0], namespace, calc.socket, user);
1895 | calc.set_chat(chat);
1896 |
1897 | resizeable_sidebar_box(function(proportion){
1898 | chat.resize(1-proportion);
1899 | });
1900 |
1901 | function resizeable_sidebar_box(callback){
1902 | // Make chat + palette resizable
1903 | var resizable_header = qsa(".sidebar-resize-header")[0];
1904 | var dragging = false;
1905 | var initial_pos = 0;
1906 | var upper_box = calc.palette;
1907 | var lower_box = chat.root_el;
1908 |
1909 | resizable_header.addEventListener("mousedown",function(e){
1910 | e.preventDefault();
1911 | dragging = true;
1912 | });
1913 |
1914 |
1915 | resizable_header.addEventListener("mouseup",function(e){
1916 | dragging = false;
1917 | });
1918 |
1919 | document.body.addEventListener("mousemove",function(e){
1920 | if(dragging){
1921 | var current_pos = e.clientY;
1922 | var proportion = current_pos / window.innerHeight;
1923 |
1924 | proportion = clip(proportion, 0.2,0.8);
1925 |
1926 | var upper_proportion =
1927 | parseInt(proportion * 100) + "%";
1928 | var lower_proportion =
1929 | parseInt((1-proportion) * 100) + "%";
1930 |
1931 | upper_box.style.height = upper_proportion;
1932 | lower_box.style.height = lower_proportion;
1933 |
1934 | callback(proportion);
1935 |
1936 | function clip(val, min, max){
1937 | if(val > min){
1938 | return val < max? val :max;
1939 | } else {
1940 | return min;
1941 | }
1942 | }
1943 | }
1944 | });
1945 | }
1946 |
1947 | // Start documentation
1948 | init_doc(calc);
1949 | } else if (is_landing){
1950 | // TODO: create something nice but easy on CPU for background
1951 | }
1952 |
1953 | /**
1954 | modal
1955 |
1956 | Example:
1957 |
1958 | var buttons = [
1959 | {
1960 | text: "Accept",
1961 | action: function(modal){
1962 | modal.close();
1963 | }
1964 | }
1965 | ];
1966 | */
1967 | function modal(message, buttons){
1968 | var modal = render("modal-inform").children[0];
1969 | var overlay = render("modal-overlay").children[0];
1970 | var content = subqsa(modal, ".content p")[0];
1971 | var buttons_container = subqsa(modal, ".buttons")[0];
1972 | var buttons = buttons || [];
1973 |
1974 | content.textContent = message;
1975 | document.body.appendChild(overlay);
1976 | overlay.appendChild(modal);
1977 |
1978 | var exports = {};
1979 |
1980 | exports.close = function(){
1981 | modal.parentNode.removeChild(modal);
1982 | overlay.parentNode.removeChild(overlay);
1983 | }
1984 |
1985 | for(var i = 0; i < buttons.length; i++){
1986 | var button = dom("");
1987 | var callback = buttons[i].action || function(){};
1988 | var text = buttons[i].text || "No button text";
1989 |
1990 | button.textContent = text;
1991 |
1992 | enable_click(button, callback);
1993 |
1994 | buttons_container.appendChild(button);
1995 | }
1996 |
1997 | function enable_click(button, callback){
1998 | // Onclick: Call callback with modal as argument
1999 | button.addEventListener("click", function(){
2000 | callback(exports);
2001 | });
2002 | }
2003 |
2004 | return exports;
2005 | }
2006 |
2007 | function modal_inform(message){
2008 | var buttons = [
2009 | {
2010 | text: "Accept",
2011 | action: function(modal){
2012 | modal.close();
2013 | }
2014 | }
2015 | ];
2016 |
2017 | return modal(message, buttons);
2018 | }
2019 |
2020 | /**
2021 | callback(bool: answer) (true == yes)
2022 | */
2023 | function modal_yesno(message, callback){
2024 | var buttons = [
2025 | {
2026 | text: "Yes",
2027 | action: function(modal){
2028 | callback(true);
2029 | modal.close();
2030 | }
2031 | },
2032 | {
2033 | text: "No",
2034 | action: function(modal){
2035 | callback(false);
2036 | modal.close();
2037 | }
2038 | }
2039 | ];
2040 |
2041 | return modal(message, buttons);
2042 | }
2043 |
2044 | console.log("Hello!");
2045 | console.log("Fork me on http://github.com/antoineMoPa/livecalc !");
2046 |
--------------------------------------------------------------------------------