├── app ├── robots.txt ├── lib │ ├── angular │ │ ├── version.txt │ │ ├── angular-cookies.min.js │ │ ├── angular-loader.min.js │ │ ├── angular-resource.min.js │ │ └── angular-sanitize.min.js │ ├── jquery.cookie.js │ ├── respond.min.js │ ├── modernizr.custom.07116.js │ └── underscore.min.js ├── favicon.ico ├── img │ ├── led.gif │ ├── noise.png │ ├── title.png │ ├── bg-mount.png │ ├── sun-mask.png │ ├── title@2x.png │ ├── glyphicons.png │ ├── apple-touch-icon.png │ ├── apple-touch-icon-72x72.png │ ├── apple-touch-icon-114x114.png │ ├── apple-touch-icon-144x144.png │ ├── apple-touch-startup-image.png │ ├── apple-touch-startup-image-748x1024.png │ └── apple-touch-startup-image-768x1004.png ├── css │ ├── sg.css │ ├── prism.css │ └── app.css ├── js │ ├── filters.js │ ├── app.js │ ├── testing.js │ ├── directives.js │ ├── dom.js │ ├── sg.js │ ├── services.js │ ├── prism.js │ └── controllers.js ├── partials │ ├── lobby.html │ └── room.html ├── index.ejs └── styleguide.ejs ├── test ├── lib │ ├── angular │ │ ├── version.txt │ │ └── jstd-scenario-adapter.js │ ├── jasmine │ │ ├── version.txt │ │ ├── jasmine_favicon.png │ │ ├── MIT.LICENSE │ │ ├── jasmine.css │ │ ├── index.js │ │ └── jasmine-html.js │ ├── jstestdriver │ │ ├── version.txt │ │ └── JsTestDriver.jar │ └── jasmine-jstd-adapter │ │ ├── version.txt │ │ └── JasmineAdapter.js ├── e2e │ ├── runner.html │ └── scenarios.js └── unit │ ├── servicesSpec.js │ ├── filtersSpec.js │ ├── controllersSpec.js │ └── directivesSpec.js ├── Procfile ├── resources ├── bg-mount.psd └── glyphicons.psd ├── Dockerfile ├── config ├── jstd-scenario-adapter-config.js ├── jsTestDriver-scenario.conf └── jsTestDriver.conf ├── docker-compose.yml ├── .gitignore ├── scripts ├── test.sh ├── e2e-test.sh ├── e2e-test.bat ├── test.bat ├── test-server.sh ├── e2e-test-server.sh ├── watchr.rb ├── e2e-test-server.bat ├── test-server.bat └── web-server.js ├── config.js ├── package.json ├── README.md ├── LICENCE ├── lib ├── lobby.js └── room.js └── server.js /app/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * -------------------------------------------------------------------------------- /app/lib/angular/version.txt: -------------------------------------------------------------------------------- 1 | 1.0.1 2 | -------------------------------------------------------------------------------- /test/lib/angular/version.txt: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | -------------------------------------------------------------------------------- /test/lib/jasmine/version.txt: -------------------------------------------------------------------------------- 1 | 1.1.0 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: app_port=$PORT node server.js 2 | -------------------------------------------------------------------------------- /test/lib/jstestdriver/version.txt: -------------------------------------------------------------------------------- 1 | 1.3.3d 2 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richarcher/Hatjitsu/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /app/img/led.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richarcher/Hatjitsu/HEAD/app/img/led.gif -------------------------------------------------------------------------------- /app/img/noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richarcher/Hatjitsu/HEAD/app/img/noise.png -------------------------------------------------------------------------------- /app/img/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richarcher/Hatjitsu/HEAD/app/img/title.png -------------------------------------------------------------------------------- /app/img/bg-mount.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richarcher/Hatjitsu/HEAD/app/img/bg-mount.png -------------------------------------------------------------------------------- /app/img/sun-mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richarcher/Hatjitsu/HEAD/app/img/sun-mask.png -------------------------------------------------------------------------------- /app/img/title@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richarcher/Hatjitsu/HEAD/app/img/title@2x.png -------------------------------------------------------------------------------- /app/img/glyphicons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richarcher/Hatjitsu/HEAD/app/img/glyphicons.png -------------------------------------------------------------------------------- /resources/bg-mount.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richarcher/Hatjitsu/HEAD/resources/bg-mount.psd -------------------------------------------------------------------------------- /resources/glyphicons.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richarcher/Hatjitsu/HEAD/resources/glyphicons.psd -------------------------------------------------------------------------------- /test/lib/jasmine-jstd-adapter/version.txt: -------------------------------------------------------------------------------- 1 | f6b1cf6cac90932c72c4349df8847e0ffd9acbc3 @ 2012-04-29 2 | -------------------------------------------------------------------------------- /app/img/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richarcher/Hatjitsu/HEAD/app/img/apple-touch-icon.png -------------------------------------------------------------------------------- /app/img/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richarcher/Hatjitsu/HEAD/app/img/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /app/img/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richarcher/Hatjitsu/HEAD/app/img/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /app/img/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richarcher/Hatjitsu/HEAD/app/img/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /test/lib/jasmine/jasmine_favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richarcher/Hatjitsu/HEAD/test/lib/jasmine/jasmine_favicon.png -------------------------------------------------------------------------------- /app/img/apple-touch-startup-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richarcher/Hatjitsu/HEAD/app/img/apple-touch-startup-image.png -------------------------------------------------------------------------------- /test/lib/jstestdriver/JsTestDriver.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richarcher/Hatjitsu/HEAD/test/lib/jstestdriver/JsTestDriver.jar -------------------------------------------------------------------------------- /app/img/apple-touch-startup-image-748x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richarcher/Hatjitsu/HEAD/app/img/apple-touch-startup-image-748x1024.png -------------------------------------------------------------------------------- /app/img/apple-touch-startup-image-768x1004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richarcher/Hatjitsu/HEAD/app/img/apple-touch-startup-image-768x1004.png -------------------------------------------------------------------------------- /app/css/sg.css: -------------------------------------------------------------------------------- 1 | body { 2 | font: 16px/26px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; 3 | color: #111; 4 | } 5 | h1 { 6 | line-height: 36px; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | 3 | ENV instDir /Hatjitsu 4 | WORKDIR ${instDir} 5 | COPY . . 6 | RUN npm install -d 7 | 8 | EXPOSE 5000 9 | 10 | CMD node server 11 | -------------------------------------------------------------------------------- /config/jstd-scenario-adapter-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration for jstd scenario adapter 3 | */ 4 | var jstdScenarioAdapter = { 5 | relativeUrlPrefix: '/test/e2e/' 6 | }; 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | poker: 4 | build: . 5 | ports: 6 | - "5000:5000" 7 | volumes: 8 | - /Hatjitsu/node_modules 9 | - .:/Hatjitsu 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | node_modules 15 | npm-debug.log 16 | app/generated 17 | .DS_Store 18 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_DIR=`dirname $0` 4 | 5 | java -jar "$BASE_DIR/../test/lib/jstestdriver/JsTestDriver.jar" \ 6 | --config "$BASE_DIR/../config/jsTestDriver.conf" \ 7 | --basePath "$BASE_DIR/.." \ 8 | --tests all 9 | -------------------------------------------------------------------------------- /scripts/e2e-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_DIR=`dirname $0` 4 | 5 | java -jar "$BASE_DIR/../test/lib/jstestdriver/JsTestDriver.jar" \ 6 | --config "$BASE_DIR/../config/jsTestDriver-scenario.conf" \ 7 | --basePath "$BASE_DIR/.." \ 8 | --tests all --reset 9 | -------------------------------------------------------------------------------- /test/e2e/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | End2end Test Runner 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // configuration here without worrying about the quotes 3 | development: { 4 | hostname: "localhost", 5 | port: 5000, 6 | packAssets: false 7 | }, 8 | production: { 9 | hostname: "hat.jit.su", 10 | port: 80, 11 | packAssets: true 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /config/jsTestDriver-scenario.conf: -------------------------------------------------------------------------------- 1 | server: http://localhost:9877 2 | 3 | load: 4 | - test/lib/angular/angular-scenario.js 5 | - config/jstd-scenario-adapter-config.js 6 | - test/lib/angular/jstd-scenario-adapter.js 7 | - test/e2e/scenarios.js 8 | 9 | proxy: 10 | - {matcher: "*", server: "http://localhost:8000"} 11 | -------------------------------------------------------------------------------- /app/js/filters.js: -------------------------------------------------------------------------------- 1 | /*jslint indent: 2, browser: true */ 2 | /*global angular, $ */ 3 | 4 | 'use strict'; 5 | 6 | /* Filters */ 7 | 8 | angular.module('pokerApp.filters', []). 9 | filter('interpolate', ['version', function (version) { 10 | return function (text) { 11 | return String(text).replace(/\%VERSION\%/mg, version); 12 | }; 13 | }]); 14 | -------------------------------------------------------------------------------- /test/unit/servicesSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jasmine specs for services go here */ 4 | 5 | describe('service', function() { 6 | beforeEach(module('myApp.services')); 7 | 8 | 9 | describe('version', function() { 10 | it('should return current version', inject(function(version) { 11 | expect(version).toEqual('0.1'); 12 | })); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /config/jsTestDriver.conf: -------------------------------------------------------------------------------- 1 | server: http://localhost:9876 2 | 3 | load: 4 | - test/lib/jasmine/jasmine.js 5 | - test/lib/jasmine-jstd-adapter/JasmineAdapter.js 6 | - app/lib/angular/angular.js 7 | - app/lib/angular/angular-cookies.js 8 | - app/lib/angular/angular-resource.js 9 | - app/lib/angular/angular-sanitize.js 10 | - test/lib/angular/angular-mocks.js 11 | - app/js/*.js 12 | - test/unit/*.js 13 | 14 | exclude: 15 | -------------------------------------------------------------------------------- /scripts/e2e-test.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | REM Windows script for running e2e tests 4 | REM You have to run server and capture some browser first 5 | REM 6 | REM Requirements: 7 | REM - Java (http://www.java.com) 8 | 9 | set BASE_DIR=%~dp0 10 | java -jar "%BASE_DIR%\..\test\lib\jstestdriver\JsTestDriver.jar" ^ 11 | --config "%BASE_DIR%\..\config\jsTestDriver-scenario.conf" ^ 12 | --basePath "%BASE_DIR%\.." ^ 13 | --tests all --reset 14 | -------------------------------------------------------------------------------- /scripts/test.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | REM Windows script for running unit tests 4 | REM You have to run server and capture some browser first 5 | REM 6 | REM Requirements: 7 | REM - Java (http://www.java.com) 8 | 9 | set BASE_DIR=%~dp0 10 | 11 | java -jar "%BASE_DIR%\..\test\lib\jstestdriver\JsTestDriver.jar" ^ 12 | --config "%BASE_DIR%\..\config\jsTestDriver.conf" ^ 13 | --basePath "%BASE_DIR%\.." ^ 14 | --tests all 15 | -------------------------------------------------------------------------------- /scripts/test-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_DIR=`dirname $0` 4 | PORT=9876 5 | 6 | echo "Starting JsTestDriver Server (http://code.google.com/p/js-test-driver/)" 7 | echo "Please open the following url and capture one or more browsers:" 8 | echo "http://localhost:$PORT" 9 | 10 | java -jar "$BASE_DIR/../test/lib/jstestdriver/JsTestDriver.jar" \ 11 | --port $PORT \ 12 | --browserTimeout 20000 \ 13 | --config "$BASE_DIR/../config/jsTestDriver.conf" \ 14 | --basePath "$BASE_DIR/.." 15 | -------------------------------------------------------------------------------- /scripts/e2e-test-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_DIR=`dirname $0` 4 | PORT=9877 5 | 6 | echo "Starting JsTestDriver Server (http://code.google.com/p/js-test-driver/)" 7 | echo "Please open the following url and capture one or more browsers:" 8 | echo "http://localhost:$PORT" 9 | 10 | java -jar "$BASE_DIR/../test/lib/jstestdriver/JsTestDriver.jar" \ 11 | --port $PORT \ 12 | --browserTimeout 20000 \ 13 | --config "$BASE_DIR/../config/jsTestDriver-scenario.conf" \ 14 | --basePath "$BASE_DIR/.." 15 | -------------------------------------------------------------------------------- /test/unit/filtersSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jasmine specs for filters go here */ 4 | 5 | describe('filter', function() { 6 | beforeEach(module('myApp.filters')); 7 | 8 | 9 | describe('interpolate', function() { 10 | beforeEach(module(function($provide) { 11 | $provide.value('version', 'TEST_VER'); 12 | })); 13 | 14 | 15 | it('should replace VERSION', inject(function(interpolateFilter) { 16 | expect(interpolateFilter('before %VERSION% after')).toEqual('before TEST_VER after'); 17 | })); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/unit/controllersSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jasmine specs for controllers go here */ 4 | 5 | describe('MyCtrl1', function(){ 6 | var myCtrl1; 7 | 8 | beforeEach(function(){ 9 | myCtrl1 = new MyCtrl1(); 10 | }); 11 | 12 | 13 | it('should ....', function() { 14 | //spec body 15 | }); 16 | }); 17 | 18 | 19 | describe('MyCtrl2', function(){ 20 | var myCtrl2; 21 | 22 | 23 | beforeEach(function(){ 24 | myCtrl2 = new MyCtrl2(); 25 | }); 26 | 27 | 28 | it('should ....', function() { 29 | //spec body 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hatchetapp.net", 3 | "version": "0.0.1-82", 4 | "subdomain": "hat", 5 | "domains": [ 6 | "hatchetapp.net", 7 | "www.hatchetapp.net" 8 | ], 9 | "scripts": { 10 | "start": "server.js" 11 | }, 12 | "engines": { 13 | "node": "0.8.x" 14 | }, 15 | "dependencies": { 16 | "express": "4.17.1", 17 | "express-cdn": "0.0.3", 18 | "ejs": "3.1.5", 19 | "gzippo": "0.1.7", 20 | "socket.io": "~0.9.13", 21 | "underscore": "1.3.3" 22 | }, 23 | "devDependencies": { 24 | "socket.io-client": "*" 25 | } 26 | } -------------------------------------------------------------------------------- /scripts/watchr.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env watchr 2 | 3 | # config file for watchr http://github.com/mynyml/watchr 4 | # install: gem install watchr 5 | # run: watch watchr.rb 6 | # note: make sure that you have jstd server running (server.sh) and a browser captured 7 | 8 | log_file = File.expand_path(File.dirname(__FILE__) + '/../logs/jstd.log') 9 | 10 | `cd ..` 11 | `touch #{log_file}` 12 | 13 | puts "String watchr... log file: #{log_file}" 14 | 15 | watch( '(app/js|test/unit)' ) do 16 | `echo "\n\ntest run started @ \`date\`" > #{log_file}` 17 | `scripts/test.sh &> #{log_file}` 18 | end 19 | 20 | -------------------------------------------------------------------------------- /test/unit/directivesSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jasmine specs for directives go here */ 4 | 5 | describe('directives', function() { 6 | beforeEach(module('myApp.directives')); 7 | 8 | describe('app-version', function() { 9 | it('should print current version', function() { 10 | module(function($provide) { 11 | $provide.value('version', 'TEST_VER'); 12 | }); 13 | inject(function($compile, $rootScope) { 14 | var element = $compile('')($rootScope); 15 | expect(element.text()).toEqual('TEST_VER'); 16 | }); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /scripts/e2e-test-server.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | REM Windows script for starting JSTD server 4 | REM 5 | REM Requirements: 6 | REM - Java (http://www.java.com) 7 | 8 | set BASE_DIR=%~dp0 9 | set PORT=9877 10 | 11 | echo "Starting JsTestDriver Server (http://code.google.com/p/js-test-driver/)" 12 | echo "Please open the following url and capture one or more browsers:" 13 | echo "http://localhost:%PORT%" 14 | java -jar "%BASE_DIR%\..\test\lib\jstestdriver\JsTestDriver.jar" ^ 15 | --port %PORT% ^ 16 | --browserTimeout 20000 ^ 17 | --config "%BASE_DIR%\..\config\jsTestDriver-scenario.conf" ^ 18 | --basePath "%BASE_DIR%\.." 19 | -------------------------------------------------------------------------------- /scripts/test-server.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | REM Windows script for starting JSTD server 4 | REM 5 | REM Requirements: 6 | REM - Java (http://www.java.com) 7 | 8 | set BASE_DIR=%~dp0 9 | set PORT=9876 10 | 11 | echo Starting JsTestDriver Server (http://code.google.com/p/js-test-driver/) 12 | echo Please open the following url and capture one or more browsers: 13 | echo http://localhost:%PORT%/ 14 | 15 | java -jar "%BASE_DIR%\..\test\lib\jstestdriver\JsTestDriver.jar" ^ 16 | --port %PORT% ^ 17 | --browserTimeout 20000 ^ 18 | --config "%BASE_DIR%\..\config\jsTestDriver.conf" ^ 19 | --basePath "%BASE_DIR%\.." 20 | -------------------------------------------------------------------------------- /app/js/app.js: -------------------------------------------------------------------------------- 1 | /*jslint indent: 2, browser: true */ 2 | /*global angular, LobbyCtrl, RoomCtrl */ 3 | 4 | 'use strict'; 5 | 6 | 7 | // Declare app level module which depends on filters, and services 8 | angular.module('pokerApp', ['pokerApp.filters', 'pokerApp.services', 'pokerApp.directives']). 9 | config(['$routeProvider', '$locationProvider', function ($routeProvider, $locationProvider) { 10 | $locationProvider.html5Mode(true).hashPrefix('!'); 11 | $routeProvider.when('/', { templateUrl: 'partials/lobby.html', controller: LobbyCtrl}); 12 | $routeProvider.when('/:roomId', { templateUrl: 'partials/room.html', controller: RoomCtrl}); 13 | $routeProvider.otherwise({redirectTo: '/'}); 14 | }]); -------------------------------------------------------------------------------- /app/js/testing.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true */ 2 | /*global Modernizr */ 3 | 4 | Modernizr.addTest('checked', function () { 5 | return Modernizr.testStyles("#modernizr div {width:10px;} #modernizr input:checked ~ div {width: 20px;}", function (elem) { 6 | var chx = document.createElement('input'), 7 | div = document.createElement('div'); 8 | 9 | chx.setAttribute("type", "checkbox"); 10 | chx.setAttribute("checked", "checked"); 11 | elem.appendChild(chx); 12 | elem.appendChild(div); 13 | 14 | return elem.lastChild.offsetWidth > 10; 15 | }); 16 | }); 17 | 18 | Modernizr.load([ 19 | { 20 | test : Modernizr.mq('only all'), 21 | nope : '/lib/respond.min.js' 22 | } 23 | ]); -------------------------------------------------------------------------------- /app/lib/angular/angular-cookies.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.1 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(m,f,l){'use strict';f.module("ngCookies",["ng"]).factory("$cookies",["$rootScope","$browser",function(d,c){var b={},g={},h,i=!1,j=f.copy,k=f.isUndefined;c.addPollFn(function(){var a=c.cookies();h!=a&&(h=a,j(a,g),j(a,b),i&&d.$apply())})();i=!0;d.$watch(function(){var a,e,d;for(a in g)k(b[a])&&c.cookies(a,l);for(a in b)e=b[a],f.isString(e)?e!==g[a]&&(c.cookies(a,e),d=!0):f.isDefined(g[a])?b[a]=g[a]:delete b[a];if(d)for(a in e=c.cookies(),b)b[a]!==e[a]&&(k(e[a])?delete b[a]:b[a]=e[a])});return b}]).factory("$cookieStore", 7 | ["$cookies",function(d){return{get:function(c){return f.fromJson(d[c])},put:function(c,b){d[c]=f.toJson(b)},remove:function(c){delete d[c]}}}])})(window,window.angular); -------------------------------------------------------------------------------- /app/js/directives.js: -------------------------------------------------------------------------------- 1 | /*jslint indent: 2, browser: true */ 2 | /*global angular */ 3 | 4 | 'use strict'; 5 | 6 | /* Directives */ 7 | 8 | 9 | angular.module('pokerApp.directives', []). 10 | directive('appVersion', ['version', function (version) { 11 | return function (scope, elm, attrs) { 12 | elm.text(version); 13 | }; 14 | }]). 15 | directive('cardvalue', function () { 16 | return function (scope, elm, attrs) { 17 | var value = scope.card || scope.vote.vote, 18 | code = isNaN(parseInt(value, 10)) ? value.charCodeAt() : value; 19 | elm.addClass('card--' + code); 20 | }; 21 | }). 22 | directive('selectedvote', function () { 23 | return function (scope, elm) { 24 | if (scope.vote.sessionId === scope.sessionId) { 25 | elm.addClass('card--selected'); 26 | } 27 | }; 28 | }); 29 | -------------------------------------------------------------------------------- /app/lib/angular/angular-loader.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.1 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(i){'use strict';function d(c,b,e){return c[b]||(c[b]=e())}return d(d(i,"angular",Object),"module",function(){var c={};return function(b,e,f){e&&c.hasOwnProperty(b)&&(c[b]=null);return d(c,b,function(){function a(a,b,d){return function(){c[d||"push"]([a,b,arguments]);return g}}if(!e)throw Error("No module: "+b);var c=[],d=[],h=a("$injector","invoke"),g={_invokeQueue:c,_runBlocks:d,requires:e,name:b,provider:a("$provide","provider"),factory:a("$provide","factory"),service:a("$provide","service"), 7 | value:a("$provide","value"),constant:a("$provide","constant","unshift"),filter:a("$filterProvider","register"),controller:a("$controllerProvider","register"),directive:a("$compileProvider","directive"),config:h,run:function(a){d.push(a);return this}};f&&h(f);return g})}})})(window); -------------------------------------------------------------------------------- /app/js/dom.js: -------------------------------------------------------------------------------- 1 | /*jslint indent: 2, browser: true */ 2 | /*global angular, $, document */ 3 | 4 | $(function () { 5 | $('.no-js-hide').removeClass('no-js-hide'); 6 | }); 7 | 8 | function DropDown(el) { 9 | this.dd = el; 10 | this.val = ''; 11 | this.initEvents(); 12 | } 13 | DropDown.prototype = { 14 | initEvents : function () { 15 | var obj = this; 16 | $(document).on('click', this.dd, function (event) { 17 | $(this).toggleClass('active'); 18 | return false; 19 | }); 20 | $(document).on('click', '.dropdown > li', function () { 21 | $('span', obj.dd).text($(this).text() + ' pack'); 22 | }); 23 | $(document).click(function () { 24 | $('.dropdown-wrapper', obj.dd).removeClass('active'); 25 | }); 26 | } 27 | }; 28 | 29 | function ScrollIntoView(el) { 30 | this.el = el; 31 | } 32 | ScrollIntoView.prototype = { 33 | now : function () { 34 | $('body').animate({ scrollTop : this.el.offset().top }, 'slow'); 35 | } 36 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Hatjitsu 2 | ======== 3 | 4 | Create disposable online [Planning Poker](http://en.wikipedia.org/wiki/Planning_poker) rooms for quick and easy estimations. 5 | 6 | Features 7 | ======== 8 | 9 | * Simple interface 10 | * No login/signup required 11 | * Votes are kept hidden until all have voted to prevent coercion 12 | * 'Observer feature' - watch the planning session without having to vote 13 | * Multiple planning card decks 14 | * Adaptive design allows to work on desktop, tablet and mobile 15 | 16 | Installation 17 | ============ 18 | 19 | npm install -d 20 | node server 21 | 22 | [http://localhost:5000](http://localhost:5000) 23 | 24 | Installation (Docker) 25 | ===================== 26 | 27 | Just checkout the repository and run: 28 | 29 | docker-compose up -d 30 | 31 | TODOs 32 | ===== 33 | 34 | * [x] Collapsible card view / jump to votes on vote 35 | * [x] Update favicon, iOS splash page, Twitter avatar etc with new design 36 | * [ ] Unicode symbol fallback (coffee/ace/king) 37 | * [x] Improve CTA buttons 38 | * [ ] Testing harness 39 | -------------------------------------------------------------------------------- /app/partials/lobby.html: -------------------------------------------------------------------------------- 1 |
2 |

Distributed scrum planning poker for estimating agile projects.

3 |

First person to create the room is the moderator. Share the url or room number with other team members to join the room.

4 | 5 |
6 | 7 |
8 | 9 | 10 |
11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 | -------------------------------------------------------------------------------- /test/e2e/scenarios.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* http://docs.angularjs.org/guide/dev_guide.e2e-testing */ 4 | 5 | describe('my app', function() { 6 | 7 | beforeEach(function() { 8 | browser().navigateTo('../../app/index.html'); 9 | }); 10 | 11 | 12 | it('should automatically redirect to /view1 when location hash/fragment is empty', function() { 13 | expect(browser().location().url()).toBe("/view1"); 14 | }); 15 | 16 | 17 | describe('view1', function() { 18 | 19 | beforeEach(function() { 20 | browser().navigateTo('#/view1'); 21 | }); 22 | 23 | 24 | it('should render view1 when user navigates to /view1', function() { 25 | expect(element('[ng-view] p:first').text()). 26 | toMatch(/partial for view 1/); 27 | }); 28 | 29 | }); 30 | 31 | 32 | describe('view2', function() { 33 | 34 | beforeEach(function() { 35 | browser().navigateTo('#/view2'); 36 | }); 37 | 38 | 39 | it('should render view2 when user navigates to /view2', function() { 40 | expect(element('[ng-view] p:first').text()). 41 | toMatch(/partial for view 2/); 42 | }); 43 | 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Rich Archer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/lib/jasmine/MIT.LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2011 Pivotal Labs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /app/lib/jquery.cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Cookie Plugin 3 | * https://github.com/carhartl/jquery-cookie 4 | * 5 | * Copyright 2011, Klaus Hartl 6 | * Dual licensed under the MIT or GPL Version 2 licenses. 7 | * http://www.opensource.org/licenses/mit-license.php 8 | * http://www.opensource.org/licenses/GPL-2.0 9 | */ 10 | (function($) { 11 | $.cookie = function(key, value, options) { 12 | 13 | // key and at least value given, set cookie... 14 | if (arguments.length > 1 && (!/Object/.test(Object.prototype.toString.call(value)) || value === null || value === undefined)) { 15 | options = $.extend({}, options); 16 | 17 | if (value === null || value === undefined) { 18 | options.expires = -1; 19 | } 20 | 21 | if (typeof options.expires === 'number') { 22 | var days = options.expires, t = options.expires = new Date(); 23 | t.setDate(t.getDate() + days); 24 | } 25 | 26 | value = String(value); 27 | 28 | return (document.cookie = [ 29 | encodeURIComponent(key), '=', options.raw ? value : encodeURIComponent(value), 30 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 31 | options.path ? '; path=' + options.path : '', 32 | options.domain ? '; domain=' + options.domain : '', 33 | options.secure ? '; secure' : '' 34 | ].join('')); 35 | } 36 | 37 | // key and possibly options given, get cookie... 38 | options = value || {}; 39 | var decode = options.raw ? function(s) { return s; } : decodeURIComponent; 40 | 41 | var pairs = document.cookie.split('; '); 42 | for (var i = 0, pair; pair = pairs[i] && pairs[i].split('='); i++) { 43 | if (decode(pair[0]) === key) return decode(pair[1] || ''); // IE saves cookies with empty string as "c; ", e.g. without "=" as opposed to EOMB, thus pair[1] may be undefined 44 | } 45 | return null; 46 | }; 47 | })($); -------------------------------------------------------------------------------- /app/css/prism.css: -------------------------------------------------------------------------------- 1 | /** 2 | * prism.js Dark theme for JavaScript, CSS and HTML 3 | * Based on the slides of the talk “/Reg(exp){2}lained/” 4 | * @author Lea Verou 5 | */ 6 | 7 | code[class*="language-"], 8 | pre[class*="language-"] { 9 | color: white; 10 | text-shadow: 0 -.1em .2em black; 11 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 12 | direction: ltr; 13 | text-align: left; 14 | white-space: pre; 15 | word-spacing: normal; 16 | 17 | -moz-tab-size: 4; 18 | -o-tab-size: 4; 19 | tab-size: 4; 20 | 21 | -webkit-hyphens: none; 22 | -moz-hyphens: none; 23 | -ms-hyphens: none; 24 | hyphens: none; 25 | } 26 | 27 | pre[class*="language-"], 28 | :not(pre) > code[class*="language-"] { 29 | background: hsl(30,20%,25%); 30 | } 31 | 32 | /* Code blocks */ 33 | pre[class*="language-"] { 34 | padding: 1em; 35 | margin: .5em 0; 36 | overflow: auto; 37 | border: .3em solid hsl(30,20%,40%); 38 | border-radius: .5em; 39 | box-shadow: 1px 1px .5em black inset; 40 | } 41 | 42 | /* Inline code */ 43 | :not(pre) > code[class*="language-"] { 44 | padding: .15em .2em .05em; 45 | border-radius: .3em; 46 | border: .13em solid hsl(30,20%,40%); 47 | box-shadow: 1px 1px .3em -.1em black inset; 48 | } 49 | 50 | .token.comment, 51 | .token.prolog, 52 | .token.doctype, 53 | .token.cdata { 54 | color: hsl(30,20%,50%); 55 | } 56 | 57 | .token.punctuation { 58 | opacity: .7; 59 | } 60 | 61 | .namespace { 62 | opacity: .7; 63 | } 64 | 65 | .token.property, 66 | .token.tag, 67 | .token.boolean, 68 | .token.number { 69 | color: hsl(350, 40%, 70%); 70 | } 71 | 72 | .token.selector, 73 | .token.attr-name, 74 | .token.string { 75 | color: hsl(75, 70%, 60%); 76 | } 77 | 78 | .token.operator, 79 | .token.entity, 80 | .token.url, 81 | .language-css .token.string, 82 | .style .token.string { 83 | color: hsl(40, 90%, 60%); 84 | } 85 | 86 | .token.atrule, 87 | .token.attr-value, 88 | .token.keyword { 89 | color: hsl(350, 40%, 70%); 90 | } 91 | 92 | 93 | .token.regex, 94 | .token.important { 95 | color: #e90; 96 | } 97 | 98 | .token.important { 99 | font-weight: bold; 100 | } 101 | 102 | .token.entity { 103 | cursor: help; 104 | } 105 | -------------------------------------------------------------------------------- /lib/lobby.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore')._; 2 | 3 | var RoomClass = require('./room.js'); 4 | 5 | var Lobby = function(io) { 6 | this.io = io; 7 | this.rooms = {}; 8 | }; 9 | 10 | 11 | Lobby.prototype.createRoom = function(roomUrl) { 12 | roomUrl = roomUrl === undefined ? this.createUniqueURL() : roomUrl + this.createUniqueURL(); 13 | if (this.rooms[roomUrl]) { 14 | this.createRoom(roomUrl); 15 | } 16 | 17 | // remove any existing empty rooms first 18 | var thatRooms = this.rooms; 19 | _.each(this.rooms, function(room, key, rooms) { 20 | if (room.getClientCount() == 0) { 21 | delete thatRooms[key]; 22 | // console.log("removed room " + key); 23 | } 24 | }); 25 | 26 | this.rooms[roomUrl] = new RoomClass.Room(this.io, roomUrl); 27 | return roomUrl; 28 | }; 29 | 30 | 31 | Lobby.prototype.createUniqueURL = function() { 32 | var text = "" 33 | , possible = "0123456789" 34 | , i 35 | ; 36 | for ( i = 0; i < 5; i++ ) { 37 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 38 | } 39 | return text; 40 | }; 41 | 42 | Lobby.prototype.joinRoom = function(socket, data) { 43 | if(data.roomUrl && data.roomUrl in this.rooms) { 44 | var room = this.getRoom(data.roomUrl); 45 | if (socket != null && data && data.sessionId != null) { 46 | room.enter(socket, data); 47 | socket.join(data.roomUrl); 48 | socket.broadcast.to(data.roomUrl).emit('room joined'); 49 | } 50 | return room; 51 | } else { 52 | return { error: 'Sorry, this room no longer exists ...'}; 53 | } 54 | }; 55 | 56 | Lobby.prototype.getRoom = function(roomUrl) { 57 | var room = this.rooms[roomUrl]; 58 | if (room) { 59 | return room; 60 | } else { 61 | return { error: 'Sorry, this room no longer exists ...'}; 62 | } 63 | }; 64 | 65 | Lobby.prototype.broadcastDisconnect = function(socket) { 66 | var clientRooms = this.io.sockets.manager.roomClients[socket.id] 67 | , socketRoom, room 68 | ; 69 | // console.log("broadcast Disconnect"); 70 | for (socketRoom in clientRooms) { 71 | if (socketRoom.length) { 72 | roomUrl = socketRoom.substr(1); 73 | var room = this.getRoom(roomUrl); 74 | if (room) { 75 | room.leave(socket); 76 | } 77 | this.io.sockets.in(roomUrl).emit('room left'); 78 | } 79 | } 80 | }; 81 | 82 | 83 | exports.Lobby = Lobby; -------------------------------------------------------------------------------- /app/lib/angular/angular-resource.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.1 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(A,f,u){'use strict';f.module("ngResource",["ng"]).factory("$resource",["$http","$parse",function(v,w){function g(b,c){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(c?null:/%20/g,"+")}function l(b,c){this.template=b+="#";this.defaults=c||{};var a=this.urlParams={};j(b.split(/\W/),function(c){c&&b.match(RegExp("[^\\\\]:"+c+"\\W"))&&(a[c]=!0)});this.template=b.replace(/\\:/g,":")}function s(b,c,a){function f(d){var b= 7 | {};j(c||{},function(a,x){var m;a.charAt&&a.charAt(0)=="@"?(m=a.substr(1),m=w(m)(d)):m=a;b[x]=m});return b}function e(a){t(a||{},this)}var y=new l(b),a=r({},z,a);j(a,function(d,g){var l=d.method=="POST"||d.method=="PUT"||d.method=="PATCH";e[g]=function(a,b,c,g){var i={},h,k=o,p=null;switch(arguments.length){case 4:p=g,k=c;case 3:case 2:if(q(b)){if(q(a)){k=a;p=b;break}k=b;p=c}else{i=a;h=b;k=c;break}case 1:q(a)?k=a:l?h=a:i=a;break;case 0:break;default:throw"Expected between 0-4 arguments [params, data, success, error], got "+ 8 | arguments.length+" arguments.";}var n=this instanceof e?this:d.isArray?[]:new e(h);v({method:d.method,url:y.url(r({},f(h),d.params||{},i)),data:h}).then(function(a){var b=a.data;if(b)d.isArray?(n.length=0,j(b,function(a){n.push(new e(a))})):t(b,n);(k||o)(n,a.headers)},p);return n};e.bind=function(d){return s(b,r({},c,d),a)};e.prototype["$"+g]=function(a,b,d){var c=f(this),i=o,h;switch(arguments.length){case 3:c=a;i=b;h=d;break;case 2:case 1:q(a)?(i=a,h=b):(c=a,i=b||o);case 0:break;default:throw"Expected between 1-3 arguments [params, success, error], got "+ 9 | arguments.length+" arguments.";}e[g].call(this,c,l?this:u,i,h)}});return e}var z={get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}},o=f.noop,j=f.forEach,r=f.extend,t=f.copy,q=f.isFunction;l.prototype={url:function(b){var c=this,a=this.template,f,b=b||{};j(this.urlParams,function(e,d){f=g(b[d]||c.defaults[d]||"",!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+");a=a.replace(RegExp(":"+d+"(\\W)"),f+"$1")});var a= 10 | a.replace(/\/?#$/,""),e=[];j(b,function(a,b){c.urlParams[b]||e.push(g(b)+"="+g(a))});e.sort();a=a.replace(/\/*$/,"");return a+(e.length?"?"+e.join("&"):"")}};return s}])})(window,window.angular); -------------------------------------------------------------------------------- /app/js/sg.js: -------------------------------------------------------------------------------- 1 | /* jshint jquery:true */ 2 | /* Author: Dave Rupert 3 | * License: WTFPL 4 | * Liberally pinched and readapted with thanks 5 | ----------------------*/ 6 | 7 | (function($){ 8 | 'use strict'; 9 | 10 | $.fn.dataCodeBlock = function(){ 11 | 12 | // Yoinked from Prototype.js 13 | var escapeHTML = function( code ) { 14 | return code.replace(/&/g,'&').replace(//g,'>'); 15 | }; 16 | var lastIndentationSize = function ( ary ){ 17 | if (ary.length <= 1){ 18 | return 0; 19 | } 20 | var str = ary[ary.length-1] 21 | var string = ""; 22 | var spaces = str.match(/ /g) === null ? 0 : str.match(/ /g).length; 23 | for (var i = 0; i < spaces; i++) { 24 | string += " "; 25 | }; 26 | return string; 27 | }; 28 | var reIndent = function ( str, sub ){ 29 | var intIndexOfMatch = str.indexOf( sub ); 30 | while (intIndexOfMatch != -1){ 31 | str = str.replace( sub, "" ) 32 | intIndexOfMatch = str.indexOf( sub ); 33 | } 34 | return str; 35 | }; 36 | 37 | return $('[data-codeblock]').each(function(){ 38 | var target = $(this).data('codeblock') 39 | , html = $(this).clone().removeAttr('data-codeblock')[0].outerHTML 40 | , codeblock = $('
')
41 |         , indentation = html.split("\n")
42 |         , whitespace = lastIndentationSize( indentation )
43 |         , newhtml = html;
44 | 
45 |       if ( whitespace.length ) {
46 |         newhtml = reIndent( html, whitespace);
47 |       }
48 | 
49 |       codeblock.find('code').append( escapeHTML(newhtml) );
50 | 
51 |       if(target) {
52 |         $(target).append(codeblock);
53 |       } else {
54 |         $(this).after(codeblock);
55 |       }
56 |     });
57 | 
58 |   };
59 | 
60 |   // Self Execute!!
61 |   $.fn.dataCodeBlock();
62 | })(jQuery);
63 | 
64 | 
65 | 
66 | $(document).ready(function(){
67 |   $('#toggleFlipper01').click(function(){
68 |     $('#toggleFlippee01').toggleClass('flipped');
69 |   });
70 |   $('#toggleFlipper02').click(function(){
71 |     $('#toggleFlippee02').toggleClass('flipped');
72 |   });
73 |   $('#toggleFlipper03').click(function(){
74 |     $('#toggleFlippee03').toggleClass('flipped');
75 |   });
76 |   $('#toggleFlipper04').click(function(){
77 |     $('#toggleFlippee04').toggleClass('flipped');
78 |   });
79 |   $('#toggleFlipper05').click(function(){
80 |     $('#toggleFlippee05').toggleClass('flipped-stagger');
81 |   });
82 | });


--------------------------------------------------------------------------------
/test/lib/jasmine/jasmine.css:
--------------------------------------------------------------------------------
  1 | body {
  2 |   font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif;
  3 | }
  4 | 
  5 | 
  6 | .jasmine_reporter a:visited, .jasmine_reporter a {
  7 |   color: #303; 
  8 | }
  9 | 
 10 | .jasmine_reporter a:hover, .jasmine_reporter a:active {
 11 |   color: blue; 
 12 | }
 13 | 
 14 | .run_spec {
 15 |   float:right;
 16 |   padding-right: 5px;
 17 |   font-size: .8em;
 18 |   text-decoration: none;
 19 | }
 20 | 
 21 | .jasmine_reporter {
 22 |   margin: 0 5px;
 23 | }
 24 | 
 25 | .banner {
 26 |   color: #303;
 27 |   background-color: #fef;
 28 |   padding: 5px;
 29 | }
 30 | 
 31 | .logo {
 32 |   float: left;
 33 |   font-size: 1.1em;
 34 |   padding-left: 5px;
 35 | }
 36 | 
 37 | .logo .version {
 38 |   font-size: .6em;
 39 |   padding-left: 1em;
 40 | }
 41 | 
 42 | .runner.running {
 43 |   background-color: yellow;
 44 | }
 45 | 
 46 | 
 47 | .options {
 48 |   text-align: right;
 49 |   font-size: .8em;
 50 | }
 51 | 
 52 | 
 53 | 
 54 | 
 55 | .suite {
 56 |   border: 1px outset gray;
 57 |   margin: 5px 0;
 58 |   padding-left: 1em;
 59 | }
 60 | 
 61 | .suite .suite {
 62 |   margin: 5px; 
 63 | }
 64 | 
 65 | .suite.passed {
 66 |   background-color: #dfd;
 67 | }
 68 | 
 69 | .suite.failed {
 70 |   background-color: #fdd;
 71 | }
 72 | 
 73 | .spec {
 74 |   margin: 5px;
 75 |   padding-left: 1em;
 76 |   clear: both;
 77 | }
 78 | 
 79 | .spec.failed, .spec.passed, .spec.skipped {
 80 |   padding-bottom: 5px;
 81 |   border: 1px solid gray;
 82 | }
 83 | 
 84 | .spec.failed {
 85 |   background-color: #fbb;
 86 |   border-color: red;
 87 | }
 88 | 
 89 | .spec.passed {
 90 |   background-color: #bfb;
 91 |   border-color: green;
 92 | }
 93 | 
 94 | .spec.skipped {
 95 |   background-color: #bbb;
 96 | }
 97 | 
 98 | .messages {
 99 |   border-left: 1px dashed gray;
100 |   padding-left: 1em;
101 |   padding-right: 1em;
102 | }
103 | 
104 | .passed {
105 |   background-color: #cfc;
106 |   display: none;
107 | }
108 | 
109 | .failed {
110 |   background-color: #fbb;
111 | }
112 | 
113 | .skipped {
114 |   color: #777;
115 |   background-color: #eee;
116 |   display: none;
117 | }
118 | 
119 | 
120 | /*.resultMessage {*/
121 |   /*white-space: pre;*/
122 | /*}*/
123 | 
124 | .resultMessage span.result {
125 |   display: block;
126 |   line-height: 2em;
127 |   color: black;
128 | }
129 | 
130 | .resultMessage .mismatch {
131 |   color: black;
132 | }
133 | 
134 | .stackTrace {
135 |   white-space: pre;
136 |   font-size: .8em;
137 |   margin-left: 10px;
138 |   max-height: 5em;
139 |   overflow: auto;
140 |   border: 1px inset red;
141 |   padding: 1em;
142 |   background: #eef;
143 | }
144 | 
145 | .finished-at {
146 |   padding-left: 1em;
147 |   font-size: .6em;
148 | }
149 | 
150 | .show-passed .passed,
151 | .show-skipped .skipped {
152 |   display: block;
153 | }
154 | 
155 | 
156 | #jasmine_content {
157 |   position:fixed;
158 |   right: 100%;
159 | }
160 | 
161 | .runner {
162 |   border: 1px solid gray;
163 |   display: block;
164 |   margin: 5px 0;
165 |   padding: 2px 0 2px 10px;
166 | }
167 | 


--------------------------------------------------------------------------------
/app/lib/angular/angular-sanitize.min.js:
--------------------------------------------------------------------------------
 1 | /*
 2 |  AngularJS v1.0.1
 3 |  (c) 2010-2012 Google, Inc. http://angularjs.org
 4 |  License: MIT
 5 | */
 6 | (function(I,g){'use strict';function i(a){var d={},a=a.split(","),b;for(b=0;b=0;e--)if(f[e]==b)break;if(e>=0){for(c=f.length-1;c>=e;c--)d.end&&d.end(f[c]);f.length=
 7 | e}}var c,h,f=[],j=a;for(f.last=function(){return f[f.length-1]};a;){h=!0;if(!f.last()||!q[f.last()]){if(a.indexOf("<\!--")===0)c=a.indexOf("--\>"),c>=0&&(d.comment&&d.comment(a.substring(4,c)),a=a.substring(c+3),h=!1);else if(B.test(a)){if(c=a.match(r))a=a.substring(c[0].length),c[0].replace(r,e),h=!1}else if(C.test(a)&&(c=a.match(s)))a=a.substring(c[0].length),c[0].replace(s,b),h=!1;h&&(c=a.indexOf("<"),h=c<0?a:a.substring(0,c),a=c<0?"":a.substring(c),d.chars&&d.chars(k(h)))}else a=a.replace(RegExp("(.*)<\\s*\\/\\s*"+
 8 | f.last()+"[^>]*>","i"),function(b,a){a=a.replace(D,"$1").replace(E,"$1");d.chars&&d.chars(k(a));return""}),e("",f.last());if(a==j)throw"Parse Error: "+a;j=a}e()}function k(a){l.innerHTML=a.replace(//g,">")}function u(a){var d=!1,b=g.bind(a,a.push);return{start:function(a,c,h){a=g.lowercase(a);!d&&q[a]&&(d=a);!d&&v[a]==
 9 | !0&&(b("<"),b(a),g.forEach(c,function(a,c){var e=g.lowercase(c);if(G[e]==!0&&(w[e]!==!0||a.match(H)))b(" "),b(c),b('="'),b(t(a)),b('"')}),b(h?"/>":">"))},end:function(a){a=g.lowercase(a);!d&&v[a]==!0&&(b(""));a==d&&(d=!1)},chars:function(a){d||b(t(a))}}}var s=/^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,r=/^<\s*\/\s*([\w:-]+)[^>]*>/,A=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,C=/^/g,
10 | E=//g,H=/^((ftp|https?):\/\/|mailto:|#)/,F=/([^\#-~| |!])/g,p=i("area,br,col,hr,img,wbr"),x=i("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),y=i("rp,rt"),o=g.extend({},y,x),m=g.extend({},x,i("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")),n=g.extend({},y,i("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")),
11 | q=i("script,style"),v=g.extend({},p,m,n,o),w=i("background,cite,href,longdesc,src,usemap"),G=g.extend({},w,i("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,span,start,summary,target,title,type,valign,value,vspace,width")),l=document.createElement("pre");g.module("ngSanitize",[]).value("$sanitize",function(a){var d=[];
12 | z(a,u(d));return d.join("")});g.module("ngSanitize").directive("ngBindHtml",["$sanitize",function(a){return function(d,b,e){b.addClass("ng-binding").data("$binding",e.ngBindHtml);d.$watch(e.ngBindHtml,function(c){c=a(c);b.html(c||"")})}}]);g.module("ngSanitize").filter("linky",function(){var a=/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/,d=/^mailto:/;return function(b){if(!b)return b;for(var e=b,c=[],h=u(c),f,g;b=e.match(a);)f=b[0],b[2]==b[3]&&(f="mailto:"+f),g=b.index,
13 | h.chars(e.substr(0,g)),h.start("a",{href:f}),h.chars(b[0].replace(d,"")),h.end("a"),e=e.substring(g+b[0].length);h.chars(e);return c.join("")}})})(window,window.angular);


--------------------------------------------------------------------------------
/app/lib/respond.min.js:
--------------------------------------------------------------------------------
1 | /*! matchMedia() polyfill - Test a CSS media type/query in JS. Authors & copyright (c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas. Dual MIT/BSD license */
2 | /*! NOTE: If you're already including a window.matchMedia polyfill via Modernizr or otherwise, you don't need this part */
3 | window.matchMedia=window.matchMedia||(function(e,f){var c,a=e.documentElement,b=a.firstElementChild||a.firstChild,d=e.createElement("body"),g=e.createElement("div");g.id="mq-test-1";g.style.cssText="position:absolute;top:-100em";d.style.background="none";d.appendChild(g);return function(h){g.innerHTML='­';a.insertBefore(d,b);c=g.offsetWidth==42;a.removeChild(d);return{matches:c,media:h}}})(document);
4 | 
5 | /*! Respond.js v1.1.0: min/max-width media query polyfill. (c) Scott Jehl. MIT/GPLv2 Lic. j.mp/respondjs  */
6 | (function(e){e.respond={};respond.update=function(){};respond.mediaQueriesSupported=e.matchMedia&&e.matchMedia("only all").matches;if(respond.mediaQueriesSupported){return}var w=e.document,s=w.documentElement,i=[],k=[],q=[],o={},h=30,f=w.getElementsByTagName("head")[0]||s,g=w.getElementsByTagName("base")[0],b=f.getElementsByTagName("link"),d=[],a=function(){var D=b,y=D.length,B=0,A,z,C,x;for(;B-1,minw:F.match(/\(min\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:F.match(/\(max\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}}j()},l,r,v=function(){var z,A=w.createElement("div"),x=w.body,y=false;A.style.cssText="position:absolute;font-size:1em;width:1em";if(!x){x=y=w.createElement("body");x.style.background="none"}x.appendChild(A);s.insertBefore(x,s.firstChild);z=A.offsetWidth;if(y){s.removeChild(x)}else{x.removeChild(A)}z=p=parseFloat(z);return z},p,j=function(I){var x="clientWidth",B=s[x],H=w.compatMode==="CSS1Compat"&&B||w.body[x]||B,D={},G=b[b.length-1],z=(new Date()).getTime();if(I&&l&&z-l-1?(p||v()):1)}if(!!J){J=parseFloat(J)*(J.indexOf(y)>-1?(p||v()):1)}if(!K.hasquery||(!A||!L)&&(A||H>=C)&&(L||H<=J)){if(!D[K.media]){D[K.media]=[]}D[K.media].push(k[K.rules])}}for(var E in q){if(q[E]&&q[E].parentNode===f){f.removeChild(q[E])}}for(var E in D){var M=w.createElement("style"),F=D[E].join("\n");M.type="text/css";M.media=E;f.insertBefore(M,G.nextSibling);if(M.styleSheet){M.styleSheet.cssText=F}else{M.appendChild(w.createTextNode(F))}q.push(M)}},n=function(x,z){var y=c();if(!y){return}y.open("GET",x,true);y.onreadystatechange=function(){if(y.readyState!=4||y.status!=200&&y.status!=304){return}z(y.responseText)};if(y.readyState==4){return}y.send(null)},c=(function(){var x=false;try{x=new XMLHttpRequest()}catch(y){x=new ActiveXObject("Microsoft.XMLHTTP")}return function(){return x}})();a();respond.update=a;function t(){j(true)}if(e.addEventListener){e.addEventListener("resize",t,false)}else{if(e.attachEvent){e.attachEvent("onresize",t)}}})(this);


--------------------------------------------------------------------------------
/app/partials/room.html:
--------------------------------------------------------------------------------
 1 | 
2 |

Room: {{roomId}}

3 | 4 |
5 |
6 |
7 |
8 | Choose your estimate... 9 |
10 |
11 | You have chosen not to vote. 12 |
13 |
14 |
15 |
16 | 26 |
27 |
28 |
29 | 30 | 31 |
32 |
33 | {{card}} 34 |
35 |
36 | No cards found 37 |
38 |
39 | 40 |
41 | 42 |
43 | 44 | 45 |
46 | 47 | 48 |
49 | 50 |
51 | 52 |
53 |

 

54 |
55 | 56 |
57 |
58 |

You haven't estimated yet

59 |

Your current estimate: {{myVote}}

60 |
61 |
62 | 63 |
64 | 65 |
66 | 67 |
68 |
x
69 |
{{vote.visibleVote}}
70 |
71 |
72 |   73 |
74 | 75 |
Average: {{votingAverage}} (StdDev = {{votingStandardDeviation}})
76 |
77 | 78 |
79 | 80 |
81 |
82 | 85 | 88 | 91 |
92 |
93 | 94 |
95 | 96 |
97 | 98 |
99 | -------------------------------------------------------------------------------- /app/js/services.js: -------------------------------------------------------------------------------- 1 | /*jslint indent: 2, browser: true */ 2 | /*global angular, Sock, io, $ */ 3 | 4 | 'use strict'; 5 | 6 | /* Services */ 7 | 8 | 9 | // Demonstrate how to register services 10 | // In this case it is a simple value service. 11 | var pokerAppServices = angular.module('pokerApp.services', []); 12 | 13 | pokerAppServices.value('version', '0.1'); 14 | 15 | pokerAppServices.service('socket', ['$rootScope', '$timeout', function ($rootScope) { 16 | var sock = new Sock($rootScope); 17 | return sock; 18 | }]); 19 | 20 | pokerAppServices.factory('socket', ['$rootScope', function ($rootScope) { 21 | var socket = io.connect(location.protocol + '//' + location.hostname, { 22 | 'port': location.port, 23 | 'reconnect': true, 24 | 'reconnection delay': 500, 25 | 'max reconnection attempts': 10, 26 | 'try multiple transports': true, 27 | 'transports': ['websocket', 'htmlfile', 'xhr-polling', 'jsonp-polling'] 28 | }); 29 | 30 | $rootScope.socketMessage = null; 31 | $rootScope.activity = false; 32 | $rootScope.sessionId = null; 33 | 34 | socket.on('error', function (reason) { 35 | // console.log('service: on error', reason); 36 | $rootScope.$apply(function () { 37 | $rootScope.socketMessage = ":-( Error = " + reason; 38 | }); 39 | // console.log(reason); 40 | }); 41 | socket.on('connect_failed', function (reason) { 42 | // console.log('service: on connect failed', reason); 43 | $rootScope.$apply(function () { 44 | $rootScope.socketMessage = ":-( Connect failed"; 45 | }); 46 | // console.log(reason); 47 | }); 48 | socket.on('disconnect', function () { 49 | // console.log('service: on disconnect'); 50 | $rootScope.$apply(function () { 51 | $rootScope.socketMessage = ":-( Disconnected"; 52 | }); 53 | // console.log('disconnected'); 54 | }); 55 | socket.on('connecting', function () { 56 | // console.log('service: on connecting'); 57 | $rootScope.$apply(function () { 58 | $rootScope.socketMessage = "Connecting..."; 59 | }); 60 | // console.log('disconnected'); 61 | }); 62 | socket.on('reconnecting', function () { 63 | // console.log('service: on reconnecting'); 64 | $rootScope.$apply(function () { 65 | $rootScope.socketMessage = "Reconnecting..."; 66 | }); 67 | // console.log('disconnected'); 68 | }); 69 | socket.on('reconnect', function () { 70 | // console.log('service: on reconnect'); 71 | $rootScope.$apply(function () { 72 | $rootScope.socketMessage = null; 73 | }); 74 | // console.log('disconnected'); 75 | }); 76 | socket.on('reconnect_failed', function () { 77 | // console.log('service: on reconnect_failed'); 78 | $rootScope.$apply(function () { 79 | $rootScope.socketMessage = ":-( Reconnect failed"; 80 | }); 81 | // console.log('disconnected'); 82 | }); 83 | socket.on('connect', function () { 84 | var sessionId = this.socket.sessionid; 85 | // console.log('service: on connect'); 86 | $rootScope.$apply(function () { 87 | $rootScope.socketMessage = null; 88 | // console.log("new session id = " + sessionId); 89 | if (!$.cookie("sessionId")) { 90 | $.cookie("sessionId", sessionId); 91 | } 92 | $rootScope.sessionId = $.cookie("sessionId"); 93 | // console.log("session id = " + that.rootScope.sessionId); 94 | }); 95 | }); 96 | 97 | return { 98 | on: function (eventName, callback) { 99 | $rootScope.socketMessage = null; 100 | socket.on(eventName, function () { 101 | var args = arguments; 102 | $rootScope.$apply(function () { 103 | callback.apply(socket, args); 104 | }); 105 | }); 106 | }, 107 | emit: function (eventName, data, callback) { 108 | $rootScope.activity = true; 109 | socket.emit(eventName, data, function () { 110 | var args = arguments; 111 | $rootScope.$apply(function () { 112 | $rootScope.activity = false; 113 | if (callback) { 114 | callback.apply(socket, args); 115 | } 116 | }); 117 | }); 118 | } 119 | }; 120 | }]); -------------------------------------------------------------------------------- /lib/room.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore')._; 2 | var util = require('util'); 3 | 4 | var Room = function(io, roomUrl) { 5 | this.io = io; 6 | this.roomUrl = roomUrl; 7 | this.createdAt = calcTime(2); 8 | this.createAdmin = true; 9 | this.hasAdmin = false; 10 | this.cardPack = 'goat'; 11 | this.connections = {}; // we collect the votes in here 12 | this.forcedReveal = false; 13 | this.alreadySorted = false; 14 | }; 15 | 16 | Room.prototype.info = function() { 17 | this.createAdmin = this.hasAdmin === false; 18 | this.hasAdmin = true; 19 | // console.log("room info = ", this.json()); 20 | return this.json(); 21 | }; 22 | 23 | Room.prototype.enter = function(socket, data) { 24 | // console.log("room entered as " + socket.id); 25 | if (this.connections[data.sessionId]) { 26 | this.connections[data.sessionId].socketId = socket.id; 27 | } else { 28 | this.connections[data.sessionId] = { sessionId: data.sessionId, socketId: socket.id, vote: null, voter: true }; 29 | } 30 | } 31 | 32 | Room.prototype.leave = function(socket) { 33 | var connection = _.find(this.connections, function(c) { return c.socketId === socket.id } ); 34 | if (connection && connection.sessionId) { 35 | connection.socketId = null; 36 | } 37 | } 38 | 39 | Room.prototype.setCardPack = function(data) { 40 | this.cardPack = data.cardPack; 41 | this.io.sockets.in(this.roomUrl).emit('card pack set'); 42 | // console.log('card pack set'); 43 | } 44 | 45 | Room.prototype.toggleVoter = function(data) { 46 | if (this.connections[data.sessionId]) { 47 | this.connections[data.sessionId]['voter'] = data.voter; 48 | if (!data.voter) { 49 | this.connections[data.sessionId]['vote'] = null; 50 | } 51 | // console.log("voter set to " + data.voter + " for " + data.sessionId); 52 | } 53 | this.io.sockets.in(this.roomUrl).emit('voter status changed'); 54 | } 55 | 56 | Room.prototype.recordVote = function(socket, data) { 57 | if (this.connections[data.sessionId]) { 58 | this.connections[data.sessionId]['vote'] = data.vote; 59 | } 60 | socket.broadcast.to(this.roomUrl).emit('voted'); 61 | // this.io.sockets.in(this.roomUrl).emit('voted'); 62 | } 63 | 64 | Room.prototype.destroyVote = function(socket, data) { 65 | if (this.connections[data.sessionId]) { 66 | this.connections[data.sessionId]['vote'] = null; 67 | } 68 | socket.broadcast.to(this.roomUrl).emit('unvoted'); 69 | // this.io.sockets.in(this.roomUrl).emit('unvoted'); 70 | } 71 | 72 | Room.prototype.resetVote = function() { 73 | _.forEach(this.connections, function(c) { 74 | c.vote = null; 75 | }) 76 | this.forcedReveal = false; 77 | this.alreadySorted = false; 78 | this.io.sockets.in(this.roomUrl).emit('vote reset'); 79 | } 80 | 81 | Room.prototype.forceReveal = function() { 82 | this.forcedReveal = true; 83 | this.alreadySorted = false; 84 | this.io.sockets.in(this.roomUrl).emit('reveal'); 85 | } 86 | 87 | Room.prototype.sortVotes = function() { 88 | this.alreadySorted = true; 89 | this.io.sockets.in(this.roomUrl).emit('reveal'); 90 | } 91 | 92 | Room.prototype.getClientCount = function() { 93 | return _.filter(this.connections, function(c) { return c.socketId }).length; 94 | } 95 | 96 | Room.prototype.json = function() { 97 | return { 98 | roomUrl: this.roomUrl, 99 | createdAt: this.createdAt, 100 | createAdmin: this.createAdmin, 101 | hasAdmin: this.hasAdmin, 102 | cardPack: this.cardPack, 103 | forcedReveal: this.forcedReveal, 104 | alreadySorted: this.alreadySorted, 105 | connections: _.filter(this.connections, function(c) { return c.socketId }) 106 | }; 107 | } 108 | 109 | 110 | function calcTime(offset) { 111 | // create Date object for current location 112 | d = new Date(); 113 | 114 | // convert to msec 115 | // add local time zone offset 116 | // get UTC time in msec 117 | utc = d.getTime() + (d.getTimezoneOffset() * 60000); 118 | 119 | // create new Date object for different place 120 | // using supplied offset 121 | nd = new Date(utc + (3600000*offset)); 122 | 123 | // return time as a string 124 | return nd.toLocaleString(); 125 | } 126 | 127 | 128 | exports.Room = Room; -------------------------------------------------------------------------------- /app/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | 18 | 19 | Hatjitsu :: Online Scrum Planning Poker for Agile Projects 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | <%- CDN([ '/lib/modernizr.custom.07116.js', '/js/testing.js' ]) %> 35 | 36 | 49 | 50 | 51 |
52 |
53 |
54 | 55 | 56 |

Hatjitsu

57 |
58 | 61 |
62 |
63 | 64 |
65 |
Activity…
66 |
67 |
68 |
69 |
70 | 71 |
72 | 73 | 78 |
79 |
80 | 81 | 82 | 87 | 88 | <%- CDN([ '/lib/underscore.min.js', '/lib/jquery.cookie.js' ]) %> 89 | <%- CDN([ '/js/app.js', '/js/controllers.js', '/js/directives.js', '/js/filters.js', '/js/services.js', '/js/dom.js']) %> 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /test/lib/jasmine/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var sys = require('sys'); 3 | var path = require('path'); 4 | 5 | var filename = __dirname + '/jasmine.js'; 6 | global.window = { 7 | setTimeout: setTimeout, 8 | clearTimeout: clearTimeout, 9 | setInterval: setInterval, 10 | clearInterval: clearInterval 11 | }; 12 | var src = fs.readFileSync(filename); 13 | var jasmine = process.compile(src + '\njasmine;', filename); 14 | delete global.window; 15 | 16 | function noop(){} 17 | 18 | jasmine.executeSpecsInFolder = function(folder, done, isVerbose, showColors, matcher){ 19 | var log = []; 20 | var columnCounter = 0; 21 | var start = 0; 22 | var elapsed = 0; 23 | var verbose = isVerbose || false; 24 | var fileMatcher = new RegExp(matcher || "\.js$"); 25 | var colors = showColors || false; 26 | var specs = jasmine.getAllSpecFiles(folder, fileMatcher); 27 | 28 | var ansi = { 29 | green: '\033[32m', 30 | red: '\033[31m', 31 | yellow: '\033[33m', 32 | none: '\033[0m' 33 | }; 34 | 35 | for (var i = 0, len = specs.length; i < len; ++i){ 36 | var filename = specs[i]; 37 | require(filename.replace(/\.*$/, "")); 38 | } 39 | 40 | var jasmineEnv = jasmine.getEnv(); 41 | jasmineEnv.reporter = { 42 | log: function(str){ 43 | }, 44 | 45 | reportSpecStarting: function(runner) { 46 | }, 47 | 48 | reportRunnerStarting: function(runner) { 49 | sys.puts('Started'); 50 | start = Number(new Date); 51 | }, 52 | 53 | reportSuiteResults: function(suite) { 54 | var specResults = suite.results(); 55 | var path = []; 56 | while(suite) { 57 | path.unshift(suite.description); 58 | suite = suite.parentSuite; 59 | } 60 | var description = path.join(' '); 61 | 62 | if (verbose) 63 | log.push('Spec ' + description); 64 | 65 | specResults.items_.forEach(function(spec){ 66 | if (spec.failedCount > 0 && spec.description) { 67 | if (!verbose) 68 | log.push(description); 69 | log.push(' it ' + spec.description); 70 | spec.items_.forEach(function(result){ 71 | log.push(' ' + result.trace.stack + '\n'); 72 | }); 73 | } 74 | }); 75 | }, 76 | 77 | reportSpecResults: function(spec) { 78 | var result = spec.results(); 79 | var msg = ''; 80 | if (result.passed()) 81 | { 82 | msg = (colors) ? (ansi.green + '.' + ansi.none) : '.'; 83 | // } else if (result.skipped) { TODO: Research why "result.skipped" returns false when "xit" is called on a spec? 84 | // msg = (colors) ? (ansi.yellow + '*' + ansi.none) : '*'; 85 | } else { 86 | msg = (colors) ? (ansi.red + 'F' + ansi.none) : 'F'; 87 | } 88 | sys.print(msg); 89 | if (columnCounter++ < 50) return; 90 | columnCounter = 0; 91 | sys.print('\n'); 92 | }, 93 | 94 | 95 | reportRunnerResults: function(runner) { 96 | elapsed = (Number(new Date) - start) / 1000; 97 | sys.puts('\n'); 98 | log.forEach(function(log){ 99 | sys.puts(log); 100 | }); 101 | sys.puts('Finished in ' + elapsed + ' seconds'); 102 | 103 | var summary = jasmine.printRunnerResults(runner); 104 | if(colors) 105 | { 106 | if(runner.results().failedCount === 0 ) 107 | sys.puts(ansi.green + summary + ansi.none); 108 | else 109 | sys.puts(ansi.red + summary + ansi.none); 110 | } else { 111 | sys.puts(summary); 112 | } 113 | (done||noop)(runner, log); 114 | } 115 | }; 116 | jasmineEnv.execute(); 117 | }; 118 | 119 | jasmine.getAllSpecFiles = function(dir, matcher){ 120 | var specs = []; 121 | 122 | if (fs.statSync(dir).isFile() && dir.match(matcher)) { 123 | specs.push(dir); 124 | } else { 125 | var files = fs.readdirSync(dir); 126 | for (var i = 0, len = files.length; i < len; ++i){ 127 | var filename = dir + '/' + files[i]; 128 | if (fs.statSync(filename).isFile() && filename.match(matcher)){ 129 | specs.push(filename); 130 | }else if (fs.statSync(filename).isDirectory()){ 131 | var subfiles = this.getAllSpecFiles(filename, matcher); 132 | subfiles.forEach(function(result){ 133 | specs.push(result); 134 | }); 135 | } 136 | } 137 | } 138 | 139 | return specs; 140 | }; 141 | 142 | jasmine.printRunnerResults = function(runner){ 143 | var results = runner.results(); 144 | var suites = runner.suites(); 145 | var msg = ''; 146 | msg += suites.length + ' test' + ((suites.length === 1) ? '' : 's') + ', '; 147 | msg += results.totalCount + ' assertion' + ((results.totalCount === 1) ? '' : 's') + ', '; 148 | msg += results.failedCount + ' failure' + ((results.failedCount === 1) ? '' : 's') + '\n'; 149 | return msg; 150 | }; 151 | 152 | function now(){ 153 | return new Date().getTime(); 154 | } 155 | 156 | jasmine.asyncSpecWait = function(){ 157 | var wait = jasmine.asyncSpecWait; 158 | wait.start = now(); 159 | wait.done = false; 160 | (function innerWait(){ 161 | waits(10); 162 | runs(function() { 163 | if (wait.start + wait.timeout < now()) { 164 | expect('timeout waiting for spec').toBeNull(); 165 | } else if (wait.done) { 166 | wait.done = false; 167 | } else { 168 | innerWait(); 169 | } 170 | }); 171 | })(); 172 | }; 173 | jasmine.asyncSpecWait.timeout = 4 * 1000; 174 | jasmine.asyncSpecDone = function(){ 175 | jasmine.asyncSpecWait.done = true; 176 | }; 177 | 178 | for ( var key in jasmine) { 179 | exports[key] = jasmine[key]; 180 | } -------------------------------------------------------------------------------- /app/js/prism.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prism: Lightweight, robust, elegant syntax highlighting 3 | * MIT license http://www.opensource.org/licenses/mit-license.php/ 4 | * @author Lea Verou http://lea.verou.me 5 | */(function(){var e=/\blang(?:uage)?-(?!\*)(\w+)\b/i,t=self.Prism={util:{type:function(e){return Object.prototype.toString.call(e).match(/\[object (\w+)\]/)[1]},clone:function(e){var n=t.util.type(e);switch(n){case"Object":var r={};for(var i in e)e.hasOwnProperty(i)&&(r[i]=t.util.clone(e[i]));return r;case"Array":return e.slice()}return e}},languages:{extend:function(e,n){var r=t.util.clone(t.languages[e]);for(var i in n)r[i]=n[i];return r},insertBefore:function(e,n,r,i){i=i||t.languages;var s=i[e],o={};for(var u in s)if(s.hasOwnProperty(u)){if(u==n)for(var a in r)r.hasOwnProperty(a)&&(o[a]=r[a]);o[u]=s[u]}return i[e]=o},DFS:function(e,n){for(var r in e){n.call(e,r,e[r]);t.util.type(e)==="Object"&&t.languages.DFS(e[r],n)}}},highlightAll:function(e,n){var r=document.querySelectorAll('code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code');for(var i=0,s;s=r[i++];)t.highlightElement(s,e===!0,n)},highlightElement:function(r,i,s){var o,u,a=r;while(a&&!e.test(a.className))a=a.parentNode;if(a){o=(a.className.match(e)||[,""])[1];u=t.languages[o]}if(!u)return;r.className=r.className.replace(e,"").replace(/\s+/g," ")+" language-"+o;a=r.parentNode;/pre/i.test(a.nodeName)&&(a.className=a.className.replace(e,"").replace(/\s+/g," ")+" language-"+o);var f=r.textContent;if(!f)return;f=f.replace(/&/g,"&").replace(//g,">").replace(/\u00a0/g," ");var l={element:r,language:o,grammar:u,code:f};t.hooks.run("before-highlight",l);if(i&&self.Worker){var c=new Worker(t.filename);c.onmessage=function(e){l.highlightedCode=n.stringify(JSON.parse(e.data));l.element.innerHTML=l.highlightedCode;s&&s.call(l.element);t.hooks.run("after-highlight",l)};c.postMessage(JSON.stringify({language:l.language,code:l.code}))}else{l.highlightedCode=t.highlight(l.code,l.grammar);l.element.innerHTML=l.highlightedCode;s&&s.call(r);t.hooks.run("after-highlight",l)}},highlight:function(e,r){return n.stringify(t.tokenize(e,r))},tokenize:function(e,n){var r=t.Token,i=[e],s=n.rest;if(s){for(var o in s)n[o]=s[o];delete n.rest}e:for(var o in n){if(!n.hasOwnProperty(o)||!n[o])continue;var u=n[o],a=u.inside,f=!!u.lookbehind||0;u=u.pattern||u;for(var l=0;le.length)break e;if(c instanceof r)continue;u.lastIndex=0;var h=u.exec(c);if(h){f&&(f=h[1].length);var p=h.index-1+f,h=h[0].slice(f),d=h.length,v=p+d,m=c.slice(0,p+1),g=c.slice(v+1),y=[l,1];m&&y.push(m);var b=new r(o,a?t.tokenize(h,a):h);y.push(b);g&&y.push(g);Array.prototype.splice.apply(i,y)}}}return i},hooks:{all:{},add:function(e,n){var r=t.hooks.all;r[e]=r[e]||[];r[e].push(n)},run:function(e,n){var r=t.hooks.all[e];if(!r||!r.length)return;for(var i=0,s;s=r[i++];)s(n)}}},n=t.Token=function(e,t){this.type=e;this.content=t};n.stringify=function(e){if(typeof e=="string")return e;if(Object.prototype.toString.call(e)=="[object Array]"){for(var r=0;r"+i.content+""};if(!self.document){self.addEventListener("message",function(e){var n=JSON.parse(e.data),r=n.language,i=n.code;self.postMessage(JSON.stringify(t.tokenize(i,t.languages[r])));self.close()},!1);return}var r=document.getElementsByTagName("script");r=r[r.length-1];if(r){t.filename=r.src;document.addEventListener&&!r.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)}})();; 6 | Prism.languages.markup={comment:/<!--[\w\W]*?--(>|>)/g,prolog:/<\?.+?\?>/,doctype:/<!DOCTYPE.+?>/,cdata:/<!\[CDATA\[[\w\W]+?]]>/i,tag:{pattern:/<\/?[\w:-]+\s*(?:\s+[\w:-]+(?:=(?:("|')(\\?[\w\W])*?\1|\w+))?\s*)*\/?>/gi,inside:{tag:{pattern:/^<\/?[\w:-]+/i,inside:{punctuation:/^<\/?/,namespace:/^[\w-]+?:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/gi,inside:{punctuation:/=|>|"/g}},punctuation:/\/?>/g,"attr-name":{pattern:/[\w:-]+/g,inside:{namespace:/^[\w-]+?:/}}}},entity:/&#?[\da-z]{1,8};/gi};Prism.hooks.add("wrap",function(e){e.type==="entity"&&(e.attributes.title=e.content.replace(/&/,"&"))});; 7 | Prism.languages.css={comment:/\/\*[\w\W]*?\*\//g,atrule:/@[\w-]+?(\s+[^;{]+)?(?=\s*{|\s*;)/gi,url:/url\((["']?).*?\1\)/gi,selector:/[^\{\}\s][^\{\}]*(?=\s*\{)/g,property:/(\b|\B)[a-z-]+(?=\s*:)/ig,string:/("|')(\\?.)*?\1/g,important:/\B!important\b/gi,ignore:/&(lt|gt|amp);/gi,punctuation:/[\{\};:]/g};Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{style:{pattern:/(<|<)style[\w\W]*?(>|>)[\w\W]*?(<|<)\/style(>|>)/ig,inside:{tag:{pattern:/(<|<)style[\w\W]*?(>|>)|(<|<)\/style(>|>)/ig,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.css}}});; 8 | Prism.languages.clike={comment:{pattern:/(^|[^\\])(\/\*[\w\W]*?\*\/|\/\/.*?(\r?\n|$))/g,lookbehind:!0},string:/("|')(\\?.)*?\1/g,keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|catch|finally|null|break|continue)\b/g,"boolean":/\b(true|false)\b/g,number:/\b-?(0x)?\d*\.?[\da-f]+\b/g,operator:/[-+]{1,2}|!|=?<|=?>|={1,2}|(&){1,2}|\|?\||\?|\*|\//g,ignore:/&(lt|gt|amp);/gi,punctuation:/[{}[\];(),.:]/g};; 9 | Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(var|let|if|else|while|do|for|return|in|instanceof|function|new|with|typeof|try|catch|finally|null|break|continue)\b/g,number:/\b(-?(0x)?\d*\.?[\da-f]+|NaN|-?Infinity)\b/g});Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/g,lookbehind:!0}});Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/(<|<)script[\w\W]*?(>|>)[\w\W]*?(<|<)\/script(>|>)/ig,inside:{tag:{pattern:/(<|<)script[\w\W]*?(>|>)|(<|<)\/script(>|>)/ig,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.javascript}}});; 10 | -------------------------------------------------------------------------------- /test/lib/angular/jstd-scenario-adapter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.0rc1 3 | * (c) 2010-2011 AngularJS http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window) { 7 | 'use strict'; 8 | 9 | /** 10 | * JSTestDriver adapter for angular scenario tests 11 | * 12 | * Example of jsTestDriver.conf for running scenario tests with JSTD: 13 |
 14 |     server: http://localhost:9877
 15 | 
 16 |     load:
 17 |       - lib/angular-scenario.js
 18 |       - lib/jstd-scenario-adapter-config.js
 19 |       - lib/jstd-scenario-adapter.js
 20 |       # your test files go here #
 21 | 
 22 |     proxy:
 23 |      - {matcher: "/your-prefix/*", server: "http://localhost:8000/"}
 24 |   
25 | * 26 | * For more information on how to configure jstd proxy, see {@link http://code.google.com/p/js-test-driver/wiki/Proxy} 27 | * Note the order of files - it's important ! 28 | * 29 | * Example of jstd-scenario-adapter-config.js 30 |
 31 |     var jstdScenarioAdapter = {
 32 |       relativeUrlPrefix: '/your-prefix/'
 33 |     };
 34 |   
35 | * 36 | * Whenever you use browser().navigateTo('relativeUrl') in your scenario test, the relativeUrlPrefix will be prepended. 37 | * You have to configure this to work together with JSTD proxy. 38 | * 39 | * Let's assume you are using the above configuration (jsTestDriver.conf and jstd-scenario-adapter-config.js): 40 | * Now, when you call browser().navigateTo('index.html') in your scenario test, the browser will open /your-prefix/index.html. 41 | * That matches the proxy, so JSTD will proxy this request to http://localhost:8000/index.html. 42 | */ 43 | 44 | /** 45 | * Custom type of test case 46 | * 47 | * @const 48 | * @see jstestdriver.TestCaseInfo 49 | */ 50 | var SCENARIO_TYPE = 'scenario'; 51 | 52 | /** 53 | * Plugin for JSTestDriver 54 | * Connection point between scenario's jstd output and jstestdriver. 55 | * 56 | * @see jstestdriver.PluginRegistrar 57 | */ 58 | function JstdPlugin() { 59 | var nop = function() {}; 60 | 61 | this.reportResult = nop; 62 | this.reportEnd = nop; 63 | this.runScenario = nop; 64 | 65 | this.name = 'Angular Scenario Adapter'; 66 | 67 | /** 68 | * Called for each JSTD TestCase 69 | * 70 | * Handles only SCENARIO_TYPE test cases. There should be only one fake TestCase. 71 | * Runs all scenario tests (under one fake TestCase) and report all results to JSTD. 72 | * 73 | * @param {jstestdriver.TestRunConfiguration} configuration 74 | * @param {Function} onTestDone 75 | * @param {Function} onAllTestsComplete 76 | * @returns {boolean} True if this type of test is handled by this plugin, false otherwise 77 | */ 78 | this.runTestConfiguration = function(configuration, onTestDone, onAllTestsComplete) { 79 | if (configuration.getTestCaseInfo().getType() != SCENARIO_TYPE) return false; 80 | 81 | this.reportResult = onTestDone; 82 | this.reportEnd = onAllTestsComplete; 83 | this.runScenario(); 84 | 85 | return true; 86 | }; 87 | 88 | this.getTestRunsConfigurationFor = function(testCaseInfos, expressions, testRunsConfiguration) { 89 | testRunsConfiguration.push( 90 | new jstestdriver.TestRunConfiguration( 91 | new jstestdriver.TestCaseInfo( 92 | 'Angular Scenario Tests', function() {}, SCENARIO_TYPE), [])); 93 | 94 | return true; 95 | }; 96 | } 97 | 98 | /** 99 | * Singleton instance of the plugin 100 | * Accessed using closure by: 101 | * - jstd output (reports to this plugin) 102 | * - initScenarioAdapter (register the plugin to jstd) 103 | */ 104 | var plugin = new JstdPlugin(); 105 | 106 | /** 107 | * Initialise scenario jstd-adapter 108 | * (only if jstestdriver is defined) 109 | * 110 | * @param {Object} jstestdriver Undefined when run from browser (without jstd) 111 | * @param {Function} initScenarioAndRun Function that inits scenario and runs all the tests 112 | * @param {Object=} config Configuration object, supported properties: 113 | * - relativeUrlPrefix: prefix for all relative links when navigateTo() 114 | */ 115 | function initScenarioAdapter(jstestdriver, initScenarioAndRun, config) { 116 | if (jstestdriver) { 117 | // create and register ScenarioPlugin 118 | jstestdriver.pluginRegistrar.register(plugin); 119 | plugin.runScenario = initScenarioAndRun; 120 | 121 | /** 122 | * HACK (angular.scenario.Application.navigateTo) 123 | * 124 | * We need to navigate to relative urls when running from browser (without JSTD), 125 | * because we want to allow running scenario tests without creating its own virtual host. 126 | * For example: http://angular.local/build/docs/docs-scenario.html 127 | * 128 | * On the other hand, when running with JSTD, we need to navigate to absolute urls, 129 | * because of JSTD proxy. (proxy, because of same domain policy) 130 | * 131 | * So this hack is applied only if running with JSTD and change all relative urls to absolute. 132 | */ 133 | var appProto = angular.scenario.Application.prototype, 134 | navigateTo = appProto.navigateTo, 135 | relativeUrlPrefix = config && config.relativeUrlPrefix || '/'; 136 | 137 | appProto.navigateTo = function(url, loadFn, errorFn) { 138 | if (url.charAt(0) != '/' && url.charAt(0) != '#' && 139 | url != 'about:blank' && !url.match(/^https?/)) { 140 | url = relativeUrlPrefix + url; 141 | } 142 | 143 | return navigateTo.call(this, url, loadFn, errorFn); 144 | }; 145 | } 146 | } 147 | 148 | /** 149 | * Builds proper TestResult object from given model spec 150 | * 151 | * TODO(vojta) report error details 152 | * 153 | * @param {angular.scenario.ObjectModel.Spec} spec 154 | * @returns {jstestdriver.TestResult} 155 | */ 156 | function createTestResultFromSpec(spec) { 157 | var map = { 158 | success: 'PASSED', 159 | error: 'ERROR', 160 | failure: 'FAILED' 161 | }; 162 | 163 | return new jstestdriver.TestResult( 164 | spec.fullDefinitionName, 165 | spec.name, 166 | jstestdriver.TestResult.RESULT[map[spec.status]], 167 | spec.error || '', 168 | spec.line || '', 169 | spec.duration); 170 | } 171 | 172 | /** 173 | * Generates JSTD output (jstestdriver.TestResult) 174 | */ 175 | angular.scenario.output('jstd', function(context, runner, model) { 176 | model.on('SpecEnd', function(spec) { 177 | plugin.reportResult(createTestResultFromSpec(spec)); 178 | }); 179 | 180 | model.on('RunnerEnd', function() { 181 | plugin.reportEnd(); 182 | }); 183 | }); 184 | initScenarioAdapter(window.jstestdriver, angular.scenario.setUpAndRun, window.jstdScenarioAdapter); 185 | })(window); 186 | -------------------------------------------------------------------------------- /test/lib/jasmine-jstd-adapter/JasmineAdapter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Jasmine JsTestDriver Adapter. 3 | * @author misko@hevery.com (Misko Hevery) 4 | */ 5 | (function(window) { 6 | var rootDescribes = new Describes(window); 7 | rootDescribes.collectMode(); 8 | 9 | var JASMINE_TYPE = 'jasmine test case'; 10 | TestCase('Jasmine Adapter Tests', null, JASMINE_TYPE); 11 | 12 | var jasminePlugin = { 13 | name:'jasmine', 14 | 15 | getTestRunsConfigurationFor: function(testCaseInfos, expressions, testRunsConfiguration) { 16 | for (var i = 0; i < testCaseInfos.length; i++) { 17 | if (testCaseInfos[i].getType() == JASMINE_TYPE) { 18 | testRunsConfiguration.push(new jstestdriver.TestRunConfiguration(testCaseInfos[i], [])); 19 | } 20 | } 21 | return false; 22 | }, 23 | 24 | runTestConfiguration: function(testRunConfiguration, onTestDone, onTestRunConfigurationComplete) { 25 | if (testRunConfiguration.getTestCaseInfo().getType() != JASMINE_TYPE) return false; 26 | 27 | var jasmineEnv = jasmine.currentEnv_ = new jasmine.Env(); 28 | rootDescribes.playback(); 29 | var specLog = jstestdriver.console.log_ = []; 30 | var start; 31 | jasmineEnv.specFilter = function(spec) { 32 | return rootDescribes.isExclusive(spec); 33 | }; 34 | jasmineEnv.reporter = { 35 | log: function(str) { 36 | specLog.push(str); 37 | }, 38 | 39 | reportRunnerStarting: function(runner) { }, 40 | 41 | reportSpecStarting: function(spec) { 42 | specLog = jstestdriver.console.log_ = []; 43 | start = new Date().getTime(); 44 | }, 45 | 46 | reportSpecResults: function(spec) { 47 | var suite = spec.suite; 48 | var results = spec.results(); 49 | if (results.skipped) return; 50 | var end = new Date().getTime(); 51 | var messages = []; 52 | var resultItems = results.getItems(); 53 | var state = 'passed'; 54 | for ( var i = 0; i < resultItems.length; i++) { 55 | if (!resultItems[i].passed()) { 56 | state = resultItems[i].message.match(/AssertionError:/) ? 'error' : 'failed'; 57 | messages.push({ 58 | message: resultItems[i].toString(), 59 | name: resultItems[i].trace.name, 60 | stack: formatStack(resultItems[i].trace.stack) 61 | }); 62 | } 63 | } 64 | onTestDone( 65 | new jstestdriver.TestResult( 66 | suite.getFullName(), 67 | spec.description, 68 | state, 69 | jstestdriver.angular.toJson(messages), 70 | specLog.join('\n'), 71 | end - start)); 72 | }, 73 | 74 | reportSuiteResults: function(suite) {}, 75 | 76 | reportRunnerResults: function(runner) { 77 | onTestRunConfigurationComplete(); 78 | } 79 | }; 80 | jasmineEnv.execute(); 81 | return true; 82 | }, 83 | 84 | onTestsFinish: function() { 85 | jasmine.currentEnv_ = null; 86 | rootDescribes.collectMode(); 87 | } 88 | }; 89 | jstestdriver.pluginRegistrar.register(jasminePlugin); 90 | 91 | function formatStack(stack) { 92 | var lines = (stack||'').split(/\r?\n/); 93 | var frames = []; 94 | for (var i = 0; i < lines.length; i++) { 95 | if (!lines[i].match(/\/jasmine[\.-]/)) { 96 | frames.push(lines[i].replace(/https?:\/\/\w+(:\d+)?\/test\//, '').replace(/^\s*/, ' ')); 97 | } 98 | } 99 | return frames.join('\n'); 100 | } 101 | 102 | function noop() {} 103 | function Describes(window) { 104 | var describes = {}; 105 | var beforeEachs = {}; 106 | var afterEachs = {}; 107 | // Here we store: 108 | // 0: everyone runs 109 | // 1: run everything under ddescribe 110 | // 2: run only iits (ignore ddescribe) 111 | var exclusive = 0; 112 | var collectMode = true; 113 | intercept('describe', describes); 114 | intercept('xdescribe', describes); 115 | intercept('beforeEach', beforeEachs); 116 | intercept('afterEach', afterEachs); 117 | 118 | function intercept(functionName, collection) { 119 | window[functionName] = function(desc, fn) { 120 | if (collectMode) { 121 | collection[desc] = function() { 122 | jasmine.getEnv()[functionName](desc, fn); 123 | }; 124 | } else { 125 | jasmine.getEnv()[functionName](desc, fn); 126 | } 127 | }; 128 | } 129 | window.ddescribe = function(name, fn) { 130 | if (exclusive < 1) { 131 | exclusive = 1; // run ddescribe only 132 | } 133 | window.describe(name, function() { 134 | var oldIt = window.it; 135 | window.it = function(name, fn) { 136 | fn.exclusive = 1; // run anything under ddescribe 137 | jasmine.getEnv().it(name, fn); 138 | }; 139 | try { 140 | fn.call(this); 141 | } finally { 142 | window.it = oldIt; 143 | }; 144 | }); 145 | }; 146 | window.iit = function(name, fn) { 147 | exclusive = fn.exclusive = 2; // run only iits 148 | jasmine.getEnv().it(name, fn); 149 | }; 150 | 151 | 152 | this.collectMode = function() { 153 | collectMode = true; 154 | exclusive = 0; // run everything 155 | }; 156 | this.playback = function() { 157 | collectMode = false; 158 | playback(beforeEachs); 159 | playback(afterEachs); 160 | playback(describes); 161 | 162 | function playback(set) { 163 | for ( var name in set) { 164 | set[name](); 165 | } 166 | } 167 | }; 168 | 169 | this.isExclusive = function(spec) { 170 | if (exclusive) { 171 | var blocks = spec.queue.blocks; 172 | for ( var i = 0; i < blocks.length; i++) { 173 | if (blocks[i].func.exclusive >= exclusive) { 174 | return true; 175 | } 176 | } 177 | return false; 178 | } 179 | return true; 180 | }; 181 | } 182 | 183 | })(window); 184 | 185 | // Patch Jasmine for proper stack traces 186 | jasmine.Spec.prototype.fail = function (e) { 187 | var expectationResult = new jasmine.ExpectationResult({ 188 | passed: false, 189 | message: e ? jasmine.util.formatException(e) : 'Exception' 190 | }); 191 | // PATCH 192 | if (e) { 193 | expectationResult.trace = e; 194 | } 195 | this.results_.addResult(expectationResult); 196 | }; 197 | -------------------------------------------------------------------------------- /scripts/web-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var sys = require('sys'), 4 | http = require('http'), 5 | fs = require('fs'), 6 | url = require('url'), 7 | events = require('events'); 8 | 9 | var DEFAULT_PORT = 8000; 10 | 11 | function main(argv) { 12 | new HttpServer({ 13 | 'GET': createServlet(StaticServlet), 14 | 'HEAD': createServlet(StaticServlet) 15 | }).start(Number(argv[2]) || DEFAULT_PORT); 16 | } 17 | 18 | function escapeHtml(value) { 19 | return value.toString(). 20 | replace('<', '<'). 21 | replace('>', '>'). 22 | replace('"', '"'); 23 | } 24 | 25 | function createServlet(Class) { 26 | var servlet = new Class(); 27 | return servlet.handleRequest.bind(servlet); 28 | } 29 | 30 | /** 31 | * An Http server implementation that uses a map of methods to decide 32 | * action routing. 33 | * 34 | * @param {Object} Map of method => Handler function 35 | */ 36 | function HttpServer(handlers) { 37 | this.handlers = handlers; 38 | this.server = http.createServer(this.handleRequest_.bind(this)); 39 | } 40 | 41 | HttpServer.prototype.start = function(port) { 42 | this.port = port; 43 | this.server.listen(port); 44 | sys.puts('Http Server running at http://localhost:' + port + '/'); 45 | }; 46 | 47 | HttpServer.prototype.parseUrl_ = function(urlString) { 48 | var parsed = url.parse(urlString); 49 | parsed.pathname = url.resolve('/', parsed.pathname); 50 | return url.parse(url.format(parsed), true); 51 | }; 52 | 53 | HttpServer.prototype.handleRequest_ = function(req, res) { 54 | var logEntry = req.method + ' ' + req.url; 55 | if (req.headers['user-agent']) { 56 | logEntry += ' ' + req.headers['user-agent']; 57 | } 58 | sys.puts(logEntry); 59 | req.url = this.parseUrl_(req.url); 60 | var handler = this.handlers[req.method]; 61 | if (!handler) { 62 | res.writeHead(501); 63 | res.end(); 64 | } else { 65 | handler.call(this, req, res); 66 | } 67 | }; 68 | 69 | /** 70 | * Handles static content. 71 | */ 72 | function StaticServlet() {} 73 | 74 | StaticServlet.MimeMap = { 75 | 'txt': 'text/plain', 76 | 'html': 'text/html', 77 | 'css': 'text/css', 78 | 'xml': 'application/xml', 79 | 'json': 'application/json', 80 | 'js': 'application/javascript', 81 | 'jpg': 'image/jpeg', 82 | 'jpeg': 'image/jpeg', 83 | 'gif': 'image/gif', 84 | 'png': 'image/png' 85 | }; 86 | 87 | StaticServlet.prototype.handleRequest = function(req, res) { 88 | var self = this; 89 | var path = ('./' + req.url.pathname).replace('//','/').replace(/%(..)/, function(match, hex){ 90 | return String.fromCharCode(parseInt(hex, 16)); 91 | }); 92 | var parts = path.split('/'); 93 | if (parts[parts.length-1].charAt(0) === '.') 94 | return self.sendForbidden_(req, res, path); 95 | fs.stat(path, function(err, stat) { 96 | if (err) 97 | return self.sendMissing_(req, res, path); 98 | if (stat.isDirectory()) 99 | return self.sendDirectory_(req, res, path); 100 | return self.sendFile_(req, res, path); 101 | }); 102 | } 103 | 104 | StaticServlet.prototype.sendError_ = function(req, res, error) { 105 | res.writeHead(500, { 106 | 'Content-Type': 'text/html' 107 | }); 108 | res.write('\n'); 109 | res.write('Internal Server Error\n'); 110 | res.write('

Internal Server Error

'); 111 | res.write('
' + escapeHtml(sys.inspect(error)) + '
'); 112 | sys.puts('500 Internal Server Error'); 113 | sys.puts(sys.inspect(error)); 114 | }; 115 | 116 | StaticServlet.prototype.sendMissing_ = function(req, res, path) { 117 | path = path.substring(1); 118 | res.writeHead(404, { 119 | 'Content-Type': 'text/html' 120 | }); 121 | res.write('\n'); 122 | res.write('404 Not Found\n'); 123 | res.write('

Not Found

'); 124 | res.write( 125 | '

The requested URL ' + 126 | escapeHtml(path) + 127 | ' was not found on this server.

' 128 | ); 129 | res.end(); 130 | sys.puts('404 Not Found: ' + path); 131 | }; 132 | 133 | StaticServlet.prototype.sendForbidden_ = function(req, res, path) { 134 | path = path.substring(1); 135 | res.writeHead(403, { 136 | 'Content-Type': 'text/html' 137 | }); 138 | res.write('\n'); 139 | res.write('403 Forbidden\n'); 140 | res.write('

Forbidden

'); 141 | res.write( 142 | '

You do not have permission to access ' + 143 | escapeHtml(path) + ' on this server.

' 144 | ); 145 | res.end(); 146 | sys.puts('403 Forbidden: ' + path); 147 | }; 148 | 149 | StaticServlet.prototype.sendRedirect_ = function(req, res, redirectUrl) { 150 | res.writeHead(301, { 151 | 'Content-Type': 'text/html', 152 | 'Location': redirectUrl 153 | }); 154 | res.write('\n'); 155 | res.write('301 Moved Permanently\n'); 156 | res.write('

Moved Permanently

'); 157 | res.write( 158 | '

The document has moved here.

' 161 | ); 162 | res.end(); 163 | sys.puts('301 Moved Permanently: ' + redirectUrl); 164 | }; 165 | 166 | StaticServlet.prototype.sendFile_ = function(req, res, path) { 167 | var self = this; 168 | var file = fs.createReadStream(path); 169 | res.writeHead(200, { 170 | 'Content-Type': StaticServlet. 171 | MimeMap[path.split('.').pop()] || 'text/plain' 172 | }); 173 | if (req.method === 'HEAD') { 174 | res.end(); 175 | } else { 176 | file.on('data', res.write.bind(res)); 177 | file.on('close', function() { 178 | res.end(); 179 | }); 180 | file.on('error', function(error) { 181 | self.sendError_(req, res, error); 182 | }); 183 | } 184 | }; 185 | 186 | StaticServlet.prototype.sendDirectory_ = function(req, res, path) { 187 | var self = this; 188 | if (path.match(/[^\/]$/)) { 189 | req.url.pathname += '/'; 190 | var redirectUrl = url.format(url.parse(url.format(req.url))); 191 | return self.sendRedirect_(req, res, redirectUrl); 192 | } 193 | fs.readdir(path, function(err, files) { 194 | if (err) 195 | return self.sendError_(req, res, error); 196 | 197 | if (!files.length) 198 | return self.writeDirectoryIndex_(req, res, path, []); 199 | 200 | var remaining = files.length; 201 | files.forEach(function(fileName, index) { 202 | fs.stat(path + '/' + fileName, function(err, stat) { 203 | if (err) 204 | return self.sendError_(req, res, err); 205 | if (stat.isDirectory()) { 206 | files[index] = fileName + '/'; 207 | } 208 | if (!(--remaining)) 209 | return self.writeDirectoryIndex_(req, res, path, files); 210 | }); 211 | }); 212 | }); 213 | }; 214 | 215 | StaticServlet.prototype.writeDirectoryIndex_ = function(req, res, path, files) { 216 | path = path.substring(1); 217 | res.writeHead(200, { 218 | 'Content-Type': 'text/html' 219 | }); 220 | if (req.method === 'HEAD') { 221 | res.end(); 222 | return; 223 | } 224 | res.write('\n'); 225 | res.write('' + escapeHtml(path) + '\n'); 226 | res.write('\n'); 229 | res.write('

Directory: ' + escapeHtml(path) + '

'); 230 | res.write('
    '); 231 | files.forEach(function(fileName) { 232 | if (fileName.charAt(0) !== '.') { 233 | res.write('
  1. ' + 235 | escapeHtml(fileName) + '
  2. '); 236 | } 237 | }); 238 | res.write('
'); 239 | res.end(); 240 | }; 241 | 242 | // Must be last, 243 | main(process.argv); 244 | -------------------------------------------------------------------------------- /test/lib/jasmine/jasmine-html.js: -------------------------------------------------------------------------------- 1 | jasmine.TrivialReporter = function(doc) { 2 | this.document = doc || document; 3 | this.suiteDivs = {}; 4 | this.logRunningSpecs = false; 5 | }; 6 | 7 | jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) { 8 | var el = document.createElement(type); 9 | 10 | for (var i = 2; i < arguments.length; i++) { 11 | var child = arguments[i]; 12 | 13 | if (typeof child === 'string') { 14 | el.appendChild(document.createTextNode(child)); 15 | } else { 16 | if (child) { el.appendChild(child); } 17 | } 18 | } 19 | 20 | for (var attr in attrs) { 21 | if (attr == "className") { 22 | el[attr] = attrs[attr]; 23 | } else { 24 | el.setAttribute(attr, attrs[attr]); 25 | } 26 | } 27 | 28 | return el; 29 | }; 30 | 31 | jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) { 32 | var showPassed, showSkipped; 33 | 34 | this.outerDiv = this.createDom('div', { className: 'jasmine_reporter' }, 35 | this.createDom('div', { className: 'banner' }, 36 | this.createDom('div', { className: 'logo' }, 37 | this.createDom('span', { className: 'title' }, "Jasmine"), 38 | this.createDom('span', { className: 'version' }, runner.env.versionString())), 39 | this.createDom('div', { className: 'options' }, 40 | "Show ", 41 | showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }), 42 | this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "), 43 | showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }), 44 | this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped") 45 | ) 46 | ), 47 | 48 | this.runnerDiv = this.createDom('div', { className: 'runner running' }, 49 | this.createDom('a', { className: 'run_spec', href: '?' }, "run all"), 50 | this.runnerMessageSpan = this.createDom('span', {}, "Running..."), 51 | this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, "")) 52 | ); 53 | 54 | this.document.body.appendChild(this.outerDiv); 55 | 56 | var suites = runner.suites(); 57 | for (var i = 0; i < suites.length; i++) { 58 | var suite = suites[i]; 59 | var suiteDiv = this.createDom('div', { className: 'suite' }, 60 | this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"), 61 | this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description)); 62 | this.suiteDivs[suite.id] = suiteDiv; 63 | var parentDiv = this.outerDiv; 64 | if (suite.parentSuite) { 65 | parentDiv = this.suiteDivs[suite.parentSuite.id]; 66 | } 67 | parentDiv.appendChild(suiteDiv); 68 | } 69 | 70 | this.startedAt = new Date(); 71 | 72 | var self = this; 73 | showPassed.onclick = function(evt) { 74 | if (showPassed.checked) { 75 | self.outerDiv.className += ' show-passed'; 76 | } else { 77 | self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, ''); 78 | } 79 | }; 80 | 81 | showSkipped.onclick = function(evt) { 82 | if (showSkipped.checked) { 83 | self.outerDiv.className += ' show-skipped'; 84 | } else { 85 | self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, ''); 86 | } 87 | }; 88 | }; 89 | 90 | jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) { 91 | var results = runner.results(); 92 | var className = (results.failedCount > 0) ? "runner failed" : "runner passed"; 93 | this.runnerDiv.setAttribute("class", className); 94 | //do it twice for IE 95 | this.runnerDiv.setAttribute("className", className); 96 | var specs = runner.specs(); 97 | var specCount = 0; 98 | for (var i = 0; i < specs.length; i++) { 99 | if (this.specFilter(specs[i])) { 100 | specCount++; 101 | } 102 | } 103 | var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s"); 104 | message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"; 105 | this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild); 106 | 107 | this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString())); 108 | }; 109 | 110 | jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) { 111 | var results = suite.results(); 112 | var status = results.passed() ? 'passed' : 'failed'; 113 | if (results.totalCount === 0) { // todo: change this to check results.skipped 114 | status = 'skipped'; 115 | } 116 | this.suiteDivs[suite.id].className += " " + status; 117 | }; 118 | 119 | jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) { 120 | if (this.logRunningSpecs) { 121 | this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); 122 | } 123 | }; 124 | 125 | jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) { 126 | var results = spec.results(); 127 | var status = results.passed() ? 'passed' : 'failed'; 128 | if (results.skipped) { 129 | status = 'skipped'; 130 | } 131 | var specDiv = this.createDom('div', { className: 'spec ' + status }, 132 | this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"), 133 | this.createDom('a', { 134 | className: 'description', 135 | href: '?spec=' + encodeURIComponent(spec.getFullName()), 136 | title: spec.getFullName() 137 | }, spec.description)); 138 | 139 | 140 | var resultItems = results.getItems(); 141 | var messagesDiv = this.createDom('div', { className: 'messages' }); 142 | for (var i = 0; i < resultItems.length; i++) { 143 | var result = resultItems[i]; 144 | 145 | if (result.type == 'log') { 146 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); 147 | } else if (result.type == 'expect' && result.passed && !result.passed()) { 148 | messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); 149 | 150 | if (result.trace.stack) { 151 | messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); 152 | } 153 | } 154 | } 155 | 156 | if (messagesDiv.childNodes.length > 0) { 157 | specDiv.appendChild(messagesDiv); 158 | } 159 | 160 | this.suiteDivs[spec.suite.id].appendChild(specDiv); 161 | }; 162 | 163 | jasmine.TrivialReporter.prototype.log = function() { 164 | var console = jasmine.getGlobal().console; 165 | if (console && console.log) { 166 | if (console.log.apply) { 167 | console.log.apply(console, arguments); 168 | } else { 169 | console.log(arguments); // ie fix: console.log.apply doesn't exist on ie 170 | } 171 | } 172 | }; 173 | 174 | jasmine.TrivialReporter.prototype.getLocation = function() { 175 | return this.document.location; 176 | }; 177 | 178 | jasmine.TrivialReporter.prototype.specFilter = function(spec) { 179 | var paramMap = {}; 180 | var params = this.getLocation().search.substring(1).split('&'); 181 | for (var i = 0; i < params.length; i++) { 182 | var p = params[i].split('='); 183 | paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); 184 | } 185 | 186 | if (!paramMap.spec) { 187 | return true; 188 | } 189 | return spec.getFullName().indexOf(paramMap.spec) === 0; 190 | }; 191 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | var _ = require('underscore')._; 6 | 7 | var env = process.env.NODE_ENV || 'development'; 8 | 9 | var express = require('express'), 10 | fs = require('fs'); 11 | 12 | var app = module.exports = express.createServer(); 13 | var io = require('socket.io').listen(app); 14 | var lobbyClass = require('./lib/lobby.js'); 15 | var config = require('./config.js')[env]; 16 | var path = require('path'); 17 | 18 | var gzippo = require('gzippo'); 19 | 20 | var lobby = new lobbyClass.Lobby(io); 21 | 22 | var statsConnectionCount = 0; 23 | var statsDisconnectCount = 0; 24 | var statsSocketCount = 0; 25 | var statsSocketMessagesReceived = 0; 26 | 27 | // Configuration 28 | 29 | // Set the CDN options 30 | var options = { 31 | publicDir : path.join(__dirname, 'app') 32 | , viewsDir : path.join(__dirname, 'app') 33 | , domain : 'dkb4nwmyziz71.cloudfront.net' 34 | , bucket : 'hatchetapp' 35 | , key : 'AKIAIS3XCFXFKWXGKK7Q' 36 | , secret : '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' 37 | , hostname : config.hostname 38 | , port : config.port 39 | , ssl : false 40 | , production : config.packAssets 41 | }; 42 | 43 | // Initialize the CDN magic 44 | var CDN = require('express-cdn')(app, options); 45 | 46 | app.configure(function(){ 47 | app.set('views', __dirname + '/app'); 48 | app.set('view engine', 'ejs'); 49 | app.set('view options', { 50 | layout: false 51 | }); 52 | app.use(express.logger()); 53 | app.use(express.bodyParser()); 54 | app.use(express.methodOverride()); 55 | app.use(express.staticCache()); 56 | }); 57 | 58 | app.configure('development', function(){ 59 | app.use(express.static(__dirname + '/app')); 60 | app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 61 | }); 62 | 63 | app.configure('production', function(){ 64 | var oneDay = 86400000; 65 | // app.use(assetsManagerMiddleware); 66 | app.use(gzippo.staticGzip(__dirname + '/app')); 67 | app.use(express.errorHandler()); 68 | }); 69 | 70 | // Add the dynamic view helper 71 | app.dynamicHelpers({ CDN: CDN }); 72 | 73 | app.get('/', function(req, res) { 74 | res.render('index.ejs'); 75 | }); 76 | 77 | app.get('/debug_state', function(req, res) { 78 | res.json({ 79 | "stats": { 80 | "connectionCount": statsConnectionCount, 81 | "disconnectCount": statsDisconnectCount, 82 | "currentSocketCount": statsSocketCount, 83 | "socketMessagesReceived": statsSocketMessagesReceived 84 | }, 85 | "rooms": _.map(lobby.rooms, function(room, key) { return room.json() } ) 86 | }); 87 | }); 88 | 89 | app.get('/styleguide', function(req, res) { 90 | res.render('styleguide.ejs'); 91 | }); 92 | 93 | app.get('/:id', function(req, res) { 94 | if (req.params.id in lobby.rooms) { 95 | res.render('index.ejs'); 96 | } else { 97 | res.redirect('/'); 98 | } 99 | }); 100 | 101 | 102 | io.configure(function () { 103 | io.set('transports', ['websocket', 'htmlfile', 'xhr-polling', 'jsonp-polling']); 104 | }); 105 | 106 | io.configure('production', function(){ 107 | io.enable('browser client minification'); 108 | io.enable('browser client etag'); 109 | io.enable('browser client gzip'); 110 | io.set("polling duration", 10); 111 | io.set('log level', 1); 112 | }); 113 | io.configure('development', function(){ 114 | io.set('log level', 2); 115 | }); 116 | 117 | var port = process.env.app_port || 5000; // Use the port that Heroku provides or default to 5000 118 | app.listen(port, function() { 119 | console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env); 120 | }); 121 | 122 | 123 | 124 | 125 | /* EVENT LISTENERS */ 126 | 127 | io.sockets.on('connection', function (socket) { 128 | 129 | statsConnectionCount++; 130 | statsSocketCount++; 131 | 132 | // console.log("On connect", socket.id); 133 | 134 | socket.on('disconnect', function () { 135 | statsDisconnectCount++; 136 | statsSocketCount--; 137 | // console.log("On disconnect", socket.id); 138 | lobby.broadcastDisconnect(socket); 139 | }); 140 | 141 | socket.on('create room', function (data, callback) { 142 | statsSocketMessagesReceived++; 143 | // console.log("on create room", socket.id, data); 144 | callback(lobby.createRoom()); 145 | }); 146 | 147 | socket.on('join room', function (data, callback) { 148 | statsSocketMessagesReceived++; 149 | // console.log("on join room " + data.roomUrl, socket.id, data); 150 | var room = lobby.joinRoom(socket, data); 151 | if(room.error) { 152 | callback( { error: room.error } ); 153 | } else { 154 | callback(room.info()); 155 | } 156 | }); 157 | 158 | socket.on('room info', function (data, callback) { 159 | statsSocketMessagesReceived++; 160 | // console.log("on room info for " + data.roomUrl, socket.id, data); 161 | var room = lobby.getRoom(data.roomUrl); 162 | // room = { error: "there was an error" }; 163 | if (room.error) { 164 | callback( { error: room.error } ); 165 | } else { 166 | callback(room.info()); 167 | } 168 | }); 169 | 170 | socket.on('set card pack', function (data, cardPack) { 171 | statsSocketMessagesReceived++; 172 | // console.log("on set card pack " + data.cardPack + " for " + data.roomUrl, socket.id, data); 173 | var room = lobby.getRoom(data.roomUrl); 174 | // console.log("error=" + room.error); 175 | if (!room.error) { 176 | room.setCardPack(data); 177 | } 178 | }); 179 | 180 | socket.on('vote', function (data, callback) { 181 | statsSocketMessagesReceived++; 182 | // console.log("on vote " + data.vote + " received for " + data.roomUrl, socket.id, data); 183 | var room = lobby.getRoom(data.roomUrl); 184 | if (room.error) { 185 | callback( { error: room.error }); 186 | } else { 187 | room.recordVote(socket, data); 188 | callback( {} ); 189 | } 190 | }); 191 | 192 | socket.on('unvote', function (data, callback) { 193 | statsSocketMessagesReceived++; 194 | // console.log("omn unvote received for " + data.roomUrl, socket.id, data); 195 | var room = lobby.getRoom(data.roomUrl); 196 | if (room.error) { 197 | callback( { error: room.error }); 198 | } else { 199 | room.destroyVote(socket, data); 200 | callback( {} ); 201 | } 202 | }); 203 | 204 | socket.on('reset vote', function (data, callback) { 205 | statsSocketMessagesReceived++; 206 | // console.log("on reset vote received for " + data.roomUrl, socket.id, data); 207 | var room = lobby.getRoom(data.roomUrl); 208 | if (room.error) { 209 | callback( { error: room.error }); 210 | } else { 211 | room.resetVote(); 212 | callback( {} ); 213 | } 214 | }); 215 | 216 | socket.on('force reveal', function (data, callback) { 217 | statsSocketMessagesReceived++; 218 | var room = lobby.getRoom(data.roomUrl); 219 | if (room.error) { 220 | callback( { error: room.error }); 221 | } else { 222 | room.forceReveal(); 223 | callback( {} ); 224 | } 225 | }); 226 | 227 | socket.on('sort votes', function (data, callback) { 228 | statsSocketMessagesReceived++; 229 | var room = lobby.getRoom(data.roomUrl); 230 | if (room.error) { 231 | callback( { error: room.error }); 232 | } else { 233 | room.sortVotes(); 234 | callback( {} ); 235 | } 236 | }); 237 | 238 | socket.on('toggle voter', function (data, callback) { 239 | statsSocketMessagesReceived++; 240 | // console.log("on toggle voter for " + data.roomUrl, socket.id, data); 241 | var room = lobby.getRoom(data.roomUrl); 242 | if (room.error) { 243 | callback( { error: room.error }); 244 | } else { 245 | room.toggleVoter(data); 246 | callback( {} ); 247 | } 248 | }); 249 | 250 | }); -------------------------------------------------------------------------------- /app/lib/modernizr.custom.07116.js: -------------------------------------------------------------------------------- 1 | /* Modernizr 2.6.2 (Custom Build) | MIT & BSD 2 | * Build: http://modernizr.com/download/#-generatedcontent-csstransforms-csstransforms3d-csstransitions-websockets-svg-printshiv-mq-cssclasses-addtest-teststyles-testprop-testallprops-prefixes-domprefixes-load 3 | */ 4 | ;window.Modernizr=function(a,b,c){function C(a){j.cssText=a}function D(a,b){return C(n.join(a+";")+(b||""))}function E(a,b){return typeof a===b}function F(a,b){return!!~(""+a).indexOf(b)}function G(a,b){for(var d in a){var e=a[d];if(!F(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function H(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:E(f,"function")?f.bind(d||b):f}return!1}function I(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+p.join(d+" ")+d).split(" ");return E(b,"string")||E(b,"undefined")?G(e,b):(e=(a+" "+q.join(d+" ")+d).split(" "),H(e,b,c))}var d="2.6.2",e={},f=!0,g=b.documentElement,h="modernizr",i=b.createElement(h),j=i.style,k,l=":)",m={}.toString,n=" -webkit- -moz- -o- -ms- ".split(" "),o="Webkit Moz O ms",p=o.split(" "),q=o.toLowerCase().split(" "),r={svg:"http://www.w3.org/2000/svg"},s={},t={},u={},v=[],w=v.slice,x,y=function(a,c,d,e){var f,i,j,k,l=b.createElement("div"),m=b.body,n=m||b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:h+(d+1),l.appendChild(j);return f=["­",'"].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},z=function(b){var c=a.matchMedia||a.msMatchMedia;if(c)return c(b).matches;var d;return y("@media "+b+" { #"+h+" { position: absolute; } }",function(b){d=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle)["position"]=="absolute"}),d},A={}.hasOwnProperty,B;!E(A,"undefined")&&!E(A.call,"undefined")?B=function(a,b){return A.call(a,b)}:B=function(a,b){return b in a&&E(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=w.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(w.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(w.call(arguments)))};return e}),s.websockets=function(){return"WebSocket"in a||"MozWebSocket"in a},s.csstransforms=function(){return!!I("transform")},s.csstransforms3d=function(){var a=!!I("perspective");return a&&"webkitPerspective"in g.style&&y("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(b,c){a=b.offsetLeft===9&&b.offsetHeight===3}),a},s.csstransitions=function(){return I("transition")},s.generatedcontent=function(){var a;return y(["#",h,"{font:0/0 a}#",h,':after{content:"',l,'";visibility:hidden;font:3px/1 a}'].join(""),function(b){a=b.offsetHeight>=3}),a},s.svg=function(){return!!b.createElementNS&&!!b.createElementNS(r.svg,"svg").createSVGRect};for(var J in s)B(s,J)&&(x=J.toLowerCase(),e[x]=s[J](),v.push((e[x]?"":"no-")+x));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)B(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},C(""),i=k=null,e._version=d,e._prefixes=n,e._domPrefixes=q,e._cssomPrefixes=p,e.mq=z,e.testProp=function(a){return G([a])},e.testAllProps=I,e.testStyles=y,g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+v.join(" "):""),e}(this,this.document),function(a,b){function k(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function l(){var a=r.elements;return typeof a=="string"?a.split(" "):a}function m(a){var b=i[a[g]];return b||(b={},h++,a[g]=h,i[h]=b),b}function n(a,c,f){c||(c=b);if(j)return c.createElement(a);f||(f=m(c));var g;return f.cache[a]?g=f.cache[a].cloneNode():e.test(a)?g=(f.cache[a]=f.createElem(a)).cloneNode():g=f.createElem(a),g.canHaveChildren&&!d.test(a)?f.frag.appendChild(g):g}function o(a,c){a||(a=b);if(j)return a.createDocumentFragment();c=c||m(a);var d=c.frag.cloneNode(),e=0,f=l(),g=f.length;for(;e+~])("+l().join("|")+")(?=[[\\s,>+~#.:]|$)","gi"),f="$1"+t+"\\:$2";while(d--)b=c[d]=c[d].split("}"),b[b.length-1]=b[b.length-1].replace(e,f),c[d]=b.join("}");return c.join("{")}function y(a){var b=a.length;while(b--)a[b].removeNode()}function z(a){function g(){clearTimeout(d._removeSheetTimer),b&&b.removeNode(!0),b=null}var b,c,d=m(a),e=a.namespaces,f=a.parentWindow;return!u||a.printShived?a:(typeof e[t]=="undefined"&&e.add(t),f.attachEvent("onbeforeprint",function(){g();var d,e,f,h=a.styleSheets,i=[],j=h.length,l=Array(j);while(j--)l[j]=h[j];while(f=l.pop())if(!f.disabled&&s.test(f.media)){try{d=f.imports,e=d.length}catch(m){e=0}for(j=0;j",f="hidden"in a,j=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){f=!0,j=!0}})();var r={elements:c.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",shivCSS:c.shivCSS!==!1,supportsUnknownElements:j,shivMethods:c.shivMethods!==!1,type:"default",shivDocument:q,createElement:n,createDocumentFragment:o};a.html5=r,q(b);var s=/^$|\b(?:all|print)\b/,t="html5shiv",u=!j&&function(){var c=b.documentElement;return typeof b.namespaces!="undefined"&&typeof b.parentWindow!="undefined"&&typeof c.applyElement!="undefined"&&typeof c.removeNode!="undefined"&&typeof a.attachEvent!="undefined"}();r.type+=" print",r.shivPrint=z,z(b)}(this,document),function(a,b,c){function d(a){return"[object Function]"==o.call(a)}function e(a){return"string"==typeof a}function f(){}function g(a){return!a||"loaded"==a||"complete"==a||"uninitialized"==a}function h(){var a=p.shift();q=1,a?a.t?m(function(){("c"==a.t?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&&g(l.readyState)&&(u.r=o=1,!q&&h(),l.onload=l.onreadystatechange=null,b)){"img"!=a&&m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&&y[c][d].onload()}}var j=j||B.errorTimeout,l=b.createElement(a),o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};1===y[c]&&(r=1,y[c]=[]),"object"==a?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),"img"!=a&&(r||2===y[c]?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i("c"==b?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),1==p.length&&h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=s?l:n.parentNode,l=a.opera&&"[object Opera]"==o.call(a.opera),l=!!b.attachEvent&&!l,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return"[object Array]"==o.call(a)},x=[],y={},z={timeout:function(a,b){return b.length&&(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;f2;a==null&&(a=[]);if(A&& 12 | a.reduce===A){e&&(c=b.bind(c,e));return f?a.reduce(c,d):a.reduce(c)}j(a,function(a,b,i){if(f)d=c.call(e,d,a,b,i);else{d=a;f=true}});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(B&&a.reduceRight===B){e&&(c=b.bind(c,e));return f?a.reduceRight(c,d):a.reduceRight(c)}var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect=function(a, 13 | c,b){var e;G(a,function(a,g,h){if(c.call(b,a,g,h)){e=a;return true}});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(C&&a.filter===C)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(D&&a.every===D)return a.every(c,b);j(a,function(a,g,h){if(!(e=e&&c.call(b, 14 | a,g,h)))return o});return!!e};var G=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(E&&a.some===E)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return o});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;if(q&&a.indexOf===q)return a.indexOf(c)!=-1;return b=G(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck= 15 | function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;bd?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]}; 17 | j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e>1;d(a[g])=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1),true);return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a= 20 | i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=L||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&& 25 | c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.pick=function(a){var c={};j(b.flatten(i.call(arguments,1)),function(b){b in a&&(c[b]=a[b])});return c};b.defaults=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return r(a,b,[])};b.isEmpty= 26 | function(a){if(a==null)return true;if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=p||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)};b.isArguments=function(a){return l.call(a)=="[object Arguments]"};b.isArguments(arguments)||(b.isArguments=function(a){return!(!a||!b.has(a,"callee"))});b.isFunction=function(a){return l.call(a)=="[object Function]"}; 27 | b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isFinite=function(a){return b.isNumber(a)&&isFinite(a)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"};b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a, 28 | b){return K.call(a,b)};b.noConflict=function(){s._=I;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};b.result=function(a,c){if(a==null)return null;var d=a[c];return b.isFunction(d)?d.call(a):d};b.mixin=function(a){j(b.functions(a),function(c){M(c,b[c]=a[c])})};var N=0;b.uniqueId= 29 | function(a){var b=N++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var u=/.^/,n={"\\":"\\","'":"'",r:"\r",n:"\n",t:"\t",u2028:"\u2028",u2029:"\u2029"},v;for(v in n)n[n[v]]=v;var O=/\\|'|\r|\n|\t|\u2028|\u2029/g,P=/\\(\\|'|r|n|t|u2028|u2029)/g,w=function(a){return a.replace(P,function(a,b){return n[b]})};b.template=function(a,c,d){d=b.defaults(d||{},b.templateSettings);a="__p+='"+a.replace(O,function(a){return"\\"+n[a]}).replace(d.escape|| 30 | u,function(a,b){return"'+\n_.escape("+w(b)+")+\n'"}).replace(d.interpolate||u,function(a,b){return"'+\n("+w(b)+")+\n'"}).replace(d.evaluate||u,function(a,b){return"';\n"+w(b)+"\n;__p+='"})+"';\n";d.variable||(a="with(obj||{}){\n"+a+"}\n");var a="var __p='';var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n"+a+"return __p;\n",e=new Function(d.variable||"obj","_",a);if(c)return e(c,b);c=function(a){return e.call(this,a,b)};c.source="function("+(d.variable||"obj")+"){\n"+a+"}";return c}; 31 | b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var x=function(a,c){return c?b(a).chain():a},M=function(a,c){m.prototype[a]=function(){var a=i.call(arguments);J.call(a,this._wrapped);return x(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return x(d, 32 | this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return x(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain=true;return this};m.prototype.value=function(){return this._wrapped}}).call(this); -------------------------------------------------------------------------------- /app/js/controllers.js: -------------------------------------------------------------------------------- 1 | /*jslint indent: 2, browser: true */ 2 | /*global angular, _, $, ScrollIntoView, DropDown */ 3 | 4 | 'use strict'; 5 | 6 | /* Controllers */ 7 | 8 | function MainCtrl($scope, $timeout) { 9 | $scope.logoState = ''; 10 | $scope.errorMessage = null; 11 | $scope.message = null; 12 | 13 | $scope.$on('$routeChangeSuccess', function () { 14 | $scope.logoState = ''; 15 | $scope.bodyState = ''; 16 | }); 17 | $scope.$on('unanimous vote', function () { 18 | $scope.logoState = ' header__logo--green'; 19 | $scope.bodyState = ' body--green'; 20 | }); 21 | $scope.$on('not unanimous vote', function () { 22 | $scope.logoState = ' header__logo--yellow'; 23 | $scope.bodyState = ' body--yellow'; 24 | }); 25 | $scope.$on('problem vote', function () { 26 | $scope.logoState = ' header__logo--red'; 27 | $scope.bodyState = ' body--red'; 28 | }); 29 | $scope.$on('unfinished vote', function () { 30 | $scope.logoState = ''; 31 | $scope.bodyState = ''; 32 | }); 33 | 34 | $scope.$on('show message', function (evnt, msg) { 35 | $scope.message = msg; 36 | $timeout(function () { 37 | $scope.message = null; 38 | }, 4000); 39 | }); 40 | $scope.$on('show error', function (evnt, msg) { 41 | $scope.errorMessage = msg; 42 | $timeout(function () { 43 | $scope.errorMessage = null; 44 | }, 3000); 45 | }); 46 | } 47 | 48 | MainCtrl.$inject = ['$scope', '$timeout']; 49 | 50 | function LobbyCtrl($scope, $location, socket) { 51 | $scope.disableButtons = false; 52 | $scope.createRoom = function () { 53 | // console.log('createRoom: emit create room'); 54 | $scope.disableButtons = true; 55 | socket.emit('create room', {}, function (roomUrl) { 56 | $location.path(roomUrl); 57 | }); 58 | }; 59 | $scope.enterRoom = function (room) { 60 | // console.log('enterRoom: room info'); 61 | $scope.disableButtons = true; 62 | socket.emit('room info', { roomUrl: room }, function (response) { 63 | if (response.error) { 64 | $scope.disableButtons = false; 65 | $scope.$emit('show error', response.error); 66 | } else { 67 | // console.log("going to enter room " + response.roomUrl); 68 | $location.path(response.roomUrl); 69 | } 70 | }); 71 | }; 72 | } 73 | 74 | LobbyCtrl.$inject = ['$scope', '$location', 'socket']; 75 | 76 | function standardDeviation(values, avg){ 77 | var squareDiffs = values.map(function(value){ 78 | var diff = value - avg; 79 | var sqrDiff = diff * diff; 80 | return sqrDiff; 81 | }); 82 | 83 | var avgSquareDiff = average(squareDiffs); 84 | 85 | var stdDev = Math.sqrt(avgSquareDiff); 86 | return stdDev; 87 | } 88 | 89 | function average(data){ 90 | var sum = data.reduce(function(sum, value){ 91 | return sum + value; 92 | }, 0); 93 | 94 | var avg = sum / data.length; 95 | return avg; 96 | } 97 | 98 | function cardValue(vote){ 99 | if (vote.match(/^[0-9]+$/)) { 100 | return parseFloat(vote); 101 | } else if (vote == '\u00BD') { 102 | return 0.5; 103 | } else if (vote == 'A\u2660') { 104 | return 1; 105 | } else if (vote == '\u2654') { 106 | return 13; 107 | } 108 | } 109 | 110 | function RoomCtrl($scope, $routeParams, $timeout, socket) { 111 | 112 | var processMessage = function (response, process) { 113 | // console.log("processMessage: response:", response) 114 | if (response.error) { 115 | $scope.$emit('show error', response.error); 116 | } else { 117 | (process || angular.noop)(response); 118 | } 119 | }; 120 | 121 | var sumOfTwo = function (a, b) { 122 | return a + b; 123 | }; 124 | 125 | // wipe out vote if voting state is not yet finished to prevent cheating. 126 | // if it has already been set - use the actual vote. This works for unvoting - so that 127 | // before the flip occurs - we don't display 'oi' 128 | var processVotes = function () { 129 | var voteCount = $scope.votes.length; 130 | _.each($scope.votes, function (v) { 131 | v.visibleVote = v.visibleVote === undefined && (!$scope.forcedReveal && voteCount < $scope.voterCount) ? 'oi!' : v.vote; 132 | }); 133 | var voteArr = []; 134 | voteArr.length = $scope.voterCount - voteCount; 135 | $scope.placeholderVotes = voteArr; 136 | 137 | var cardValues = _.filter(_.map(_.pluck($scope.votes, 'vote'), cardValue), _.isNumber); 138 | $scope.votingAverage = average(cardValues); 139 | $scope.votingStandardDeviation = standardDeviation(cardValues, $scope.votingAverage).toFixed(2); 140 | $scope.showAverage = voteArr.length === 0 && cardValues.length > 0; 141 | 142 | $scope.forceRevealDisable = (!$scope.forcedReveal && ($scope.votes.length < $scope.voterCount || $scope.voterCount === 0)) ? false : true; 143 | console.log("forceRevealDisable", $scope.forceRevealDisable) 144 | console.log("alreadySorted;", $scope.alreadySorted) 145 | $scope.sortVotesDisable = !$scope.forceRevealDisable || $scope.alreadySorted; 146 | console.log("sortVotesDisable", $scope.sortVotesDisable) 147 | 148 | if ($scope.votes.length === $scope.voterCount || $scope.forcedReveal) { 149 | if ($scope.alreadySorted) { 150 | $scope.votes = $scope.votes.sort(function(el1, el2) { 151 | return $scope.cards.indexOf(el1.vote) - $scope.cards.indexOf(el2.vote); 152 | }); 153 | } 154 | 155 | var uniqVotes = _.chain($scope.votes).pluck('vote').uniq().value().length; 156 | if (uniqVotes === 1) { 157 | $scope.$emit('unanimous vote'); 158 | } else if (uniqVotes === $scope.voterCount) { 159 | $scope.$emit('problem vote'); 160 | } else if ($scope.voterCount > 3 && uniqVotes === ($scope.voterCount - 1)) { 161 | $scope.$emit('problem vote'); 162 | } else { 163 | $scope.$emit('not unanimous vote'); 164 | } 165 | } else { 166 | $scope.$emit('unfinished vote'); 167 | } 168 | }; 169 | 170 | var myConnectionHash = function () { 171 | return _.find($scope.connections, function (c) { return c.sessionId === $scope.sessionId; }); 172 | }; 173 | 174 | var myVoteHash = function () { 175 | return _.find($scope.votes, function (c) { return c.sessionId === $scope.sessionId; }); 176 | }; 177 | 178 | var haveIVoted = function () { 179 | if ($scope.myVote === 'undefined' || $scope.myVote === null) { 180 | return false; 181 | } 182 | return true; 183 | }; 184 | 185 | var votingFinished = function () { 186 | return $scope.forcedReveal || $scope.votes.length === $scope.voterCount; 187 | }; 188 | 189 | var setVotingState = function () { 190 | $scope.cardsState = votingFinished() || !$scope.voter ? ' card--disabled' : ''; 191 | $scope.votingState = votingFinished() ? ' flipped-stagger' : ''; 192 | }; 193 | 194 | var setLocalVote = function (vote) { 195 | var voteHash = myVoteHash(); 196 | $scope.myVote = vote; 197 | $scope.voted = haveIVoted(); 198 | if (!voteHash) { 199 | // initialize connections array with my first vote. (just to speed up UI) 200 | $scope.votes.push({ sessionId: $scope.sessionId, vote: vote }); 201 | } else { 202 | if (vote) { 203 | voteHash.vote = vote; 204 | } else { 205 | // we're unvoting - lets remove it from the votes. 206 | $scope.votes = _.filter($scope.votes, function (v) { 207 | return v.sessionId !== $scope.sessionId; 208 | }); 209 | // the above works - but causes an error in the UI. 210 | } 211 | } 212 | processVotes(); 213 | setVotingState(); 214 | $scope.scrollToSelectedCards.now(); 215 | }; 216 | 217 | var chooseCardPack = function (val) { 218 | var fib = ['0', '1', '2', '3', '5', '8', '13', '21', '34', '55', '89', '?']; 219 | var goat = ['0', '\u00BD', '1', '2', '3', '5', '8', '13', '20', '40', '100', '?', '\u2615']; 220 | var seq = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '?']; 221 | var play = ['A\u2660', '2', '3', '5', '8', '\u2654']; 222 | var tshirt = ['XL', 'L', 'M', 'S', 'XS', '?']; 223 | switch (val) { 224 | case ('fib'): 225 | return fib; 226 | case ('goat'): 227 | return goat; 228 | case ('seq'): 229 | return seq; 230 | case ('play'): 231 | return play; 232 | case ('tshirt'): 233 | return tshirt; 234 | default: 235 | return []; 236 | } 237 | }; 238 | 239 | var refreshRoomInfo = function (roomObj) { 240 | // console.log("refreshRoomInfo: roomObj:", roomObj) 241 | if (roomObj.createAdmin) { 242 | $.cookie("admin-" + roomObj.roomUrl, true); 243 | } 244 | if ($.cookie("admin-" + roomObj.roomUrl)) { 245 | $scope.showAdmin = true; 246 | } 247 | 248 | $scope.connections = roomObj.connections; 249 | $scope.humanCount = $scope.connections.length; 250 | $scope.cardPack = roomObj.cardPack; 251 | $scope.forcedReveal = roomObj.forcedReveal; 252 | $scope.alreadySorted = roomObj.alreadySorted; 253 | $scope.cards = chooseCardPack($scope.cardPack); 254 | 255 | $scope.votes = _.chain($scope.connections).filter(function (c) { 256 | return c.vote; 257 | }).values().value(); 258 | $scope.voterCount = _.filter($scope.connections, function (c) { 259 | return c.voter; 260 | }).length; 261 | 262 | var connection = myConnectionHash(); 263 | 264 | if (connection) { 265 | $scope.voter = connection.voter; 266 | $scope.myVote = connection.vote; 267 | $scope.voted = haveIVoted(); 268 | } 269 | 270 | processVotes(); 271 | 272 | // we first want the cards to be displayed as hidden, and then apply the finished state 273 | // if voting has finished - which then actions the transition. 274 | $timeout(function () { 275 | setVotingState(); 276 | }, 100); 277 | 278 | }; 279 | 280 | $scope.configureRoom = function () { 281 | 282 | socket.on('room joined', function () { 283 | // console.log("on room joined"); 284 | // console.log("emit room info", { roomUrl: $scope.roomId }); 285 | this.emit('room info', { roomUrl: $scope.roomId }, function (response) { 286 | processMessage(response, refreshRoomInfo); 287 | }); 288 | }); 289 | socket.on('room left', function () { 290 | // console.log("on room left"); 291 | // console.log("emit room info", { roomUrl: $scope.roomId }); 292 | this.emit('room info', { roomUrl: $scope.roomId }, function (response) { 293 | processMessage(response, refreshRoomInfo); 294 | }); 295 | }); 296 | socket.on('card pack set', function () { 297 | $scope.$emit('show message', 'Card pack changed...'); 298 | // console.log("on card pack set"); 299 | // console.log("emit room info", { roomUrl: $scope.roomId }); 300 | this.emit('room info', { roomUrl: $scope.roomId }, function (response) { 301 | processMessage(response, refreshRoomInfo); 302 | }); 303 | }); 304 | socket.on('voter status changed', function () { 305 | // console.log("on voter status changed"); 306 | // console.log("emit room info", { roomUrl: $scope.roomId }); 307 | this.emit('room info', { roomUrl: $scope.roomId }, function (response) { 308 | processMessage(response, refreshRoomInfo); 309 | }); 310 | }); 311 | socket.on('voted', function () { 312 | // console.log("on voted"); 313 | // console.log("emit room info", { roomUrl: $scope.roomId }); 314 | this.emit('room info', { roomUrl: $scope.roomId }, function (response) { 315 | processMessage(response, refreshRoomInfo); 316 | }); 317 | }); 318 | socket.on('unvoted', function () { 319 | // console.log("on unvoted"); 320 | // console.log("emit room info", { roomUrl: $scope.roomId }); 321 | this.emit('room info', { roomUrl: $scope.roomId }, function (response) { 322 | processMessage(response, refreshRoomInfo); 323 | }); 324 | }); 325 | socket.on('vote reset', function () { 326 | // console.log("on vote reset"); 327 | // console.log("emit room info", { roomUrl: $scope.roomId }); 328 | this.emit('room info', { roomUrl: $scope.roomId }, function (response) { 329 | processMessage(response, refreshRoomInfo); 330 | }); 331 | }); 332 | 333 | socket.on('reveal', function () { 334 | // console.log("reveal event received"); 335 | // setLocalVote(null); 336 | this.emit('room info', { roomUrl: $scope.roomId }, function (response) { 337 | processMessage(response, refreshRoomInfo); 338 | }); 339 | }); 340 | 341 | socket.on('connect', function () { 342 | // console.log("on connect"); 343 | var sessionId = this.socket.sessionid; 344 | // console.log("new socket id = " + sessionId); 345 | if (!$.cookie("sessionId")) { 346 | $.cookie("sessionId", sessionId); 347 | } 348 | $scope.sessionId = $.cookie("sessionId"); 349 | // console.log("session id = " + $scope.sessionId); 350 | // console.log("emit join room", { roomUrl: $scope.roomId, sessionId: $scope.sessionId }); 351 | socket.emit('join room', { roomUrl: $scope.roomId, sessionId: $scope.sessionId }, function (response) { 352 | processMessage(response, refreshRoomInfo); 353 | }); 354 | }); 355 | socket.on('disconnect', function () { 356 | // console.log("on disconnect"); 357 | }); 358 | 359 | // console.log("emit join room", { roomUrl: $scope.roomId, sessionId: $scope.sessionId }); 360 | socket.emit('join room', { roomUrl: $scope.roomId, sessionId: $scope.sessionId }, function (response) { 361 | processMessage(response, refreshRoomInfo); 362 | }); 363 | }; 364 | 365 | $scope.setCardPack = function (cardPack) { 366 | $scope.cardPack = cardPack; 367 | $scope.resetVote(); 368 | 369 | // console.log("set card pack", { roomUrl: $scope.roomId, cardPack: cardPack }); 370 | socket.emit('set card pack', { roomUrl: $scope.roomId, cardPack: cardPack }); 371 | }; 372 | 373 | $scope.vote = function (vote) { 374 | if ($scope.myVote !== vote) { 375 | if (!votingFinished() && $scope.voter) { 376 | setLocalVote(vote); 377 | 378 | // console.log("emit vote", { roomUrl: $scope.roomId, vote: vote, sessionId: $scope.sessionId }); 379 | socket.emit('vote', { roomUrl: $scope.roomId, vote: vote, sessionId: $scope.sessionId }, function (response) { 380 | processMessage(response); 381 | }); 382 | } 383 | } 384 | }; 385 | 386 | $scope.unvote = function (sessionId) { 387 | if (sessionId === $scope.sessionId) { 388 | if (!votingFinished()) { 389 | setLocalVote(undefined); 390 | 391 | // console.log("emit unvote", { roomUrl: $scope.roomId, sessionId: $scope.sessionId }); 392 | socket.emit('unvote', { roomUrl: $scope.roomId, sessionId: $scope.sessionId }, function (response) { 393 | processMessage(response); 394 | }); 395 | } 396 | } 397 | }; 398 | 399 | $scope.resetVote = function () { 400 | // console.log("emit reset vote", { roomUrl: $scope.roomId }); 401 | socket.emit('reset vote', { roomUrl: $scope.roomId }, function (response) { 402 | processMessage(response); 403 | }); 404 | }; 405 | 406 | $scope.forceReveal = function () { 407 | // console.log("emit force reveal", { roomUrl: $scope.roomId }); 408 | $scope.forceRevealDisable = true; 409 | socket.emit('force reveal', { roomUrl: $scope.roomId }, function (response) { 410 | processMessage(response); 411 | }); 412 | }; 413 | 414 | $scope.sortVotes = function () { 415 | // console.log("emit sort votes", { roomUrl: $scope.roomId }); 416 | $scope.sortVotesDisable = true; 417 | socket.emit('sort votes', { roomUrl: $scope.roomId }, function (response) { 418 | processMessage(response); 419 | }); 420 | }; 421 | 422 | $scope.toggleVoter = function () { 423 | // console.log("emit toggle voter", { roomUrl: $scope.roomId, voter: $scope.voter, sessionId: $scope.sessionId }); 424 | socket.emit('toggle voter', { roomUrl: $scope.roomId, voter: $scope.voter, sessionId: $scope.sessionId }, function (response) { 425 | processMessage(response); 426 | }); 427 | }; 428 | 429 | $scope.roomId = $routeParams.roomId; 430 | $scope.humanCount = 0; 431 | $scope.voterCount = 0; 432 | $scope.showAdmin = false; 433 | $scope.voter = true; 434 | $scope.connections = {}; 435 | $scope.votes = []; 436 | $scope.cardPack = ''; 437 | $scope.myVote = undefined; 438 | $scope.voted = haveIVoted(); 439 | $scope.votingState = ""; 440 | $scope.forcedReveal = false; 441 | $scope.forceRevealDisable = true; 442 | $scope.sortVotesDisable = true; 443 | $scope.scrollToSelectedCards = new ScrollIntoView($('#chosenCards')); 444 | 445 | $scope.dropDown = new DropDown('#dd'); 446 | $scope.votingAverage = 0; 447 | } 448 | 449 | RoomCtrl.$inject = ['$scope', '$routeParams', '$timeout', 'socket']; 450 | -------------------------------------------------------------------------------- /app/styleguide.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hat.jit.su style guide 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

Hat.jit.su styleguide: a cut-out-and-keep reference for all the family

18 |
19 |
20 |

Header/Logo

21 |

First thing's first; the header. The glorious introduction to the majesterial wonder that is HatJitSu. Here's an example of it in action;

22 |
23 | 24 | 25 |

Hatjitsu

26 |
27 |
28 |
29 |
30 |

Pretty awesome, hey? Have a look at the source code to see how that was done. We've used header tags and such, but that's not too important.

31 |

Neither's the a wrapped around it. That's purely optional; if you only want the logo and the header without a link, just take it out.

32 |
33 | 34 |

Hatjitsu

35 |
36 |

Actually, it's pretty much the same deal with the logo. Just remove the .header__text.

37 |
38 | 39 |
40 |

Just remember to put wrap it in a container classed with .header.

41 |

With the .header__text class, you can add multiple elements, allowing for multiline headings:

42 |
43 | 44 |
45 |

hatjitsu

46 |
47 |
48 |

It's also possible to change the colour of the logo dependant upon a status, simply add a second class of .header__logo--[colour] to the existing .header__logo element.

49 |
50 | 51 | 52 | 53 |
54 |
55 |
56 |
57 |

Footer

58 |

The footer of the page is a fairly simple center alignment issue. Just add in the .footer class and let the typographical styles do the rest.

59 | 62 |
63 |
64 |
65 |

Body

66 |

The .body--[colour] applies new styling based upon the consensus of voting, similar to the header logo.

67 |
68 |
Green
69 |
Yellow
70 |
Red
71 |
72 |
73 |
74 |
75 |

Cards

76 |

1 sided cards

77 |

Cards are obviously central to this app, and form a large part of the look. They all follow a fairly standard markup pattern.

78 | 1 79 |

Each card item is tagged with .card.

80 |

If there are multiple cards, the parent wrapper is tagged with .cards.

81 |
82 | 1 83 | 2 84 | 3 85 | 4 86 | 5 87 | 6 88 |
89 |

If a card is selected or active, add a class of .card--selected to the .card element in question.

90 |

Conversely, .card--disabled will mark it as disabled.

91 |

Finally, .card--placeholder is used to visually represent where a card will be placed.

92 |

These 3 classes can be mixed and matched as needed.

93 | 94 |
95 | 1 96 | 2 97 | 3 98 | 4 99 | 100 | 101 |
102 |

2 sided cards

103 |

2-sided cards are available to those with fancy JavaScriptin' and CSS3 3D-Transforms on their calculatin' machines. The basic markup is a little more involved, but should make sense:

104 |
105 | 106 | F 107 | B 108 | 109 | 110 | O 111 | A 112 | 113 | 114 | O 115 | R 116 | 117 |
118 | 119 |
120 |

By the way; we all understand that not all browsers support CSS 3D Transforms, right? Those browsers will simply swap cards around.

121 |

..and adding .card--selected or .code-disabled will still perform as before.

122 | 123 |
124 | 125 | R 126 | L 127 | 128 | 129 | O 130 | M 131 | 132 | 133 | F 134 | A 135 | 136 | 137 | L 138 | O 139 | 140 | 141 |
142 | 143 | 144 |
145 |

or selected only on one side...

146 |
147 | 148 | W 149 | L 150 | 151 | 152 | T 153 | O 154 | 155 | 156 | F 157 | L 158 | 159 |
160 | 161 |
162 |

Or toggle an individual card, simple add .flipped to the .card in question...

163 |
164 | 165 | 1 166 | 1 167 | 168 | 169 | 2 170 | 2 171 | 172 | 173 | 3 174 | 3 175 | 176 |
177 | 178 |
179 |

Or to stagger the animation of cards, simply add .flipped-stagger to .cards... ( note staggering only works up to a maximum of 8 votes, any more will be flipped at the same time)

180 |
181 | 182 | 1 183 | A 184 | 185 | 186 | 2 187 | B 188 | 189 | 190 | 3 191 | C 192 | 193 | 194 | 4 195 | D 196 | 197 | 198 | 5 199 | E 200 | 201 | 202 | 6 203 | F 204 | 205 | 206 | 7 207 | G 208 | 209 | 210 | 8 211 | H 212 | 213 | 214 | 9 215 | I 216 | 217 | 218 | 10 219 | J 220 | 221 | 222 | 11 223 | K 224 | 225 | 226 | 12 227 | L 228 | 229 | 230 | 13 231 | M 232 | 233 | 234 | 14 235 | N 236 | 237 | 238 | 15 239 | O 240 | 241 | 242 | 243 | 16 244 | P 245 | 246 |
247 | 248 |
249 |
250 |
251 |
252 |

Buttons

253 |

Basic setup: button.btn

254 |

Use the .icon to assign a space, and then either .icon-refresh or .icon-exclamation-sign for the various icons to use.

255 |
256 | 257 | 258 | 259 |
260 |

2-way toggle

261 |

Basic setup:

262 |
263 |
264 | 265 | 266 |
267 |
268 | 269 | 270 |
271 |
272 | 273 | 274 |
275 |
276 |
277 |
278 |
279 |

Dropdown

280 |

Javascript is required to use this customised drop down menu. The #dd selector is customisable to whatever jQuery selector you would prefer.

281 |
282 | 292 | 295 |
296 |
297 |
298 |
299 |
300 |

Typography

301 |

TBC

302 |
303 |
304 |
305 |

Concertina section

306 |

TBC

307 |
308 |
309 |
310 |

Panels

311 |

.cardPanel

312 |

The card panel typically contains all available vote combinations, and has a responsive breakpoint before viewport width of 28ems that centrally aligns the cards. Afterwards, the design will revert back to the left. Otherwise, it is also responsible for some simple styling.

313 |
314 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Illo et reprehenderit architecto eum delectus molestias ipsa corrupti perferendis aperiam suscipit repellat blanditiis rerum ea tempore doloribus provident quisquam impedit vel. 315 |
316 |
317 |
318 |
319 |

Surgical classes

320 |
321 |

.bg

322 |

Applies a transparent background texture.

323 |
324 |
325 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptatum neque excepturi facilis consequuntur illo rem ipsa ad aspernatur veniam eos dicta minus vero mollitia perferendis quibusdam iure laudantium animi sed.

326 |
327 |
328 |
329 |
330 |

.no-js-hide

331 |

With JavaScript disabled, this section will not be visible at all.

332 |

Think of it as an opposite to <noscript> Add a .no-js-hide class to those elements you want to remove.

333 |
334 | 338 |
339 |
340 | 341 | 342 | 343 | 344 | -------------------------------------------------------------------------------- /app/css/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | body { 5 | margin: 0; 6 | } 7 | hr { 8 | clear: both; 9 | } 10 | /* 11 | Links 12 | 13 | */ 14 | a { 15 | -webkit-transition: color .1s ease-out; 16 | -moz-transition: color .1s ease-out; 17 | -ms-transition: color .1s ease-out; 18 | -o-transition: color .1s ease-out; 19 | transition: color .1s ease-out; 20 | } 21 | a, 22 | a:visited, 23 | a:active { 24 | color: #493e27; 25 | } 26 | a:hover, 27 | a:focus { 28 | color: rgb(189, 27, 27); 29 | } 30 | /* 31 | Body 32 | 33 | */ 34 | 35 | .body { 36 | font: 16px/1.3em 'Helvetica neue', 'Helvetica', 'Arial', sans-serif; 37 | background-color: #c5bdab; 38 | color: #3d3938; 39 | text-shadow: 0px 0px 1px #f4f0e9; 40 | min-height: 100%; 41 | -webkit-transition: .4s background-color ease; 42 | -moz-transition: .4s background-color ease; 43 | -ms-transition: .4s background-color ease; 44 | -o-transition: .4s background-color ease; 45 | transition: .4s background-color ease; 46 | } 47 | .body--green { background-color: #c5c2ad; } 48 | .body--yellow { background-color: #c5bca1; } 49 | .body--red { background-color: #d1bdad; } 50 | 51 | /* 52 | Header 53 | 54 | */ 55 | .header { 56 | position: relative; 57 | z-index: 3; 58 | padding-top: 8px; 59 | padding-bottom: 67px; /* allow for absolutely positioned mountain */ 60 | zoom: 1; 61 | border: 1px solid #B9AAA7; 62 | border-width: 0 1px; 63 | } 64 | .header:before, 65 | .header:after { 66 | content: "\0020"; 67 | display: block; 68 | height: 0; 69 | overflow: hidden; 70 | } 71 | .header:after { 72 | clear: both; 73 | } 74 | .header a { 75 | display: block; 76 | overflow: hidden; 77 | text-decoration: none; 78 | position: relative; 79 | } 80 | .header__text { 81 | line-height: 1; 82 | font-size: 3em; 83 | margin: 0.5em 0 0 0.5em; 84 | position: relative; 85 | z-index: 1; 86 | text-align: left; 87 | height: 56px; 88 | width: 155px; 89 | text-indent: -1000px; 90 | background-image: url(/img/title.png); 91 | background-repeat: no-repeat; 92 | } 93 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { 94 | background-image: url(/img/title@2x.png); 95 | } 96 | @media screen and (min-width: 17em) { 97 | .header__text { 98 | position: relative; 99 | margin-left: 2.5em; 100 | right: auto; 101 | width: auto; 102 | } 103 | } 104 | .header__text > * { 105 | font-size: 1em; 106 | } 107 | .header__logo { 108 | float: left; 109 | display: block; 110 | margin-left: 1em; 111 | width: 6em; 112 | height: 6em; 113 | border-radius: 50%; 114 | background-size: 100%; 115 | -webkit-transition: .3s background-color ease; 116 | -moz-transition: .3s background-color ease; 117 | -ms-transition: .3s background-color ease; 118 | -o-transition: .3s background-color ease; 119 | transition: .3s background-color ease; 120 | z-index: 1; 121 | background-color: rgb(226, 222, 212); 122 | background-color: rgba(226, 222, 212, 0.8); 123 | } 124 | .header__logo--green { 125 | background-color: rgb(209, 229, 181); 126 | background-color: rgba(209, 229, 181, 0.9); 127 | } 128 | .header__logo--yellow { 129 | background-color: rgb(237, 226, 177); 130 | background-color: rgba(237, 226, 177, 0.9); 131 | } 132 | .header__logo--red { 133 | background-color: rgb(224, 157, 154); 134 | background-color: rgba(224, 157, 154, 0.9); 135 | } 136 | 137 | .no-svg .header__logo { 138 | position: relative; 139 | } 140 | .no-svg .header__logo:after { 141 | content: " "; 142 | position: absolute; 143 | top: 0; 144 | left: 0; 145 | display: block; 146 | width: 100%; 147 | height: 100%; 148 | background: url(/img/sun-mask.png) no-repeat center; 149 | } 150 | 151 | .roomNumber { 152 | position: absolute; 153 | z-index: 4; 154 | top: 4.3em; 155 | left: 7.7em; 156 | } 157 | 158 | /* 159 | 160 | Footer 161 | 162 | */ 163 | .footer { 164 | overflow: hidden; 165 | text-align: center; 166 | position: relative; 167 | border: 1px solid #b9aaa7; 168 | border-width: 0 1px; 169 | } 170 | 171 | /* 172 | 173 | Panel layouts 174 | 175 | */ 176 | 177 | .lobby { 178 | padding: 0 8px; 179 | position: relative; 180 | overflow: hidden; 181 | border: 1px solid #b9aaa7; 182 | border-width: 0 1px; 183 | } 184 | .lobby p, 185 | .lobby .subheading, 186 | .lobby label { 187 | padding-left: 0.5em; 188 | padding-right: 0.5em; 189 | } 190 | .votePanel { 191 | overflow: hidden; 192 | padding: 1em 8px 0; 193 | position: relative; 194 | border: 1px solid #b9aaa7; 195 | border-width: 0 1px; 196 | } 197 | .cardPanel { 198 | padding: 0 8px; 199 | text-align: center; 200 | position: relative; 201 | -webkit-box-shadow: inset 0px 1px 1px rgba(0,0,0,0.3); 202 | -moz-box-shadow: inset 0px 1px 1px rgba(0,0,0,0.3); 203 | box-shadow: inset 0px 1px 1px rgba(0,0,0,0.3); 204 | -webkit-transition: all .4s ease-in-out; 205 | -moz-transition: all .4s ease-in-out; 206 | -ms-transition: all .4s ease-in-out; 207 | -o-transition: all .4s ease-in-out; 208 | transition: all .4s ease-in-out; 209 | 210 | } 211 | .cardPanel:before { 212 | content: ""; 213 | display: block; 214 | position: absolute; 215 | width: 100%; 216 | height: 100%; 217 | background-color: rgba(0,0,0,0.1); 218 | top: 0; 219 | left: 0; 220 | } 221 | .cardPanel p { 222 | text-align: left; 223 | } 224 | @media screen and (min-width: 28em) { 225 | .cardPanel { 226 | text-align: left; 227 | } 228 | } 229 | .cardPanel-meta { 230 | padding: 1em 0; 231 | } 232 | .container { 233 | margin: 0 auto; 234 | max-width: 55em; 235 | overflow: hidden; 236 | position: relative; 237 | } 238 | 239 | /* 240 | 241 | Grid 242 | 243 | */ 244 | .row { 245 | width: 100%; 246 | display: block; 247 | text-align: left; 248 | } 249 | .row:before, 250 | .row:after { 251 | content: "\0020"; 252 | display: block; 253 | height: 0; 254 | overflow: hidden; 255 | } 256 | .row:after { 257 | clear: both; 258 | } 259 | .row .span2, .row .span1 { 260 | display: block; 261 | width: 100% 262 | } 263 | @media screen and (min-width: 16em) { 264 | .row .span1 { 265 | width: 16em; 266 | } 267 | .row .span2 { 268 | display: inline-block; 269 | width: 16em; 270 | } 271 | } 272 | 273 | /* 274 | 275 | Cards 276 | 277 | */ 278 | .cards { 279 | overflow: hidden; 280 | } 281 | .cards .card { 282 | margin-bottom: 0.5em; 283 | margin-right: 0.5em; 284 | } 285 | .card { 286 | display: inline-block; 287 | width: 2em; 288 | height: 3em; 289 | margin: 0; 290 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 291 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 292 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 293 | border: 1px solid transparent; 294 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 295 | border-bottom-color: #A2A2A2; 296 | position: relative; 297 | -webkit-border-radius: 0.2em; 298 | border-radius: 0.2em; 299 | 300 | color: #25201c; 301 | font-size: 1.5em; 302 | text-align: center; 303 | text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); 304 | line-height: 3em; 305 | cursor: pointer; 306 | 307 | background-color: rgb(224, 217, 207); 308 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(rgb(224, 217, 207)), to(rgb(216, 208, 197))); 309 | background-image: -webkit-linear-gradient(rgb(224, 217, 207), rgb(216, 208, 197)); 310 | background-image: -moz-linear-gradient(rgb(224, 217, 207), rgb(216, 208, 197)); 311 | background-image: -o-linear-gradient(rgb(224, 217, 207), rgb(216, 208, 197)); 312 | background-image: linear-gradient(rgb(224, 217, 207), rgb(216, 208, 197)); 313 | background-repeat: repeat-x; 314 | } 315 | .card:hover, 316 | .card:focus { 317 | background-color: rgb(216, 208, 197); 318 | background-position: 0 -30px; 319 | border-color: transparent; 320 | -webkit-transition: background-position 0.1s ease; 321 | -moz-transition: background-position 0.1s ease; 322 | -ms-transition: background-position 0.1s ease; 323 | -o-transition: background-position 0.1s ease; 324 | transition: background-position 0.1s ease; 325 | } 326 | .card--2-sided { 327 | -webkit-box-shadow: none; 328 | -moz-box-shadow: none; 329 | box-shadow: none; 330 | background-color: transparent; 331 | background-image: none; 332 | border: none; 333 | float: left; 334 | } 335 | .card--selected { 336 | background-color: gold; 337 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(gold), to(gold)); 338 | background-image: -webkit-linear-gradient(top, gold, gold); 339 | background-image: -moz-linear-gradient(top, gold, gold); 340 | background-image: -o-linear-gradient(top, gold, gold); 341 | background-image: linear-gradient(to bottom, gold, gold); 342 | background-repeat: repeat-x; 343 | cursor: default; 344 | } 345 | .card--selected:hover, 346 | .card--selected:focus { 347 | background-position: 0 0; 348 | background-color: gold; 349 | } 350 | .card--2-sided > * { 351 | position: absolute; 352 | display: inline-block; 353 | width: 100%; 354 | top: 0; 355 | left: 0; 356 | -webkit-border-radius: 0.2em; 357 | border-radius: 0.2em; 358 | -webkit-transform-style: preserve-3d; 359 | -moz-transform-style: preserve-3d; 360 | -ms-transform-style: preserve-3d; 361 | -o-transform-style: preserve-3d; 362 | transform-style: preserve-3d; 363 | -webkit-backface-visibility: hidden; 364 | -moz-backface-visibility: hidden; 365 | -ms-backface-visibility: hidden; 366 | -o-backface-visibility: hidden; 367 | backface-visibility: hidden; 368 | -webkit-transition: -webkit-transform .4s ease-in-out; 369 | -moz-transition: -moz-transform .4s ease-in-out; 370 | -ms-transition: -ms-transform .4s ease-in-out; 371 | -o-transition: -o-transform .4s ease-in-out; 372 | transition: transform .4s ease-in-out; 373 | -webkit-transform-style: preserve-3d; 374 | -moz-transform-style: preserve-3d; 375 | -ms-transform-style: preserve-3d; 376 | -o-transform-style: preserve-3d; 377 | transform-style: preserve-3d; 378 | background-color: #E0D9CF; 379 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(rgb(224, 217, 207)), to(rgb(216, 208, 197))); 380 | background-image: -webkit-linear-gradient(top, rgb(224, 217, 207), rgb(216, 208, 197)); 381 | background-image: -moz-linear-gradient(top, rgb(224, 217, 207), rgb(216, 208, 197)); 382 | background-image: -o-linear-gradient(top, rgb(224, 217, 207), rgb(216, 208, 197)); 383 | background-image: linear-gradient(to bottom, rgb(224, 217, 207), rgb(216, 208, 197)); 384 | background-repeat: repeat-x; 385 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 386 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 387 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 388 | border: 1px solid #BBB; 389 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 390 | border-bottom-color: #A2A2A2; 391 | } 392 | .card--2-sided > *:hover { 393 | border-color: transparent; 394 | } 395 | .card--2-sided.card--selected { 396 | background-color: transparent; 397 | background-image: none; 398 | } 399 | .card--2-sided > .card--selected, 400 | .card--2-sided.card--selected > * { 401 | background-color: gold; 402 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(gold), to(gold)); 403 | background-image: -webkit-linear-gradient(top, gold, gold); 404 | background-image: -moz-linear-gradient(top, gold, gold); 405 | background-image: -o-linear-gradient(top, gold, gold); 406 | background-image: linear-gradient(to bottom, gold, gold); 407 | background-repeat: repeat-x; 408 | } 409 | .card--2-sided > .card--disabled, 410 | .card--2-sided.card--disabled { 411 | opacity: 0.5; 412 | background-color: transparent; 413 | color: #AAA; 414 | } 415 | .no-csstransforms3d .card--2-sided > .card--disabled, 416 | .no-csstransforms3d .card--2-sided.card--disabled { 417 | background-color: rgb(224, 217, 207); 418 | } 419 | .card--2-sided.card--disabled > * { 420 | opacity: 0.5; 421 | } 422 | .no-csstransforms3d .card--2-sided.card--disabled > * { 423 | opacity: 1; 424 | } 425 | .card--placeholder { 426 | background-color: #cccccc; 427 | background-color: rgba(0, 0, 0, 0); 428 | -webkit-box-shadow: inset 0px 0px 3px rgba(15, 14, 12, 0.7); 429 | -moz-box-shadow: inset 0px 0px 3px rgba(15, 14, 12, 0.7); 430 | box-shadow: inset 0px 0px 3px rgba(15, 14, 12, 0.7); 431 | background-image: none; 432 | border: none; 433 | border-top: 1px solid transparent; 434 | cursor: default; 435 | } 436 | .card--placeholder:hover, 437 | .card--placeholder:focus { 438 | background-image: none; 439 | background-color: #cccccc; 440 | background-color: rgba(0, 0, 0, 0); 441 | } 442 | .card--disabled, 443 | .card--disabled > * { 444 | cursor: default; 445 | } 446 | .card--disabled { 447 | opacity: 0.5; 448 | } 449 | .card--side-1 { z-index: 2; } 450 | .card--side-2 { z-index: 1; } 451 | /* Step 1: by default, the second side is reversed */ 452 | .csstransforms3d .card--side-2{ 453 | -webkit-transform: rotateY(-180deg); 454 | -moz-transform: rotateY(-180deg); 455 | -ms-transform: rotateY(-180deg); 456 | -o-transform: rotateY(-180deg); 457 | transform: rotateY(-180deg); 458 | } 459 | /* Step 2: adding .flipped to a parent triggers animation */ 460 | .csstransforms3d .flipped .card--side-1 { 461 | -webkit-transform: rotateY(-180deg); 462 | -moz-transform: rotateY(-180deg); 463 | -ms-transform: rotateY(-180deg); 464 | -o-transform: rotateY(-180deg); 465 | transform: rotateY(-180deg); 466 | } 467 | .csstransforms3d .flipped .card--side-2 { 468 | -webkit-transform: rotateX(0deg) rotateY(0deg); 469 | -moz-transform: rotateX(0deg) rotateY(0deg); 470 | -ms-transform: rotateX(0deg) rotateY(0deg); 471 | -o-transform: rotateX(0deg) rotateY(0deg); 472 | transform: rotateX(0deg) rotateY(0deg); 473 | } 474 | .no-csstransforms3d .card--side-1 { z-index: 2; } 475 | .no-csstransforms3d .card--side-2 { z-index: 1; } 476 | .no-csstransforms3d .flipped .card--side-1 { z-index: 1; } 477 | .no-csstransforms3d .flipped .card--side-2 { z-index: 2; } 478 | /* Step 2b: adding .flipped-stagger to a parent triggers staggered animation */ 479 | .csstransforms3d .flipped-stagger .card--side-1 { 480 | -webkit-transform: rotateY(180deg); 481 | -moz-transform: rotateY(180deg); 482 | -ms-transform: rotateY(180deg); 483 | -o-transform: rotateY(180deg); 484 | transform: rotateY(180deg); 485 | } 486 | .csstransforms3d .flipped-stagger .card--side-2 { 487 | -webkit-transform: rotateX(0deg) rotateY(0deg); 488 | -moz-transform: rotateX(0deg) rotateY(0deg); 489 | -ms-transform: rotateX(0deg) rotateY(0deg); 490 | -o-transform: rotateX(0deg) rotateY(0deg); 491 | transform: rotateX(0deg) rotateY(0deg); 492 | } 493 | .csstransforms3d .flipped-stagger .card:nth-child(1) > * { 494 | -webkit-transition-delay: 0.1s; 495 | -moz-transition-delay: 0.1s; 496 | -ms-transition-delay: 0.1s; 497 | -o-transition-delay: 0.1s; 498 | transition-delay: 0.1s; 499 | } 500 | .csstransforms3d .flipped-stagger .card:nth-child(2) > * { 501 | -webkit-transition-delay: 0.2s; 502 | -moz-transition-delay: 0.2s; 503 | -ms-transition-delay: 0.2s; 504 | -o-transition-delay: 0.2s; 505 | transition-delay: 0.2s; 506 | } 507 | .csstransforms3d .flipped-stagger .card:nth-child(3) > * { 508 | -webkit-transition-delay: 0.3s; 509 | -moz-transition-delay: 0.3s; 510 | -ms-transition-delay: 0.3s; 511 | -o-transition-delay: 0.3s; 512 | transition-delay: 0.3s; 513 | } 514 | .csstransforms3d .flipped-stagger .card:nth-child(4) > * { 515 | -webkit-transition-delay: 0.4s; 516 | -moz-transition-delay: 0.4s; 517 | -ms-transition-delay: 0.4s; 518 | -o-transition-delay: 0.4s; 519 | transition-delay: 0.4s; 520 | } 521 | .csstransforms3d .flipped-stagger .card:nth-child(5) > *{ 522 | -webkit-transition-delay: 0.5s; 523 | -moz-transition-delay: 0.5s; 524 | -ms-transition-delay: 0.5s; 525 | -o-transition-delay: 0.5s; 526 | transition-delay: 0.5s; 527 | } 528 | .csstransforms3d .flipped-stagger .card:nth-child(6) > *{ 529 | -webkit-transition-delay: 0.6s; 530 | -moz-transition-delay: 0.6s; 531 | -ms-transition-delay: 0.6s; 532 | -o-transition-delay: 0.6s; 533 | transition-delay: 0.6s; 534 | } 535 | .csstransforms3d .flipped-stagger .card:nth-child(7) > *{ 536 | -webkit-transition-delay: 0.7s; 537 | -moz-transition-delay: 0.7s; 538 | -ms-transition-delay: 0.7s; 539 | -o-transition-delay: 0.7s; 540 | transition-delay: 0.7s; 541 | } 542 | .csstransforms3d .flipped-stagger .card:nth-child(8) > *{ 543 | -webkit-transition-delay: 0.8s; 544 | -moz-transition-delay: 0.8s; 545 | -ms-transition-delay: 0.8s; 546 | -o-transition-delay: 0.8s; 547 | transition-delay: 0.8s; 548 | } 549 | .csstransforms3d .flipped-stagger .card > *{ 550 | -webkit-transition-delay: 1s; 551 | -moz-transition-delay: 1s; 552 | -ms-transition-delay: 1s; 553 | -o-transition-delay: 1s; 554 | transition-delay: 1s; 555 | } 556 | .no-csstransforms3d .flipped-stagger .card--side-1 { z-index: 1; } 557 | .no-csstransforms3d .flipped-stagger .card--side-2 { z-index: 2; } 558 | .vote.card { cursor: default; } 559 | .vote.card--selected { cursor: pointer; } 560 | 561 | /* 562 | 563 | Buttons 564 | 565 | */ 566 | 567 | .btn { 568 | padding: 4px 12px; 569 | margin: 2px 0 0; 570 | line-height: 31px; 571 | display: inline-block; 572 | width: 100%; 573 | min-height: 44px; 574 | color: #333; 575 | font-size: 16px; 576 | text-align: left; 577 | text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); 578 | vertical-align: middle; 579 | cursor: pointer; 580 | background-color: rgb(216, 208, 197); 581 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(rgb(224, 217, 207)), to(rgb(216, 208, 197))); 582 | background-image: -webkit-linear-gradient(top, rgb(224, 217, 207), rgb(216, 208, 197)); 583 | background-image: -moz-linear-gradient(top, rgb(224, 217, 207), rgb(216, 208, 197)); 584 | background-image: -o-linear-gradient(top, rgb(224, 217, 207), rgb(216, 208, 197)); 585 | background-image: linear-gradient(to bottom, rgb(224, 217, 207), rgb(216, 208, 197)); 586 | background-repeat: repeat-x; 587 | border: 1px solid transparent; 588 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 589 | -webkit-box-sizing: border-box; 590 | -moz-box-sizing: border-box; 591 | box-sizing: border-box; 592 | -webkit-border-radius: 4px; 593 | -moz-border-radius: 4px; 594 | border-radius: 4px; 595 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 596 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 597 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 598 | -webkit-transition: background-position 0.1s ease; 599 | -moz-transition: background-position 0.1s ease; 600 | -ms-transition: background-position 0.1s ease; 601 | -o-transition: background-position 0.1s ease; 602 | transition: background-position 0.1s ease; 603 | } 604 | @media screen and (min-width: 16em) { 605 | .btn { 606 | width: 16em; 607 | } 608 | } 609 | .btn:hover, 610 | .btn:focus { 611 | background-position: 0 -40px; 612 | border-color: transparent; 613 | } 614 | .icon { 615 | display: inline-block; 616 | width: 14px; 617 | height: 14px; 618 | margin-top: 2px; 619 | line-height: 14px; 620 | vertical-align: text-top; 621 | background-image: url(/img/glyphicons.png); 622 | background-repeat: no-repeat; 623 | background-position: 14px 14px; 624 | } 625 | .icon-refresh { 626 | background-position: 0px 0px; 627 | } 628 | .icon-exclamation-sign { 629 | background-position: -90px 0px; 630 | } 631 | .icon-sort, 632 | .icon-sort::after, 633 | .icon-sort::before { 634 | box-sizing: border-box; 635 | height: 2px; 636 | border-radius: 4px; 637 | background: currentColor 638 | } 639 | .icon-sort { 640 | display: block; 641 | position: relative; 642 | transform: scale(var(--ggs,1)); 643 | width: 8px; 644 | top: 4px; 645 | left: 2px 646 | } 647 | .icon-sort::after, 648 | .icon-sort::before { 649 | content: ""; 650 | position: absolute 651 | } 652 | .icon-sort::before { 653 | width: 12px; 654 | top: -4px; 655 | left: -2px 656 | } 657 | .icon-sort::after { 658 | width: 4px; 659 | top: 4px; 660 | left: 2px 661 | } 662 | 663 | .switch { 664 | position: relative; 665 | margin: 2px 0 0; 666 | width: 100%; 667 | } 668 | @media screen and (min-width: 16em) { 669 | .switch { 670 | width: 16em; 671 | } 672 | } 673 | 674 | .switch input { 675 | position: absolute; 676 | width: 100%; 677 | height: 100%; 678 | z-index: 100; 679 | opacity: 0; 680 | cursor: pointer; 681 | margin: 0; 682 | } 683 | .switch .btn { 684 | padding: 0; 685 | text-indent: -100%; 686 | line-height: 40px; 687 | color: #B4573A; 688 | margin: 0; 689 | -webkit-transition: background-position 0.1s ease; 690 | -moz-transition: background-position 0.1s ease; 691 | -ms-transition: background-position 0.1s ease; 692 | -o-transition: background-position 0.1s ease; 693 | transition: background-position 0.1s ease; 694 | } 695 | .switch:hover .btn, 696 | .switch:focus .btn { 697 | background-position: 0 -40px; 698 | border-color: transparent; 699 | } 700 | 701 | .switch .btn:after { 702 | content: attr(data-off); 703 | position: absolute; 704 | z-index: 1; 705 | width: 100%; 706 | height: 100%; 707 | top: 0px; 708 | left: 0px; 709 | text-indent: 31px; 710 | overflow: hidden; 711 | white-space: nowrap; 712 | text-overflow: ellipsis; 713 | line-height: 40px; 714 | } 715 | .switch .btn:before { 716 | content: ""; 717 | position: absolute; 718 | z-index: 1; 719 | width: 15px; 720 | height: 13px; 721 | background-image: url(/img/glyphicons.png); 722 | background-position: -58px 0px; 723 | top: 15px; 724 | left: 11px; 725 | } 726 | .switch input:checked ~ .btn { 727 | color: #408F21; 728 | background-position: 0 -40px; 729 | } 730 | .switch input:checked ~ .btn:after { 731 | content: attr(data-on); 732 | } 733 | .switch input:checked ~ .btn:before { 734 | background-position: -28px 0; 735 | } 736 | .no-checked .switch .btn:before, 737 | .no-checked .switch .btn:after { 738 | display: none; 739 | } 740 | .no-checked .switch .btn { 741 | text-indent: 1.5em; 742 | color: #333; 743 | } 744 | .no-checked .switch input { 745 | width: auto; 746 | height: auto; 747 | top: 11px; 748 | left: 2px; 749 | } 750 | .roomUrl { 751 | font-size: 16px; 752 | line-height: 31px; 753 | -webkit-box-sizing: border-box; 754 | -moz-box-sizing: border-box; 755 | box-sizing: border-box; 756 | padding: 4px 12px; 757 | margin-bottom: 0; 758 | vertical-align: middle; 759 | -webkit-border-radius: 4px; 760 | -moz-border-radius: 4px; 761 | border-radius: 4px; 762 | border-width: 0px; 763 | } 764 | 765 | /* 766 | 767 | Typography 768 | 769 | */ 770 | 771 | .subheading { 772 | font-size: 1.1em; 773 | font-weight: bold; 774 | letter-spacing: -0.1px; 775 | line-height: 1.2em; 776 | text-align: left; 777 | } 778 | 779 | /* 780 | 781 | Dropdown 782 | 783 | */ 784 | .dropdown-wrapper { 785 | position: relative; 786 | max-width: 13em; 787 | padding: 0 1em; 788 | outline: none; 789 | cursor: pointer; 790 | min-height: 34px; 791 | } 792 | .dropdown-wrapper:after { 793 | content: ""; 794 | width: 0; 795 | height: 0; 796 | position: absolute; 797 | right: 16px; 798 | top: 50%; 799 | margin-top: -6px; 800 | border-width: 6px 0 6px 6px; 801 | border-style: solid; 802 | border-color: transparent #fff; 803 | } 804 | .dropdown-wrapper .dropdown { 805 | position: absolute; 806 | top: 98%; 807 | left: 0; 808 | right: 0; 809 | background: #fff; 810 | opacity: 0; 811 | pointer-events: none; 812 | margin: 0; 813 | padding: 0; 814 | z-index: 3; 815 | } 816 | .dropdown-wrapper.active .dropdown { 817 | opacity: 1; 818 | pointer-events: auto; 819 | } 820 | .dropdown-wrapper.active:after { 821 | border-width: 6px 6px 0 6px; 822 | border-color: rgb(189, 27, 27) transparent; 823 | margin-top: -3px; 824 | } 825 | 826 | .dropdown li { 827 | list-style-type: none; 828 | } 829 | .dropdown .dropdown__item { 830 | display: block; 831 | text-decoration: none; 832 | padding: 10px 20px; 833 | } 834 | 835 | /* 836 | 837 | Alerts 838 | 839 | */ 840 | .alert { 841 | z-index: 3; 842 | top: 0; 843 | left: 0; 844 | width: 100%; 845 | position: fixed; 846 | } 847 | @media screen and (min-width: 55em) { 848 | .alert { 849 | position: absolute; 850 | } 851 | } 852 | .alert .activity { 853 | width: 1em; 854 | height: 1em; 855 | background: url(/img/led.gif); 856 | text-indent: 100%; 857 | margin-top: 0.2em; 858 | margin-bottom: 0.2em; 859 | margin-right: 0.2em; 860 | float: right; 861 | text-align: right; 862 | -webkit-border-radius: 0.5em; 863 | -moz-border-radius: 0.5em; 864 | border-radius: 0.5em; 865 | } 866 | .socketMessage, 867 | .appError, 868 | .message { 869 | padding: 0 2em 0 0.5em; 870 | line-height: 1.5em; 871 | } 872 | .message { 873 | background-color: rgb(255, 255, 111); 874 | background-color: rgba(255, 255, 111, 0.4); 875 | color: rgb(129, 129, 15); 876 | } 877 | .socketMessage { 878 | background-color: rgba(0, 255, 255, 0.2); 879 | } 880 | .appError { 881 | background-color: rgba(255, 0, 0, 0.3); 882 | color: rgb(102, 43, 43); 883 | text-shadow: none; 884 | } 885 | /* 886 | 887 | Surgical classes 888 | 889 | */ 890 | .no-js-hide { 891 | display: none; 892 | } 893 | 894 | .pullright { 895 | float: right; 896 | } 897 | /* 898 | 899 | Decorators 900 | 901 | */ 902 | .bg { 903 | background-image: url(data:image/png;base64,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); 904 | } 905 | 906 | .bg-mount { 907 | display: block; 908 | background: url(/img/bg-mount.png) no-repeat -100px bottom; 909 | position: absolute; 910 | width: 100%; 911 | height: 135px; 912 | top: 51px; 913 | left: -1%; 914 | } 915 | .no-svg .bg-mount { 916 | z-index: 2; 917 | } 918 | .bg-tree { 919 | display: none; 920 | position: absolute; 921 | width: 320px; 922 | height: 200px; 923 | bottom: 33px; 924 | right: 0; 925 | z-index: 2; 926 | } 927 | --------------------------------------------------------------------------------