I am developing a real-time text editor with paragraph locking property similar to https://quip.com/. in socket.io and nodejs.
It means when you write onto a given paragraph, other collaborators cant edit it.
Moment you hit enter or move cursor to a new line that paragraph becomes Editable for other Collaborators.
I am quite stuck after this. I am thinking a nice approach to move further. Suggestions please.
Below is my code which works perfectly. Till now i can get list of all collaborators and broadcast the content of editor to other collaborators.
index.html
<!DOCTYPE html>
<html>
<head>
<title>Connected Clients</title>
<!--<meta charset="UTF-8"> -->
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<!--<script type="text/javascript" src="jquery.js"></script> -->
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<textarea id="editor" style="height:200px;width:300px">
Thinknest Pragraph locking Test sample !
</textarea>
<script>
function msgReceived(msg){
//clientCounter.html(msg.clients);
document.getElementById('people').innerHTML=msg.uid;
//console.log(msg.id);
}
var clientCounter;
$(document).ready(function () {
clientCounter = $("#client_count");
var socket = io.connect(
'http://localhost:5000',
{'sync disconnect on unload':true}
);
var uId=prompt("enter your userId",'');
socket.emit('collabrator',uId);
socket.on('message', function(msg){
msgReceived(msg);
});
socket.on('online_collabs',function(data){
$('#online_ppl').html(data);
clientCounter.html(data.length);
});
socket.on('remained_collabs',function(data){
$('#online_ppl').html(data);
clientCounter.html(data.length);
});
socket.on('note_collabs',function(data){
$('#note_colabs').html(data);
});
socket.on('updated_para',function(data){
//$('#editor').append(data);
document.getElementById('editor').innerHTML=data;
});
$('#editor').on('keydown',function(){
//var para=$('#editor').value;
var para= $('#editor').val();
//var para=document.querySelector('[contenteditable]');
// var text=para.textContent;
socket.emit('para',{paragraph:para});
});
});
</script>
<p><span id="client_count">0</span> connected clients</p><br/>
<ul id="people"></ul>
<h3>Online Collaborators</h3>
<span id="online_ppl"></span> <br>
<h3>Note Collaborators</h3>
<span id="note_colabs"></span>
</body>
</html>
server.js
var app = require('express')()
, server = require('http').createServer(app)
, io = require('socket.io').listen(server);
server.listen(5000);
app.get('/',function(req,res){
res.sendfile("./index.html");
});
var activeClients = 0;
var Collaborators=['Colab1','Colab2','Colab3'];
var people=[];
io.sockets.on('connection', function(socket){
clientConnect(socket);
socket.on('disconnect', function(){
clientDisconnect(socket);
});
socket.on('para',function(data){
//io.sockets.emit('updated_para',data.paragraph);
socket.broadcast.emit('updated_para',data.paragraph);
});
});
function clientConnect(socket){
//activeClients +=1;
var userSocketId=socket.id;
check_Collaborator(socket);
io.sockets.emit('message', {uid:userSocketId});
}
var online_collabs=[];
function check_Collaborator(socket){
socket.on('collabrator',function(data){
if(Collaborators.indexOf(data)!=-1){
socket.data=data;
if(online_collabs.indexOf(data)==-1) {
online_collabs.push(data);
}
io.sockets.emit('online_collabs',online_collabs);
io.sockets.emit('note_collabs',Collaborators);
} else {
console.log("collabrator not found");
}
});
}
function clientDisconnect(socket){
var index=online_collabs.indexOf(socket.data)
if(index>-1)
online_collabs.splice(index,1);
//activeClients -=1;
//io.sockets.emit('message', {clients:activeClients});
io.sockets.emit('remained_collabs',online_collabs);
}
I saw this yesterday already. What exactly is your question? Do you want to know how to 'lock' a text area with javascript? I am confused as to why you put such a strong emphasis on node/socket.io in your question.
Also, next time please format your code. You want help, I get it, but then make it easier for others to help you.
What you have to do in order to make a paragraph not editable by others, I don't know. But let me suggest what I'ld do in socket.io:
Store each paragraph separately and remember who has a lock on it. For locking, I would use the sessionID in case users don't have to register. This would look something like this:
var paragraphs = {
data : [
{
text: "this is an unlocked paragraph",
lock: ""
},
{
text: "this is a locked paragraph",
lock: "oWEALlLx5E-VejicAAAC"
}
]
}
Now, users will likely be allowed to add a paragraph before an existing one. Therefore you should keep an additional index like:
var paragraphs = {
index : [
1,
0
],
data : [
{
text: "this the second paragraph",
lock: "oWEALlLx5E-VejicAAAC"
},
{
text: "this is the first paragraph",
lock: ""
}
]
}
The amount of data being sent over the sockets should now be very small - altough with additional client/server-side logic.
Paragraph lock can be easily achieved by adding a class to the currently editing paragraph. Transfer this paragraph with the class to the other user. so that if the user tries to write over that prevent him by validate with the class.
Generate a class name look like - className_userid (className_1).
Related
I work for a school system looking to have their lunch numbers for the day saved each night and then the numbers wiped. We have tried to find someone to write us a script with triggers but no one has been able to figure it out thus far. Any help would be highly appreciated. This is our last hope. https://docs.google.com/a/bradleyschools.org/spreadsheets/d/1KZ8LuABtUE1I4jFKVzroXKaJVOcD9LArewRl5wRFRbA/edit?usp=sharing
Lunch Counter WebApp
I took a stab at the problem and here it is. I have it working as a webapp contained in a spreadsheet and the lunchcount gets posted and reset every night around 10:00 PM. Every time you do a get on the url it checks to see if the trigger is set and if it's not then it creates a new one. But it won't create more than one. I checked it last night and the trigger is working fine. Don't forget to put in the SpreadSheetID for the postLunchCount and also run setupNightlyTrigger() once to get the trigger installed. It run's by itself after that.
LunchCounter.html
<!DOCTYPE html>
<html>
<head>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>
$(function() {
google.script.run
.withSuccessHandler(displayCount)
.getCount();
});
function incrementCount()
{
google.script.run
.withSuccessHandler(displayCount)
.lunchCounter();
}
function displayCount(n)
{
$('#cnt').text(n);
}
console.log('My Code');
</script>
</head>
<body>
<div id="cnt"></div>
<input type="button" id="btn1" value="Increment" onClick="incrementCount();" style="width:200px;height:200px;"/>
</body>
</html>
Code.gs
function onOpen()
{
SpreadsheetApp.getUi().createMenu('My Tools')
.addItem('Lunch Counter','lunchCounter')
.addItem('Load SideBar', 'loadSideBar')
.addItem('Reset Lunch Count','resetLunchCount')
.addItem('Post Lunch','postLunchCount')
.addToUi();
}
function lunchCounter()
{
var prop=PropertiesService.getScriptProperties();
var count=Number(prop.getProperty('LunchCount'));
count++;
prop.setProperty('LunchCount', count);
return(count);
}
function getCount()
{
var prop=PropertiesService.getScriptProperties();
var count=Number(prop.getProperty('LunchCount'));
return(count);
}
function resetLunchCount()
{
postLunchCount();
PropertiesService.getScriptProperties().setProperty('LunchCount', '0');
}
function postLunchCount()
{
var ss=SpreadsheetApp.openById('SpreadSheetID');
var sht=ss.getSheetByName('DailyLunchCount');
var dt=Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyy-MM-dd");
var cnt=getCount();
var row=[dt,cnt];
sht.appendRow(row);
}
function setupNightlyTrigger()
{
if(!isTrigger('resetLunchCount'))
{
ScriptApp.newTrigger('resetLunchCount').timeBased().atHour(22).everyDays(1).create();
}
}
function loadSideBar()
{
var html = HtmlService.createHtmlOutputFromFile('LunchCounter');
SpreadsheetApp.getUi().showSidebar(html);
}
function doGet()
{
setupNightlyTrigger();
var html = HtmlService.createHtmlOutputFromFile('LunchCounter');
html.addMetaTag('viewport', 'width=device-width, initial-scale=1');
return html.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
}
function isTrigger(handlerName)
{
var r=false;
var triggers = ScriptApp.getProjectTriggers();
for (var i=0;i<triggers.length;i++)
{
if (handlerName==triggers[i].getHandlerFunction())
{
r=true;
break;
}
}
return r;
}
This is what the webapp looks like on an Iphone:
It's really not that big. I did it as a screen shot. On my iphone the button size is about 3/4" and the lunch count is diplayed above it. I'm don't really know what you wanted but I figured a single button is simple User Interface.
Spreadsheet Looks Like this:
Can someone please point me in the right direction. I tried google and didn't find much regarding my issue. While the following code works perfectly fine when running the .html file directly, it doesn't while serving the file in a node express app. The problem is that with node I don't see any data. page loads fine but no data.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="jquery.js"></script>
<script src="handlebars.js"></script>
<script src="underscore.js"></script>
<script src="backbone.js"></script>
<script src="moment.js"></script>
<!-- Setup our templates -->
<script id="PersonTemplate" type="text/x-handlebars-template">
<div>
Person:<br>
{{name}}
{{age}}
</div>
</script>
<!--
Note the [] this is important
because handlebars and backbone collections
dont play well with each other in regards
to naming JSON groups
-->
<script id="PeopleTemplate" type="text/x-handlebars-template">
<div>
People:<br>
{{#each []}}
{{this.name}}
{{this.age}}
<br/>
{{/each}}
</div>
</script>
<!-- End templates setup -->
<script>
// Stub out the person model
var Person = Backbone.Model.extend({
});
// Create a collection of persons
var People = Backbone.Collection.extend({
model: Person
});
// Define the view for a single person
var PersonView = Backbone.View.extend({
render: function() {
// This is method that can be called
// once an object is init. You could
// also do this in the initialize event
var source = $('#PersonTemplate').html();
var template = Handlebars.compile(source);
var html = template(this.model.toJSON());
$(this.el).html(html);
}
});
// Define the view for People
var PeopleView = Backbone.View.extend({
render: function() {
// This is method that can be called
// once an object is init. You could
// also do this in the initialize event
var source = $('#PeopleTemplate').html();
var template = Handlebars.compile(source);
var html = template(this.collection.toJSON());
$(this.el).html(html);
},
initialize: function(){
this.collection.on('add', this.render, this)
}
});
// Create instance of People Collection
var people = new People();
// Create new instances of the person models
var person = new Person({name: "Tim", age: 5});
var person2 = new Person({name: "Jill", age: 15});
// Create instances of the views
var personView = new PersonView({
model: person
});
var peopleView = new PeopleView({
collection: people
});
$(document).ready(function(){
// We have to do this stuff in the dom ready
// so that the container objects get built out
// Set el of the views.
personView.el = $('#personContainer');
peopleView.el = $('#peopleContainer');
// Add them to a new instance of the people collection
people.add(person);
people.add(person2);
// Render the views. If you are using the initialize
// method then you do not have to do this step.
personView.render();
//peopleView.render();
// Try on console!
// people.add(new Person({name: 'Rames', age:'23'}));
});
</script>
</head>
<body>
<div id='personContainer' ></div>
<hr>
<div id='peopleContainer' ></div>
</body>
</html>
Thanks in advance for your help.
i'm trying to do something similar to some websites where you leave and it'll display a popup saying "Are you sure you want to leave this page" and with two options saying "Cancel" and "OK".
How would I do that and make it so when you click "Cancel" it'll just cancel the box and when they click "OK" it'll do a 'leaveChat();' function and leave the website?
I've tried using the onbeforeunload which was working but i'm not sure on the function part.
Thanks for the help!
There is no way to do that.
You can try sending the AJAX request in onunload, but I don't think that will work reliably.
To pop a message when the user is leaving the page to confirm leaving, you just do:
<script>
window.onbeforeunload = function(e) {
return 'Are you sure you want to leave this page? You will lose any unsaved data.';
};
</script>
To call a function:
<script>
window.onbeforeunload = function(e) {
callSomeFunction();
return null;
};
</script>
here is my html
<!DOCTYPE HMTL>
<meta charset="UTF-8">
<html>
<head>
<title>Home</title>
<script type="text/javascript" src="script.js"></script>
</head>
<body onload="myFunction()">
<h1 id="belong">
Welcome To My Home
</h1>
<p>
<a id="replaceME" onclick="myFunction2(event)" href="https://www.ccis.edu">I am a student at Columbia College of Missouri.</a>
</p>
</body>
And so this is how I did something similar in javaScript
var myGlobalNameHolder ="";
function myFunction(){
var myString = prompt("Enter a name", "Name Goes Here");
myGlobalNameHolder = myString;
if (myString != null) {
document.getElementById("replaceME").innerHTML =
"Hello " + myString + ". Welcome to my site";
document.getElementById("belong").innerHTML =
"A place you belong";
}
}
// create a function to pass our event too
function myFunction2(event) {
// variable to make our event short and sweet
var x=window.onbeforeunload;
// logic to make the confirm and alert boxes
if (confirm("Are you sure you want to leave my page?") == true) {
x = alert("Thank you " + myGlobalNameHolder + " for visiting!");
}
}
I have a pretty large object that I need to pass to a function in a client script. I have tried using JSON.stringify, but have run into a few issues with this approach - mostly performance related. Is it possible to do something like this in ejs?
app.get('/load', function(req, res) {
var data = {
layout:'interview/load',
locals: {
interview: '',
data: someLargeObj
}
};
res.render('load', data);
});
And in my client script, I would pass this object to a function like so
<script type="text/javascript">
load(<%- data %>); // load is a function in a client script
</script>
When I try this I get either
<script type="text/javascript">
load();
</script>
or
<script type="text/javascript">
load([Object object]);
</script>
In Node.js:
res.render('mytemplate', {data: myobject});
In EJS:
<script type='text/javascript'>
var rows =<%-JSON.stringify(data)%>
</script>
SECURITY NOTE : Don't use this to render an object with user-supplied data. It would be possible for someone like Little Bobby Tables to include a substring that breaks the JSON string and starts an executable tag or somesuch. For instance, in Node.js this looks pretty innocent...
var data = {"color": client.favorite_color}
but could result in a client-provided script being executed in user's browsers if they enter a color such as:
"titanium </script><script>alert('pwnd!')</script> oxide"
If you need to include user-provided content, please see https://stackoverflow.com/a/37920555/645715 for a better answer using Base64 encoding
That is the expected behavior. Your template engine is trying to create a string from your object which leads to [Object object]. If you really want to pass data like that I think you did the correct thing by stringifying the object.
If you are using templating, then it would be much better to get the values in the template, for example whether user is signed in or not. You can get the send local data using
<script>
window.user = <%- JSON.stringify(user || null) %>
</script>
From the server side code, you are sending user data.
res.render('profile', {
user: user.loggedin,
title: "Title of page"
});
Think there's a much better way when passing an object to the ejs , you dont have to deal with JSON.stringfy and JSON.parse methods, those are a little bit tricky and confusing. Instead you can use the for in loop to travel the keys of your objects, for example:
if you have an object like such hierarchy
{
"index": {
"url": "/",
"path_to_layout": "views/index.ejs",
"path_to_data": [
"data/global.json",
{
"data/meta.json": "default"
}
]
},
"home": {
"url": "/home",
"path_to_layout": "views/home/index.ejs",
"path_to_data": [
"data/global.json",
{
"data/meta.json": "home"
}
]
},
"about": {
"url": "/about",
"path_to_layout": "views/default.ejs",
"path_to_data": [
"data/global.json",
{
"data/meta.json": "about"
}
]
}
}
On the EJS side you can loop yourObject like this;
<% if ( locals.yourObject) { %>
<% for(key in yourObject) { %>
<% if(yourObject.hasOwnProperty(key)) { %>
<div> <a class="pagelist" href="<%= yourObject[key]['subkey'] %>"><%= key %></a></div>
<% } %>
<% } %>
<% } %>
For this example [key] can take 'index','home' and 'about' values and subkey can be any of it's children such as 'url','path_to_layout','path_to_data'
What you have is a result like this
[{'re': 'tg'}]
You actually need to loop it. See javascript while loop https://www.w3schools.com/js/js_loop_while.asp
Then, render it in your front end with ejs... i can't help on that, i use hbs
is there a way of getting the nivo slider to use effects in order ie:
slideUp --> then slideDown --> then slide slideUp ect...
rather then using effect: "slideUp,slideDown" which randomly fires either effect.
regards sam
I wrote this for you. (Note, I have not tested it or validated it works, but the theory is sound)
<script>
var slideEffects = new Array('fade', 'slide', 'fade');
var currentEffectIndex = 0;
$(document).ready(function(){
$('#slider').nivoSlider({
effect: slideEffects[0],
afterChange: function(){
currentEffectIndex++;
$(this).effect = slideEffects[currentEffectIndex % slideEffects.length];
},
};
});
</script>
Here is a correct version.
<script type="text/javascript">
var slideEffects = new Array('fade', 'slide', 'fade');
var currentEffectIndex = 0;
$(document).ready(function(){
$('#slider').nivoSlider({
effect: slideEffects[0],
afterChange: function(){
currentEffectIndex++;
$(this).effect = slideEffects[currentEffectIndex % slideEffects.length];
},
});
});
</script>