├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── app ├── components │ ├── comment-controls.js │ ├── comment-form.js │ ├── comment-list.js │ ├── comment.js │ ├── conversation.js │ └── login.js ├── index.js ├── ouija.js ├── post.js ├── styles │ ├── ouija.css │ └── ouija.scss └── users.js ├── config ├── acl.json └── ouija.json.example ├── deploy ├── config.json ├── deploy.js └── lib │ ├── hipchat_notify.js │ ├── s3_deploy.js │ └── tag_checker.js ├── gulpfile.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | ouija.json 3 | dist/ 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "passfail" : false, 3 | "maxerr" : 100, 4 | 5 | "browser" : true, 6 | "node" : false, 7 | "jquery" : true, 8 | "predef" : [ "goinstant" ], 9 | 10 | "debug" : false, 11 | "devel" : true, 12 | 13 | "strict" : false, 14 | "globalstrict" : false, 15 | 16 | "quotmark" : "single", 17 | 18 | 19 | "asi" : false, 20 | "laxbreak" : false, 21 | "bitwise" : true, 22 | "boss" : false, 23 | "curly" : true, 24 | "eqeqeq" : true, 25 | "eqnull" : false, 26 | "evil" : false, 27 | "expr" : false, 28 | "forin" : false, 29 | "immed" : true, 30 | "latedef" : true, 31 | "loopfunc" : false, 32 | "noarg" : true, 33 | "regexp" : true, 34 | "regexdash" : false, 35 | "scripturl" : true, 36 | "shadow" : false, 37 | "supernew" : false, 38 | "undef" : true, 39 | 40 | "nomen" : false, 41 | "unused" : true, 42 | 43 | "newcap" : false, 44 | "noempty" : true, 45 | "nonew" : true, 46 | "nomen" : true, 47 | "onevar" : true, 48 | "plusplus" : true, 49 | "sub" : true, 50 | "trailing" : true, 51 | "white" : true, 52 | "indent" : 4 53 | } 54 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_install: 5 | - npm install -g gulp 6 | before_script: 7 | - gulp build 8 | after_success: 9 | - node deploy/deploy.js 10 | notifications: 11 | hipchat: 12 | rooms: 13 | secure: dtqcEHHl3llzuyRBL1lutQOGbfU6RAzbQuEgijoWD1iP21q4Khj4UJCObQ5dCx3Kfc6/VP2o2l597afcjdsCuWV04tqeyFrvSFcxAdVZDpSnaSfj4xmzRI5+OwIID/+spTgMo2Duxu23yC4fGRFzVAxexs3QA/qyu1Re7eEgKww= 14 | template: 15 | - '%{branch} - %{commit} 16 | : %{message}' 17 | format: html 18 | env: 19 | global: 20 | - secure: c0xU8GirCCHM65ZMo7ZXVa2Ra8uPZXWT7wDz/GlO2Q6AJSl7ni+XKsKOCQEWqEEnZJWRi3jOwDEnkzx7bohEdPd8VB4JddloqjJKLmEApomwqPXfoAamQxJluM9b4AI6VIOTEfdFftpEudr4krXbU0D085JIYhDU57I3F17fcuA= 21 | - secure: XUT7XOlzwfXi+wHNyrgYFpSQGeSB6COrBsf1UMPqql0f/oIoyENAsW1KEpllDiF9/L7wy7U1ktFm2BuB+H2W8GMDHqtAsKDM8DRqYeD3ZrAt+djWzQmvFgdI9Sl5xLUdrvvdUXAmGkF7+XRLrXp715N8jBGBXlMN0G9d1zj1Yrc= 22 | - secure: KJS+ofxvbnD9yvW34dFHlT1D1MUhe3eOQwf6TC62fZ9BKlWTSKG3B3JvfMuLMsW7mtafTaswD9FYCiGcyXTNbh/S13glvt5aIsY7MeMfhXdos7YfNlzHP6znzWSepQzmRw4AR2H4DFTXxB1skh9+d/ePbGQhgLuh0CKxUMBNqAY= 23 | - secure: Yi/5ZVdWl90ieppK+r96LGmCoOgNrNehAbTDJfhunsZdPk9FtbZNXZF77WgSymHMc7pEwKBhfC8ZHmhr+5ebbhG0R8QqifMcuMoNQzQY/qPdk+gNDlzoxZ34k/UdY4d18RI2DwJMRid+sBn+m6kBoi/lqyw80T5Ms4zL1YNBIlE= 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v0.1.4 2 | 3 | - keep control visible if block has comments 4 | - disable block transition if ouija is already active 5 | - added colophon 6 | - jshint code linting 7 | - refactor .jsx to .js 8 | - added keyboard submitting with meta+enter/ctrl+enter 9 | - fix active state for first comment 10 | 11 | ### v0.1.3 12 | 13 | - set comments visibility to hidden 14 | - moved sizing variables, updated offset 15 | - fix(click controls): don't collapse on all clicks 16 | 17 | ### v0.1.2 18 | 19 | - section_elements option for specifying which elements should display a commenting block 20 | - css reset for better theme compatibility 21 | 22 | ### v0.1.1 23 | 24 | - feat(article_content option): adds a article_content option. 25 | 26 | ### v.1.0 27 | 28 | - The beginning of time. 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Salesforce.com, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | - Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | - Neither the name of Salesforce.com nor the names of its contributors may be 15 | used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Ouija Banner](https://dl.dropboxusercontent.com/u/783535/ouija/ouija-readme.png) 2 | 3 | # [Ouija](http://ouija.io) 4 | #### An inline commenting app for Ghost: http://ouija.io 5 | Ouija brings inline commenting to your Ghost blog. See below for install instructions. 6 | 7 | ## Quick Install 8 | 9 | 1. Paste the Ouija CSS into the `` section of your theme's `default.hbs` file. `` 10 | 11 | 2. Paste the following snippet before the closing `` tag. Replace `YOURACCOUNT/YOURAPP` with your GoInstant details. 12 | Need a GoInstant connect URL? [Sign up here](https://goinstant.com/signup?src=ouija). 13 | 14 | ``` 15 | {{#with post}} 16 | 20 | 21 | 22 | {{/with}} 23 | ``` 24 | 25 | ## Developer Setup 26 | These instructions assume you already have a Ghost blog set up. If not, follow the [Getting Started Guide for Developers](https://github.com/TryGhost/Ghost#getting-started-guide-for-developers) from Ghost to set one up. 27 | 28 | 1. Clone this repo into the `content/apps` folder of your blog. 29 | 30 | 1. Copy the `config/ouija.json.example` to `config/ouija.json` and insert the name of your custom theme folder. If you're using the default theme Casper, you don't need to change anything. 31 | 32 | 1. Execute `$ npm install` 33 | 34 | 1. Execute `$ gulp develop` 35 | 36 | 1. Add the CSS to the `` section of your blog's `default.hbs` file: `` 37 | 38 | 1. Paste the following snippet before the closing `` tag. Replace `YOURACCOUNT/YOURAPP` with your GoInstant connect URL. 39 | Need a GoInstant connect URL? [Sign up here](https://goinstant.com/signup?src=ouija). 40 | 41 | ``` 42 | {{#with post}} 43 | 47 | 48 | 49 | {{/with}} 50 | ``` 51 | 52 | ## How to create a GoInstant app 53 | 54 | GoInstant is used to sync comments in real-time. You'll need to create a new app in GoInstant in order to use Ouija. 55 | 56 | ##### Created by GoInstant 57 | 1. If you haven't already, [sign up for GoInstant](https://goinstant.com/signup?src=ouija). From the GoInstant dashboard, __create a new app__ for Ouija. 58 | 59 | 1. Navigate to your new app's __Authentication__ page. 60 | 61 | 1. Add your blog's URL as an Authorized origin. If you're working locally, you might want to add `localhost`, too. 62 | 63 | 1. Ouija commenters login through Twitter (for now). Turn on __Twitter__ in the list of providers. Don't worry about the settings inside the Twitter box. 64 | 65 | 1. Copy the contents of `config/acl.json` to your app's ACL. This gives Ouija the correct permissions to run on your blog. You can change your app's ACL under __Security__. 66 | 67 | ## How to configure Ouija 68 | 69 | You can configure Ouija by putting vars on the window before the Ouija script loads. 70 | 71 | #### List of available options 72 | 73 | | Option | Type | Default | Description | 74 | |:---|:---|:---|:---| 75 | | `window.ouija_identifier` | [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) | N/A | Ghost Post UID. A unique, static ID for the blog post. | 76 | | `window.ouija_connect_url` | [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) | N/A | Your GoInstant connect URL. Used for connecting to the [GoInstant platform](https://goinstant.com/). | 77 | | `window.ouija_article_content` | [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) | `'.post-content'` | Selector for the section elements' parent. | 78 | | `window.ouija_section_elements` | [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) | `'p, ol, :has(img)'` | Selector for which sections in the post should have a comment block. | 79 | 80 | #### Example 81 | 82 | ```html 83 | 89 | 90 | 91 | ``` 92 | 93 | ## License 94 | © 2014 GoInstant Inc., a salesforce.com company. Licensed under the BSD 3-clause license. 95 | 96 | [![GoInstant](http://goinstant.com/static/img/logo.png)](http://goinstant.com) 97 | -------------------------------------------------------------------------------- /app/components/comment-controls.js: -------------------------------------------------------------------------------- 1 | /* jshint browser:true */ 2 | /* global require, module */ 3 | 4 | /** 5 | * @fileOverview 6 | * 7 | * Comment Controls React Component 8 | **/ 9 | 10 | var React = require('react'), 11 | AddControl, 12 | LoadControl, 13 | CountControl, 14 | CommentControls = {}; 15 | 16 | AddControl = React.createClass({displayName: 'AddControl', 17 | render: function () { 18 | return React.DOM.a({ href: '#', className: 'add'}); 19 | } 20 | }); 21 | 22 | LoadControl = React.createClass({ 23 | displayName: 'LoadControl', 24 | render: function () { 25 | return React.DOM.a({ href: '#', className: 'loader'}, 26 | React.DOM.span({ className: 'ouija-loader'}) 27 | ); 28 | } 29 | }); 30 | 31 | CountControl = React.createClass({ 32 | displayName: 'CountControl', 33 | render: function () { 34 | return React.DOM.a({ href: '#', className: 'count'}, 35 | React.DOM.span(null, this.props.count ) 36 | ); 37 | } 38 | }); 39 | 40 | CommentControls.render = function () { 41 | var activeControl = (LoadControl(null)); 42 | 43 | if (!this.props.isLoading) { 44 | activeControl = ( 45 | AddControl(null) 46 | ); 47 | } 48 | 49 | if (this.props.commentCount) { 50 | activeControl = CountControl({ count: this.props.commentCount }); 51 | } 52 | 53 | return React.DOM.div({ className: 'ouija-controls'}, activeControl); 54 | }; 55 | 56 | module.exports = React.createClass(CommentControls); 57 | -------------------------------------------------------------------------------- /app/components/comment-form.js: -------------------------------------------------------------------------------- 1 | /* jshint browser:true */ 2 | /* global require, module */ 3 | 4 | /** 5 | * @fileOverview 6 | * 7 | * Comment Form React Component 8 | **/ 9 | 10 | var React = require('react'), 11 | Q = require('q'), 12 | Login = require('./login'), 13 | CommentForm = {}; 14 | 15 | CommentForm.getInitialState = function () { 16 | return { user: {} }; 17 | }; 18 | 19 | CommentForm.componentWillMount = function () { 20 | var self = this, 21 | users = this.props.users; 22 | 23 | Q.all([ 24 | users.isGuest(), 25 | users.getSelf(), 26 | users.logoutUrl(), 27 | users.loginUrl() 28 | ]).spread(function (isGuest, currentUser, logoutUrl, loginUrl) { 29 | if (isGuest) { 30 | return self.setState({ 31 | isGuest: true, 32 | user: null, 33 | loginComponent: ( 34 | Login({ loginUrl: loginUrl } ) 35 | ) 36 | }); 37 | } 38 | 39 | self.setState({ 40 | user: currentUser, 41 | logoutUrl: logoutUrl 42 | }); 43 | }).fail(function (err) { 44 | // ToDo: Do proper error handling :-) 45 | console.log('ahh', err.stack); 46 | }); 47 | }; 48 | 49 | CommentForm.handleSubmit = function (e) { 50 | var content = this.refs.content.getDOMNode().value.trim(); 51 | 52 | if (content) { 53 | this.props.onCommentSubmit({ content: content }); 54 | this.refs.content.getDOMNode().value = ''; 55 | } 56 | 57 | e.preventDefault(); 58 | }; 59 | 60 | CommentForm.handleCancel = function (e) { 61 | this.props.onCommentCancel(); 62 | 63 | e.preventDefault(); 64 | }; 65 | 66 | CommentForm.handleKeyDown = function (e) { 67 | if (e.keyCode === 13 && (e.metaKey || e.ctrlKey)) { 68 | this.handleSubmit(e); 69 | } 70 | }; 71 | 72 | CommentForm.render = function () { 73 | if (this.state && this.state.isGuest) { 74 | return this.state.loginComponent; 75 | } 76 | 77 | return React.DOM.form({ className: 'ouija-comment ouija-new', onSubmit: this.handleSubmit }, 78 | React.DOM.span({ className: 'ouija-avatar'}, 79 | React.DOM.img({ src: this.state.user.avatarUrl, alt: 'avatar'}) 80 | ), 81 | React.DOM.div({ className: 'ouija-author'}, 82 | React.DOM.a({ 83 | href: 'https://twitter.com/' + this.state.user.username, 84 | target: '_blank', 85 | alt: this.state.user.displayName 86 | }, this.state.user.displayName), 87 | React.DOM.a({ 88 | className: 'ouija-button text', 89 | href: this.state.logoutUrl 90 | }, 'Logout') 91 | ), 92 | React.DOM.div({ className: 'ouija-content'}, 93 | React.DOM.textarea({ 94 | ref: 'content', 95 | placeholder: 'Leave a comment...', 96 | onKeyDown: this.handleKeyDown 97 | }), 98 | React.DOM.footer(null, 99 | React.DOM.button({ className: 'text ouija-cancel', onClick: this.handleCancel }, 'Cancel'), 100 | React.DOM.button({ type: 'submit'}, 'Comment') 101 | ) 102 | ) 103 | ); 104 | }; 105 | 106 | module.exports = React.createClass(CommentForm); 107 | -------------------------------------------------------------------------------- /app/components/comment-list.js: -------------------------------------------------------------------------------- 1 | /* jshint browser:true */ 2 | /* global require, module */ 3 | 4 | /** 5 | * @fileOverview 6 | * 7 | * Comment-list React Component 8 | **/ 9 | 10 | var _ = require('lodash'), 11 | React = require('react'), 12 | Comment = require('./comment'), 13 | CommentList = {}; 14 | 15 | CommentList.componentWillUpdate = function () { 16 | var node = this.getDOMNode(), 17 | scrollPosition; 18 | 19 | if (node.scrollTop) { 20 | scrollPosition = node.scrollTop + node.offsetHeight; 21 | this.shouldScrollBottom = scrollPosition >= node.scrollHeight*0.8; 22 | } 23 | }; 24 | 25 | CommentList.componentDidUpdate = function () { 26 | var node; 27 | 28 | if (this.shouldScrollBottom) { 29 | node = this.getDOMNode(); 30 | node.scrollTop = node.scrollHeight; 31 | } 32 | }; 33 | 34 | CommentList.render = function() { 35 | var commentNodes = _.map(this.props.data, function (comment, id) { 36 | var author = _.pick(comment, [ 37 | 'displayName', 38 | 'userId', 39 | 'username', 40 | 'avatarUrl' 41 | ]); 42 | 43 | return Comment({ key: id, author: author}, comment.content); 44 | }); 45 | 46 | return React.DOM.div(null, commentNodes); 47 | }; 48 | 49 | module.exports = React.createClass(CommentList); 50 | -------------------------------------------------------------------------------- /app/components/comment.js: -------------------------------------------------------------------------------- 1 | /* jshint browser:true */ 2 | /* global require, module */ 3 | 4 | /** 5 | * @fileOverview 6 | * 7 | * Comment Component 8 | **/ 9 | 10 | var React = require('react'), 11 | Comment = {}; 12 | 13 | Comment.render = function () { 14 | return React.DOM.div({ className: 'ouija-comment'}, 15 | React.DOM.span({ className: 'ouija-avatar'}, 16 | React.DOM.img({ src: this.props.author.avatarUrl, alt: 'avatar'}) 17 | ), 18 | React.DOM.div({ className:'ouija-author'}, 19 | React.DOM.a({ 20 | href: 'https://twitter.com/' + this.props.author.username, 21 | target: '_blank', 22 | alt: 'Timestamp' 23 | }, this.props.author.displayName) 24 | ), 25 | React.DOM.div({ className: 'ouija-content'}, 26 | React.DOM.p(null, this.props.children ) 27 | ) 28 | ); 29 | }; 30 | 31 | module.exports = React.createClass(Comment); 32 | -------------------------------------------------------------------------------- /app/components/conversation.js: -------------------------------------------------------------------------------- 1 | /* jshint browser:true */ 2 | /* global require, module */ 3 | 4 | /** 5 | * @fileOverview 6 | * 7 | * Conversation React Component 8 | **/ 9 | 10 | var _ = require('lodash'), 11 | React = require('react/addons'), 12 | CommentList = require('./comment-list'), 13 | CommentForm = require('./comment-form'), 14 | CommentControls = require('./comment-controls'), 15 | Conversation = {}; 16 | 17 | Conversation.getInitialState = function () { 18 | return { 19 | comments: {}, 20 | isActive: false, 21 | loading: true, 22 | count: 0 23 | }; 24 | }; 25 | 26 | Conversation.handleCommentSubmit = function (comment) { 27 | var sectionName = this.props.section; 28 | 29 | this.props.comments.add(sectionName, comment); 30 | }; 31 | 32 | Conversation.componentWillMount = function () { 33 | var self = this, 34 | comments = this.props.comments; 35 | 36 | comments.getComments(this.props.section).then(function (comments) { 37 | self.setState({ comments: comments, loading: false, count: _.keys(comments).length }); 38 | }).fail(function (err) { 39 | // ToDo: Proper error handling :) 40 | console.log('wuh oh', err); 41 | }); 42 | 43 | comments.on('newComment', function (sectionName) { 44 | if (sectionName !== self.props.section) { 45 | return; 46 | } 47 | 48 | self.props.comments.getComments(self.props.section).then(function (data) { 49 | self.setState({ comments: data, count: _.keys(data).length }); 50 | }); 51 | }); 52 | }; 53 | 54 | Conversation.render = function () { 55 | var cx = React.addons.classSet, 56 | classes = cx({ 57 | 'ouija-active': this.state.isActive, 58 | 'ouija-has-comments': this.state.count 59 | }); 60 | 61 | return React.DOM.div({ className: 'ouija ' + classes }, 62 | CommentControls({ 63 | isLoading: this.state.loading, 64 | commentCount: this.state.count 65 | }), 66 | 67 | React.DOM.div({ className:'ouija-comments'}, 68 | CommentList({ data: this.state.comments }), 69 | CommentForm({ 70 | onCommentSubmit: this.handleCommentSubmit, 71 | onCommentCancel: this.handleCommentClose, 72 | users: this.props.users 73 | }) 74 | 75 | ) 76 | ); 77 | }; 78 | 79 | module.exports = React.createClass(Conversation); 80 | -------------------------------------------------------------------------------- /app/components/login.js: -------------------------------------------------------------------------------- 1 | /* jshint browser:true */ 2 | /* global require, module */ 3 | 4 | /** 5 | * @fileOverview 6 | * 7 | * Login React Component 8 | **/ 9 | 10 | var React = require('react'), 11 | Login = {}; 12 | 13 | Login.render = function () { 14 | return React.DOM.form({ className: 'ouija-comment ouija-login'}, 15 | React.DOM.h5(null, 'Sign in to comment'), 16 | React.DOM.a({ href: this.props.loginUrl, className: 'ouija-button'}, 17 | React.DOM.span({ className: 'icon-twitter'}), 18 | 'Sign in with Twitter' 19 | ) 20 | ); 21 | }; 22 | 23 | module.exports = React.createClass(Login); -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | /* jshint browser:true */ 2 | /* global require */ 3 | 4 | /** 5 | * @fileOverview 6 | * 7 | * We abstract retrieving the configuration options and instantiate Ouija here 8 | **/ 9 | 10 | var _ = require('lodash'), 11 | Ouija = require('./ouija'), 12 | DEFAULTS = { 13 | articleContent: '.post-content', 14 | sectionElements: 'p, ol, :has(img)' 15 | }, 16 | config, 17 | ouija; 18 | 19 | config = { 20 | identifier: window.ouija_identifier, // Ghost Post UID 21 | connectUrl: window.ouija_connect_url, // GoInstant connect URL 22 | articleContent: window.ouija_article_content, // Selector for article content 23 | sectionElements: window.ouija_section_elements // Section selector 24 | }; 25 | 26 | config = _.defaults(config, DEFAULTS); 27 | 28 | ouija = new Ouija(config); 29 | 30 | ouija.initialize(); 31 | -------------------------------------------------------------------------------- /app/ouija.js: -------------------------------------------------------------------------------- 1 | /* jshint browser:true */ 2 | /* global require, module, goinstant */ 3 | 4 | /** 5 | * @fileOverview 6 | * 7 | * This file contains the hot, gooey class at the center of Ouija 8 | **/ 9 | 10 | var _ = require('lodash'), 11 | React = require('react'), 12 | Post = require('./post'), 13 | Users = require('./users'), 14 | Conversation = require('./components/conversation'), 15 | 16 | OUIJA_POST_CLASS = 'post-ouija', 17 | ACTIVE_CLASS = 'ouija-active', 18 | CONTROLS_CLASS = '.ouija-controls', 19 | COMMENTS_CLASS = '.ouija-comments', 20 | ARTICLE_SEL = 'article', 21 | CONVERSATIONS_SEL = 'div.ouija', 22 | CANCEL_SEL = 'div.ouija .ouija-cancel'; 23 | 24 | /** 25 | * The Ouija class is responsible for: 26 | * (a) connecting to GoInstant 27 | * (b) creating User & Post instances 28 | * (c) identifying relevant DOM nodes 29 | * (d) creating and inserting the Conversation components 30 | * 31 | * @public 32 | * @class 33 | * @constructor 34 | * @param {Object} config 35 | */ 36 | function Ouija(config) { 37 | _.extend(this, { 38 | url: config.connectUrl, 39 | identifier: config.identifier, 40 | articleContent: config.articleContent, 41 | sectionElements: config.sectionElements, 42 | el: {}, 43 | conversations: {}, 44 | activeComponent: null 45 | }); 46 | 47 | _.bindAll(this, [ 48 | 'handleControlsClick', 49 | 'handleCommentsClick', 50 | 'handleCollapseClick' 51 | ]); 52 | } 53 | 54 | Ouija.NAMESPACE = 'ouija'; 55 | 56 | Ouija.prototype.initialize = function() { 57 | this.connection = this.connect(); 58 | this.users = new Users(this.connection); 59 | this.post = new Post(this.identifier, this.connection, this.users); 60 | 61 | this.el.article = $(ARTICLE_SEL); 62 | this.el.article.addClass(OUIJA_POST_CLASS); 63 | 64 | this.parseContent(); 65 | this.labelSections(); 66 | this.renderSections(); 67 | this.registerListeners(); 68 | }; 69 | 70 | Ouija.prototype.connect = function () { 71 | var self = this; 72 | 73 | return goinstant.connect(this.url, { 74 | room: ['lobby', 'post_' + self.identifier] 75 | }); 76 | }; 77 | 78 | Ouija.prototype.getSections = function (content) { 79 | return content 80 | .children(this.sectionElements) 81 | .filter(function(el) { 82 | return $(el).not(':empty'); 83 | }); 84 | }; 85 | 86 | Ouija.prototype.parseContent = function () { 87 | this.el.content = $(this.articleContent); 88 | this.el.sections = this.getSections(this.el.content); 89 | }; 90 | 91 | Ouija.prototype.labelSections = function () { 92 | var self = this; 93 | 94 | this.sections = {}; 95 | 96 | _.each(this.el.sections, function (elem, index) { 97 | var sectionName = 'section_' + index; 98 | var $section = $('
').appendTo(elem); 99 | 100 | self.sections[sectionName] = $section; 101 | }); 102 | }; 103 | 104 | Ouija.prototype.renderSections = function () { 105 | var self = this; 106 | 107 | _.each(self.sections, function ($section, sectionName) { 108 | self.conversations[sectionName] = React.renderComponent( 109 | Conversation({ 110 | comments: self.post, 111 | users: self.users, 112 | section: sectionName 113 | }), $section[0] 114 | ); 115 | }); 116 | }; 117 | 118 | Ouija.prototype.registerListeners = function () { 119 | $(CONTROLS_CLASS).on('click', this.handleControlsClick); 120 | $(CANCEL_SEL).on('click', this.handleCollapseClick); 121 | $(COMMENTS_CLASS).on('click', this.handleCommentsClick); 122 | $(document).on('click', this.handleCollapseClick); 123 | }; 124 | 125 | Ouija.prototype.handleControlsClick = function (e) { 126 | e.preventDefault(); 127 | 128 | var $conversation = $(e.target).closest(CONVERSATIONS_SEL); 129 | var sectionName = $conversation.parent().attr('id'); 130 | var component = this.conversations[sectionName]; 131 | 132 | // Let the event bubble up to document to collapse conversation 133 | if (component.state.isActive) { 134 | return; 135 | } 136 | 137 | // Don't let event bubble up to document 138 | e.stopPropagation(); 139 | 140 | this.collapseCurrent(); 141 | 142 | component.setState({ isActive: true }); 143 | this.el.article.addClass(ACTIVE_CLASS); 144 | 145 | this.activeComponent = component; 146 | }; 147 | 148 | Ouija.prototype.handleCommentsClick = function (e) { 149 | // Don't let event bubble up to document 150 | e.stopPropagation(); 151 | }; 152 | 153 | Ouija.prototype.handleCollapseClick = function () { 154 | this.collapseCurrent(); 155 | 156 | this.el.article.removeClass(ACTIVE_CLASS); 157 | }; 158 | 159 | Ouija.prototype.collapseCurrent = function () { 160 | if (!this.activeComponent) { 161 | return; 162 | } 163 | 164 | this.activeComponent.setState({ isActive: false }); 165 | this.activeComponent = null; 166 | }; 167 | 168 | module.exports = Ouija; 169 | -------------------------------------------------------------------------------- /app/post.js: -------------------------------------------------------------------------------- 1 | /* jshint browser:true */ 2 | /* global require, module */ 3 | 4 | /** 5 | * @fileOverview 6 | * 7 | * This file should contain a Post model, except... it's a mess 8 | **/ 9 | 10 | var _ = require('lodash'), 11 | Emitter = require('emitter-component'), 12 | Q = require('q'); 13 | 14 | Q.longStackSupport = true; // TODO: Remove in Beta 15 | 16 | /** 17 | * The Post class abstracts retrieving and adding the comments associated 18 | * with a single Ghost article 19 | * 20 | * @public 21 | * @class 22 | * @constructor 23 | * @param {int} identifier - Post UID 24 | * @param {Deferred} connection - GoInstant connection promise 25 | * @param {Object} users - Users instance 26 | */ 27 | function Post(identifier, connection, users) { 28 | _.extend(this, { 29 | identifier: identifier, 30 | postRoom: connection.get('rooms').get(1).invoke('join').get('room'), 31 | comments: {}, 32 | users: users, 33 | cached: Q.defer() 34 | }); 35 | 36 | this.initialize(); 37 | } 38 | 39 | Emitter(Post.prototype); 40 | 41 | Post.prototype.initialize = function() { 42 | this.fetchComments(); 43 | this.observeChanges(); 44 | }; 45 | 46 | Post.prototype.fetchComments = function () { 47 | this.postRoom 48 | .invoke('key', '/sections') 49 | .invoke('get') 50 | .get('value') 51 | .then(this.getUsers.bind(this)) 52 | .then(this.cacheComments.bind(this)) 53 | .then(this.cached.resolve) 54 | .fail(this.cached.reject); 55 | }; 56 | 57 | // TODO: add set/remove handlers 58 | Post.prototype.observeChanges = function () { 59 | var options = { bubble: true, local: true }; 60 | var futureKey = this.postRoom.invoke('key', '/sections'); 61 | 62 | futureKey.invoke('on', 'add', options, this.addHandler.bind(this)); 63 | }; 64 | 65 | Post.prototype.addHandler = function (comment, context) { 66 | var self = this; 67 | 68 | var sectionName = _.last(context.key.split('/')); 69 | var commentId = _.last(context.addedKey.split('/')); 70 | 71 | this.comments[sectionName] = this.comments[sectionName] || {}; 72 | this.users.getUser(comment.userId).then(function(user) { 73 | comment.displayName = user.displayName; 74 | comment.avatarUrl = user.avatarUrl; 75 | comment.username = user.username; 76 | 77 | self.comments[sectionName][commentId] = comment; 78 | self.emit('newComment', sectionName); 79 | }); 80 | }; 81 | 82 | Post.prototype.getUsers = function (sections) { 83 | var self = this, 84 | deferred = Q.defer(), 85 | promises = []; 86 | 87 | _(sections).each(function( comments) { 88 | _(comments).each(function (comment) { 89 | var promise = self.users.getUser(comment.userId); 90 | promise.then(function (user) { 91 | 92 | comment.displayName = user.displayName; 93 | comment.avatarUrl = user.avatarUrl; 94 | comment.username = user.username; 95 | }); 96 | 97 | promises.push(promise); 98 | }); 99 | 100 | }); 101 | 102 | Q.all(promises).then(function() { 103 | deferred.resolve(sections); 104 | }); 105 | 106 | return deferred.promise; 107 | }; 108 | 109 | Post.prototype.cacheComments = function (sections) { 110 | var self = this; 111 | 112 | _(sections).each(function(comments, sectionName) { 113 | self.comments[sectionName] = _.reduce(comments, function(o, comment, id) { 114 | comment.id = id; 115 | o[id] = comment; 116 | 117 | return o; 118 | }, {}); 119 | }); 120 | 121 | return this.comments; 122 | }; 123 | 124 | Post.prototype.add = function (sectionName, comment) { 125 | var self = this, 126 | deferred = Q.defer(), 127 | keyName = 'sections/' + sectionName; 128 | 129 | this.users.getSelf().then(function (localUser) { 130 | comment.userId = localUser.id; 131 | 132 | self.postRoom 133 | .invoke('key', keyName) 134 | .invoke('add', comment) 135 | .then(deferred.resolve) 136 | .fail(deferred.reject); 137 | }); 138 | 139 | return deferred.promise; 140 | }; 141 | 142 | Post.prototype.getComments = function (sectionName) { 143 | var self = this; 144 | 145 | return this.cached.promise.then(function() { 146 | return _.sortBy(self.comments[sectionName], 'id') || null; 147 | }); 148 | }; 149 | 150 | module.exports = Post; 151 | -------------------------------------------------------------------------------- /app/styles/ouija.css: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | Ouija inline commenting for Ghost 3 | 4 | ___===_*_ 5 | ===+++=-==---- 6 | =-=-======+==--=;-;-# 7 | =;--=-=+=-###xx=+++-----;# 8 | ===----===+X########Xxx==----# 9 | =+==+;;---==#X x#X---=# 10 | +====+=;--====x #-;---# 11 | ==+===-==---==++== -----=# 12 | +=++====---===++==+=++= ;;--====# 13 | ++=++==---===+++=++=xx++==--=-+==---======# 14 | ==+====--==++++=++x=-====-=----=--==++===-x# 15 | +====-===+++++=x++=-==---=-==--=-+=++=====## 16 | =-==--=+=++x=x+=;-=;--;-----;;;-=-====++==## 17 | ##--=+++++x+===;---------=-;----==-+--===## 18 | ####+==xxxx=------;-;---;;--=-=========## 19 | ############x--------;----====--==-=### 20 | ######---;-----=-======--### 21 | ####-;--;---=--=+==### 22 | #####+=--==--x##### 23 | ############# 24 | 25 | ========================================================================== */ 26 | /* ========================================================================== 27 | Reset theme defaults 28 | ========================================================================== */ 29 | .ouija { 30 | text-shadow: none; 31 | line-height: 1; 32 | font-family: "Open Sans", "Helvetica Neue", "Helvetica", Arial, sans-serif; 33 | font-weight: normal; } 34 | .ouija h1, .ouija h2, .ouija h3, .ouija h4, .ouija h5, .ouija h6, 35 | .ouija p, 36 | .ouija textarea { 37 | margin: 0; 38 | padding: 0; 39 | font-family: "Open Sans", "Helvetica Neue", "Helvetica", Arial, sans-serif; 40 | font-weight: normal; 41 | text-shadow: none; } 42 | 43 | /* ========================================================================== 44 | Ouija container 45 | ========================================================================== */ 46 | .ouija * { 47 | -webkit-box-sizing: border-box; 48 | -moz-box-sizing: border-box; 49 | box-sizing: border-box; } 50 | 51 | .ouija { 52 | position: absolute; 53 | top: 0; 54 | right: -50px; 55 | width: 40px; 56 | z-index: 1000; 57 | overflow: hidden; } 58 | .ouija .ouija-controls { 59 | position: relative; 60 | float: left; 61 | width: 40px; 62 | margin-top: 5px; 63 | z-index: 1; } 64 | .ouija .ouija-controls a { 65 | display: block; 66 | height: 21px; 67 | padding: 0 0 1px 15px; 68 | line-height: 1.6; 69 | background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjQsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMzFweCIgaGVpZ2h0PSIyMXB4IiB2aWV3Qm94PSIwIDAgMzEgMjEiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDMxIDIxIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxnPg0KCTxwYXRoIGZpbGw9IiNCQkMzQzgiIGQ9Ik0zMSwxMC41QzMxLDE2LjI5OSwyNi4yOTksMjEsMjAuNSwyMVMxLjYyNywxMS42NjIsMS42MjcsMTEuNjYyYy0wLjg5NS0wLjYzOS0wLjg5NS0xLjY4NSwwLTIuMzI1DQoJCUMxLjYyNyw5LjMzNywxNC43MDEsMCwyMC41LDBTMzEsNC43MDEsMzEsMTAuNXoiLz4NCjwvZz4NCjwvc3ZnPg0K); 70 | background-repeat: no-repeat; 71 | text-decoration: none; 72 | font-weight: 600; 73 | font-size: 13px; 74 | font-family: "Helvetica Neue", Arial, sans-serif; 75 | color: #222; 76 | -webkit-font-smoothing: antialiased; } 77 | .ouija .ouija-controls a:hover { 78 | opacity: 1; 79 | text-decoration: none; 80 | border: 0; 81 | background-color: transparent; } 82 | .ouija .ouija-controls a:focus { 83 | outline: none; } 84 | .ouija .ouija-controls a.add { 85 | font-size: 17px; } 86 | .ouija .ouija-controls a.add:after { 87 | display: block; 88 | content: '+'; 89 | line-height: 1; 90 | -webkit-transform-origin: 8px 10px; 91 | -moz-transform-origin: 8px 10px; 92 | -ms-transform-origin: 8px 10px; 93 | -o-transform-origin: 8px 10px; 94 | transform-origin: 8px 10px; 95 | -webkit-transition: all 0.25s linear; 96 | -moz-transition: all 0.25s linear; 97 | transition: all 0.25s linear; } 98 | .ouija .ouija-controls a.count { 99 | letter-spacing: -1px; } 100 | .ouija .ouija-controls a.count span { 101 | display: block; 102 | position: relative; 103 | width: 21px; 104 | left: -6px; 105 | text-align: center; } 106 | .ouija .ouija-controls a.loader { 107 | height: 22px; } 108 | .ouija .ouija-controls a.loader span { 109 | top: 2px; 110 | left: -1px; } 111 | .ouija button, 112 | .ouija .ouija-button { 113 | position: relative; 114 | border: 0; 115 | padding: 5px 10px; 116 | border-radius: 2px; 117 | font-family: "Open Sans", "Helvetica Neue", "Helvetica", Arial, sans-serif; 118 | text-decoration: none; 119 | background: #57a3e8; 120 | font-size: 11px; 121 | color: white; 122 | cursor: pointer; 123 | -webkit-transition: all 0.3s linear; 124 | -moz-transition: all 0.3s linear; 125 | transition: all 0.3s linear; 126 | -webkit-apppearance: none; } 127 | .ouija button:hover, 128 | .ouija .ouija-button:hover { 129 | background: #4197e5; } 130 | .ouija button:active, 131 | .ouija .ouija-button:active { 132 | top: 1px; 133 | outline: none; } 134 | .ouija button:focus, 135 | .ouija .ouija-button:focus { 136 | outline: none; } 137 | .ouija button.text, 138 | .ouija .ouija-button.text { 139 | border: 0; 140 | background: none; 141 | color: #999; 142 | font-weight: 300; } 143 | .ouija button.text:hover, 144 | .ouija .ouija-button.text:hover { 145 | color: #555; } 146 | .ouija .ouija-comments { 147 | float: left; 148 | width: 260px; 149 | font-size: 13px; 150 | font-family: "Open Sans", "Helvetica Neue", "Helvetica", Arial, sans-serif; 151 | background: transparent; 152 | border-radius: 3px; } 153 | .ouija .ouija-comments > div { 154 | max-height: 350px; 155 | overflow: auto; } 156 | .ouija .ouija-comment { 157 | position: relative; 158 | min-height: 40px; 159 | margin-bottom: 20px; 160 | padding-left: 44px; } 161 | .ouija .ouija-avatar { 162 | display: block; 163 | position: absolute; 164 | top: 2px; 165 | left: 0; } 166 | .ouija .ouija-avatar figure { 167 | display: none; } 168 | .ouija .ouija-avatar img { 169 | width: 34px; 170 | height: 34px; 171 | margin: 0; 172 | padding: 0; 173 | border-radius: 2px; } 174 | .ouija .ouija-author { 175 | font-size: 14px; 176 | font-weight: 600; 177 | margin-bottom: 5px; } 178 | .ouija .ouija-author a { 179 | text-decoration: none; } 180 | .ouija .ouija-content p { 181 | font-size: 13px; 182 | line-height: 1.5; 183 | padding: 0; 184 | margin-bottom: 5px; } 185 | .ouija .ouija-content textarea { 186 | width: 100%; 187 | min-height: 60px; 188 | padding: 5px; 189 | margin: 5px 0; 190 | resize-x: none; 191 | border: 1px solid #ccc; 192 | border-radius: 2px; 193 | font-size: 12px; } 194 | .ouija .ouija-content textarea:focus { 195 | border: 1px solid #aaa; 196 | outline: none; } 197 | .ouija .ouija-content footer { 198 | text-align: right; } 199 | .ouija .ouija-login { 200 | padding: 10px; 201 | text-align: center; 202 | border-radius: 3px; 203 | border: 1px solid #ddd; 204 | background: #f7f7f7; } 205 | .ouija .ouija-login h5 { 206 | margin-bottom: 10px; 207 | font-size: 12px; 208 | font-weight: 100; 209 | color: #999; } 210 | .ouija .ouija-login .ouija-button { 211 | display: inline-block; 212 | text-decoration: none; 213 | font-size: 14px; 214 | padding: 7px 10px; 215 | background: #57a3e8; 216 | color: white; } 217 | .ouija .ouija-login .ouija-button [class^='icon-'], 218 | .ouija .ouija-login .ouija-button [class*=' icon-'] { 219 | position: relative; 220 | top: 2px; 221 | margin-right: 5px; } 222 | .ouija.ouija-active:after { 223 | content: ''; 224 | position: absolute; 225 | height: 100%; 226 | left: 20px; 227 | top: 0; 228 | width: 1px; 229 | margin-top: 27.66667px; 230 | background: rgba(34, 34, 34, 0.1); } 231 | .ouija .ouija-colophon { 232 | display: block; 233 | float: right; 234 | margin-top: 10px; 235 | padding: 5px; 236 | border-radius: 3px; 237 | text-transform: uppercase; 238 | font-size: .7em; 239 | color: #ccc; 240 | letter-spacing: .1em; 241 | text-decoration: none; } 242 | .ouija .ouija-colophon span.ouija-logo { 243 | color: #bbb; 244 | font-size: 1.1em; } 245 | .ouija .ouija-colophon span.ouija-logo:before { 246 | content: ""; 247 | display: inline-block; 248 | width: 12px; 249 | height: 12px; 250 | margin-bottom: -2px; 251 | background-repeat: no-repeat; 252 | background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjExcHgiIGhlaWdodD0iMTJweCIgdmlld0JveD0iMCAwIDExIDEyIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbG5zOnNrZXRjaD0iaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoL25zIj4KICAgIDwhLS0gR2VuZXJhdG9yOiBTa2V0Y2ggMy4wLjEgKDc1OTcpIC0gaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoIC0tPgogICAgPHRpdGxlPmJhc2Vsb2dvPC90aXRsZT4KICAgIDxkZXNjcmlwdGlvbj5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzY3JpcHRpb24+CiAgICA8ZGVmcz48L2RlZnM+CiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4KICAgICAgICA8cGF0aCBkPSJNNC4xMjE4MjM1NSw4LjgzNzU4OTI2IEM0LjgwNTg3ODUsOS41MjE2Njc0OSA0LjE3MDMzNzYxLDExLjQxMDI5MDUgNS42NDA2NTE2NCwxMS45MDM1NzgyIEM5LjMyOTQyOTYsMTMuMDk1MTQ5NCAxMS41OTE2NjAzLDIuODYzMDcyMDggMTAuODY0MzI4NCwyLjEzNTY5MjQ5IEMxMC4xMzY5NzIsMS40MDgyODg0MSAtMC4wOTUxNzIzOTcsMy42NzA1ODg1OCAxLjA5NjQyNTM3LDcuMzU5Mzg0NDggQzEuNTg5Njg5NDgsOC44Mjk2NzE0MSAzLjQzNzcxOTYxLDguMTUzNTM1NTMgNC4xMjE4MjM1NSw4LjgzNzU4OTI2IFogTTguNzA3MDk4MSw0LjI5MjkwMDg1IEM5LjA5NzYzMzk3LDQuNjgzNDIxMDEgOS4wOTc2MzM5Nyw1LjMxNjU4OTczIDguNzA3MDk4MSw1LjcwNzEwOTg5IEM4LjMxNjYwNTE4LDYuMDk3NjE1NzIgNy42ODM0MDU1NSw2LjA5NzY0NDM2IDcuMjkyODY5NjgsNS43MDcxMDk4OSBDNi45MDIzNzY3Nyw1LjMxNjU2MTA5IDYuOTAyMzc2NzcsNC42ODMzOTIzNyA3LjI5Mjg2OTY4LDQuMjkyOTAwODUgQzcuNjgzNDA1NTUsMy45MDIzNjYzOCA4LjMxNjU2MjIzLDMuOTAyMzY2MzggOC43MDcwOTgxLDQuMjkyOTAwODUgTDguNzA3MDk4MSw0LjI5MjkwMDg1IFoiIGlkPSJiYXNlIiBmaWxsPSIjRDNEOERCIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2LjAwMDAwMCwgNy4wMDAwMDApIHJvdGF0ZSgtMTUuMDAwMDAwKSB0cmFuc2xhdGUoLTYuMDAwMDAwLCAtNy4wMDAwMDApICI+PC9wYXRoPgogICAgPC9nPgo8L3N2Zz4=); } 253 | 254 | /* ========================================================================== 255 | Loader 256 | ========================================================================== */ 257 | @-webkit-keyframes ouija-load { 258 | 0% { 259 | -webkit-transform: rotate(0deg); } 260 | 261 | 100% { 262 | -webkit-transform: rotate(360deg); } } 263 | @-moz-keyframes ouija-load { 264 | 0% { 265 | -moz-transform: rotate(0deg); } 266 | 267 | 100% { 268 | -moz-transform: rotate(360deg); } } 269 | @keyframes ouija-load { 270 | 0% { 271 | -webkit-transform: rotate(0deg); 272 | -moz-transform: rotate(0deg); 273 | -ms-transform: rotate(0deg); 274 | -o-transform: rotate(0deg); 275 | transform: rotate(0deg); } 276 | 277 | 100% { 278 | -webkit-transform: rotate(360deg); 279 | -moz-transform: rotate(360deg); 280 | -ms-transform: rotate(360deg); 281 | -o-transform: rotate(360deg); 282 | transform: rotate(360deg); } } 283 | .ouija-loader { 284 | display: inline-block; 285 | position: relative; 286 | width: 14px; 287 | height: 14px; 288 | border: 3px solid #222222; 289 | border-left-color: transparent; 290 | border-radius: 7px; 291 | overflow: hidden; 292 | text-indent: -9999px; 293 | -webkit-animation: ouija-load 0.75s infinite linear; 294 | -moz-animation: ouija-load 0.75s infinite linear; 295 | animation: ouija-load 0.75s infinite linear; } 296 | 297 | /* ========================================================================== 298 | UI States 299 | ========================================================================== */ 300 | .post-ouija { 301 | position: relative; 302 | right: 0; 303 | width: 100%; 304 | -webkit-transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1); 305 | -moz-transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1); 306 | transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1); } 307 | .post-ouija.ouija-active { 308 | right: 150px; } 309 | .post-ouija.ouija-active .ouija .ouija-comments { 310 | margin-left: 0; } 311 | .post-ouija p, .post-ouija ol, .post-ouija ul, .post-ouija blockquote { 312 | position: relative; } 313 | .post-ouija p:hover .ouija .ouija-controls, .post-ouija ol:hover .ouija .ouija-controls, .post-ouija ul:hover .ouija .ouija-controls, .post-ouija blockquote:hover .ouija .ouija-controls { 314 | opacity: 1; 315 | -webkit-transition-delay: 0; 316 | -moz-transition-delay: 0; 317 | transition-delay: 0; } 318 | 319 | .ouija .ouija-controls { 320 | opacity: 0; 321 | -webkit-transition: opacity 0.5s cubic-bezier(0.19, 1, 0.22, 1); 322 | -moz-transition: opacity 0.5s cubic-bezier(0.19, 1, 0.22, 1); 323 | transition: opacity 0.5s cubic-bezier(0.19, 1, 0.22, 1); 324 | -webkit-transition-delay: 0.8s; 325 | -moz-transition-delay: 0.8s; 326 | transition-delay: 0.8s; } 327 | .ouija .ouija-controls a { 328 | opacity: .85; 329 | -webkit-transition: all 0.2s cubic-bezier(0.19, 1, 0.22, 1); 330 | -moz-transition: all 0.2s cubic-bezier(0.19, 1, 0.22, 1); 331 | transition: all 0.2s cubic-bezier(0.19, 1, 0.22, 1); } 332 | .ouija.ouija-has-comments .ouija-controls { 333 | opacity: .75; } 334 | .ouija .ouija-comments { 335 | opacity: 0; 336 | margin-left: -150px; 337 | visibility: hidden; } 338 | .ouija.ouija-active { 339 | width: 300px; 340 | right: -310px; } 341 | .ouija.ouija-active .ouija-controls, 342 | .ouija.ouija-active .ouija-comments { 343 | opacity: 1; 344 | visibility: visible; } 345 | .ouija.ouija-active .ouija-controls a { 346 | opacity: 0.45; } 347 | .ouija.ouija-active .ouija-controls a.add:after { 348 | font-size: 20px; 349 | -webkit-transform: rotate(45deg); 350 | -moz-transform: rotate(45deg); 351 | -ms-transform: rotate(45deg); 352 | -o-transform: rotate(45deg); 353 | transform: rotate(45deg); } 354 | .ouija.ouija-active .ouija-comments { 355 | margin-left: 0; 356 | -webkit-transition: all 0.25s cubic-bezier(0.19, 1, 0.22, 1), margin-left 0.5s cubic-bezier(0.19, 1, 0.22, 1), margin-bottom 0.5s cubic-bezier(0.19, 1, 0.22, 1); 357 | -moz-transition: all 0.25s cubic-bezier(0.19, 1, 0.22, 1), margin-left 0.5s cubic-bezier(0.19, 1, 0.22, 1), margin-bottom 0.5s cubic-bezier(0.19, 1, 0.22, 1); 358 | transition: all 0.25s cubic-bezier(0.19, 1, 0.22, 1), margin-left 0.5s cubic-bezier(0.19, 1, 0.22, 1), margin-bottom 0.5s cubic-bezier(0.19, 1, 0.22, 1); } 359 | 360 | /* ========================================================================== 361 | Responsive Styling 362 | Edit these styles to change how Ouija works on mobile devices 363 | and small screens 364 | ========================================================================== */ 365 | @media all and (max-width: 600px) { 366 | .post-ouija > section, 367 | .post-ouija > .post-content { 368 | padding-right: 25px; } 369 | .post-ouija.ouija-active { 370 | right: 0; } 371 | .post-ouija.ouija-active p, .post-ouija.ouija-active ol, .post-ouija.ouija-active ul, .post-ouija.ouija-active blockquote { 372 | position: static; } 373 | .post-ouija.ouija-active .ouija-controls { 374 | display: none; } 375 | 376 | .ouija { 377 | right: -45px; } 378 | .ouija .ouija-comments { 379 | position: fixed; 380 | bottom: 0; 381 | float: none; 382 | width: 100%; 383 | padding: 15px; 384 | margin-left: 0; 385 | margin-bottom: -25%; } 386 | .ouija .ouija-comments > div { 387 | margin-bottom: 15px; 388 | background: white; 389 | overflow-y: scroll; 390 | -webkit-overflow-scrolling: touch; } 391 | .ouija.ouija-has-comments .ouija-comments > div { 392 | padding: 15px 15px 0; 393 | border: 1px solid #ccc; 394 | border-radius: 3px; } 395 | .ouija.ouija-active { 396 | position: fixed; 397 | top: auto; 398 | left: 0; 399 | right: auto; 400 | bottom: 0; 401 | height: 100%; 402 | width: 100%; 403 | background: rgba(255, 255, 255, 0.85); 404 | background-color: transparent; 405 | background-image: -webkit-linear-gradient(rgba(255, 255, 255, 0.75), white); 406 | background-image: linear-gradient(rgba(255, 255, 255, 0.75), white); } 407 | .ouija.ouija-active:after { 408 | display: none; } 409 | .ouija.ouija-active .ouija-controls { 410 | display: block; 411 | float: right; 412 | margin-top: 15px; } 413 | .ouija.ouija-active .ouija-controls a { 414 | opacity: .8; } 415 | .ouija.ouija-active .ouija-controls a.count, .ouija.ouija-active .ouija-controls a.add { 416 | background: transparent; } 417 | .ouija.ouija-active .ouija-controls a.count span, .ouija.ouija-active .ouija-controls a.add span { 418 | display: none; } 419 | .ouija.ouija-active .ouija-controls a.count:after, .ouija.ouija-active .ouija-controls a.add:after { 420 | display: block; 421 | content: '+'; 422 | line-height: 1; 423 | font-size: 36px; 424 | -webkit-transform-origin: 8px 10px; 425 | -moz-transform-origin: 8px 10px; 426 | -ms-transform-origin: 8px 10px; 427 | -o-transform-origin: 8px 10px; 428 | transform-origin: 8px 10px; 429 | -webkit-transition: all 0 linear; 430 | -moz-transition: all 0 linear; 431 | transition: all 0 linear; 432 | -webkit-transform: rotate(45deg); 433 | -moz-transform: rotate(45deg); 434 | -ms-transform: rotate(45deg); 435 | -o-transform: rotate(45deg); 436 | transform: rotate(45deg); } 437 | .ouija.ouija-active .ouija-comments { 438 | margin-bottom: 0; } } 439 | -------------------------------------------------------------------------------- /app/styles/ouija.scss: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | Ouija inline commenting for Ghost 3 | 4 | ___===_*_ 5 | ===+++=-==---- 6 | =-=-======+==--=;-;-# 7 | =;--=-=+=-###xx=+++-----;# 8 | ===----===+X########Xxx==----# 9 | =+==+;;---==#X x#X---=# 10 | +====+=;--====x #-;---# 11 | ==+===-==---==++== -----=# 12 | +=++====---===++==+=++= ;;--====# 13 | ++=++==---===+++=++=xx++==--=-+==---======# 14 | ==+====--==++++=++x=-====-=----=--==++===-x# 15 | +====-===+++++=x++=-==---=-==--=-+=++=====## 16 | =-==--=+=++x=x+=;-=;--;-----;;;-=-====++==## 17 | ##--=+++++x+===;---------=-;----==-+--===## 18 | ####+==xxxx=------;-;---;;--=-=========## 19 | ############x--------;----====--==-=### 20 | ######---;-----=-======--### 21 | ####-;--;---=--=+==### 22 | #####+=--==--x##### 23 | ############# 24 | 25 | ========================================================================== */ 26 | 27 | @import '../../node_modules/node-bourbon/assets/stylesheets/bourbon'; 28 | 29 | // ========================================================================== */ 30 | // Templating Variables 31 | // Use these variables to tweak Ouija to work with your theme. 32 | // ========================================================================== */ 33 | 34 | // Trigger elements 35 | // Elements in the post content that 36 | $triggers: p, ol, ul, blockquote; 37 | 38 | // Ouija Sizing 39 | $width: 300px; 40 | $margin: 10px; 41 | $controls-width: $margin*4; 42 | $controls-height: 21px; 43 | 44 | // Ouija affects the main block of content. 45 | // Set either 'offset-' variable with a value other than 'false' to choose 46 | // which type of modification you want to make to the article. 47 | // You must set the unused value to false. 48 | 49 | // Push the article 50 | $offset-push: $width/2; 51 | 52 | // Resize the article 53 | // If using resize, set $offset-width-original to your article's original 54 | // width. If you don't set it, the article won't transition with an animation. 55 | $offset-width: false; 56 | $offset-width-original: 100%; 57 | 58 | 59 | // Comments list styling 60 | $background-color: transparent; 61 | $avatar-size: 34px; 62 | $comments-height: 350px; 63 | $link-color: #57A3E8; 64 | $border-color: fade-out(#222, .9); 65 | 66 | // SVGs 67 | $pointer-bg: 'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjQsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMzFweCIgaGVpZ2h0PSIyMXB4IiB2aWV3Qm94PSIwIDAgMzEgMjEiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDMxIDIxIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxnPg0KCTxwYXRoIGZpbGw9IiNCQkMzQzgiIGQ9Ik0zMSwxMC41QzMxLDE2LjI5OSwyNi4yOTksMjEsMjAuNSwyMVMxLjYyNywxMS42NjIsMS42MjcsMTEuNjYyYy0wLjg5NS0wLjYzOS0wLjg5NS0xLjY4NSwwLTIuMzI1DQoJCUMxLjYyNyw5LjMzNywxNC43MDEsMCwyMC41LDBTMzEsNC43MDEsMzEsMTAuNXoiLz4NCjwvZz4NCjwvc3ZnPg0K'; 68 | $logo-colophon: 'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjExcHgiIGhlaWdodD0iMTJweCIgdmlld0JveD0iMCAwIDExIDEyIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbG5zOnNrZXRjaD0iaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoL25zIj4KICAgIDwhLS0gR2VuZXJhdG9yOiBTa2V0Y2ggMy4wLjEgKDc1OTcpIC0gaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoIC0tPgogICAgPHRpdGxlPmJhc2Vsb2dvPC90aXRsZT4KICAgIDxkZXNjcmlwdGlvbj5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzY3JpcHRpb24+CiAgICA8ZGVmcz48L2RlZnM+CiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4KICAgICAgICA8cGF0aCBkPSJNNC4xMjE4MjM1NSw4LjgzNzU4OTI2IEM0LjgwNTg3ODUsOS41MjE2Njc0OSA0LjE3MDMzNzYxLDExLjQxMDI5MDUgNS42NDA2NTE2NCwxMS45MDM1NzgyIEM5LjMyOTQyOTYsMTMuMDk1MTQ5NCAxMS41OTE2NjAzLDIuODYzMDcyMDggMTAuODY0MzI4NCwyLjEzNTY5MjQ5IEMxMC4xMzY5NzIsMS40MDgyODg0MSAtMC4wOTUxNzIzOTcsMy42NzA1ODg1OCAxLjA5NjQyNTM3LDcuMzU5Mzg0NDggQzEuNTg5Njg5NDgsOC44Mjk2NzE0MSAzLjQzNzcxOTYxLDguMTUzNTM1NTMgNC4xMjE4MjM1NSw4LjgzNzU4OTI2IFogTTguNzA3MDk4MSw0LjI5MjkwMDg1IEM5LjA5NzYzMzk3LDQuNjgzNDIxMDEgOS4wOTc2MzM5Nyw1LjMxNjU4OTczIDguNzA3MDk4MSw1LjcwNzEwOTg5IEM4LjMxNjYwNTE4LDYuMDk3NjE1NzIgNy42ODM0MDU1NSw2LjA5NzY0NDM2IDcuMjkyODY5NjgsNS43MDcxMDk4OSBDNi45MDIzNzY3Nyw1LjMxNjU2MTA5IDYuOTAyMzc2NzcsNC42ODMzOTIzNyA3LjI5Mjg2OTY4LDQuMjkyOTAwODUgQzcuNjgzNDA1NTUsMy45MDIzNjYzOCA4LjMxNjU2MjIzLDMuOTAyMzY2MzggOC43MDcwOTgxLDQuMjkyOTAwODUgTDguNzA3MDk4MSw0LjI5MjkwMDg1IFoiIGlkPSJiYXNlIiBmaWxsPSIjRDNEOERCIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2LjAwMDAwMCwgNy4wMDAwMDApIHJvdGF0ZSgtMTUuMDAwMDAwKSB0cmFuc2xhdGUoLTYuMDAwMDAwLCAtNy4wMDAwMDApICI+PC9wYXRoPgogICAgPC9nPgo8L3N2Zz4='; 69 | 70 | // Fonts 71 | $font-sans: 'Open Sans', 'Helvetica Neue', 'Helvetica', Arial, sans-serif; 72 | $font-serif: 'Noto Serif', Georgia, serif; 73 | 74 | //Animation speed function 75 | $time-function: $ease-out-expo; 76 | 77 | /* ========================================================================== 78 | Reset theme defaults 79 | ========================================================================== */ 80 | 81 | .ouija { 82 | text-shadow: none; 83 | line-height: 1; 84 | font-family: $font-sans; 85 | font-weight: normal; 86 | h1, h2, h3, h4, h5, h6, 87 | p, 88 | textarea { 89 | margin: 0; 90 | padding: 0; 91 | font-family: $font-sans; 92 | font-weight: normal; 93 | text-shadow: none; 94 | } 95 | 96 | } 97 | 98 | /* ========================================================================== 99 | Ouija container 100 | ========================================================================== */ 101 | 102 | .ouija * { @include box-sizing(border-box); } 103 | 104 | .ouija { 105 | position: absolute; 106 | top: 0; 107 | right: -($controls-width + $margin); 108 | width: $controls-width; 109 | z-index: 1000; 110 | overflow: hidden; 111 | 112 | //Trigger Button 113 | .ouija-controls { 114 | position: relative; 115 | float: left; 116 | width: $controls-width; 117 | margin-top: $margin/2; 118 | z-index: 1; 119 | 120 | a { 121 | display: block; 122 | height: $controls-height; 123 | padding: 0 0 1px 15px; 124 | line-height: 1.6; 125 | background-image: url(data:image/svg+xml;base64,#{$pointer-bg}); 126 | background-repeat: no-repeat; 127 | text-decoration: none; 128 | font-weight: 600; 129 | font-size: 13px; 130 | font-family: "Helvetica Neue", Arial, sans-serif; 131 | color: #222; 132 | -webkit-font-smoothing: antialiased; 133 | &:hover { 134 | opacity: 1; 135 | text-decoration: none; 136 | border: 0; 137 | background-color: transparent; 138 | } 139 | &:focus { 140 | outline: none; 141 | } 142 | 143 | //Add Button 144 | &.add { 145 | font-size: 17px; 146 | &:after { 147 | display: block; 148 | content: '+'; 149 | line-height: 1; 150 | @include transform-origin(8px 10px); 151 | @include transition(all .25s linear); 152 | } 153 | } 154 | 155 | //Comment Count 156 | &.count { 157 | letter-spacing: -1px; 158 | span { 159 | display: block; 160 | position: relative; 161 | width: 21px; 162 | left: -6px; 163 | text-align: center; 164 | } 165 | } 166 | 167 | //Loader 168 | &.loader { 169 | height: 22px; 170 | span { 171 | top: 2px; 172 | left: -1px; 173 | } 174 | } 175 | 176 | } 177 | 178 | } 179 | 180 | //Comment form controls 181 | button, 182 | .ouija-button { 183 | position: relative; 184 | border: 0; 185 | padding: $margin/2 $margin; 186 | border-radius: 2px; 187 | font-family: $font-sans; 188 | text-decoration: none; 189 | background: $link-color; 190 | font-size: 11px; 191 | color: white; 192 | cursor: pointer; 193 | @include transition(all .3s linear); 194 | -webkit-apppearance: none; 195 | &:hover { 196 | background: darken($link-color, 5%); 197 | } 198 | &:active { 199 | top: 1px; 200 | outline: none; 201 | } 202 | &:focus { 203 | outline: none; 204 | } 205 | &.text { 206 | border: 0; 207 | background: none; 208 | color: #999; 209 | font-weight: 300; 210 | &:hover { 211 | color: #555; 212 | } 213 | } 214 | } 215 | 216 | //Comments Container 217 | .ouija-comments { 218 | float: left; 219 | width: $width - $controls-width; 220 | font-size: 13px; 221 | font-family: $font-sans; 222 | background: $background-color; 223 | border-radius: 3px; 224 | 225 | //Overflow container 226 | > div { 227 | max-height: $comments-height; 228 | overflow: auto; 229 | } 230 | 231 | } 232 | 233 | //Comment styling 234 | .ouija-comment { 235 | position: relative; 236 | min-height: 40px; 237 | margin-bottom: $margin*2; 238 | padding-left: $avatar-size + $margin; 239 | } 240 | 241 | .ouija-avatar { 242 | display: block; 243 | position: absolute; 244 | top: 2px; 245 | left: 0; 246 | 247 | figure { display: none; } 248 | 249 | img { 250 | width: $avatar-size; 251 | height: $avatar-size; 252 | margin: 0; 253 | padding: 0; 254 | border-radius: 2px; 255 | } 256 | 257 | } 258 | 259 | .ouija-author { 260 | font-size: 14px; 261 | font-weight: 600; 262 | margin-bottom: $margin/2; 263 | a { text-decoration: none; } 264 | } 265 | 266 | .ouija-content { 267 | 268 | p { 269 | font-size: 13px; 270 | line-height: 1.5; 271 | padding: 0; 272 | margin-bottom: 5px; 273 | } 274 | 275 | textarea { 276 | width: 100%; 277 | min-height: 60px; 278 | padding: $margin/2; 279 | margin: $margin/2 0; 280 | resize-x: none; 281 | border: 1px solid #ccc; 282 | border-radius: 2px; 283 | font-size: 12px; 284 | &:focus { 285 | border: 1px solid #aaa; 286 | outline: none; 287 | } 288 | } 289 | 290 | footer { 291 | text-align: right; 292 | } 293 | 294 | } 295 | 296 | .ouija-login { 297 | padding: $margin; 298 | text-align: center; 299 | border-radius: 3px; 300 | border: 1px solid #ddd; 301 | background: #f7f7f7; 302 | h5 { 303 | margin-bottom: $margin; 304 | font-size: 12px; 305 | font-weight: 100; 306 | color: #999; 307 | } 308 | .ouija-button { 309 | display: inline-block; 310 | text-decoration: none; 311 | font-size: 14px; 312 | padding: 7px 10px; 313 | background: $link-color; 314 | color: white; 315 | [class^='icon-'], 316 | [class*=' icon-'] { 317 | position: relative; 318 | top: 2px; 319 | margin-right: 5px; 320 | } 321 | } 322 | } 323 | 324 | //Border separator 325 | &.ouija-active:after { 326 | content: ''; 327 | position: absolute; 328 | height: 100%; 329 | left: 20px; 330 | top: 0; 331 | width: 1px; 332 | margin-top: $controls-height + ($margin/1.5); 333 | background: $border-color; 334 | } 335 | 336 | //Colophon 337 | .ouija-colophon { 338 | display: block; 339 | float: right; 340 | margin-top: $margin; 341 | padding: $margin/2; 342 | border-radius: 3px; 343 | text-transform: uppercase; 344 | font-size: .7em; 345 | color: #ccc; 346 | letter-spacing: .1em; 347 | text-decoration: none; 348 | span.ouija-logo { 349 | color: #bbb; 350 | font-size: 1.1em; 351 | &:before { 352 | content: ""; 353 | display: inline-block; 354 | width: 12px; 355 | height: 12px; 356 | margin-bottom: -2px; 357 | background-repeat: no-repeat; 358 | background-image: url(data:image/svg+xml;base64,#{$logo-colophon}); 359 | } 360 | } 361 | } 362 | 363 | } 364 | 365 | /* ========================================================================== 366 | Loader 367 | ========================================================================== */ 368 | 369 | 370 | @include keyframes(ouija-load) { 371 | 0% { @include transform(rotate(0deg)); } 372 | 100% { @include transform(rotate(360deg)); } 373 | } 374 | 375 | $loader-size: 14px; 376 | $loader-color: #222; 377 | 378 | .ouija-loader { 379 | display: inline-block; 380 | position: relative; 381 | width: $loader-size; 382 | height: $loader-size; 383 | border: 3px solid $loader-color; 384 | border-left-color: transparent; 385 | border-radius: $loader-size/2; 386 | overflow: hidden; 387 | text-indent: -9999px; 388 | @include animation(ouija-load .75s infinite linear); 389 | } 390 | 391 | 392 | 393 | /* ========================================================================== 394 | UI States 395 | ========================================================================== */ 396 | 397 | @mixin resize-content($offset-push, $offset-width) { 398 | @if $offset-push != false { 399 | right: $offset-push; 400 | } 401 | @if $offset-width != false { 402 | width: $offset-width; 403 | } 404 | } 405 | 406 | .post-ouija { 407 | position: relative; 408 | right: 0; 409 | @if $offset-width == false { 410 | width: $offset-width-original; 411 | } 412 | @include transition(all .5s $time-function); 413 | 414 | //Active post 415 | &.ouija-active { 416 | @include resize-content($offset-push, $offset-width); 417 | 418 | //Stop animation when moving to another comment thread 419 | .ouija .ouija-comments { 420 | margin-left: 0; 421 | } 422 | 423 | } 424 | 425 | //Trigger Ouija to show on hover of these elements 426 | #{$triggers} { 427 | position: relative; 428 | &:hover { 429 | .ouija .ouija-controls { 430 | opacity: 1; 431 | @include transition-delay(0); 432 | } 433 | } 434 | } 435 | 436 | } 437 | 438 | .ouija { 439 | 440 | //Comment Button 441 | .ouija-controls { 442 | opacity: 0; 443 | @include transition(opacity .5s $time-function); 444 | @include transition-delay(.8s); //Delay fade out of conrols 445 | 446 | a { 447 | opacity: .85; 448 | @include transition(all .2s $time-function); 449 | } 450 | 451 | } 452 | 453 | //Show when comments are found 454 | &.ouija-has-comments { 455 | .ouija-controls { 456 | opacity: .75; 457 | } 458 | } 459 | 460 | //Comment List 461 | .ouija-comments { 462 | opacity: 0; 463 | margin-left: -$width/2; 464 | visibility: hidden; 465 | } 466 | 467 | //Active Ouija 468 | &.ouija-active { 469 | width: $width; 470 | right: - ($width + $margin); 471 | 472 | .ouija-controls, 473 | .ouija-comments { 474 | opacity: 1; 475 | visibility: visible; 476 | } 477 | 478 | .ouija-controls a { 479 | opacity: 0.45; 480 | &.add:after { 481 | font-size: 20px; 482 | @include transform(rotate(45deg)); 483 | } 484 | } 485 | 486 | .ouija-comments { 487 | margin-left: 0; 488 | @include transition(all .25s $time-function, margin-left .5s $time-function, margin-bottom .5s $time-function); 489 | } 490 | 491 | } 492 | 493 | } 494 | 495 | 496 | /* ========================================================================== 497 | Responsive Styling 498 | Edit these styles to change how Ouija works on mobile devices 499 | and small screens 500 | ========================================================================== */ 501 | 502 | $mobile-width: 600px; 503 | 504 | @media all and (max-width: $mobile-width) { 505 | 506 | //Update this if your theme uses a different selector for the post's content. 507 | .post-ouija { 508 | > section, 509 | > .post-content { 510 | padding-right: 25px; 511 | } 512 | &.ouija-active { 513 | right: 0; 514 | #{$triggers} { 515 | position: static; 516 | } 517 | .ouija-controls { 518 | display: none; 519 | } 520 | } 521 | } 522 | 523 | 524 | .ouija { 525 | right: -45px; 526 | .ouija-comments { 527 | position: fixed; 528 | bottom: 0; 529 | float: none; 530 | width: 100%; 531 | padding: 15px; 532 | margin-left: 0; 533 | margin-bottom: -25%; 534 | > div { 535 | margin-bottom: 15px; 536 | background: white; 537 | overflow-y: scroll; 538 | -webkit-overflow-scrolling: touch; 539 | } 540 | } 541 | &.ouija-has-comments { 542 | .ouija-comments { 543 | > div { 544 | padding: 15px 15px 0; 545 | border: 1px solid #ccc; 546 | border-radius: 3px; 547 | } 548 | } 549 | } 550 | &.ouija-active { 551 | position: fixed; 552 | top: auto; 553 | left: 0; 554 | right: auto; 555 | bottom: 0; 556 | height: 100%; 557 | width: 100%; 558 | background: fade-out(white, .15); 559 | @include linear-gradient(fade-out(white, .25), white, $fallback: transparent); 560 | 561 | &:after { 562 | display: none; 563 | } 564 | .ouija-controls { 565 | display: block; 566 | float: right; 567 | margin-top: 15px; 568 | a { 569 | opacity: .8; 570 | &.count, &.add { 571 | background: transparent; 572 | span { display: none; } 573 | &:after { 574 | display: block; 575 | content: '+'; 576 | line-height: 1; 577 | font-size: 36px; 578 | @include transform-origin(8px 10px); 579 | @include transition(all 0 linear); 580 | @include transform(rotate(45deg)); 581 | } 582 | } 583 | } 584 | } 585 | .ouija-comments { 586 | margin-bottom: 0; 587 | } 588 | } 589 | } 590 | 591 | } 592 | 593 | 594 | -------------------------------------------------------------------------------- /app/users.js: -------------------------------------------------------------------------------- 1 | /*jshint browser: true */ 2 | /* global require, module */ 3 | 4 | /** 5 | * @fileOverview 6 | * 7 | * This file should contain a User model, except... it's a mess 8 | **/ 9 | 10 | var _ = require('lodash'), 11 | Q = require('q'), 12 | 13 | USER_PROPERTIES = ['displayName', 'avatarUrl', 'id', 'username']; 14 | 15 | Q.longStackSupport = true; 16 | 17 | function Users(conn) { 18 | this.conn = conn; 19 | this.room = null; 20 | this.cacheKey = null; 21 | 22 | this.cache = {}; 23 | this.users = {}; 24 | this.localId = null; 25 | this.localDeferred = Q.defer(); 26 | 27 | this.initialize(); 28 | } 29 | 30 | Users.prototype.initialize = function () { 31 | var self = this; 32 | 33 | this.conn.then(function (result) { 34 | self.room = result.rooms[0]; 35 | self.cacheKey = self.room.key('cachedUsers'); 36 | 37 | return self.room.self().get(); 38 | 39 | }).then(function (result) { 40 | var user = result.value; 41 | 42 | self.localId = user.id; 43 | self.updateUser(user); 44 | }).fail(this.localDeferred.reject); 45 | }; 46 | 47 | Users.prototype.updateUser = function (user) { 48 | var self = this; 49 | 50 | this.isGuest().then(function (isGuest) { 51 | if (isGuest) { 52 | self.localDeferred.resolve(null); 53 | return; 54 | } 55 | 56 | user = _.pick(user, USER_PROPERTIES); 57 | self.cache[user.id] = user; 58 | 59 | self.localDeferred.resolve(user); 60 | 61 | _.each(user, function (value, property) { 62 | self.cacheKey.key(user.id).key(property).set(value); 63 | }); 64 | }); 65 | }; 66 | 67 | Users.prototype.loginUrl = function () { 68 | var deferred = Q.defer(); 69 | 70 | this.conn.then(function(result) { 71 | var url = result.connection.loginUrl('twitter'); 72 | 73 | deferred.resolve(url); 74 | }); 75 | 76 | return deferred.promise; 77 | }; 78 | 79 | Users.prototype.logoutUrl = function () { 80 | var deferred = Q.defer(); 81 | 82 | this.conn.then(function (result) { 83 | var url = result.connection.logoutUrl(); 84 | 85 | deferred.resolve(url); 86 | }); 87 | 88 | return deferred.promise; 89 | }; 90 | 91 | Users.prototype.isGuest = function () { 92 | var deferred = Q.defer(); 93 | 94 | this.conn.then(function (result) { 95 | var isGuest = result.connection.isGuest(); 96 | 97 | deferred.resolve(isGuest); 98 | }); 99 | 100 | return deferred.promise; 101 | }; 102 | 103 | Users.prototype.getSelf = function () { 104 | return this.localDeferred.promise; 105 | }; 106 | 107 | Users.prototype.getUser = function(id) { 108 | var deferred = Q.defer(); 109 | 110 | var user = this.cache[id]; 111 | 112 | if (user) { 113 | deferred.resolve(user); 114 | } else { 115 | this.cacheKey.key(id).get().then(function (result) { 116 | user = result.value; 117 | 118 | deferred.resolve(user); 119 | }); 120 | } 121 | 122 | return deferred.promise; 123 | }; 124 | 125 | module.exports = Users; 126 | -------------------------------------------------------------------------------- /config/acl.json: -------------------------------------------------------------------------------- 1 | { 2 | "$room": { 3 | "#join": { 4 | "users": [ 5 | "*" 6 | ] 7 | }, 8 | "#visible": { 9 | "users": [ 10 | "*" 11 | ] 12 | }, 13 | "#all": { 14 | "users": [ 15 | "*" 16 | ] 17 | }, 18 | ".users": { 19 | "$userId": { 20 | "#write": { 21 | "users": [ 22 | "$userId" 23 | ], 24 | "groups": [] 25 | }, 26 | "#read": { 27 | "users": [ 28 | "*" 29 | ] 30 | }, 31 | "email": { 32 | "#write": {}, 33 | "#read": { 34 | "users": [ 35 | "$userId" 36 | ], 37 | "groups": [] 38 | } 39 | }, 40 | "id": { 41 | "#all": {}, 42 | "#get": { 43 | "users": [ 44 | "*" 45 | ] 46 | } 47 | }, 48 | "groups": { 49 | "#all": {}, 50 | "#get": { 51 | "users": [ 52 | "*" 53 | ] 54 | } 55 | }, 56 | "provider": { 57 | "#all": {}, 58 | "#get": { 59 | "users": [ 60 | "*" 61 | ] 62 | } 63 | } 64 | } 65 | }, 66 | "sections": { 67 | "#all": {}, 68 | "#read": { 69 | "users": [ 70 | "*" 71 | ] 72 | }, 73 | "#add": { 74 | "users": [ 75 | "*" 76 | ] 77 | } 78 | } 79 | }, 80 | "lobby": { 81 | "#join": { 82 | "users": [ 83 | "*" 84 | ] 85 | }, 86 | "#visible": { 87 | "users": [ 88 | "*" 89 | ] 90 | }, 91 | "#all": { 92 | "users": [ 93 | "*" 94 | ] 95 | }, 96 | ".users": { 97 | "$userId": { 98 | "#write": { 99 | "users": [ 100 | "$userId" 101 | ], 102 | "groups": [] 103 | }, 104 | "#read": { 105 | "users": [ 106 | "*" 107 | ] 108 | }, 109 | "email": { 110 | "#write": {}, 111 | "#read": { 112 | "users": [ 113 | "$userId" 114 | ], 115 | "groups": [] 116 | } 117 | }, 118 | "id": { 119 | "#all": {}, 120 | "#get": { 121 | "users": [ 122 | "*" 123 | ] 124 | } 125 | }, 126 | "groups": { 127 | "#all": {}, 128 | "#get": { 129 | "users": [ 130 | "*" 131 | ] 132 | } 133 | }, 134 | "provider": { 135 | "#all": {}, 136 | "#get": { 137 | "users": [ 138 | "*" 139 | ] 140 | } 141 | } 142 | } 143 | }, 144 | "cachedUsers": { 145 | "#all": {}, 146 | "#read": { 147 | "users": [ 148 | "*" 149 | ] 150 | }, 151 | "$userId": { 152 | "#write": { 153 | "users": [ 154 | "$userId" 155 | ] 156 | } 157 | } 158 | } 159 | }, 160 | "#connect": { 161 | "users": [ 162 | "*" 163 | ] 164 | } 165 | } -------------------------------------------------------------------------------- /config/ouija.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "theme" : "casper" 3 | } 4 | -------------------------------------------------------------------------------- /deploy/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "dist", 3 | "user": "goinstant", 4 | "repo": "ouija", 5 | "namespace": "external", 6 | "bucket": "cdn.goinstant.net" 7 | } 8 | -------------------------------------------------------------------------------- /deploy/deploy.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | 3 | 'use strict'; 4 | 5 | var config = require('./config.json'); 6 | var S3Deploy = require('./lib/s3_deploy'); 7 | 8 | function deploy() { 9 | var s3Deploy = new S3Deploy(config); 10 | 11 | s3Deploy.deploy(function(err, success) { 12 | if (err) { 13 | return console.log('deploy failed!'); 14 | } 15 | 16 | if (success) { 17 | console.log('deploy successful!'); 18 | 19 | } else { 20 | console.log('no deploy'); 21 | } 22 | }); 23 | } 24 | 25 | deploy(); 26 | 27 | 28 | -------------------------------------------------------------------------------- /deploy/lib/hipchat_notify.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true */ 2 | 3 | 'use strict'; 4 | 5 | var hipchat = require('node-hipchat'); 6 | var async = require('async'); 7 | var _ = require('lodash'); 8 | 9 | var HIPCHAT_TOKEN = process.env.HIPCHAT_TOKEN; 10 | var TAG = process.env.TRAVIS_BRANCH; 11 | var ROOMS = process.env.ROOMS.split(/,\s?/); 12 | var TEMPLATE = 'Update of <%- repo %>:<%- tag %><%- latest %>' + 14 | ' to Production was successful!'; 16 | 17 | module.exports = HipchatNotify; 18 | 19 | function HipchatNotify(opts) { 20 | this._HC = new hipchat(HIPCHAT_TOKEN); 21 | 22 | this._user = opts.user; 23 | this._repo = opts.repo; 24 | this._cdn = opts.cdn; 25 | this._tagData = opts.tagData; 26 | this._template = opts.template || TEMPLATE; 27 | } 28 | 29 | HipchatNotify.prototype.sendDeployMessage = function(cb) { 30 | var self = this; 31 | 32 | cb = _.isFunction(cb) ? cb : function() {}; 33 | 34 | var vars = { 35 | user: self._user, 36 | repo: self._repo, 37 | cdn: self._cdn, 38 | tag: TAG, 39 | latest: self._tagData.isLatest ? '/latest' : null 40 | }; 41 | 42 | var message = _.template(self._template, vars); 43 | 44 | var params = { 45 | from: 'Travis CI', 46 | message: message, 47 | color: 'purple' 48 | }; 49 | 50 | async.each(ROOMS, function(room, done) { 51 | params.room = room; 52 | 53 | self._HC.postMessage(params, function(data) { 54 | var err = null; 55 | 56 | if (!data || data.status !== 'sent') { 57 | err = new Error('Hipchat message failed'); 58 | } 59 | 60 | done(err); 61 | }); 62 | }, cb); 63 | }; 64 | -------------------------------------------------------------------------------- /deploy/lib/s3_deploy.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | 'use strict'; 4 | 5 | var AWS = require('aws-sdk'); 6 | var mime = require('mime'); 7 | var fs = require('fs'); 8 | var async = require('async'); 9 | var _ = require('lodash'); 10 | 11 | var TagChecker = require('./tag_checker'); 12 | var HipchatNotify = require('./hipchat_notify'); 13 | 14 | var S3_ID = process.env.AWS_S3_ID; 15 | var S3_SECRET = process.env.AWS_S3_SECRET; 16 | 17 | var TRAVIS_REPO_SLUG = process.env.TRAVIS_REPO_SLUG; 18 | var TAG = process.env.TRAVIS_BRANCH; 19 | var LATEST = 'latest'; 20 | 21 | module.exports = S3Deploy; 22 | 23 | function S3Deploy(config) { 24 | this._source = config.source; 25 | this._user = config.user; 26 | this._repo = config.repo; 27 | this._namespace = config.namespace; 28 | this._template = config._template || null; 29 | this._tagData = null; 30 | this._bucket = config.bucket; 31 | 32 | this._s3Config = { 33 | accessKeyId: S3_ID, 34 | secretAccessKey: S3_SECRET 35 | }; 36 | 37 | var path = this._namespace + '/' + this._repo + '/'; 38 | 39 | this._keys = { 40 | tag: path + TAG + '/', 41 | latest: path + LATEST + '/' 42 | }; 43 | 44 | this._tagChecker = new TagChecker(this._user, this._repo); 45 | this._hipchatNotify = null; 46 | 47 | AWS.config.update(this._s3Config); 48 | this._s3 = new AWS.S3(); 49 | 50 | _.bindAll(this, [ 51 | '_validate', 52 | '_getFileNames', 53 | '_uploadFiles', 54 | '_uploadFile' 55 | ]); 56 | } 57 | 58 | S3Deploy.prototype.deploy = function(cb) { 59 | var self = this; 60 | 61 | this._validate(function(err, result) { 62 | if (err) { 63 | return cb(err, false); 64 | } 65 | 66 | if (!result) { 67 | return cb(null, false); 68 | } 69 | 70 | var deployType = self._tagData.isLatest ? TAG + '/LATEST': TAG; 71 | console.log('starting deploy of: ' + deployType + '...'); 72 | 73 | var tasks = [ 74 | _.bind(self._getFileNames, self), 75 | _.bind(self._uploadFiles, self) 76 | ]; 77 | 78 | async.waterfall(tasks, function(err) { 79 | if (err) { 80 | return cb(err); 81 | } 82 | 83 | var opts = { 84 | user: self._user, 85 | repo: self._repo, 86 | cdn: 'https://' + self._bucket + '/' + self._namespace, 87 | tagData: self._tagData, 88 | template: self._template 89 | }; 90 | 91 | self._hipchatNotify = new HipchatNotify(opts); 92 | 93 | self._hipchatNotify.sendDeployMessage(function(err) { 94 | cb(err, true); 95 | }); 96 | }); 97 | }); 98 | }; 99 | 100 | S3Deploy.prototype._validate = function(cb) { 101 | var self = this; 102 | 103 | this._tagChecker.check(function(err, tagData) { 104 | if (err) { 105 | return cb(err, false); 106 | } 107 | 108 | self._tagData = tagData; 109 | 110 | if (self._tagData.isTag === false) { 111 | return cb(null, false); 112 | } 113 | 114 | if (TRAVIS_REPO_SLUG !== self._user + '/' + self._repo) { 115 | return cb(null, false); 116 | } 117 | 118 | return cb(null, true); 119 | }); 120 | }; 121 | 122 | S3Deploy.prototype._getFileNames = function (cb) { 123 | fs.readdir(this._source, function(err, fileNames) { 124 | if (err) { 125 | return cb(err); 126 | } 127 | 128 | cb(null, fileNames); 129 | }); 130 | }; 131 | 132 | S3Deploy.prototype._uploadFiles = function(fileNames, cb) { 133 | var self = this; 134 | 135 | async.each(fileNames, function(fileName, done) { 136 | var filePath = self._source + '/' + fileName; 137 | var type = mime.lookup(filePath); 138 | 139 | fs.readFile(filePath, function(err, data) { 140 | if (err) { 141 | return cb(err); 142 | } 143 | 144 | var tasks = [ 145 | _.bind(self._uploadFile, self, type, data, self._keys.tag + fileName) 146 | ]; 147 | 148 | if (self._tagData.isLatest) { 149 | tasks.push(_.bind(self._uploadFile, self, type, data, 150 | self._keys.latest + fileName)); 151 | } 152 | 153 | async.parallel(tasks, function(err) { 154 | done(err); 155 | }); 156 | }); 157 | }, cb); 158 | }; 159 | 160 | S3Deploy.prototype._uploadFile = function(type, data, dest, cb) { 161 | var params = { 162 | Bucket: this._bucket, 163 | Key: dest, 164 | Body: data, 165 | ContentType: type, 166 | ACL: 'public-read' 167 | }; 168 | 169 | this._s3.putObject(params, cb); 170 | }; 171 | -------------------------------------------------------------------------------- /deploy/lib/tag_checker.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | 'use strict'; 4 | 5 | var GitHubApi = require("github"); 6 | var _ = require('lodash'); 7 | 8 | var TRAVIS_COMMIT = process.env.TRAVIS_COMMIT; 9 | var TRAVIS_BRANCH = process.env.TRAVIS_BRANCH; 10 | 11 | module.exports = TagChecker; 12 | 13 | function TagChecker(user, repo) { 14 | var opts = { 15 | version: '3.0.0', 16 | timeout: 5000 17 | }; 18 | 19 | this._github = new GitHubApi(opts); 20 | 21 | this._user = user; 22 | this._repo = repo; 23 | } 24 | 25 | TagChecker.prototype.check = function(cb) { 26 | var opts = { 27 | user: this._user, 28 | repo: this._repo 29 | }; 30 | 31 | this._github.repos.getTags(opts, function(err, tags) { 32 | if (err) { 33 | return cb(err); 34 | } 35 | 36 | var tag = tags[0]; 37 | 38 | var result = { 39 | isTag: isTag(tags), 40 | isLatest: TRAVIS_BRANCH === tag.name 41 | }; 42 | 43 | cb(null, result); 44 | }); 45 | }; 46 | 47 | function isTag(tags) { 48 | var result = false; 49 | 50 | _.each(tags, function(tag) { 51 | var sha = tag.commit.sha; 52 | 53 | if (sha === TRAVIS_COMMIT && tag.name === TRAVIS_BRANCH) { 54 | result = true; 55 | 56 | return false; 57 | } 58 | }); 59 | 60 | return result; 61 | } 62 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | 3 | var gulp = require('gulp'), 4 | gutil = require('gulp-util'), 5 | gulpLoadPlugins = require('gulp-load-plugins'), 6 | minifyCss = require('gulp-minify-css'), 7 | plugins = gulpLoadPlugins(), 8 | config = {}, 9 | pathTo; 10 | 11 | try { 12 | config = require('./config/ouija.json'); 13 | } catch(e) { 14 | console.log('ouija.json not found in config/'); 15 | } 16 | 17 | pathTo = { 18 | assets: '../../themes/' + config.theme + '/assets/', 19 | styles: 'app/styles/**.scss', 20 | entry: 'app/index.js', 21 | dist: 'dist/', 22 | get distScripts () { return this.assets + 'js/' }, 23 | get distStyles () { return this.assets + 'css/' }, 24 | sourceFiles: './app/**/*.js', 25 | watch: ['**.js', 'styles/**.scss'].map(function (path) { 26 | return 'app/' + path; 27 | }) 28 | }; 29 | 30 | gulp.task('build', function () { 31 | gulp.src(pathTo.styles) 32 | .pipe(plugins.sass({ 33 | includePaths: require('node-bourbon').includePaths 34 | })) 35 | .pipe(plugins.rename('ouija.css')) 36 | .pipe(gulp.dest(pathTo.dist)) 37 | .pipe(minifyCss({ keepBreaks:true })) 38 | .pipe(plugins.rename('ouija.min.css')) 39 | .pipe(gulp.dest(pathTo.dist)); 40 | 41 | gulp.src(pathTo.entry) 42 | .pipe(plugins.browserify({ 43 | insertGlobals : false, 44 | debug: false 45 | })) 46 | .pipe(plugins.rename('ouija.js')) 47 | .pipe(gulp.dest(pathTo.dist)) 48 | .pipe(plugins.uglify({ mangle: false })) 49 | .pipe(plugins.rename('ouija.min.js')) 50 | .pipe(gulp.dest(pathTo.dist)); 51 | }); 52 | 53 | gulp.task('develop', ['sass'], function () { 54 | gulp.src(pathTo.entry) 55 | .pipe(plugins.browserify({ 56 | insertGlobals : false, 57 | debug: !process.env.NODE_ENV 58 | })) 59 | .pipe(plugins.rename('ouija.js')) 60 | .pipe(gulp.dest(pathTo.distScripts)); 61 | }); 62 | 63 | gulp.task('sass', function () { 64 | gulp.src(pathTo.styles) 65 | .pipe(plugins.sass({ 66 | errLogToConsole: true, 67 | sourceComments: 'none', 68 | includePaths: require('node-bourbon').includePaths 69 | })) 70 | .pipe(plugins.rename('ouija.css')) 71 | .pipe(gulp.dest(pathTo.distStyles)); 72 | }); 73 | 74 | gulp.task('lint', function () { 75 | return gulp.src(pathTo.sourceFiles) 76 | .pipe(plugins.jshint()) 77 | .pipe(plugins.jshint.reporter('jshint-stylish')) 78 | .pipe(plugins.jshint.reporter('fail')); 79 | }); 80 | 81 | gulp.task('test', ['lint']); 82 | 83 | // Run the develop task when a file change changes 84 | gulp.task('default', ['develop'], function () { 85 | gulp.watch(pathTo.watch).on('change', function () { 86 | gulp.start('develop'); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ouija", 3 | "version": "0.1.4", 4 | "description": "Spirited Conversation & Annotation System", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "./node_modules/.bin/gulp", 8 | "test": "./node_modules/.bin/gulp test" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/ouija-io/ouija.git" 13 | }, 14 | "keywords": [ 15 | "Comments", 16 | "Ghost", 17 | "GoInstant" 18 | ], 19 | "contributors": [ 20 | "Matthew Creager ", 21 | "Colin MacDonald ", 22 | "Nick Tassone ", 23 | "Fabian Becker " 24 | ], 25 | "license": "BSD-3-Clause", 26 | "bugs": { 27 | "url": "https://github.com/ouija-ui/ouija/issues" 28 | }, 29 | "devDependencies": { 30 | "async": "0.7.0", 31 | "aws-sdk": "2.0.*", 32 | "emitter-component": "1.1.1", 33 | "github": "0.1.16", 34 | "gulp": "~3.6.1", 35 | "gulp-browserify": "~0.5.0", 36 | "gulp-clean": "~0.2.4", 37 | "gulp-jshint": "~1.5.5", 38 | "gulp-livereload": "~1.3.1", 39 | "gulp-load-plugins": "~0.5.0", 40 | "gulp-minify-css": "~0.3.4", 41 | "gulp-rename": "~1.2.0", 42 | "gulp-sass": "~0.7.1", 43 | "gulp-traceur": "~0.4.0", 44 | "gulp-uglify": "~0.2.1", 45 | "gulp-util": "~2.2.14", 46 | "jshint-stylish": "~0.2.0", 47 | "lodash": "~2.4.1", 48 | "mime": "1.2.11", 49 | "node-bourbon": "~1.2.2", 50 | "node-hipchat": "0.4.5", 51 | "react": "^0.10.0", 52 | "q": "~1.0.1" 53 | } 54 | } 55 | --------------------------------------------------------------------------------