Merge remote-tracking branch 'open-sausages/feature/field-groups' into feature/multi-page-forms-v2

This commit is contained in:
Damian Mooyman 2015-08-13 12:28:31 +12:00
commit 5c0f172ee5
28 changed files with 930 additions and 413 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
.DS_Store .DS_Store
.sass-cache/

View File

@ -37,17 +37,17 @@ class UserFormFieldEditorExtension extends DataExtension {
$this->createInitialFormStep(true); $this->createInitialFormStep(true);
$editableColumns = new GridFieldEditableColumns(); $editableColumns = new GridFieldEditableColumns();
$fieldClasses = $this->getEditableFieldClasses();
$editableColumns->setDisplayFields(array( $editableColumns->setDisplayFields(array(
'ClassName' => function($record, $column, $grid) { 'ClassName' => function($record, $column, $grid) use ($fieldClasses) {
if($record instanceof EditableFormStep) { if($record instanceof EditableFormField) {
return new LabelField($column, "Page Break"); return $record->getInlineClassnameField($column, $fieldClasses);
} else {
return DropdownField::create($column, '', $this->getEditableFieldClasses());
} }
}, },
'Title' => function($record, $column, $grid) { 'Title' => function($record, $column, $grid) {
return TextField::create($column, ' ') if($record instanceof EditableFormField) {
->setAttribute('placeholder', _t('UserDefinedForm.TITLE', 'Title')); return $record->getInlineTitleField($column);
}
} }
)); ));
@ -55,8 +55,13 @@ class UserFormFieldEditorExtension extends DataExtension {
->addComponents( ->addComponents(
$editableColumns, $editableColumns,
new GridFieldButtonRow(), new GridFieldButtonRow(),
$addField = new GridFieldAddNewInlineButton(), GridFieldAddClassesButton::create('EditableFormField')
$addStep = new GridFieldAddItemInlineButton('EditableFormStep'), ->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 GridFieldEditButton(),
new GridFieldDeleteAction(), new GridFieldDeleteAction(),
new GridFieldToolbarHeader(), new GridFieldToolbarHeader(),
@ -64,9 +69,6 @@ class UserFormFieldEditorExtension extends DataExtension {
new GridState_Component(), new GridState_Component(),
new GridFieldDetailForm() new GridFieldDetailForm()
); );
$addField->setTitle('Add Field');
$addStep->setTitle('Add Page Break');
$addStep->setExtraClass('uf-gridfield-steprow');
$fieldEditor = GridField::create( $fieldEditor = GridField::create(
'Fields', 'Fields',
@ -113,7 +115,7 @@ class UserFormFieldEditorExtension extends DataExtension {
// Add step // Add step
$step = EditableFormStep::create(); $step = EditableFormStep::create();
$step->Title = _t('EditableFormStep.TITLE_FIRST', 'First Step'); $step->Title = _t('EditableFormStep.TITLE_FIRST', 'First Page');
$step->Sort = 1; $step->Sort = 1;
$step->write(); $step->write();
$fields->add($step); $fields->add($step);
@ -126,21 +128,20 @@ class UserFormFieldEditorExtension extends DataExtension {
$classes = ClassInfo::getValidSubClasses('EditableFormField'); $classes = ClassInfo::getValidSubClasses('EditableFormField');
// Remove classes we don't want to display in the dropdown. // Remove classes we don't want to display in the dropdown.
$classes = array_diff($classes, array(
'EditableFormField',
'EditableMultipleOptionField'
));
$editableFieldClasses = array(); $editableFieldClasses = array();
foreach ($classes as $class) {
if(in_array($class, array('EditableFormField', 'EditableMultipleOptionField'))
|| Config::inst()->get($class, 'hidden')
) {
continue;
}
foreach ($classes as $key => $className) { $singleton = singleton($class);
$singleton = singleton($className);
if(!$singleton->canCreate()) { if(!$singleton->canCreate()) {
continue; continue;
} }
$editableFieldClasses[$className] = $singleton->i18n_singular_name(); $editableFieldClasses[$class] = $singleton->i18n_singular_name();
} }
return $editableFieldClasses; return $editableFieldClasses;

View File

@ -0,0 +1,108 @@
<?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();
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);
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);
}
}
return true;
}
}

View 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;
}
}

View 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();
}

View File

@ -0,0 +1,32 @@
<?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;
}
}

View 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);
}
}

View File

@ -0,0 +1,7 @@
<?php
/**
* Represents a page step in a form, which may contain form fields or other groups
*/
class UserFormsStepField extends UserFormsCompositeField {
}

View File

@ -0,0 +1,225 @@
<?php
/**
* A button which allows objects to be created with a specified classname(s)
*/
class GridFieldAddClassesButton extends Object implements GridField_HTMLProvider, GridField_URLHandler {
private static $allowed_actions = array(
'handleAdd'
);
/**
* 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->modelClasses;
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));
}
$data = new ArrayData(array(
'Title' => $this->getButtonName(),
'ButtonClass' => $this->getButtonClass(),
'Link' => Controller::join_links($grid->Link(), $this->getAction())
));
return array(
$this->getFragment() => $data->renderWith(__CLASS__)
);
}
/**
* Get the action suburl for this component
*
* @return type
*/
protected function getAction() {
$classes = implode('-', $this->getClasses());
return Controller::join_links('add-classes', $classes);
}
/**
* {@inheritDoc}
*/
public function getURLHandlers($grid) {
return array(
$this->getAction() => 'handleAdd'
);
}
/**
* Handles adding a new instance of a selected class.
*
* @param GridField $grid
* @param SS_HTTPRequest $request
*/
public function handleAdd($grid, $request) {
$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);
}
// Return directly to the gridfield again
return $grid
->getForm()
->getController()
->redirectBack();
}
}

View File

@ -75,31 +75,11 @@ class UserForm extends Form {
* @return FieldList * @return FieldList
*/ */
public function getFormFields() { public function getFormFields() {
$fields = new FieldList(); $fields = new UserFormsFieldList();
$emptyStep = null; // Last empty step, which may or may not later have children $target = $fields;
foreach ($this->controller->Fields() as $field) { foreach ($this->controller->Fields() as $field) {
// When we encounter a step, save it $target = $target->processNext($field);
if ($field instanceof EditableFormStep) {
$emptyStep = $field->getFormField();
continue;
}
// 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());
}
$fields->last()->push($field->getFormField());
} }
$this->extend('updateFormFields', $fields); $this->extend('updateFormFields', $fields);
return $fields; return $fields;
} }

View File

@ -1,246 +0,0 @@
<?php
/**
* Allows inline adding of classes with a default type
*
* Provides an alternative to GridFieldAddNewInlineButton, but allows you to set a classname
*/
class GridFieldAddItemInlineButton implements GridField_HTMLProvider, GridField_SaveHandler {
/**
* Fragment id
*
* @var string
*/
protected $fragment;
/**
* Button title
*
* @var string
*/
protected $title;
/**
* Class name
*
* @var string
*/
protected $modelClass = null;
/**
* Extra CSS classes for this row
*
* @var string
*/
protected $extraClass = null;
/**
* @param string $class Name of class to default to
* @param string $fragment The fragment to render the button into
*/
public function __construct($class, $fragment = 'buttons-before-left') {
$this->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();
}
}

View File

@ -178,6 +178,8 @@ SQL;
$config->addComponent($export = new GridFieldExportButton()); $config->addComponent($export = new GridFieldExportButton());
$config->addComponent($print = new GridFieldPrintButton()); $config->addComponent($print = new GridFieldPrintButton());
Requirements::javascript(USERFORMS_DIR . '/javascript/Gridfield.js');
/** /**
* Support for {@link https://github.com/colymba/GridFieldBulkEditingTools} * Support for {@link https://github.com/colymba/GridFieldBulkEditingTools}
*/ */
@ -284,6 +286,14 @@ SQL;
DB::alteration_message('Migrated userforms', 'changed'); DB::alteration_message('Migrated userforms', 'changed');
} }
/**
* Validate formfields
*/
public function getCMSValidator() {
return new UserFormValidator();
}
} }
/** /**

View File

@ -0,0 +1,80 @@
<?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;
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->removeByName(array('MergeField', 'Default', 'Validation', 'DisplayRules'));
return $fields;
}
public function getCMSTitle() {
return _t(
'EditableFieldGroupEnd.FIELD_GROUP_START',
'Start of {group}',
array(
'group' => $this->Title ?: 'group'
)
);
}
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($field->ExtraClass) {
$field->addExtraClass($field->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();
}
}
}

View File

@ -0,0 +1,86 @@
<?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;
public function getCMSTitle() {
$group = $this->Group();
return _t(
'EditableFieldGroupEnd.FIELD_GROUP_END',
'End of {group}',
array(
'group' => ($group && $group->exists() && $group->Title) ? $group->Title : '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();
}
}
}

View File

@ -8,6 +8,14 @@
*/ */
class EditableFormField extends DataObject { class EditableFormField extends DataObject {
/**
* Set to true to hide from class selector
*
* @config
* @var bool
*/
private static $hidden = false;
/** /**
* Default sort order * Default sort order
* *
@ -442,6 +450,10 @@ class EditableFormField extends DataObject {
return Convert::raw2xml($this->Title); return Convert::raw2xml($this->Title);
} }
public function getCMSTitle() {
return $this->i18n_singular_name() . ' (' . $this->Title . ')';
}
/** /**
* @deprecated since version 4.0 * @deprecated since version 4.0
*/ */
@ -616,4 +628,27 @@ 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'));
}
} }

View File

@ -6,17 +6,17 @@
*/ */
class EditableFormStep extends EditableFormField { class EditableFormStep extends EditableFormField {
/** private static $singular_name = 'Page Break';
* @config
* @var string private static $plural_name = 'Page Breaks';
*/
private static $singular_name = 'Step';
/** /**
* Disable selection of step class
*
* @config * @config
* @var string * @var bool
*/ */
private static $plural_name = 'Steps'; private static $hidden = true;
/** /**
* @return FieldList * @return FieldList
@ -24,10 +24,7 @@ class EditableFormStep extends EditableFormField {
public function getCMSFields() { public function getCMSFields() {
$fields = parent::getCMSFields(); $fields = parent::getCMSFields();
$fields->removeByName('MergeField'); $fields->removeByName(array('MergeField', 'Default', 'Validation', 'DisplayRules'));
$fields->removeByName('Default');
$fields->removeByName('Validation');
$fields->removeByName('CustomRules');
return $fields; return $fields;
} }
@ -36,7 +33,7 @@ class EditableFormStep extends EditableFormField {
* @return FormField * @return FormField
*/ */
public function getFormField() { public function getFormField() {
$field = CompositeField::create() $field = UserFormsStepField::create()
->setTitle($this->EscapedTitle); ->setTitle($this->EscapedTitle);
$this->doUpdateFormField($field); $this->doUpdateFormField($field);
return $field; return $field;
@ -55,4 +52,19 @@ class EditableFormStep extends EditableFormField {
public function showInReports() { public function showInReports() {
return false; return false;
} }
public function getInlineClassnameField($column, $fieldClasses) {
return new LabelField(
$column,
$this->CMSTitle
);
}
public function getCMSTitle() {
$title = $this->i18n_singular_name();
if($this->Title) {
$title .= ' (' . $this->Title . ')';
}
return $title;
}
} }

12
config.rb Normal file
View 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

View File

@ -1,38 +1,37 @@
/** /**
* Lightweight base styles for the front-end form. * Lightweight base styles for the front-end form.
*/ */
.userform-progress .step-buttons,
.step-navigation .step-buttons {
margin-left: 0;
}
.userform-progress .step-buttons {
position: relative;
}
.userform-progress .step-button-jump {
position: absolute;
top: 0;
}
.userform-progress .step-buttons .step-button-wrapper,
.step-navigation .step-buttons .step-button-wrapper {
display: inline-block;
list-style-type: none;
}
.userform-progress .progress { .userform-progress .progress {
position: relative; position: relative;
height: 1em; height: 1em;
background: #eee; background: #eee;
}
.userform-progress .progress .progress-bar {
position: absolute;
height: 1em;
background: #666;
}
.userform-progress .step-buttons {
margin-left: 0;
position: relative;
}
.userform-progress .step-buttons .step-button-wrapper {
display: inline-block;
list-style-type: none;
}
.userform-progress .step-button-jump {
position: absolute;
top: 0;
} }
.userform-progress .progress .progress-bar { .step-navigation .step-buttons {
position: absolute; margin-left: 0;
height: 1em; }
background: #666; .step-navigation .step-buttons .step-button-wrapper {
display: inline-block;
list-style-type: none;
} }
.userform { .userform {
clear: both; clear: both;
} }

View File

@ -1,24 +1,28 @@
.cms .ss-gridfield > div.ss-gridfield-buttonrow-before {
margin-bottom: 10px;
overflow: auto;
}
/** /**
* Styles for page steps * Styles for cms
*/ */
.cms table.ss-gridfield-table .ss-gridfield-inline-new.uf-gridfield-steprow, .cms .ss-gridfield > div.ss-gridfield-buttonrow-before {
.cms table.ss-gridfield-table .ss-gridfield-item[data-class='EditableFormStep'], margin-bottom: 10px;
.cms table.ss-gridfield-table .ss-gridfield-inline-new.uf-gridfield-steprow:hover, overflow: auto;
.cms table.ss-gridfield-table .ss-gridfield-item[data-class='EditableFormStep']:hover
{
background: #dae2e7;
} }
.cms table.ss-gridfield-table .ss-gridfield-item[data-class='EditableFormStep'], .cms table.ss-gridfield-table .ss-gridfield-item[data-class='EditableFormStep']:hover {
.cms table.ss-gridfield-table .ss-gridfield-inline-new.uf-gridfield-steprow label, background: #dae2e7;
.cms table.ss-gridfield-table .ss-gridfield-item[data-class='EditableFormStep'] label }
{ .cms table.ss-gridfield-table .ss-gridfield-item[data-class='EditableFormStep'] label {
font-weight: bold; font-weight: bold;
color: #393939; color: #444;
font-size: 1.1em; font-size: 1.1em;
}
.cms table.ss-gridfield-table .ss-gridfield-item[data-class^='EditableFieldGroup'], .cms table.ss-gridfield-table .ss-gridfield-item[data-class^='EditableFieldGroup']:hover {
background: #E7EFF4;
}
.cms table.ss-gridfield-table .ss-gridfield-item[data-class^='EditableFieldGroup'] label {
font-weight: bold;
}
.cms table.ss-gridfield-table .ss-gridfield-item[data-class='EditableFieldGroupEnd'] .col-Title input {
display: none;
}
.cms table.ss-gridfield-table .ss-gridfield-item.inFieldGroup .col-reorder,
.cms table.ss-gridfield-table .ss-gridfield-item.inFieldGroup .handle {
background: #E7EFF4;
} }

View File

@ -1,41 +0,0 @@
(function($) {
$.entwine("ss", function($) {
// See gridfieldextensions/javascript/GridFieldExtensions.js
$(".ss-gridfield.ss-gridfield-editable").entwine({
onaddnewiteminline: function(e, template) {
var tmpl = window.tmpl;
var row = this.find("." + template);
var num = this.data("add-inline-num") || 1;
tmpl.cache[template] = tmpl(row.html());
this.find("tbody").append(tmpl(template, { num: num }));
this.find(".ss-gridfield-no-items").hide();
this.data("add-inline-num", num + 1);
}
});
$(".ss-gridfield-add-new-item-inline").entwine({
onclick: function() {
// Get custom class from button
var template = this.data('template');
this.getGridField().trigger("addnewiteminline", template);
return false;
}
});
$(".ss-gridfield-delete-inline").entwine({
onclick: function() {
var msg = ss.i18n._t("GridFieldExtensions.CONFIRMDEL", "Are you sure you want to delete this?");
if(confirm(msg)) {
this.parents("tr").remove();
}
return false;
}
});
});
})(jQuery);

23
javascript/Gridfield.js Normal file
View File

@ -0,0 +1,23 @@
/**
* form builder behaviour.
*/
(function($) {
$.entwine('ss', function($) {
$(".ss-gridfield-orderable tbody").entwine({
onmatch: function() {
this._super();
this.find('.ss-gridfield-item')
.removeClass('inFieldGroup');
this.find('.ss-gridfield-item[data-class="EditableFieldGroup"]')
.nextUntil('.ss-gridfield-item[data-class="EditableFieldGroupEnd"]')
.addClass('inFieldGroup');
},
onunmatch: function () {
this._super();
}
});
});
}(jQuery));

View File

@ -322,6 +322,7 @@ jQuery(function ($) {
this.$el = element instanceof jQuery ? element : $(element); this.$el = element instanceof jQuery ? element : $(element);
this.$buttons = this.$el.find('.step-button-jump'); this.$buttons = this.$el.find('.step-button-jump');
this.$jsAlign = this.$el.find('.js-align');
// Update the progress bar when 'step' buttons are clicked. // Update the progress bar when 'step' buttons are clicked.
this.$buttons.each(function (i, stepButton) { this.$buttons.each(function (i, stepButton) {
@ -337,9 +338,9 @@ jQuery(function ($) {
}); });
// Spaces out the steps below progress bar evenly // Spaces out the steps below progress bar evenly
this.$buttons.each(function (index, button) { this.$jsAlign.each(function (index, button) {
var $button = $(button), var $button = $(button),
leftPercent = (100 / (self.$buttons.length - 1) * index + '%'), leftPercent = (100 / (self.$jsAlign.length - 1) * index + '%'),
buttonOffset = -1 * ($button.innerWidth() / 2); buttonOffset = -1 * ($button.innerWidth() / 2);
$button.css({left: leftPercent, marginLeft: buttonOffset}); $button.css({left: leftPercent, marginLeft: buttonOffset});

47
scss/UserForm.scss Normal file
View File

@ -0,0 +1,47 @@
/**
* 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;
}
}
.step-button-jump {
position: absolute;
top: 0;
}
}
.step-navigation {
.step-buttons {
margin-left: 0;
.step-button-wrapper {
display: inline-block;
list-style-type: none;
}
}
}
.userform {
clear: both;
}

50
scss/UserForm_cms.scss Normal file
View File

@ -0,0 +1,50 @@
/**
* Styles for cms
*/
.cms {
.ss-gridfield > div.ss-gridfield-buttonrow-before {
margin-bottom: 10px;
overflow: auto;
}
table.ss-gridfield-table {
.ss-gridfield-item[data-class='EditableFormStep'] {
&, &:hover {
background: #dae2e7;
}
label {
font-weight: bold;
color: #444;
font-size: 1.1em;
}
}
.ss-gridfield-item[data-class^='EditableFieldGroup'] {
&, &:hover {
background: #E7EFF4;
}
label {
font-weight: bold;
}
}
.ss-gridfield-item[data-class='EditableFieldGroupEnd'] {
.col-Title input {
display: none;
}
}
.ss-gridfield-item.inFieldGroup {
.col-reorder,
.handle {
background: #E7EFF4;
}
}
}
}

View File

@ -10,7 +10,7 @@
<ul class="step-buttons"> <ul class="step-buttons">
<% loop $NumberOfSteps %> <% loop $NumberOfSteps %>
<li class="step-button-wrapper<% if $Pos == '1' %> current<% end_if %>"> <li class="step-button-wrapper<% if $Pos == '1' %> current<% end_if %>">
<button class="step-button-jump" disabled="disabled">$Pos</button> <button class="step-button-jump js-align" disabled="disabled">$Pos</button><!-- Remove js-align class to remove javascript positioning -->
</li> </li>
<% end_loop %> <% end_loop %>
<ul> <ul>

View File

@ -0,0 +1,3 @@
<a href="$Link.ATT" class="action action-detail ss-ui-button ui-button ui-widget ui-state-default ui-corner-all new new-link $ButtonClass.ATT" data-icon="add">
$Title.XML
</a>

View File

@ -1,3 +0,0 @@
<button href="$Link" class="ss-gridfield-add-new-item-inline ss-ui-button" data-icon="add" data-template="$TemplateName.ATT">
$Title
</button>

View File

@ -1,13 +0,0 @@
<script type="text/x-tmpl" class="$TemplateName">
<tr class="ss-gridfield-inline-new $ExtraClass.ATT">
<% loop $Columns %>
<% if $IsActions %>
<td$Attributes>
<button class="ss-gridfield-delete-inline gridfield-button-delete ss-ui-button" data-icon="cross-circle"></button>
</td>
<% else %>
<td$Attributes>$Content</td>
<% end_if %>
<% end_loop %>
</tr>
</script>