NEW Add sass-lint configuration and refactor. Refactor JS for AirBnB ES6 syntax

This commit is contained in:
Sacha Judd 2017-08-21 12:13:19 +12:00
parent c8698d99bd
commit 51631a167a
16 changed files with 1273 additions and 1042 deletions

View File

@ -10,7 +10,7 @@ indent_style = space
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[{*.yml,package.json}] [{*.yml,package.json,*.js,*.scss}]
indent_size = 2 indent_size = 2
# The indent size used in the package.json file cannot be changed: # The indent size used in the package.json file cannot be changed:

179
.sass-lint.yml Normal file
View File

@ -0,0 +1,179 @@
# sass-lint config to match the AirBNB style guide
# See silverstripe-admin
files:
include: '**/client/src/**/*.scss'
ignore:
- 'client/src/styles/legacy/*'
- 'src/**/*'
options:
formatter: stylish
merge-default-rules: false
rules:
# Warnings
# Things that require actual refactoring are marked as warnings
class-name-format:
- 1
- convention: hyphenatedbem
placeholder-name-format:
- 1
- convention: hyphenatedlowercase
nesting-depth:
- 1
- max-depth: 3
no-ids: 1
no-important: 1
no-misspelled-properties:
- 1
- extra-properties:
- "-moz-border-radius-topleft"
- "-moz-border-radius-topright"
- "-moz-border-radius-bottomleft"
- "-moz-border-radius-bottomright"
variable-name-format:
- 1
- allow-leading-underscore: true
convention: hyphenatedlowercase
no-extends: 1
# Warnings: these things are preferential rather than mandatory
no-css-comments: 1
# Errors
# Things that can be easily fixed are marked as errors
indentation:
- 2
- size: 2
final-newline:
- 2
- include: true
no-trailing-whitespace: 2
border-zero:
- 2
- convention: '0'
brace-style:
- 2
- allow-single-line: true
clean-import-paths:
- 2
- filename-extension: false
leading-underscore: false
no-debug: 2
no-empty-rulesets: 2
no-invalid-hex: 2
no-mergeable-selectors: 2
# no-qualifying-elements:
# - 1
# - allow-element-with-attribute: false
# allow-element-with-class: false
# allow-element-with-id: false
no-trailing-zero: 2
no-url-protocols: 2
quotes:
- 2
- style: double
space-after-bang:
- 2
- include: false
space-after-colon:
- 2
- include: true
space-after-comma:
- 2
- include: true
space-before-bang:
- 2
- include: true
space-before-brace:
- 2
- include: true
space-before-colon: 2
space-between-parens:
- 2
- include: false
trailing-semicolon: 2
url-quotes: 2
zero-unit: 2
single-line-per-selector: 2
one-declaration-per-line: 2
empty-line-between-blocks:
- 2
- ignore-single-line-rulesets: true
# Missing rules
# There are no sass-lint rules for the following AirBNB style items, but thess
# - Put comments on their own line
# - Put property delcarations before mixins
# Disabled rules
# These are other rules that we may wish to consider using in the future
# They are not part of the AirBNB CSS standard but they would introduce some strictness
# bem-depth: 0
# variable-for-property: 0
# no-transition-all: 0
# hex-length:
# - 1
# - style: short
# hex-notation:
# - 1
# - style: lowercase
# property-units:
# - 1
# - global:
# - ch
# - em
# - ex
# - rem
# - cm
# - in
# - mm
# - pc
# - pt
# - px
# - q
# - vh
# - vw
# - vmin
# - vmax
# - deg
# - grad
# - rad
# - turn
# - ms
# - s
# - Hz
# - kHz
# - dpi
# - dpcm
# - dppx
# - '%'
# per-property: {}
# force-attribute-nesting: 1
# force-element-nesting: 1
# force-pseudo-nesting: 1
# function-name-format:
# - 1
# - allow-leading-underscore: true
# convention: hyphenatedlowercase
# no-color-literals: 1
# no-duplicate-properties: 1
# mixin-name-format:
# - 1
# - allow-leading-underscore: true
# convention: hyphenatedlowercase
# shorthand-values:
# - 1
# - allowed-shorthands:
# - 1
# - 2
# - 3
# leading-zero:
# - 1
# - include: false
# no-vendor-prefixes:
# - 1
# - additional-identifiers: []
# excluded-identifiers: []
# placeholder-in-extend: 1
# no-color-keywords: 2

View File

@ -1 +1 @@
!function(t){function e(i){if(n[i])return n[i].exports;var o=n[i]={i:i,l:!1,exports:{}};return t[i].call(o.exports,o,o.exports,e),o.l=!0,o.exports}var n={};e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,i){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:i})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=3)}([function(t,e){t.exports=jQuery},function(t,e,n){(function(t){!function(t){t.entwine("ss",function(t){var e;t(".uf-field-editor tbody").entwine({onmatch:function(){var n,i,o=0,s=t(".uf-field-editor .ss-gridfield-buttonrow").addClass("stickyButtons"),c=t(".cms-content-header.north").height()+parseInt(t(".stickyButtons").css("padding-top"),10),r=t(".uf-field-editor");this._super(),this.find(".ss-gridfield-item").each(function(){switch(t(this).data("class")){case"EditableFormStep":return void(o=0);case"EditableFieldGroup":i=++o;break;case"EditableFieldGroupEnd":i=o--;break;default:i=o}for(t(this).toggleClass("inFieldGroup",i>0),n=1;n<=5;n++)t(this).toggleClass("inFieldGroup-level-"+n,i>=n)}),e=setInterval(function(){var t=r.offset().top;s.width("100%"),t>c||0===t?s.removeClass("stickyButtons"):s.addClass("stickyButtons")},300)},onunmatch:function(){this._super(),clearInterval(e)}}),t(".uf-field-editor .ss-gridfield-buttonrow .action").entwine({onclick:function(t){this._super(t),this.trigger("addnewinline")}}),t(".uf-field-editor").entwine({onmatch:function(){var e=this;this._super(),this.on("addnewinline",function(){e.one("reload",function(){var n,i=e.find(".ss-gridfield-item").last();"EditableFieldGroupEnd"===i.attr("data-class")?(n=i,n.prev().find(".col-Title input").focus(),i=n.add(n.prev()),n.css("visibility","hidden")):i.find(".col-Title input").focus(),void 0!==document.createElement("div").style.animationName&&i.addClass("newField"),setTimeout(function(){i.removeClass("newField").addClass("flashBackground"),t(".cms-content-fields").scrollTop(t(".cms-content-fields")[0].scrollHeight),n&&n.css("visibility","visible")},500)})})},onummatch:function(){this._super()}})})}(t)}).call(e,n(0))},function(t,e,n){(function(t){!function(t){t(document).ready(function(){var e={updateFormatSpecificFields:function(){var e=t("#SendPlain").find('input[type="checkbox"]').is(":checked");t(".field.toggle-html-only")[e?"hide":"show"](),t(".field.toggle-plain-only")[e?"show":"hide"]()}};t.entwine("udf.recipient",function(t){t("#Form_ItemEditForm").entwine({onmatch:function(){e.updateFormatSpecificFields()},onunmatch:function(){this._super()}}),t("#SendPlain").entwine({onchange:function(){e.updateFormatSpecificFields()}})})})}(t)}).call(e,n(0))},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=(n.n(i),n(2));n.n(o)}]); !function(e){function t(i){if(n[i])return n[i].exports;var o=n[i]={i:i,l:!1,exports:{}};return e[i].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var n={};t.m=e,t.c=n,t.i=function(e){return e},t.d=function(e,n,i){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:i})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=3)}([function(e,t){e.exports=jQuery},function(e,t,n){"use strict";var i=n(0),o=n.n(i),r=this;o.a.entwine("ss",function(){var e=null;o()(".uf-field-editor tbody").entwine({onmatch:function(){var t=0,n=0,i=0,s=o()(".uf-field-editor .ss-gridfield-buttonrow").addClass("sticky-buttons"),c=o()(".cms-content-header.north").height()+parseInt(o()(".sticky-buttons").css("padding-top"),10),u=o()(".uf-field-editor");r._super(),r.find(".ss-gridfield-item").each(function(){switch(o()(r).data("class")){case"EditableFormStep":return void(i=0);case"EditableFieldGroup":n=++i;break;case"EditableFieldGroupEnd":n=i--;break;default:n=i}for(o()(r).toggleClass("infieldgroup",n>0),t=1;t<=5;t++)o()(r).toggleClass("infieldgroup-level-"+t,n>=t)}),e=setInterval(function(){var e=u.offset().top;s.width("100%"),e>c||0===e?s.removeClass("sticky-buttons"):s.addClass("sticky-buttons")},300)},onunmatch:function(){r._super(),clearInterval(e)}}),o()(".uf-field-editor .ss-gridfield-buttonrow .action").entwine({onclick:function(e){r._super(e),r.trigger("addnewinline")}}),o()(".uf-field-editor").entwine({onmatch:function(){r._super(),r.on("addnewinline",function(){r.one("reload",function(){var e=r.find(".ss-gridfield-item").last(),t=null;"EditableFieldGroupEnd"===e.attr("data-class")?(t=e,t.prev().find(".col-Title input").focus(),e=t.add(t.prev()),t.css("visibility","hidden")):e.find(".col-Title input").focus(),e.addClass("flashBackground"),o()(".cms-content-fields").scrollTop(o()(".cms-content-fields")[0].scrollHeight),t&&t.css("visibility","visible")})})},onummatch:function(){r._super()}})})},function(e,t,n){"use strict";var i=n(0),o=n.n(i),r=this;o()(document).ready(function(){var e=o()('input[name="SendPlain"]'),t={updateFormatSpecificFields:function(){var t=e.is(":checked");o()(".field.toggle-html-only")[t?"hide":"show"](),o()(".field.toggle-plain-only")[t?"show":"hide"]()}};o.a.entwine("udf.recipient",function(){o()("#Form_ItemEditForm").entwine({onmatch:function(){t.updateFormatSpecificFields()},onunmatch:function(){r._super()}}),e.entwine({onchange:function(){t.updateFormatSpecificFields()}})})})},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),n(1),n(2)}]);

File diff suppressed because one or more lines are too long

1
client/dist/js/userforms.js.map vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
@-webkit-keyframes rowSlide{0%{top:20%}to{top:80%}}@-o-keyframes rowSlide{0%{top:20%}to{top:80%}}@keyframes rowSlide{0%{top:20%}to{top:80%}}@-webkit-keyframes flashBackground{0%{background-color:#fff}10%{background-color:#dcfedd}70%{background-color:#dcfedd}}@-o-keyframes flashBackground{0%{background-color:#fff}10%{background-color:#dcfedd}70%{background-color:#dcfedd}}@keyframes flashBackground{0%{background-color:#fff}10%{background-color:#dcfedd}70%{background-color:#dcfedd}}.cms .uf-field-editor{padding-bottom:0}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item{height:46px}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item,.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item:hover{background:#fff}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item td{border-right-width:0;border-top:1px solid #eee}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item td:last-child{border-right-width:1px}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item .handle{min-height:46px}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item.newField{position:fixed;-webkit-animation:rowSlide .5s ease forwards;-o-animation:rowSlide .5s ease forwards;animation:rowSlide .5s ease forwards}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item.flashBackground{-webkit-animation:flashBackground 2s linear;-o-animation:flashBackground 2s linear;animation:flashBackground 2s linear}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item.ui-sortable-placeholder{height:50px}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item.inFieldGroup,.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item.inFieldGroup:hover{background:#f2f9fd}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item.inFieldGroup td{border-bottom:0;border-top:1px solid #eee}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item.inFieldGroup .col-reorder,.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item.inFieldGroup .handle{background:#bee0f8;border-top:0}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item.inFieldGroup.inFieldGroup-level-2 .col-reorder,.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item.inFieldGroup.inFieldGroup-level-2 .handle{background:#99cef4;border-top:0}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item.inFieldGroup.inFieldGroup-level-3 .col-reorder,.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item.inFieldGroup.inFieldGroup-level-3 .handle{background:#89bef4;border-top:0}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFormStep],.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFormStep]:hover{background:#dae2e7}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFormStep] label{font-weight:700;color:#000;font-size:1.1em}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFormStep] td{border-top:1px solid #a6b6c1}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFormStep]+.ss-gridfield-item td{border-top:1px solid #dae2e7}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFieldGroup] td,.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFormStep]+.ss-gridfield-item[data-class=EditableFieldGroup] td{border-top:1px solid #a8d7f5}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFieldGroup] label{font-weight:700;color:#444}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFieldGroupEnd] td{border-bottom:1px solid #a8d7f5}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFieldGroupEnd]+.ss-gridfield-item[data-class=EditableFieldGroupEnd]{border-top:0}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFieldGroupEnd] .col-buttons .action{display:none}.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFieldGroupEnd] label{color:#777}.cms .uf-field-editor .stickyButtons{position:fixed;top:40px;z-index:2;background:#e6eaed;-webkit-box-shadow:0 12px 4px -8px #999;box-shadow:0 12px 4px -8px #999;padding:12px;margin-left:-12px}.cms .uf-field-editor .stickyButtons button.action{margin-bottom:0}.cms .uf-field-editor .stickyButtons~.ss-gridfield-table{margin-top:40px} @-webkit-keyframes flash-background{0%{background-color:#fff}10%{background-color:#dcfedd}70%{background-color:#dcfedd}}@-o-keyframes flash-background{0%{background-color:#fff}10%{background-color:#dcfedd}70%{background-color:#dcfedd}}@keyframes flash-background{0%{background-color:#fff}10%{background-color:#dcfedd}70%{background-color:#dcfedd}}.uf-field-editor{padding-bottom:0}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item{height:46px}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item,.uf-field-editor table.ss-gridfield-table .ss-gridfield-item:hover{background:#fff}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item td{border-right-width:0;border-top:1px solid #999}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item td:last-child{border-right-width:1px}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item .handle{min-height:46px}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item.flash-background{-webkit-animation:flash-background 2s linear;-o-animation:flash-background 2s linear;animation:flash-background 2s linear}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item.ui-sortable-placeholder{height:50px}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item.infieldgroup,.uf-field-editor table.ss-gridfield-table .ss-gridfield-item.infieldgroup:hover{background:#f2f9fd}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item.infieldgroup td{border-bottom:0;border-top:1px solid #999}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item.infieldgroup .col-reorder,.uf-field-editor table.ss-gridfield-table .ss-gridfield-item.infieldgroup .handle{background:#bee0f8;border-top:0}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item.infieldgroup.infieldgroup-level-2 .col-reorder,.uf-field-editor table.ss-gridfield-table .ss-gridfield-item.infieldgroup.infieldgroup-level-2 .handle{background:#99cef4;border-top:0}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item.infieldgroup.infieldgroup-level-3 .col-reorder,.uf-field-editor table.ss-gridfield-table .ss-gridfield-item.infieldgroup.infieldgroup-level-3 .handle{background:#89bef4;border-top:0}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFormStep],.uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFormStep]:hover{background:#dae2e7}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFormStep] label{font-weight:700;color:#000;font-size:1.1em}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFormStep] td{border-top:1px solid #a6b6c1}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFormStep]+.ss-gridfield-item td{border-top:1px solid #dae2e7}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFieldGroup] td,.uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFormStep]+.ss-gridfield-item[data-class=EditableFieldGroup] td{border-top:1px solid #a8d7f5}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFieldGroup] label{font-weight:700;color:#444}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFieldGroupEnd] td{border-bottom:1px solid #a8d7f5}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFieldGroupEnd]+.ss-gridfield-item[data-class=EditableFieldGroupEnd]{border-top:0}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFieldGroupEnd] .col-buttons .action{display:none}.uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class=EditableFieldGroupEnd] label{color:#777}.uf-field-editor .sticky-buttons{position:fixed;top:40px;z-index:2;background:#999;-webkit-box-shadow:0 12px -4px #999;box-shadow:0 12px -4px #999;padding:12px;margin-left:-12px}.uf-field-editor .sticky-buttons button.action{margin-bottom:0}.uf-field-editor .sticky-buttons~.ss-gridfield-table{margin-top:40px}

View File

@ -2,24 +2,25 @@
* form builder behaviour. * form builder behaviour.
*/ */
(function($) { import $ from 'jquery';
$.entwine('ss', function($) {
var stickyHeaderInterval;
$(".uf-field-editor tbody").entwine({ $.entwine('ss', () => {
onmatch: function() { let stickyHeaderInterval = null;
var i,
thisLevel, $('.uf-field-editor tbody').entwine({
depth = 0, onmatch: () => {
$buttonrow = $('.uf-field-editor .ss-gridfield-buttonrow').addClass('stickyButtons'), let i = 0;
navHeight = $('.cms-content-header.north').height() + parseInt($('.stickyButtons').css('padding-top'), 10), let thisLevel = 0;
fieldEditor = $('.uf-field-editor'), let depth = 0;
self = this; const $buttonrow = $('.uf-field-editor .ss-gridfield-buttonrow').addClass('sticky-buttons');
const navHeight = $('.cms-content-header.north').height()
+ parseInt($('.sticky-buttons').css('padding-top'), 10);
const fieldEditor = $('.uf-field-editor');
this._super(); this._super();
// Loop through all rows and set necessary styles // Loop through all rows and set necessary styles
this.find('.ss-gridfield-item').each(function() { this.find('.ss-gridfield-item').each(() => {
switch ($(this).data('class')) { switch ($(this).data('class')) {
case 'EditableFormStep': { case 'EditableFormStep': {
depth = 0; depth = 0;
@ -38,51 +39,50 @@
} }
} }
$(this).toggleClass('inFieldGroup', thisLevel > 0); $(this).toggleClass('infieldgroup', thisLevel > 0);
for (i = 1; i <= 5; i++) { for (i = 1; i <= 5; i++) {
$(this).toggleClass('inFieldGroup-level-'+i, thisLevel >= i); $(this).toggleClass(`infieldgroup-level-${i}`, thisLevel >= i);
} }
}); });
// Make sure gridfield buttons stick to top of page when user scrolls down // Make sure gridfield buttons stick to top of page when user scrolls down
stickyHeaderInterval = setInterval(function () { stickyHeaderInterval = setInterval(() => {
var offsetTop = fieldEditor.offset().top; const offsetTop = fieldEditor.offset().top;
$buttonrow.width('100%'); $buttonrow.width('100%');
if (offsetTop > navHeight || offsetTop === 0) { if (offsetTop > navHeight || offsetTop === 0) {
$buttonrow.removeClass('stickyButtons'); $buttonrow.removeClass('sticky-buttons');
} else { } else {
$buttonrow.addClass('stickyButtons'); $buttonrow.addClass('sticky-buttons');
}; }
}, 300); }, 300);
}, },
onunmatch: function () { onunmatch: () => {
this._super(); this._super();
clearInterval(stickyHeaderInterval); clearInterval(stickyHeaderInterval);
} },
}); });
// When new fields are added.. // When new fields are added.
$('.uf-field-editor .ss-gridfield-buttonrow .action').entwine({ $('.uf-field-editor .ss-gridfield-buttonrow .action').entwine({
onclick: function (e) { onclick: (e) => {
this._super(e); this._super(e);
this.trigger('addnewinline'); this.trigger('addnewinline');
} },
}); });
$('.uf-field-editor').entwine({ $('.uf-field-editor').entwine({
onmatch: function () { onmatch: () => {
var self = this;
this._super(); this._super();
// When the 'Add field' button is clicked set a one time listener. // When the 'Add field' button is clicked set a one time listener.
// When the GridField is reloaded focus on the newly added field. // When the GridField is reloaded focus on the newly added field.
this.on('addnewinline', function () { this.on('addnewinline', () => {
self.one('reload', function () { this.one('reload', () => {
// If fieldgroup, focus on the start marker // If fieldgroup, focus on the start marker
var $newField = self.find('.ss-gridfield-item').last(), $groupEnd; let $newField = this.find('.ss-gridfield-item').last();
let $groupEnd = null;
if ($newField.attr('data-class') === 'EditableFieldGroupEnd') { if ($newField.attr('data-class') === 'EditableFieldGroupEnd') {
$groupEnd = $newField; $groupEnd = $newField;
$groupEnd.prev().find('.col-Title input').focus(); $groupEnd.prev().find('.col-Title input').focus();
@ -93,16 +93,15 @@
} }
$newField.addClass('flashBackground'); $newField.addClass('flashBackground');
$(".cms-content-fields").scrollTop($(".cms-content-fields")[0].scrollHeight); $('.cms-content-fields').scrollTop($('.cms-content-fields')[0].scrollHeight);
if ($groupEnd) { if ($groupEnd) {
$groupEnd.css('visibility', 'visible'); $groupEnd.css('visibility', 'visible');
} }
}); });
}); });
}, },
onummatch: function () { onummatch: () => {
this._super(); this._super();
} },
}); });
}); });
}(jQuery));

View File

@ -2,32 +2,32 @@
* Email recipient behaviour. * Email recipient behaviour.
*/ */
(function($) { import $ from 'jquery';
$.entwine('ss', function($) {
var recipient = { $.entwine('ss', () => {
const recipient = {
// Some fields are only visible when HTML email are being sent. // Some fields are only visible when HTML email are being sent.
updateFormatSpecificFields: function () { updateFormatSpecificFields: () => {
var sendPlainChecked = $('input[name="SendPlain"]').is(':checked'); const sendPlainChecked = $('input[name="SendPlain"]').is(':checked');
$('.field.toggle-html-only')[sendPlainChecked ? 'hide' : 'show'](); $('.field.toggle-html-only')[sendPlainChecked ? 'hide' : 'show']();
$('.field.toggle-plain-only')[sendPlainChecked ? 'show' : 'hide'](); $('.field.toggle-plain-only')[sendPlainChecked ? 'show' : 'hide']();
} },
}; };
$('#Form_ItemEditForm .EmailRecipientForm').entwine({ $('#Form_ItemEditForm .EmailRecipientForm').entwine({
onmatch: function () { onmatch: () => {
recipient.updateFormatSpecificFields(); recipient.updateFormatSpecificFields();
}, },
onunmatch: function () { onunmatch: () => {
this._super(); this._super();
} },
}); });
$('#Form_ItemEditForm .EmailRecipientForm input[name="SendPlain"]').entwine({ $('#Form_ItemEditForm .EmailRecipientForm input[name="SendPlain"]').entwine({
onchange: function () { onchange: () => {
recipient.updateFormatSpecificFields(); recipient.updateFormatSpecificFields();
} },
}); });
}); });
}(jQuery));

View File

@ -1,29 +1,432 @@
/** /**
* @file Manages the multi-step navigation. * @file Manages the multi-step navigation.
*/ */
jQuery(function ($) {
import $ from 'jquery';
$(document).ready(() => {
// A reference to the UserForm instance. // A reference to the UserForm instance.
var userform = null; let userform = null;
// Settings that come from the CMS. // Settings that come from the CMS.
var CONSTANTS = {}; const CONSTANTS = {};
// Common functions that extend multiple classes. // Common functions that extend multiple classes.
var commonMixin = { const commonMixin = {
/** /**
* @func show * @func show
* @desc Show the form step. Looks after aria attributes too. * @desc Show the form step. Looks after aria attributes too.
*/ */
show: function () { show: () => {
this.$el.attr('aria-hidden', false).show(); this.$el.attr('aria-hidden', false).show();
}, },
/** /**
* @func hide * @func hide
* @desc Hide the form step. Looks after aria attributes too. * @desc Hide the form step. Looks after aria attributes too.
*/ */
hide: function () { hide: () => {
this.$el.attr('aria-hidden', true).hide(); this.$el.attr('aria-hidden', true).hide();
},
};
/**
* @func ErrorContainer
* @constructor
* @param {object} element - The error container element.
* @return {object} - The ErrorContainer instance.
* @desc Creates an error container. Used to display step error messages at the top.
*/
function ErrorContainer(element) {
this.$el = element instanceof $ ? element : $(element);
// Set the error container's heading.
this.$el.find('h4').text(window.ss.i18n._t('UserForms.ERROR_CONTAINER_HEADER',
'Please correct the following errors and try again:'));
return this;
}
/**
* @func hasErrors
* @return boolean
* @desc Checks if the error container has any error messages.
*/
ErrorContainer.prototype.hasErrors = () => (
this.$el.find('.error-list').children().length > 0
);
/**
* @func removeErrorMessage
* @desc Removes an error message from the error container.
*/
ErrorContainer.prototype.removeErrorMessage = (fieldId) => {
this.$el.find(`#${fieldId}-top-error`).remove();
// If there are no more error then hide the container.
if (!this.hasErrors()) {
this.hide();
}
};
/**
* @func addStepLink
* @param {object} step - FormStep instance.
* @desc Adds a link to a form step as an error message.
*/
ErrorContainer.prototype.addStepLink = (step) => {
const itemID = `${step.$el.attr('id')}-error-link`;
let $itemElement = this.$el.find(`#${itemID}`);
const stepID = step.$el.attr('id');
const stepTitle = step.$el.data('title');
// If the item already exists we don't need to do anything.
if ($itemElement.length) {
return;
}
$itemElement = $(`<li id="${itemID}"><a href="#${stepID}">${stepTitle}</a></li>`);
$itemElement.on('click', (e) => {
e.preventDefault();
userform.jumpToStep(step.id);
});
this.$el.find('.error-list').append($itemElement);
};
/**
* @func removeStepLink
* @param {object} step - FormStep instance.
* @desc Removes a step link from the error container.
*/
ErrorContainer.prototype.removeStepLink = (fieldId) => {
const stepID = $(`#${fieldId}`).closest('.form-step').attr('id');
this.$el.find(`#${stepID}-error-link`).remove();
// Hide the error container if we've just removed the last error.
if (this.$el.find('.error-list').is(':empty')) {
this.hide();
}
};
/**
* @func ErrorContainer.updateErrorMessage
* @param {object} $input - The jQuery input object which contains the field to validate.
* @param {object} message - The error message to display (html escaped).
* @desc Update an error message (displayed at the top of the form).
*/
ErrorContainer.prototype.updateErrorMessage = ($input, message) => {
const inputID = $input.attr('id');
let anchor = `#${inputID}`;
const elementID = `${inputID}-top-error`;
let messageElement = $(`#${elementID}`);
let describedBy = $input.attr('aria-describedby');
// The 'message' param will be an empty string if the field is valid.
if (!message) {
// Style issues as fixed if they already exist
messageElement.addClass('fixed');
return;
}
messageElement.removeClass('fixed');
this.show();
if (messageElement.length === 1) {
// Update the existing error message.
messageElement.show().find('a').html(message);
} else {
// Generate better link to field
$input.closest('.field[id]').each(() => {
anchor = `#${$(this).attr('id')}`;
});
// Add a new error message
messageElement = $('<li><a></a></li>');
messageElement
.attr('id', elementID)
.find('a')
.attr('href', location.pathname + location.search + anchor)
.html(message);
this.$el.find('ul').append(messageElement);
// Link back to original input via aria
// Respect existing non-error aria-describedby
if (!describedBy) {
describedBy = elementID;
} else if (!describedBy.match(new RegExp(`\\b${elementID}\\b`))) {
// Add to end of list if not already present
describedBy += ` ${elementID}`;
}
$input.attr('aria-describedby', describedBy);
}
};
/**
* @func FormStep
* @constructor
* @param {object} element
* @return {object} - The FormStep instance.
* @desc Creates a form step.
*/
function FormStep(element) {
const self = this;
this.$el = element instanceof $ ? element : $(element);
// Find button for this step
this.$elButton = $(`.step-button-wrapper[data-for='${this.$el.prop('id')}]`);
// Has the step been viewed by the user?
this.viewed = false;
// Is the form step valid?
// This value is used on form submission, which fails, if any of the steps are invalid.
this.valid = false;
// The internal id of the step. Used for getting the step from the UserForm.steps array.
this.id = null;
this.hide();
if (CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP) {
this.errorContainer = new ErrorContainer(this.$el.find('.error-container'));
// Listen for errors on the UserForm.
userform.$el.on('userform.form.error', (e, validator) => {
// The step only cares about errors if it's currently visible.
if (!self.$el.is(':visible')) {
return;
}
// Add or update each error in the list.
$.each(validator.errorList, (i, error) => {
self.errorContainer.updateErrorMessage($(error.element), error.message);
});
});
// Listen for fields becoming valid
userform.$el.on('userform.form.valid', (e, fieldId) => {
self.errorContainer.removeErrorMessage(fieldId);
});
}
// Ensure that page visibilty updates the step navigation
this
.$elButton
.on('userform.field.hide userform.field.show', () => {
userform.$el.trigger('userform.form.conditionalstep');
});
return this;
}
/**
* Determine if this step is conditionally disabled
*
* @returns {Boolean}
*/
// Because the element itself could be visible but 0 height, so check visibility of button
FormStep.prototype.conditionallyHidden = () => (
!this.$elButton.find('button').is(':visible')
);
/**
* @func ProgressBar
* @constructor
* @param {object} element
* @return {object} - The Progress bar instance.
* @desc Creates a progress bar.
*/
function ProgressBar(element) {
const self = this;
this.$el = element instanceof $ ? element : $(element);
this.$buttons = this.$el.find('.step-button-jump');
this.$jsAlign = this.$el.find('.js-align');
// Update the progress bar when 'step' buttons are clicked.
this.$buttons.each((i, stepButton) => {
$(stepButton).on('click', (e) => {
e.preventDefault();
self.$el.trigger('userform.progress.changestep', [parseInt($(this).data('step'), 10)]);
});
});
// Update the progress bar when 'prev' and 'next' buttons are clicked.
userform.$el.on('userform.form.changestep', (e, stepID) => {
self.update(stepID);
});
// Listen for steps being conditionally shown / hidden by display rules.
// We need to update step related UI like the number of step buttons
// and any text that shows the total number of steps.
userform.$el.on('userform.form.conditionalstep', () => {
// Update the step numbers on the buttons.
const $visibleButtons = self.$buttons.filter(':visible');
$visibleButtons.each((i, button) => {
$(button).text(i + 1);
});
// Update the actual progress bar.
self.$el.find('.progress-bar').attr('aria-valuemax', $visibleButtons.length);
// Update any text that uses the total number of steps.
self.$el.find('.total-step-number').text($visibleButtons.length);
});
// Spaces out the steps below progress bar evenly
this.$jsAlign.each((index, button) => {
const $button = $(button);
const leftPercent = (100 / (self.$jsAlign.length - 1) * `${index}%`);
const buttonOffset = -1 * ($button.innerWidth() / 2);
$button.css({ left: leftPercent, marginLeft: buttonOffset });
// First and last buttons are kept within userform-progress container
if (index === self.$jsAlign.length - 1) {
$button.css({ marginLeft: buttonOffset * 2 });
} else if (index === 0) {
$button.css({ marginLeft: 0 });
}
});
this.update(0);
return this;
}
/**
* @func ProgressBar.update
* @param {number} stepID - Zero based index of the new step.
* @desc Update the progress element to show a new step.
*/
ProgressBar.prototype.update = (stepID) => {
const $newStepElement = $($('.form-step')[stepID]);
let stepNumber = 0;
let barWidth = stepID / (this.$buttons.length - 1) * 100;
// Set the current step number.
this.$buttons.each((i, button) => {
if (i > stepID) {
// Break the loop
return false;
}
if ($(button).is(':visible')) {
stepNumber += 1;
}
return true;
});
// Update elements that contain the current step number.
this.$el.find('.current-step-number').each((i, element) => {
$(element).text(stepNumber);
});
// Update aria attributes.
this.$el.find('[aria-valuenow]').each((i, element) => {
$(element).attr('aria-valuenow', stepNumber);
});
// Update the CSS classes on step buttons.
this.$buttons.each((i, element) => {
const $element = $(element);
const $item = $element.parent();
if (parseInt($element.data('step'), 10) === stepNumber && $element.is(':visible')) {
$item.addClass('current viewed');
$element.removeAttr('disabled');
return;
}
$item.removeClass('current');
});
// Update the progress bar's title with the new step's title.
this.$el.siblings('.progress-title').text($newStepElement.data('title'));
// Update the width of the progress bar.
barWidth = barWidth ? `${barWidth}%` : '';
this.$el.find('.progress-bar').width(barWidth);
};
/**
* @func FormActions
* @constructor
* @param {object} element
* @desc Creates the navigation and actions (Prev, Next, Submit buttons).
*/
function FormActions(element) {
const self = this;
this.$el = element instanceof $ ? element : $(element);
this.$prevButton = this.$el.find('.step-button-prev');
this.$nextButton = this.$el.find('.step-button-next');
// Show the buttons.
this.$prevButton.parent().attr('aria-hidden', false).show();
this.$nextButton.parent().attr('aria-hidden', false).show();
// Bind the step navigation event listeners.
this.$prevButton.on('click', (e) => {
e.preventDefault();
self.$el.trigger('userform.action.prev');
});
this.$nextButton.on('click', (e) => {
e.preventDefault();
self.$el.trigger('userform.action.next');
});
// Listen for changes to the current form step, or conditional pages,
// so we can show hide buttons appropriately.
userform.$el.on('userform.form.changestep userform.form.conditionalstep', () => {
self.update();
});
this.update();
return this;
}
/**
* @func FormActions.update
* @param {number} stepID - Zero based ID of the current step.
* @desc Updates the form actions element to reflect the current state of the page.
*/
FormActions.prototype.update = () => {
const numberOfSteps = userform.steps.length;
const stepID = userform.currentStep ? userform.currentStep.id : 0;
let i = null;
let lastStep = null;
// Update the "Prev" button.
this.$el.find('.step-button-prev')[stepID === 0 ? 'hide' : 'show']();
// Find last step, skipping hidden ones
for (i = numberOfSteps - 1; i >= 0; i--) {
lastStep = userform.steps[i];
// Skip if step is hidden
if (lastStep.conditionallyHidden()) {
continue;
}
// Update the "Next" button.
this.$el.find('.step-button-next')[stepID >= i ? 'hide' : 'show']();
// Update the "Actions".
this.$el.find('.Actions')[stepID >= i ? 'show' : 'hide']();
// Stop processing last step
break;
} }
}; };
@ -35,38 +438,38 @@ jQuery(function ($) {
* @desc The form * @desc The form
*/ */
function UserForm(element) { function UserForm(element) {
var self = this; const self = this;
this.$el = element instanceof jQuery ? element : $(element); this.$el = element instanceof $ ? element : $(element);
this.steps = []; this.steps = [];
// Add an error container which displays a list of invalid steps on form submission. // Add an error container which displays a list of invalid steps on form submission.
this.errorContainer = new ErrorContainer(this.$el.children('.error-container')); this.errorContainer = new ErrorContainer(this.$el.children('.error-container'));
// Listen for events triggered by form steps. // Listen for events triggered by form steps.
this.$el.on('userform.action.prev', function (e) { this.$el.on('userform.action.prev', () => {
self.prevStep(); self.prevStep();
}); });
this.$el.on('userform.action.next', function (e) { this.$el.on('userform.action.next', () => {
self.nextStep(); self.nextStep();
}); });
// Listen for events triggered by the progress bar. // Listen for events triggered by the progress bar.
$('#userform-progress').on('userform.progress.changestep', function (e, stepNumber) { $('#userform-progress').on('userform.progress.changestep', (e, stepNumber) => {
self.jumpToStep(stepNumber - 1); self.jumpToStep(stepNumber - 1);
}); });
// When a field becomes valid, remove errors from the error container. // When a field becomes valid, remove errors from the error container.
this.$el.on('userform.form.valid', function (e, fieldId) { this.$el.on('userform.form.valid', (e, fieldId) => {
self.errorContainer.removeStepLink(fieldId); self.errorContainer.removeStepLink(fieldId);
}); });
this.$el.validate(this.validationOptions); this.$el.validate(this.validationOptions);
// Ensure checkbox groups are validated correctly // Ensure checkbox groups are validated correctly
$('.optionset.requiredField input').each(function() { $('.optionset.requiredField input').each(() => {
$(this).rules('add', { $(this).rules('add', {
required: true required: true,
}); });
}); });
@ -80,7 +483,7 @@ jQuery(function ($) {
ignore: ':hidden,ul', ignore: ':hidden,ul',
errorClass: 'error', errorClass: 'error',
errorElement: 'span', errorElement: 'span',
errorPlacement: function (error, element) { errorPlacement: (error, element) => {
error.addClass('message'); error.addClass('message');
if (element.is(':radio') || element.parents('.checkboxset').length > 0) { if (element.is(':radio') || element.parents('.checkboxset').length > 0) {
@ -91,25 +494,25 @@ jQuery(function ($) {
error.insertAfter(element); error.insertAfter(element);
} }
}, },
invalidHandler: function (event, validator) { invalidHandler: (event, validator) => {
// setTimeout 0 so it runs after errorPlacement // setTimeout 0 so it runs after errorPlacement
setTimeout(function () { setTimeout(() => {
validator.currentElements.filter('.error').first().focus(); validator.currentElements.filter('.error').first().focus();
}, 0); }, 0);
}, },
// Callback for handling the actual submit when the form is valid. // Callback for handling the actual submit when the form is valid.
// Submission in the jQuery.validate sence is handled at step level. // Submission in the jQuery.validate sence is handled at step level.
// So when the final step is submitted we have to also check all previous steps are valid. // So when the final step is submitted we have to also check all previous steps are valid.
submitHandler: function (form, e) { submitHandler: (form) => {
var isValid = true; let isValid = true;
// validate the current step // Validate the current step
if (userform.currentStep) { if (userform.currentStep) {
userform.currentStep.valid = $(form).valid(); userform.currentStep.valid = $(form).valid();
} }
// Check for invalid previous steps. // Check for invalid previous steps.
$.each(userform.steps, function (i, step) { $.each(userform.steps, (i, step) => {
if (!step.valid && !step.conditionallyHidden()) { if (!step.valid && !step.conditionallyHidden()) {
isValid = false; isValid = false;
userform.errorContainer.addStepLink(step); userform.errorContainer.addStepLink(step);
@ -117,7 +520,6 @@ jQuery(function ($) {
}); });
if (isValid) { if (isValid) {
// When using the "are you sure?" plugin, ensure the form immediately submits. // When using the "are you sure?" plugin, ensure the form immediately submits.
$(form).removeClass('dirty'); $(form).removeClass('dirty');
@ -127,9 +529,9 @@ jQuery(function ($) {
} }
}, },
// When a field becomes valid. // When a field becomes valid.
success: function (error) { success: (error) => {
var errorId = $(error).attr('id'), const errorId = $(error).attr('id');
fieldId = errorId.substr(0, errorId.indexOf('-error')).replace(/[\\[\\]]/, ''); const fieldId = errorId.substr(0, errorId.indexOf('-error')).replace(/[\\[\\]]/, '');
// Remove square brackets since jQuery.validate.js uses idOrName, // Remove square brackets since jQuery.validate.js uses idOrName,
// which breaks further on when using a selector that end with // which breaks further on when using a selector that end with
@ -139,7 +541,7 @@ jQuery(function ($) {
// Pass the field's ID with the event. // Pass the field's ID with the event.
userform.$el.trigger('userform.form.valid', [fieldId]); userform.$el.trigger('userform.form.valid', [fieldId]);
} },
}; };
/** /**
@ -147,12 +549,13 @@ jQuery(function ($) {
* @param {object} step - An instance of FormStep. * @param {object} step - An instance of FormStep.
* @desc Adds a step to the UserForm. * @desc Adds a step to the UserForm.
*/ */
UserForm.prototype.addStep = function (step) { UserForm.prototype.addStep = (step) => {
// Make sure we're dealing with a form step. // Make sure we're dealing with a form step.
if (!step instanceof FormStep) { if (!step instanceof FormStep) {
return; return;
} }
// eslint-disable-next-line no-param-reassign
step.id = this.steps.length; step.id = this.steps.length;
this.steps.push(step); this.steps.push(step);
@ -163,7 +566,7 @@ jQuery(function ($) {
* @param {object} step - An instance of FormStep. * @param {object} step - An instance of FormStep.
* @desc Sets the step the user is currently on. * @desc Sets the step the user is currently on.
*/ */
UserForm.prototype.setCurrentStep = function (step) { UserForm.prototype.setCurrentStep = (step) => {
// Make sure we're dealing with a form step. // Make sure we're dealing with a form step.
if (!(step instanceof FormStep)) { if (!(step instanceof FormStep)) {
return; return;
@ -173,8 +576,8 @@ jQuery(function ($) {
this.currentStep.show(); this.currentStep.show();
// Record the user has viewed the step. // Record the user has viewed the step.
step.viewed = true; this.currentStep.viewed = true;
step.$el.addClass('viewed'); this.currentStep.$el.addClass('viewed');
}; };
/** /**
@ -183,10 +586,10 @@ jQuery(function ($) {
* @param {boolean} [direction] - Defaults to forward (true). * @param {boolean} [direction] - Defaults to forward (true).
* @desc Jumps to a specific form step. * @desc Jumps to a specific form step.
*/ */
UserForm.prototype.jumpToStep = function (stepNumber, direction) { UserForm.prototype.jumpToStep = (stepNumber, direction) => {
var targetStep = this.steps[stepNumber], const targetStep = this.steps[stepNumber];
isValid = false, let isValid = false;
forward = direction === void 0 ? true : direction; const forward = direction === void 0 ? true : direction;
// Make sure the target step exists. // Make sure the target step exists.
if (targetStep === void 0) { if (targetStep === void 0) {
@ -229,7 +632,7 @@ jQuery(function ($) {
* @func UserForm.nextStep * @func UserForm.nextStep
* @desc Advances the form to the next step. * @desc Advances the form to the next step.
*/ */
UserForm.prototype.nextStep = function () { UserForm.prototype.nextStep = () => {
this.jumpToStep(this.steps.indexOf(this.currentStep) + 1, true); this.jumpToStep(this.steps.indexOf(this.currentStep) + 1, true);
}; };
@ -237,419 +640,16 @@ jQuery(function ($) {
* @func UserForm.prevStep * @func UserForm.prevStep
* @desc Goes back one step (not bound to browser history). * @desc Goes back one step (not bound to browser history).
*/ */
UserForm.prototype.prevStep = function () { UserForm.prototype.prevStep = () => {
this.jumpToStep(this.steps.indexOf(this.currentStep) - 1, false); this.jumpToStep(this.steps.indexOf(this.currentStep) - 1, false);
}; };
/**
* @func ErrorContainer
* @constructor
* @param {object} element - The error container element.
* @return {object} - The ErrorContainer instance.
* @desc Creates an error container. Used to display step error messages at the top.
*/
function ErrorContainer(element) {
this.$el = element instanceof jQuery ? element : $(element);
// Set the error container's heading.
this.$el.find('h4').text(ss.i18n._t('UserForms.ERROR_CONTAINER_HEADER', 'Please correct the following errors and try again:'));
return this;
}
/**
* @func hasErrors
* @return boolean
* @desc Checks if the error container has any error messages.
*/
ErrorContainer.prototype.hasErrors = function () {
return this.$el.find('.error-list').children().length > 0;
};
/**
* @func removeErrorMessage
* @desc Removes an error message from the error container.
*/
ErrorContainer.prototype.removeErrorMessage = function (fieldId) {
this.$el.find('#' + fieldId + '-top-error').remove();
// If there are no more error then hide the container.
if (!this.hasErrors()) {
this.hide();
}
};
/**
* @func addStepLink
* @param {object} step - FormStep instance.
* @desc Adds a link to a form step as an error message.
*/
ErrorContainer.prototype.addStepLink = function (step) {
var self = this,
itemID = step.$el.attr('id') + '-error-link',
$itemElement = this.$el.find('#' + itemID),
stepID = step.$el.attr('id'),
stepTitle = step.$el.data('title');
// If the item already exists we don't need to do anything.
if ($itemElement.length) {
return;
}
$itemElement = $('<li id="' + itemID + '"><a href="#' + stepID + '">' + stepTitle + '</a></li>');
$itemElement.on('click', function (e) {
e.preventDefault();
userform.jumpToStep(step.id);
});
this.$el.find('.error-list').append($itemElement);
};
/**
* @func removeStepLink
* @param {object} step - FormStep instance.
* @desc Removes a step link from the error container.
*/
ErrorContainer.prototype.removeStepLink = function (fieldId) {
var stepID = $('#' + fieldId).closest('.form-step').attr('id');
this.$el.find('#' + stepID + '-error-link').remove();
// Hide the error container if we've just removed the last error.
if (this.$el.find('.error-list').is(':empty')) {
this.hide();
}
};
/**
* @func ErrorContainer.updateErrorMessage
* @param {object} $input - The jQuery input object which contains the field to validate.
* @param {object} message - The error message to display (html escaped).
* @desc Update an error message (displayed at the top of the form).
*/
ErrorContainer.prototype.updateErrorMessage = function ($input, message) {
var inputID = $input.attr('id'),
anchor = '#' + inputID,
elementID = inputID + '-top-error',
messageElement = $('#' + elementID),
describedBy = $input.attr('aria-describedby');
// The 'message' param will be an empty string if the field is valid.
if (!message) {
// Style issues as fixed if they already exist
messageElement.addClass('fixed');
return;
}
messageElement.removeClass('fixed');
this.show();
if (messageElement.length === 1) {
// Update the existing error message.
messageElement.show().find('a').html(message);
} else {
// Generate better link to field
$input.closest('.field[id]').each(function(){
anchor = '#' + $(this).attr('id');
});
// Add a new error message
messageElement = $('<li><a></a></li>');
messageElement
.attr('id', elementID)
.find('a')
.attr('href', location.pathname + location.search + anchor)
.html(message);
this.$el.find('ul').append(messageElement);
// link back to original input via aria
// Respect existing non-error aria-describedby
if (!describedBy) {
describedBy = elementID;
} else if (!describedBy.match(new RegExp('\\b' + elementID + '\\b'))) {
// Add to end of list if not already present
describedBy += " " + elementID;
}
$input.attr('aria-describedby', describedBy);
}
};
/**
* @func FormStep
* @constructor
* @param {object} element
* @return {object} - The FormStep instance.
* @desc Creates a form step.
*/
function FormStep(element) {
var self = this;
this.$el = element instanceof jQuery ? element : $(element);
// Find button for this step
this.$elButton = $(".step-button-wrapper[data-for='" + this.$el.prop('id') + "']");
// Has the step been viewed by the user?
this.viewed = false;
// Is the form step valid?
// This value is used on form submission, which fails, if any of the steps are invalid.
this.valid = false;
// The internal id of the step. Used for getting the step from the UserForm.steps array.
this.id = null;
this.hide();
if (CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP) {
this.errorContainer = new ErrorContainer(this.$el.find('.error-container'));
// Listen for errors on the UserForm.
userform.$el.on('userform.form.error', function (e, validator) {
// The step only cares about errors if it's currently visible.
if (!self.$el.is(':visible')) {
return;
}
// Add or update each error in the list.
$.each(validator.errorList, function (i, error) {
self.errorContainer.updateErrorMessage($(error.element), error.message);
});
});
// Listen for fields becoming valid
userform.$el.on('userform.form.valid', function (e, fieldId) {
self.errorContainer.removeErrorMessage(fieldId);
});
}
// Ensure that page visibilty updates the step navigation
this
.$elButton
.on('userform.field.hide userform.field.show', function(){
userform.$el.trigger('userform.form.conditionalstep');
});
return this;
}
/**
* Determine if this step is conditionally disabled
*
* @returns {Boolean}
*/
FormStep.prototype.conditionallyHidden = function(){
// Because the element itself could be visible but 0 height, so check visibility of button
return ! this
.$elButton
.find('button')
.is(':visible');
};
/**
* @func ProgressBar
* @constructor
* @param {object} element
* @return {object} - The Progress bar instance.
* @desc Creates a progress bar.
*/
function ProgressBar(element) {
var self = this;
this.$el = element instanceof jQuery ? element : $(element);
this.$buttons = this.$el.find('.step-button-jump');
this.$jsAlign = this.$el.find('.js-align');
// Update the progress bar when 'step' buttons are clicked.
this.$buttons.each(function (i, stepButton) {
$(stepButton).on('click', function (e) {
e.preventDefault();
self.$el.trigger('userform.progress.changestep', [parseInt($(this).data('step'), 10)]);
});
});
// Update the progress bar when 'prev' and 'next' buttons are clicked.
userform.$el.on('userform.form.changestep', function (e, stepID) {
self.update(stepID);
});
// Listen for steps being conditionally shown / hidden by display rules.
// We need to update step related UI like the number of step buttons
// and any text that shows the total number of steps.
userform.$el.on('userform.form.conditionalstep', function () {
// Update the step numbers on the buttons.
var $visibleButtons = self.$buttons.filter(':visible');
$visibleButtons.each(function (i, button) {
$(button).text(i + 1);
});
// Update the actual progress bar.
self.$el.find('.progress-bar').attr('aria-valuemax', $visibleButtons.length);
// Update any text that uses the total number of steps.
self.$el.find('.total-step-number').text($visibleButtons.length);
});
// Spaces out the steps below progress bar evenly
this.$jsAlign.each(function (index, button) {
var $button = $(button),
leftPercent = (100 / (self.$jsAlign.length - 1) * index + '%'),
buttonOffset = -1 * ($button.innerWidth() / 2);
$button.css({left: leftPercent, marginLeft: buttonOffset});
// First and last buttons are kept within userform-progress container
if (index === self.$jsAlign.length - 1) {
$button.css({marginLeft: buttonOffset * 2});
} else if (index === 0) {
$button.css({marginLeft: 0});
}
});
this.update(0);
return this;
}
/**
* @func ProgressBar.update
* @param {number} stepID - Zero based index of the new step.
* @desc Update the progress element to show a new step.
*/
ProgressBar.prototype.update = function (stepID) {
var $newStepElement = $($('.form-step')[stepID]),
stepNumber = 0,
barWidth = stepID / (this.$buttons.length - 1) * 100;
// Set the current step number.
this.$buttons.each(function (i, button) {
if (i > stepID) {
return false; // break the loop
}
if ($(button).is(':visible')) {
stepNumber += 1;
}
});
// Update elements that contain the current step number.
this.$el.find('.current-step-number').each(function (i, element) {
$(element).text(stepNumber);
});
// Update aria attributes.
this.$el.find('[aria-valuenow]').each(function (i, element) {
$(element).attr('aria-valuenow', stepNumber);
});
// Update the CSS classes on step buttons.
this.$buttons.each(function (i, element) {
var $element = $(element),
$item = $element.parent();
if (parseInt($element.data('step'), 10) === stepNumber && $element.is(':visible')) {
$item.addClass('current viewed');
$element.removeAttr('disabled');
return;
}
$item.removeClass('current');
});
// Update the progress bar's title with the new step's title.
this.$el.siblings('.progress-title').text($newStepElement.data('title'));
// Update the width of the progress bar.
barWidth = barWidth ? barWidth + '%' : '';
this.$el.find('.progress-bar').width(barWidth);
};
/**
* @func FormActions
* @constructor
* @param {object} element
* @desc Creates the navigation and actions (Prev, Next, Submit buttons).
*/
function FormActions (element) {
var self = this;
this.$el = element instanceof jQuery ? element : $(element);
this.$prevButton = this.$el.find('.step-button-prev');
this.$nextButton = this.$el.find('.step-button-next');
// Show the buttons.
this.$prevButton.parent().attr('aria-hidden', false).show();
this.$nextButton.parent().attr('aria-hidden', false).show();
// Bind the step navigation event listeners.
this.$prevButton.on('click', function (e) {
e.preventDefault();
self.$el.trigger('userform.action.prev');
});
this.$nextButton.on('click', function (e) {
e.preventDefault();
self.$el.trigger('userform.action.next');
});
// Listen for changes to the current form step, or conditional pages,
// so we can show hide buttons appropriatly.
userform.$el.on('userform.form.changestep userform.form.conditionalstep', function () {
self.update();
});
this.update();
return this;
}
/**
* @func FormAcrions.update
* @param {number} stepID - Zero based ID of the current step.
* @desc Updates the form actions element to reflect the current state of the page.
*/
FormActions.prototype.update = function () {
var numberOfSteps = userform.steps.length,
stepID = userform.currentStep ? userform.currentStep.id : 0,
i, lastStep;
// Update the "Prev" button.
this.$el.find('.step-button-prev')[stepID === 0 ? 'hide' : 'show']();
// Find last step, skipping hidden ones
for(i = numberOfSteps - 1; i >= 0; i--) {
lastStep = userform.steps[i];
// Skip if step is hidden
if(lastStep.conditionallyHidden()) {
continue;
}
// Update the "Next" button.
this.$el.find('.step-button-next')[stepID >= i ? 'hide' : 'show']();
// Update the "Actions".
this.$el.find('.Actions')[stepID >= i ? 'show' : 'hide']();
// Stop processing last step
break;
}
};
/** /**
* @func main * @func main
* @desc Bootstraps the front-end. * @desc Bootstraps the front-end.
*/ */
function main() { function main() {
var progressBar = null, const $userform = $('.userform');
formActions = null,
$userform = $('.userform');
// If there's no userform, do nothing. // If there's no userform, do nothing.
if ($userform.length === 0) { if ($userform.length === 0) {
@ -663,17 +663,17 @@ jQuery(function ($) {
// that are set by the user in the CMS. // that are set by the user in the CMS.
if (CONSTANTS.ENABLE_LIVE_VALIDATION === false) { if (CONSTANTS.ENABLE_LIVE_VALIDATION === false) {
$.extend(UserForm.prototype.validationOptions, { $.extend(UserForm.prototype.validationOptions, {
onfocusout: false onfocusout: false,
}); });
} }
if (CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP) { if (CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP) {
$.extend(UserForm.prototype.validationOptions, { $.extend(UserForm.prototype.validationOptions, {
// Callback for custom code when an invalid form / step is submitted. // Callback for custom code when an invalid form / step is submitted.
invalidHandler: function (event, validator) { invalidHandler: (event, validator) => {
$userform.trigger('userform.form.error', [validator]); $userform.trigger('userform.form.error', [validator]);
}, },
onfocusout: false onfocusout: false,
}); });
} }
@ -688,17 +688,17 @@ jQuery(function ($) {
// Conditionally hide field labels and use HTML5 placeholder instead. // Conditionally hide field labels and use HTML5 placeholder instead.
if (CONSTANTS.HIDE_FIELD_LABELS) { if (CONSTANTS.HIDE_FIELD_LABELS) {
$userform.find('label.left').each(function () { $userform.find('label.left').each(() => {
var $label = $(this); const $label = $(this);
$('[name="' + $label.attr('for') + '"]').attr('placeholder', $label.text()); $(`[name="${$label.attr('for')}"]`).attr('placeholder', $label.text());
$label.remove(); $label.remove();
}); });
} }
// Initialise the form steps. // Initialise the form steps.
userform.$el.find('.form-step').each(function (i, element) { userform.$el.find('.form-step').each((i, element) => {
var step = new FormStep(element); const step = new FormStep(element);
userform.addStep(step); userform.addStep(step);
}); });
@ -706,12 +706,14 @@ jQuery(function ($) {
userform.setCurrentStep(userform.steps[0]); userform.setCurrentStep(userform.steps[0]);
// Initialise actions and progressbar // Initialise actions and progressbar
progressBar = new ProgressBar($('#userform-progress')); // @todo Commented out because they appear unused - are they expected to be exported to the
formActions = new FormActions($('#step-navigation')); // global scope? Check this works on the frontend
// const progressBar = new ProgressBar($('#userform-progress'));
// const formActions = new FormActions($('#step-navigation'));
// Enable jQuery UI datepickers // Enable jQuery UI datepickers
$(document).on('click', 'input.text[data-showcalendar]', function() { $(document).on('click', 'input.text[data-showcalendar]', () => {
var $element = $(this); const $element = $(this);
$element.ssDatepicker(); $element.ssDatepicker();
@ -721,15 +723,15 @@ jQuery(function ($) {
}); });
// Make sure the form doesn't expire on the user. Pings every 3 mins. // Make sure the form doesn't expire on the user. Pings every 3 mins.
setInterval(function () { setInterval(() => {
$.ajax({ url: 'UserDefinedForm_Controller/ping' }); $.ajax({ url: 'UserDefinedForm_Controller/ping' });
}, 180 * 1000); }, 180 * 1000);
// Bind a confirmation message when navigating away from a partially completed form. // Bind a confirmation message when navigating away from a partially completed form.
var form = $('form.userform'); const form = $('form.userform');
if(typeof form.areYouSure != 'undefined') { if (typeof form.areYouSure !== 'undefined') {
form.areYouSure({ form.areYouSure({
message: ss.i18n._t('UserForms.LEAVE_CONFIRMATION', 'You have unsaved changes!') message: window.ss.i18n._t('UserForms.LEAVE_CONFIRMATION', 'You have unsaved changes!'),
}); });
} }
} }

View File

@ -0,0 +1,3 @@
// CMS SASS bundle
@import "variables";
@import "userforms-cms";

View File

@ -0,0 +1,3 @@
// Frontend SASS bundle
@import "variables";
@import "userforms";

View File

@ -1,18 +1,11 @@
/** // Animations
* Animations
*/
@keyframes flashBackground { @keyframes flash-background {
0% {background-color: white;} 0% {background-color: $body-bg;}
10% {background-color: #dcfedd;} 10% {background-color: $green-bg;}
70% {background-color: #dcfedd;} 70% {background-color: $green-bg;}
} }
/**
* Styles for cms
*/
.cms {
.uf-field-editor { .uf-field-editor {
padding-bottom: 0; padding-bottom: 0;
@ -20,15 +13,16 @@
table.ss-gridfield-table { table.ss-gridfield-table {
// Standard rows // Standard rows
.ss-gridfield-item { .ss-gridfield-item {
height: 46px; height: $height-base;
&, &:hover { &,
background: white; &:hover {
background: $body-bg;
} }
td { td {
border-right-width: 0; border-right-width: 0;
border-top: 1px solid #EEE; border-top: 1px solid $gray-light;
&:last-child { &:last-child {
border-right-width: 1px; border-right-width: 1px;
@ -36,89 +30,94 @@
} }
.handle { .handle {
min-height: 46px; min-height: $height-base;
} }
&.flashBackground { &.flash-background {
animation: flashBackground 2s linear; animation: flash-background 2s linear;
} }
&.ui-sortable-placeholder { &.ui-sortable-placeholder {
height: 50px; height: $placeholder-height;
} }
} }
.ss-gridfield-item.inFieldGroup { .ss-gridfield-item.infieldgroup {
&, &:hover { &,
background: #f2f9fd; &:hover {
background: $blue-light;
} }
td { td {
border-bottom: 0; border-bottom: 0;
border-top: 1px solid #eee; border-top: 1px solid $gray-light;
} }
.col-reorder, .handle { .col-reorder,
background: #BEE0F8; .handle {
background: $blue;
border-top: 0; border-top: 0;
} }
&.inFieldGroup-level-2 { &.infieldgroup-level-2 {
.col-reorder, .handle { .col-reorder,
background: #99CEF4; .handle {
background: $blue-dark;
border-top: 0; border-top: 0;
} }
} }
&.inFieldGroup-level-3 { &.infieldgroup-level-3 {
.col-reorder, .handle { .col-reorder,
background: #89BEF4; .handle {
background: $blue-darker;
border-top: 0; border-top: 0;
} }
} }
} }
.ss-gridfield-item[data-class='EditableFormStep'] { .ss-gridfield-item[data-class="EditableFormStep"] {
&, &:hover { &,
background: #dae2e7; &:hover {
background: $gray-bg;
} }
label { label {
font-weight: bold; font-weight: bold;
color: black; color: $gray-base;
font-size: 1.1em; font-size: 1.1em;
} }
td { td {
border-top: 1px solid #a6b6c1; border-top: 1px solid $gray;
} }
+ .ss-gridfield-item td { + .ss-gridfield-item td {
border-top: 1px solid #dae2e7; border-top: 1px solid $gray-bg;
} }
+ .ss-gridfield-item[data-class='EditableFieldGroup'] td { + .ss-gridfield-item[data-class="EditableFieldGroup"] td {
border-top: 1px solid #a8d7f5; border-top: 1px solid $blue-base;
} }
} }
.ss-gridfield-item[data-class='EditableFieldGroup'] { .ss-gridfield-item[data-class="EditableFieldGroup"] {
td { td {
border-top: 1px solid #a8d7f5; border-top: 1px solid $blue-base;
} }
label { label {
font-weight: bold; font-weight: bold;
color: #444; color: $gray-darker;
} }
} }
.ss-gridfield-item[data-class='EditableFieldGroupEnd'] { .ss-gridfield-item[data-class="EditableFieldGroupEnd"] {
td { td {
border-bottom: 1px solid #a8d7f5; border-bottom: 1px solid $blue-base;
} }
+ .ss-gridfield-item[data-class='EditableFieldGroupEnd'] { + .ss-gridfield-item[data-class="EditableFieldGroupEnd"] {
border-top: 0; border-top: 0;
} }
@ -127,30 +126,26 @@
} }
label { label {
color: #777; color: $gray-dark;
} }
} }
} }
.stickyButtons { .sticky-buttons {
position: fixed; position: fixed;
top: 40px; top: $height-top;
z-index: 2; z-index: 2;
background: #E6EAED; background: $gray-light;
box-shadow: 0 12px 4px -8px #999; box-shadow: 0 $padding-base $padding-xs -$padding-base-horizontal $gray-light;
padding: 12px; padding: $padding-base;
margin-left: -12px; margin-left: -$padding-base;
& button.action { & button.action {
margin-bottom: 0; margin-bottom: 0;
} }
~ .ss-gridfield-table { ~ .ss-gridfield-table {
margin-top: 40px; margin-top: $height-top;
} }
} }
} }
}

View File

@ -1,18 +1,16 @@
/** // Lightweight base styles for the front-end form.
* Lightweight base styles for the front-end form.
*/
.userform-progress { .userform-progress {
.progress { .progress {
position: relative; position: relative;
height: 1em; height: 1em;
background: #eee; background: $gray-lighter;
} }
.progress-bar { .progress-bar {
position: absolute; position: absolute;
height: 1em; height: 1em;
background: #666; background: $gray-progress-bg;
} }
.step-buttons { .step-buttons {
@ -53,20 +51,20 @@
max-width: 100%; max-width: 100%;
.field label.right { .field label.right {
color: #555; color: $gray-dark-label;
} }
} }
.userformsgroup { .userformsgroup {
border: 1px solid #ccc; border: 1px solid $gray-light-border;
border-radius: 4px; border-radius: $padding-xs;
padding: 8px; padding: $padding-base-horizontal;
margin-top: 12px; margin-top: $padding-base;
margin-bottom: 12px; margin-bottom: $padding-base;
> legend { > legend {
padding-left: 4px; padding-left: $padding-xs;
padding-right: 4px; padding-right: $padding-xs;
border: 0; border: 0;
width: auto; width: auto;
} }
@ -82,6 +80,6 @@
} }
.userform .left { .userform .left {
margin-bottom: 5px; margin-bottom: $padding-sm;
font-weight: bold; font-weight: bold;
} }

View File

@ -0,0 +1,51 @@
//
// Variables
// --------------------------------------------------
//== Colors
$gray-base: #000;
$gray-dark: #777;
$gray-darker: #444;
$gray: #a6b6c1;
$gray-light: #999;
$gray-lighter: #eee;
//== Custom colors
$gray-bg: #dae2e7;
$gray-progress-bg: #666;
$gray-dark-label: #555;
$gray-light-border: #ccc;
$blue-base: #a8d7f5;
$blue-dark: #99cef4;
$blue-darker: #89bef4;
$blue: #bee0f8;
$blue-light: #f2f9fd;
$green-bg: #dcfedd;
//== Scaffolding
//
//## Settings for some of the most global styles.
$body-bg: #fff;
//== Components
//
//## Define common padding and border radius sizes and more.
$padding-base-horizontal: 8px;
//== Custom Components
$padding-base: 12px;
$padding-xs: 4px;
$padding-sm: 5px;
$height-base: 46px;
$height-top: 40px;
$placeholder-height: 50px;

View File

@ -6,7 +6,7 @@
"build": "yarn && NODE_ENV=production webpack -p --bail --progress", "build": "yarn && NODE_ENV=production webpack -p --bail --progress",
"watch": "yarn && NODE_ENV=development webpack --watch --progress", "watch": "yarn && NODE_ENV=development webpack --watch --progress",
"css": "WEBPACK_CHILD=css npm run build", "css": "WEBPACK_CHILD=css npm run build",
"lint": "eslint client/src && sass-lint -v" "lint": "eslint client/src; sass-lint -v"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -54,8 +54,8 @@ const config = [
{ {
name: 'css', name: 'css',
entry: { entry: {
userforms: `${PATHS.SRC}/styles/userforms.scss`, userforms: `${PATHS.SRC}/styles/bundle.scss`,
'userforms-cms': `${PATHS.SRC}/styles/userforms-cms.scss`, 'userforms-cms': `${PATHS.SRC}/styles/bundle-cms.scss`,
}, },
output: { output: {
path: PATHS.DIST, path: PATHS.DIST,