mirror of
https://github.com/silverstripe/silverstripe-userforms.git
synced 2024-10-22 15:05:42 +00:00
Merge pull request #293 from open-sausages/feature/multi-page-forms-v2
API (Major) Multipage forms, nested field grouping, UX improvements
This commit is contained in:
commit
a96d25ce1a
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
.DS_Store
|
||||
.sass-cache/
|
||||
|
12
.scrutinizer.yml
Normal file
12
.scrutinizer.yml
Normal file
@ -0,0 +1,12 @@
|
||||
inherit: true
|
||||
|
||||
tools:
|
||||
external_code_coverage: true
|
||||
|
||||
checks:
|
||||
php:
|
||||
code_rating: true
|
||||
duplication: true
|
||||
|
||||
filter:
|
||||
paths: [code/*, tests/*]
|
34
.travis.yml
34
.travis.yml
@ -4,8 +4,11 @@ language: php
|
||||
|
||||
sudo: false
|
||||
|
||||
php:
|
||||
- 5.4
|
||||
php:
|
||||
- 5.3
|
||||
- 5.4
|
||||
- 5.5
|
||||
- 5.6
|
||||
|
||||
env:
|
||||
- DB=MYSQL CORE_RELEASE=3
|
||||
@ -13,14 +16,27 @@ env:
|
||||
- DB=PGSQL CORE_RELEASE=3.1
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- php: 5.3
|
||||
env: DB=MYSQL CORE_RELEASE=3.1
|
||||
exclude:
|
||||
- php: 5.4
|
||||
env: DB=MYSQL CORE_RELEASE=3
|
||||
- php: 5.5
|
||||
env: DB=MYSQL CORE_RELEASE=3
|
||||
- php: 5.6
|
||||
env: DB=MYSQL CORE_RELEASE=3
|
||||
- php: 5.4
|
||||
env: DB=PGSQL CORE_RELEASE=3.1
|
||||
- php: 5.5
|
||||
env: DB=PGSQL CORE_RELEASE=3.1
|
||||
- php: 5.6
|
||||
env: DB=PGSQL CORE_RELEASE=3.1
|
||||
|
||||
before_script:
|
||||
- git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support
|
||||
- php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss
|
||||
- cd ~/builds/ss
|
||||
- git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support
|
||||
- php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss
|
||||
- cd ~/builds/ss
|
||||
|
||||
script:
|
||||
- phpunit userforms/tests/
|
||||
- phpunit --coverage-clover userforms/coverage.clover userforms/tests
|
||||
- cd userforms
|
||||
- wget https://scrutinizer-ci.com/ocular.phar
|
||||
- php ocular.phar code-coverage:upload --format=php-clover coverage.clover
|
||||
|
@ -32,65 +32,101 @@ class UserFormFieldEditorExtension extends DataExtension {
|
||||
* @return GridField
|
||||
*/
|
||||
public function getFieldEditorGrid() {
|
||||
Requirements::javascript(USERFORMS_DIR . '/javascript/FieldEditor.js');
|
||||
|
||||
$fields = $this->owner->Fields();
|
||||
|
||||
$this->createInitialFormStep(true);
|
||||
|
||||
$editableColumns = new GridFieldEditableColumns();
|
||||
$fieldClasses = singleton('EditableFormField')->getEditableFieldClasses();
|
||||
$editableColumns->setDisplayFields(array(
|
||||
'ClassName' => function($record, $column, $grid) {
|
||||
return DropdownField::create($column, '', $this->getEditableFieldClasses());
|
||||
'ClassName' => function($record, $column, $grid) use ($fieldClasses) {
|
||||
if($record instanceof EditableFormField) {
|
||||
return $record->getInlineClassnameField($column, $fieldClasses);
|
||||
}
|
||||
},
|
||||
'Title' => function($record, $column, $grid) {
|
||||
return TextField::create($column, ' ')
|
||||
->setAttribute('placeholder', _t('UserDefinedForm.TITLE', 'Title'));
|
||||
if($record instanceof EditableFormField) {
|
||||
return $record->getInlineTitleField($column);
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
$config = GridFieldConfig::create()
|
||||
->addComponents(
|
||||
$editableColumns,
|
||||
new GridFieldButtonRow(),
|
||||
GridFieldAddClassesButton::create('EditableTextField')
|
||||
->setButtonName(_t('UserFormFieldEditorExtension.ADD_FIELD', 'Add Field'))
|
||||
->setButtonClass('ss-ui-action-constructive'),
|
||||
GridFieldAddClassesButton::create('EditableFormStep')
|
||||
->setButtonName(_t('UserFormFieldEditorExtension.ADD_PAGE_BREAK', 'Add Page Break')),
|
||||
GridFieldAddClassesButton::create(array('EditableFieldGroup', 'EditableFieldGroupEnd'))
|
||||
->setButtonName(_t('UserFormFieldEditorExtension.ADD_FIELD_GROUP', 'Add Field Group')),
|
||||
new GridFieldEditButton(),
|
||||
new GridFieldDeleteAction(),
|
||||
new GridFieldToolbarHeader(),
|
||||
new GridFieldOrderableRows('Sort'),
|
||||
new GridFieldDetailForm()
|
||||
);
|
||||
|
||||
$fieldEditor = GridField::create(
|
||||
'Fields',
|
||||
_t('UserDefinedForm.FIELDS', 'Fields'),
|
||||
$fields,
|
||||
GridFieldConfig::create()
|
||||
->addComponents(
|
||||
$editableColumns,
|
||||
new GridFieldButtonRow(),
|
||||
new GridFieldAddNewInlineButton(),
|
||||
new GridFieldEditButton(),
|
||||
new GridFieldDeleteAction(),
|
||||
new GridFieldToolbarHeader(),
|
||||
new GridFieldOrderableRows('Sort'),
|
||||
new GridState_Component(),
|
||||
new GridFieldDetailForm()
|
||||
)
|
||||
);
|
||||
$config
|
||||
)->addExtraClass('uf-field-editor');
|
||||
|
||||
return $fieldEditor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* A UserForm must have at least one step.
|
||||
* If no steps exist, create an initial step, and put all fields inside it.
|
||||
*
|
||||
* @param bool $force
|
||||
* @return void
|
||||
*/
|
||||
public function getEditableFieldClasses() {
|
||||
$classes = ClassInfo::getValidSubClasses('EditableFormField');
|
||||
|
||||
// Remove classes we don't want to display in the dropdown.
|
||||
$classes = array_diff($classes, array(
|
||||
'EditableFormField',
|
||||
'EditableMultipleOptionField'
|
||||
));
|
||||
|
||||
$editableFieldClasses = array();
|
||||
|
||||
foreach ($classes as $key => $className) {
|
||||
$singleton = singleton($className);
|
||||
|
||||
if(!$singleton->canCreate()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$editableFieldClasses[$className] = $singleton->i18n_singular_name();
|
||||
public function createInitialFormStep($force = false) {
|
||||
// Only invoke once saved
|
||||
if(!$this->owner->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $editableFieldClasses;
|
||||
// Check if first field is a step
|
||||
$fields = $this->owner->Fields();
|
||||
$firstField = $fields->first();
|
||||
if($firstField instanceof EditableFormStep) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't create steps on write if there are no formfields, as this
|
||||
// can create duplicate first steps during publish of new records
|
||||
if(!$force && !$firstField) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-apply sort to each field starting at 2
|
||||
$next = 2;
|
||||
foreach($fields as $field) {
|
||||
$field->Sort = $next++;
|
||||
$field->write();
|
||||
}
|
||||
|
||||
// Add step
|
||||
$step = EditableFormStep::create();
|
||||
$step->Title = _t('EditableFormStep.TITLE_FIRST', 'First Page');
|
||||
$step->Sort = 1;
|
||||
$step->write();
|
||||
$fields->add($step);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that at least one page exists at the start
|
||||
*/
|
||||
public function onAfterWrite() {
|
||||
$this->createInitialFormStep();
|
||||
}
|
||||
|
||||
/**
|
||||
|
126
code/extensions/UserFormValidator.php
Normal file
126
code/extensions/UserFormValidator.php
Normal file
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
|
||||
class UserFormValidator extends RequiredFields {
|
||||
public function php($data) {
|
||||
if(!parent::php($data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip unsaved records
|
||||
if(empty($data['ID']) || !is_numeric($data['ID'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$fields = EditableFormField::get()->filter('ParentID', $data['ID'])->sort('"Sort" ASC');
|
||||
|
||||
// Current nesting
|
||||
$stack = array();
|
||||
$conditionalStep = false; // Is the current step conditional?
|
||||
foreach($fields as $field) {
|
||||
if($field instanceof EditableFormStep) {
|
||||
// Page at top level, or after another page is ok
|
||||
if(empty($stack) || (count($stack) === 1 && $stack[0] instanceof EditableFormStep)) {
|
||||
$stack = array($field);
|
||||
$conditionalStep = $field->DisplayRules()->count() > 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->validationError(
|
||||
'FormFields',
|
||||
_t(
|
||||
"UserFormValidator.UNEXPECTED_BREAK",
|
||||
"Unexpected page break '{name}' inside nested field '{group}'",
|
||||
array(
|
||||
'name' => $field->CMSTitle,
|
||||
'group' => end($stack)->CMSTitle
|
||||
)
|
||||
),
|
||||
'error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate no pages
|
||||
if(empty($stack)) {
|
||||
$this->validationError(
|
||||
'FormFields',
|
||||
_t(
|
||||
"UserFormValidator.NO_PAGE",
|
||||
"Field '{name}' found before any pages",
|
||||
array(
|
||||
'name' => $field->CMSTitle
|
||||
)
|
||||
),
|
||||
'error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Nest field group
|
||||
if($field instanceof EditableFieldGroup) {
|
||||
$stack[] = $field;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unnest field group
|
||||
if($field instanceof EditableFieldGroupEnd) {
|
||||
$top = end($stack);
|
||||
|
||||
// Check that the top is a group at all
|
||||
if(!$top instanceof EditableFieldGroup) {
|
||||
$this->validationError(
|
||||
'FormFields',
|
||||
_t(
|
||||
"UserFormValidator.UNEXPECTED_GROUP_END",
|
||||
"'{name}' found without a matching group",
|
||||
array(
|
||||
'name' => $field->CMSTitle
|
||||
)
|
||||
),
|
||||
'error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check that the top is the right group
|
||||
if($top->EndID != $field->ID) {
|
||||
$this->validationError(
|
||||
'FormFields',
|
||||
_t(
|
||||
"UserFormValidator.WRONG_GROUP_END",
|
||||
"'{name}' found closes the wrong group '{group}'",
|
||||
array(
|
||||
'name' => $field->CMSTitle,
|
||||
'group' => $top->CMSTitle
|
||||
)
|
||||
),
|
||||
'error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Unnest group
|
||||
array_pop($stack);
|
||||
}
|
||||
|
||||
// Normal field type
|
||||
if($conditionalStep && $field->Required) {
|
||||
$this->validationError(
|
||||
'FormFields',
|
||||
_t(
|
||||
"UserFormValidator.CONDITIONAL_REQUIRED",
|
||||
"Required field '{name}' cannot be placed within a conditional page",
|
||||
array(
|
||||
'name' => $field->CMSTitle
|
||||
)
|
||||
),
|
||||
'error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
47
code/formfields/UserFormsCompositeField.php
Normal file
47
code/formfields/UserFormsCompositeField.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Represents a composite field group, which may contain other groups
|
||||
*/
|
||||
abstract class UserFormsCompositeField extends CompositeField implements UserFormsFieldContainer {
|
||||
|
||||
/**
|
||||
* Parent field
|
||||
*
|
||||
* @var UserFormsFieldContainer
|
||||
*/
|
||||
protected $parent = null;
|
||||
|
||||
public function getParent() {
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
public function setParent(UserFormsFieldContainer $parent) {
|
||||
$this->parent = $parent;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function processNext(EditableFormField $field) {
|
||||
// When we find a step, bubble up to the top
|
||||
if($field instanceof EditableFormStep) {
|
||||
return $this->getParent()->processNext($field);
|
||||
}
|
||||
|
||||
// Skip over fields that don't generate formfields
|
||||
$formField = $field->getFormField();
|
||||
if(!$formField) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Save this field
|
||||
$this->push($formField);
|
||||
|
||||
// Nest fields that are containers
|
||||
if($formField instanceof UserFormsFieldContainer) {
|
||||
return $formField->setParent($this);
|
||||
}
|
||||
|
||||
// Add any subsequent fields to this
|
||||
return $this;
|
||||
}
|
||||
}
|
30
code/formfields/UserFormsFieldContainer.php
Normal file
30
code/formfields/UserFormsFieldContainer.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Represents a field container which can iteratively process nested fields, converting it into a fieldset
|
||||
*/
|
||||
interface UserFormsFieldContainer {
|
||||
|
||||
/**
|
||||
* Process the next field in the list, returning the container to add the next field to.
|
||||
*
|
||||
* @param EditableFormField $field
|
||||
* @return EditableContainerField
|
||||
*/
|
||||
public function processNext(EditableFormField $field);
|
||||
|
||||
/**
|
||||
* Set the parent
|
||||
*
|
||||
* @param UserFormsFieldContainer $parent
|
||||
* @return $this
|
||||
*/
|
||||
public function setParent(UserFormsFieldContainer $parent);
|
||||
|
||||
/**
|
||||
* Get the parent
|
||||
*
|
||||
* @return UserFormsFieldContainer
|
||||
*/
|
||||
public function getParent();
|
||||
}
|
43
code/formfields/UserFormsFieldList.php
Normal file
43
code/formfields/UserFormsFieldList.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A list of formfields which allows for iterative processing of nested composite fields
|
||||
*/
|
||||
class UserFormsFieldList extends FieldList implements UserFormsFieldContainer {
|
||||
|
||||
public function processNext(EditableFormField $field) {
|
||||
$formField = $field->getFormField();
|
||||
if(!$formField) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->push($formField);
|
||||
|
||||
if($formField instanceof UserFormsFieldContainer) {
|
||||
return $formField->setParent($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParent() {
|
||||
// Field list does not have a parent
|
||||
return null;
|
||||
}
|
||||
|
||||
public function setParent(UserFormsFieldContainer $parent) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all empty steps
|
||||
*/
|
||||
public function clearEmptySteps() {
|
||||
foreach($this as $field) {
|
||||
if($field instanceof UserFormsStepField && count($field->getChildren()) === 0) {
|
||||
$this->remove($field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
27
code/formfields/UserFormsGroupField.php
Normal file
27
code/formfields/UserFormsGroupField.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Front end composite field for userforms
|
||||
*/
|
||||
class UserFormsGroupField extends UserFormsCompositeField {
|
||||
|
||||
public function __construct($children = null) {
|
||||
parent::__construct($children);
|
||||
$this->setTag('fieldset');
|
||||
}
|
||||
|
||||
public function getLegend() {
|
||||
// Legend defaults to title
|
||||
return parent::getLegend() ?: $this->Title();
|
||||
}
|
||||
|
||||
public function processNext(EditableFormField $field) {
|
||||
// When ending a group, jump up one level
|
||||
if($field instanceof EditableFieldGroupEnd) {
|
||||
return $this->getParent();
|
||||
}
|
||||
|
||||
// Otherwise behave as per normal composite field
|
||||
return parent::processNext($field);
|
||||
}
|
||||
}
|
44
code/formfields/UserFormsStepField.php
Normal file
44
code/formfields/UserFormsStepField.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Represents a page step in a form, which may contain form fields or other groups
|
||||
*/
|
||||
class UserFormsStepField extends UserFormsCompositeField {
|
||||
|
||||
private static $casting = array(
|
||||
'StepNumber' => 'Int'
|
||||
);
|
||||
|
||||
/**
|
||||
* Numeric index (1 based) of this step
|
||||
*
|
||||
* Null if unassigned
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
protected $number = null;
|
||||
|
||||
public function FieldHolder($properties = array()) {
|
||||
return $this->Field($properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the step number
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getStepNumber() {
|
||||
return $this->number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-assign this step to another number
|
||||
*
|
||||
* @param type $number
|
||||
* @return $this
|
||||
*/
|
||||
public function setStepNumber($number) {
|
||||
$this->number = $number;
|
||||
return $this;
|
||||
}
|
||||
}
|
232
code/forms/GridFieldAddClassesButton.php
Normal file
232
code/forms/GridFieldAddClassesButton.php
Normal file
@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A button which allows objects to be created with a specified classname(s)
|
||||
*/
|
||||
class GridFieldAddClassesButton extends Object implements GridField_HTMLProvider, GridField_ActionProvider {
|
||||
|
||||
/**
|
||||
* Name of fragment to insert into
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $targetFragment;
|
||||
|
||||
/**
|
||||
* Button title
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $buttonName;
|
||||
|
||||
/**
|
||||
* Additonal CSS classes for the button
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $buttonClass = null;
|
||||
|
||||
/**
|
||||
* Class names
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $modelClasses = null;
|
||||
|
||||
/**
|
||||
* @param array $classes Class or list of classes to create.
|
||||
* If you enter more than one class, each click of the "add" button will create one of each
|
||||
* @param string $targetFragment The fragment to render the button into
|
||||
*/
|
||||
public function __construct($classes, $targetFragment = 'buttons-before-left') {
|
||||
parent::__construct();
|
||||
$this->setClasses($classes);
|
||||
$this->setFragment($targetFragment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the button name
|
||||
*
|
||||
* @param string $name
|
||||
* @return $this
|
||||
*/
|
||||
public function setButtonName($name) {
|
||||
$this->buttonName = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the button name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getButtonName() {
|
||||
return $this->buttonName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the fragment name this button is rendered into.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFragment() {
|
||||
return $this->targetFragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the fragment name this button is rendered into.
|
||||
*
|
||||
* @param string $fragment
|
||||
* @return GridFieldAddNewInlineButton $this
|
||||
*/
|
||||
public function setFragment($fragment) {
|
||||
$this->targetFragment = $fragment;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extra button class
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getButtonClass() {
|
||||
return $this->buttonClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets extra CSS classes for this button
|
||||
*
|
||||
* @param string $buttonClass
|
||||
* @return $this
|
||||
*/
|
||||
public function setButtonClass($buttonClass) {
|
||||
$this->buttonClass = $buttonClass;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the classes of the objects to create
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getClasses() {
|
||||
return $this->modelClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of classes which can be created, with checks for permissions.
|
||||
* Will fallback to the default model class for the given DataGrid
|
||||
*
|
||||
* @param DataGrid $grid
|
||||
* @return array
|
||||
*/
|
||||
public function getClassesCreate($grid) {
|
||||
// Get explicit or fallback class list
|
||||
$classes = $this->getClasses();
|
||||
if(empty($classes) && $grid) {
|
||||
$classes = array($grid->getModelClass());
|
||||
}
|
||||
|
||||
// Filter out classes without permission
|
||||
return array_filter($classes, function($class) {
|
||||
return singleton($class)->canCreate();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the classes to create
|
||||
*
|
||||
* @param array $classes
|
||||
*/
|
||||
public function setClasses($classes) {
|
||||
if(!is_array($classes)) {
|
||||
$classes = $classes ? array($classes) : array();
|
||||
}
|
||||
$this->modelClasses = $classes;
|
||||
}
|
||||
|
||||
public function getHTMLFragments($grid) {
|
||||
// Check create permission
|
||||
$singleton = singleton($grid->getModelClass());
|
||||
if(!$singleton->canCreate()) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Get button name
|
||||
$buttonName = $this->getButtonName();
|
||||
if(!$buttonName) {
|
||||
// provide a default button name, can be changed by calling {@link setButtonName()} on this component
|
||||
$objectName = $singleton->i18n_singular_name();
|
||||
$buttonName = _t('GridField.Add', 'Add {name}', array('name' => $objectName));
|
||||
}
|
||||
|
||||
$addAction = new GridField_FormAction(
|
||||
$grid,
|
||||
$this->getAction(),
|
||||
$buttonName,
|
||||
$this->getAction(),
|
||||
array()
|
||||
);
|
||||
$addAction->setAttribute('data-icon', 'add');
|
||||
|
||||
if($this->getButtonClass()) {
|
||||
$addAction->addExtraClass($this->getButtonClass());
|
||||
}
|
||||
|
||||
return array(
|
||||
$this->targetFragment => $addAction->forTemplate()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getActions($gridField) {
|
||||
return array(
|
||||
$this->getAction()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the action suburl for this component
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getAction() {
|
||||
return 'add-classes-' . strtolower(implode('-', $this->getClasses()));
|
||||
}
|
||||
|
||||
public function handleAction(GridField $gridField, $actionName, $arguments, $data) {
|
||||
switch(strtolower($actionName)) {
|
||||
case $this->getAction():
|
||||
return $this->handleAdd($gridField);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles adding a new instance of a selected class.
|
||||
*
|
||||
* @param GridField $grid
|
||||
* @return null
|
||||
*/
|
||||
public function handleAdd($grid) {
|
||||
$classes = $this->getClassesCreate($grid);
|
||||
if(empty($classes)) {
|
||||
throw new SS_HTTPResponse_Exception(400);
|
||||
}
|
||||
|
||||
// Add item to gridfield
|
||||
$list = $grid->getList();
|
||||
foreach($classes as $class) {
|
||||
$item = $class::create();
|
||||
$item->write();
|
||||
$list->add($item);
|
||||
}
|
||||
|
||||
// Should trigger a simple reload
|
||||
return null;
|
||||
}
|
||||
}
|
@ -22,6 +22,12 @@ class UserForm extends Form {
|
||||
$this->getRequiredFields()
|
||||
);
|
||||
|
||||
// Number each page
|
||||
$stepNumber = 1;
|
||||
foreach($this->getSteps() as $step) {
|
||||
$step->setStepNumber($stepNumber++);
|
||||
}
|
||||
|
||||
if($controller->DisableCsrfSecurityToken) {
|
||||
$this->disableSecurityToken();
|
||||
}
|
||||
@ -35,57 +41,50 @@ class UserForm extends Form {
|
||||
$this->extend('updateForm');
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for partial caching in the template.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLastEdited() {
|
||||
return $this->controller->LastEdited;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getDisplayErrorMessagesAtTop() {
|
||||
return (bool)$this->controller->DisplayErrorMessagesAtTop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the fieldlist, filtered to only contain steps
|
||||
*
|
||||
* @return ArrayList
|
||||
*/
|
||||
public function getSteps() {
|
||||
return $this->Fields()->filterByCallback(function($field) {
|
||||
return $field instanceof UserFormsStepField;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the form fields for the form on this page. Can modify this FieldSet
|
||||
* by using {@link updateFormFields()} on an {@link Extension} subclass which
|
||||
* is applied to this controller.
|
||||
*
|
||||
* This will be a list of top level composite steps
|
||||
*
|
||||
* @return FieldList
|
||||
*/
|
||||
public function getFormFields() {
|
||||
$fields = new FieldList();
|
||||
|
||||
foreach($this->controller->Fields() as $editableField) {
|
||||
// get the raw form field from the editable version
|
||||
$field = $editableField->getFormField();
|
||||
|
||||
if(!$field) continue;
|
||||
|
||||
// set the error / formatting messages
|
||||
$field->setCustomValidationMessage($editableField->getErrorMessage());
|
||||
|
||||
// set the right title on this field
|
||||
if($right = $editableField->RightTitle) {
|
||||
// Since this field expects raw html, safely escape the user data prior
|
||||
$field->setRightTitle(Convert::raw2xml($right));
|
||||
}
|
||||
|
||||
// if this field is required add some
|
||||
if($editableField->Required) {
|
||||
$field->addExtraClass('requiredField');
|
||||
|
||||
if($identifier = UserDefinedForm::config()->required_identifier) {
|
||||
|
||||
$title = $field->Title() ." <span class='required-identifier'>". $identifier . "</span>";
|
||||
$field->setTitle($title);
|
||||
}
|
||||
}
|
||||
// if this field has an extra class
|
||||
if($extraClass = $editableField->ExtraClass) {
|
||||
$field->addExtraClass(Convert::raw2att($extraClass));
|
||||
}
|
||||
|
||||
// set the values passed by the url to the field
|
||||
$request = $this->controller->getRequest();
|
||||
if($value = $request->getVar($field->getName())) {
|
||||
$field->setValue($value);
|
||||
}
|
||||
|
||||
$fields->push($field);
|
||||
$fields = new UserFormsFieldList();
|
||||
$target = $fields;
|
||||
foreach ($this->controller->Fields() as $field) {
|
||||
$target = $target->processNext($field);
|
||||
}
|
||||
|
||||
$fields->clearEmptySteps();
|
||||
$this->extend('updateFormFields', $fields);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
@ -128,9 +127,7 @@ class UserForm extends Form {
|
||||
->filter('Required', true)
|
||||
->column('Name');
|
||||
$required = new RequiredFields($requiredNames);
|
||||
|
||||
$this->extend('updateRequiredFields', $required);
|
||||
|
||||
return $required;
|
||||
}
|
||||
|
||||
@ -155,4 +152,20 @@ class UserForm extends Form {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override some we can add UserForm specific attributes to the form.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAttributes() {
|
||||
$attrs = parent::getAttributes();
|
||||
|
||||
$attrs['class'] = $attrs['class'] . ' userform';
|
||||
$attrs['data-livevalidation'] = (bool)$this->controller->EnableLiveValidation;
|
||||
$attrs['data-toperrors'] = (bool)$this->controller->DisplayErrorMessagesAtTop;
|
||||
$attrs['data-hidefieldlabels'] = (bool)$this->controller->HideFieldLabels;
|
||||
|
||||
return $attrs;
|
||||
}
|
||||
}
|
||||
|
@ -98,6 +98,7 @@ class UserDefinedForm extends Page {
|
||||
* @return FieldList
|
||||
*/
|
||||
public function getCMSFields() {
|
||||
Requirements::css(USERFORMS_DIR . '/css/UserForm_cms.css');
|
||||
|
||||
$self = $this;
|
||||
|
||||
@ -169,7 +170,6 @@ SQL;
|
||||
$config->addComponent($filter = new UserFormsGridFieldFilterHeader());
|
||||
$config->addComponent(new GridFieldDataColumns());
|
||||
$config->addComponent(new GridFieldEditButton());
|
||||
$config->addComponent(new GridState_Component());
|
||||
$config->addComponent(new GridFieldDeleteAction());
|
||||
$config->addComponent(new GridFieldPageCount('toolbar-header-right'));
|
||||
$config->addComponent($pagination = new GridFieldPaginator(25));
|
||||
@ -283,6 +283,14 @@ SQL;
|
||||
|
||||
DB::alteration_message('Migrated userforms', 'changed');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validate formfields
|
||||
*/
|
||||
public function getCMSValidator() {
|
||||
return new UserFormValidator();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -307,10 +315,11 @@ class UserDefinedForm_Controller extends Page_Controller {
|
||||
|
||||
// load the jquery
|
||||
$lang = i18n::get_lang_from_locale(i18n::get_locale());
|
||||
Requirements::css(USERFORMS_DIR . '/css/UserForm.css');
|
||||
Requirements::javascript(FRAMEWORK_DIR .'/thirdparty/jquery/jquery.js');
|
||||
Requirements::javascript(USERFORMS_DIR . '/thirdparty/jquery-validate/jquery.validate.min.js');
|
||||
Requirements::add_i18n_javascript(USERFORMS_DIR . '/javascript/lang');
|
||||
Requirements::javascript(USERFORMS_DIR . '/javascript/UserForm_frontend.js');
|
||||
Requirements::javascript(USERFORMS_DIR . '/javascript/UserForm.js');
|
||||
Requirements::javascript(
|
||||
USERFORMS_DIR . "/thirdparty/jquery-validate/localization/messages_{$lang}.min.js"
|
||||
);
|
||||
@ -364,28 +373,10 @@ class UserDefinedForm_Controller extends Page_Controller {
|
||||
*/
|
||||
public function Form() {
|
||||
$form = UserForm::create($this);
|
||||
|
||||
$this->generateConditionalJavascript();
|
||||
$this->generateValidationJavascript($form);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build jQuery validation script and require as a custom script
|
||||
*
|
||||
* @param UserForm $form
|
||||
*/
|
||||
public function generateValidationJavascript(UserForm $form) {
|
||||
// set the custom script for this form
|
||||
Requirements::customScript(
|
||||
$this
|
||||
->customise(array('Form' => $form))
|
||||
->renderWith('ValidationScript'),
|
||||
'UserFormsValidation'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the javascript for the conditional field show / hiding logic.
|
||||
*
|
||||
@ -400,15 +391,11 @@ class UserDefinedForm_Controller extends Page_Controller {
|
||||
|
||||
if($this->Fields()) {
|
||||
foreach($this->Fields() as $field) {
|
||||
$fieldId = $field->Name;
|
||||
|
||||
if($field instanceof EditableFormHeading) {
|
||||
$fieldId = 'UserForm_Form_' . $field->Name;
|
||||
}
|
||||
$holderSelector = $field->getSelectorHolder();
|
||||
|
||||
// Is this Field Show by Default
|
||||
if(!$field->ShowOnLoad) {
|
||||
$default .= "$(\"#" . $fieldId . "\").hide();\n";
|
||||
$default .= "{$holderSelector}.hide().trigger('userform.field.hide');\n";
|
||||
}
|
||||
|
||||
// Check for field dependencies / default
|
||||
@ -417,22 +404,8 @@ class UserDefinedForm_Controller extends Page_Controller {
|
||||
// Get the field which is effected
|
||||
$formFieldWatch = EditableFormField::get()->byId($rule->ConditionFieldID);
|
||||
|
||||
if($formFieldWatch->RecordClassName == 'EditableDropdown') {
|
||||
// watch out for multiselect options - radios and check boxes
|
||||
$fieldToWatch = "$(\"select[name='" . $formFieldWatch->Name . "']\")";
|
||||
$fieldToWatchOnLoad = $fieldToWatch;
|
||||
} else if($formFieldWatch->RecordClassName == 'EditableCheckboxGroupField') {
|
||||
// watch out for checkboxs as the inputs don't have values but are 'checked
|
||||
$fieldToWatch = "$(\"input[name='" . $formFieldWatch->Name . "[" . $rule->FieldValue . "]']\")";
|
||||
$fieldToWatchOnLoad = $fieldToWatch;
|
||||
} else if($formFieldWatch->RecordClassName == 'EditableRadioField') {
|
||||
$fieldToWatch = "$(\"input[name='" . $formFieldWatch->Name . "']\")";
|
||||
// We only want to trigger on load once for the radio group - hence we focus on the first option only.
|
||||
$fieldToWatchOnLoad = "$(\"input[name='" . $formFieldWatch->Name . "']:first\")";
|
||||
} else {
|
||||
$fieldToWatch = "$(\"input[name='" . $formFieldWatch->Name . "']\")";
|
||||
$fieldToWatchOnLoad = $fieldToWatch;
|
||||
}
|
||||
$fieldToWatch = $formFieldWatch->getSelectorField($rule);
|
||||
$fieldToWatchOnLoad = $formFieldWatch->getSelectorField($rule, true);
|
||||
|
||||
// show or hide?
|
||||
$view = ($rule->Display == 'Hide') ? 'hide' : 'show';
|
||||
@ -442,7 +415,7 @@ class UserDefinedForm_Controller extends Page_Controller {
|
||||
// @todo encapulsation
|
||||
$action = "change";
|
||||
|
||||
if($formFieldWatch->ClassName == "EditableTextField") {
|
||||
if($formFieldWatch instanceof EditableTextField) {
|
||||
$action = "keyup";
|
||||
}
|
||||
|
||||
@ -460,11 +433,11 @@ class UserDefinedForm_Controller extends Page_Controller {
|
||||
// and what should we evaluate
|
||||
switch($rule->ConditionOption) {
|
||||
case 'IsNotBlank':
|
||||
$expression = ($checkboxField || $radioField) ? '$(this).prop("checked")' :'$(this).val() != ""';
|
||||
$expression = ($checkboxField || $radioField) ? '$(this).is(":checked")' :'$(this).val() != ""';
|
||||
|
||||
break;
|
||||
case 'IsBlank':
|
||||
$expression = ($checkboxField || $radioField) ? '!($(this).prop("checked"))' : '$(this).val() == ""';
|
||||
$expression = ($checkboxField || $radioField) ? '!($(this).is(":checked"))' : '$(this).val() == ""';
|
||||
|
||||
break;
|
||||
case 'HasValue':
|
||||
@ -511,9 +484,9 @@ class UserDefinedForm_Controller extends Page_Controller {
|
||||
$watch[$fieldToWatch] = array();
|
||||
}
|
||||
|
||||
$watch[$fieldToWatch][] = array(
|
||||
$watch[$fieldToWatch][] = array(
|
||||
'expression' => $expression,
|
||||
'field_id' => $fieldId,
|
||||
'holder_selector' => $holderSelector,
|
||||
'view' => $view,
|
||||
'opposite' => $opposite,
|
||||
'action' => $action
|
||||
@ -530,16 +503,26 @@ class UserDefinedForm_Controller extends Page_Controller {
|
||||
$actions = array();
|
||||
|
||||
foreach($values as $rule) {
|
||||
// Register conditional behaviour with an element, so it can be triggered from many places.
|
||||
$logic[] = sprintf(
|
||||
'if(%s) { $("#%s").%s(); } else { $("#%2$s").%s(); }',
|
||||
$rule['expression'],
|
||||
$rule['field_id'],
|
||||
$rule['view'],
|
||||
$rule['opposite']
|
||||
);
|
||||
|
||||
// Assign action
|
||||
$actions[$rule['action']] = $rule['action'];
|
||||
|
||||
// Assign behaviour
|
||||
$expression = $rule['expression'];
|
||||
$holder = $rule['holder_selector'];
|
||||
$view = $rule['view']; // hide or show
|
||||
$opposite = $rule['opposite'];
|
||||
// Generated javascript for triggering visibility
|
||||
$logic[] = <<<"EOS"
|
||||
if({$expression}) {
|
||||
{$holder}
|
||||
.{$view}()
|
||||
.trigger('userform.field.{$view}');
|
||||
} else {
|
||||
{$holder}
|
||||
.{$opposite}()
|
||||
.trigger('userform.field.{$opposite}');
|
||||
}
|
||||
EOS;
|
||||
}
|
||||
|
||||
$logic = implode("\n", $logic);
|
||||
@ -581,41 +564,15 @@ JS
|
||||
|
||||
/**
|
||||
* Process the form that is submitted through the site
|
||||
*
|
||||
*
|
||||
* {@see UserForm::validate()} for validation step prior to processing
|
||||
*
|
||||
* @param array $data
|
||||
* @param Form $form
|
||||
*
|
||||
* @return Redirection
|
||||
*/
|
||||
public function process($data, $form) {
|
||||
Session::set("FormInfo.{$form->FormName()}.data",$data);
|
||||
Session::clear("FormInfo.{$form->FormName()}.errors");
|
||||
|
||||
foreach($this->Fields() as $field) {
|
||||
$messages[$field->Name] = $field->getErrorMessage()->HTML();
|
||||
$formField = $field->getFormField();
|
||||
|
||||
if($field->Required && $field->DisplayRules()->Count() == 0) {
|
||||
if(isset($data[$field->Name])) {
|
||||
$formField->setValue($data[$field->Name]);
|
||||
}
|
||||
|
||||
if(
|
||||
!isset($data[$field->Name]) ||
|
||||
!$data[$field->Name] ||
|
||||
!$formField->validate($form->getValidator())
|
||||
) {
|
||||
$form->addErrorMessage($field->Name, $field->getErrorMessage(), 'bad');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(Session::get("FormInfo.{$form->FormName()}.errors")){
|
||||
Controller::curr()->redirectBack();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$submittedForm = Object::create('SubmittedForm');
|
||||
$submittedForm->SubmittedByID = ($id = Member::currentUserID()) ? $id : 0;
|
||||
$submittedForm->ParentID = $this->ID;
|
||||
@ -625,9 +582,7 @@ JS
|
||||
$submittedForm->write();
|
||||
}
|
||||
|
||||
$values = array();
|
||||
$attachments = array();
|
||||
|
||||
$submittedFields = new ArrayList();
|
||||
|
||||
foreach($this->Fields() as $field) {
|
||||
@ -809,10 +764,10 @@ JS
|
||||
* Allows the use of field values in email body.
|
||||
*
|
||||
* @param ArrayList fields
|
||||
* @return ViewableData
|
||||
* @return ArrayData
|
||||
*/
|
||||
private function getMergeFieldsMap($fields = array()) {
|
||||
$data = new ViewableData();
|
||||
$data = new ArrayData(array());
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$data->setField($field->Name, DBField::create_field('Text', $field->Value));
|
||||
|
@ -14,17 +14,16 @@ class EditableCheckboxGroupField extends EditableMultipleOptionField {
|
||||
private static $plural_name = "Checkbox Groups";
|
||||
|
||||
public function getFormField() {
|
||||
$optionSet = $this->Options();
|
||||
$optionMap = $optionSet->map('EscapedTitle', 'Title');
|
||||
|
||||
$field = new UserFormsCheckboxSetField($this->Name, $this->Title, $optionMap);
|
||||
$field = new UserFormsCheckboxSetField($this->Name, $this->EscapedTitle, $this->getOptionsMap());
|
||||
$field->setTemplate('forms/UserFormsCheckboxSetField');
|
||||
|
||||
// Set the default checked items
|
||||
$defaultCheckedItems = $optionSet->filter('Default', 1);
|
||||
$defaultCheckedItems = $this->getDefaultOptions();
|
||||
if ($defaultCheckedItems->count()) {
|
||||
$field->setDefaultItems($defaultCheckedItems->map('EscapedTitle')->keys());
|
||||
}
|
||||
|
||||
$this->doUpdateFormField($field);
|
||||
return $field;
|
||||
}
|
||||
|
||||
@ -46,4 +45,14 @@ class EditableCheckboxGroupField extends EditableMultipleOptionField {
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getSelectorField(EditableCustomRule $rule, $forOnLoad = false) {
|
||||
// watch out for checkboxs as the inputs don't have values but are 'checked
|
||||
// @todo - Test this
|
||||
if($rule->FieldValue) {
|
||||
return "$(\"input[name='{$this->Name}[]'][value='{$rule->FieldValue}']\")";
|
||||
} else {
|
||||
return "$(\"input[name='{$this->Name}[]']:first\")";
|
||||
}
|
||||
}
|
||||
}
|
@ -23,7 +23,9 @@ class EditableCountryDropdownField extends EditableFormField {
|
||||
}
|
||||
|
||||
public function getFormField() {
|
||||
return CountryDropdownField::create($this->Name, $this->Title);
|
||||
$field = CountryDropdownField::create($this->Name, $this->EscapedTitle);
|
||||
$this->doUpdateFormField($field);
|
||||
return $field;
|
||||
}
|
||||
|
||||
public function getValueFromData($data) {
|
||||
@ -36,4 +38,8 @@ class EditableCountryDropdownField extends EditableFormField {
|
||||
public function getIcon() {
|
||||
return USERFORMS_DIR . '/images/editabledropdown.png';
|
||||
}
|
||||
|
||||
public function getSelectorField(EditableCustomRule $rule, $forOnLoad = false) {
|
||||
return "$(\"select[name='{$this->Name}']\")";
|
||||
}
|
||||
}
|
@ -43,17 +43,9 @@ class EditableDateField extends EditableFormField {
|
||||
$defaultValue = $this->DefaultToToday
|
||||
? SS_Datetime::now()->Format('Y-m-d')
|
||||
: $this->Default;
|
||||
$field = EditableDateField_FormField::create( $this->Name, $this->Title, $defaultValue);
|
||||
$field = EditableDateField_FormField::create( $this->Name, $this->EscapedTitle, $defaultValue);
|
||||
$field->setConfig('showcalendar', true);
|
||||
|
||||
if ($this->Required) {
|
||||
// Required validation can conflict so add the Required validation messages
|
||||
// as input attributes
|
||||
$errorMessage = $this->getErrorMessage()->HTML();
|
||||
$field->setAttribute('data-rule-required', 'true');
|
||||
$field->setAttribute('data-msg-required', $errorMessage);
|
||||
}
|
||||
|
||||
$this->doUpdateFormField($field);
|
||||
return $field;
|
||||
}
|
||||
}
|
45
code/model/editableformfields/EditableDropdown.php
Executable file
45
code/model/editableformfields/EditableDropdown.php
Executable file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/**
|
||||
* EditableDropdown
|
||||
*
|
||||
* Represents a modifiable dropdown (select) box on a form
|
||||
*
|
||||
* @package userforms
|
||||
*/
|
||||
|
||||
class EditableDropdown extends EditableMultipleOptionField {
|
||||
|
||||
private static $singular_name = 'Dropdown Field';
|
||||
|
||||
private static $plural_name = 'Dropdowns';
|
||||
|
||||
/**
|
||||
* @return FieldList
|
||||
*/
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
$fields->removeByName('Default');
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DropdownField
|
||||
*/
|
||||
public function getFormField() {
|
||||
$field = DropdownField::create($this->Name, $this->EscapedTitle, $this->getOptionsMap());
|
||||
|
||||
// Set default
|
||||
$defaultOption = $this->getDefaultOptions()->first();
|
||||
if($defaultOption) {
|
||||
$field->setValue($defaultOption->EscapedTitle);
|
||||
}
|
||||
$this->doUpdateFormField($field);
|
||||
return $field;
|
||||
}
|
||||
|
||||
public function getSelectorField(EditableCustomRule $rule, $forOnLoad = false) {
|
||||
return "$(\"select[name='{$this->Name}']\")";
|
||||
}
|
||||
}
|
36
code/model/editableformfields/EditableEmailField.php
Executable file
36
code/model/editableformfields/EditableEmailField.php
Executable file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/**
|
||||
* EditableEmailField
|
||||
*
|
||||
* Allow users to define a validating editable email field for a UserDefinedForm
|
||||
*
|
||||
* @package userforms
|
||||
*/
|
||||
|
||||
class EditableEmailField extends EditableFormField {
|
||||
|
||||
private static $singular_name = 'Email Field';
|
||||
|
||||
private static $plural_name = 'Email Fields';
|
||||
|
||||
public function getSetsOwnError() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getFormField() {
|
||||
$field = EmailField::create($this->Name, $this->EscapedTitle, $this->Default);
|
||||
$this->doUpdateFormField($field);
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a formfield with the additional metadata specified by this field
|
||||
*
|
||||
* @param FormField $field
|
||||
*/
|
||||
protected function updateFormField($field) {
|
||||
parent::updateFormField($field);
|
||||
|
||||
$field->setAttribute('data-rule-email', true);
|
||||
}
|
||||
}
|
91
code/model/editableformfields/EditableFieldGroup.php
Normal file
91
code/model/editableformfields/EditableFieldGroup.php
Normal file
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Specifies that this ends a group of fields
|
||||
*/
|
||||
class EditableFieldGroup extends EditableFormField {
|
||||
|
||||
private static $has_one = array(
|
||||
'End' => 'EditableFieldGroupEnd'
|
||||
);
|
||||
|
||||
/**
|
||||
* Disable selection of group class
|
||||
*
|
||||
* @config
|
||||
* @var bool
|
||||
*/
|
||||
private static $hidden = true;
|
||||
|
||||
/**
|
||||
* Non-data field type
|
||||
*
|
||||
* @var type
|
||||
*/
|
||||
private static $literal = true;
|
||||
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
$fields->removeByName(array('MergeField', 'Default', 'Validation', 'DisplayRules'));
|
||||
return $fields;
|
||||
}
|
||||
|
||||
public function getCMSTitle() {
|
||||
$title = $this->getFieldNumber()
|
||||
?: $this->Title
|
||||
?: 'group';
|
||||
|
||||
return _t(
|
||||
'EditableFieldGroupEnd.FIELD_GROUP_START',
|
||||
'Group {group}',
|
||||
array(
|
||||
'group' => $title
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function getInlineClassnameField($column, $fieldClasses) {
|
||||
return new LabelField($column, $this->CMSTitle);
|
||||
}
|
||||
|
||||
public function showInReports() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getFormField() {
|
||||
$field = UserFormsGroupField::create()
|
||||
->setTitle($this->EscapedTitle ?: false);
|
||||
$this->doUpdateFormField($field);
|
||||
return $field;
|
||||
}
|
||||
|
||||
protected function updateFormField($field) {
|
||||
// set the right title on this field
|
||||
if($this->RightTitle) {
|
||||
// Since this field expects raw html, safely escape the user data prior
|
||||
$field->setRightTitle(Convert::raw2xml($this->RightTitle));
|
||||
}
|
||||
|
||||
// if this field has an extra class
|
||||
if($this->ExtraClass) {
|
||||
$field->addExtraClass($this->ExtraClass);
|
||||
}
|
||||
}
|
||||
|
||||
protected function onBeforeDelete() {
|
||||
parent::onBeforeDelete();
|
||||
|
||||
// Ensures EndID is lazy-loaded for onAfterDelete
|
||||
$this->EndID;
|
||||
}
|
||||
|
||||
protected function onAfterDelete() {
|
||||
parent::onAfterDelete();
|
||||
|
||||
// Delete end
|
||||
if(($end = $this->End()) && $end->exists()) {
|
||||
$end->delete();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
94
code/model/editableformfields/EditableFieldGroupEnd.php
Normal file
94
code/model/editableformfields/EditableFieldGroupEnd.php
Normal file
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Specifies that this ends a group of fields
|
||||
*/
|
||||
class EditableFieldGroupEnd extends EditableFormField {
|
||||
|
||||
private static $belongs_to = array(
|
||||
'Group' => 'EditableFieldGroup'
|
||||
);
|
||||
|
||||
/**
|
||||
* Disable selection of group class
|
||||
*
|
||||
* @config
|
||||
* @var bool
|
||||
*/
|
||||
private static $hidden = true;
|
||||
|
||||
/**
|
||||
* Non-data type
|
||||
*
|
||||
* @config
|
||||
* @var bool
|
||||
*/
|
||||
private static $literal = true;
|
||||
|
||||
public function getCMSTitle() {
|
||||
$group = $this->Group();
|
||||
return _t(
|
||||
'EditableFieldGroupEnd.FIELD_GROUP_END',
|
||||
'{group} end',
|
||||
array(
|
||||
'group' => ($group && $group->exists()) ? $group->CMSTitle : 'Group'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
$fields->removeByName(array('MergeField', 'Default', 'Validation', 'DisplayRules'));
|
||||
return $fields;
|
||||
}
|
||||
|
||||
public function getInlineClassnameField($column, $fieldClasses) {
|
||||
return new LabelField($column, $this->CMSTitle);
|
||||
}
|
||||
|
||||
public function getInlineTitleField($column) {
|
||||
return HiddenField::create($column);
|
||||
}
|
||||
|
||||
public function getFormField() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function showInReports() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function onAfterWrite() {
|
||||
parent::onAfterWrite();
|
||||
|
||||
// If this is not attached to a group, find the first group prior to this
|
||||
// with no end attached
|
||||
$group = $this->Group();
|
||||
if(!($group && $group->exists()) && $this->ParentID) {
|
||||
$group = EditableFieldGroup::get()
|
||||
->filter(array(
|
||||
'ParentID' => $this->ParentID,
|
||||
'Sort:LessThanOrEqual' => $this->Sort
|
||||
))
|
||||
->where('"EditableFieldGroup"."EndID" IS NULL OR "EditableFieldGroup"."EndID" = 0')
|
||||
->sort('"Sort" DESC')
|
||||
->first();
|
||||
|
||||
// When a group is found, attach it to this end
|
||||
if($group) {
|
||||
$group->EndID = $this->ID;
|
||||
$group->write();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function onAfterDelete() {
|
||||
parent::onAfterDelete();
|
||||
|
||||
// Delete group
|
||||
if(($group = $this->Group()) && $group->exists()) {
|
||||
$group->delete();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -35,7 +35,7 @@ class EditableFileField extends EditableFormField {
|
||||
}
|
||||
|
||||
public function getFormField() {
|
||||
$field = FileField::create($this->Name, $this->Title);
|
||||
$field = FileField::create($this->Name, $this->EscapedTitle);
|
||||
|
||||
// filter out '' since this would be a regex problem on JS end
|
||||
$field->getValidator()->setAllowedExtensions(
|
||||
@ -49,13 +49,7 @@ class EditableFileField extends EditableFormField {
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->Required) {
|
||||
// Required validation can conflict so add the Required validation messages
|
||||
// as input attributes
|
||||
$errorMessage = $this->getErrorMessage()->HTML();
|
||||
$field->setAttribute('data-rule-required', 'true');
|
||||
$field->setAttribute('data-msg-required', $errorMessage);
|
||||
}
|
||||
$this->doUpdateFormField($field);
|
||||
|
||||
return $field;
|
||||
}
|
@ -1,13 +1,45 @@
|
||||
<?php
|
||||
|
||||
use SilverStripe\Forms\SegmentField;
|
||||
use SilverStripe\Forms\SegmentFieldModifier\IDSegmentFieldModifier;
|
||||
use SilverStripe\Forms\SegmentFieldModifier\SlugSegmentFieldModifier;
|
||||
|
||||
/**
|
||||
* Represents the base class of a editable form field
|
||||
* object like {@link EditableTextField}.
|
||||
* Represents the base class of a editable form field
|
||||
* object like {@link EditableTextField}.
|
||||
*
|
||||
* @package userforms
|
||||
*
|
||||
* @property string Name
|
||||
*
|
||||
* @method DataList DisplayRules() List of EditableCustomRule objects
|
||||
*/
|
||||
class EditableFormField extends DataObject {
|
||||
|
||||
/**
|
||||
* Set to true to hide from class selector
|
||||
*
|
||||
* @config
|
||||
* @var bool
|
||||
*/
|
||||
private static $hidden = false;
|
||||
|
||||
/**
|
||||
* Define this field as abstract (not inherited)
|
||||
*
|
||||
* @config
|
||||
* @var bool
|
||||
*/
|
||||
private static $abstract = true;
|
||||
|
||||
/**
|
||||
* Flag this field type as non-data (e.g. literal, header, html)
|
||||
*
|
||||
* @config
|
||||
* @var bool
|
||||
*/
|
||||
private static $literal = false;
|
||||
|
||||
/**
|
||||
* Default sort order
|
||||
*
|
||||
@ -15,7 +47,7 @@ class EditableFormField extends DataObject {
|
||||
* @var string
|
||||
*/
|
||||
private static $default_sort = '"Sort"';
|
||||
|
||||
|
||||
/**
|
||||
* A list of CSS classes that can be added
|
||||
*
|
||||
@ -87,14 +119,14 @@ class EditableFormField extends DataObject {
|
||||
* Set the visibility of an individual form field
|
||||
*
|
||||
* @param bool
|
||||
*/
|
||||
*/
|
||||
public function setReadonly($readonly = true) {
|
||||
$this->readonly = $readonly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this field is readonly
|
||||
*
|
||||
* Returns whether this field is readonly
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function isReadonly() {
|
||||
@ -130,7 +162,11 @@ class EditableFormField extends DataObject {
|
||||
),
|
||||
TextField::create('Title'),
|
||||
TextField::create('Default', _t('EditableFormField.DEFAULT', 'Default value')),
|
||||
TextField::create('RightTitle', _t('EditableFormField.RIGHTTITLE', 'Right title'))
|
||||
TextField::create('RightTitle', _t('EditableFormField.RIGHTTITLE', 'Right title')),
|
||||
SegmentField::create('Name')->setModifiers(array(
|
||||
UnderscoreSegmentFieldModifier::create()->setDefault('FieldName'),
|
||||
DisambiguationSegmentFieldModifier::create(),
|
||||
))->setPreview($this->Name)
|
||||
)
|
||||
);
|
||||
|
||||
@ -168,21 +204,25 @@ class EditableFormField extends DataObject {
|
||||
}
|
||||
|
||||
// Validation
|
||||
$fields->addFieldsToTab(
|
||||
'Root.Validation',
|
||||
$this->getFieldValidationOptions()
|
||||
);
|
||||
|
||||
$validationFields = $this->getFieldValidationOptions();
|
||||
if($validationFields) {
|
||||
$fields->addFieldsToTab(
|
||||
'Root.Validation',
|
||||
$this->getFieldValidationOptions()
|
||||
);
|
||||
}
|
||||
$allowedClasses = array_keys($this->getEditableFieldClasses(false));
|
||||
$editableColumns = new GridFieldEditableColumns();
|
||||
$editableColumns->setDisplayFields(array(
|
||||
'Display' => '',
|
||||
'ConditionFieldID' => function($record, $column, $grid) {
|
||||
'ConditionFieldID' => function($record, $column, $grid) use ($allowedClasses) {
|
||||
return DropdownField::create(
|
||||
$column,
|
||||
'',
|
||||
EditableFormField::get()
|
||||
->filter(array(
|
||||
'ParentID' => $this->ParentID
|
||||
'ParentID' => $this->ParentID,
|
||||
'ClassName' => $allowedClasses
|
||||
))
|
||||
->exclude(array(
|
||||
'ID' => $this->ID
|
||||
@ -209,8 +249,7 @@ class EditableFormField extends DataObject {
|
||||
new GridFieldButtonRow(),
|
||||
new GridFieldToolbarHeader(),
|
||||
new GridFieldAddNewInlineButton(),
|
||||
new GridFieldDeleteAction(),
|
||||
new GridState_Component()
|
||||
new GridFieldDeleteAction()
|
||||
);
|
||||
|
||||
$fields->addFieldsToTab('Root.DisplayRules', array(
|
||||
@ -238,6 +277,10 @@ class EditableFormField extends DataObject {
|
||||
public function onBeforeWrite() {
|
||||
parent::onBeforeWrite();
|
||||
|
||||
if($this->Name === 'Field') {
|
||||
throw new ValidationException('Field name cannot be "Field"');
|
||||
}
|
||||
|
||||
if(!$this->Sort && $this->ParentID) {
|
||||
$parentID = $this->ParentID;
|
||||
$this->Sort = EditableFormField::get()
|
||||
@ -254,20 +297,20 @@ class EditableFormField extends DataObject {
|
||||
|
||||
// Set a field name.
|
||||
if(!$this->Name) {
|
||||
$this->Name = $this->RecordClassName . $this->ID;
|
||||
$this->Name = get_class($this) . '_' . $this->ID;
|
||||
$this->write();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Flag indicating that this field will set its own error message via data-msg='' attributes
|
||||
*
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getSetsOwnError() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return whether a user can delete this form field
|
||||
* based on whether they can edit the page
|
||||
@ -281,7 +324,7 @@ class EditableFormField extends DataObject {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return whether a user can edit this form field
|
||||
* based on whether they can edit the page
|
||||
@ -295,10 +338,10 @@ class EditableFormField extends DataObject {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Publish this Form Field to the live site
|
||||
*
|
||||
*
|
||||
* Wrapper for the {@link Versioned} publish function
|
||||
*/
|
||||
public function doPublish($fromStage, $toStage, $createNewVersion = false) {
|
||||
@ -309,7 +352,7 @@ class EditableFormField extends DataObject {
|
||||
$rule->doPublish($fromStage, $toStage, $createNewVersion);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete this form from a given stage
|
||||
*
|
||||
@ -323,7 +366,7 @@ class EditableFormField extends DataObject {
|
||||
$rule->deleteFromStage($stage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* checks wether record is new, copied from Sitetree
|
||||
*/
|
||||
@ -348,7 +391,7 @@ class EditableFormField extends DataObject {
|
||||
|
||||
return ($stageVersion && $stageVersion != $liveVersion);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated since version 4.0
|
||||
*/
|
||||
@ -356,7 +399,7 @@ class EditableFormField extends DataObject {
|
||||
Deprecation::notice('4.0', 'getSettings is deprecated');
|
||||
return (!empty($this->CustomSettings)) ? unserialize($this->CustomSettings) : array();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated since version 4.0
|
||||
*/
|
||||
@ -364,7 +407,7 @@ class EditableFormField extends DataObject {
|
||||
Deprecation::notice('4.0', 'setSettings is deprecated');
|
||||
$this->CustomSettings = serialize($settings);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated since version 4.0
|
||||
*/
|
||||
@ -372,13 +415,13 @@ class EditableFormField extends DataObject {
|
||||
Deprecation::notice('4.0', "setSetting({$key}) is deprecated");
|
||||
$settings = $this->getSettings();
|
||||
$settings[$key] = $value;
|
||||
|
||||
|
||||
$this->setSettings($settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the allowed css classes for the extraClass custom setting
|
||||
*
|
||||
*
|
||||
* @param array The permissible CSS classes to add
|
||||
*/
|
||||
public function setAllowedCss(array $allowed) {
|
||||
@ -403,7 +446,7 @@ class EditableFormField extends DataObject {
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the path to the icon for this field type, relative to the site root.
|
||||
*
|
||||
@ -412,7 +455,7 @@ class EditableFormField extends DataObject {
|
||||
public function getIcon() {
|
||||
return USERFORMS_DIR . '/images/' . strtolower($this->class) . '.png';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return whether or not this field has addable options
|
||||
* such as a dropdown field or radio set
|
||||
@ -422,42 +465,67 @@ class EditableFormField extends DataObject {
|
||||
public function getHasAddableOptions() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return whether or not this field needs to show the extra
|
||||
* options dropdown list
|
||||
*
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function showExtraOptions() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Title field of the field in the backend of the page
|
||||
*
|
||||
* @return TextField
|
||||
*/
|
||||
public function TitleField() {
|
||||
$label = _t('EditableFormField.ENTERQUESTION', 'Enter Question');
|
||||
|
||||
$field = new TextField('Title', $label, $this->getField('Title'));
|
||||
$field->setName($this->getFieldName('Title'));
|
||||
|
||||
if(!$this->canEdit()) {
|
||||
return $field->performReadonlyTransformation();
|
||||
}
|
||||
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Title for rendering in the front-end (with XML values escaped)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle() {
|
||||
return Convert::raw2att($this->getField('Title'));
|
||||
public function getEscapedTitle() {
|
||||
return Convert::raw2xml($this->Title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the numeric indicator (1.1.2) that represents it's nesting value
|
||||
*
|
||||
* Only useful for fields attached to a current page, and that contain other fields such as pages
|
||||
* or groups
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFieldNumber() {
|
||||
// Check if exists
|
||||
if(!$this->exists()) {
|
||||
return null;
|
||||
}
|
||||
// Check parent
|
||||
$form = $this->Parent();
|
||||
if(!$form || !$form->exists() || !($fields = $form->Fields())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$prior = 0; // Number of prior group at this level
|
||||
$stack = array(); // Current stack of nested groups, where the top level = the page
|
||||
foreach($fields->map('ID', 'ClassName') as $id => $className) {
|
||||
if($className === 'EditableFormStep') {
|
||||
$priorPage = empty($stack) ? $prior : $stack[0];
|
||||
$stack = array($priorPage + 1);
|
||||
$prior = 0;
|
||||
} elseif($className === 'EditableFieldGroup') {
|
||||
$stack[] = $prior + 1;
|
||||
$prior = 0;
|
||||
} elseif($className === 'EditableFieldGroupEnd') {
|
||||
$prior = array_pop($stack);
|
||||
}
|
||||
if($id == $this->ID) {
|
||||
return implode('.', $stack);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getCMSTitle() {
|
||||
return $this->i18n_singular_name() . ' (' . $this->Title . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -467,44 +535,91 @@ class EditableFormField extends DataObject {
|
||||
Deprecation::notice('4.0', "getFieldName({$field}) is deprecated");
|
||||
return ($field) ? "Fields[".$this->ID."][".$field."]" : "Fields[".$this->ID."]";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated since version 4.0
|
||||
*/
|
||||
public function getSettingName($field) {
|
||||
Deprecation::notice('4.0', "getSettingName({$field}) is deprecated");
|
||||
$name = $this->getFieldName('CustomSettings');
|
||||
|
||||
|
||||
return $name . '[' . $field .']';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Append custom validation fields to the default 'Validation'
|
||||
* Append custom validation fields to the default 'Validation'
|
||||
* section in the editable options view
|
||||
*
|
||||
*
|
||||
* @return FieldList
|
||||
*/
|
||||
public function getFieldValidationOptions() {
|
||||
$fields = new FieldList(
|
||||
CheckboxField::create('Required', _t('EditableFormField.REQUIRED', 'Is this field Required?')),
|
||||
CheckboxField::create('Required', _t('EditableFormField.REQUIRED', 'Is this field Required?'))
|
||||
->setDescription(_t('EditableFormField.REQUIRED_DESCRIPTION', 'Please note that conditional fields can\'t be required')),
|
||||
TextField::create('CustomErrorMessage', _t('EditableFormField.CUSTOMERROR','Custom Error Message'))
|
||||
);
|
||||
|
||||
$this->extend('updateFieldValidationOptions', $fields);
|
||||
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a FormField to appear on the front end. Implement on
|
||||
* your subclass
|
||||
* Return a FormField to appear on the front end. Implement on
|
||||
* your subclass.
|
||||
*
|
||||
* @return FormField
|
||||
*/
|
||||
public function getFormField() {
|
||||
user_error("Please implement a getFormField() on your EditableFormClass ". $this->ClassName, E_USER_ERROR);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates a formfield with extensions
|
||||
*
|
||||
* @param FormField $field
|
||||
*/
|
||||
public function doUpdateFormField($field) {
|
||||
$this->extend('beforeUpdateFormField', $field);
|
||||
$this->updateFormField($field);
|
||||
$this->extend('afterUpdateFormField', $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a formfield with the additional metadata specified by this field
|
||||
*
|
||||
* @param FormField $field
|
||||
*/
|
||||
protected function updateFormField($field) {
|
||||
// set the error / formatting messages
|
||||
$field->setCustomValidationMessage($this->getErrorMessage());
|
||||
|
||||
// set the right title on this field
|
||||
if($this->RightTitle) {
|
||||
// Since this field expects raw html, safely escape the user data prior
|
||||
$field->setRightTitle(Convert::raw2xml($this->RightTitle));
|
||||
}
|
||||
|
||||
// if this field is required add some
|
||||
if($this->Required) {
|
||||
// Required validation can conflict so add the Required validation messages as input attributes
|
||||
$errorMessage = $this->getErrorMessage()->HTML();
|
||||
$field->addExtraClass('requiredField');
|
||||
$field->setAttribute('data-rule-required', 'true');
|
||||
$field->setAttribute('data-msg-required', $errorMessage);
|
||||
|
||||
if($identifier = UserDefinedForm::config()->required_identifier) {
|
||||
$title = $field->Title() . " <span class='required-identifier'>". $identifier . "</span>";
|
||||
$field->setTitle($title);
|
||||
}
|
||||
}
|
||||
|
||||
// if this field has an extra class
|
||||
if($this->ExtraClass) {
|
||||
$field->addExtraClass($this->ExtraClass);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the instance of the submission field class
|
||||
*
|
||||
@ -513,8 +628,8 @@ class EditableFormField extends DataObject {
|
||||
public function getSubmittedFormField() {
|
||||
return new SubmittedFormField();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Show this form field (and its related value) in the reports and in emails.
|
||||
*
|
||||
@ -523,25 +638,7 @@ class EditableFormField extends DataObject {
|
||||
public function showInReports() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the validation information related to this field. This is
|
||||
* interrupted as a JSON object for validate plugin and used in the
|
||||
* PHP.
|
||||
*
|
||||
* @see http://docs.jquery.com/Plugins/Validation/Methods
|
||||
* @return Array
|
||||
*/
|
||||
public function getValidation() {
|
||||
return $this->Required
|
||||
? array('required' => true)
|
||||
: array();
|
||||
}
|
||||
|
||||
public function getValidationJSON() {
|
||||
return Convert::raw2json($this->getValidation());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the error message for this field. Either uses the custom
|
||||
* one (if provided) or the default SilverStripe message
|
||||
@ -551,10 +648,10 @@ class EditableFormField extends DataObject {
|
||||
public function getErrorMessage() {
|
||||
$title = strip_tags("'". ($this->Title ? $this->Title : $this->Name) . "'");
|
||||
$standard = sprintf(_t('Form.FIELDISREQUIRED', '%s is required').'.', $title);
|
||||
|
||||
|
||||
// only use CustomErrorMessage if it has a non empty value
|
||||
$errorMessage = (!empty($this->CustomErrorMessage)) ? $this->CustomErrorMessage : $standard;
|
||||
|
||||
|
||||
return DBField::create_field('Varchar', $errorMessage);
|
||||
}
|
||||
|
||||
@ -575,11 +672,11 @@ class EditableFormField extends DataObject {
|
||||
}
|
||||
|
||||
if(
|
||||
!isset($data[$this->Name]) ||
|
||||
!isset($data[$this->Name]) ||
|
||||
!$data[$this->Name] ||
|
||||
!$formField->validate($form->getValidator())
|
||||
) {
|
||||
$form->addErrorMessage($this->Name, $this->getErrorMessage(), 'bad');
|
||||
$form->addErrorMessage($this->Name, $this->getErrorMessage()->HTML(), 'error', false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -598,7 +695,7 @@ class EditableFormField extends DataObject {
|
||||
$this->ShowOnLoad = $data['ShowOnLoad'] === '' || ($data['ShowOnLoad'] && $data['ShowOnLoad'] !== 'Hide');
|
||||
unset($data['ShowOnLoad']);
|
||||
}
|
||||
|
||||
|
||||
// Migrate all other settings
|
||||
foreach($data as $key => $value) {
|
||||
if($this->hasField($key)) {
|
||||
@ -606,4 +703,89 @@ class EditableFormField extends DataObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the formfield to use when editing this inline in gridfield
|
||||
*
|
||||
* @param string $column name of column
|
||||
* @param array $fieldClasses List of allowed classnames if this formfield has a selectable class
|
||||
* @return FormField
|
||||
*/
|
||||
public function getInlineClassnameField($column, $fieldClasses) {
|
||||
return DropdownField::create($column, false, $fieldClasses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the formfield to use when editing the title inline
|
||||
*
|
||||
* @param string $column
|
||||
* @return FormField
|
||||
*/
|
||||
public function getInlineTitleField($column) {
|
||||
return TextField::create($column, false)
|
||||
->setAttribute('placeholder', _t('EditableFormField.TITLE', 'Title'))
|
||||
->setAttribute('data-placeholder', _t('EditableFormField.TITLE', 'Title'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JS expression for selecting the holder for this field
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSelectorHolder() {
|
||||
return "$(\"#{$this->Name}\")";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the JS expression for selecting the value for this field
|
||||
*
|
||||
* @param EditableCustomRule $rule Custom rule this selector will be used with
|
||||
* @param bool $forOnLoad Set to true if this will be invoked on load
|
||||
*/
|
||||
public function getSelectorField(EditableCustomRule $rule, $forOnLoad = false) {
|
||||
return "$(\"input[name='{$this->Name}']\")";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the list of classes that can be selected and used as data-values
|
||||
*
|
||||
* @param $includeLiterals Set to false to exclude non-data fields
|
||||
* @return array
|
||||
*/
|
||||
public function getEditableFieldClasses($includeLiterals = true) {
|
||||
$classes = ClassInfo::getValidSubClasses('EditableFormField');
|
||||
|
||||
// Remove classes we don't want to display in the dropdown.
|
||||
$editableFieldClasses = array();
|
||||
foreach ($classes as $class) {
|
||||
// Skip abstract / hidden classes
|
||||
if(Config::inst()->get($class, 'abstract', Config::UNINHERITED) || Config::inst()->get($class, 'hidden')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!$includeLiterals && Config::inst()->get($class, 'literal')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$singleton = singleton($class);
|
||||
if(!$singleton->canCreate()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$editableFieldClasses[$class] = $singleton->i18n_singular_name();
|
||||
}
|
||||
|
||||
asort($editableFieldClasses);
|
||||
return $editableFieldClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EditableFormFieldValidator
|
||||
*/
|
||||
public function getCMSValidator() {
|
||||
return EditableFormFieldValidator::create()
|
||||
->setRecord($this);
|
||||
}
|
||||
}
|
61
code/model/editableformfields/EditableFormFieldValidator.php
Normal file
61
code/model/editableformfields/EditableFormFieldValidator.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
class EditableFormFieldValidator extends RequiredFields {
|
||||
|
||||
/**
|
||||
*
|
||||
* @var EditableFormField
|
||||
*/
|
||||
protected $record = null;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param EditableFormField $record
|
||||
* @return $this
|
||||
*/
|
||||
public function setRecord($record) {
|
||||
$this->record = $record;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/*
|
||||
* @return EditableFormField
|
||||
*/
|
||||
public function getRecord() {
|
||||
return $this->record;
|
||||
}
|
||||
|
||||
|
||||
public function php($data) {
|
||||
if(!parent::php($data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip unsaved records
|
||||
if(!$this->record || !$this->record->exists()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip validation if not required
|
||||
if(empty($data['Required'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip validation if no rules
|
||||
$count = EditableCustomRule::get()->filter('ParentID', $this->record->ID)->count();
|
||||
if($count == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Both required = true and rules > 0 should error
|
||||
$this->validationError(
|
||||
'Required_Error',
|
||||
_t(
|
||||
"EditableFormFieldValidator.REQUIRED_ERROR",
|
||||
"Form fields cannot be required and have conditional display rules."
|
||||
),
|
||||
'error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
@ -11,6 +11,8 @@ class EditableFormHeading extends EditableFormField {
|
||||
|
||||
private static $plural_name = 'Headings';
|
||||
|
||||
private static $literal = true;
|
||||
|
||||
private static $db = array(
|
||||
'Level' => 'Int(3)', // From CustomSettings
|
||||
'HideFromReports' => 'Boolean(0)' // from CustomSettings
|
||||
@ -55,10 +57,24 @@ class EditableFormHeading extends EditableFormField {
|
||||
}
|
||||
|
||||
public function getFormField() {
|
||||
$labelField = new HeaderField($this->Name, $this->Title, $this->Level);
|
||||
$labelField = new HeaderField($this->Name, $this->EscapedTitle, $this->Level);
|
||||
$labelField->addExtraClass('FormHeading');
|
||||
$labelField->setAttribute('data-id', $this->Name);
|
||||
$this->doUpdateFormField($labelField);
|
||||
return $labelField;
|
||||
}
|
||||
|
||||
protected function updateFormField($field) {
|
||||
// set the right title on this field
|
||||
if($this->RightTitle) {
|
||||
// Since this field expects raw html, safely escape the user data prior
|
||||
$field->setRightTitle(Convert::raw2xml($this->RightTitle));
|
||||
}
|
||||
// if this field has an extra class
|
||||
if($this->ExtraClass) {
|
||||
$field->addExtraClass($this->ExtraClass);
|
||||
}
|
||||
}
|
||||
|
||||
public function showInReports() {
|
||||
return !$this->HideFromReports;
|
||||
@ -67,4 +83,8 @@ class EditableFormHeading extends EditableFormField {
|
||||
public function getFieldValidationOptions() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getSelectorHolder() {
|
||||
return "$(\":header[data-id='{$this->Name}']\")";
|
||||
}
|
||||
}
|
86
code/model/editableformfields/EditableFormStep.php
Normal file
86
code/model/editableformfields/EditableFormStep.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
/**
|
||||
* A step in multi-page user form
|
||||
*
|
||||
* @package userforms
|
||||
*/
|
||||
class EditableFormStep extends EditableFormField {
|
||||
|
||||
private static $singular_name = 'Page Break';
|
||||
|
||||
private static $plural_name = 'Page Breaks';
|
||||
|
||||
/**
|
||||
* Disable selection of step class
|
||||
*
|
||||
* @config
|
||||
* @var bool
|
||||
*/
|
||||
private static $hidden = true;
|
||||
|
||||
/**
|
||||
* @return FieldList
|
||||
*/
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
$fields->removeByName(array('MergeField', 'Default', 'Validation'));
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FormField
|
||||
*/
|
||||
public function getFormField() {
|
||||
$field = UserFormsStepField::create()
|
||||
->setName($this->Name)
|
||||
->setTitle($this->EscapedTitle);
|
||||
$this->doUpdateFormField($field);
|
||||
return $field;
|
||||
}
|
||||
|
||||
protected function updateFormField($field) {
|
||||
// if this field has an extra class
|
||||
if($this->ExtraClass) {
|
||||
$field->addExtraClass($this->ExtraClass);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function showInReports() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getInlineClassnameField($column, $fieldClasses) {
|
||||
return new LabelField(
|
||||
$column,
|
||||
$this->CMSTitle
|
||||
);
|
||||
}
|
||||
|
||||
public function getCMSTitle() {
|
||||
$title = $this->getFieldNumber()
|
||||
?: $this->Title
|
||||
?: '';
|
||||
|
||||
return _t(
|
||||
'EditableFormStep.STEP_TITLE',
|
||||
'Page {page}',
|
||||
array(
|
||||
'page' => $title
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JS expression for selecting the holder for this field
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSelectorHolder() {
|
||||
return "$(\".step-button-wrapper[data-for='{$this->Name}']\")";
|
||||
}
|
||||
}
|
@ -13,6 +13,14 @@ class EditableLiteralField extends EditableFormField {
|
||||
|
||||
private static $plural_name = 'HTML Blocks';
|
||||
|
||||
/**
|
||||
* Mark as literal only
|
||||
*
|
||||
* @config
|
||||
* @var bool
|
||||
*/
|
||||
private static $literal = true;
|
||||
|
||||
/**
|
||||
* Get the name of the editor config to use for HTML sanitisation. Defaults to the active config.
|
||||
*
|
||||
@ -103,12 +111,16 @@ class EditableLiteralField extends EditableFormField {
|
||||
}
|
||||
|
||||
public function getFormField() {
|
||||
$label = $this->Title
|
||||
? "<label class='left'>".Convert::raw2xml($this->Title)."</label>"
|
||||
: "";
|
||||
$classes = $this->Title ? "" : " nolabel";
|
||||
// Build label and css classes
|
||||
$label = '';
|
||||
$classes = $this->ExtraClass;
|
||||
if(empty($this->Title)) {
|
||||
$classes .= " nolabel";
|
||||
} else {
|
||||
$label = "<label class='left'>{$this->EscapedTitle}</label>";
|
||||
}
|
||||
|
||||
return new LiteralField(
|
||||
$field = new LiteralField(
|
||||
"LiteralField[{$this->ID}]",
|
||||
sprintf(
|
||||
"<div id='%s' class='field text%s'>
|
||||
@ -121,6 +133,9 @@ class EditableLiteralField extends EditableFormField {
|
||||
$this->Content
|
||||
)
|
||||
);
|
||||
|
||||
// When dealing with literal fields there is no further customisation that can be added at this point
|
||||
return $field;
|
||||
}
|
||||
|
||||
public function showInReports() {
|
@ -42,7 +42,9 @@ class EditableMemberListField extends EditableFormField {
|
||||
}
|
||||
|
||||
$members = Member::map_in_groups($this->GroupID);
|
||||
return new DropdownField($this->Name, $this->Title, $members);
|
||||
$field = new DropdownField($this->Name, $this->EscapedTitle, $members);
|
||||
$this->doUpdateFormField($field);
|
||||
return $field;
|
||||
}
|
||||
|
||||
public function getValueFromData($data) {
|
@ -14,6 +14,14 @@
|
||||
*/
|
||||
|
||||
class EditableMultipleOptionField extends EditableFormField {
|
||||
|
||||
/**
|
||||
* Define this field as abstract (not inherited)
|
||||
*
|
||||
* @config
|
||||
* @var bool
|
||||
*/
|
||||
private static $abstract = true;
|
||||
|
||||
private static $has_many = array(
|
||||
"Options" => "EditableOption"
|
||||
@ -48,8 +56,7 @@ class EditableMultipleOptionField extends EditableFormField {
|
||||
$editableColumns,
|
||||
new GridFieldButtonRow(),
|
||||
new GridFieldAddNewInlineButton(),
|
||||
new GridFieldDeleteAction(),
|
||||
new GridState_Component()
|
||||
new GridFieldDeleteAction()
|
||||
);
|
||||
|
||||
$optionsGrid = GridField::create(
|
||||
@ -156,11 +163,25 @@ class EditableMultipleOptionField extends EditableFormField {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the form field for this object in the front end form view
|
||||
* Gets map of field options suitable for use in a form
|
||||
*
|
||||
* @return FormField
|
||||
* @return array
|
||||
*/
|
||||
public function getFormField() {
|
||||
return user_error('Please implement getFormField() on '. $this->class, E_USER_ERROR);
|
||||
protected function getOptionsMap() {
|
||||
$optionSet = $this->Options();
|
||||
$optionMap = $optionSet->map('EscapedTitle', 'Title');
|
||||
if($optionMap instanceof SS_Map) {
|
||||
return $optionMap->toArray();
|
||||
}
|
||||
return $optionMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all default options
|
||||
*
|
||||
* @return SS_List
|
||||
*/
|
||||
protected function getDefaultOptions() {
|
||||
return $this->Options()->filter('Default', 1);
|
||||
}
|
||||
}
|
@ -26,18 +26,9 @@ class EditableNumericField extends EditableFormField {
|
||||
* @return NumericField
|
||||
*/
|
||||
public function getFormField() {
|
||||
$field = new NumericField($this->Name, $this->Title);
|
||||
$field = new NumericField($this->Name, $this->EscapedTitle, $this->Default);
|
||||
$field->addExtraClass('number');
|
||||
$field->setValue($this->Default);
|
||||
|
||||
if ($this->Required) {
|
||||
// Required and numeric validation can conflict so add the
|
||||
// required validation messages as input attributes
|
||||
$errorMessage = $this->getErrorMessage()->HTML();
|
||||
$field->setAttribute('data-rule-required', 'true');
|
||||
$field->setAttribute('data-msg-required', $errorMessage);
|
||||
}
|
||||
|
||||
$this->doUpdateFormField($field);
|
||||
return $field;
|
||||
}
|
||||
|
||||
@ -54,14 +45,20 @@ class EditableNumericField extends EditableFormField {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
public function getValidation() {
|
||||
$options = array();
|
||||
/**
|
||||
* Updates a formfield with the additional metadata specified by this field
|
||||
*
|
||||
* @param FormField $field
|
||||
*/
|
||||
protected function updateFormField($field) {
|
||||
parent::updateFormField($field);
|
||||
|
||||
if($this->MinValue) {
|
||||
$options['min'] = (int)$this->MinValue;
|
||||
$field->setAttribute('data-rule-min', $this->MinValue);
|
||||
}
|
||||
|
||||
if($this->MaxValue) {
|
||||
$options['max'] = (int)$this->MaxValue;
|
||||
$field->setAttribute('data-rule-max', $this->MaxValue);
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
}
|
@ -49,39 +49,6 @@ class EditableOption extends DataObject {
|
||||
return ($this->Parent()->canDelete($member));
|
||||
}
|
||||
|
||||
/**
|
||||
* Template for the editing view of this option field
|
||||
*/
|
||||
public function EditSegment() {
|
||||
return $this->renderWith('EditableOption');
|
||||
}
|
||||
|
||||
/**
|
||||
* The Title Field for this object
|
||||
*
|
||||
* @return FormField
|
||||
*/
|
||||
public function TitleField() {
|
||||
return new TextField("Fields[{$this->ParentID}][{$this->ID}][Title]", null, $this->Title );
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of this field in the form
|
||||
*
|
||||
* @return String
|
||||
*/
|
||||
public function FieldName() {
|
||||
return "Fields[{$this->ParentID}][{$this->ID}]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Make this option readonly
|
||||
*/
|
||||
public function ReadonlyOption() {
|
||||
$this->readonly = true;
|
||||
return $this->EditSegment();
|
||||
}
|
||||
|
||||
public function getEscapedTitle() {
|
||||
return Convert::raw2att($this->Title);
|
||||
}
|
44
code/model/editableformfields/EditableRadioField.php
Executable file
44
code/model/editableformfields/EditableRadioField.php
Executable file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* EditableRadioField
|
||||
*
|
||||
* Represents a set of selectable radio buttons
|
||||
*
|
||||
* @package userforms
|
||||
*/
|
||||
|
||||
class EditableRadioField extends EditableMultipleOptionField {
|
||||
|
||||
private static $singular_name = 'Radio Group';
|
||||
|
||||
private static $plural_name = 'Radio Groups';
|
||||
|
||||
/**
|
||||
* @return FieldList
|
||||
*/
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
$fields->removeByName('Default');
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
public function getFormField() {
|
||||
$field = OptionsetField::create($this->Name, $this->EscapedTitle, $this->getOptionsMap());
|
||||
|
||||
// Set default item
|
||||
$defaultOption = $this->getDefaultOptions()->first();
|
||||
if($defaultOption) {
|
||||
$field->setValue($defaultOption->EscapedTitle);
|
||||
}
|
||||
$this->doUpdateFormField($field);
|
||||
return $field;
|
||||
}
|
||||
|
||||
public function getSelectorField(EditableCustomRule $rule, $forOnLoad = false) {
|
||||
// We only want to trigger on load once for the radio group - hence we focus on the first option only.
|
||||
$first = $forOnLoad ? ':first' : '';
|
||||
return "$(\"input[name='{$this->Name}']{$first}\")";
|
||||
}
|
||||
}
|
@ -65,41 +65,29 @@ class EditableTextField extends EditableFormField {
|
||||
*/
|
||||
public function getFormField() {
|
||||
if($this->Rows > 1) {
|
||||
$field = TextareaField::create($this->Name, $this->Title);
|
||||
$field = TextareaField::create($this->Name, $this->EscapedTitle, $this->Default);
|
||||
$field->setRows($this->Rows);
|
||||
} else {
|
||||
$field = TextField::create($this->Name, $this->Title, null, $this->MaxLength);
|
||||
$field = TextField::create($this->Name, $this->EscapedTitle, $this->Default, $this->MaxLength);
|
||||
}
|
||||
|
||||
if ($this->Required) {
|
||||
// Required validation can conflict so add the Required validation messages
|
||||
// as input attributes
|
||||
$errorMessage = $this->getErrorMessage()->HTML();
|
||||
$field->setAttribute('data-rule-required', 'true');
|
||||
$field->setAttribute('data-msg-required', $errorMessage);
|
||||
}
|
||||
|
||||
$field->setValue($this->Default);
|
||||
|
||||
$this->doUpdateFormField($field);
|
||||
return $field;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the validation information related to this field. This is
|
||||
* interrupted as a JSON object for validate plugin and used in the
|
||||
* PHP.
|
||||
* Updates a formfield with the additional metadata specified by this field
|
||||
*
|
||||
* @see http://docs.jquery.com/Plugins/Validation/Methods
|
||||
* @return array
|
||||
* @param FormField $field
|
||||
*/
|
||||
public function getValidation() {
|
||||
$options = parent::getValidation();
|
||||
protected function updateFormField($field) {
|
||||
parent::updateFormField($field);
|
||||
|
||||
if($this->MinLength) {
|
||||
$options['minlength'] = (int)$this->MinLength;
|
||||
}
|
||||
if($this->MaxLength) {
|
||||
$options['maxlength'] = (int)$this->MaxLength;
|
||||
$field->setAttribute('data-rule-minlength', $this->MinLength);
|
||||
}
|
||||
|
||||
if($this->MaxLength) {
|
||||
$field->setAttribute('data-rule-maxlength', $this->MaxLength);
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
}
|
@ -32,16 +32,8 @@ class EditableCheckbox extends EditableFormField {
|
||||
}
|
||||
|
||||
public function getFormField() {
|
||||
$field = CheckboxField::create($this->Name, $this->Title, $this->CheckedDefault);
|
||||
|
||||
if ($this->Required) {
|
||||
// Required validation can conflict so add the Required validation messages
|
||||
// as input attributes
|
||||
$errorMessage = $this->getErrorMessage()->HTML();
|
||||
$field->setAttribute('data-rule-required', 'true');
|
||||
$field->setAttribute('data-msg-required', $errorMessage);
|
||||
}
|
||||
|
||||
$field = CheckboxField::create($this->Name, $this->EscapedTitle, $this->CheckedDefault);
|
||||
$this->doUpdateFormField($field);
|
||||
return $field;
|
||||
}
|
||||
|
||||
|
@ -1,57 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* EditableDropdown
|
||||
*
|
||||
* Represents a modifiable dropdown (select) box on a form
|
||||
*
|
||||
* @package userforms
|
||||
*/
|
||||
|
||||
class EditableDropdown extends EditableMultipleOptionField {
|
||||
|
||||
private static $singular_name = 'Dropdown Field';
|
||||
|
||||
private static $plural_name = 'Dropdowns';
|
||||
|
||||
/**
|
||||
* @return FieldList
|
||||
*/
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
$fields->removeByName('Default');
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DropdownField
|
||||
*/
|
||||
public function getFormField() {
|
||||
$optionSet = $this->Options();
|
||||
$defaultOptions = $optionSet->filter('Default', 1);
|
||||
$options = array();
|
||||
|
||||
if($optionSet) {
|
||||
foreach($optionSet as $option) {
|
||||
$options[$option->Title] = $option->Title;
|
||||
}
|
||||
}
|
||||
|
||||
$field = DropdownField::create($this->Name, $this->Title, $options);
|
||||
|
||||
if ($this->Required) {
|
||||
// Required validation can conflict so add the Required validation messages
|
||||
// as input attributes
|
||||
$errorMessage = $this->getErrorMessage()->HTML();
|
||||
$field->setAttribute('data-rule-required', 'true');
|
||||
$field->setAttribute('data-msg-required', $errorMessage);
|
||||
}
|
||||
|
||||
if($defaultOptions->count()) {
|
||||
$field->setValue($defaultOptions->First()->EscapedTitle);
|
||||
}
|
||||
|
||||
return $field;
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* EditableEmailField
|
||||
*
|
||||
* Allow users to define a validating editable email field for a UserDefinedForm
|
||||
*
|
||||
* @package userforms
|
||||
*/
|
||||
|
||||
class EditableEmailField extends EditableFormField {
|
||||
|
||||
private static $singular_name = 'Email Field';
|
||||
|
||||
private static $plural_name = 'Email Fields';
|
||||
|
||||
public function getSetsOwnError() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getFormField() {
|
||||
|
||||
$field = EmailField::create($this->Name, $this->Title);
|
||||
|
||||
if ($this->Required) {
|
||||
// Required and Email validation can conflict so add the Required validation messages
|
||||
// as input attributes
|
||||
$errorMessage = $this->getErrorMessage()->HTML();
|
||||
$field->setAttribute('data-rule-required', 'true');
|
||||
$field->setAttribute('data-msg-required', $errorMessage);
|
||||
}
|
||||
|
||||
$field->setValue($this->Default);
|
||||
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the validation information related to this field. This is
|
||||
* interrupted as a JSON object for validate plugin and used in the
|
||||
* PHP.
|
||||
*
|
||||
* @see http://docs.jquery.com/Plugins/Validation/Methods
|
||||
* @return Array
|
||||
*/
|
||||
public function getValidation() {
|
||||
return array_merge(parent::getValidation(), array(
|
||||
'email' => true
|
||||
));
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* EditableRadioField
|
||||
*
|
||||
* Represents a set of selectable radio buttons
|
||||
*
|
||||
* @package userforms
|
||||
*/
|
||||
|
||||
class EditableRadioField extends EditableMultipleOptionField {
|
||||
|
||||
private static $singular_name = 'Radio field';
|
||||
|
||||
private static $plural_name = 'Radio fields';
|
||||
|
||||
/**
|
||||
* @return FieldList
|
||||
*/
|
||||
public function getCMSFields() {
|
||||
$fields = parent::getCMSFields();
|
||||
|
||||
$fields->removeByName('Default');
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
public function getFormField() {
|
||||
$optionSet = $this->Options();
|
||||
$defaultOptions = $optionSet->filter('Default', 1);
|
||||
$options = array();
|
||||
|
||||
if($optionSet) {
|
||||
foreach($optionSet as $option) {
|
||||
$options[$option->EscapedTitle] = $option->Title;
|
||||
}
|
||||
}
|
||||
|
||||
$field = OptionsetField::create($this->Name, $this->Title, $options);
|
||||
|
||||
if($defaultOptions->count()) {
|
||||
$field->setValue($defaultOptions->First()->EscapedTitle);
|
||||
}
|
||||
|
||||
return $field;
|
||||
}
|
||||
}
|
@ -88,7 +88,6 @@ class UserDefinedForm_EmailRecipient extends DataObject {
|
||||
new GridFieldButtonRow('before'),
|
||||
new GridFieldToolbarHeader(),
|
||||
new GridFieldAddNewInlineButton(),
|
||||
new GridState_Component(),
|
||||
new GridFieldDeleteAction(),
|
||||
$columns = new GridFieldEditableColumns()
|
||||
);
|
||||
|
54
code/modifiers/DisambiguationSegmentFieldModifier.php
Normal file
54
code/modifiers/DisambiguationSegmentFieldModifier.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
use SilverStripe\Forms\SegmentFieldModifier\AbstractSegmentFieldModifier;
|
||||
|
||||
class DisambiguationSegmentFieldModifier extends AbstractSegmentFieldModifier {
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPreview($value) {
|
||||
if($this->form instanceof Form && $record = $this->form->getRecord()) {
|
||||
$parent = $record->Parent();
|
||||
|
||||
$try = $value;
|
||||
|
||||
$sibling = EditableformField::get()
|
||||
->filter('ParentID', $parent->ID)
|
||||
->filter('Name', $try)
|
||||
->where('"ID" != ' . $record->ID)
|
||||
->first();
|
||||
|
||||
$counter = 1;
|
||||
|
||||
while($sibling !== null) {
|
||||
$try = $value . '_' . $counter++;
|
||||
|
||||
$sibling = EditableformField::get()
|
||||
->filter('ParentID', $parent->ID)
|
||||
->filter('Name', $try)
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($try !== $value) {
|
||||
return $try;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSuggestion($value) {
|
||||
return $this->getPreview($value);
|
||||
}
|
||||
}
|
27
code/modifiers/UnderscoreSegmentFieldModifier.php
Normal file
27
code/modifiers/UnderscoreSegmentFieldModifier.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use SilverStripe\Forms\SegmentFieldModifier\SlugSegmentFieldModifier;
|
||||
|
||||
class UnderscoreSegmentFieldModifier extends SlugSegmentFieldModifier {
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPreview($value) {
|
||||
return str_replace('-', '_', parent::getPreview($value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSuggestion($value) {
|
||||
return str_replace('-', '_', parent::getSuggestion($value));
|
||||
}
|
||||
}
|
@ -11,7 +11,8 @@
|
||||
"require": {
|
||||
"silverstripe/framework": ">=3.1.0",
|
||||
"silverstripe/cms": ">=3.1.0",
|
||||
"silverstripe-australia/gridfieldextensions": "1.1.0"
|
||||
"silverstripe-australia/gridfieldextensions": "1.1.0",
|
||||
"silverstripe/segment-field": "^1.0"
|
||||
},
|
||||
"suggest": {
|
||||
"colymba/gridfield-bulk-editing-tools": "Allows for bulk management of form submissions"
|
||||
|
12
config.rb
Normal file
12
config.rb
Normal file
@ -0,0 +1,12 @@
|
||||
require 'compass/import-once/activate'
|
||||
# Require any additional compass plugins here.
|
||||
|
||||
# Set this to the root of your project when deployed:
|
||||
http_path = "/"
|
||||
css_dir = "css"
|
||||
sass_dir = "scss"
|
||||
images_dir = "images"
|
||||
javascripts_dir = "javascript"
|
||||
|
||||
line_comments = false
|
||||
|
57
css/UserForm.css
Normal file
57
css/UserForm.css
Normal file
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Lightweight base styles for the front-end form.
|
||||
*/
|
||||
.userform-progress .progress {
|
||||
position: relative;
|
||||
height: 1em;
|
||||
background: #eee;
|
||||
}
|
||||
.userform-progress .progress-bar {
|
||||
position: absolute;
|
||||
height: 1em;
|
||||
background: #666;
|
||||
}
|
||||
.userform-progress .step-buttons {
|
||||
margin-left: 0;
|
||||
position: relative;
|
||||
}
|
||||
.userform-progress .step-button-wrapper {
|
||||
display: inline-block;
|
||||
list-style-type: none;
|
||||
}
|
||||
.userform-progress .step-button-wrapper.viewed .step-button-jump {
|
||||
opacity: 1;
|
||||
}
|
||||
.userform-progress .step-button-jump {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.step-navigation .step-buttons {
|
||||
margin-left: 0;
|
||||
}
|
||||
.step-navigation .step-button-wrapper {
|
||||
display: inline-block;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.userform {
|
||||
clear: both;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.userformsgroup {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.userformsgroup legend {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
border: 0;
|
||||
width: auto;
|
||||
}
|
68
css/UserForm_cms.css
Normal file
68
css/UserForm_cms.css
Normal file
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Styles for cms
|
||||
*/
|
||||
.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item, .cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item:hover {
|
||||
background: white;
|
||||
}
|
||||
.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.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: bold;
|
||||
color: black;
|
||||
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='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'] td {
|
||||
border-top: 1px solid #a8d7f5;
|
||||
}
|
||||
.cms .uf-field-editor table.ss-gridfield-table .ss-gridfield-item[data-class='EditableFieldGroup'] label {
|
||||
font-weight: bold;
|
||||
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;
|
||||
}
|
BIN
docs/en/_images/add-field.png
Normal file
BIN
docs/en/_images/add-field.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
Before Width: | Height: | Size: 76 KiB |
BIN
docs/en/_images/fieldgroups.png
Normal file
BIN
docs/en/_images/fieldgroups.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
BIN
docs/en/_images/multi-page-forms.png
Normal file
BIN
docs/en/_images/multi-page-forms.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
@ -24,9 +24,9 @@ and without getting involved in any PHP code.
|
||||
|
||||
## Documentation
|
||||
|
||||
* [Installation instructions](installation.md)
|
||||
* [Troubleshooting](troubleshooting.md)
|
||||
* [User Documentation](user-documentation.md)
|
||||
* [Installation instructions](installation)
|
||||
* [Troubleshooting](troubleshooting)
|
||||
* [User Documentation](user-documentation)
|
||||
|
||||
## Thanks
|
||||
|
||||
|
@ -52,8 +52,7 @@ puts the form at the end of all the content.
|
||||
|
||||
## Adding fields
|
||||
|
||||
To add a field to the form, click on the "Form" tab under the "Content" tab in the
|
||||
Editing Pane. Click the "Add" button then select the type of field you want from the dropdown.
|
||||
To add a field to the form, click on the "Form Fields" in the Editing Pane. Click the "Add Field" button then select the type of field you want from the dropdown.
|
||||
Save or publish the form to start editing your new field's properties.
|
||||
|
||||
![Adding fields](_images/add-fields.png)
|
||||
@ -174,6 +173,18 @@ to determine the size and the number of rows in a text field.
|
||||
|
||||
* Use [HTML Block](#html-block), with the appropriate level [Heading](#heading).
|
||||
|
||||
## Creating a multi-page form
|
||||
|
||||
To create a multi-page form, simply click the 'Add Page Break' button.
|
||||
This will create a page break field which is used to create a new page in the form. You can drag and drop this page break to separate form fields at the desired location.
|
||||
|
||||
![Multi-page forms](_images/multi-page-forms.png)
|
||||
|
||||
## Adding fieldgroups
|
||||
|
||||
Creating fieldgroups is as simple as clicking the 'Add fieldgroup' button, this create two 'markers' which act as the beginning/end for the fieldgroup. Any fields place between these two markers will be automatically included within the fieldgroup.
|
||||
|
||||
![Adding fieldgroups](_images/fieldgroups.png)
|
||||
|
||||
## Viewing form submissions
|
||||
|
||||
|
71
javascript/FieldEditor.js
Normal file
71
javascript/FieldEditor.js
Normal file
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* form builder behaviour.
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
$.entwine('ss', function($) {
|
||||
$(".uf-field-editor tbody").entwine({
|
||||
onmatch: function() {
|
||||
var i, thisLevel, depth = 0;
|
||||
this._super();
|
||||
|
||||
// Loop through all rows and set necessary styles
|
||||
this.find('.ss-gridfield-item').each(function() {
|
||||
switch($(this).data('class')) {
|
||||
case 'EditableFormStep': {
|
||||
depth = 0;
|
||||
return;
|
||||
}
|
||||
case 'EditableFieldGroup': {
|
||||
thisLevel = ++depth;
|
||||
break;
|
||||
}
|
||||
case 'EditableFieldGroupEnd': {
|
||||
thisLevel = depth--;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
thisLevel = depth;
|
||||
}
|
||||
}
|
||||
|
||||
$(this).toggleClass('inFieldGroup', thisLevel > 0);
|
||||
for(i = 1; i <= 5; i++) {
|
||||
$(this).toggleClass('inFieldGroup-level-'+i, thisLevel >= i);
|
||||
}
|
||||
});
|
||||
},
|
||||
onunmatch: function () {
|
||||
this._super();
|
||||
}
|
||||
});
|
||||
|
||||
// When new fields are added..
|
||||
$('.uf-field-editor .ss-gridfield-buttonrow .action').entwine({
|
||||
onclick: function (e) {
|
||||
this._super(e);
|
||||
|
||||
this.trigger('addnewinline');
|
||||
}
|
||||
});
|
||||
|
||||
$('.uf-field-editor').entwine({
|
||||
onmatch: function () {
|
||||
var self = this;
|
||||
|
||||
// When the 'Add field' button is clicked set a one time listener.
|
||||
// When the GridField is reloaded focus on the newly added field.
|
||||
this.on('addnewinline', function () {
|
||||
self.one('reload', function () {
|
||||
//If fieldgroup, focus on the start marker
|
||||
if ($('.uf-field-editor .ss-gridfield-item').last().attr('data-class') === 'EditableFieldGroupEnd') {
|
||||
$('.uf-field-editor .ss-gridfield-item').last().prev().find('.col-Title input').focus();
|
||||
} else {
|
||||
$('.uf-field-editor .ss-gridfield-item:last-child .col-Title input').focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}(jQuery));
|
700
javascript/UserForm.js
Normal file
700
javascript/UserForm.js
Normal file
@ -0,0 +1,700 @@
|
||||
/**
|
||||
* @file Manages the multi-step navigation.
|
||||
*/
|
||||
jQuery(function ($) {
|
||||
|
||||
// A reference to the UserForm instance.
|
||||
var userform = null;
|
||||
|
||||
// Settings that come from the CMS.
|
||||
var CONSTANTS = {};
|
||||
|
||||
// Common functions that extend multiple classes.
|
||||
var commonMixin = {
|
||||
/**
|
||||
* @func show
|
||||
* @desc Show the form step. Looks after aria attributes too.
|
||||
*/
|
||||
show: function () {
|
||||
this.$el.attr('aria-hidden', false).show();
|
||||
},
|
||||
/**
|
||||
* @func hide
|
||||
* @desc Hide the form step. Looks after aria attributes too.
|
||||
*/
|
||||
hide: function () {
|
||||
this.$el.attr('aria-hidden', true).hide();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @func UserForm
|
||||
* @constructor
|
||||
* @param {object} element
|
||||
* @return {object} - The UserForm instance.
|
||||
* @desc The form
|
||||
*/
|
||||
function UserForm(element) {
|
||||
var self = this;
|
||||
|
||||
this.$el = element instanceof jQuery ? element : $(element);
|
||||
this.steps = [];
|
||||
|
||||
// Add an error container which displays a list of invalid steps on form submission.
|
||||
this.errorContainer = new ErrorContainer(this.$el.children('.error-container'));
|
||||
|
||||
// Listen for events triggered my form steps.
|
||||
this.$el.on('userform.action.prev', function (e) {
|
||||
self.prevStep();
|
||||
});
|
||||
this.$el.on('userform.action.next', function (e) {
|
||||
self.nextStep();
|
||||
});
|
||||
|
||||
// Listen for events triggered by the progress bar.
|
||||
$('#userform-progress').on('userform.progress.changestep', function (e, stepNumber) {
|
||||
self.jumpToStep(stepNumber - 1);
|
||||
});
|
||||
|
||||
// When a field becomes valid, remove errors from the error container.
|
||||
this.$el.on('userform.form.valid', function (e, fieldId) {
|
||||
self.errorContainer.removeStepLink(fieldId);
|
||||
});
|
||||
|
||||
this.$el.validate(this.validationOptions);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/*
|
||||
* Default options for step validation. These get extended in main().
|
||||
*/
|
||||
UserForm.prototype.validationOptions = {
|
||||
ignore: ':hidden',
|
||||
errorClass: 'error',
|
||||
errorElement: 'span',
|
||||
errorPlacement: function (error, element) {
|
||||
error.addClass('message');
|
||||
|
||||
if(element.is(':radio') || element.parents('.checkboxset').length > 0) {
|
||||
error.insertAfter(element.closest('ul'));
|
||||
} else {
|
||||
error.insertAfter(element);
|
||||
}
|
||||
},
|
||||
// Callback for handling the actual submit when the form is valid.
|
||||
// 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.
|
||||
submitHandler: function (form, e) {
|
||||
var isValid = true;
|
||||
|
||||
// validate the current step
|
||||
if(userform.currentStep) {
|
||||
userform.currentStep.valid = $(form).valid();
|
||||
}
|
||||
|
||||
// Check for invalid previous steps.
|
||||
$.each(userform.steps, function (i, step) {
|
||||
if (!step.valid && !step.conditionallyHidden()) {
|
||||
isValid = false;
|
||||
userform.errorContainer.addStepLink(step);
|
||||
}
|
||||
});
|
||||
|
||||
if (isValid) {
|
||||
form.submit();
|
||||
} else {
|
||||
userform.errorContainer.show();
|
||||
}
|
||||
},
|
||||
// When a field becomes valid.
|
||||
success: function (error) {
|
||||
var errorId = $(error).attr('id');
|
||||
|
||||
error.remove();
|
||||
|
||||
// Pass the field's ID with the event.
|
||||
userform.$el.trigger('userform.form.valid', [errorId.substr(0, errorId.indexOf('-error'))]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @func UserForm.addStep
|
||||
* @param {object} step - An instance of FormStep.
|
||||
* @desc Adds a step to the UserForm.
|
||||
*/
|
||||
UserForm.prototype.addStep = function (step) {
|
||||
// Make sure we're dealing with a form step.
|
||||
if (!step instanceof FormStep) {
|
||||
return;
|
||||
}
|
||||
|
||||
step.id = this.steps.length;
|
||||
|
||||
this.steps.push(step);
|
||||
};
|
||||
|
||||
/**
|
||||
* @func UserForm.setCurrentStep
|
||||
* @param {object} step - An instance of FormStep.
|
||||
* @desc Sets the step the user is currently on.
|
||||
*/
|
||||
UserForm.prototype.setCurrentStep = function (step) {
|
||||
// Make sure we're dealing with a form step.
|
||||
if (!(step instanceof FormStep)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentStep = step;
|
||||
this.currentStep.show();
|
||||
|
||||
// Record the user has viewed the step.
|
||||
step.viewed = true;
|
||||
step.$el.addClass('viewed');
|
||||
};
|
||||
|
||||
/**
|
||||
* @func UserForm.jumpToStep
|
||||
* @param {number} stepNumber
|
||||
* @param {boolean} [direction] - Defaults to forward (true).
|
||||
* @desc Jumps to a specific form step.
|
||||
*/
|
||||
UserForm.prototype.jumpToStep = function (stepNumber, direction) {
|
||||
var targetStep = this.steps[stepNumber],
|
||||
isValid = false,
|
||||
forward = direction === void 0 ? true : direction;
|
||||
|
||||
// Make sure the target step exists.
|
||||
if (targetStep === void 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the step we're trying to set as current is not
|
||||
// hidden by custom display rules. If it is then jump to the next step.
|
||||
if (targetStep.conditionallyHidden()) {
|
||||
if (forward) {
|
||||
this.jumpToStep(stepNumber + 1);
|
||||
} else {
|
||||
this.jumpToStep(stepNumber - 1);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate the form.
|
||||
// This well effectivly validate the current step and not the entire form.
|
||||
// This is because hidden fields are excluded from validation, and all fields
|
||||
// on all other steps, are currently hidden.
|
||||
isValid = this.$el.valid();
|
||||
|
||||
// Set the 'valid' property on the current step.
|
||||
this.currentStep.valid = isValid;
|
||||
|
||||
// Users can navigate to step's they've already viewed even if the current step is invalid.
|
||||
if (isValid === false && targetStep.viewed === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentStep.hide();
|
||||
this.setCurrentStep(targetStep);
|
||||
|
||||
this.$el.trigger('userform.form.changestep', [targetStep.id]);
|
||||
};
|
||||
|
||||
/**
|
||||
* @func UserForm.nextStep
|
||||
* @desc Advances the form to the next step.
|
||||
*/
|
||||
UserForm.prototype.nextStep = function () {
|
||||
this.jumpToStep(this.steps.indexOf(this.currentStep) + 1, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* @func UserForm.prevStep
|
||||
* @desc Goes back one step (not bound to browser history).
|
||||
*/
|
||||
UserForm.prototype.prevStep = function () {
|
||||
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).text(), 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;
|
||||
|
||||
// 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.text(), 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.find('.progress-title').text($newStepElement.data('title'));
|
||||
|
||||
// Update the width of the progress bar.
|
||||
this.$el.find('.progress-bar').width(stepID / (this.$buttons.length - 1) * 100 + '%');
|
||||
};
|
||||
|
||||
/**
|
||||
* @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);
|
||||
|
||||
// Bind the step navigation event listeners.
|
||||
this.$el.find('.step-button-prev').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
self.$el.trigger('userform.action.prev');
|
||||
});
|
||||
this.$el.find('.step-button-next').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.id,
|
||||
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
|
||||
* @desc Bootstraps the front-end.
|
||||
*/
|
||||
function main() {
|
||||
var progressBar = null,
|
||||
formActions = null,
|
||||
$userform = $('.userform');
|
||||
|
||||
CONSTANTS.ENABLE_LIVE_VALIDATION = $userform.data('livevalidation') !== void 0;
|
||||
CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP = $userform.data('toperrors') !== void 0;
|
||||
CONSTANTS.HIDE_FIELD_LABELS = $userform.data('hidefieldlabels') !== void 0;
|
||||
|
||||
// Extend the default validation options with conditional options
|
||||
// that are set by the user in the CMS.
|
||||
if (CONSTANTS.ENABLE_LIVE_VALIDATION === false) {
|
||||
$.extend(UserForm.prototype.validationOptions, {
|
||||
onfocusout: false
|
||||
});
|
||||
}
|
||||
|
||||
if (CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP) {
|
||||
$.extend(UserForm.prototype.validationOptions, {
|
||||
// Callback for custom code when an invalid form / step is submitted.
|
||||
invalidHandler: function (event, validator) {
|
||||
$userform.trigger('userform.form.error', [validator]);
|
||||
},
|
||||
onfocusout: false
|
||||
});
|
||||
}
|
||||
|
||||
// Display all the things that are hidden when JavaScript is disabled.
|
||||
$('.userform-progress, .step-navigation').attr('aria-hidden', false).show();
|
||||
|
||||
// Extend classes with common functionality.
|
||||
$.extend(FormStep.prototype, commonMixin);
|
||||
$.extend(ErrorContainer.prototype, commonMixin);
|
||||
|
||||
userform = new UserForm($userform);
|
||||
|
||||
// Conditionally hide field labels and use HTML5 placeholder instead.
|
||||
if (CONSTANTS.HIDE_FIELD_LABELS) {
|
||||
$userform.find('label.left').each(function () {
|
||||
var $label = $(this);
|
||||
|
||||
$('[name="' + $label.attr('for') + '"]').attr('placeholder', $label.text());
|
||||
$label.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Initialise the form steps.
|
||||
userform.$el.find('.form-step').each(function (i, element) {
|
||||
var step = new FormStep(element);
|
||||
|
||||
userform.addStep(step);
|
||||
});
|
||||
|
||||
userform.setCurrentStep(userform.steps[0]);
|
||||
|
||||
// Initialise actions and progressbar
|
||||
progressBar = new ProgressBar($('#userform-progress'));
|
||||
formActions = new FormActions($('#step-navigation'));
|
||||
|
||||
// Hide the form-wide actions on multi-step forms.
|
||||
// Because JavaScript is enabled we'll use the actions contained
|
||||
// in the final step's navigation.
|
||||
if (userform.steps.length > 1) {
|
||||
userform.$el.children('.Actions').attr('aria-hidden', true).hide();
|
||||
}
|
||||
|
||||
// Enable jQuery UI datepickers
|
||||
$(document).on('click', 'input.text[data-showcalendar]', function() {
|
||||
var $element = $(this);
|
||||
|
||||
$element.ssDatepicker();
|
||||
|
||||
if($element.data('datepicker')) {
|
||||
$element.datepicker('show');
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure the form doesn't expire on the user. Pings every 3 mins.
|
||||
setInterval(function () {
|
||||
$.ajax({ url: 'UserDefinedForm_Controller/ping' });
|
||||
}, 180 * 1000);
|
||||
}
|
||||
|
||||
main();
|
||||
});
|
@ -1,9 +0,0 @@
|
||||
jQuery(function($) {
|
||||
/**
|
||||
* Make sure the form does not expire on the user.
|
||||
*/
|
||||
setInterval(function() {
|
||||
// Ping every 3 mins.
|
||||
$.ajax({url: "UserDefinedForm_Controller/ping"});
|
||||
}, 180*1000);
|
||||
});
|
@ -100,8 +100,8 @@ en:
|
||||
PLURALNAME: 'Editable Options'
|
||||
SINGULARNAME: 'Editable Option'
|
||||
EditableRadioField:
|
||||
PLURALNAME: 'Radio fields'
|
||||
SINGULARNAME: 'Radio field'
|
||||
PLURALNAME: 'Radio Groups'
|
||||
SINGULARNAME: 'Radio Group'
|
||||
EditableTextField:
|
||||
NUMBERROWS: 'Number of rows'
|
||||
PLURALNAME: 'Text Fields'
|
||||
|
69
scss/UserForm.scss
Normal file
69
scss/UserForm.scss
Normal file
@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Lightweight base styles for the front-end form.
|
||||
*/
|
||||
|
||||
.userform-progress {
|
||||
.progress {
|
||||
position: relative;
|
||||
height: 1em;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: absolute;
|
||||
height: 1em;
|
||||
background: #666;
|
||||
}
|
||||
|
||||
.step-buttons {
|
||||
margin-left: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.step-button-wrapper {
|
||||
display: inline-block;
|
||||
list-style-type: none;
|
||||
|
||||
&.viewed .step-button-jump {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.step-button-jump {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
|
||||
.step-navigation {
|
||||
.step-buttons {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.step-button-wrapper {
|
||||
display: inline-block;
|
||||
list-style-type: none;
|
||||
}
|
||||
}
|
||||
|
||||
.userform {
|
||||
clear: both;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.userformsgroup {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
legend {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
border: 0;
|
||||
width: auto;
|
||||
}
|
||||
}
|
113
scss/UserForm_cms.scss
Normal file
113
scss/UserForm_cms.scss
Normal file
@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Styles for cms
|
||||
*/
|
||||
|
||||
.cms {
|
||||
.uf-field-editor {
|
||||
|
||||
// Row styles
|
||||
table.ss-gridfield-table {
|
||||
// Standard rows
|
||||
.ss-gridfield-item {
|
||||
&, &:hover {
|
||||
background: white;
|
||||
}
|
||||
|
||||
td {
|
||||
border-right-width: 0;
|
||||
border-top: 1px solid #EEE;
|
||||
|
||||
&:last-child {
|
||||
border-right-width: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ss-gridfield-item.inFieldGroup {
|
||||
&, &:hover {
|
||||
background: #f2f9fd;
|
||||
}
|
||||
|
||||
td {
|
||||
border-bottom: 0;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.col-reorder, .handle {
|
||||
background: #BEE0F8;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
&.inFieldGroup-level-2 {
|
||||
.col-reorder, .handle {
|
||||
background: #99CEF4;
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.inFieldGroup-level-3 {
|
||||
.col-reorder, .handle {
|
||||
background: #89BEF4;
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ss-gridfield-item[data-class='EditableFormStep'] {
|
||||
&, &:hover {
|
||||
background: #dae2e7;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
td {
|
||||
border-top: 1px solid #a6b6c1;
|
||||
}
|
||||
|
||||
+ .ss-gridfield-item td {
|
||||
border-top: 1px solid #dae2e7;
|
||||
}
|
||||
|
||||
+ .ss-gridfield-item[data-class='EditableFieldGroup'] td {
|
||||
border-top: 1px solid #a8d7f5;
|
||||
}
|
||||
}
|
||||
|
||||
.ss-gridfield-item[data-class='EditableFieldGroup'] {
|
||||
td {
|
||||
border-top: 1px solid #a8d7f5;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
color: #444;
|
||||
}
|
||||
}
|
||||
|
||||
.ss-gridfield-item[data-class='EditableFieldGroupEnd'] {
|
||||
td {
|
||||
border-bottom: 1px solid #a8d7f5;
|
||||
}
|
||||
|
||||
+ .ss-gridfield-item[data-class='EditableFieldGroupEnd'] {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.col-buttons .action{
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
19
templates/Includes/UserFormProgress.ss
Normal file
19
templates/Includes/UserFormProgress.ss
Normal file
@ -0,0 +1,19 @@
|
||||
<% if $Steps.Count > 1 %>
|
||||
<div id="userform-progress" class="userform-progress" aria-hidden="true" style="display:none;">
|
||||
<h2 class="progress-title"></h2>
|
||||
<p>Step <span class="current-step-number">1</span> of <span class="total-step-number">$Steps.Count</span></p>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="1" aria-valuemin="1" aria-valuemax="$Steps.Count"></div>
|
||||
</div>
|
||||
<nav>
|
||||
<ul class="step-buttons">
|
||||
<% loop $Steps %>
|
||||
<li class="step-button-wrapper<% if $First %> current<% end_if %>" data-for="$Name">
|
||||
<%-- Remove js-align class to remove javascript positioning --%>
|
||||
<button class="step-button-jump js-align" disabled="disabled">$Pos</button>
|
||||
</li>
|
||||
<% end_loop %>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<% end_if %>
|
8
templates/Includes/UserFormStepErrors.ss
Normal file
8
templates/Includes/UserFormStepErrors.ss
Normal file
@ -0,0 +1,8 @@
|
||||
<% if $Steps.Count > 1 %>
|
||||
<fieldset class="error-container form-wide-errors" aria-hidden="true" style="display: none;">
|
||||
<div>
|
||||
<h4></h4>
|
||||
<ul class="error-list"></ul>
|
||||
</div>
|
||||
</fieldset>
|
||||
<% end_if %>
|
17
templates/Includes/UserFormStepNav.ss
Normal file
17
templates/Includes/UserFormStepNav.ss
Normal file
@ -0,0 +1,17 @@
|
||||
<nav id="step-navigation" class="step-navigation" aria-hidden="true" style="display:none;">
|
||||
<ul class="step-buttons">
|
||||
<li class="step-button-wrapper">
|
||||
<button class="step-button-prev">Prev</button>
|
||||
</li>
|
||||
<li class="step-button-wrapper">
|
||||
<button class="step-button-next">Next</button>
|
||||
</li>
|
||||
<% if $Actions %>
|
||||
<li class="step-button-wrapper Actions">
|
||||
<% loop $Actions %>
|
||||
$Field
|
||||
<% end_loop %>
|
||||
</li>
|
||||
<% end_if %>
|
||||
</ul>
|
||||
</nav>
|
44
templates/UserForm.ss
Normal file
44
templates/UserForm.ss
Normal file
@ -0,0 +1,44 @@
|
||||
<form $AttributesHTML>
|
||||
|
||||
<% include UserFormProgress %>
|
||||
<% include UserFormStepErrors %>
|
||||
|
||||
<% if $Message %>
|
||||
<p id="{$FormName}_error" class="message $MessageType">$Message</p>
|
||||
<% else %>
|
||||
<p id="{$FormName}_error" class="message $MessageType" aria-hidden="true" style="display: none;"></p>
|
||||
<% end_if %>
|
||||
|
||||
<fieldset>
|
||||
<% if $Legend %><legend>$Legend</legend><% end_if %>
|
||||
<% loop $Fields %>
|
||||
$FieldHolder
|
||||
<% end_loop %>
|
||||
<div class="clear"><!-- --></div>
|
||||
</fieldset>
|
||||
|
||||
<%--
|
||||
Include step navigation if it's a multi-page form.
|
||||
The markup inside this include is hidden by default and displayed if JavaScript is enabled.
|
||||
--%>
|
||||
|
||||
<% if $Steps.Count > 1 %>
|
||||
<% include UserFormStepNav %>
|
||||
<% end_if %>
|
||||
|
||||
<%--
|
||||
When JavaScript is disabled, multi-page forms are diaplayed as a single page form,
|
||||
and these actions are used instead of the step navigation include.
|
||||
|
||||
These actions are hidden by JavaScript on multi-page forms.
|
||||
--%>
|
||||
|
||||
<% if $Actions %>
|
||||
<div class="Actions">
|
||||
<% loop $Actions %>
|
||||
$Field
|
||||
<% end_loop %>
|
||||
</div>
|
||||
<% end_if %>
|
||||
|
||||
</form>
|
15
templates/UserFormsStepField.ss
Normal file
15
templates/UserFormsStepField.ss
Normal file
@ -0,0 +1,15 @@
|
||||
<fieldset id="$Name" class="form-step $extraClass" data-title="$Title">
|
||||
<% if $Form.DisplayErrorMessagesAtTop %>
|
||||
<fieldset class="error-container" aria-hidden="true" style="display: none;">
|
||||
<div>
|
||||
<h4></h4>
|
||||
<ul class="error-list"></ul>
|
||||
</div>
|
||||
</fieldset>
|
||||
<% end_if %>
|
||||
|
||||
<% loop $Children %>
|
||||
$FieldHolder
|
||||
<% end_loop %>
|
||||
|
||||
</fieldset>
|
@ -1,157 +0,0 @@
|
||||
(function($) {
|
||||
$(document).ready(function() {
|
||||
var formId = "{$Form.FormName.JS}",
|
||||
errorContainerId = "{$ErrorContainerID.JS}",
|
||||
errorContainer = $('<fieldset><div><h4></h4><ul></ul></div></fieldset>');
|
||||
|
||||
var messages = {<% loop $Fields %><% if $ErrorMessage && not $SetsOwnError %><% if $ClassName == 'EditableCheckboxGroupField' %>
|
||||
'{$Name.JS}[]': '{$ErrorMessage.JS}'<% if not Last %>,<% end_if %><% else %>
|
||||
'{$Name.JS}': '{$ErrorMessage.JS}'<% if not Last %>,<% end_if %><% end_if %><% end_if %><% end_loop %>
|
||||
};
|
||||
|
||||
$(document).on("click", "input.text[data-showcalendar]", function() {
|
||||
$(this).ssDatepicker();
|
||||
|
||||
if($(this).data('datepicker')) {
|
||||
$(this).datepicker('show');
|
||||
}
|
||||
});
|
||||
|
||||
$("#" + formId).validate({
|
||||
ignore: ':hidden',
|
||||
errorClass: "required",
|
||||
errorElement: "span",
|
||||
errorPlacement: function(error, element) {
|
||||
error.addClass('message');
|
||||
|
||||
if(element.is(":radio") || element.parents(".checkboxset").length > 0) {
|
||||
error.insertAfter(element.closest("ul"));
|
||||
} else {
|
||||
error.insertAfter(element);
|
||||
}
|
||||
|
||||
<% if $DisplayErrorMessagesAtTop %>
|
||||
applyTopErrorMessage(element, error.html());
|
||||
<% end_if %>
|
||||
},
|
||||
success: function (error) {
|
||||
error.remove();
|
||||
},
|
||||
messages: messages,
|
||||
rules: {
|
||||
<% loop $Fields %>
|
||||
<% if $Validation %><% if ClassName == EditableCheckboxGroupField %>
|
||||
'{$Name.JS}[]': {$ValidationJSON.RAW},
|
||||
<% else %>
|
||||
'{$Name.JS}': {$ValidationJSON.RAW},
|
||||
<% end_if %><% end_if %>
|
||||
<% end_loop %>
|
||||
}
|
||||
|
||||
/*
|
||||
* Conditional options.
|
||||
* Using leading commas so we don't get a trailing comma on
|
||||
* the last option. Trailing commas can break IE.
|
||||
*/
|
||||
<% if $EnableLiveValidation %>
|
||||
// Enable live validation
|
||||
,onfocusout: function (element) { this.element(element); }
|
||||
<% end_if %>
|
||||
|
||||
<% if $DisplayErrorMessagesAtTop %>
|
||||
,invalidHandler: function (event, validator) {
|
||||
var errorList = $('#' + errorContainerId + ' ul');
|
||||
|
||||
// Update the error list with errors from the validator.
|
||||
// We do this because top messages are not part of the regular
|
||||
// error message life cycle, which jquery.validate handles for us.
|
||||
errorList.empty();
|
||||
|
||||
$.each(validator.errorList, function () {
|
||||
applyTopErrorMessage($(this.element), this.message);
|
||||
});
|
||||
}
|
||||
,onfocusout: false
|
||||
<% end_if %>
|
||||
});
|
||||
|
||||
<% if $HideFieldLabels %>
|
||||
// Hide field labels (use HTML5 placeholder instead)
|
||||
$("#" + formId + "label.left").each(function() {
|
||||
$("#"+$(this).attr("for"))
|
||||
.attr("placeholder", $(this).text());
|
||||
$(this).remove();
|
||||
});
|
||||
Placeholders.init();
|
||||
<% end_if %>
|
||||
|
||||
<% if $DisplayErrorMessagesAtTop %>
|
||||
/**
|
||||
* @applyTopErrorMessage
|
||||
* @param {jQuery} input - The jQuery input object which contains the field to validate
|
||||
* @param {string} message - The error message to display (html escaped)
|
||||
* @desc Update an error message (displayed at the top of the form).
|
||||
*/
|
||||
function applyTopErrorMessage(input, message) {
|
||||
var inputID = input.attr('id'),
|
||||
anchor = '#' + inputID,
|
||||
elementID = inputID + '-top-error',
|
||||
errorContainer = $('#' + errorContainerId),
|
||||
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');
|
||||
errorContainer.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);
|
||||
errorContainer
|
||||
.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 );
|
||||
}
|
||||
}
|
||||
|
||||
// Build container
|
||||
errorContainer
|
||||
.hide()
|
||||
.attr('id', errorContainerId)
|
||||
.find('h4')
|
||||
.text(ss.i18n._t(
|
||||
"UserForms.ERROR_CONTAINER_HEADER",
|
||||
"Please correct the following errors and try again:"
|
||||
));
|
||||
$('#' + formId).prepend(errorContainer);
|
||||
<% end_if %>
|
||||
});
|
||||
})(jQuery);
|
@ -80,27 +80,6 @@ class EditableFormFieldTest extends FunctionalTest {
|
||||
$this->assertEquals(array('Option 5' => 'Option 5', 'Option 6' => 'Option 6'), $values);
|
||||
}
|
||||
|
||||
function testTitleField() {
|
||||
$text = $this->objFromFixture('EditableTextField', 'basic-text');
|
||||
$this->logInWithPermission('ADMIN');
|
||||
|
||||
$title = $text->TitleField();
|
||||
|
||||
$this->assertThat($title, $this->isInstanceOf('TextField'));
|
||||
$this->assertEquals($title->Title(), "Enter Question");
|
||||
$this->assertEquals($title->Value(), "Basic Text Field");
|
||||
|
||||
$member = Member::currentUser();
|
||||
$member->logOut();
|
||||
|
||||
// read only version
|
||||
$title = $text->TitleField();
|
||||
|
||||
$this->assertThat($title, $this->isInstanceOf('ReadonlyField'));
|
||||
$this->assertEquals($title->Title(), "Enter Question");
|
||||
$this->assertEquals($title->Value(), "Basic Text Field");
|
||||
}
|
||||
|
||||
function testMultipleOptionDuplication() {
|
||||
$dropdown = $this->objFromFixture('EditableDropdown','basic-dropdown');
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
class UserDefinedFormControllerTest extends FunctionalTest {
|
||||
|
||||
static $fixture_file = 'userforms/tests/UserDefinedFormTest.yml';
|
||||
static $fixture_file = 'UserDefinedFormTest.yml';
|
||||
|
||||
function testProcess() {
|
||||
$form = $this->setupFormFrontend();
|
||||
@ -15,10 +15,13 @@ class UserDefinedFormControllerTest extends FunctionalTest {
|
||||
|
||||
$this->autoFollowRedirection = false;
|
||||
$this->clearEmails();
|
||||
|
||||
|
||||
// load the form
|
||||
$this->get($form->URLSegment);
|
||||
$response = $this->submitForm('UserForm_Form', null, array('basic-text-name' => 'Basic Value'));
|
||||
$this->get($form->URLSegment);
|
||||
|
||||
$field = $this->objFromFixture('EditableTextField', 'basic-text');
|
||||
|
||||
$response = $this->submitForm('UserForm_Form', null, array($field->Name => 'Basic Value'));
|
||||
|
||||
// should have a submitted form field now
|
||||
$submitted = DataObject::get('SubmittedFormField', "\"Name\" = 'basic-text-name'");
|
||||
@ -83,7 +86,7 @@ class UserDefinedFormControllerTest extends FunctionalTest {
|
||||
|
||||
function testForm() {
|
||||
$form = $this->objFromFixture('UserDefinedForm', 'basic-form-page');
|
||||
|
||||
|
||||
$controller = new UserDefinedFormControllerTest_Controller($form);
|
||||
|
||||
// test form
|
||||
@ -106,30 +109,35 @@ class UserDefinedFormControllerTest extends FunctionalTest {
|
||||
|
||||
$controller = new UserDefinedFormControllerTest_Controller($form);
|
||||
|
||||
$fields = $controller->Form()->getFormFields();
|
||||
|
||||
$this->assertEquals($fields->Count(), 1);
|
||||
$formSteps = $controller->Form()->getFormFields();
|
||||
$firstStep = $formSteps->first();
|
||||
|
||||
$this->assertEquals($formSteps->Count(), 1);
|
||||
$this->assertEquals($firstStep->getChildren()->Count(), 1);
|
||||
|
||||
// custom error message on a form field
|
||||
$requiredForm = $this->objFromFixture('UserDefinedForm', 'validation-form');
|
||||
$controller = new UserDefinedFormControllerTest_Controller($requiredForm);
|
||||
|
||||
UserDefinedForm::config()->required_identifier = "*";
|
||||
|
||||
$fields = $controller->Form()->getFormFields();
|
||||
|
||||
$this->assertEquals($fields->First()->getCustomValidationMessage()->getValue(), 'Custom Error Message');
|
||||
$this->assertEquals($fields->First()->Title(), 'Required Text Field <span class=\'required-identifier\'>*</span>');
|
||||
|
||||
$formSteps = $controller->Form()->getFormFields();
|
||||
$firstStep = $formSteps->first();
|
||||
$firstField = $firstStep->getChildren()->first();
|
||||
|
||||
$this->assertEquals('Custom Error Message', $firstField->getCustomValidationMessage()->getValue());
|
||||
$this->assertEquals($firstField->Title(), 'Required Text Field <span class=\'required-identifier\'>*</span>');
|
||||
|
||||
// test custom right title
|
||||
$field = $form->Fields()->First();
|
||||
$field = $form->Fields()->limit(1, 1)->First();
|
||||
$field->RightTitle = 'Right Title';
|
||||
$field->write();
|
||||
|
||||
$controller = new UserDefinedFormControllerTest_Controller($form);
|
||||
$fields = $controller->Form()->getFormFields();
|
||||
$formSteps = $controller->Form()->getFormFields();
|
||||
$firstStep = $formSteps->first();
|
||||
|
||||
$this->assertEquals($fields->First()->RightTitle(), "Right Title");
|
||||
$this->assertEquals($firstStep->getChildren()->First()->RightTitle(), "Right Title");
|
||||
|
||||
// test empty form
|
||||
$emptyForm = $this->objFromFixture('UserDefinedForm', 'empty-form');
|
||||
|
@ -3,13 +3,11 @@
|
||||
/**
|
||||
* @package userforms
|
||||
*/
|
||||
|
||||
class UserDefinedFormTest extends FunctionalTest {
|
||||
|
||||
static $fixture_file = 'UserDefinedFormTest.yml';
|
||||
|
||||
|
||||
function testRollbackToVersion() {
|
||||
public function testRollbackToVersion() {
|
||||
$this->markTestSkipped(
|
||||
'UserDefinedForm::rollback() has not been implemented completely'
|
||||
);
|
||||
@ -37,7 +35,7 @@ class UserDefinedFormTest extends FunctionalTest {
|
||||
$this->assertEquals($orignal->SubmitButtonText, 'Button Text');
|
||||
}
|
||||
|
||||
function testGetCMSFields() {
|
||||
public function testGetCMSFields() {
|
||||
$this->logInWithPermission('ADMIN');
|
||||
$form = $this->objFromFixture('UserDefinedForm', 'basic-form-page');
|
||||
|
||||
@ -49,7 +47,7 @@ class UserDefinedFormTest extends FunctionalTest {
|
||||
$this->assertTrue($fields->dataFieldByName('OnCompleteMessage') != null);
|
||||
}
|
||||
|
||||
function testEmailRecipientPopup() {
|
||||
public function testEmailRecipientPopup() {
|
||||
$this->logInWithPermission('ADMIN');
|
||||
|
||||
$form = $this->objFromFixture('UserDefinedForm', 'basic-form-page');
|
||||
@ -160,13 +158,13 @@ class UserDefinedFormTest extends FunctionalTest {
|
||||
$live = Versioned::get_one_by_stage("UserDefinedForm", "Live", "\"UserDefinedForm_Live\".\"ID\" = $form->ID");
|
||||
|
||||
$this->assertNotNull($live);
|
||||
$this->assertEquals($live->Fields()->Count(), 1);
|
||||
$this->assertEquals(2, $live->Fields()->Count()); // one page and one field
|
||||
|
||||
$dropdown = $this->objFromFixture('EditableDropdown', 'basic-dropdown');
|
||||
$form->Fields()->add($dropdown);
|
||||
|
||||
$stage = Versioned::get_one_by_stage("UserDefinedForm", "Stage", "\"UserDefinedForm\".\"ID\" = $form->ID");
|
||||
$this->assertEquals($stage->Fields()->Count(), 2);
|
||||
$this->assertEquals(3, $stage->Fields()->Count());
|
||||
|
||||
// should not have published the dropdown
|
||||
$liveDropdown = Versioned::get_one_by_stage("EditableFormField", "Live", "\"EditableFormField_Live\".\"ID\" = $dropdown->ID");
|
||||
@ -176,11 +174,10 @@ class UserDefinedFormTest extends FunctionalTest {
|
||||
$form->doPublish();
|
||||
|
||||
$live = Versioned::get_one_by_stage("UserDefinedForm", "Live", "\"UserDefinedForm_Live\".\"ID\" = $form->ID");
|
||||
$this->assertEquals($live->Fields()->Count(), 2);
|
||||
$this->assertEquals(3, $live->Fields()->Count());
|
||||
|
||||
// edit the title
|
||||
$text = $form->Fields()->First();
|
||||
|
||||
$text = $form->Fields()->limit(1, 1)->First();
|
||||
$text->Title = 'Edited title';
|
||||
$text->write();
|
||||
|
||||
@ -197,21 +194,20 @@ class UserDefinedFormTest extends FunctionalTest {
|
||||
$this->logInWithPermission('ADMIN');
|
||||
$form = $this->objFromFixture('UserDefinedForm', 'basic-form-page');
|
||||
$form->write();
|
||||
|
||||
$this->assertEquals(0, DB::query("SELECT COUNT(*) FROM \"EditableFormField_Live\"")->value());
|
||||
$form->doPublish();
|
||||
|
||||
// assert that it exists and has a field
|
||||
$live = Versioned::get_one_by_stage("UserDefinedForm", "Live", "\"UserDefinedForm_Live\".\"ID\" = $form->ID");
|
||||
|
||||
$this->assertTrue(isset($live));
|
||||
$this->assertEquals(DB::query("SELECT COUNT(*) FROM \"EditableFormField_Live\"")->value(), 1);
|
||||
$this->assertEquals(2, DB::query("SELECT COUNT(*) FROM \"EditableFormField_Live\"")->value());
|
||||
|
||||
// unpublish
|
||||
$form->doUnpublish();
|
||||
|
||||
$this->assertNull(Versioned::get_one_by_stage("UserDefinedForm", "Live", "\"UserDefinedForm_Live\".\"ID\" = $form->ID"));
|
||||
$this->assertEquals(DB::query("SELECT COUNT(*) FROM \"EditableFormField_Live\"")->value(), 0);
|
||||
|
||||
$this->assertEquals(0, DB::query("SELECT COUNT(*) FROM \"EditableFormField_Live\"")->value());
|
||||
}
|
||||
|
||||
function testDoRevertToLive() {
|
||||
|
@ -1,3 +1,19 @@
|
||||
EditableFormStep:
|
||||
form1step1:
|
||||
Title: 'Step 1'
|
||||
form3step1:
|
||||
Title: 'Step 1'
|
||||
form4step1:
|
||||
Title: 'Step 1'
|
||||
form5step1:
|
||||
Title: 'Step 1'
|
||||
form6step1:
|
||||
Title: 'Step 1'
|
||||
form6step2:
|
||||
Title: 'Step 2'
|
||||
form6step3:
|
||||
Title: 'Step 2'
|
||||
|
||||
EditableOption:
|
||||
option-1:
|
||||
Name: Option1
|
||||
@ -74,6 +90,12 @@ EditableTextField:
|
||||
CustomErrorMessage: Custom Error Message
|
||||
Required: true
|
||||
|
||||
field-1:
|
||||
Name: Field1
|
||||
|
||||
field-2:
|
||||
Name: Field2
|
||||
|
||||
EditableDropdown:
|
||||
basic-dropdown:
|
||||
Name: basic-dropdown
|
||||
@ -183,7 +205,7 @@ UserDefinedForm_EmailRecipient:
|
||||
UserDefinedForm:
|
||||
basic-form-page:
|
||||
Title: User Defined Form
|
||||
Fields: =>EditableTextField.basic-text
|
||||
Fields: =>EditableFormStep.form1step1,=>EditableTextField.basic-text
|
||||
EmailRecipients: =>UserDefinedForm_EmailRecipient.recipient-1, =>UserDefinedForm_EmailRecipient.no-html, =>UserDefinedForm_EmailRecipient.no-data
|
||||
|
||||
form-with-reset-and-custom-action:
|
||||
@ -193,15 +215,19 @@ UserDefinedForm:
|
||||
|
||||
validation-form:
|
||||
Title: Validation Form
|
||||
Fields: =>EditableTextField.required-text
|
||||
Fields: =>EditableFormStep.form3step1,=>EditableTextField.required-text
|
||||
|
||||
custom-rules-form:
|
||||
Title: Custom Rules Form
|
||||
Fields: =>EditableCheckbox.checkbox-2, =>EditableTextField.basic-text-2
|
||||
Fields: =>EditableFormStep.form4step1,=>EditableCheckbox.checkbox-2, =>EditableTextField.basic-text-2
|
||||
empty-form:
|
||||
Title: Empty Form
|
||||
|
||||
filtered-form-page:
|
||||
Title: 'Page with filtered recipients'
|
||||
Fields: =>EditableCheckboxGroupField.checkbox-group, =>EditableTextField.your-name-field, =>EditableTextField.street-field, =>EditableTextField.city-field
|
||||
Fields: =>EditableFormStep.form5step1,=>EditableCheckboxGroupField.checkbox-group, =>EditableTextField.your-name-field, =>EditableTextField.street-field, =>EditableTextField.city-field
|
||||
EmailRecipients: =>UserDefinedForm_EmailRecipient.unfiltered-recipient-1, =>UserDefinedForm_EmailRecipient.filtered-recipient-1, =>UserDefinedForm_EmailRecipient.filtered-recipient-2
|
||||
|
||||
empty-page:
|
||||
Title: 'Page with empty step'
|
||||
Fields: =>EditableFormStep.form6step1, =>EditableTextField.field-1, =>EditableFormStep.form6step2, =>EditableTextField.field-2, =>EditableFormStep.form6step3
|
||||
|
18
tests/UserFormTest.php
Normal file
18
tests/UserFormTest.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
|
||||
class UserFormTest extends SapphireTest {
|
||||
|
||||
protected static $fixture_file = 'UserDefinedFormTest.yml';
|
||||
|
||||
/**
|
||||
* Tests that a form will not generate empty pages
|
||||
*/
|
||||
public function testEmptyPages() {
|
||||
$page = $this->objFromFixture('UserDefinedForm', 'empty-page');
|
||||
$this->assertEquals(5, $page->Fields()->count());
|
||||
$controller = ModelAsController::controller_for($page);
|
||||
$form = new UserForm($controller);
|
||||
$this->assertEquals(2, $form->getSteps()->count());
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user