Using bootstrap 4 and related UI refresh (#205)

Changes at a glance:

- Switch from BS3 to BS4
- Polishing of some UI elements (low-hanging fruit for UX)
- Mobile-friendly layout.
This commit is contained in:
sappor0 2019-06-07 11:22:13 +02:00 committed by jelhan
parent 25271e4019
commit c23ba1f6fb
42 changed files with 471 additions and 675 deletions

View file

@ -2,16 +2,14 @@ import { inject as service } from '@ember/service';
import { readOnly, mapBy, filter } from '@ember/object/computed';
import Component from '@ember/component';
import { isPresent, isEmpty } from '@ember/utils';
import { observer, computed, get } from '@ember/object';
import { observer, get } from '@ember/object';
import {
validator, buildValidations
}
from 'ember-cp-validations';
import { raw } from 'ember-awesome-macros';
import { groupBy } from 'ember-awesome-macros/array';
import { next, scheduleOnce } from '@ember/runloop';
import BsForm from 'ember-bootstrap/components/bs-form';
import BsFormElement from 'ember-bootstrap/components/bs-form/element';
import { next } from '@ember/runloop';
let modelValidations = buildValidations({
dates: [
@ -101,10 +99,6 @@ export default Component.extend(modelValidations, {
}
});
}
next(() => {
this.notifyPropertyChange('_nestedChildViews');
});
});
},
@ -153,73 +147,5 @@ export default Component.extend(modelValidations, {
groupedDates: groupBy('dates', raw('day')),
childFormElements: computed('_nestedChildViews', function() {
let form = this.childViews.find((childView) => {
return childView instanceof BsForm;
});
if (!form) {
return [];
}
return form.childViews.filter((childView) => {
return childView instanceof BsFormElement;
});
}),
// Can't use a computed property cause Ember Bootstrap seem to modify validation twice in a single render.
// Therefor we use the scheduleOnce trick.
// This is the same for {{create-options-text}} component.
daysValidationState: null,
updateDaysValidationState: observer('childFormElements.@each.validation', function() {
scheduleOnce('sync', () => {
this.set('daysValidationState',
this.childFormElements.reduce(function(daysValidationState, item) {
const day = item.get('model.day');
const validation = item.get('validation');
let currentValidationState;
// there maybe form elements without model or validation
if (isEmpty(day) || validation === undefined) {
return daysValidationState;
}
// if it's not existing initialize with current value
if (!daysValidationState.hasOwnProperty(day)) {
daysValidationState[day] = validation;
return daysValidationState;
}
currentValidationState = daysValidationState[day];
switch (currentValidationState) {
// error overrules all validation states
case 'error':
break;
// null ist overruled by 'error'
case null:
if (validation === 'error') {
daysValidationState[day] = 'error';
}
break;
// success is overruled by anyother validation state
case 'success':
daysValidationState[day] = validation;
break;
}
return daysValidationState;
}, {})
);
});
}).on('init'),
store: service(),
didInsertElement() {
// childViews is not observeable by default. Need to notify about a change manually.
// Lucky enough we know that child views will only be changed on init and if times are added / removed.
this.notifyPropertyChange('_nestedChildViews');
}
});

View file

@ -1,9 +1,6 @@
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { observer, computed, get } from '@ember/object';
import { run, next } from '@ember/runloop';
import BsFormElement from 'ember-bootstrap/components/bs-form/element';
import { any } from 'ember-awesome-macros/array';
import { observer } from '@ember/object';
export default Component.extend({
actions: {
@ -15,76 +12,13 @@ export default Component.extend({
position,
fragment
);
next(() => {
this.notifyPropertyChange('_childViews');
});
},
deleteOption(element) {
let position = this.options.indexOf(element);
this.options.removeAt(position);
next(() => {
this.notifyPropertyChange('_childViews');
});
}
},
anyElementHasFeedback: any('childFormElements.@each.hasFeedback', function(childFormElement) {
return get(childFormElement, 'hasFeedback');
}),
anyElementIsInvalid: any('childFormElements.@each.validation', function(childFormElement) {
return get(childFormElement, 'validation') === 'error';
}),
everyElementIsValid: computed('childFormElements.@each.validation', function() {
const anyElementIsInvalid = this.anyElementIsInvalid;
if (anyElementIsInvalid) {
return false;
}
// childFormElements contains button wrapper element which should not be taken into account here
const childFormElements = this.childFormElements.filterBy('hasValidator');
if (childFormElements) {
return childFormElements.every((childFormElement) => {
return childFormElement.get('hasFeedback') && childFormElement.get('validation') === 'success';
});
} else {
return false;
}
}),
childFormElements: computed('_childViews', function() {
return this.childViews.filter((childView) => {
return childView instanceof BsFormElement;
});
}),
// Can't use a computed property cause Ember Bootstrap seem to modify validation twice in a single render.
// Therefor we use the scheduleOnce trick.
// This is the same for {{create-options-datetime}} component.
labelValidationClass: 'label-has-no-validation',
updateLabelValidationClass: observer('anyElementHasFeedback', 'anyElementIsInvalid', 'everyElementIsValid', function() {
run.scheduleOnce('sync', () => {
let validationClass;
if (!this.anyElementHasFeedback) {
validationClass = 'label-has-no-validation';
} else if (this.anyElementIsInvalid) {
validationClass = 'label-has-error';
} else if (this.everyElementIsValid) {
validationClass = 'label-has-success';
} else {
validationClass = 'label-has-no-validation';
}
this.set('labelValidationClass', validationClass);
});
}).on('init'),
classNameBindings: ['labelValidationClass'],
enforceMinimalOptionsAmount: observer('options', 'isMakeAPoll', function() {
if (this.get('options.length') < 2) {
let options = this.options;
@ -97,11 +31,4 @@ export default Component.extend({
}).on('init'),
store: service('store'),
didInsertElement() {
// childViews is not observeable by default. Need to notify about a change manually.
// Lucky enough we know that child views will only be changed on init and if times are added / removed.
// Use a custom variable to avoid any complications with framework.
this.notifyPropertyChange('_childViews');
}
});

View file

@ -41,9 +41,9 @@ export default Component.extend({
let renderPrevButton = this.renderPrevButton;
if (renderPrevButton) {
return ['col-xs-6', 'col-md-8'];
return ['col-6', 'col-md-8'];
} else {
return ['col-md-8', 'col-md-offset-4'];
return ['col-md-8', 'offset-md-4'];
}
}),

View file

@ -24,18 +24,18 @@
{{content-for "body-footer"}}
<noscript>
<div class="container-fluid">
<div id="header">
<h1 class="logo">Croodle</h1>
<nav class="cr-navbar navbar navbar-dark">
<h1 class="cr-logo">Croodle</h1>
</nav>
<main role="main" class="container cr-main">
<div class="alert alert-danger" role="alert">
<h2>Please enable JavaScript!</h2>
<p>JavaScript is currently disabled in your browser settings or your browser does not support JavaScript.</p>
<p>Croodle requires JavaScript. Therefore to use Croodle, you must first enable JavaScript.</p>
<a class="btn btn-outline-dark btn-lg" href="http://www.enable-javascript.com/">Click here for instructions on how to enable JavaScript.</a>
</div>
<div id="content">
<div class="box">
<h1>Please enable JavaScript</h1>
<p>Croodle requires JavaScript. But JavaScript is currently disabled in your browser settings or your browser does not support JavaScript. Therefore you can't use Croodle.</p>
<h2><a href="http://www.enable-javascript.com/">Click here for instructions on how to enable JavaScript.</a></h2>
</div>
</div>
</div>
</main>
</noscript>
</body>
</html>

View file

@ -98,6 +98,7 @@ export default {
'poll.input.newUserName.label': 'Nom',
'poll.input.newUserName.placeholder': 'El teu nom',
'poll.link.copied': 'Enllaç copiat al porta-retalls.',
'poll.link.copy-label': 'Copia l\'enllaç al porta-retalls',
'poll.link.selected': 'Enllaç seleccionat. Premeu Command + C per copiar.',
'poll.modal.timezoneDiffers.title': 'En quines zones horàries s\'han de presentar les dates?',
'poll.modal.timezoneDiffers.body': 'L\'enquesta es va crear per a una zona horària diferent de la vostra hora local. En quines zones horàries s\'han de presentar les dates?',

View file

@ -98,6 +98,7 @@ export default {
'poll.input.newUserName.label': 'Name',
'poll.input.newUserName.placeholder': 'Dein Name',
'poll.link.copied': 'Link in die Zwischenablage kopiert.',
'poll.link.copy-label': 'Kopiere Link in die Zwischenablage',
'poll.link.selected': 'Link markiert. Drücke Steuerung + C zum Kopieren.',
'poll.modal.timezoneDiffers.title': 'In welcher Zeitzone sollen die Daten angezeigt werden?',
'poll.modal.timezoneDiffers.body': 'Die Umfrage wurde für eine Zeitzone angelegt, die von deiner lokalen Zeit abweicht. In welcher Zeitzone sollen die Daten angezeigt werden?',

View file

@ -98,6 +98,7 @@ export default {
'poll.input.newUserName.label': 'Name',
'poll.input.newUserName.placeholder': 'Your Name',
'poll.link.copied': 'Link copied to clipboard.',
'poll.link.copy-label': 'Copy link to clipboard',
'poll.link.selected': 'Link selected. Press Command+C to copy.',
'poll.modal.timezoneDiffers.title': 'In which time zones should the dates be presented?',
'poll.modal.timezoneDiffers.body': 'The poll was created for a time zone which differs from your local time. In which time zones should the dates be presented?',

View file

@ -98,6 +98,7 @@ export default {
'poll.input.newUserName.label': 'Nombre',
'poll.input.newUserName.placeholder': 'Tu nombre',
'poll.link.copied': 'Enlace copiado al portapapeles.',
'poll.link.copy-label': 'Copiar enlace al portapapeles',
'poll.link.selected': 'Enlace seleccionado. Presiona Comando+C para copiarlo.',
'poll.modal.timezoneDiffers.title': '¿Que zona horaria deseas utilizar para mostrar los datos?',
'poll.modal.timezoneDiffers.body': 'La encuesta ha sido configurada para una zona horaria distinta de tu hora local. ¿Con qué zona horaria debería mostrarse la información?',

View file

@ -98,6 +98,7 @@ export default {
'poll.input.newUserName.label': 'Nome',
'poll.input.newUserName.placeholder': 'Il tuo nome',
'poll.link.copied': 'Il link è stato copiato.',
'poll.link.copy-label': 'Copia il link negli appunti',
'poll.link.selected': 'Link selezionato. Preme Command+C per copiarlo.',
'poll.modal.timezoneDiffers.title': 'In quale fuso orario devono essere presentate le date?',
'poll.modal.timezoneDiffers.body': 'Il sondaggio è stato creato per un fuso orario diverso dal tuo. In quali orari devono essere presentate le date?',

View file

@ -0,0 +1,24 @@
// We don't want the extra space added to jumbotron at larger screen sizes.
.jumbotron {
@include media-breakpoint-up(sm) {
padding: $jumbotron-padding $jumbotron-padding;
}
}
.sr-only {
padding: 0 !important;
}
.tab-pane {
padding-top: map-get($spacers, 4);
}
.navbar-dark .custom-select {
background-color: gray("900");
color: gray("500");
border: $custom-select-border-width solid gray("500");
}
h3, .h3 {
margin-top: map-get($spacers, 5);
}

View file

@ -1,35 +1,20 @@
@import "ember-power-calendar";
@media only screen and (max-width: $screen-sm-min) {
.ember-power-calendar {
@include ember-power-calendar($cell-size: 63px);
}
.ember-power-calendar {
@include ember-power-calendar(30px);
width: 100%;
}
@media only screen and (min-width: $screen-sm-min) {
.ember-power-calendar {
@include ember-power-calendar($cell-size: 47px);
float:left;
&:first-child {
margin-right: 10px;
}
}
.ember-power-calendar ~ .help-block {
clear: both;
}
.ember-power-calendar .ember-power-calendar-day,
.ember-power-calendar .ember-power-calendar-weekday {
max-width: none;
width: auto;
max-height: none;
height: auto;
margin: 1px;
}
@media only screen and (min-width: $screen-md-min) {
.ember-power-calendar {
@include ember-power-calendar($cell-size: 40px);
}
}
@media only screen and (min-width: $screen-lg-min) {
.ember-power-calendar {
@include ember-power-calendar($cell-size: 50px);
}
.ember-power-calendar .ember-power-calendar-weekdays,
.ember-power-calendar .ember-power-calendar-week {
height: 3em;
display: flex;
justify-content: space-around;
}

View file

@ -0,0 +1,26 @@
.cr-navbar {
background: map-get($theme-colors, dark);
}
.cr-logo {
font-size: $font-size-base;
margin-bottom: 0;
// This is needed for cases when the h1 does not contain a link, like when JS
// is disabled.
color: $body-bg;
}
.cr-main {
padding-top: map-get($spacers, 5);
}
.cr-claim {
margin-bottom: map-get($spacers, 4);
}
.cr-hide-on-mobile {
display: none;
@include media-breakpoint-up(md) {
display: block;
}
}

View file

@ -0,0 +1,7 @@
.cr-option-menu {
margin-top: map-get($spacers, 2);
&__button {
margin-right: map-get($spacers, 2);
}
}

View file

@ -0,0 +1,18 @@
.cr-poll-link {
&__link {
position: relative;
word-wrap: break-word;
background: gray("200");
padding: map-get($spacers, 3) map-get($spacers, 3) 3em map-get($spacers, 3);
}
&__url {
user-select: all;
}
&__copy-btn {
position: absolute;
bottom: 0;
right: 0;
}
}

98
app/styles/_steps.scss Normal file
View file

@ -0,0 +1,98 @@
$steps-border-width: 5px;
.cr-steps-top-nav {
margin-bottom: map-get($spacers, 5);
flex-direction: column;
width: 100%;
@include media-breakpoint-up(md) {
flex-direction: row;
width: auto;
}
&__button {
text-align: left;
border-width: 0;
border-left-width: $steps-border-width;
&:not(:first-child) {
margin-left: 0 !important;
}
@include media-breakpoint-up(md) {
text-align: inherit;
border-left: 0;
border-right: 0;
}
&:not(:disabled) {
border-left-color: gray("300");
@include media-breakpoint-up(md) {
border-bottom: $steps-border-width solid gray("300");
}
}
&.is-active {
border-left-color: theme-color-level("primary", 2);
@include media-breakpoint-up(md) {
border-bottom: $steps-border-width solid theme-color-level("primary", 2);
}
}
}
}
$bottom-nav-height: 5.5em;
.cr-form-wrapper {
padding-bottom: $bottom-nav-height;
@include media-breakpoint-up(md) {
padding-bottom: 0;
}
}
.cr-steps-bottom-nav {
margin-top: map-get($spacers, 4);
padding-top: map-get($spacers, 4);
padding-bottom: map-get($spacers, 4);
border-top: 2px solid gray("100");
background: $body-bg;
height: $bottom-nav-height;
z-index: 9;
position: fixed;
bottom: 0;
left: $grid-gutter-width / 2;
right: $grid-gutter-width / 2;
@include media-breakpoint-up(md) {
margin-top: map-get($spacers, 5);
padding-top: map-get($spacers, 5);
padding-bottom: map-get($spacers, 5);
background: transparent;
position: static;
bottom: auto;
left: auto;
right: 0;
}
&__button {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
@include media-breakpoint-up(md) {
width: auto;
}
}
&__prev-button .cr-steps-bottom-nav__label {
margin-left: map-get($spacers, 3);
}
&__next-button .cr-steps-bottom-nav__label {
margin-right: map-get($spacers, 3);
}
&__next-button {
justify-content: flex-end;
@include media-breakpoint-up(md) {
justify-content: normal;
}
}
}

View file

@ -0,0 +1,9 @@
$icon-font-path: '../open-iconic/font/fonts/';
$enable-rounded: false;
$font-family-base: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
$jumbotron-padding: 2rem;
$btn-disabled-opacity: 0.45;

View file

@ -1,192 +1,45 @@
@import "ember-bootstrap/bootstrap";
@import "calendar";
@import "participants-table";
// Required
@import "ember-bootstrap/functions";
@import "ember-bootstrap/variables";
@import "ember-bootstrap/mixins";
table tr td .form-group {
margin-bottom:0;
}
table tr td .help-block {
margin-top:0;
margin-bottom:0;
}
table tr td .form-control {
min-width:6em;
}
body {
background-color:#d3d3d3;
}
#header h1.logo {
float: left;
}
#header .language-select {
float: right;
margin-top: 20px;
width: auto;
}
#content {
clear: both;
}
.box {
padding:10px;
margin-bottom:10px;
background-color:#ffffff;
border:#556b2f 1px solid;
-webkit-border-radius:10px;
-moz-border-radius:10px;
border-radius:10px;
h2:first-child, h3:first-child {
margin-top:0;
}
}
#poll .participation {
.radio.yes {
color: green;
}
.radio.no {
color: red;
}
.radio.maybe {
color: orange;
}
}
#poll {
tbody td .yes, tfoot td option[value="yes"] {
color:green;
}
tbody td .no, tfoot td option[value="no"] {
color:red;
}
tbody td .maybe, tfoot td option[value="maybe"] {
color:orange;
}
.index {
.start, .have-a-try {
font-size:150%;
}
}
.meta-data .description {
white-space:pre-wrap;
}
.meta-data .dates {
color:grey;
}
.poll-link .link {
color:#556b2f;
word-break:break-all;
// Overriding bootstrap variables
@import "./variable-overrides";
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
}
.poll-link .link button {
-webkit-align-items: stretch;
-moz-align-items: stretch;
-ms-align-items: stretch;
align-items: stretch;
// Optional - Bootstrapping/Resetting
@import "ember-bootstrap/root";
@import "ember-bootstrap/print";
@import "ember-bootstrap/reboot";
margin-left: 0.5em;
}
.poll-link .link button span {
font-size: 1.6em;
}
.poll-link .notice {
font-size:90%;
color:grey;
}
.table-scroll {
width:100%;
margin-bottom:15px;
overflow-y:hidden;
overflow-x:scroll;
-ms-overflow-style:-ms-autohiding-scrollbar;
border:1px solid #dddddd;
-webkit-overflow-scrolling:touch;
}
.evaluation-header {
font-weight:bold;
}
}
#loading {
letter-spacing:5px;
}
.loading-animation-container {
position:relative;
display:inline-block;
width:100%;
}
.loading-animation-container:before {
content:'';
display:block;
margin-top:12%;
}
.loading-animation-circle {
position:absolute;
top:0;
bottom:0;
width:12%;
animation-name:bounce-loading-animation-circle;
animation-duration:1.3s;
animation-iteration-count:infinite;
animation-direction:linear;
transform:scale(.3);
border-radius:100%;
}
#loading-animation-circle-1 {
left:0;
animation-delay:0.52s;
}
#loading-animation-circle-2 {
left:12.5%;
animation-delay:0.65s;
}
#loading-animation-circle-3 {
left:25%;
animation-delay:0.78s;
}
#loading-animation-circle-4 {
left:37.5%;
animation-delay:0.91s;
}
#loading-animation-circle-5 {
left:50%;
animation-delay:1.04s;
}
#loading-animation-circle-6 {
left:62.5%;
animation-delay:1.17s;
}
#loading-animation-circle-7 {
left:75%;
animation-delay:1.3s;
}
#loading-animation-circle-8 {
left:87.5%;
animation-delay:1.43s;
}
#loading-animation-circle-9 {
left:100%;
animation-delay:1.43s;
}
// Open Iconic icon font
@import "open-iconic/font/css/open-iconic-bootstrap.scss";
ul.nav-tabs {
margin-bottom: 15px;
}
// Optional - Everything else
@import "ember-bootstrap/utilities/screenreaders";
@import "ember-bootstrap/type";
@import "ember-bootstrap/tables";
@import "ember-bootstrap/nav";
@import "ember-bootstrap/navbar";
@import "ember-bootstrap/grid";
@import "ember-bootstrap/buttons";
@import "ember-bootstrap/jumbotron";
@import "ember-bootstrap/alert";
@import "ember-bootstrap/button-group";
@import "ember-bootstrap/forms";
@import "ember-bootstrap/modal";
@import "ember-bootstrap/input-group";
@import "ember-bootstrap/custom-forms";
/*
* Override label validation state
*/
.label-has-error .control-label {
color: $state-danger-text;
}
.label-has-success .control-label {
color: $state-success-text;
}
.label-has-no-validation .control-label {
color: inherit;
}
// Overriding bootstrap selectors with properties we cannot influence by
// changing variables.
@import "./bootstrap-tweaking";
.form-steps {
margin-bottom: 1em;
}
// Finally adding our own custom styles
@import "./generic-croodle-styles";
// Component specific styles
@import "./calendar";
@import "./steps";
@import "./participants-table";
@import "./option-menu";
@import "./poll-link";

View file

@ -3,4 +3,4 @@
<p>
The poll with you url could not be found. Perhaps it got deleted?
</p>
</div>
</div>

View file

@ -2,24 +2,25 @@
{{title "Croodle"}}
<div class="container">
<div id="header">
<h1 class="logo">{{#link-to "index"}}Croodle{{/link-to}}</h1>
<form class="form-inline">
{{language-select class="form-control"}}
<nav class="cr-navbar navbar navbar-dark">
<h1 class="cr-logo">{{#link-to "index" class="navbar-brand"}}Croodle{{/link-to}}</h1>
<div class="collapse" id="headerNavbar">
<form class="form-inline my-2 my-lg-0">
{{language-select class="custom-select custom-select-sm"}}
</form>
</div>
<div id="content">
<div id="messages">
{{#each flashMessages.queue as |flash|}}
{{#flash-message flash=flash}}
{{t flash.message}}
{{/flash-message}}
{{/each}}
</div>
</nav>
{{outlet}}
<main role="main" class="container cr-main">
<div id="messages">
{{#each flashMessages.queue as |flash|}}
{{#flash-message flash=flash}}
{{t flash.message}}
{{/flash-message}}
{{/each}}
</div>
</div>
{{outlet}}
</main>
{{outlet "modal"}}

View file

@ -1 +1 @@
{{yield}}
{{yield}}

View file

@ -3,16 +3,22 @@
property="options"
data-test-form-element-for="days"
}}
<InlineDatepicker
@center={{calendarCenter}}
@selectedDays={{selectedDays}}
@onCenterChange={{action (mut calendarCenter) value="moment"}}
@onSelect={{action "daysSelected"}}
/>
<InlineDatepicker
@center={{calendarCenterNext}}
@selectedDays={{selectedDays}}
@onCenterChange={{action (mut calendarCenter) value="moment"}}
@onSelect={{action "daysSelected"}}
/>
<div class="row">
<div class="col-12 col-md-6">
<InlineDatepicker
@center={{calendarCenter}}
@selectedDays={{selectedDays}}
@onCenterChange={{action (mut calendarCenter) value="moment"}}
@onSelect={{action "daysSelected"}}
/>
</div>
<div class="col-md-6 cr-hide-on-mobile">
<InlineDatepicker
@center={{calendarCenterNext}}
@selectedDays={{selectedDays}}
@onCenterChange={{action (mut calendarCenter) value="moment"}}
@onSelect={{action "daysSelected"}}
/>
</div>
</div>
{{/form.element}}

View file

@ -1,4 +1,4 @@
<div class="box">
<div class="cr-form-wrapper box">
{{#if errorMessage}}
{{#bs-alert type="warning"}}
{{t errorMessage}}
@ -41,7 +41,7 @@
as |el|
}}
<div class="input-group">
{{bs-form/element/control/input
{{el.control
autofocus=(unless index true false)
id=el.id
placeholder="00:00"
@ -49,42 +49,28 @@
value=el.value
onChange=(action (mut el.value))
}}
<div class="input-group-btn">
<div class="input-group-append">
{{! disable delete button if there is only one option }}
{{#bs-button
onClick=(action "deleteOption" date)
type=(if
(eq el.validation "success")
"btn-success"
(if
(eq el.validation "error")
"btn-danger"
"btn-default"
)
)
type="link"
class="delete"
disabled=(lte dates.length 1)
}}
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
<span class="oi oi-trash" title={{t "create.options.button.delete.label"}} aria-hidden="true"></span>
<span class="sr-only">{{t "create.options.button.delete.label"}}</span>
{{/bs-button}}
{{#bs-button
onClick=(action "addOption" date)
type=(if
(eq el.validation "success")
"btn-success"
(if
(eq el.validation "error")
"btn-danger"
"btn-default"
)
)
class="add"
}}
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="sr-only">{{t "create.options.button.add.label"}}</span>
{{/bs-button}}
</div>
</div>
{{#bs-button
onClick=(action "addOption" date)
type="link"
size="sm"
class="add cr-option-menu__button cr-option-menu__add-button"
}}
<span class="oi oi-plus" title={{t "create.options.button.add.label"}} aria-hidden="true"></span>
<span class="sr-only">{{t "create.options.button.add.label"}}</span>
{{/bs-button}}
{{/form.element}}
</div>
{{/each}}
@ -95,6 +81,7 @@
{{#bs-button
onClick=(action "adoptTimesOfFirstDay")
class="adopt-times-of-first-day"
size="sm"
}}
{{t "create.options-datetime.copy-first-line"}}
{{/bs-button}}

View file

@ -8,48 +8,33 @@
as |el|
}}
<div class="input-group">
{{bs-form/element/control/input
{{el.control
autofocus=(unless index true false)
id=el.id
value=el.value
onChange=(action (mut el.value))
}}
<div class="input-group-btn">
<div class="input-group-append">
{{! disable delete button if there is only one option }}
{{#bs-button
onClick=(action "deleteOption" option)
type=(if
(eq el.validation "success")
"btn-success"
(if
(eq el.validation "error")
"btn-danger"
"btn-default"
)
)
disabled=(lte options.length 1)
type="link"
class="delete"
disabled=(lte options.length 1)
}}
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
<span class="oi oi-trash" title={{t "create.options.button.delete.label"}} aria-hidden="true"></span>
<span class="sr-only">{{t "create.options.button.delete.label"}}</span>
{{/bs-button}}
{{#bs-button
onClick=(action "addOption" option)
type=(if
(eq el.validation "success")
"btn-success"
(if
(eq el.validation "error")
"btn-danger"
"btn-default"
)
)
class="add"
}}
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
<span class="sr-only">{{t "create.options.button.add.label"}}</span>
{{/bs-button}}
</div>
</div>
{{#bs-button
onClick=(action "addOption" option)
type="link"
size="sm"
class="add"
}}
<span class="oi oi-plus" title={{t "create.options.button.add.label"}} aria-hidden="true"></span>
<span class="sr-only">{{t "create.options.button.add.label"}}</span>
{{/bs-button}}
{{/form.element}}
{{/each}}

View file

@ -1,4 +1,4 @@
<div class="box">
<div class="cr-form-wrapper box">
{{#bs-form
action="submit"
formLayout="horizontal"

View file

@ -1,23 +1,31 @@
<div class="row">
<div class="row cr-steps-bottom-nav">
{{#if renderPrevButton}}
<div class="col-xs-6 col-md-4 text-right">
{{bs-button
<div class="col-6 col-md-4 text-right">
{{#bs-button
onClick=(action "prev")
classNames="prev"
classNames="cr-steps-bottom-nav__button cr-steps-bottom-nav__prev-button prev"
disabled=disablePrevButton
defaultText=prevButtonText
}}
<span class="cr-steps-bottom-nav__icon oi oi-caret-left" title={{ prevButtonText }} aria-hidden="true"></span>
<span class="cr-steps-bottom-nav__label">
{{ prevButtonText }}
</span>
{{/bs-button}}
</div>
{{/if}}
{{#if renderNextButton}}
<div class={{nextButtonClassesString}}>
{{bs-button
{{#bs-button
buttonType="submit"
classNames="next"
defaultText=nextButtonText
classNames="cr-steps-bottom-nav__button cr-steps-bottom-nav__next-button next"
disabled=disableNextButton
type="primary"
}}
<span class="cr-steps-bottom-nav__label">
{{ nextButtonText }}
</span>
<span class="cr-steps-bottom-nav__icon oi oi-caret-right" title={{ nextButtonText }} aria-hidden="true"></span>
{{/bs-button}}
</div>
{{/if}}
</div>

View file

@ -1,17 +1,16 @@
{{title (t "create.title")}}
{{#bs-button-group justified=true classNames="form-steps"}}
{{#bs-button-group justified=true classNames="cr-steps-top-nav form-steps"}}
{{#each formSteps as |formStep|}}
{{#unless formStep.hidden}}
<div class="btn-group" role="group">
{{#bs-button
onClick=(transition-to formStep.route)
type=(if formStep.active "primary" "default")
disabled=formStep.disabled
}}
{{t formStep.label}}
{{/bs-button}}
</div>
{{#bs-button
onClick=(transition-to formStep.route)
type=(if formStep.active "primary is-active" "default")
disabled=formStep.disabled
classNames="cr-steps-top-nav__button"
}}
{{t formStep.label}}
{{/bs-button}}
{{/unless}}
{{/each}}
{{/bs-button-group}}

View file

@ -1,4 +1,4 @@
<div class="box">
<div class="cr-form-wrapper box">
{{#bs-form
formLayout="horizontal"
model=this

View file

@ -1,4 +1,4 @@
<div class="box">
<div class="cr-form-wrapper box">
{{#bs-form
formLayout="horizontal"
model=this

View file

@ -1,4 +1,4 @@
<div class="box">
<div class="cr-form-wrapper box">
{{#bs-form
formLayout="horizontal"
model=this
@ -18,7 +18,7 @@
tagName="select"
change=(action "updateAnswerType" value="target.value")
id=el.id
class="form-control"
class="custom-select"
}}
{{#each answerTypes as |answerType|}}
<option value={{answerType.id}} selected={{eq el.value answerType.id}}>
@ -33,12 +33,13 @@
property="expirationDuration"
showValidationOn=(array "change" "focusOut")
useIcons=false
controlType="select"
as |el|
}}
<select
id={{el.id}}
onchange={{action (mut el.value) value="target.value"}}
class="form-control"
class="custom-select"
>
{{#each expirationDurations as |duration|}}
<option value={{duration.id}} selected={{eq el.value duration.id}}>

View file

@ -1,48 +1,39 @@
<div class="index">
<div class="index cr-screen-index">
<div class="box teaser">
<h2>
<div class="jumbotron">
<h2 class="cr-claim">
{{t "index.title"}}
</h2>
{{#link-to "create" class="btn btn-primary btn-lg"}}{{t "index.link.have-a-try"}}{{/link-to}}
</div>
<div class="row">
<div class="col-md-6">
<div class="box features">
<h3>{{t "index.features.title"}}</h3>
<ul>
<li>
{{t "index.features.list.overview"}}
</li>
<li>
{{t "index.features.list.options"}}
</li>
<li>
{{t "index.features.list.answers"}}
</li>
<li>
{{t "index.features.list.evaluation"}}
</li>
<li>
{{t "index.features.list.privacy"}}
</li>
</ul>
<p class="have-a-try">
<span class="glyphicon glyphicon-share-alt"></span>
{{#link-to "create"}}{{t "index.link.have-a-try"}}{{/link-to}}
</p>
</div>
<div class="col-lg-6">
<h3>{{t "index.features.title"}}</h3>
<ul>
<li>
{{t "index.features.list.overview"}}
</li>
<li>
{{t "index.features.list.options"}}
</li>
<li>
{{t "index.features.list.answers"}}
</li>
<li>
{{t "index.features.list.evaluation"}}
</li>
<li>
{{t "index.features.list.privacy"}}
</li>
</ul>
</div>
<div class="col-md-6">
<div class="box features">
<h3>{{t "index.hoster.title"}}</h3>
<p>
{{t "index.hoster.text"}}
</p>
</div>
<div class="col-lg-5 offset-lg-1">
<h3>{{t "index.hoster.title"}}</h3>
<p>
{{t "index.hoster.text"}}
</p>
</div>
</div>
</div>

View file

@ -29,4 +29,4 @@
{{t "error.poll.unexpected.description"}}
</p>
{{/if}}
</div>
</div>

View file

@ -25,25 +25,26 @@
</p>
</div>
</div>
<div class="col-sm-6 col-lg-5 col-lg-offset-2">
<div class="box poll-link">
<div class="col-sm-6 col-lg-6 offset-lg-1">
<div class="box poll-link cr-poll-link">
<p>{{t "poll.share"}}</p>
<p class="link">
<a href={{pollUrl}}>
{{pollUrl}}
</a>
<p class="link cr-poll-link__link">
<small>
<code class="cr-poll-link__url">{{pollUrl}}</code>
</small>
{{#copy-button
clipboardText=pollUrl
classNames="btn btn-default"
classNames="btn btn-secondary cr-poll-link__copy-btn btn-sm"
success=(action "linkAction" "copied")
error=(action "linkAction" "selected")
}}
<span class="glyphicon glyphicon-copy"></span>
{{t "poll.link.copy-label"}}&nbsp;
<span class="oi oi-clipboard" title={{t "poll.link.copy-label"}} aria-hidden="true"></span>
{{/copy-button}}
</p>
<p class="notice">
<small class="text-muted">
{{t "poll.share.notice"}}
</p>
</small>
</div>
</div>
</div>
@ -61,16 +62,16 @@
</div>
{{/if}}
<div class="box container-fluid">
<div class="box">
<ul class="nav nav-tabs" role="tablist">
{{#link-to
"poll.participation"
model
tagName="li"
activeClass="active"
class="participation"
class="participation nav-item"
}}
{{#link-to "poll.participation" model}}
{{#link-to "poll.participation" model class="nav-link"}}
{{t "poll.tab-title.participation"}}
{{/link-to}}
{{/link-to}}
@ -79,9 +80,9 @@
model
tagName="li"
activeClass="active"
class="evaluation"
class="evaluation nav-item"
}}
{{#link-to "poll.evaluation" model}}
{{#link-to "poll.evaluation" model class="nav-link"}}
{{t "poll.tab-title.evaluation"}}
{{/link-to}}
{{/link-to}}

View file

@ -1,4 +1,4 @@
<div class="participation">
<div class="participation cr-form-wrapper">
{{#bs-form
onSubmit=(action "submit")
formLayout="horizontal"
@ -15,7 +15,7 @@
classNames="name"
}}
<div class="selections">
{{#each selections as |selection|}}
{{#each selections as |selection index|}}
{{#if isFreeText}}
{{form.element
controlType="text"
@ -49,15 +49,17 @@
as |el|
}}
{{#each possibleAnswers as |possibleAnswer|}}
<div class="radio {{possibleAnswer.type}}">
<label>
<input
type="radio"
value={{possibleAnswer.type}}
checked={{eq possibleAnswer.type el.value}}
onchange={{action (mut el.value) possibleAnswer.type}}
>
<span class={{possibleAnswer.icon}} aria-hidden="true"></span>
<div class="radio custom-control custom-radio custom-control-inline {{possibleAnswer.type}}">
<input
class="custom-control-input"
type="radio"
value={{possibleAnswer.type}}
checked={{eq possibleAnswer.type el.value}}
onchange={{action (mut el.value) possibleAnswer.type}}
id={{concat possibleAnswer.type index}}
name={{concat possibleAnswer.type index}}
>
<label class="custom-control-label" for={{concat possibleAnswer.type index}}>
{{possibleAnswer.label}}
</label>
</div>

View file

@ -29,7 +29,7 @@ module.exports = function(environment) {
'script-src': "'self'",
'font-src': "'self'",
'connect-src': "'self'",
'img-src': "'self'",
'img-src': "'self' data:",
'style-src': "'self'",
'media-src': "'none'",
},

View file

@ -20,8 +20,8 @@ module.exports = function(defaults) {
},
'ember-bootstrap': {
importBootstrapCSS: false,
'bootstrapVersion': 3,
'importBootstrapFont': true,
'bootstrapVersion': 4,
'importBootstrapFont': false,
whitelist: ['bs-alert', 'bs-button', 'bs-button-group', 'bs-form', 'bs-modal'],
},
'ember-cli-babel': {
@ -33,6 +33,15 @@ module.exports = function(defaults) {
'ember-math-helpers': {
only: ['lte', 'sub'],
},
autoprefixer: {
browsers: ['last 2 ios version'],
cascade: false,
sourcemap: true
},
sassOptions: {
sourceMapEmbed: true,
includePaths: ['node_modules'],
},
});
// Use `app.import` to add additional libraries to the generated
@ -48,5 +57,8 @@ module.exports = function(defaults) {
// please specify an object with the list of modules as keys
// along with the exports of each module as its value.
app.import('node_modules/open-iconic/font/fonts/open-iconic.ttf');
app.import('node_modules/open-iconic/font/fonts/open-iconic.woff');
return app.toTree();
};

View file

@ -18,11 +18,11 @@
},
"devDependencies": {
"@ember/optional-features": "^0.7.0",
"bootstrap-sass": "^3.3.7",
"bootstrap": "^4.3.1",
"ember-ajax": "^5.0.0",
"ember-auto-import": "^1.3.0",
"ember-awesome-macros": "^5.0.0",
"ember-bootstrap": "^2.6.1",
"ember-bootstrap": "^2.7.1",
"ember-bootstrap-cp-validations": "^1.0.0",
"ember-cli": "~3.8.2",
"ember-cli-acceptance-test-helpers": "^1.0.0",
@ -72,6 +72,7 @@
"eslint-plugin-ember": "^6.4.1",
"fs-extra": "^8.0.0",
"loader.js": "^4.7.0",
"open-iconic": "^1.1.1",
"qunit-dom": "^0.8.0",
"sass": "^1.19.0",
"sjcl": "^1.0.8"

View file

@ -316,15 +316,13 @@ module('Integration | Component | create options datetime', function(hooks) {
this.set('options', poll.options);
await render(hbs`{{create-options-datetime dates=options}}`);
assert.ok(
findAll('.has-error').length === 0 && findAll('.has-success').length === 0,
'does not show a validation error before user interaction'
);
assert.dom('.form-group .is-invalid').doesNotExist('does not show validation errors before user interaction');
assert.dom('.form-group .is-valid').doesNotExist('does not show validation success before user interaction');
await fillIn('[data-test-day="2015-01-01"] .form-group input', '10:');
await blur('[data-test-day="2015-01-01"] .form-group input');
assert.ok(
find('[data-test-day="2015-01-01"] .form-group').classList.contains('has-error') ||
find('[data-test-day="2015-01-01"] .form-group input').classList.contains('is-invalid') ||
// browsers with input type time support prevent non time input
find('[data-test-day="2015-01-01"] .form-group input').value === '',
'shows error after invalid input or prevents invalid input'
@ -336,22 +334,11 @@ module('Integration | Component | create options datetime', function(hooks) {
await fillIn(findAll('[data-test-day="2015-01-01"]')[1].querySelector('input'), '10:00');
await fillIn('[data-test-day="2015-02-02"] .form-group input', '10:00');
await triggerEvent('form', 'submit');
assert.dom(findAll('[data-test-day="2015-01-01"]')[0].querySelector('.form-group')).hasClass('has-success',
'first time shows validation success'
);
assert.dom(findAll('[data-test-day="2015-01-01"]')[1].querySelector('.form-group')).hasClass('has-error',
'same time for same day shows validation error'
);
assert.dom('[data-test-day="2015-02-02"] .form-group').hasClass('has-success',
'same time for different day shows validation success'
);
// label reflects validation state for all times of this day
assert.dom(find('[data-test-day="2015-01-01"]')).hasClass('label-has-error',
'label reflects validation state for all times (error)'
);
assert.dom('[data-test-day="2015-02-02"]').hasClass('label-has-success',
'label reflects validation state for all times (success)'
);
assert.dom(findAll('[data-test-day="2015-01-01"]')[0].querySelector('.form-group input'))
.hasClass('is-valid', 'first time shows validation success');
assert.dom(findAll('[data-test-day="2015-01-01"]')[1].querySelector('.form-group input'))
.hasClass('is-invalid', 'same time for same day shows validation error');
assert.dom('[data-test-day="2015-02-02"] .form-group input')
.hasClass('is-valid', 'same time for different day shows validation success');
});
});

View file

@ -1,7 +1,7 @@
import { run } from '@ember/runloop';
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, find, findAll, blur, fillIn, focus } from '@ember/test-helpers';
import { render, findAll, blur, fillIn, focus } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import hasComponent from 'croodle/tests/helpers/201-created/raw/has-component';
@ -65,35 +65,23 @@ module('Integration | Component | create options', function(hooks) {
hbs`{{create-options options=options isDateTime=isDateTime isFindADate=isFindADate isMakeAPoll=isMakeAPoll}}`
);
assert.ok(
findAll('input').length === 2,
'assumptions are correct'
);
assert.dom('.form-group').exists({ count: 2 }, 'assumption: renders two form groups');
await fillIn(findAll('input')[0], 'foo');
await blur(findAll('input')[0]);
await fillIn(findAll('input')[1], 'foo');
await blur(findAll('input')[1]);
assert.ok(
findAll('.form-group')[1].classList.contains('has-error'),
'second input field has validation error'
);
assert.ok(
findAll('.form-group')[1].querySelector('.help-block'),
'validation error is shown'
);
await fillIn('.form-group:nth-child(1) input', 'foo');
await blur('.form-group:nth-child(1) input');
await fillIn('.form-group:nth-child(2) input', 'foo');
await blur('.form-group:nth-child(2) input');
assert.dom('.form-group:nth-child(2) input')
.hasClass('is-invalid', 'second input field has validation error');
assert.dom('.form-group:nth-child(2) .invalid-feedback')
.exists('validation error is shown');
await fillIn(findAll('input')[0], 'bar');
await blur(findAll('input')[0]);
assert.ok(
findAll('.form-group .help-block').length === 0,
'there is no validation error anymore after a unique value is entered'
);
assert.ok(
findAll('.form-group.has-error').length === 0,
'has-error classes are removed'
);
assert.dom('.form-group .invalid-feedback')
.doesNotExist('there is no validation error anymore after a unique value is entered');
assert.dom('.form-group .is-invalid')
.doesNotExist('.is-invalid classes are removed');
});
test('shows validation errors if option is empty (makeAPoll)', async function(assert) {
@ -127,70 +115,14 @@ module('Integration | Component | create options', function(hooks) {
await blur(findAll('input')[0]);
await focus(findAll('input')[1]);
await blur(findAll('input')[1]);
assert.equal(
findAll('.form-group.has-error').length, 2
);
assert.dom('.form-group .invalid-feedback').exists({ count: 2 });
await fillIn(findAll('input')[0], 'foo');
await blur(findAll('input')[0]);
assert.equal(
findAll('.form-group.has-error').length, 1
);
assert.dom('.form-group .invalid-feedback').exists({ count: 1 });
await fillIn(findAll('input')[1], 'bar');
await blur(findAll('input')[1]);
assert.equal(
findAll('.form-group.has-error').length, 0
);
});
test('label reflects validation state of all inputs (makeAPoll)', async function(assert) {
this.set('isDateTime', false);
this.set('isFindADate', false);
this.set('isMakeAPoll', true);
// validation is based on validation of every option fragment
// which validates according to poll model it belongs to
// therefore each option needs to be pushed to poll model to have it as
// it's owner
let poll;
run(() => {
poll = this.store.createRecord('poll', {
isFindADate: this.get('isFindADate'),
isDateTime: this.get('isDateTime'),
isMakeAPoll: this.get('isMakeAPoll')
});
});
this.set('options', poll.get('options'));
await render(
hbs`{{create-options options=options isDateTime=isDateTime isFindADate=isFindADate isMakeAPoll=isMakeAPoll}}`
);
assert.ok(
find('form').firstElementChild.classList.contains('label-has-no-validation'),
'does not show validation state if there wasn\'t any user interaction yet'
);
await focus(findAll('input')[0]);
await blur(findAll('input')[0]);
assert.ok(
find('form').firstElementChild.classList.contains('label-has-error'),
'shows as having error if atleast on field has an error'
);
await fillIn(findAll('input')[0], 'foo');
await blur(findAll('input')[0]);
assert.ok(
find('form').firstElementChild.classList.contains('label-has-no-validation'),
'does not show validation state if no field has error but not all fields are showing error yet'
);
await fillIn(findAll('input')[1], 'bar');
await blur(findAll('input')[1]);
assert.ok(
find('form').firstElementChild.classList.contains('label-has-success'),
'shows as having success if all fields are showing success'
);
assert.dom('.form-group .invalid-feedback').doesNotExist();
});
});

View file

@ -43,7 +43,7 @@ export default create(assign({}, defaultsForCreate, {
item: {
add: clickable('button.add'),
delete: clickable('button.delete'),
hasError: hasClass('has-error'),
hasError: hasClass('is-invalid', 'input'),
title: fillable('input')
}
}),

View file

@ -13,7 +13,7 @@ const urlMatches = function(regExp) {
export const definition = {
showsExpirationWarning: isVisible('.expiration-warning'),
url: text('.poll-link .link a'),
url: text('.poll-link .link code'),
urlIsValid: urlMatches(/^\/poll\/[a-zA-Z0-9]{10}\/participation\?encryptionKey=[a-zA-Z0-9]{40}$/)
};

View file

@ -20,9 +20,9 @@ export default PageObject.create(assign({}, defaultsForApplication, Poll, {
answers: text('.selections .form-group:eq(0) .radio', { multiple: true }),
itemScope: '.selections .form-group',
item: {
label: text('label.control-label')
label: text('label')
},
labels: text('.selections .form-group label.control-label', { multiple: true })
labels: text('.selections .form-group > label', { multiple: true })
}),
title: text('h2.title'),
// use as .visit({ encryptionKey: ??? })

View file

@ -2216,10 +2216,10 @@ boom@2.x.x:
dependencies:
hoek "2.x.x"
bootstrap-sass@^3.3.7:
version "3.4.1"
resolved "https://registry.yarnpkg.com/bootstrap-sass/-/bootstrap-sass-3.4.1.tgz#6843c73b1c258a0ac5cb2cc6f6f5285b664a8e9a"
integrity sha512-p5rxsK/IyEDQm2CwiHxxUi0MZZtvVFbhWmyMOt4lLkA4bujDA1TGoKT0i1FKIWiugAdP+kK8T5KMDFIKQCLYIA==
bootstrap@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.3.1.tgz#280ca8f610504d99d7b6b4bfc4b68cec601704ac"
integrity sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag==
bower-config@^1.3.0:
version "1.4.1"
@ -4135,7 +4135,7 @@ ember-bootstrap-cp-validations@^1.0.0:
dependencies:
ember-cli-babel "^6.6.0"
ember-bootstrap@^2.6.1:
ember-bootstrap@^2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/ember-bootstrap/-/ember-bootstrap-2.7.1.tgz#44f4c9ad83c543f447796429d27ddb621d70b820"
integrity sha512-qqB0GPNZa3ypwoaI11GtrIs6Ugs3KPz/0TWG6M3eHbRb6BKTLfx3mUh0Ws0SwJDgZ5pMPsdHI4m1yDBrR9/ulQ==
@ -9026,6 +9026,11 @@ onetime@^2.0.0:
dependencies:
mimic-fn "^1.0.0"
open-iconic@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/open-iconic/-/open-iconic-1.1.1.tgz#9dcfc8c7cd3c61cdb4a236b1a347894c97adc0c6"
integrity sha1-nc/Ix808Yc20ojaxo0eJTJetwMY=
opener@~1.4.1:
version "1.4.3"
resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8"