diff --git a/app/_config.php b/app/_config.php index 346773e..78d5a31 100644 --- a/app/_config.php +++ b/app/_config.php @@ -4,7 +4,8 @@ use SilverStripe\Forms\HTMLEditor\HtmlEditorConfig; use SilverStripe\Core\Manifest\ModuleResourceLoader; use SilverStripe\ORM\Search\FulltextSearchable; -HtmlEditorConfig::get('cms')->enablePlugins([ +$config = HtmlEditorConfig::get('cms'); +$config->enablePlugins([ 'template', 'fullscreen', 'hr', @@ -12,11 +13,12 @@ HtmlEditorConfig::get('cms')->enablePlugins([ 'charmap', 'visualblocks', 'lists', - 'anchor', 'charcount' => ModuleResourceLoader::resourceURL( 'drmartingonzo/ss-tinymce-charcount:client/dist/js/bundle.js' ), ]); +$config->addButtonsToLine(2, 'hr'); +$config->setOption('block_formats', 'Paragraph=p;Heading 2=h2;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Address=address;Pre=pre'); +$config->setOption('invalid_elements', 'h1'); -HtmlEditorConfig::get('cms')->insertButtonsAfter('sslink', 'anchor'); FulltextSearchable::enable(); diff --git a/app/client/src/js/_components/_ui.ajax.js b/app/client/src/js/_components/_ui.ajax.js index b010abc..3417c0e 100644 --- a/app/client/src/js/_components/_ui.ajax.js +++ b/app/client/src/js/_components/_ui.ajax.js @@ -5,255 +5,258 @@ import Events from '../_events'; import Spinner from './_ui.spinner'; const AjaxUI = (($) => { - // Constants - const G = window; - const D = document; - const $Html = $('html'); - const $Body = $('body'); + // Constants + const G = window; + const D = document; + const $Html = $('html'); + const $Body = $('body'); - const NAME = 'jsAjaxUI'; - const DATA_KEY = NAME; + const NAME = 'jsAjaxUI'; + const DATA_KEY = NAME; - class AjaxUI { - // Constructor - constructor(element) { - this._element = element; - const $element = $(this._element); - $element.addClass(`${NAME}-active`); + class AjaxUI { + // Constructor + constructor(element) { + this._element = element; + const $element = $(this._element); + $element.addClass(`${NAME}-active`); - $element.bind('click', function (e) { - e.preventDefault(); + $element.bind('click', function(e) { + e.preventDefault(); - const $this = $(this); + const $this = $(this); - $('.ajax').each(function () { - const $this = $(this); - $this.removeClass('active'); - $this.parents('.nav-item').removeClass('active'); - }); + $('.ajax').each(function() { + const $this = $(this); + $this.removeClass('active'); + $this.parents('.nav-item').removeClass('active'); + }); - $this.addClass('loading'); + $this.addClass('loading'); - AjaxUI.load($this.attr('href'), () => { - $this.removeClass('loading'); - $this.parents('.nav-item').addClass('active'); - $this.addClass('active'); - }); - }); - } - - // Public methods - static load(url, callback) { - // show spinner - Spinner.show(() => { - $Body.removeClass('loaded'); - }); - - // update document location - G.MainUI.updateLocation(url); - - const absoluteLocation = G.URLDetails['base'] + G.URLDetails['relative'].substring(1); - if (absoluteLocation !== G.location.href) { - G.history.pushState({ - ajax: true, - page: absoluteLocation, - }, document.title, absoluteLocation); - } - - $.ajax({ - sync: false, - async: true, - url, - dataType: 'json', - method: 'GET', - cache: false, - error(jqXHR) { - console.warn(`AJAX request failure: ${jqXHR.statusText}`); - G.location.href = url; - - // google analytics - if (typeof G.ga === 'function') { - G.ga('send', 'event', 'error', 'AJAX ERROR', jqXHR.statusText); - } - }, - success(data, status, jqXHR) { - AjaxUI.process(data,jqXHR, callback); - - // google analytics - if (typeof G.ga === 'function') { - G.ga('set', { - page: G.URLDetails['relative'] + G.URLDetails['hash'], - title: jqXHR.getResponseHeader('X-Title'), + AjaxUI.load($this.attr('href'), () => { + $this.removeClass('loading'); + $this.parents('.nav-item').addClass('active'); + $this.addClass('active'); + }); }); - G.ga('send', 'pageview'); - } - }, - }); - } - - static process(data, jqXHR, callback) { - const css = jqXHR.getResponseHeader('X-Include-CSS').split(',') || []; - const js = jqXHR.getResponseHeader('X-Include-JS').split(',') || []; - - // Replace HTML regions - if (typeof (data.regions) === 'object') { - for (const key in data.regions) { - if (typeof (data.regions[key]) === 'string') { - AjaxUI.replaceRegion(data.regions[key], key); - } } - } - // remove already loaded scripts - $('link[type="text/css"]').each(function () { - const i = css.indexOf($(this).attr('href')); - if (i > -1) { - css.splice(i, 1); - }else if(!$Body.data('unload-blocked')) { - console.log(`Unloading: ${ $(this).attr('href')}`); - $(this).remove(); - } - }); + // Public methods + static load(url, callback) { + // show spinner + Spinner.show(() => { + $Body.removeClass('loaded'); + }); - $('script[type="text/javascript"]').each(function () { - const i = js.indexOf($(this).attr('src')); - if (i > -1) { - js.splice(i, 1); - }else if(!$Body.data('unload-blocked')) { - console.log(`Unloading: ${ $(this).attr('src')}`); - $(this).remove(); - } - }); + // update document location + G.MainUI.updateLocation(url); - // preload CSS - this.preload(css).then(() => { - const $head = $('head'); - css.forEach((el) => { - $head.append(``); - }); - - // preload JS - this.preload(js, 'script').then(() => { - - js.forEach((el) => { - $Body.append(``); - }); - - console.log('New page is loaded!'); - - // trigger events - if (typeof (data.events) === 'object') { - for (const eventName in data.events) { - $(D).trigger(eventName, [data.events[eventName]]); + const absoluteLocation = G.URLDetails['base'] + G.URLDetails['relative'].substring(1); + if (absoluteLocation !== G.location.href) { + G.history.pushState({ + ajax: true, + page: absoluteLocation, + }, document.title, absoluteLocation); } - } - if (typeof callback !== 'undefined') { - callback(); - } + $.ajax({ + sync: false, + async: true, + url, + dataType: 'json', + method: 'GET', + cache: false, + error(jqXHR) { + console.warn(`AJAX request failure: ${jqXHR.statusText}`); + G.location.href = url; - $(G).trigger(Events.AJAX); - }); - }); - } + // google analytics + if (typeof G.ga === 'function') { + G.ga('send', 'event', 'error', 'AJAX ERROR', jqXHR.statusText); + } + }, + success(data, status, jqXHR) { + AjaxUI.process(data, jqXHR, callback); - static preload(items, type = 'text', cache = true) { - if (!items.length) { - return $.Deferred().resolve().promise(); - } - - const dfds = []; - items.forEach((url) => { - const dfd = $.Deferred(); - - $.ajax({ - dataType: type, - cache, - url, - }).always(() => { - dfd.resolve(); - }); - - dfds.push(dfd); - }); - - // return a master promise object which will resolve when all the deferred objects have resolved - return $.when(...dfds); - } - - static replaceRegion(html, key) { - const $region = $(`[data-ajax-region="${key}"]`); - - if ($region.length) { - $region.empty().append(html); - } else { - console.warn('Region returned without class or id!'); - } - } - - dispose() { - const $element = $(this._element); - - $element.removeClass(`${NAME}-active`); - $.removeData(this._element, DATA_KEY); - this._element = null; - } - - static _jQueryInterface() { - return this.each(function () { - // attach functionality to element - const $element = $(this); - let data = $element.data(DATA_KEY); - - if (!data) { - data = new AjaxUI(this); - $element.data(DATA_KEY, data); + // google analytics + if (typeof G.ga === 'function') { + G.ga('set', { + page: G.URLDetails['relative'] + G.URLDetails['hash'], + title: jqXHR.getResponseHeader('X-Title'), + }); + G.ga('send', 'pageview'); + } + }, + }); + } + + static process(data, jqXHR, callback) { + const css = jqXHR.getResponseHeader('X-Include-CSS').split(',') || []; + const js = jqXHR.getResponseHeader('X-Include-JS').split(',') || []; + + // Replace HTML regions + if (typeof(data.regions) === 'object') { + for (const key in data.regions) { + if (typeof(data.regions[key]) === 'string') { + AjaxUI.replaceRegion(data.regions[key], key); + } + } + } + + // remove already loaded scripts + $('link[type="text/css"]').each(function() { + const i = css.indexOf($(this).attr('href')); + if (i > -1) { + css.splice(i, 1); + } else if (!$Body.data('unload-blocked')) { + console.log(`Unloading: ${ $(this).attr('href')}`); + $(this).remove(); + } + }); + + $('script[type="text/javascript"]').each(function() { + const i = js.indexOf($(this).attr('src')); + if (i > -1) { + js.splice(i, 1); + } else if (!$Body.data('unload-blocked')) { + console.log(`Unloading: ${ $(this).attr('src')}`); + $(this).remove(); + } + }); + + // preload CSS + this.preload(css).then(() => { + const $head = $('head'); + css.forEach((el) => { + $head.append(``); + }); + + // preload JS + this.preload(js, 'script').then(() => { + + js.forEach((el) => { + $Body.append(``); + }); + + console.log('New page is loaded!'); + + // trigger events + if (typeof(data.events) === 'object') { + for (const eventName in data.events) { + $(D).trigger(eventName, [data.events[eventName]]); + } + } + + if (typeof callback !== 'undefined') { + callback(); + } + + $(G).trigger(Events.AJAX); + }); + }); + } + + static preload(items, type = 'text', cache = true, itemCallback = false) { + if (!items.length) { + return $.Deferred().resolve().promise(); + } + + const dfds = []; + items.forEach((url, i) => { + const dfd = $.Deferred(); + + $.ajax({ + dataType: type, + cache, + url, + }).always(() => { + dfd.resolve(); + if (itemCallback) { + itemCallback(i, url); + } + }); + + dfds.push(dfd); + }); + + // return a master promise object which will resolve when all the deferred objects have resolved + return $.when(...dfds); + } + + static replaceRegion(html, key) { + const $region = $(`[data-ajax-region="${key}"]`); + + if ($region.length) { + $region.empty().append(html); + } else { + console.warn('Region returned without class or id!'); + } + } + + dispose() { + const $element = $(this._element); + + $element.removeClass(`${NAME}-active`); + $.removeData(this._element, DATA_KEY); + this._element = null; + } + + static _jQueryInterface() { + return this.each(function() { + // attach functionality to element + const $element = $(this); + let data = $element.data(DATA_KEY); + + if (!data) { + data = new AjaxUI(this); + $element.data(DATA_KEY, data); + } + }); } - }); } - } - // jQuery interface - $.fn[NAME] = AjaxUI._jQueryInterface; - $.fn[NAME].Constructor = AjaxUI; - $.fn[NAME].noConflict = function () { - $.fn[NAME] = JQUERY_NO_CONFLICT; - return AjaxUI._jQueryInterface; - }; + // jQuery interface + $.fn[NAME] = AjaxUI._jQueryInterface; + $.fn[NAME].Constructor = AjaxUI; + $.fn[NAME].noConflict = function() { + $.fn[NAME] = JQUERY_NO_CONFLICT; + return AjaxUI._jQueryInterface; + }; - // auto-apply - $('.ajax').ready(() => { - $('.ajax').jsAjaxUI(); - }); + // auto-apply + $('.ajax').ready(() => { + $('.ajax').jsAjaxUI(); + }); - // AJAX update browser title - $(D).on('layoutRefresh', (e, data) => { - D.title = data.Title; + // AJAX update browser title + $(D).on('layoutRefresh', (e, data) => { + D.title = data.Title; - $Html.attr('class',''); - if(data.ClassName){ - $Html.addClass(data.ClassName); - } - //data.Link = (data.Link === '/home/') ? '/' : data.Link; - }); + $Html.attr('class', ''); + if (data.ClassName) { + $Html.addClass(data.ClassName); + } + //data.Link = (data.Link === '/home/') ? '/' : data.Link; + }); - // Back/Forward functionality - G.onpopstate = function(event) { - const $existingLink = $(`a[href^="${ D.location }"]`); + // Back/Forward functionality + G.onpopstate = function(event) { + const $existingLink = $(`a[href^="${ D.location }"]`); - if(event.state !== null && event.state.ajax){ - console.log('GOBACK (AJAX state)'); - AjaxUI.load(event.state.page); - }else if($existingLink.length && $existingLink.hasClass('ajax')){ - console.log('GOBACK (AJAX link)'); - $existingLink.trigger('click'); - }else{ - console.log('GOBACK (HTTP)'); - G.location.href = D.location; - } - }; + if (event.state !== null && event.state.ajax) { + console.log('GOBACK (AJAX state)'); + AjaxUI.load(event.state.page); + } else if ($existingLink.length && $existingLink.hasClass('ajax')) { + console.log('GOBACK (AJAX link)'); + $existingLink.trigger('click'); + } else { + console.log('GOBACK (HTTP)'); + G.location.href = D.location; + } + }; - return AjaxUI; + return AjaxUI; })($); export default AjaxUI; diff --git a/app/client/src/js/_components/_ui.form.basics.js b/app/client/src/js/_components/_ui.form.basics.js new file mode 100644 index 0000000..a8a661a --- /dev/null +++ b/app/client/src/js/_components/_ui.form.basics.js @@ -0,0 +1,111 @@ +import 'bootstrap-select/js/bootstrap-select'; + +import $ from 'jquery'; +import Events from "../_events"; + +const FormBasics = (($) => { + // Constants + const NAME = 'jsFormBasics'; + const DATA_KEY = NAME; + const $Html = $('html, body'); + + class FormBasics { + + constructor(element) { + const ui = this; + const $element = $(element); + + ui._element = element; + $element.data(DATA_KEY, this); + + const $fields = $element.find('input,textarea,select'); + const $selectFields = $element.find('select:not([readonly])'); + const $radioOptions = $element.find('input[type="radio"]'); + + $selectFields.each((i, el) => { + const $el = $(el); + + $el.selectpicker({ + liveSearch: $el.data('live-search') + }); + }); + + $fields.each((e, el) => { + const $el = $(el); + + if ($el.hasClass('required') || $el.attr('aria-required')) { + $el.closest('.field').addClass('required'); + } + }); + + $radioOptions.each((e, el) => { + const $el = $(el); + + if ($el.is(':checked')) { + $el.parents('.radio').addClass('checked'); + } + }); + + $radioOptions.on('change', (e) => { + const $el = $(e.currentTarget); + const $parent = $el.parents('.radio'); + + $parent.siblings('.radio').removeClass('checked'); + if ($el.is(':checked')) { + $parent.addClass('checked'); + } + }); + + $element.addClass(`${NAME}-active`); + $element.trigger(Events.FORM_INIT_BASICS); + } + + // Public methods + dispose() { + const $element = $(this._element); + + $element.removeClass(`${NAME}-active`); + $.removeData(this._element, DATA_KEY); + this._element = null; + } + + static _jQueryInterface() { + return this.each(function() { + // attach functionality to element + const $element = $(this); + let data = $element.data(DATA_KEY); + + if (!data) { + data = new FormBasics(this); + $element.data(DATA_KEY, data); + } + }); + } + } + + // jQuery interface + $.fn[NAME] = FormBasics._jQueryInterface; + $.fn[NAME].Constructor = FormBasics; + $.fn[NAME].noConflict = function() { + $.fn[NAME] = JQUERY_NO_CONFLICT; + return FormBasics._jQueryInterface; + }; + + // auto-apply + $(window).on(`${Events.AJAX} ${Events.LOADED}`, () => { + $('form').each((i, el) => { + const $el = $(el); + + // skip some forms + if ($el.hasClass('no-validation')) { + return true; + } + + $el.jsFormBasics(); + }); + }); + + return FormBasics; +})($); + +export default FormBasics; diff --git a/app/client/src/js/_components/_ui.form.jqte.js b/app/client/src/js/_components/_ui.form.jqte.js index ad023f9..302ebd7 100644 --- a/app/client/src/js/_components/_ui.form.jqte.js +++ b/app/client/src/js/_components/_ui.form.jqte.js @@ -4,7 +4,7 @@ import $ from 'jquery'; import Events from '../_events'; import Spinner from '../_components/_ui.spinner'; -import FormValidate from './_ui.form.validate'; +import FormValidateField from "./_ui.form.validate.field"; import '../../thirdparty/jquery-te/jquery-te.js'; @@ -29,24 +29,18 @@ const JqteUI = (($) => { constructor(element) { const ui = this; const $element = $(element); - const $jqteFields = $element.find('textarea.jqte-field'); - const validationUI = $element.parents('form').data('jsFormValidate'); + const validationUI = $element.data('jsFormValidateField'); - $element.data(DATA_KEY, this); ui._element = element; + $element.data(DATA_KEY, this); $element.jqte(jqteOptions); // dynamic error control - $element.parents('.jqte').find('.jqte_editor').on('change', (e) => { - const $field = $(e.target); - const $container = $field.closest('.field'); - - if (!$field.text().length && $container.hasClass('required')) { - validationUI.setError($container); - } else { - validationUI.removeError($container); - } - }); + if (validationUI) { + $element.parents('.jqte').find('.jqte_editor').on('change', (e) => { + validationUI.validate(); + }); + } } static dispose() { diff --git a/app/client/src/js/_components/_ui.form.stepped.js b/app/client/src/js/_components/_ui.form.stepped.js index 6ecdd6c..316253f 100644 --- a/app/client/src/js/_components/_ui.form.stepped.js +++ b/app/client/src/js/_components/_ui.form.stepped.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import Events from '../_events'; import LANG from '../lang/_en'; -import FormValidate from './_ui.form.validate'; +import FormValidateField from "./_ui.form.validate.field"; const SteppedForm = (($) => { // Constants @@ -13,6 +13,7 @@ const SteppedForm = (($) => { constructor(element) { const ui = this; const $element = $(element); + $element.data(DATA_KEY, this); if (!$element.find('.steps-counter').length) { @@ -28,16 +29,33 @@ const SteppedForm = (($) => { ui._steps = $element.find('.step'); ui._stepNext = $element.find('.step-next'); + ui._stepPrev = $element.find('.step-prev'); ui._actions = $element.children('.btn-toolbar,.form-actions'); ui._element = element; ui._currentStep = 1; - ui._totalSteps = ui._steps.length; + ui._totalSteps = ui._steps.last().data('step') || ui._steps.length; ui._stepsOrder = []; ui._totalStepsCounter.text(ui._totalSteps); + // check if one of the steps already has an error + const $hasError = ui._steps + .find('.field.error,.field.holder-error,.field.holder-validation,.field.holder-info,.field.holder-warning,.field.holder-good') + .first(); + if ($hasError.length) { + const $modal = $element.parents('.modal'); + + // show modal + if ($modal.length && typeof $modal.modal !== 'undefined') { + $modal.modal('show'); + } + + ui._currentStep = $hasError.parents('.step').data('step') || ui._currentStep; + } + // + ui.step('.step[data-step="' + ui._currentStep + '"]'); ui._stepNext.on('click', (e) => { @@ -78,8 +96,7 @@ const SteppedForm = (($) => { return; } - ui._currentStep++; - ui.step('.step[data-step="' + ui._currentStep + '"]'); + ui.step('.step[data-step="' + (ui._currentStep + 1) + '"]'); } prev() { @@ -89,17 +106,37 @@ const SteppedForm = (($) => { return; } - ui._currentStep--; - ui.step(ui._stepsOrder[ui._currentStep]); + ui.step(ui._stepsOrder[ui._currentStep - 1]); } step(target) { const ui = this; const $element = $(ui._element); const $target = $element.find(target); + const targetStep = parseInt($target.data('step')); + + // validate current step + let valid = true; + + if (targetStep > ui._currentStep) { + ui.currentStep().find('input,textarea,select').each((i, el) => { + const $el = $(el); + const fieldUI = $el.data('jsFormValidateField'); + + if (fieldUI && !fieldUI.validate()) { + valid = false; + } + }); + } + + if (!valid) { + return false; + } + // if (parseInt($target.data('step')) <= '1') { ui._stepPrev.hide(); + $element.trigger(Events.FORM_STEPPED_FIRST_STEP); } else { ui._stepPrev.show(); } @@ -107,18 +144,30 @@ const SteppedForm = (($) => { if (parseInt($target.data('step')) >= ui._totalSteps) { ui._stepNext.hide(); ui._actions.show(); + + $element.trigger(Events.FORM_STEPPED_LAST_STEP); } else { ui._stepNext.show(); ui._actions.hide(); } - ui._currentStep = parseInt($target.data('step')); + ui._currentStep = targetStep; ui._stepsOrder[ui._currentStep] = $target; ui._steps.removeClass('active'); $target.addClass('active'); ui._currentStepCounter.text(ui._currentStep); + + $target.trigger(Events.FORM_STEPPED_NEW_STEP); + $element.trigger(Events.FORM_STEPPED_NEW_STEP); + } + + currentStep() { + const ui = this; + const $element = $(ui._element); + + return $element.find('.step.active'); } static _jQueryInterface() { diff --git a/app/client/src/js/_components/_ui.form.storage.js b/app/client/src/js/_components/_ui.form.storage.js index 141a035..b2cbc52 100644 --- a/app/client/src/js/_components/_ui.form.storage.js +++ b/app/client/src/js/_components/_ui.form.storage.js @@ -31,6 +31,10 @@ const FormStorage = (($) => { const type = $el.attr('type'); const val = STORAGE.getItem(NAME + id); + if (type === 'file') { + return true; + } + if (id && val && type) { if (type && (type === 'checkbox' || type === 'radio')) { $el.prop('checked', val); diff --git a/app/client/src/js/_components/_ui.form.validate.field.js b/app/client/src/js/_components/_ui.form.validate.field.js new file mode 100644 index 0000000..d6fc0d1 --- /dev/null +++ b/app/client/src/js/_components/_ui.form.validate.field.js @@ -0,0 +1,120 @@ +import $ from 'jquery'; +import Events from "../_events"; + +const FormValidateField = (($) => { + // Constants + const NAME = 'jsFormValidateField'; + const DATA_KEY = NAME; + const $Html = $('html, body'); + + class FormValidateField { + + constructor(element) { + const ui = this; + const $element = $(element); + + ui._element = element; + ui._actions = $element.parents('form').children('.btn-toolbar,.form-actions'); + $element.data(DATA_KEY, this); + + // prevent browsers checks (will do it using JS) + $element.attr('novalidate', 'novalidate'); + + $element.on('change', (e) => { + ui.validate(false); + }); + + $element.addClass(`${NAME}-active`); + $element.trigger(Events.FORM_INIT_VALIDATE_FIELD); + } + + // Public methods + dispose() { + const $element = $(this._element); + + $element.removeClass(`${NAME}-active`); + $.removeData(this._element, DATA_KEY); + this._element = null; + } + + validate(scrollTo = true) { + const ui = this; + const $el = $(ui._element); + const $field = $el.closest('.field'); + const extraChecks = $el.data(`${NAME}-extra`); + let valid = true; + + // browser checks + required + if (!ui._element.checkValidity() || + ($el.hasClass('required') && !$el.val().trim().length) + ) { + valid = false; + } + + // extra checks + if (extraChecks) { + extraChecks.forEach((check) => { + valid = valid && check(); + }); + } + + if (valid) { + this.removeError(); + return true; + } + + this.setError(scrollTo); + return false; + } + + setError(scrollTo = true) { + const ui = this; + const $field = $(ui._element).closest('.field'); + const pos = $field.offset().top; + + $field.addClass('error'); + + if (scrollTo) { + $field.focus(); + $Html.scrollTop(pos - 100); + + } + } + + removeError() { + const ui = this; + const $field = $(ui._element).closest('.field'); + + $field.removeClass('error'); + + $field.removeClass('holder-error'); + $field.removeClass('holder-validation'); + $field.find('.message').remove(); + } + + static _jQueryInterface() { + return this.each(function() { + // attach functionality to element + const $element = $(this); + let data = $element.data(DATA_KEY); + + if (!data) { + data = new FormValidateField(this); + $element.data(DATA_KEY, data); + } + }); + } + } + + // jQuery interface + $.fn[NAME] = FormValidateField._jQueryInterface; + $.fn[NAME].Constructor = FormValidateField; + $.fn[NAME].noConflict = function() { + $.fn[NAME] = JQUERY_NO_CONFLICT; + return FormValidateField._jQueryInterface; + }; + + return FormValidateField; +})($); + +export default FormValidateField; diff --git a/app/client/src/js/_components/_ui.form.validate.js b/app/client/src/js/_components/_ui.form.validate.js index dd239cc..18896af 100644 --- a/app/client/src/js/_components/_ui.form.validate.js +++ b/app/client/src/js/_components/_ui.form.validate.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import Events from "../_events"; +import FormValidateField from "./_ui.form.validate.field"; const FormValidate = (($) => { // Constants @@ -19,23 +20,22 @@ const FormValidate = (($) => { ui._fields = $fields; ui._stepped_form = $element.data('jsSteppedForm'); + + // prevent browsers checks (will do it using JS) + $element.attr('novalidate', 'novalidate'); + $element.on(Events.FORM_INIT_STEPPED, () => { ui._stepped_form = $element.data('jsSteppedForm'); }); - // prevent browsers checks (will do it using JS) - $element.attr('novalidate', 'novalidate'); + // init fields validation $fields.each((i, el) => { - el.required = false; - }); - - $fields.on('change', (e) => { - ui.validateField($(e.target), false); + new FormValidateField(el); }); // check form $element.on('submit', (e) => { - ui.validateForm($element, true, () => { + ui.validate(true, () => { e.preventDefault(); // switch to step @@ -64,52 +64,24 @@ const FormValidate = (($) => { this._element = null; } - validateForm($form, scrollTo = true, badCallback = false) { + validate(scrollTo = true, badCallback = false) { console.log('Checking the form ...'); const ui = this; ui._fields.each(function(i, el) { const $el = $(el); + const fieldUI = $el.data('jsFormValidateField'); - if (!ui.validateField($el)) { + if (fieldUI && !fieldUI.validate()) { if (badCallback) { badCallback(); } + return false; } }); } - validateField($el, scrollTo = true) { - const $field = $el.closest('.field'); - - if (!$el[0].checkValidity() || - ($el.hasClass('required') && !$el.val().trim().length) - ) { - this.setError($field, scrollTo); - return false; - } else { - this.removeError($field); - } - - return true; - } - - setError($field, scrollTo = true) { - const pos = $field.offset().top; - - $field.addClass('error'); - - if (scrollTo) { - $field.focus(); - $Html.scrollTop(pos - 100); - } - } - - removeError($field) { - $field.removeClass('error'); - } - static _jQueryInterface() { return this.each(function() { // attach functionality to element diff --git a/app/client/src/js/_events.js b/app/client/src/js/_events.js index ed2be33..d6a4ea2 100644 --- a/app/client/src/js/_events.js +++ b/app/client/src/js/_events.js @@ -5,9 +5,15 @@ module.exports = { AJAX: 'ajax-load', LOADED: 'load', + SET_TARGET_UPDATE: 'set-target-update', RESTORE_FIELD: 'restore-field', + FORM_INIT_BASICS: 'form-basics', FORM_INIT_STEPPED: 'form-init-stepped', FORM_INIT_VALIDATE: 'form-init-validate', + FORM_INIT_VALIDATE_FIELD: 'form-init-validate-field', FORM_INIT_STORAGE: 'form-init-storage', FORM_VALIDATION_FAILED: 'form-validation-failed', + FORM_STEPPED_NEW_STEP: 'form-new-step', + FORM_STEPPED_FIRST_STEP: 'form-first-step', + FORM_STEPPED_LAST_STEP: 'form-last-step', }; diff --git a/app/client/src/js/_main.js b/app/client/src/js/_main.js index ee97842..e4be3d5 100644 --- a/app/client/src/js/_main.js +++ b/app/client/src/js/_main.js @@ -12,10 +12,16 @@ import './_components/routes/index'; import Events from './_events'; import Spinner from './_components/_ui.spinner'; -import './_components/_ui.shrink'; import './_components/_ui.carousel'; import './_components/_ui.menu'; -import './_components/_ui.form.storage'; + +import FormBasics from './_components/_ui.form.basics'; +import FormDatetime from './_components/_ui.form.datetime'; +import FormStepped from './_components/_ui.form.stepped'; +import FormValidate from './_components/_ui.form.validate'; +import FormStorage from './_components/_ui.form.storage'; +import FormCroppie from './_components/_ui.form.croppie'; + import AjaxUI from './_components/_ui.ajax'; import SmoothScroll from 'smooth-scroll'; @@ -144,27 +150,6 @@ const MainUI = (($) => { // mark external links $('a.external,a[rel="external"]').attr('target', '_blank'); - // data-set links - $('[data-set-target]').on('click', (e) => { - const $el = $(e.currentTarget); - const $target = $($el.data('set-target')); - - if (!$target.length) { - return; - } - - $target.each((i, targetEl) => { - const $targetEl = $(targetEl); - const tag = $targetEl.prop('tagName').toLowerCase(); - - if (tag === 'input' || tag === 'select') { - $targetEl.val($el.data('set-val')); - } else if (!$targetEl.hasClass('field')) { - $targetEl.text($el.data('set-val')); - } - }); - }); - // show encoded emails /*$(D).find('.obm').each(function () { if ($(this).attr('data-val') !== undefined) { @@ -189,18 +174,23 @@ const MainUI = (($) => { // // scroll links - $(D).on('click', '.js-scrollTo', function(e) { + $(D).on('click', '.js-scrollTo', (e) => { e.preventDefault(); - ScrollTo(this, $(this).attr('data-target')); + const el = e.currentTarget; + const $el = $(e.currentTarget); + + ScrollTo(el, $el.attr('data-target')); }); // load external fonts if ($('[data-extfont]').length) { $.getScript('//ajax.googleapis.com/ajax/libs/webfont/1/webfont.js', () => { const fonts = []; - $('[data-extfont]').each(function(i) { - fonts[i] = $(this).attr('data-extfont'); + + $('[data-extfont]').each(function(i, el) { + fonts[i] = $(el).attr('data-extfont'); }); + W.WebFont.load({ google: { families: fonts, @@ -209,6 +199,30 @@ const MainUI = (($) => { }); } + // data-set links + $('[data-set-target]').on('click', (e) => { + const $el = $(e.currentTarget); + const $target = $($el.data('set-target')); + + if (!$target.length) { + return; + } + + $target.each((i, targetEl) => { + const $targetEl = $(targetEl); + const tag = $targetEl.prop('tagName').toLowerCase(); + + if (tag === 'input' || tag === 'select') { + $targetEl.val($el.data('set-val')); + } else if (!$targetEl.hasClass('field')) { + $targetEl.text($el.data('set-val')); + } + }); + + $el.trigger(Events.SET_TARGET_UPDATE); + $target.closest('form').trigger(Events.SET_TARGET_UPDATE); + }); + // hide spinner Spinner.hide(() => { $Body.addClass('loaded'); @@ -276,14 +290,26 @@ const MainUI = (($) => { const $imgLazyUrls = []; // collect image details - $imgs.each(function() { - const src = $(this).attr('src'); - const lazySrc = $(this).data('lazy-src'); - if (src.length) { + $imgs.each((i, el) => { + const $el = $(el); + const src = $el.attr('src'); + const lazySrc = $el.data('lazy-src'); + + if (src && src.length) { $imgUrls.push(src); } - if (lazySrc) { + if (lazySrc && lazySrc.length) { $imgLazyUrls.push(lazySrc); + $el.addClass('loading'); + + AjaxUI.preload([lazySrc]).then(() => { + $el.attr('src', lazySrc); + + $el.addClass('loaded'); + $el.removeClass('loading'); + + $el.trigger('image-lazy-loaded'); + }); } }); @@ -293,14 +319,6 @@ const MainUI = (($) => { // load lazy images AjaxUI.preload($imgLazyUrls).then(() => { - // update lazy img src - $('img[data-lazy-src]').each(function() { - if (!$(this).attr('src')) { - return; - } - $(this).attr('src', $(this).data('lazy-src')); - }); - console.log('All images are loaded!'); $(W).trigger('images-lazy-loaded'); diff --git a/app/client/src/scss/_components/_ui.main.scss b/app/client/src/scss/_components/_ui.main.scss index f787f86..244a640 100644 --- a/app/client/src/scss/_components/_ui.main.scss +++ b/app/client/src/scss/_components/_ui.main.scss @@ -8,18 +8,27 @@ img { max-width: 100%; } -a:hover, -a:focus, -button:hover, -button:focus { - opacity: .8; +a, +button, +.jqte_tool_icon { + &:hover, + &:focus { + opacity: .8; - .fas, - .fab, - .far, - &.fas, - &.fab, - &.far { + .fas, + .fab, + .far, + &.fas, + &.fab, + &.far { + transform: scale(-1, 1); + } + } +} + +.jqte_tool_icon { + &:hover, + &:focus { transform: scale(-1, 1); } } @@ -27,6 +36,7 @@ button:focus { // transactions .transition, a, a img, +.jqte_tool_icon, a .fas, a .fab, a .far, a.fas, a.fab, a.far, button .fas, button .fab, button .far, @@ -43,16 +53,73 @@ button, input, optgroup, select, textarea, transition: all 0.4s ease; } +.alert-fixed-top { + position: fixed; + top: 0; + z-index: 999; + left: 4rem; + right: 4rem; +} + .btn-toolbar { margin-top: $grid-gutter-height / 2; } .field { + position: relative; + margin: 2rem 0; margin: ($grid-gutter-height / 4) 0; &:first-child { margin-top: 0; } + + &.required { + &:after { + display: block; + position: absolute; + top: 2rem; + right: .5rem; + content: "*"; + color: $red; + z-index: 2; + } + } + + &.holder-error, + &.error { + input, select, textarea { + border-color: $red; + } + + label { + color: $red; + } + } + + label { + font-weight: bold; + } + + .bootstrap-select:not([class*="col-"]):not([class*="form-control"]):not(.input-group-btn) { + width: 100%; + } +} + +.message { + @extend .alert; + + @extend .alert-info; + + display: block; + margin: .5rem 0; +} + +.message.required, +.message.error { + @extend .alert; + + @extend .alert-danger; } // stick navbar to top using mobile layout diff --git a/app/client/src/scss/app.scss b/app/client/src/scss/app.scss index 2967ace..37905fd 100644 --- a/app/client/src/scss/app.scss +++ b/app/client/src/scss/app.scss @@ -1,3 +1,5 @@ +@import "~bootstrap-select/sass/bootstrap-select.scss"; + @import "~bootstrap-datepicker/dist/css/bootstrap-datepicker.css"; @import "~bootstrap-timepicker/css/bootstrap-timepicker.css"; diff --git a/app/client/src/scss/types/editor.scss b/app/client/src/scss/types/editor.scss index 5ccefe7..08303af 100644 --- a/app/client/src/scss/types/editor.scss +++ b/app/client/src/scss/types/editor.scss @@ -1 +1,48 @@ @import "../_variables"; + +@import "~bootstrap/scss/tables"; + +.image { + &.left { + float: left; + clear: left; + } + + &.center { + display: block; + margin: 0 auto; + } + + &.right { + float: right; + clear: right; + } +} + +.text-left { + text-align: left; +} + +.text-center { + text-align: center; +} + +.text-right { + text-align: right; +} + +.text-justify { + text-align: justify; +} + +table { + width: 100%; + max-width: 100%; + border-collapse: collapse; + + @extend .table; + + @extend .table-bordered; + + @extend .table-striped; +} diff --git a/app/src/Extensions/SocialExtension.php b/app/src/Extensions/SocialExtension.php index 2eb283b..4d5125c 100644 --- a/app/src/Extensions/SocialExtension.php +++ b/app/src/Extensions/SocialExtension.php @@ -8,12 +8,12 @@ namespace Site\Extensions; - use Sheadawson\Linkable\Forms\LinkField; use Sheadawson\Linkable\Models\Link; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\TextField; use SilverStripe\ORM\DataExtension; +use SilverStripe\Security\Member; class SocialExtension extends DataExtension { @@ -59,4 +59,18 @@ class SocialExtension extends DataExtension $fields->addFieldsToTab('Root.Social', $linkFields); } + + public static function byPhone($phone) + { + $links = Link::get()->filter('Phone', $phone); + + if ($links->exists()) { + return Member::get()->filter( + 'PhoneNumberID', + array_keys($links->map('ID', 'Title')->toArray()) + )->first(); + } + + return null; + } } diff --git a/app/src/Pages/PageController.php b/app/src/Pages/PageController.php index c823fe5..66bbea6 100644 --- a/app/src/Pages/PageController.php +++ b/app/src/Pages/PageController.php @@ -23,9 +23,10 @@ class PageController extends ContentController public function getSiteWideMessage() { $session = $this->getRequest()->getSession(); + $message = $session->get('SiteWideMessage'); $session->clear('SiteWideMessage'); - return $session->get('SiteWideMessage'); + return $message; } public function CurrentTime() diff --git a/app/templates/Includes/SiteWideMessage.ss b/app/templates/Includes/SiteWideMessage.ss index a48a301..5d54809 100644 --- a/app/templates/Includes/SiteWideMessage.ss +++ b/app/templates/Includes/SiteWideMessage.ss @@ -1,5 +1,10 @@ <% if $SiteWideMessage %> -
- {$Message} -
+ <% with $SiteWideMessage %> +
+ {$Message} + +
+ <% end_with %> <% end_if %> diff --git a/composer.json b/composer.json index 0c3cb0b..26f740f 100755 --- a/composer.json +++ b/composer.json @@ -19,7 +19,6 @@ "dnadesign/silverstripe-elemental-virtual": "*", "dnadesign/silverstripe-elemental-userforms": "*", "dynamic/silverstripe-elemental-blocks": "*", - "hestec/silverstripe-cookiebar": "^1.0", "drmartingonzo/ss-tinymce-charcount": "^1.0", "axllent/silverstripe-version-truncator": "*", "firesphere/googlemapsfield": "^1.0@dev", diff --git a/package.json b/package.json index 66c3b88..7dc9946 100755 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "bootstrap": "^4.1.1", "bootstrap-datepicker": "^1.8.0", "bootstrap-offcanvas": "^1.0.0", + "bootstrap-select": "^1.13.2", "bootstrap-timepicker": "^0.5.2", "core-util-is": "^1.0.2", "font-awesome": "^4.7.0", @@ -35,6 +36,7 @@ "jquery": "^3.3.1", "jquery-hammerjs": "^2.0.0", "jquery-zoom": "^1.7.21", + "jquery.appear": "^1.0.1", "mapbox-gl": "^0.48.0", "meta-lightbox": "^1.0.0", "offcanvas-bootstrap": "^2.5.2",