diff --git a/code/extensions/UserFormFieldEditorExtension.php b/code/extensions/UserFormFieldEditorExtension.php index db0bac9..7526216 100644 --- a/code/extensions/UserFormFieldEditorExtension.php +++ b/code/extensions/UserFormFieldEditorExtension.php @@ -34,12 +34,16 @@ class UserFormFieldEditorExtension extends DataExtension { public function getFieldEditorGrid() { $fields = $this->owner->Fields(); - $this->createInitialFormStep(); + $this->createInitialFormStep(true); $editableColumns = new GridFieldEditableColumns(); $editableColumns->setDisplayFields(array( 'ClassName' => function($record, $column, $grid) { - return DropdownField::create($column, '', $this->getEditableFieldClasses()); + if($record instanceof EditableFormStep) { + return new LabelField($column, "Page Break"); + } else { + return DropdownField::create($column, '', $this->getEditableFieldClasses()); + } }, 'Title' => function($record, $column, $grid) { return TextField::create($column, ' ') @@ -47,22 +51,28 @@ class UserFormFieldEditorExtension extends DataExtension { } )); + $config = GridFieldConfig::create() + ->addComponents( + $editableColumns, + new GridFieldButtonRow(), + $addField = new GridFieldAddNewInlineButton(), + $addStep = new GridFieldAddItemInlineButton('EditableFormStep'), + new GridFieldEditButton(), + new GridFieldDeleteAction(), + new GridFieldToolbarHeader(), + new GridFieldOrderableRows('Sort'), + new GridState_Component(), + new GridFieldDetailForm() + ); + $addField->setTitle('Add Field'); + $addStep->setTitle('Add Page Break'); + $addStep->setExtraClass('uf-gridfield-steprow'); + $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 ); return $fieldEditor; @@ -72,24 +82,41 @@ class UserFormFieldEditorExtension extends DataExtension { * 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 createInitialFormStep() { - // If there's already an initial step, do nothing. - if ($this->owner->Fields()->filter('ClassName', 'EditableFormStep')->Count()) { + public function createInitialFormStep($force = false) { + // Only invoke once saved + if(!$this->owner->exists()) { return; } - $step = EditableFormStep::create(); + // Check if first field is a step + $fields = $this->owner->Fields(); + $firstField = $fields->first(); + if($firstField instanceof EditableFormStep) { + return; + } - $step->ParentID = $this->owner->ID; - $step->write(); + // 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; + } - // Assign each field to the initial step. - foreach ($this->owner->Fields()->exclude('ID', $step->ID) as $field) { - $field->StepID = $step->ID; + // 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 Step'); + $step->Sort = 1; + $step->write(); + $fields->add($step); } /** @@ -119,6 +146,13 @@ class UserFormFieldEditorExtension extends DataExtension { return $editableFieldClasses; } + /** + * Ensure that at least one page exists at the start + */ + public function onAfterWrite() { + $this->createInitialFormStep(); + } + /** * @see SiteTree::doPublish * @param Page $original diff --git a/code/forms/UserForm.php b/code/forms/UserForm.php index 7787f12..8cf5644 100644 --- a/code/forms/UserForm.php +++ b/code/forms/UserForm.php @@ -65,82 +65,42 @@ class UserForm extends Form { return $steps; } - /** - * Get the form steps. - * - * @return ArrayList - */ - public function getFormSteps() { - $steps = new ArrayList(); - - foreach ($this->controller->Fields() as $field) { - if ($field instanceof EditableFormStep) { - $steps->push($field->getFormField()); - continue; - } - - if(empty($steps->last())) { - trigger_error('Missing first step in form', E_USER_WARNING); - $steps->push(CompositeField::create()); - } - - $steps->last()->push($field->getFormField()); - } - - return $steps; - } - /** * 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(); + $emptyStep = null; // Last empty step, which may or may not later have children - 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)); + foreach ($this->controller->Fields() as $field) { + // When we encounter a step, save it + if ($field instanceof EditableFormStep) { + $emptyStep = $field->getFormField(); + continue; } - // if this field is required add some - if($editableField->Required) { - $field->addExtraClass('requiredField'); - - if($identifier = UserDefinedForm::config()->required_identifier) { - - $title = $field->Title() ." ". $identifier . ""; - $field->setTitle($title); - } - } - // if this field has an extra class - if($extraClass = $editableField->ExtraClass) { - $field->addExtraClass(Convert::raw2att($extraClass)); + // Ensure that the last field is a step + if($emptyStep) { + // When we reach the first non-step field, any empty step will no longer be empty + $fields->push($emptyStep); + $emptyStep = null; + + } elseif(! $fields->last()) { + // If no steps have been saved yet, warn + trigger_error('Missing first step in form', E_USER_WARNING); + $fields->push(singleton('EditableFormStep')->getFormField()); } - // 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->last()->push($field->getFormField()); } $this->extend('updateFormFields', $fields); - return $fields; } diff --git a/code/forms/gridfield/GridFieldAddItemInlineButton.php b/code/forms/gridfield/GridFieldAddItemInlineButton.php new file mode 100644 index 0000000..b3b196a --- /dev/null +++ b/code/forms/gridfield/GridFieldAddItemInlineButton.php @@ -0,0 +1,246 @@ +setClass($class); + $this->setFragment($fragment); + $this->setTitle(_t('GridFieldExtensions.ADD', 'Add')); + } + + /** + * Gets the fragment name this button is rendered into. + * + * @return string + */ + public function getFragment() { + return $this->fragment; + } + + /** + * Sets the fragment name this button is rendered into. + * + * @param string $fragment + * @return GridFieldAddNewInlineButton $this + */ + public function setFragment($fragment) { + $this->fragment = $fragment; + return $this; + } + + /** + * Gets the button title text. + * + * @return string + */ + public function getTitle() { + return $this->title; + } + + /** + * Sets the button title text. + * + * @param string $title + * @return GridFieldAddNewInlineButton $this + */ + public function setTitle($title) { + $this->title = $title; + return $this; + } + + public function getHTMLFragments($grid) { + if($grid->getList() && !singleton($grid->getModelClass())->canCreate()) { + return array(); + } + + $fragment = $this->getFragment(); + + if(!$editable = $grid->getConfig()->getComponentByType('GridFieldEditableColumns')) { + throw new Exception('Inline adding requires the editable columns component'); + } + + Requirements::javascript(THIRDPARTY_DIR . '/javascript-templates/tmpl.js'); + GridFieldExtensions::include_requirements(); + Requirements::javascript(USERFORMS_DIR . '/javascript/GridFieldAddItemInlineButton.js'); + + $data = new ArrayData(array( + 'Title' => $this->getTitle(), + 'TemplateName' => $this->getRowTemplateName() + )); + + return array( + $fragment => $data->renderWith(__CLASS__), + 'after' => $this->getItemRowTemplate($grid, $editable) + ); + } + + /** + * Because getRowTemplate is private + * + * @param GridField $grid + * @param GridFieldEditableColumns $editable + * @return type + */ + protected function getItemRowTemplate(GridField $grid, GridFieldEditableColumns $editable) { + $columns = new ArrayList(); + $handled = array_keys($editable->getDisplayFields($grid)); + + $record = Object::create($this->getClass()); + + $fields = $editable->getFields($grid, $record); + + foreach($grid->getColumns() as $column) { + $content = null; + if(in_array($column, $handled)) { + $field = $fields->dataFieldByName($column); + if($field) { + $field->setName(sprintf( + '%s[%s][%s][{%%=o.num%%}][%s]', $grid->getName(), __CLASS__, $this->getClass(), $field->getName() + )); + } else { + $field = $fields->fieldByName($column); + } + if($field) { + $content = $field->Field(); + } + } + + $attrs = ''; + + foreach($grid->getColumnAttributes($record, $column) as $attr => $val) { + $attrs .= sprintf(' %s="%s"', $attr, Convert::raw2att($val)); + } + + $columns->push(new ArrayData(array( + 'Content' => $content, + 'Attributes' => $attrs, + 'IsActions' => $column == 'Actions' + ))); + } + + $data = new ArrayData(array( + 'Columns' => $columns, + 'ExtraClass' => $this->getExtraClass(), + 'RecordClass' => $this->getClass(), + 'TemplateName' => $this->getRowTemplateName() + )); + return $data->renderWith(__CLASS__ . '_Row'); + } + + public function handleSave(GridField $grid, DataObjectInterface $record) { + $list = $grid->getList(); + $value = $grid->Value(); + $class = $this->getClass(); + + if(!isset($value[__CLASS__][$class]) || !is_array($value[__CLASS__][$class])) { + return; + } + + $editable = $grid->getConfig()->getComponentByType('GridFieldEditableColumns'); + $form = $editable->getForm($grid, $record); + + if(!singleton($class)->canCreate()) { + return; + } + + // Process records matching the specified class + foreach($value[__CLASS__][$class] as $fields) { + $item = $class::create(); + $extra = array(); + + $form->loadDataFrom($fields, Form::MERGE_CLEAR_MISSING); + $form->saveInto($item); + + if($list instanceof ManyManyList) { + $extra = array_intersect_key($form->getData(), (array) $list->getExtraFields()); + } + + $item->write(); + $list->add($item, $extra); + } + } + + /** + * Get the class of the object to create + * + * @return string + */ + public function getClass() { + return $this->modelClass; + } + + /** + * Specify the class to create + * + * @param string $class + */ + public function setClass($class) { + $this->modelClass = $class; + } + + /** + * Get extra CSS classes for this row + * + * @return type + */ + public function getExtraClass() { + return $this->extraClass; + } + + /** + * Sets extra CSS classes for this row + * + * @param string $extraClass + */ + public function setExtraClass($extraClass) { + $this->extraClass = $extraClass; + } + + /** + * Get name of item template + * + * @return string + */ + public function getRowTemplateName() { + return 'ss-gridfield-add-inline-template-' . $this->getClass(); + } +} diff --git a/code/model/UserDefinedForm.php b/code/model/UserDefinedForm.php index 9e3e2d1..69e0feb 100755 --- a/code/model/UserDefinedForm.php +++ b/code/model/UserDefinedForm.php @@ -98,6 +98,7 @@ class UserDefinedForm extends Page { * @return FieldList */ public function getCMSFields() { + Requirements::css(USERFORMS_DIR . '/css/UserForm_cms.css'); $self = $this; @@ -365,9 +366,7 @@ class UserDefinedForm_Controller extends Page_Controller { */ public function Form() { $form = UserForm::create($this); - $this->generateConditionalJavascript(); - return $form; } diff --git a/code/model/editableformfields/EditableCheckboxGroupField.php b/code/model/editableformfields/EditableCheckboxGroupField.php index 8cdd8f2..17ea2fa 100755 --- a/code/model/editableformfields/EditableCheckboxGroupField.php +++ b/code/model/editableformfields/EditableCheckboxGroupField.php @@ -14,17 +14,15 @@ 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()); // 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; } diff --git a/code/model/editableformfields/EditableCountryDropdownField.php b/code/model/editableformfields/EditableCountryDropdownField.php index 4529e54..2485658 100644 --- a/code/model/editableformfields/EditableCountryDropdownField.php +++ b/code/model/editableformfields/EditableCountryDropdownField.php @@ -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) { diff --git a/code/model/editableformfields/EditableDateField.php b/code/model/editableformfields/EditableDateField.php index 7146737..c16ab87 100755 --- a/code/model/editableformfields/EditableDateField.php +++ b/code/model/editableformfields/EditableDateField.php @@ -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; } } diff --git a/code/model/editableformfields/EditableDropdown.php b/code/model/editableformfields/EditableDropdown.php index f9f6866..a0aa534 100755 --- a/code/model/editableformfields/EditableDropdown.php +++ b/code/model/editableformfields/EditableDropdown.php @@ -28,30 +28,14 @@ class EditableDropdown extends EditableMultipleOptionField { * @return DropdownField */ public function getFormField() { - $optionSet = $this->Options(); - $defaultOptions = $optionSet->filter('Default', 1); - $options = array(); + $field = DropdownField::create($this->Name, $this->EscapedTitle, $this->getOptionsMap()); - if($optionSet) { - foreach($optionSet as $option) { - $options[$option->Title] = $option->Title; - } + // Set default + $defaultOption = $this->getDefaultOptions()->first(); + if($defaultOption) { + $field->setValue($defaultOption->EscapedTitle); } - - $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); - } - + $this->doUpdateFormField($field); return $field; } } \ No newline at end of file diff --git a/code/model/editableformfields/EditableEmailField.php b/code/model/editableformfields/EditableEmailField.php index 7da9eb2..732828b 100755 --- a/code/model/editableformfields/EditableEmailField.php +++ b/code/model/editableformfields/EditableEmailField.php @@ -18,19 +18,8 @@ class EditableEmailField extends EditableFormField { } 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); - + $field = EmailField::create($this->Name, $this->EscapedTitle, $this->Default); + $this->doUpdateFormField($field); return $field; } diff --git a/code/model/editableformfields/EditableFileField.php b/code/model/editableformfields/EditableFileField.php index 4489577..5cff5a2 100755 --- a/code/model/editableformfields/EditableFileField.php +++ b/code/model/editableformfields/EditableFileField.php @@ -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; } diff --git a/code/model/editableformfields/EditableFormField.php b/code/model/editableformfields/EditableFormField.php index 26997af..429e0db 100755 --- a/code/model/editableformfields/EditableFormField.php +++ b/code/model/editableformfields/EditableFormField.php @@ -433,31 +433,13 @@ class EditableFormField extends DataObject { 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); } /** @@ -497,13 +479,59 @@ class EditableFormField extends DataObject { /** * Return a FormField to appear on the front end. Implement on - * your subclass + * 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() . " ". $identifier . ""; + $field->setTitle($title); + } + } + + // if this field has an extra class + if($field->ExtraClass) { + $field->addExtraClass($field->ExtraClass); + } + } /** * Return the instance of the submission field class diff --git a/code/model/editableformfields/EditableFormHeading.php b/code/model/editableformfields/EditableFormHeading.php index f5c6234..90c84ef 100755 --- a/code/model/editableformfields/EditableFormHeading.php +++ b/code/model/editableformfields/EditableFormHeading.php @@ -55,10 +55,23 @@ 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'); + $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($field->ExtraClass) { + $field->addExtraClass($field->ExtraClass); + } + } public function showInReports() { return !$this->HideFromReports; diff --git a/code/model/editableformfields/EditableFormStep.php b/code/model/editableformfields/EditableFormStep.php index 242422e..a2f6f2b 100644 --- a/code/model/editableformfields/EditableFormStep.php +++ b/code/model/editableformfields/EditableFormStep.php @@ -25,7 +25,6 @@ class EditableFormStep extends EditableFormField { $fields = parent::getCMSFields(); $fields->removeByName('MergeField'); - $fields->removeByName('StepID'); $fields->removeByName('Default'); $fields->removeByName('Validation'); $fields->removeByName('CustomRules'); @@ -37,7 +36,17 @@ class EditableFormStep extends EditableFormField { * @return FormField */ public function getFormField() { - return CompositeField::create()->setTitle($this->Title); + $field = CompositeField::create() + ->setTitle($this->EscapedTitle); + $this->doUpdateFormField($field); + return $field; + } + + protected function updateFormField($field) { + // if this field has an extra class + if($field->ExtraClass) { + $field->addExtraClass($field->ExtraClass); + } } /** diff --git a/code/model/editableformfields/EditableLiteralField.php b/code/model/editableformfields/EditableLiteralField.php index 4625038..eaba903 100644 --- a/code/model/editableformfields/EditableLiteralField.php +++ b/code/model/editableformfields/EditableLiteralField.php @@ -103,12 +103,16 @@ class EditableLiteralField extends EditableFormField { } public function getFormField() { - $label = $this->Title - ? "" - : ""; - $classes = $this->Title ? "" : " nolabel"; + // Build label and css classes + $label = ''; + $classes = $this->ExtraClass; + if(empty($this->Title)) { + $classes .= " nolabel"; + } else { + $label = ""; + } - return new LiteralField( + $field = new LiteralField( "LiteralField[{$this->ID}]", sprintf( "