Initial xentral_oss_20.3.c9ffacf
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Xentral\Widgets\ClickByClickAssistant;
|
||||
|
||||
|
||||
class Bootstrap
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerJavascript()
|
||||
{
|
||||
return [
|
||||
'ClickByClickAssistant' => [
|
||||
'./classes/Widgets/ClickByClickAssistant/www/js/click_by_click_assistant.js',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerStylesheets()
|
||||
{
|
||||
return [
|
||||
'ClickByClickAssistant' => [
|
||||
'./classes/Widgets/ClickByClickAssistant/www/css/click_by_click_assistant.css',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Widgets\ClickByClickAssistant\Exception;
|
||||
|
||||
use Xentral\Core\Exception\ComponentExceptionInterface;
|
||||
|
||||
interface ClickByClickAssistantExceptionInterface extends ComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Widgets\ClickByClickAssistant\Exception;
|
||||
|
||||
use InvalidArgumentException as SplInvalidArgumentException;
|
||||
|
||||
class InvalidArgumentException extends SplInvalidArgumentException implements ClickByClickAssistantExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Widgets\ClickByClickAssistant;
|
||||
|
||||
use Xentral\Widgets\ClickByClickAssistant\Exception\InvalidArgumentException;
|
||||
|
||||
final class VueUtil
|
||||
{
|
||||
/**
|
||||
* @param array $array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function keyValueArrayToVueOptions($array): array
|
||||
{
|
||||
self::ensureScalarKeyValueArray($array);
|
||||
|
||||
$ret = [];
|
||||
foreach ($array as $value => $text) {
|
||||
$ret[] = ['value' => (string)$value, 'text' => (string)$text,];
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array[] $pageArray
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getInputNamesFromVuePages($pageArray): array
|
||||
{
|
||||
self::ensureArray($pageArray);
|
||||
|
||||
$ret = [];
|
||||
|
||||
foreach ($pageArray as $page) {
|
||||
if (empty($page['inputs'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
self::ensureArray($page['inputs']);
|
||||
foreach ($page['inputs'] as $input) {
|
||||
if (isset($input['name'])) {
|
||||
$ret[] = $input['name'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function ensureArray($value): void
|
||||
{
|
||||
$type = gettype($value);
|
||||
if ($type !== 'array') {
|
||||
throw new InvalidArgumentException(sprintf('Wrong type "%s". Only "array" is allowed.', $type));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function ensureScalarKeyValueArray($value): void
|
||||
{
|
||||
self::ensureArray($value);
|
||||
|
||||
foreach ($value as $key => $val) {
|
||||
if ($val !== null && !is_scalar($val)) {
|
||||
$type = gettype($val);
|
||||
throw new InvalidArgumentException(sprintf('Wrong type "%s". Only scalar types ar allowed.', $type));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,514 @@
|
||||
:root {
|
||||
--main-icon-size: 65px;
|
||||
}
|
||||
|
||||
/**
|
||||
xentral corp colors:
|
||||
blueish #5a63ee
|
||||
pink #e66dcb
|
||||
green #29e7a1
|
||||
cyan #27e3e5
|
||||
**/
|
||||
|
||||
.click-by-click-assistant {
|
||||
position: fixed;
|
||||
z-index: 9998;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: table;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .header-icon {
|
||||
width: var(--main-icon-size);
|
||||
height: var(--main-icon-size);
|
||||
margin: 0 auto 12px auto;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .survey-icon {
|
||||
background: url('../themes/new/images/question-circle.svg');
|
||||
background-size: var(--main-icon-size);
|
||||
}
|
||||
.click-by-click-assistant .thanks-icon {
|
||||
background: url('../themes/new/images/thanks-smiley.svg');
|
||||
background-size: var(--main-icon-size);
|
||||
}
|
||||
|
||||
.click-by-click-assistant .password-icon {
|
||||
background: url('../themes/new/images/password-icon.svg');
|
||||
background-size: var(--main-icon-size);
|
||||
}
|
||||
|
||||
.click-by-click-assistant .add-person-icon {
|
||||
background: url('../themes/new/images/add-person-icon.svg');
|
||||
background-size: var(--main-icon-size);
|
||||
}
|
||||
|
||||
.click-by-click-assistant .consultant {
|
||||
width: 107px;
|
||||
height: 107px;
|
||||
margin-top: -85px;
|
||||
border-radius: 100px;
|
||||
background: url('../themes/new/images/consultant.jpg') center;
|
||||
background-size: 120px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant h2,
|
||||
.click-by-click-assistant h3 {
|
||||
line-height: 22px;
|
||||
color: #25233A;
|
||||
opacity: 0.7;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .h2{
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .h3{
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.click-by-click-assistant p {
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 17px;
|
||||
opacity: 0.7;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 10px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.click-by-click-assistant p.page-text{
|
||||
line-height: 22px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .wrapper {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .errorMsg {
|
||||
text-align: center;
|
||||
padding-top: 15px;
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .container {
|
||||
width: 600px;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-close-button {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 0;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: url('../themes/new/images/icon-close.svg') no-repeat center;
|
||||
background-size: 10px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .page {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .media-youtube {
|
||||
width: 100%;
|
||||
min-height: 337px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .media-image {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .page-content {
|
||||
padding: 35px 30px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .container .cta {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
padding: 10px 50px;
|
||||
margin-top: 20px;
|
||||
background-color: var(--button-primary-background);
|
||||
}
|
||||
|
||||
.click-by-click-assistant .container .button.center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .flex-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .survey-button-container {
|
||||
box-sizing: border-box;
|
||||
margin: 10px 5px 0 5px;
|
||||
width: calc(1 / 3 * 100% - 10px);
|
||||
}
|
||||
|
||||
.click-by-click-assistant label.button-secondary {
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
height: 41px;
|
||||
line-height: 41px;
|
||||
padding: 0;
|
||||
color: var(--button-secondary-color);
|
||||
border: 1px solid rgba(128, 128, 128, 0.2);
|
||||
box-shadow: 5px 5px 15px rgba(128, 128, 128, 0.05);
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.click-by-click-assistant label.button-secondary:hover {
|
||||
border: 1px solid var(--button-secondary-border-color);
|
||||
background-color: var(--button-secondary-background);
|
||||
color: var(--button-secondary-color);
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked + label.button.button-secondary {
|
||||
background-color: var(--button-secondary-background);
|
||||
color: var(--button-secondary-color);
|
||||
}
|
||||
|
||||
.click-by-click-assistant input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.click-by-click-assistant input::-webkit-textfield-decoration-container {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .next-enter {
|
||||
opacity: 0;
|
||||
transform: translate3d(100px, 0, 0);
|
||||
}
|
||||
.click-by-click-assistant .next-enter-active,
|
||||
.click-by-click-assistant .next-leave-active {
|
||||
transition: 0.2s cubic-bezier(1.0, 0.5, 0.8, 1.0);
|
||||
}
|
||||
.click-by-click-assistant .next-leave-to {
|
||||
opacity: 0;
|
||||
transform: translate3d(-100px, 0, 0);
|
||||
}
|
||||
|
||||
.click-by-click-assistant .prev-enter {
|
||||
opacity: 0;
|
||||
transform: translate3d(-100px, 0, 0);
|
||||
}
|
||||
.click-by-click-assistant .prev-enter-active,
|
||||
.click-by-click-assistant .prev-leave-active {
|
||||
transition: 0.2s cubic-bezier(1.0, 0.5, 0.8, 1.0);
|
||||
}
|
||||
.click-by-click-assistant .prev-leave-to {
|
||||
opacity: 0;
|
||||
transform: translate3d(100px, 0, 0);
|
||||
}
|
||||
|
||||
.click-by-click-assistant .fade-enter-active, .fade-leave-active {
|
||||
transition: opacity .3s;
|
||||
}
|
||||
.click-by-click-assistant .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */
|
||||
{
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Input Styles
|
||||
*/
|
||||
|
||||
.click-by-click-assistant .app-row-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-row-container .add-row,
|
||||
.click-by-click-assistant .link {
|
||||
color: #87B668;
|
||||
text-decoration: underline;
|
||||
font-size: 12px;
|
||||
line-height: 15px;
|
||||
margin: 30px auto;
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-row-container .add-row a,
|
||||
.click-by-click-assistant .link a {
|
||||
color: #87B668;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-row-container .add-row.inactive {
|
||||
cursor: auto;
|
||||
pointer-events: none;
|
||||
color: #b7b7be;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input-row {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: row;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input-row.reduced-width {
|
||||
width: 60%;
|
||||
min-width: 200px;
|
||||
margin: 0 auto;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input-row .remove-row {
|
||||
cursor: pointer;
|
||||
align-self: flex-end;
|
||||
flex: 0 0 30px;
|
||||
height: 83px;
|
||||
background: url('../themes/new/images/remove-icon.svg') no-repeat;
|
||||
background-size: 15px 12px;
|
||||
background-position-x: 10px;
|
||||
background-position-y: 47px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input-row .app-row-valid {
|
||||
position: absolute;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
bottom: 23px;
|
||||
left: -17px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input-row .app-row-valid.icon-ok {
|
||||
background: url('../themes/new/images/icon-ok.svg') no-repeat center;
|
||||
background-size: 11px 8px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input-row .app-row-valid.checkbox {
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .container a {
|
||||
color: #929292;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
padding-right: 12px;
|
||||
box-sizing: border-box;
|
||||
margin: 35px auto 10px auto;
|
||||
transition: 0.2s ease width;
|
||||
-moz-transition: 0.2s ease width;
|
||||
-webkit-transition: 0.2s ease width;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input input[type="checkbox"] {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input-row .app-input:last-of-type {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
|
||||
.click-by-click-assistant .app-input.input-error *:not(input) {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input.input-error input,
|
||||
.click-by-click-assistant .app-input.input-error select {
|
||||
border-bottom-color: #f44336;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input .input-error {
|
||||
position: absolute;
|
||||
bottom: -15px;
|
||||
left: 5px;
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input .reveal {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
display: block;
|
||||
width: 30px;
|
||||
height: 38px;
|
||||
background: url('../themes/new/images/icon-invisible.svg');
|
||||
background-size: 15px 12px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
opacity: .3;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input input,
|
||||
.click-by-click-assistant .app-input select {
|
||||
width: 100%;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 17px;
|
||||
color: #25233A;
|
||||
padding: 10px 10px 10px 5px;
|
||||
display: block;
|
||||
border: none;
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
border-radius: 0;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input select::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input input:focus,
|
||||
.click-by-click-assistant .app-input select:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input label {
|
||||
color: #b7b7be;
|
||||
font-size: 14px;
|
||||
line-height: 17px;
|
||||
font-weight: normal;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
left: 5px;
|
||||
top: 10px;
|
||||
transition: 0.2s ease all;
|
||||
-moz-transition: 0.2s ease all;
|
||||
-webkit-transition: 0.2s ease all;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input input[type="checkbox"] ~ label {
|
||||
left: 10%;
|
||||
top: 0;
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input.select:after {
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 5px solid;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
margin-top: -2.5px;
|
||||
pointer-events: none;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input input:focus ~ label,
|
||||
.click-by-click-assistant .app-input input:valid ~ label,
|
||||
.click-by-click-assistant .app-input select.hasSelected ~ label,
|
||||
.click-by-click-assistant .app-input input.hasValue ~ label,
|
||||
.click-by-click-assistant .app-input select:valid ~ label {
|
||||
top: -15px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input input[type="checkbox"]:focus ~ label,
|
||||
.click-by-click-assistant .app-input input[type="checkbox"]:valid ~ label,
|
||||
.click-by-click-assistant .app-input input[type="checkbox"].hasValue ~ label {
|
||||
top: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-pagination {
|
||||
text-align: center;
|
||||
margin: 20px 0 0 0;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-pagination div {
|
||||
display: inline-block;
|
||||
background-color: #d9d9d9;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 5px;
|
||||
margin: 0 2px;
|
||||
vertical-align: middle;
|
||||
transition: 0.2s ease all;
|
||||
-moz-transition: 0.2s ease all;
|
||||
-webkit-transition: 0.2s ease all;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-pagination div.active {
|
||||
background-color: #a0a0a0;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .spinner {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
clear: both;
|
||||
right: 15px;
|
||||
top: 9px;
|
||||
}
|
||||
|
||||
/* Spinner Circle Rotation */
|
||||
.click-by-click-assistant .spinner-circle {
|
||||
border: 3px rgba(255, 255, 255, 0.25) solid;
|
||||
border-top: 3px rgba(255, 255, 255, 1) solid;
|
||||
border-radius: 50%;
|
||||
-webkit-animation: spCircRot .6s infinite linear;
|
||||
animation: spCircRot .6s infinite linear;
|
||||
}
|
||||
@-webkit-keyframes spCircRot {
|
||||
from {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
@keyframes spCircRot {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,655 @@
|
||||
Vue.component('click-by-click-assistant', {
|
||||
props: ['pages', 'allowclose', 'pagination'],
|
||||
data: function(){
|
||||
return {
|
||||
activePage: 0,
|
||||
currentTransition:'',
|
||||
dataStorage: []
|
||||
}
|
||||
},
|
||||
template: '<div class="click-by-click-assistant"><div class="wrapper"><div class="container">' +
|
||||
'<div v-if="allowclose" class="app-close-button" @click="$emit(\'close\')"></div>' +
|
||||
|
||||
'<transition :name="currentTransition" mode="out-in">' +
|
||||
|
||||
/** DEFAULT TEXT PAGE **/
|
||||
'<div class="page" v-for="(page, index) in pages" ' +
|
||||
'v-if="page.type === \'defaultPage\' && activePage === index" ' +
|
||||
':data-pageIndex="index" ' +
|
||||
':key="index">' +
|
||||
'<app-media v-if="page.headerMedia" :media="page.headerMedia"></app-media>' +
|
||||
'<div class="page-content">' +
|
||||
'<div v-if="!page.headerMedia && page.icon" class="header-icon" :class="page.icon"></div>' +
|
||||
'<h2 v-if="page.headline" v-html="page.headline"></h2>'+
|
||||
'<h3 v-if="page.subHeadline" v-html="page.subHeadline"></h3>'+
|
||||
'<p class="page-text" v-if="page.text" v-html="page.text"></p>'+
|
||||
'<div class="flex-container" v-if="page.link">'+
|
||||
'<div v-if="page.link" class="link">'+
|
||||
'<a class="link" :href="page.link.link" >{{ page.link.title }}</a>'+
|
||||
'</div>' +
|
||||
'</div>'+
|
||||
'<button v-if="button.action === \'next\'" ' +
|
||||
'v-for="button in page.ctaButtons" ' +
|
||||
'class="button button-primary cta center" ' +
|
||||
'@click="changePage(\'next\')">{{ button.title }}</button>'+
|
||||
|
||||
'<button v-if="!button.link && button.action === \'close\'" ' +
|
||||
'v-for="button in page.ctaButtons" ' +
|
||||
'class="button button-primary cta center" ' +
|
||||
'@click="$emit(\'close\')">{{ button.title }}</button>'+
|
||||
|
||||
'<button v-if="!button.link && button.action === \'completeStep\'" ' +
|
||||
'v-for="button in page.ctaButtons" ' +
|
||||
'class="button button-primary cta center" ' +
|
||||
'@click="$emit(\'completeStep\')">{{ button.title }}</button>'+
|
||||
|
||||
'<button v-if="button.link && button.action === \'close\'" ' +
|
||||
'v-for="button in page.ctaButtons" ' +
|
||||
'class="button button-primary cta center" ' +
|
||||
'@click="link(button.link)">{{ button.title }}</button>'+
|
||||
|
||||
'<app-pagination v-if="pagination" :pages="pages" :index="index"></app-pagination>' +
|
||||
'</div>'+
|
||||
'</div>' +
|
||||
|
||||
/** FORM PAGE **/
|
||||
'<div class="page" v-for="(page, index) in pages" ' +
|
||||
'v-if="(page.type === \'form\' || page.type === \'survey\') && activePage === index" ' +
|
||||
':data-pageIndex="index" :key="index">' +
|
||||
'<app-media v-if="page.headerMedia" :media="page.headerMedia"></app-media>' +
|
||||
'<div class="page-content">' +
|
||||
'<div v-if="!page.headerMedia && page.icon" class="header-icon" :class="page.icon"></div>' +
|
||||
'<h2 v-html="page.headline"></h2>'+
|
||||
'<p v-if="page.subHeadline" v-html="page.subHeadline"></p>'+
|
||||
'<app-form :page="page"></app-form>' +
|
||||
'<app-pagination v-if="pagination" :pages="pages" :index="index"></app-pagination>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</transition>' +
|
||||
|
||||
'</div></div></div>',
|
||||
mounted: function(){
|
||||
var self = this;
|
||||
self.saveDataRequiredForSubmit();
|
||||
|
||||
self.$on('completeStep', function(){
|
||||
|
||||
if(WizardContainer !== undefined){
|
||||
WizardContainer.completeStep();
|
||||
}
|
||||
|
||||
self.$emit('close');
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* @param {string} direction
|
||||
*/
|
||||
changePage: function(direction){
|
||||
if(direction !== 'back' && direction !== 'next'){
|
||||
return;
|
||||
}
|
||||
|
||||
this.activePage = direction === 'next' ? this.activePage + 1 : this.activePage - 1;
|
||||
this.currentTransition = direction;
|
||||
},
|
||||
link: function(link)
|
||||
{
|
||||
window.location.href = link;
|
||||
},
|
||||
|
||||
/**
|
||||
* saves all data that was defined on the building JSON file in order to submit it later
|
||||
*/
|
||||
saveDataRequiredForSubmit: function(){
|
||||
var current;
|
||||
|
||||
for(var i = 0; i < this.pages.length; i++){
|
||||
current = this.pages[i].dataRequiredForSubmit;
|
||||
|
||||
if(current === undefined || current.length === 0){
|
||||
return;
|
||||
}
|
||||
this.setToStorage(current);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} object
|
||||
*/
|
||||
setToStorage: function(object){
|
||||
this.dataStorage.push(object);
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
getStorage: function(){
|
||||
return this.dataStorage;
|
||||
},
|
||||
|
||||
/**
|
||||
* clear storage, but keeps vue listener on this.dataStorage
|
||||
*/
|
||||
clearStorage: function(){
|
||||
for (var member in this.dataStorage) {
|
||||
delete this.dataStorage[member];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('app-form',{
|
||||
props: ['page'],
|
||||
data: function () {
|
||||
return {
|
||||
rowId: 0,
|
||||
surveyChoice: [],
|
||||
showSurveyError: false,
|
||||
formValid: true,
|
||||
formWasValidated: false,
|
||||
loading: false,
|
||||
errorMsg: 'Bitte überprüfe die Eingabefelder'
|
||||
}
|
||||
},
|
||||
template:
|
||||
'<form @submit.prevent="processForm" novalidate>' +
|
||||
'<div class="flex-container" v-for="(row, rowIndex) in page.form" :key="row.id">' +
|
||||
'<app-input-row v-if="row.inputs !== undefined && row.inputs.length > 0" ' +
|
||||
'ref="row" :row="row" ' +
|
||||
':hasSiblings="page.form.length > 1" ' +
|
||||
'@deleteme="removeInputRow(rowIndex)"></app-input-row>' +
|
||||
|
||||
'<span v-else-if="row.surveyButtons !== undefined && row.surveyButtons.length > 0" ' +
|
||||
'class="survey-button-container" ' +
|
||||
'v-for="button in row.surveyButtons">'+
|
||||
'<input type="checkbox" :id="button.value" name="data" :value="button.value" v-model="surveyChoice"/>'+
|
||||
'<label :for="button.value" class="button button-secondary" > {{ button.title }} </label>' +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
'<div class="flex-container" v-if="page.link">'+
|
||||
'<div v-if="page.link" class="add-row">'+
|
||||
'<a class="link" :href="page.link.link" >{{ page.link.title }}</a>'+
|
||||
'</div>' +
|
||||
'</div>'+
|
||||
'<transition name="fade">' +
|
||||
'<div v-if="page.errorMsg && showSurveyError && surveyChoice.length === 0" ' +
|
||||
'class="errorMsg">{{ page.errorMsg }}</div>'+
|
||||
'<div v-if="formWasValidated && !formValid" class="errorMsg"> {{ errorMsg }}</div>'+
|
||||
'</transition>' +
|
||||
'<button v-for="button in page.ctaButtons" ' +
|
||||
':type="button.action" class="button button-primary cta center">{{ button.title }}' +
|
||||
'<app-spinner v-if="loading"></app-spinner></button>' +
|
||||
'</form>',
|
||||
methods:{
|
||||
/**
|
||||
* @param {Object} row
|
||||
*/
|
||||
addInputRow: function(row){
|
||||
this.rowId++
|
||||
row.id = this.rowId;
|
||||
|
||||
for(var k = 0; k < row.inputs.length; k++){
|
||||
row.inputs[k].name += this.rowId;
|
||||
}
|
||||
|
||||
this.page.form.push(row);
|
||||
|
||||
for(var i = 0; i < this.page.form.length -1; i++){
|
||||
this.page.form[i].add.allow = false;
|
||||
}
|
||||
|
||||
if(row.add.maximum === this.page.form.length){
|
||||
this.allowAddOnLastRow(false);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {number} index
|
||||
*/
|
||||
removeInputRow: function(index){
|
||||
if(this.page.form.length <= 1){
|
||||
return;
|
||||
}
|
||||
|
||||
this.page.form.splice(index, 1);
|
||||
|
||||
this.allowAddOnLastRow(true);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} decision
|
||||
*/
|
||||
allowAddOnLastRow: function(decision){
|
||||
var lastIndex = this.page.form.length - 1;
|
||||
this.page.form[lastIndex].add.allow = decision;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} e
|
||||
*/
|
||||
processForm: function(e){
|
||||
this.validateForm();
|
||||
|
||||
if(!this.formValid){
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this.page.submitType){
|
||||
throw new Error("Please define submitType in your JSON");
|
||||
}
|
||||
|
||||
if(this.page.submitType === "save"){
|
||||
this.$parent.setToStorage(this.filterDataFromSubmitEvent(e));
|
||||
this.$parent.changePage("next");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.submitForm(e);
|
||||
},
|
||||
|
||||
validateForm: function(){
|
||||
if(this.page.submitType === 'survey'){
|
||||
this.formValid = this.surveyChoice.length !== 0;
|
||||
this.formWasValidated = true;
|
||||
|
||||
if(!this.formValid){
|
||||
this.showSurveyError = true;
|
||||
}
|
||||
} else {
|
||||
this.formValid = this.requiredRowsValid();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the required rows are valid
|
||||
* @returns {boolean}
|
||||
*/
|
||||
requiredRowsValid: function(){
|
||||
if(this.$refs === undefined){
|
||||
console.error("Please define ref on child component");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(this.$refs.row === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var current;
|
||||
|
||||
for(var i = 0; i < this.$refs.row.length; i++){
|
||||
current = this.$refs.row[i];
|
||||
|
||||
// case 1: if row has not been validated (no user input), form is valid in regard of this row
|
||||
if(!current.rowWasValidated){
|
||||
this.formWasValidated = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// case 2: if row is invalid, form is not valid
|
||||
// rowValid only includes required inputs (filtered out on row component)
|
||||
if(!current.rowValid){
|
||||
this.formWasValidated = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true; // if case1 or case2 didn't match, form valid
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Event} e
|
||||
*/
|
||||
submitForm: function(e){
|
||||
var request = new XMLHttpRequest(),
|
||||
self = this,
|
||||
data,
|
||||
responseJson;
|
||||
|
||||
data = this.prepareSubmitData(e);
|
||||
|
||||
request.open("POST", this.page.submitUrl + '', true);
|
||||
|
||||
request.addEventListener('load', function(event) {
|
||||
if (request.status >= 200 && request.status < 300) {
|
||||
console.log("POST " + request.statusText + " status: " + request.status);
|
||||
responseJson = JSON.parse(request.responseText);
|
||||
if(responseJson.page !== undefined) {
|
||||
self.$parent.pages.push(responseJson.page);
|
||||
}
|
||||
self.$parent.clearStorage();
|
||||
if(responseJson.dataRequiredForSubmit !== undefined){
|
||||
self.$parent.setToStorage(responseJson.dataRequiredForSubmit);
|
||||
}
|
||||
|
||||
self.$parent.changePage("next");
|
||||
|
||||
self.loading = false;
|
||||
} else {
|
||||
console.warn(request.statusText, request.responseText);
|
||||
|
||||
self.loading = false;
|
||||
self.formValid = false;
|
||||
self.formWasValidated = true;
|
||||
|
||||
responseJson = JSON.parse(request.responseText);
|
||||
|
||||
if(responseJson.error !== undefined) {
|
||||
self.errorMsg = responseJson.error;
|
||||
}
|
||||
else {
|
||||
self.errorMsg = 'Ooops, da ist etwas schief gelaufen. Bitte versuche es erneut.';
|
||||
}
|
||||
|
||||
if(responseJson.dataRequiredForSubmit !== undefined){
|
||||
self.$parent.setToStorage(responseJson.dataRequiredForSubmit);
|
||||
}
|
||||
}
|
||||
});
|
||||
self.loading = true;
|
||||
request.send(data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Combines all available data of all not-submitted pages
|
||||
*
|
||||
* @param {Object} e
|
||||
*
|
||||
* @returns {FormData}
|
||||
*/
|
||||
prepareSubmitData: function(e){
|
||||
var submitData = new FormData(),
|
||||
filteredEventData,
|
||||
storageData;
|
||||
|
||||
filteredEventData = this.filterDataFromSubmitEvent(e);
|
||||
storageData = JSON.parse(JSON.stringify(this.$parent.getStorage()));
|
||||
if(storageData !== undefined && storageData.length > 0){
|
||||
for(var i = 0; i < storageData.length; i++){
|
||||
|
||||
for(var key in storageData[i]){
|
||||
submitData.append(key, storageData[i][key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(filteredEventData !== undefined){
|
||||
for(var filteredEventDataKey in filteredEventData){
|
||||
submitData.append(filteredEventDataKey, filteredEventData[filteredEventDataKey]);
|
||||
}
|
||||
}
|
||||
return submitData;
|
||||
},
|
||||
|
||||
/**
|
||||
* Serializes all data from a form submit event
|
||||
*
|
||||
* @param e
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
filterDataFromSubmitEvent: function(e){
|
||||
var data = {},
|
||||
checkedInSurvey = [],
|
||||
current;
|
||||
|
||||
for(var i = 0; i < e.target.length; i++){
|
||||
current = e.target[i];
|
||||
|
||||
if(current.tagName === "button" || current.tagName === "BUTTON"){
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!current.name){
|
||||
throw new Error("Please define names for all inputs");
|
||||
}
|
||||
|
||||
if(this.page.type === "survey" && (current.tagName === "input" || current.tagName === "INPUT")){
|
||||
if(current.checked){
|
||||
checkedInSurvey.push(current.value);
|
||||
}
|
||||
|
||||
data[current.name] = checkedInSurvey;
|
||||
} else {
|
||||
if(current.type === 'checkbox') {
|
||||
if(current.checked){
|
||||
data[current.name] = current.value;
|
||||
}
|
||||
}
|
||||
else {
|
||||
data[current.name] = current.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('app-input-row',{
|
||||
props: ['row', 'hasSiblings'],
|
||||
data: function(){
|
||||
return {
|
||||
rowValid: true,
|
||||
rowWasValidated: false
|
||||
}
|
||||
},
|
||||
template:
|
||||
'<div class="app-row-container">' +
|
||||
'<div class="app-input-row" :class="{\'reduced-width\': row.inputs.length === 1 }">' +
|
||||
'<div class="app-row-valid" :class="{\'icon-ok\': rowValid && rowWasValidated}"></div>' +
|
||||
'<app-input ' +
|
||||
'v-for="(input, inputIndex) in row.inputs" ' +
|
||||
':type="input.type" ' +
|
||||
':validation="input.validation" ' +
|
||||
':customErrorMsg="input.customErrorMsg" ' +
|
||||
':name="input.name" ' +
|
||||
':label="input.label"' +
|
||||
':value="input.value"' +
|
||||
':connectedTo="input.connectedTo"' +
|
||||
':options="input.options"' +
|
||||
'ref="input"'+
|
||||
':key="inputIndex"></app-input>' +
|
||||
'<div v-if="row.removable && hasSiblings" @click="$emit(\'deleteme\')" class="remove-row"></div>' +
|
||||
'</div>' +
|
||||
'<div v-if="row.add && row.add.allow" @click="addRow" class="add-row">{{ row.add.text }}</div>' +
|
||||
'<div v-if="row.link" class="add-row"><a class="link" :href="row.link.link" >{{ row.link.title }}</a></div>' +
|
||||
'</div>',
|
||||
methods:{
|
||||
addRow: function(){
|
||||
var newRow = JSON.parse(JSON.stringify(this.row)); // removes vue observable and makes it possible to change
|
||||
|
||||
this.$parent.addInputRow(newRow);
|
||||
},
|
||||
|
||||
validateRow: function(){
|
||||
this.rowValid = this.requiredInputsValid();
|
||||
|
||||
this.rowWasValidated = true;
|
||||
|
||||
if(this.rowValid){
|
||||
this.$parent.validateForm();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the required Inputs are valid
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
requiredInputsValid: function(){
|
||||
if(this.$refs === undefined){
|
||||
throw new Error("Please define ref on child component");
|
||||
}
|
||||
|
||||
var valid = true,
|
||||
current;
|
||||
|
||||
for(var i = 0; i < this.$refs.input.length; i++){
|
||||
current = this.$refs.input[i];
|
||||
|
||||
if(!current.validation){
|
||||
valid = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
valid = current.validation && current.valid && current.wasValidated;
|
||||
|
||||
// row is invalid after first invalid input
|
||||
if(!valid){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('app-input', {
|
||||
props: ['type', 'validation', 'name', 'label', 'customErrorMsg', 'options', 'value', 'connectedTo'],
|
||||
data: function () {
|
||||
return {
|
||||
inputValue: this.value ? this.value : '',
|
||||
inputType: this.type,
|
||||
inputErrorMsg: undefined,
|
||||
valid: true,
|
||||
wasValidated: false
|
||||
}
|
||||
},
|
||||
template:
|
||||
'<div v-if="type === \'select\'" class="app-input select" :class="{\'input-error\': !valid }">' +
|
||||
'<select :id="name" :name="name" v-model="inputValue" :class="{\'hasSelected\': inputValue.length > 0 }" ' +
|
||||
'@change="validateInput" >' +
|
||||
'<option v-for="(option, index) in options" :value="option.value" :key="index">{{ option.text }}</option>' +
|
||||
'</select>' +
|
||||
'<label :for="name">{{ label }} <span v-if="validation"> (Pflichtfeld)</span></label>' +
|
||||
'</div>'+
|
||||
|
||||
'<div v-else class="app-input" :class="{\'input-error\': !valid}">' +
|
||||
//'<input style="display: none" type="password" />' +
|
||||
'<input :type="inputType" :id="name" :name="name" v-model="inputValue" ' +
|
||||
':class="{\'hasValue\': inputValue.length > 0 }" ' +
|
||||
'@blur="validateInput" autocomplete="off" required />' +
|
||||
'<div v-if="type === \'password\'" class="reveal" @click="togglePasswordVisibility"></div>' +
|
||||
'<label :for="name">{{ label }} <span v-if="validation"> (Pflichtfeld)</span></label>' +
|
||||
'<transition name="fade">' +
|
||||
'<div v-if="!valid && inputErrorMsg" class="input-error"> {{ inputErrorMsg }}</div>' +
|
||||
'</transition>'+
|
||||
'</div>',
|
||||
mounted: function(){
|
||||
var self = this;
|
||||
|
||||
// listens to compare request "broadcast" from other component
|
||||
self.$root.$on('compareConnected', function(data){
|
||||
if(self.name !== data.connectedTo) {
|
||||
return;
|
||||
}
|
||||
|
||||
// "broadcasts" to every component listening
|
||||
self.$root.$emit('comparisonResult', {
|
||||
requestingInput: data.requestingInput.name,
|
||||
valid: self.inputValue === data.requestingInput.inputValue && self.valid
|
||||
})
|
||||
});
|
||||
|
||||
self.$root.$on('comparisonResult', function(result){
|
||||
if(self.name === result.requestingInput){
|
||||
self.valid = result.valid;
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
validateInput: function(){
|
||||
if((this.inputValue.length === 0 && !this.wasValidated) || !this.validation){
|
||||
// input is valid if it has a value and wasn't validated before (inputs do not get validated on page render)
|
||||
// or if it's not necessary to validate
|
||||
this.valid = true;
|
||||
return;
|
||||
}
|
||||
|
||||
switch(this.type){
|
||||
case "email":
|
||||
var regex = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
this.valid = regex.test(this.inputValue);
|
||||
this.inputErrorMsg = this.customErrorMsg || "Adresse nicht gültig";
|
||||
break;
|
||||
|
||||
case "text":
|
||||
this.valid = this.inputValue.length >= 2;
|
||||
this.inputErrorMsg = this.customErrorMsg || "Mindestens zwei Zeichen";
|
||||
break;
|
||||
|
||||
case "password":
|
||||
|
||||
// "broadcasting" event to listening components
|
||||
// In this case we compare if passwords match in connected fields -> "connectedTo" option in JSON
|
||||
if(this.connectedTo !== undefined){
|
||||
this.$root.$emit('compareConnected', {
|
||||
connectedTo: this.connectedTo,
|
||||
requestingInput: {
|
||||
name: this.name,
|
||||
inputValue: this.inputValue
|
||||
}
|
||||
});
|
||||
this.inputErrorMsg = this.customErrorMsg || "Bitte wiederholen Sie das Passwort";
|
||||
|
||||
} else {
|
||||
this.valid = this.inputValue.length >= 4;
|
||||
this.inputErrorMsg = this.customErrorMsg || "Mindestens vier Zeichen";
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "select":
|
||||
// it's "selected/changed" (event) so it always has a valid value
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.wasValidated = true;
|
||||
|
||||
this.$parent.validateRow();
|
||||
},
|
||||
|
||||
togglePasswordVisibility: function(){
|
||||
this.inputType = this.inputType === 'password' ? 'text' : 'password';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('app-pagination', {
|
||||
props: ["pages", "index"],
|
||||
template: '' +
|
||||
'<div class="app-pagination">' +
|
||||
'<div v-for="(dot, dotIndex) in pages" :class="{\'active\': index === dotIndex}"></div>' +
|
||||
'</div>'
|
||||
});
|
||||
|
||||
Vue.component('app-spinner',{
|
||||
template: '<div class="spinner spinner-circle"></div>'
|
||||
});
|
||||
|
||||
Vue.component('app-media',{
|
||||
props: ["media"],
|
||||
template:
|
||||
'<div>' +
|
||||
'<iframe v-if="media.type === \'video\'"' +
|
||||
'class="media-youtube" ' +
|
||||
':src="media.link + \'?rel=0\'" ' +
|
||||
'frameborder="0" ' +
|
||||
'allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" ' +
|
||||
'allowfullscreen>' +
|
||||
'</iframe>' +
|
||||
|
||||
'<img ' +
|
||||
'v-if="media.type === \'image\'"' +
|
||||
'class="media-image"' +
|
||||
':src="media.link">' +
|
||||
'<img/>' +
|
||||
'</div>'
|
||||
});
|
||||
Reference in New Issue
Block a user