HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
Any URLs added here will be added as <link>
s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Search for and use JavaScript packages from npm here. By selecting a package, an import
statement will be added to the top of the JavaScript editor for this package.
Using packages here is powered by esm.sh, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ESM usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<html>
<title>PubNub ChatEngine</title>
<head>
<link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css"></link>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/3.0.0/handlebars.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/list.js/1.1.1/list.min.js"></script>
</head>
<body>
<div class="container clearfix">
<div class="people-list" id="people-list">
<ul class="list">
</ul>
</div>
<div class="chat">
<div class="chat-header clearfix">
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/195612/chat_avatar_01_green.jpg" alt="avatar" />
<div class="chat-about">
<div class="chat-with">ChatEngine Demo Chat</div>
</div>
</div>
<div class="chat-history">
<ul></ul>
</div>
<form id="sendMessage" class="chat-message clearfix">
<input type="text" name="message-to-send" id="message-to-send" placeholder="Type your message" rows="1"></input>
<input type="submit" value="Send"></input>
</form>
<!-- end chat-message -->
</div>
<!-- end chat -->
</div>
<!-- end container -->
<script id="message-template" type="text/x-handlebars-template">
<li class="clearfix">
<div class="message-data align-right">
<span class="message-data-time">{{time}}, Today</span>
<span class="message-data-name">{{user.first}}</span> <i class="fa fa-circle me"></i>
</div>
<div class="message other-message float-right">
{{messageOutput}}
</div>
</li>
</script>
<script id="message-response-template" type="text/x-handlebars-template">
<li>
<div class="message-data">
<span class="message-data-name"><i class="fa fa-circle online"></i> {{user.first}}</span>
<span class="message-data-time">{{time}}, Today</span>
</div>
<div class="message my-message">
{{messageOutput}}
</div>
</li>
</script>
<script id="person-template" type="text/x-handlebars-template">
{{#if state.full}}
<li class="clearfix" id="{{uuid}}">
<img src="{{state.avatar}}" alt="avatar" />
<div class="about">
<div class="name">{{state.full}}</div>
<div class="status">
<i class="fa fa-circle online"></i> online
</div>
</div>
</li>
{{/if}}
</script>
</body>
</html>
@import url(https://fonts.googleapis.com/css?family=Lato:400,700);
$green: #86BB71;
$blue: #94C2ED;
$orange: #E38968;
$gray: #92959E;
*, *:before, *:after {
box-sizing: border-box;
}
body {
background: #C5DDEB;
font: 14px/20px "Lato", Arial, sans-serif;
padding: 20px 0;
color: white;
}
.container {
margin: 0 auto;
width: 750px;
background: #444753;
border-radius: 5px;
}
.people-list {
width:260px;
float: left;
overflow-y: auto;
max-height: 532px;
.search {
padding: 20px;
}
input {
border-radius: 3px;
border: none;
padding: 14px;
color: white;
background: #6A6C75;
width: 90%;
font-size: 14px;
}
.fa-search {
position: relative;
left: -25px;
}
ul {
padding: 20px;
height: 500px;
li {
padding-bottom: 20px;
}
}
img {
float: left;
}
.about {
float: left;
margin-top: 8px;
}
.about {
padding-left: 8px;
}
.status {
color: $gray;
}
}
.chat {
width: 490px;
float:left;
background: #F2F5F8;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
color: #434651;
.chat-header {
padding: 20px;
border-bottom: 2px solid white;
img {
float: left;
}
.chat-about {
float: left;
padding-left: 10px;
margin-top: 6px;
}
.chat-with {
font-weight: bold;
font-size: 16px;
}
.chat-num-messages {
color: $gray;
}
.fa-star {
float: right;
color: #D8DADF;
font-size: 20px;
margin-top: 12px;
}
}
.chat-history {
padding: 30px 30px 20px;
border-bottom: 2px solid white;
overflow-y: scroll;
height: 300px;
.message-data {
margin-bottom: 15px;
}
.message-data-time {
color: lighten($gray, 8%);
padding-left: 6px;
}
.message {
color: white;
padding: 8px 10px;
line-height: 26px;
font-size: 16px;
border-radius: 7px;
margin-bottom: 15px;
width: 90%;
position: relative;
&:after {
bottom: 100%;
left: 7%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-bottom-color: $green;
border-width: 10px;
margin-left: -10px;
}
}
.my-message {
background: $green;
}
.other-message {
background: $blue;
&:after {
border-bottom-color: $blue;
left: 93%;
}
}
}
.chat-message {
padding: 30px;
input[type="text"] {
width: 100%;
border: none;
padding: 10px 20px;
font: 14px/22px "Lato", Arial, sans-serif;
margin-bottom: 10px;
border-radius: 5px;
resize: none;
background-color: #fff;
}
.fa-file-o, .fa-file-image-o {
font-size: 16px;
color: gray;
cursor: pointer;
}
input[type="submit"] {
float: right;
color: $blue;
font-size: 16px;
text-transform: uppercase;
border: none;
cursor: pointer;
font-weight: bold;
background: #F2F5F8;
&:hover {
color: darken($blue, 7%);
}
}
}
}
.online, .offline, .me {
margin-right: 3px;
font-size: 10px;
}
.online {
color: $green;
}
.offline {
color: $orange;
}
.me {
color: $blue;
}
.align-left {
text-align: left;
}
.align-right {
text-align: right;
}
.float-right {
float: right;
}
.clearfix:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
ChatEngine = ChatEngineCore.create({
publishKey: 'pub-c-9b914983-9487-4be5-a0e4-0c27a20e6572',
subscribeKey: 'sub-c-acc8e784-cd70-11e8-b02a-a6a8b6327be1'
});
// use a helper function to generate a new profile
let newPerson = generatePerson(true);
// create a bucket to store our ChatEngine Chat object
let myChat;
// create a bucket to store
let me;
// compile handlebars templates and store them for use later
let peopleTemplate = Handlebars.compile($("#person-template").html());
let meTemplate = Handlebars.compile($("#message-template").html());
let userTemplate = Handlebars.compile($("#message-response-template").html());
const source_language = "en";
const target_language = "es";
const virgilCrypto = new VirgilCrypto.VirgilCrypto();
let channelKeyPair;
// Identity of the pre-defined "signle" user of the chat
const USER_IDENTITY = 'chatengine-demo-e2ee-user';
// The key under which the user's encrypted private key is stored in
// the Virgil Keyknox service
const USER_KEY_ID = 'chatengine-demo-e2ee-user-key';
// Prefix we will prepend to the ciphertext before sending the encrypted
// message to be able to tell the encrypted and plaintext messages apart
const ENC_MESSAGE_PREFIX = 'e2ee_by_virgil';
const initVirgil = async () => {
// Get the JWT for authentication in Virgil APIs. Makes a request to the
// server we've deployed for this demo. The Subject of the returned JWT will
// always be equal to `USER_IDENTITY`
const fetchVirgilJwt = async () => {
const res = await fetch('https://virgil-pubnub-demo-chat-server.herokuapp.com/virgil-jwt');
if (!res.ok) throw new Error('Failed to get Virgil access token');
return await res.text();
};
// Get the pre-defined private key of the Chat Channel encrypted with the
// user's public key.
const fetchEncryptedChannelKey = async () => {
const res = await fetch('https://virgil-pubnub-demo-chat-server.herokuapp.com/channel-private-key');
if (!res.ok) throw new Error('Failed to get encrypted channel key');
return await res.text();
};
const jwtProvider = new Virgil.CachingJwtProvider(fetchVirgilJwt);
const brainKey = VirgilPythia.createBrainKey({
virgilCrypto,
virgilPythiaCrypto: new VirgilCrypto.VirgilPythiaCrypto(),
accessTokenProvider: jwtProvider
});
// Derive the key pair from password. The password is hard-coded for demo
// purposes only, it must be provided by the user in a real app.
const passwordKeyPair = await brainKey.generateKeyPair('PubNubD3m0o');
// Setup the private keys storage.
const syncKeyStorage = Keyknox.SyncKeyStorage.create({
// this key will be used to decrypt the Cloud-stored keys
privateKey: passwordKeyPair.privateKey,
// this key is used to encrypt the Cloud-stored keys
publicKeys: passwordKeyPair.publicKey,
keyEntryStorage: new Virgil.KeyEntryStorage(),
accessTokenProvider: jwtProvider
});
// Synchronize the keys between the Virgil Cloud and local storage (IndexedDB)
await syncKeyStorage.sync();
// Retrieve the pre-defined private key of the user
const userPrivateKeyEntry = await syncKeyStorage.retrieveEntry(USER_KEY_ID);
// Import to make it usable with `virgilCrypto` methods
const userPrivateKey = virgilCrypto.importPrivateKey(userPrivateKeyEntry.value);
// Retrieve the Chat Channel private key encrypted with the user's public key
const encryptedChannelPrivateKeyData = await fetchEncryptedChannelKey();
// Decrypt with user's private key
const channelPrivateKeyData = virgilCrypto.decrypt(encryptedChannelPrivateKeyData, userPrivateKey);
// Import to make it usable with `virgilCrypto` methods
const channelPrivateKey = virgilCrypto.importPrivateKey(channelPrivateKeyData);
channelKeyPair = {
privateKey: channelPrivateKey,
publicKey: virgilCrypto.extractPublicKey(channelPrivateKey)
};
};
// this is our main function that starts our chat app
const init = () => {
// connect to ChatEngine with our generated user
ChatEngine.connect(newPerson.uuid, newPerson);
// when ChatEngine is booted, it returns your new User as `data.me`
ChatEngine.on('$.ready', function(data) {
// store my new user as `me`
me = data.me;
// create a new ChatEngine Chat
myChat = new ChatEngine.Chat('chatengine-demo-chat');
// when we recieve messages in this chat, render them
myChat.on('message', (message) => {
renderMessage(message);
});
// when a user comes online, render them in the online list
myChat.on('$.online.*', (data) => {
$('#people-list ul').append(peopleTemplate(data.user));
});
// when a user goes offline, remove them from the online list
myChat.on('$.offline.*', (data) => {
$('#people-list ul').find('#' + data.user.uuid).remove();
});
// wait for our chat to be connected to the internet
myChat.on('$.connected', () => {
// search for 50 old `message` events
myChat.search({
event: 'message',
limit: 50
}).on('message', (data) => {
// when messages are returned, render them like normal messages
renderMessage(data, true);
});
});
// bind our "send" button and return key to send message
$('#sendMessage').on('submit', sendMessage)
});
};
// send a message to the Chat
const sendMessage = () => {
// get the message text from the text input
let message = $('#message-to-send').val().trim();
// if the message isn't empty
if (message.length) {
// Encrypt the message with the Channel's public key
message = virgilCrypto.encrypt(message, channelKeyPair.publicKey).toString('base64');
// Add prefix so the receiver can tell this message is encrypted
message = [ ENC_MESSAGE_PREFIX, message].join(':');
// emit the `message` event to everyone in the Chat
myChat.emit( 'message', {
text: message,
translate: {
text: message,
source: source_language,
target: target_language
}
} );
// clear out the text input
$('#message-to-send').val('');
}
// stop form submit from bubbling
return false;
};
// render messages in the list
const renderMessage = (message, isHistory = false) => {
// use the generic user template by default
let template = userTemplate;
// if I happened to send the message, use the special template for myself
if (message.sender.uuid == me.uuid) {
template = meTemplate;
}
let el = template({
// Try to decrypt the message
messageOutput: tryDecrypt(message.data.text),
time: getCurrentTime(),
user: message.sender.state
});
// render the message
if(isHistory) {
$('.chat-history ul').prepend(el);
} else {
$('.chat-history ul').append(el);
}
// scroll to the bottom of the chat
scrollToBottom();
function tryDecrypt (message) {
const [ prefix, ciphertext ] = message.split(':');
if (prefix === ENC_MESSAGE_PREFIX) {
// The message seems to be encrypted
try {
// Decrypt and convert to string
return virgilCrypto.decrypt(ciphertext, channelKeyPair.privateKey).toString('utf8')
} catch (e) {
// Return as is
return message;
}
}
// Return as is
return message;
}
};
// scroll to the bottom of the window
const scrollToBottom = () => {
$('.chat-history').scrollTop($('.chat-history')[0].scrollHeight);
};
// get the current time in a nice format
const getCurrentTime = () => {
return new Date().toLocaleTimeString().replace(/([\d]+:[\d]{2})(:[\d]{2})(.*)/, "$1$3");
};
// boot the app
initVirgil()
.then(() => init())
.catch(err => console.error(err.message));
Also see: Tab Triggers