├── .gitignore ├── bundle.js ├── entry.js ├── package.json ├── readme.md ├── script ├── build.sh ├── deploy.sh └── watch.sh └── src ├── author.js ├── comment.js ├── comment_list.js ├── echo_chamber.js ├── form.js ├── main.css └── md5.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *.db 4 | dist 5 | node_modules/ 6 | *.DS_Store 7 | log/ 8 | npm-debug.log 9 | public/js/core/models.js 10 | public/js/core/helpers.js -------------------------------------------------------------------------------- /bundle.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o" + 69 | "
" + 70 | "" + 71 | "
" + 72 | "
" + 73 | "

" + this.author.name + 74 | " on " + _renderDate(this.timestamp) + "" + 75 | "

" + 76 | "

" + this.text + "

" + 77 | "
" + 78 | "" 79 | ); 80 | } 81 | }; 82 | 83 | var _renderDate = function(timestamp) { 84 | var date = new Date(timestamp); 85 | return date.toDateString() + ' at ' + date.getHours() + ':' + date.getMinutes(); 86 | }; 87 | 88 | module.exports = Comment; 89 | 90 | },{"./md5":6}],3:[function(require,module,exports){ 91 | var Comment = require('./comment.js'); 92 | var Author = require('./author.js'); 93 | 94 | var CommentList = { 95 | 96 | init: function (form, renderCallback) { 97 | var list = document.createElement('div'); 98 | list.setAttribute('id', 'EC-list'); 99 | list.setAttribute('class', 'ec-list'); 100 | this.form = form; 101 | this.list = form.parentNode.appendChild(list); 102 | this.renderCallback = renderCallback; 103 | this.path = window.location.href; 104 | this.comments = this.load(); 105 | this.render(); 106 | }, 107 | 108 | load: function () { 109 | var rawComments = localStorage.getItem(this.path) || '[]'; 110 | return _parse(JSON.parse(rawComments)); 111 | }, 112 | 113 | fetch: function () { 114 | var rawComments = localStorage.getItem(this.path) || '[]'; 115 | return JSON.parse(rawComments); 116 | }, 117 | 118 | save: function () { 119 | localStorage.setItem(this.path, this.stringify()); 120 | }, 121 | 122 | render: function (target) { 123 | var count = this.comments.length; 124 | this.list.innerHTML = this.buildHTML(); 125 | this.form.firstChild.innerHTML = count + ' ' + _commentString(count); 126 | }, 127 | 128 | stringify: function () { 129 | return JSON.stringify(this.comments.map(function(item) { 130 | return { 131 | text: item.text, 132 | name: item.author.name, 133 | email: item.author.email, 134 | timestamp: item.timestamp 135 | } 136 | })); 137 | }, 138 | 139 | getHeight: function () { 140 | return this.list.clientHeight; 141 | }, 142 | 143 | buildHTML: function () { 144 | var comments = this.comments.slice(); 145 | return comments.reduce(function(total, comment) { 146 | return total + comment.render(); 147 | }, ''); 148 | } 149 | 150 | } 151 | 152 | var _commentString = function(count) { 153 | return count === 1 ? 'comment' : 'comments'; 154 | }; 155 | 156 | var _parse = function(srcComments) { 157 | return srcComments.map(function(comment) { 158 | var c = Object.create(Comment); 159 | var a = Object.create(Author); 160 | a.init(comment.name, comment.email); 161 | c.init(a, comment.text, comment.timestamp); 162 | return c; 163 | }); 164 | }; 165 | 166 | module.exports = CommentList; 167 | 168 | },{"./author.js":1,"./comment.js":2}],4:[function(require,module,exports){ 169 | var CommentList = require('./comment_list.js'); 170 | var Form = require('./form.js'); 171 | 172 | var App = { 173 | 174 | init: function () { 175 | this.entry = document.getElementById('echochamber'); 176 | 177 | this.attachIframe(); 178 | this.iframeDoc = this.iframe.contentWindow.document; 179 | this.pageStyles = _getBasicStyles(this.entry.parentNode); 180 | 181 | this.form = Object.create(Form); 182 | this.form.init(this.iframe); 183 | this.loadStylesheet(); 184 | }, 185 | 186 | attachIframe: function () { 187 | this.iframe = document.createElement('iframe'); 188 | this.iframe.style.width = '100%'; 189 | this.iframe.style.overflow = 'hidden'; 190 | this.iframe.style.border = "none"; 191 | this.iframe.style.opacity = 0; 192 | this.iframe.scrolling = false; 193 | this.iframe.style.transition = "opacity .5s"; 194 | this.iframe.setAttribute("horizontalscrolling", "no"); 195 | this.iframe.setAttribute("verticalscrolling", "no"); 196 | this.entry.parentNode.insertBefore(this.iframe, this.entry); 197 | }, 198 | 199 | loadStylesheet: function () { 200 | var link = document.createElement('link'), 201 | img = document.createElement( "img" ), 202 | body = document.body, 203 | head = this.iframeDoc.getElementsByTagName('head')[0], 204 | cssURL = EchoChamber.host + '/main.css'; 205 | link.rel = 'stylesheet'; 206 | link.type = 'text/css'; 207 | link.href = cssURL; 208 | head.appendChild(link); 209 | body.appendChild(img); 210 | img.src = cssURL; 211 | img.onerror = function() { 212 | body.removeChild(img); 213 | _applyPageStyles(this.iframeDoc, this.pageStyles); 214 | this.iframe.style.opacity = 1; 215 | this.addEventListeners(); 216 | }.bind(this); 217 | }, 218 | 219 | addEventListeners: function () { 220 | var self = this; 221 | this.iframe.contentWindow.addEventListener('resize', _debounce(self.form.resize.bind(self.form))); 222 | } 223 | 224 | }; 225 | 226 | var _applyPageStyles = function(doc, styles) { 227 | var body = doc.getElementsByTagName('body')[0]; 228 | for (var property in styles) { 229 | if (!styles.hasOwnProperty(property)) { 230 | return; 231 | } 232 | body.style[property] = styles[property]; 233 | } 234 | var buttons = doc.querySelectorAll(".button"); 235 | var paragraphs = doc.getElementsByTagName('p'); 236 | var bgColor; 237 | for (var i = 0; i < buttons.length; i++) { 238 | buttons[i].style['background-color'] = styles.anchorColor; 239 | } 240 | }; 241 | 242 | var _getStyle = function(node, property) { 243 | var value; 244 | value = window.getComputedStyle(node, null).getPropertyValue(property); 245 | if (value === '' || value === 'transparent' || value === 'rgba(0,0,0,0)') { 246 | return _getStyle(node.parentNode, property); 247 | } else { 248 | return value || ''; 249 | } 250 | }; 251 | 252 | var _getBasicStyles = function(container) { 253 | var anchor = document.createElement('a'); 254 | var paragraph = document.createElement('p'); 255 | container.appendChild(anchor); 256 | container.appendChild(paragraph); 257 | var styles = { 258 | anchorColor: _getStyle(anchor, 'color'), 259 | paragraphColor: _getStyle(paragraph, 'color'), 260 | fontFamily: _getStyle(container, 'font-family').replace(/['"]/g, '') 261 | } 262 | anchor.parentNode.removeChild(anchor); 263 | paragraph.parentNode.removeChild(paragraph); 264 | return styles; 265 | }; 266 | 267 | function _debounce(func, wait, immediate) { 268 | var timeout; 269 | return function() { 270 | var context = this, args = arguments; 271 | var later = function() { 272 | timeout = null; 273 | if (!immediate) func.apply(context, args); 274 | }; 275 | var callNow = immediate && !timeout; 276 | clearTimeout(timeout); 277 | timeout = setTimeout(later, wait); 278 | if (callNow) func.apply(context, args); 279 | }; 280 | }; 281 | 282 | module.exports = App; 283 | 284 | },{"./comment_list.js":3,"./form.js":5}],5:[function(require,module,exports){ 285 | var CommentList = require('./comment_list.js'); 286 | var Comment = require('./comment.js'); 287 | var Author = require('./author.js'); 288 | 289 | var Form = { 290 | 291 | init: function (iframe) { 292 | this.iframe = iframe; 293 | this.DOM = {}; 294 | this.initDOM(this.iframe); 295 | this.fields = this.DOM.form.getElementsByTagName('form')[0].elements; 296 | this.commentsList = Object.create(CommentList); 297 | this.commentsList.init(this.DOM.form, this.renderCallback); 298 | this.author = Object.create(Author); 299 | this.author.fetch(); 300 | this.addEventListeners(); 301 | this.resize(); 302 | }, 303 | 304 | addEventListeners: function () { 305 | this.DOM.form.addEventListener('submit', this.onClick.bind(this)); 306 | this.fields['text'].addEventListener('focus', this.onTextareaFocus.bind(this)); 307 | }, 308 | 309 | resize: function () { 310 | var formHeight = this.DOM.form.clientHeight; 311 | var margin = parseInt(window.getComputedStyle(this.DOM.form).marginBottom); 312 | var num = formHeight + margin + this.commentsList.getHeight() + 20; 313 | this.iframe.style.height = num + 'px'; 314 | }, 315 | 316 | initDOM: function () { 317 | this.doc = this.iframe.contentWindow.document; 318 | this.doc.write(_formTemplate); 319 | this.doc.close(); 320 | this.DOM.form = this.doc.getElementById('ECForm'); 321 | this.DOM.button = this.doc.getElementById('ECFormSubmit'); 322 | }, 323 | 324 | submit: function () { 325 | var comment = Object.create(Comment); 326 | this.author.init(this.fields['name'].value, this.fields['email'].value.trim()); 327 | comment.init(this.author, this.fields['text'].value, new Date().toString()); 328 | if (comment.validate()) { 329 | this.commentsList.comments.push(comment); 330 | this.commentsList.save(); 331 | this.commentsList.render(this.DOM.form); 332 | this.author.save(); 333 | this.clear(); 334 | } else { 335 | this.showErrors(comment.errors); 336 | } 337 | this.resize(); 338 | }, 339 | 340 | showErrors: function (errors) { 341 | errors.forEach(function(error) { 342 | var msg = this.doc.createElement('p'); 343 | msg.innerHTML = error.message; 344 | msg.classList.add('ec-error'); 345 | this.fields[error.field].parentNode.appendChild(msg); 346 | }.bind(this)); 347 | }, 348 | 349 | onTextareaFocus: function (e) { 350 | var fields = this.DOM.form.querySelectorAll('.ec-form__fields'); 351 | fields[0].style.display = 'block'; 352 | ['name', 'email'].forEach(function(property) { 353 | this.fields[property].value = this.author[property] || ''; 354 | }.bind(this)); 355 | this.resize(); 356 | }, 357 | 358 | clear: function () { 359 | ['text', 'name', 'email'].forEach(function(field) { 360 | this.fields[field].value = ''; 361 | }.bind(this)); 362 | }, 363 | 364 | onClick: function (e) { 365 | e.preventDefault(); 366 | this.submit(); 367 | } 368 | 369 | }; 370 | 371 | var _formTemplate = 372 | "
" + 373 | "

" + 374 | "
" + 375 | "
" + 376 | "" + 378 | "
" + 379 | "
" + 380 | "
" + 381 | "" + 382 | "
" + 383 | "
" + 384 | "" + 385 | "
" + 386 | "
" + 387 | "" + 388 | "
" + 389 | "
" + 390 | "
" + 391 | "
"; 392 | 393 | module.exports = Form; 394 | 395 | },{"./author.js":1,"./comment.js":2,"./comment_list.js":3}],6:[function(require,module,exports){ 396 | var md5 = function (string) { 397 | 398 | function RotateLeft(lValue, iShiftBits) { 399 | return (lValue<>>(32-iShiftBits)); 400 | } 401 | 402 | function AddUnsigned(lX,lY) { 403 | var lX4,lY4,lX8,lY8,lResult; 404 | lX8 = (lX & 0x80000000); 405 | lY8 = (lY & 0x80000000); 406 | lX4 = (lX & 0x40000000); 407 | lY4 = (lY & 0x40000000); 408 | lResult = (lX & 0x3FFFFFFF)+(lY & 0x3FFFFFFF); 409 | if (lX4 & lY4) { 410 | return (lResult ^ 0x80000000 ^ lX8 ^ lY8); 411 | } 412 | if (lX4 | lY4) { 413 | if (lResult & 0x40000000) { 414 | return (lResult ^ 0xC0000000 ^ lX8 ^ lY8); 415 | } else { 416 | return (lResult ^ 0x40000000 ^ lX8 ^ lY8); 417 | } 418 | } else { 419 | return (lResult ^ lX8 ^ lY8); 420 | } 421 | } 422 | 423 | function F(x,y,z) { return (x & y) | ((~x) & z); } 424 | function G(x,y,z) { return (x & z) | (y & (~z)); } 425 | function H(x,y,z) { return (x ^ y ^ z); } 426 | function I(x,y,z) { return (y ^ (x | (~z))); } 427 | 428 | function FF(a,b,c,d,x,s,ac) { 429 | a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac)); 430 | return AddUnsigned(RotateLeft(a, s), b); 431 | }; 432 | 433 | function GG(a,b,c,d,x,s,ac) { 434 | a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac)); 435 | return AddUnsigned(RotateLeft(a, s), b); 436 | }; 437 | 438 | function HH(a,b,c,d,x,s,ac) { 439 | a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac)); 440 | return AddUnsigned(RotateLeft(a, s), b); 441 | }; 442 | 443 | function II(a,b,c,d,x,s,ac) { 444 | a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac)); 445 | return AddUnsigned(RotateLeft(a, s), b); 446 | }; 447 | 448 | function ConvertToWordArray(string) { 449 | var lWordCount; 450 | var lMessageLength = string.length; 451 | var lNumberOfWords_temp1=lMessageLength + 8; 452 | var lNumberOfWords_temp2=(lNumberOfWords_temp1-(lNumberOfWords_temp1 % 64))/64; 453 | var lNumberOfWords = (lNumberOfWords_temp2+1)*16; 454 | var lWordArray=Array(lNumberOfWords-1); 455 | var lBytePosition = 0; 456 | var lByteCount = 0; 457 | while ( lByteCount < lMessageLength ) { 458 | lWordCount = (lByteCount-(lByteCount % 4))/4; 459 | lBytePosition = (lByteCount % 4)*8; 460 | lWordArray[lWordCount] = (lWordArray[lWordCount] | (string.charCodeAt(lByteCount)<>>29; 468 | return lWordArray; 469 | }; 470 | 471 | function WordToHex(lValue) { 472 | var WordToHexValue="",WordToHexValue_temp="",lByte,lCount; 473 | for (lCount = 0;lCount<=3;lCount++) { 474 | lByte = (lValue>>>(lCount*8)) & 255; 475 | WordToHexValue_temp = "0" + lByte.toString(16); 476 | WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length-2,2); 477 | } 478 | return WordToHexValue; 479 | }; 480 | 481 | function Utf8Encode(string) { 482 | string = string.replace(/\r\n/g,"\n"); 483 | var utftext = ""; 484 | 485 | for (var n = 0; n < string.length; n++) { 486 | 487 | var c = string.charCodeAt(n); 488 | 489 | if (c < 128) { 490 | utftext += String.fromCharCode(c); 491 | } 492 | else if((c > 127) && (c < 2048)) { 493 | utftext += String.fromCharCode((c >> 6) | 192); 494 | utftext += String.fromCharCode((c & 63) | 128); 495 | } 496 | else { 497 | utftext += String.fromCharCode((c >> 12) | 224); 498 | utftext += String.fromCharCode(((c >> 6) & 63) | 128); 499 | utftext += String.fromCharCode((c & 63) | 128); 500 | } 501 | 502 | } 503 | 504 | return utftext; 505 | }; 506 | 507 | var x=Array(); 508 | var k,AA,BB,CC,DD,a,b,c,d; 509 | var S11=7, S12=12, S13=17, S14=22; 510 | var S21=5, S22=9 , S23=14, S24=20; 511 | var S31=4, S32=11, S33=16, S34=23; 512 | var S41=6, S42=10, S43=15, S44=21; 513 | 514 | string = Utf8Encode(string); 515 | 516 | x = ConvertToWordArray(string); 517 | 518 | a = 0x67452301; b = 0xEFCDAB89; c = 0x98BADCFE; d = 0x10325476; 519 | 520 | for (k=0;k 34 | var EchoChamber = window.EchoChamber || {}; 35 | (function() { 36 | EchoChamber.discussionURL = window.location; 37 | var script = document.createElement('script'); 38 | script.src = 'https://s3.amazonaws.com/echochamberjs/dist/main.js'; 39 | script.async = true; 40 | var entry = document.getElementById('echochamber'); 41 | entry.parentNode.insertBefore(script, entry); 42 | })(); 43 | 44 | 45 | ``` 46 | 47 | ## Screenshot 48 | 49 | ![screenshot](https://s3.amazonaws.com/f.cl.ly/items/1C2d1h3E2D07432A1W2Q/Screen%20Shot%202015-07-14%20at%206.19.28%20PM.png) 50 | 51 | ## Contributing 52 | 53 | Requirements: [node](https://nodejs.org/) 54 | 55 | 1.Fork the repo 56 | * clone the fork 57 | * run `npm install` 58 | 59 | ### Local dev: 60 | 61 | If you want to work with the iframe environment, there are some steps: 62 | 63 | So you want the widget running on one server, and the host on another. I do this 64 | locally by messing with /etc/hosts like so: 65 | ``` 66 | 127.0.0.1 publisher.dev 67 | 127.0.0.1 widget.dev 68 | ``` 69 | 70 | * modify your httpd.conf file (in /etc/apache2) 71 | ``` 72 | 73 | ServerName publisher.dev 74 | DocumentRoot "/Users/username/directory-of-widget" 75 | 76 | 77 | ServerName widget.dev 78 | DocumentRoot "/Users/username/directory-of-page" 79 | 80 | ``` 81 | * restart apache 82 | * run `./script/watch.sh` during dev (unix only) 83 | * run `./script/build.sh` before making a pull request 84 | * make a pull request against the main repo referencing an issue if possible 85 | 86 | -------------------------------------------------------------------------------- /script/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | PATH=$(npm bin):$PATH 3 | if [[ ! -e dist ]]; then 4 | mkdir dist 5 | fi 6 | browserify entry.js > dist/main.js 7 | uglify -s dist/main.js -o dist/main.js 8 | cp src/main.css dist/main.css -------------------------------------------------------------------------------- /script/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./script/build.sh 3 | aws s3 cp dist s3://echochamberjs/dist --recursive --acl public-read 4 | -------------------------------------------------------------------------------- /script/watch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | PATH=$(npm bin):$PATH 3 | watchify entry.js -o bundle.js -v 4 | -------------------------------------------------------------------------------- /src/author.js: -------------------------------------------------------------------------------- 1 | var Author = { 2 | 3 | init: function(name, email) { 4 | this.name = name; 5 | this.email = email; 6 | }, 7 | 8 | gravatar: function() { 9 | return 'https://www.gravatar.com/avatar/' + _emailHash(this.email) + '?s=48'; 10 | }, 11 | 12 | save: function() { 13 | localStorage.setItem('author', JSON.stringify(this)); 14 | }, 15 | 16 | fetch: function() { 17 | var raw = localStorage.getItem('author') ? JSON.parse(localStorage.getItem('author')) : {name: "", email: ""}; 18 | this.init(raw.name, raw.email); 19 | }, 20 | 21 | validate: function() { 22 | var errors = []; 23 | ['name', 'email'].forEach(function(property) { 24 | if (!this[property]) { 25 | errors.push({ 26 | field: property, 27 | message: 'Please enter ' + property 28 | }); 29 | } 30 | }.bind(this)); 31 | return errors; 32 | } 33 | } 34 | 35 | var _emailHash = function(email) { 36 | return md5(email); 37 | }; 38 | 39 | 40 | module.exports = Author; 41 | -------------------------------------------------------------------------------- /src/comment.js: -------------------------------------------------------------------------------- 1 | md5 = require('./md5'); 2 | 3 | var Comment = { 4 | 5 | init: function(author, text, timestamp) { 6 | this.text = text; 7 | this.author = author; 8 | this.timestamp = timestamp; 9 | this.errors = []; 10 | }, 11 | 12 | validate: function() { 13 | this.errors = this.author.validate(); 14 | if (!this.text) { 15 | this.errors.push({ 16 | field: 'text', 17 | message: 'Please enter text' 18 | }); 19 | } 20 | return this.errors.length ? false : true; 21 | }, 22 | 23 | render: function() { 24 | return ( 25 | "
" + 26 | "
" + 27 | "" + 28 | "
" + 29 | "
" + 30 | "

" + this.author.name + 31 | " on " + _renderDate(this.timestamp) + "" + 32 | "

" + 33 | "

" + this.text + "

" + 34 | "
" + 35 | "
" 36 | ); 37 | } 38 | }; 39 | 40 | var _renderDate = function(timestamp) { 41 | var date = new Date(timestamp); 42 | return date.toDateString() + ' at ' + date.getHours() + ':' + date.getMinutes(); 43 | }; 44 | 45 | module.exports = Comment; 46 | -------------------------------------------------------------------------------- /src/comment_list.js: -------------------------------------------------------------------------------- 1 | var Comment = require('./comment.js'); 2 | var Author = require('./author.js'); 3 | 4 | var CommentList = { 5 | 6 | init: function (form, renderCallback) { 7 | var list = document.createElement('div'); 8 | list.setAttribute('id', 'EC-list'); 9 | list.setAttribute('class', 'ec-list'); 10 | this.form = form; 11 | this.list = form.parentNode.appendChild(list); 12 | this.renderCallback = renderCallback; 13 | this.path = EchoChamber.discussionURL; 14 | this.comments = this.load(); 15 | this.render(); 16 | }, 17 | 18 | load: function () { 19 | var rawComments = localStorage.getItem(this.path) || '[]'; 20 | return _parse(JSON.parse(rawComments)); 21 | }, 22 | 23 | fetch: function () { 24 | var rawComments = localStorage.getItem(this.path) || '[]'; 25 | return JSON.parse(rawComments); 26 | }, 27 | 28 | save: function () { 29 | localStorage.setItem(this.path, this.stringify()); 30 | }, 31 | 32 | render: function (target) { 33 | var count = this.comments.length; 34 | this.list.innerHTML = this.buildHTML(); 35 | this.form.firstChild.innerHTML = count + ' ' + _commentString(count); 36 | }, 37 | 38 | stringify: function () { 39 | return JSON.stringify(this.comments.map(function(item) { 40 | return { 41 | text: item.text, 42 | name: item.author.name, 43 | email: item.author.email, 44 | timestamp: item.timestamp 45 | } 46 | })); 47 | }, 48 | 49 | getHeight: function () { 50 | return this.list.clientHeight; 51 | }, 52 | 53 | buildHTML: function () { 54 | var comments = this.comments.slice(); 55 | return comments.reduce(function(total, comment) { 56 | return total + comment.render(); 57 | }, ''); 58 | } 59 | 60 | } 61 | 62 | var _commentString = function(count) { 63 | return count === 1 ? 'comment' : 'comments'; 64 | }; 65 | 66 | var _parse = function(srcComments) { 67 | return srcComments.map(function(comment) { 68 | var c = Object.create(Comment); 69 | var a = Object.create(Author); 70 | a.init(comment.name, comment.email); 71 | c.init(a, comment.text, comment.timestamp); 72 | return c; 73 | }); 74 | }; 75 | 76 | module.exports = CommentList; 77 | -------------------------------------------------------------------------------- /src/echo_chamber.js: -------------------------------------------------------------------------------- 1 | var CommentList = require('./comment_list.js'); 2 | var Form = require('./form.js'); 3 | 4 | var App = { 5 | 6 | init: function () { 7 | this.entry = document.getElementById('echochamber'); 8 | 9 | this.attachIframe(); 10 | this.iframeDoc = this.iframe.contentWindow.document; 11 | this.pageStyles = _getBasicStyles(this.entry.parentNode); 12 | 13 | this.form = Object.create(Form); 14 | this.form.init(this.iframe); 15 | this.loadStylesheet(); 16 | }, 17 | 18 | attachIframe: function () { 19 | this.iframe = document.createElement('iframe'); 20 | this.iframe.style.width = '100%'; 21 | this.iframe.style.overflow = 'hidden'; 22 | this.iframe.style.border = "none"; 23 | this.iframe.style.opacity = 0; 24 | this.iframe.scrolling = false; 25 | this.iframe.style.transition = "opacity .5s"; 26 | this.iframe.setAttribute("horizontalscrolling", "no"); 27 | this.iframe.setAttribute("verticalscrolling", "no"); 28 | this.entry.parentNode.insertBefore(this.iframe, this.entry); 29 | }, 30 | 31 | loadStylesheet: function () { 32 | var link = document.createElement('link'), 33 | img = document.createElement( "img" ), 34 | body = document.body, 35 | head = this.iframeDoc.getElementsByTagName('head')[0], 36 | cssURL = EchoChamber.host + '/main.css'; 37 | link.rel = 'stylesheet'; 38 | link.type = 'text/css'; 39 | link.href = cssURL; 40 | head.appendChild(link); 41 | body.appendChild(img); 42 | img.src = cssURL; 43 | img.onerror = function() { 44 | body.removeChild(img); 45 | _applyPageStyles(this.iframeDoc, this.pageStyles); 46 | this.iframe.style.opacity = 1; 47 | this.addEventListeners(); 48 | }.bind(this); 49 | }, 50 | 51 | addEventListeners: function () { 52 | var self = this; 53 | this.iframe.contentWindow.addEventListener('resize', _debounce(self.form.resize.bind(self.form))); 54 | } 55 | 56 | }; 57 | 58 | var _applyPageStyles = function(doc, styles) { 59 | var body = doc.getElementsByTagName('body')[0]; 60 | for (var property in styles) { 61 | if (!styles.hasOwnProperty(property)) { 62 | return; 63 | } 64 | body.style[property] = styles[property]; 65 | } 66 | var buttons = doc.getElementsByTagName("button"); 67 | var paragraphs = doc.getElementsByTagName('p'); 68 | var bgColor; 69 | for (var i = 0; i < buttons.length; i++) { 70 | buttons[i].style['background-color'] = styles.anchorColor; 71 | } 72 | }; 73 | 74 | var _getStyle = function(node, property) { 75 | var value; 76 | value = window.getComputedStyle(node, null).getPropertyValue(property); 77 | if (value === '' || value === 'transparent' || value === 'rgba(0,0,0,0)') { 78 | return _getStyle(node.parentNode, property); 79 | } else { 80 | return value || ''; 81 | } 82 | }; 83 | 84 | var _getBasicStyles = function(container) { 85 | var anchor = document.createElement('a'); 86 | var paragraph = document.createElement('p'); 87 | container.appendChild(anchor); 88 | container.appendChild(paragraph); 89 | var styles = { 90 | anchorColor: _getStyle(anchor, 'color'), 91 | paragraphColor: _getStyle(paragraph, 'color'), 92 | fontFamily: _getStyle(container, 'font-family').replace(/['"]/g, '') 93 | } 94 | anchor.parentNode.removeChild(anchor); 95 | paragraph.parentNode.removeChild(paragraph); 96 | return styles; 97 | }; 98 | 99 | function _debounce(func, wait, immediate) { 100 | var timeout; 101 | return function() { 102 | var context = this, args = arguments; 103 | var later = function() { 104 | timeout = null; 105 | if (!immediate) func.apply(context, args); 106 | }; 107 | var callNow = immediate && !timeout; 108 | clearTimeout(timeout); 109 | timeout = setTimeout(later, wait); 110 | if (callNow) func.apply(context, args); 111 | }; 112 | }; 113 | 114 | module.exports = App; 115 | -------------------------------------------------------------------------------- /src/form.js: -------------------------------------------------------------------------------- 1 | var CommentList = require('./comment_list.js'); 2 | var Comment = require('./comment.js'); 3 | var Author = require('./author.js'); 4 | 5 | var Form = { 6 | 7 | init: function (iframe) { 8 | this.iframe = iframe; 9 | this.DOM = {}; 10 | this.initDOM(this.iframe); 11 | this.fields = this.DOM.form.getElementsByTagName('form')[0].elements; 12 | this.commentsList = Object.create(CommentList); 13 | this.commentsList.init(this.DOM.form, this.renderCallback); 14 | this.author = Object.create(Author); 15 | this.author.fetch(); 16 | this.addEventListeners(); 17 | this.resize(); 18 | }, 19 | 20 | addEventListeners: function () { 21 | this.DOM.form.addEventListener('submit', this.onClick.bind(this)); 22 | this.fields['text'].addEventListener('focus', this.onTextareaFocus.bind(this)); 23 | }, 24 | 25 | resize: function () { 26 | var formHeight = this.DOM.form.clientHeight; 27 | var margin = parseInt(window.getComputedStyle(this.DOM.form).marginBottom); 28 | var num = formHeight + margin + this.commentsList.getHeight() + 20; 29 | this.iframe.style.height = num + 'px'; 30 | }, 31 | 32 | initDOM: function () { 33 | this.doc = this.iframe.contentWindow.document; 34 | this.doc.write(_formTemplate); 35 | this.doc.close(); 36 | this.DOM.form = this.doc.getElementById('ECForm'); 37 | this.DOM.button = this.doc.getElementById('ECFormSubmit'); 38 | }, 39 | 40 | submit: function () { 41 | var comment = Object.create(Comment); 42 | this.author.init(this.fields['name'].value, this.fields['email'].value.trim()); 43 | comment.init(this.author, this.fields['text'].value, new Date().toString()); 44 | if (comment.validate()) { 45 | this.commentsList.comments.push(comment); 46 | this.commentsList.save(); 47 | this.commentsList.render(this.DOM.form); 48 | this.author.save(); 49 | this.clear(); 50 | } else { 51 | this.showErrors(comment.errors); 52 | } 53 | this.resize(); 54 | }, 55 | 56 | showErrors: function (errors) { 57 | errors.forEach(function(error) { 58 | var msg = this.doc.createElement('p'); 59 | msg.innerHTML = error.message; 60 | msg.classList.add('ec-error'); 61 | this.fields[error.field].parentNode.appendChild(msg); 62 | }.bind(this)); 63 | }, 64 | 65 | onTextareaFocus: function (e) { 66 | var fields = this.DOM.form.querySelectorAll('.ec-form__fields'); 67 | fields[0].style.display = 'block'; 68 | ['name', 'email'].forEach(function(property) { 69 | this.fields[property].value = this.author[property] || ''; 70 | }.bind(this)); 71 | this.resize(); 72 | }, 73 | 74 | clear: function () { 75 | ['text', 'name', 'email'].forEach(function(field) { 76 | this.fields[field].value = ''; 77 | }.bind(this)); 78 | }, 79 | 80 | onClick: function (e) { 81 | e.preventDefault(); 82 | this.submit(); 83 | } 84 | 85 | }; 86 | 87 | var _formTemplate = 88 | "
" + 89 | "

" + 90 | "
" + 91 | "
" + 92 | "" + 94 | "
" + 95 | "
" + 96 | "
" + 97 | "" + 98 | "
" + 99 | "
" + 100 | "" + 101 | "
" + 102 | "
" + 103 | "" + 104 | "
" + 105 | "
" + 106 | "
" + 107 | "
"; 108 | 109 | module.exports = Form; 110 | -------------------------------------------------------------------------------- /src/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, Arial, sans-serif; 3 | margin: 0; 4 | } 5 | 6 | body.active { 7 | opacity: 1; 8 | } 9 | 10 | h1, h2, h3, h4, h5, h6 { 11 | font-family: inherit; 12 | margin-top: 0; 13 | } 14 | 15 | .ec-form-wrapper { 16 | background: white; 17 | overflow: hidden; 18 | border-top: 1px solid #e5e5e5; 19 | padding-top: 20px; 20 | } 21 | 22 | .ec-heading--2 { 23 | color: #444; 24 | font-weight: normal; 25 | } 26 | 27 | .ec-list { 28 | overflow: hidden; 29 | background: white; 30 | } 31 | 32 | .ec-form__field input, 33 | .ec-form__field textarea { 34 | border: 1px solid #e5e5e5; 35 | width: 100%; 36 | padding: 8px 12px; 37 | font-size: inherit; 38 | font-size: 14px; 39 | } 40 | 41 | .ec-form__field input { 42 | margin-top: 20px; 43 | } 44 | 45 | .ec-form__field input:focus, 46 | .ec-form__field textarea:focus { 47 | border: 1px solid #bdbdbd; 48 | outline: none; 49 | } 50 | 51 | .ec-form__fields { 52 | overflow: hidden; 53 | width: 50%; 54 | padding-bottom: 20px; 55 | display: none; 56 | } 57 | 58 | .ec-form { 59 | margin-bottom: 0; 60 | } 61 | 62 | .button { 63 | font-size: inherit; 64 | font-family: inherit; 65 | width: 100%; 66 | border: 0; 67 | color: white; 68 | padding: 8px 12px; 69 | cursor: pointer; 70 | margin-top: 20px; 71 | } 72 | 73 | .button:focus { 74 | outline: none; 75 | } 76 | 77 | .ec-list { 78 | padding-top: 20px; 79 | } 80 | 81 | .ec-comment { 82 | overflow: hidden; 83 | margin-bottom: 20px; 84 | padding-bottom: 20px; 85 | border-bottom: 1px solid #e5e5e5; 86 | } 87 | 88 | .ec-comment:last-child { 89 | margin-bottom: 0; 90 | } 91 | 92 | .ec-comment p:last-child { 93 | margin-bottom: 0; 94 | } 95 | 96 | .ec-comment__avatar { 97 | float: left; 98 | margin-right: 20px; 99 | } 100 | 101 | .ec-comment__body { 102 | overflow: hidden; 103 | } 104 | 105 | .ec-comment__body h4 { 106 | margin-bottom: 3px; 107 | line-height: 1.3; 108 | font-size: 14px; 109 | } 110 | 111 | .ec-comment__body h4 small { 112 | font-size: 12px; 113 | color: #bdbdbd; 114 | font-weight: normal; 115 | } 116 | 117 | .ec-comment__body p { 118 | margin-top: 0; 119 | font-size: 14px; 120 | line-height: 1.3; 121 | } 122 | 123 | .ec-error { 124 | margin: 0; 125 | color: #f20000; 126 | font-size: 14px; 127 | padding: 10px 0; 128 | } 129 | 130 | @media screen and (max-width: 480px) { 131 | .ec-form__fields { 132 | width: 100%; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/md5.js: -------------------------------------------------------------------------------- 1 | var md5 = function (string) { 2 | 3 | function RotateLeft(lValue, iShiftBits) { 4 | return (lValue<>>(32-iShiftBits)); 5 | } 6 | 7 | function AddUnsigned(lX,lY) { 8 | var lX4,lY4,lX8,lY8,lResult; 9 | lX8 = (lX & 0x80000000); 10 | lY8 = (lY & 0x80000000); 11 | lX4 = (lX & 0x40000000); 12 | lY4 = (lY & 0x40000000); 13 | lResult = (lX & 0x3FFFFFFF)+(lY & 0x3FFFFFFF); 14 | if (lX4 & lY4) { 15 | return (lResult ^ 0x80000000 ^ lX8 ^ lY8); 16 | } 17 | if (lX4 | lY4) { 18 | if (lResult & 0x40000000) { 19 | return (lResult ^ 0xC0000000 ^ lX8 ^ lY8); 20 | } else { 21 | return (lResult ^ 0x40000000 ^ lX8 ^ lY8); 22 | } 23 | } else { 24 | return (lResult ^ lX8 ^ lY8); 25 | } 26 | } 27 | 28 | function F(x,y,z) { return (x & y) | ((~x) & z); } 29 | function G(x,y,z) { return (x & z) | (y & (~z)); } 30 | function H(x,y,z) { return (x ^ y ^ z); } 31 | function I(x,y,z) { return (y ^ (x | (~z))); } 32 | 33 | function FF(a,b,c,d,x,s,ac) { 34 | a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac)); 35 | return AddUnsigned(RotateLeft(a, s), b); 36 | }; 37 | 38 | function GG(a,b,c,d,x,s,ac) { 39 | a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac)); 40 | return AddUnsigned(RotateLeft(a, s), b); 41 | }; 42 | 43 | function HH(a,b,c,d,x,s,ac) { 44 | a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac)); 45 | return AddUnsigned(RotateLeft(a, s), b); 46 | }; 47 | 48 | function II(a,b,c,d,x,s,ac) { 49 | a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac)); 50 | return AddUnsigned(RotateLeft(a, s), b); 51 | }; 52 | 53 | function ConvertToWordArray(string) { 54 | var lWordCount; 55 | var lMessageLength = string.length; 56 | var lNumberOfWords_temp1=lMessageLength + 8; 57 | var lNumberOfWords_temp2=(lNumberOfWords_temp1-(lNumberOfWords_temp1 % 64))/64; 58 | var lNumberOfWords = (lNumberOfWords_temp2+1)*16; 59 | var lWordArray=Array(lNumberOfWords-1); 60 | var lBytePosition = 0; 61 | var lByteCount = 0; 62 | while ( lByteCount < lMessageLength ) { 63 | lWordCount = (lByteCount-(lByteCount % 4))/4; 64 | lBytePosition = (lByteCount % 4)*8; 65 | lWordArray[lWordCount] = (lWordArray[lWordCount] | (string.charCodeAt(lByteCount)<>>29; 73 | return lWordArray; 74 | }; 75 | 76 | function WordToHex(lValue) { 77 | var WordToHexValue="",WordToHexValue_temp="",lByte,lCount; 78 | for (lCount = 0;lCount<=3;lCount++) { 79 | lByte = (lValue>>>(lCount*8)) & 255; 80 | WordToHexValue_temp = "0" + lByte.toString(16); 81 | WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length-2,2); 82 | } 83 | return WordToHexValue; 84 | }; 85 | 86 | function Utf8Encode(string) { 87 | string = string.replace(/\r\n/g,"\n"); 88 | var utftext = ""; 89 | 90 | for (var n = 0; n < string.length; n++) { 91 | 92 | var c = string.charCodeAt(n); 93 | 94 | if (c < 128) { 95 | utftext += String.fromCharCode(c); 96 | } 97 | else if((c > 127) && (c < 2048)) { 98 | utftext += String.fromCharCode((c >> 6) | 192); 99 | utftext += String.fromCharCode((c & 63) | 128); 100 | } 101 | else { 102 | utftext += String.fromCharCode((c >> 12) | 224); 103 | utftext += String.fromCharCode(((c >> 6) & 63) | 128); 104 | utftext += String.fromCharCode((c & 63) | 128); 105 | } 106 | 107 | } 108 | 109 | return utftext; 110 | }; 111 | 112 | var x=Array(); 113 | var k,AA,BB,CC,DD,a,b,c,d; 114 | var S11=7, S12=12, S13=17, S14=22; 115 | var S21=5, S22=9 , S23=14, S24=20; 116 | var S31=4, S32=11, S33=16, S34=23; 117 | var S41=6, S42=10, S43=15, S44=21; 118 | 119 | string = Utf8Encode(string); 120 | 121 | x = ConvertToWordArray(string); 122 | 123 | a = 0x67452301; b = 0xEFCDAB89; c = 0x98BADCFE; d = 0x10325476; 124 | 125 | for (k=0;k