Commit 81c09072 authored by Randall Leeds's avatar Randall Leeds

Merge pull request #1289 from hypothesis/pattern-library

Pattern library first run
parents d9711472 b67e28de
......@@ -2,6 +2,7 @@ imports = [
'bootstrap'
'ngAnimate'
'ngRoute'
'h.auth'
'h.controllers'
'h.directives'
'h.app_directives'
......
imports = [
'h.session'
]
class AuthController
this.$inject = ['$scope', '$timeout', 'session']
constructor: ( $scope, $timeout, session ) ->
timeout = null
success = ->
$scope.model = null
$scope.$broadcast 'success'
failure = (form, response) ->
{errors, reason} = response.data
if reason
if reason == 'Invalid username or password.'
form.password.$setValidity('response', false)
form.password.responseErrorMessage = reason
else
form.responseErrorMessage = reason
else
form.responseErrorMessage = null
for field, error of errors
form[field].$setValidity('response', false)
form[field].responseErrorMessage = error
$scope.$broadcast 'error', form.$name
this.submit = (form) ->
return unless form.$valid
data = {}
method = '$' + form.$name
for own key of session
delete session[key]
angular.extend session, $scope.model
session[method] success, angular.bind(this, failure, form)
$scope.$on '$destroy', ->
if timeout
$timeout.cancel timeout
$scope.$watchCollection 'model', (value) ->
# Reset the auth forms after five minutes of inactivity
if timeout
$timeout.cancel timeout
# If the model is not empty, start the timeout
if value
timeout = $timeout ->
$scope.model = null
$scope.$broadcast 'timeout'
, 300000
authDirective = ->
controller: 'AuthController'
link: (scope, elem, attrs, [form, auth]) ->
elem.on 'submit', (event) ->
scope.$apply ->
$target = angular.element event.target
$form = $target.controller('form')
$form.responseErrorMessage = null
for ctrl in $form.$error.response?.slice?() or []
ctrl.$setValidity('response', true)
auth.submit($form)
scope.$on 'error', (event) ->
scope[attrs.onError]?(event)
scope.$on 'success', (event) ->
scope[attrs.onSuccess]?(event)
scope.$on 'timeout', (event) ->
scope[attrs.onTimeout]?(event)
scope.$watch 'model', (value) ->
if value is null
form.$setPristine()
require: ['form', 'auth']
restrict: 'C'
scope: true
angular.module('h.auth', imports)
.controller('AuthController', AuthController)
.directive('auth', authDirective)
......@@ -16,10 +16,8 @@ class App
scope:
frame:
visible: false
sheet:
collapsed: true
tab: null
ongoingHighlightSwitch: false
sheet: {}
sorts: [
'Newest'
'Oldest'
......@@ -48,12 +46,12 @@ class App
frame: $scope.frame or @scope.frame
socialView: annotator.socialView
ongoingHighlightSwitch: false
model: {}
search:
facets: SEARCH_FACETS
values: SEARCH_VALUES
query: $location.search()
show: not angular.equals($location.search(), {})
session: session
_reset()
......@@ -62,23 +60,20 @@ class App
angular.extend annotator.options.Store, options
session.$promise.then (data) ->
angular.extend $scope.model, data
unless data.personas?.length
$scope.initUpdater()
$scope.reloadAnnotations()
$scope.$watch 'model.personas', (newValue, oldValue) =>
$scope.$watch 'session.personas', (newValue, oldValue) =>
if newValue?.length
unless $scope.model.persona and $scope.model.persona in newValue
$scope.model.persona = newValue[0]
unless $scope.session.persona and $scope.session.persona in newValue
$scope.session.persona = newValue[0]
else
$scope.model.persona = null
$scope.$watch 'model.persona', (newValue, oldValue) =>
$scope.sheet.collapsed = true
$scope.session.persona = null
$scope.$watch 'session.persona', (newValue, oldValue) =>
unless annotator.discardDrafts()
$scope.model.persona = oldValue
$scope.session.persona = oldValue
return
plugins.Auth?.element.removeData('annotator:headers')
......@@ -136,14 +131,13 @@ class App
annotator.show()
annotator.host.notify method: 'showFrame', params: routeName
else if oldValue
$scope.sheet.collapsed = true
annotator.hide()
annotator.host.notify method: 'hideFrame', params: routeName
for p in annotator.providers
p.channel.notify method: 'setActiveHighlights'
$scope.$watch 'sheet.collapsed', (hidden) ->
$scope.sheet.tab = if hidden then null else 'login'
$scope.$watch 'sheet.show', (visible) ->
$scope.sheet.tab = if visible then 'login' else null
$scope.$watch 'sheet.tab', (tab) ->
$timeout ->
......@@ -168,25 +162,6 @@ class App
filter = streamfilter.getFilter()
sock.send(JSON.stringify({filter}))
$scope.$on 'authTimeout', ->
# Skip the reset if we're logged in
unless $scope.model.persona
$scope.$broadcast 'reset'
flash 'info',
'For your security, the forms have been reset due to inactivity.'
$scope.$on 'showAuth', (event, show=true) ->
$scope.sheet.collapsed = !show
$scope.$on 'reset', _reset
$scope.$on 'success', (event, action) ->
angular.extend $scope.model, session.model
if action == 'forgot'
$scope.sheet.tab = 'activate'
else
$scope.sheet.collapsed = true
$rootScope.viewState =
sort: ''
view: 'Screen'
......@@ -378,6 +353,14 @@ class App
cleanup (a for a in annotations when a.thread)
annotator.subscribe 'annotationsLoaded', cleanup
$scope.authSuccess = ->
$scope.sheet.show = false
$scope.authTimeout = ->
flash 'info',
'For your security, the forms have been reset due to inactivity.'
_reset()
$scope.initUpdater = (failureCount=0) ->
_dfdSock = $q.defer()
_sock = socket()
......@@ -410,7 +393,7 @@ class App
unless data instanceof Array then data = [data]
p = $scope.model.persona
p = $scope.session.persona
user = if p? then "acct:" + p.username + "@" + p.provider else ''
unless data instanceof Array then data = [data]
......@@ -700,52 +683,6 @@ class Annotation
$scope.model.highlightText.replace regexp, annotator.highlighter
class Auth
this.$inject = ['$scope', '$timeout', 'session']
constructor: ( $scope, $timeout, session) ->
base =
username: null
email: null
password: null
code: null
_timeout = null
_reset = ->
delete $scope.errors
angular.extend $scope.model, base
for own _, ctrl of $scope when typeof ctrl?.$setPristine is 'function'
ctrl.$setPristine()
_error = (form, data) ->
{errors, reason} = data
$scope.errors = session: reason
$scope.errors[form] = {}
for field, error of errors
$scope.errors[form][field] = error
_startTimeout = ->
# Reset the auth forms after five minutes of inactivity
if _timeout then $timeout.cancel _timeout
_timeout = $timeout (-> $scope.$emit 'authTimeout'), 3000000
$scope.$on 'reset', _reset
$scope.$watchCollection 'model', ->
# (Re)start (i.e., delay) the authentication form timeout
unless $scope.sheet.collapsed
_startTimeout()
$scope.submit = (form) ->
angular.extend session, $scope.model
return unless form.$valid
promise = session["$#{form.$name}"] ->
$scope.$emit 'success', form.$name
promise.then(_reset, _error.bind(null, form.$name))
class Editor
this.$inject = [
'$location', '$routeParams', '$sce', '$scope',
......@@ -1085,7 +1022,6 @@ class Notification
angular.module('h.controllers', imports)
.controller('AppController', App)
.controller('AnnotationController', Annotation)
.controller('AuthController', Auth)
.controller('EditorController', Editor)
.controller('ViewerController', Viewer)
.controller('SearchController', Search)
......
formValidate = ->
link: (scope, elem, attr, form) ->
errorClassName = attr.formValidateErrorClass
toggleClass = (field, {addClass}) ->
fieldEl = elem.find("[data-target=#{field.$name}]")
fieldEl.toggleClass(errorClassName, addClass)
updateField = (field) ->
return unless field?
if field.$valid
toggleClass(field, addClass: false)
else
toggleClass(field, addClass: true)
# Immediately show feedback for corrections.
elem.on 'keyup', ':input', ->
updateField(form[this.name]) if form[this.name]?.$valid
# Validate field when the content changes.
elem.on 'change', ':input', ->
updateField(form[this.name])
# Validate the field when submit is clicked.
elem.on 'submit', (event) ->
updateField(field) for own _, field of form when field.$name?
# Validate when a response is processed.
scope.$on 'error', (event, name) ->
return unless form.$name == name
updateField(field) for own _, field of form when field.$name?
require: 'form'
markdown = ['$filter', '$timeout', ($filter, $timeout) ->
link: (scope, elem, attr, ctrl) ->
return unless ctrl?
......@@ -104,32 +140,6 @@ recursive = ['$compile', '$timeout', ($compile, $timeout) ->
]
###
# The slow validation directive ties an to a model controller and hides
# it while the model is being edited. This behavior improves the user
# experience of filling out forms by delaying validation messages until
# after the user has made a mistake.
###
slowValidate = ->
link: (scope, elem, attr, ctrl) ->
fieldName = attr.slowValidate
return unless ctrl.$name? and ctrl?[fieldName]
elem.addClass 'slow-validate'
fieldPath = [ctrl.$name, fieldName, '$viewValue'].join('.')
modelCtrl = ctrl?[fieldName]
scope.$watch fieldPath, ->
if modelCtrl.$invalid and modelCtrl.$dirty
elem.addClass 'slow-validate-show'
else
elem.removeClass 'slow-validate-show'
require: '^form'
restrict: 'A'
tabReveal = ['$parse', ($parse) ->
compile: (tElement, tAttrs, transclude) ->
panes = []
......@@ -285,6 +295,8 @@ tags = ['$window', ($window) ->
tag = ui.tagLabel
$window.open "/t/" + tag
elem.find('input').addClass('form-input')
ctrl.$formatters.push (tags=[]) ->
assigned = elem.tagit 'assignedTags'
for t in assigned when t not in tags
......@@ -437,12 +449,13 @@ whenscrolled = ['$window', ($window) ->
scope.$apply attr.whenscrolled
]
angular.module('h.directives', ['ngSanitize'])
.directive('formValidate', formValidate)
.directive('fuzzytime', fuzzytime)
.directive('markdown', markdown)
.directive('privacy', privacy)
.directive('recursive', recursive)
.directive('slowValidate', slowValidate)
.directive('tabReveal', tabReveal)
.directive('tags', tags)
.directive('thread', thread)
......
......@@ -64,9 +64,14 @@ class SessionProvider
($q, $resource, baseURI, flash) ->
actions = {}
_process = (response) ->
data = response.data
process = (data, headersGetter) ->
# Parse as json
data = angular.fromJson data
# Lift response data
model = data.model
model.errors = data.errors
model.reason = data.reason
# bw compat
if angular.isObject(data.persona)
......@@ -85,19 +90,12 @@ class SessionProvider
csrfToken = model.csrf
delete model.csrf
# Lift the model object so it becomes the response data.
# Return the response or a rejected response.
if data.status is 'failure'
$q.reject(data)
else
model
# Return the model
model
for name, options of ACTION_OPTION
actions[name] = angular.extend {}, options, @options
actions[name].interceptor =
response: _process
responseError: _process
actions[name].transformResponse = process
$resource("#{baseURI}app", {}, actions).load()
]
......
......@@ -35,12 +35,9 @@ ol {
@extend .btn-link;
float: right;
}
input { width: 100%; }
}
.bookmarklet {
@extend .btn;
padding: .12em;
border: 1px dashed $gray;
}
......@@ -92,29 +89,16 @@ ol {
.sheet {
@include smallshadow;
@include border-radius(2px);
border: solid 1px $grayLighter;
font-family: $sansFontFamily;
max-height: 480px;
overflow: hidden;
position: absolute;
left: 0;
right: 0;
top: 0;
margin-bottom: .72em;
position: relative;
.close {
position: absolute;
right: .5em;
top: .5em;
}
&.collapsed {
max-height: 0;
}
footer {
font-size: .8em;
font-family: $sansFontFamily;
text-align: right;
right: 1em;
top: 1em;
}
input:not([type="submit"]) { width: 100%; }
......
......@@ -69,6 +69,15 @@ $score-height: $score-width;
$heatmap-width: 22px;
$input-border-radius: 2px;
/* Style input placeholders */
@mixin placeholder {
&.placeholder { @content; }
&:-moz-placeholder { @content; }
&::-moz-placeholder { @content; }
&:-ms-input-placeholder { @content; }
&::-webkit-input-placeholder { @content; }
}
/* Shadow mixins */
@mixin smallshadow($a: 0, $b: 1px, $c: .1) {
@include box-shadow($a $b 1px hsla(0, 0%, 0%, $c));
......@@ -82,7 +91,7 @@ $input-border-radius: 2px;
font-family: "Source Sans Pro", "Open Sans", sans-serif;
font-size: 1em;
padding: .33em .5em;
&:focus {
&:focus, &.js-focus {
outline: 0;
border: 1px solid $gray;
@include box-shadow( inset 1px 1px 6px -1px $grayLighter);
......@@ -115,7 +124,7 @@ $input-border-radius: 2px;
color: $grayDark;
border-color: $grayLight $grayLight $gray;
&:hover {
&:hover, &:focus, &:active, &.js-focus, &.js-hover, &.js-active {
@include background-image(
linear-gradient(top, #fefefe 0%, #f4f4f4 50%, #e2e2e2 51%, #fdfdfd 100%));
color: black;
......@@ -125,7 +134,7 @@ $input-border-radius: 2px;
}
}
&:active:not([disabled]) {
&:active:not([disabled]), &.js-active {
@include background-image(
linear-gradient(top, #fcfcfc 0%, #f3f3f3 50%, #e1e1e1 51%, #fbfbfb 100%));
@include box-shadow(
......
......@@ -2,6 +2,8 @@
@import 'reset';
@import 'base';
@import 'forms';
@import 'spinner';
@import 'responsive';
@import 'yui_grid';
......@@ -23,8 +25,9 @@ a {
body {
background-color: $bodyBackground;
color: $textColor;
font-smoothing: antialiased;
-webkit-font-smoothing: antialiased;
// FIXME
// font-smoothing: antialiased;
// -webkit-font-smoothing: antialiased;
line-height: 1.4;
}
......@@ -93,30 +96,7 @@ h6 {
margin: 1.67em 0;
}
input, textarea {
@include plainform;
}
select {
@include plainform;
padding: 0;
text-decoration: underline;
border: 0;
&:focus {
border: 0;
}
}
label {
@extend .visuallyhidden;
}
//MCRANDOM////////////////////////////////
button, input[type=submit], .btn {
@include sweetbutton;
}
.alert-block {
span.errorMsgLbl { @extend .visuallyhidden; }
......@@ -138,7 +118,10 @@ button, input[type=submit], .btn {
cursor: pointer;
color: $linkColor;
position: static;
&:hover { color: $linkColorHover; }
&:hover, &:focus, &.js-focus, &.js-hover {
color: $linkColorHover;
}
}
.red {
......@@ -158,9 +141,11 @@ button, input[type=submit], .btn {
.close {
cursor: pointer;
float: right;
width: 1em;
height: 1em;
opacity: .4;
line-height: 1.4;
opacity: .6;
&:active, &:hover {
opacity: 1;
}
}
......@@ -182,7 +167,7 @@ button, input[type=submit], .btn {
.form-vertical {
select, textarea, input, button {
display: block;
margin-top: .75em;
// margin-top: .75em;
}
}
......@@ -190,18 +175,6 @@ button, input[type=submit], .btn {
display: none;
}
.slow-validate {
display: block;
font-family: $sansFontFamily;
max-height: 0;
overflow: hidden;
&.slow-validate-show {
@include transition(max-height .25s ease-in 2s);
max-height: 10em;
}
}
.visuallyhidden {
position: absolute;
overflow: hidden;
......@@ -387,52 +360,42 @@ blockquote {
//TABS////////////////////////////////
.nav-tabs {
@include pie-clearfix;
margin: 0;
a {
font-family: $sansFontFamily;
color: $grayDark;
}
background-color: $bodyBackground;
border: 1px none $grayLighter;
border-bottom-style: solid;
padding: 1em;
& > li {
@include box-shadow(inset 2px -10px 13px -8px hsla(0, 0%, 0%, .1));
background: darken($white, 2%);
border-top-right-radius: 32px 80px;
border-top-left-radius: 32px 80px;
border: 1px solid $grayLighter;
border-bottom: none;
cursor: pointer;
display: inline-block;
line-height: 1;
margin-right: -8px;
padding: 4px 16px 8px;
position: relative;
&:hover {
background: $white;
a {
font-family: $sansFontFamily;
font-weight: bold;
color: $grayDark;
cursor: pointer;
}
&.active, &:active {
@include box-shadow(none);
background: $white;
color: #333;
&.active a, &:active a {
color: $hypothered;
}
&.active {
z-index: 1;
&:before {
content: "/";
margin: 0 1em;
}
&:first-child:before {
content: "";
margin: 0;
}
}
}
.tab-content {
@include border-radius(1px);
background: $white;
border: solid 1px $grayLighter;
line-height: 1.4;
margin-top: -3px;
padding: 16px;
position: relative;
padding: 1em;
.tab-pane {
display: none;
......@@ -749,6 +712,11 @@ pre {outline: 1px solid #ccc; padding: 5px; margin: 5px; }
margin-top: .2em;
margin-bottom: .2em;
.tagit-choice {
border-radius: 2px;
padding: 0.307em 1.7em 0.307em 0.76em;
}
.tagit-close {
cursor: pointer;
position: absolute;
......@@ -757,6 +725,11 @@ pre {outline: 1px solid #ccc; padding: 5px; margin: 5px; }
margin-top: -8px;
}
.tagit-new input[type=text] {
padding-top: 5px;
padding-bottom: 5px;
}
.text-icon { display: none; }
li {
......@@ -768,7 +741,6 @@ pre {outline: 1px solid #ccc; padding: 5px; margin: 5px; }
opacity: .8;
&:hover { opacity: 1; }
&.tagit-choice { margin-top: .33em; }
&.tagit-new { padding: 0; }
& + li { margin-left: .4em; }
}
......
// Common form styles.
@import "compass/css3/images";
@import "compass/utilities/general/clearfix";
.form-field {
margin-bottom: 10px;
}
.form-description {
margin-bottom: 1em;
}
%form-input-error-state {
&, &:focus, &.js-focus {
color: #b4777a;
border-color: #f5adb7;
background-color: #fff1f3;
}
@include placeholder {
color: #e0b0b0;
}
}
.form-field-error {
.form-input {
@extend %form-input-error-state;
}
.form-error-list {
display: block;
}
}
.form-input,
.form-label {
width: 100%;
display: block;
}
.form-label {
cursor: pointer;
font-weight: bold;
font-size: 13px;
margin-bottom: 5px;
}
.form-hint {
font-size: 12px;
margin-left: 5px;
color: #949292;
-webkit-font-smoothing: antialiased;
}
.form-required, .form-required[title] {
cursor: help;
color: #949292;
border-bottom: none;
-webkit-font-smoothing: antialiased;
}
.form-input {
border: 1px solid #d8ddde;
border-radius: 2px;
padding: 7px 10px;
font-weight: normal;
font-size: 15px;
color: #797979;
@include placeholder {
-webkit-font-smoothing: antialiased;
}
&:focus, &.js-focus {
outline: none;
color: #7b776a;
border-color: #ece3c5;
background-color: #fffbeb;
@include placeholder {
color: #cbbb95;
}
}
}
.form-select {
display: block;
}
.form-error-list {
display: none;
&:first-child {
margin-top: 5px;
}
}
.form-error {
font-size: 13px;
line-height: 1.5;
color: #b4777a;
}
.form-checkbox-item {
padding-left: 22px;
.form-checkbox, [type=checkbox], [type=radio] {
float: left;
margin-left: -20px;
margin-top: 0.3em;
}
.form-label {
display: inline;
}
}
.form-inline {
display: flex;
.form-input {
flex-grow: 1;
width: auto;
}
.btn {
margin-left: 0.5em;
}
}
.form-actions {
@include pie-clearfix;
margin-top: 5px;
}
.form-actions-message {
font-size: 13px;
float: left;
margin-top: 7px;
}
.form-actions-buttons {
@include pie-clearfix;
float: right;
* {
float: left;
margin-left: 10px;
}
*:first-child {
margin-left: 0;
}
}
// Allows buttons to be positioned explicitly.
.form-actions-left {
float: left;
}
.form-actions-right {
float: right;
}
.btn {
@include background(linear-gradient(top, #fff, #f0f0f0));
@include box-shadow(0 1px 0 rgba(0, 0, 0, 0.15));
display: inline-block;
font-size: 13px;
font-weight: bold;
color: #585858;
text-shadow: 0 1px 0 #FFF;
border-radius: 2px;
border: 1px solid #ACACAC;
padding: 7px 12px 6px;
&:focus, &:hover, &:active, &.js-hover, &.js-focus, &.js-active {
@include box-shadow(0 1px 0 rgba(0, 0, 0, 0.05));
outline: none;
color: #585858;
background: #fff;
border-color: #bababa;
}
&:active, &.js-active {
@include box-shadow(inset 0 1px 0 rgba(0, 0, 0, 0.1));
background: #F0F0F0;
color: #424242;
border-color: #bababa;
}
&[disabled], &.js-disabled {
@include box-shadow(none);
cursor: default;
background: #F0F0F0;
border-color: #CECECE;
color: #999;
}
}
.btn-clean {
&, &:focus, &:hover, &:active, &.js-hover, &.js-focus, &.js-active {
@include box-shadow(none);
padding-left: 0;
padding-right: 0;
background: none;
border-color: transparent;
}
&:focus, &:hover, &:active, &.js-hover, &.js-focus, &.js-active {
color: $linkColor;
}
&:active, &.js-active {
color: $linkColorHover;
}
}
// Includes an icon within the button
.btn-with-icon {
position: relative;
padding-left: 34px;
.btn-icon {
position: absolute;
top: 5px;
left: 8px;
}
}
// Absolutely positions a message/icon to the left of a button.
.btn-with-message {
position: relative;
}
.btn-message {
font-size: 13px;
font-style: italic;
color: #999;
margin-right: 6px;
position: absolute;
right: 100%;
top: 50%;
margin-top: -9px;
white-space: nowrap;
}
.btn-message-icon {
display: inline-block;
position: relative;
top: 0px;
background: #76B800;
border-radius: 50%;
color: #FFF;
font-size: 12px;
height: 20px;
line-height: 13px;
margin-left: 4px;
padding: 4px;
width: 20px;
}
// Handles state transitions from "default" -> "loading" -> "success"
[data-btn-message-state] .btn-message {
top: -999em;
left: -999em;
right: auto;
}
[data-btn-message-state=success] .btn-message-success,
[data-btn-message-state=loading] .btn-message-loading {
top: 50%;
left: auto;
right: 100%;
}
[data-btn-message-state] .btn-message-text {
@include transition(opacity 0.2s 0.6s ease-in);
opacity: 0;
}
[data-btn-message-state=success] .btn-message-success .btn-message-text {
opacity: 1;
}
[data-btn-message-state] .btn-message-success .btn-message-icon {
@include transform(scale(0));
}
[data-btn-message-state=success] .btn-message-success .btn-message-icon {
@include transition(transform 0.15s 0 cubic-bezier(0, 1.8, 1, 1.8));
@include transform(scale(1));
}
// CSS Spinner modified from http://dabblet.com/gist/7615212
// Works in modern browsers & IE10, IE9 gets stationary spinner.
//
// Examples
//
// <!-- Three nested spans -->
// <span class="spinner"><span><span></span></span></span>
@-webkit-keyframes spin {
to { @include transform(rotate(1turn)); }
}
@-moz-keyframes spin {
to { @include transform(rotate(1turn)); }
}
@-o-keyframes spin {
to { @include transform(rotate(1turn)); }
}
@keyframes spin {
to { @include transform(rotate(1turn)); }
}
.spinner {
position: relative;
display: inline-block;
width: 2em;
height: 2em;
font-size: 10px;
text-indent: 999em;
overflow: hidden;
-webkit-animation: spin 1.25s infinite steps(12); /* Safari 4+ */
-moz-animation: spin 1.25s infinite steps(12); /* Fx 5+ */
-o-animation: spin 1.25s infinite steps(12); /* Opera 12+ */
animation: spin 1.25s infinite steps(12); /* IE 10+, Fx 29+ */
}
.spinner:before,
.spinner:after,
.spinner > span:before,
.spinner > span:after,
.spinner > span > span:before,
.spinner > span > span:after {
content: '';
position: absolute;
top: 0;
left: 0.9em; /* (container width - part width)/2 */
width: 0.2em;
height: 0.6em;
border-radius: 0.1em;
background: #eee;
@include box-shadow(0 1.4em rgba(0, 0, 0, 0.15)); /* container height - part height */
@include transform-origin(50%, 1em); /* container height / 2 */
}
.spinner:before {
background: rgba(0, 0, 0, 0.65);
}
.spinner:after {
@include transform(rotate(-30deg));
background: rgba(0, 0, 0, 0.6);
}
.spinner > span:before {
@include transform(rotate(-60deg));
background: rgba(0, 0, 0, 0.5);
}
.spinner > span:after {
@include transform(rotate(-90deg));
background: rgba(0, 0, 0, 0.4);
}
.spinner > span > span:before {
@include transform(rotate(-120deg));
background: rgba(0, 0, 0, 0.3);
}
.spinner > span > span:after {
@include transform(rotate(-150deg));
background: rgba(0, 0, 0, 0.2);
}
......@@ -45,10 +45,11 @@ body {
max-width: $break-medium;
padding-left: 3.6em;
padding-right: 3.6em;
position: relative;
@include respond-to(wide-handhelds handhelds) {
padding-left: .9em;
padding-right: .9em;
padding-left: 0;
padding-right: 0;
}
&.pull-right {
......@@ -56,6 +57,6 @@ body {
font-family: $sansFontFamily;
}
& > * { margin: 0 4px; }
& > * { margin: 0 .72em; }
}
}
assert = chai.assert
sinon.assert.expose assert, prefix: null
class MockSession
$login: sinon.stub()
$register: (callback, errback) ->
errback
data:
errors:
username: 'taken'
reason: 'registration error'
describe 'h.auth', ->
beforeEach module('h.auth')
beforeEach module ($provide) ->
$provide.value '$timeout', sinon.spy()
$provide.value 'flash', sinon.spy()
$provide.value 'session', new MockSession()
return
describe 'AuthController', ->
$scope = null
$timeout = null
auth = null
session = null
beforeEach inject ($controller, $rootScope, _$timeout_, _session_) ->
$scope = $rootScope.$new()
$timeout = _$timeout_
auth = $controller 'AuthController', {$scope}
session = _session_
session.$login.reset()
describe '#submit()', ->
it 'should call session methods on submit', ->
auth.submit
$name: 'login'
$valid: true
assert.called session.$login
it 'should do nothing when the form is invalid', ->
auth.submit
$name: 'login'
$valid: false
assert.notCalled session.$login
it 'should set response errors', ->
form =
$name: 'register'
$valid: true
username:
$setValidity: sinon.stub()
email:
$setValidity: sinon.stub()
auth.submit(form)
assert.calledWith form.username.$setValidity, 'response', false
assert.equal form.username.responseErrorMessage, 'taken'
assert.equal form.responseErrorMessage, 'registration error'
describe 'timeout', ->
it 'should happen after a period of inactivity', ->
sinon.spy $scope, '$broadcast'
$scope.model =
username: 'test'
email: 'test@example.com'
password: 'secret'
code: '1234'
$scope.$digest()
assert.called $timeout
$timeout.lastCall.args[0]()
assert.isNull $scope.model, 'the model is erased'
assert.calledWith $scope.$broadcast, 'timeout'
it 'should not happen if the model is empty', ->
$scope.$digest()
assert.notCalled $timeout
describe 'authDirective', ->
elem = null
session = null
$rootScope = null
$scope = null
beforeEach inject ($compile, _$rootScope_, _session_) ->
elem = angular.element(
'''
<div class="auth" ng-form="form"
on-error="stub" on-success="stub" on-timeout="stub">
<form name="login">
<input type="text" name="username" ng-model="username"></input>
</form>
</div>
'''
)
session = _session_
$rootScope = _$rootScope_
$scope = $compile(elem)($rootScope).scope()
$scope.$digest()
it 'should reset response errors before submit', ->
$scope.form.login.responseErrorMessage = 'test'
$scope.form.login.username.$setValidity('response', false)
assert.isFalse $scope.form.login.$valid
elem.find('input').trigger('submit')
assert.isTrue $scope.form.login.$valid
assert.isNull $scope.form.login.responseErrorMessage
it 'should reset to pristine state when the model is reset', ->
$scope.form.$setDirty()
$scope.$digest()
assert.isFalse $scope.form.$pristine
$scope.model = null
$scope.$digest()
assert.isTrue $scope.form.$pristine
it 'should invoke handlers set by attributes', ->
$scope.stub = sinon.stub()
for event in ['error', 'success', 'timeout']
$scope.stub.reset()
$scope.$broadcast(event)
assert.called $scope.stub
......@@ -7,8 +7,12 @@ describe 'h.directives', ->
beforeEach module ($provide, $filterProvider) ->
fakeWindow = {open: sinon.spy()}
fakeDocument = angular.element({
createElement: (tag) -> document.createElement(tag)
})
$provide.value('$window', fakeWindow)
$provide.value('$document', fakeDocument)
$filterProvider.register 'persona', ->
(user, part) ->
......@@ -23,6 +27,76 @@ describe 'h.directives', ->
$compile = _$compile_
$scope = _$rootScope_.$new()
describe '.formValidate', ->
$element = null
beforeEach ->
$scope.model = {username: ''}
template = '''
<form form-validate data-form-validate-error-class="form-field-error" name="login" onsubmit="return false">
<div class="form-field" data-error-class="form-field-error" data-target="username">
<input type="text" class="" ng-model="model.username" name="username" required ng-minlength="3" />
</div>
</form>
'''
# Needs to be passed through angular.element() to work. Otherwise it
# will not link the form-validate directive.
$element = $compile(angular.element(template))($scope)
$scope.$digest()
it 'should apply an error class to an invalid field on change', ->
$field = $element.find('.form-field')
$element.find('[name=username]').val('ab').change()
assert.include $field.prop('className'), 'form-field-error'
it 'should remove an error class to an valid field on change', ->
$field = $element.find('.form-field').addClass('form-field-error')
$input = $element.find('[name=username]')
$input.val('abc').change()
assert.notInclude $field.prop('className'), 'form-field-error'
it 'should apply an error class to an invalid field on submit', ->
$field = $element.find('.form-field')
$element.trigger('submit')
assert.include $field.prop('className'), 'form-field-error'
it 'should remove an error class from a valid field on submit', ->
$scope.model.username = 'abc'
$scope.$digest()
$field = $element.find('.form-field').addClass('form-field-error')
$element.trigger('submit')
assert.notInclude $field.prop('className'), 'form-field-error'
it 'should apply an error class to an invalid field on "error" event', ->
$scope.$emit('error', 'login')
$element.controller('form').username.$setValidity('response', false)
$field = $element.find('.form-field')
assert.include $field.prop('className'), 'form-field-error'
it 'should remove an error class on valid input on keyup', ->
$scope.model.username = 'abc'
$scope.$digest()
$field = $element.find('.form-field').addClass('form-field-error')
$element.find('[name=username]').keyup()
assert.notInclude $field.prop('className'), 'form-field-error'
it 'should not add an error class on invalid input on keyup', ->
$scope.model.username = ''
$scope.$digest()
$field = $element.find('.form-field')
$element.find('[name=username]').keyup()
assert.notInclude $field.prop('className'), 'form-field-error'
describe '.username', ->
$element = null
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment