Source of file MultiRecordField.php
Size: 66,215 Bytes - Last Modified: 2021-12-23T10:20:37+00:00
/var/www/docs.ssmods.com/process/src/code/fields/MultiRecordField.php
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744 | <?php /** * For tracking field (sent/expanded names) and values in * 'saveInto' */ class MultiRecordFieldData { /** * Keep the original requested name for the field as FileAttachmentField * needs it for processing deleted items. * * @var string */ public $requestName; /** * @var mixed */ public $value; } /** * @author Jake Bentvelzen */ class MultiRecordField extends FormField { /** * Invalid sort value. Value should be replaced by JavaScript on * new records without a sort value. * * @var int */ const SORT_INVALID = 0; /** * Structure for when 'saveInto' is tracking * new records that need to be saved. */ const NEW_RECORD = 0; const NEW_LIST = 1; /** * Set variables or call functions on certain fields underneath this field. * ie. Change rows to 6 for HtmlEditorField so it takes less space. * * @config * @var array */ private static $default_config = array( /*'HtmlEditorField' => array( 'functions' => array( 'setRows' => 6, ), ),*/ ); /** * Classes to apply to every FormAction field. * (ie. <button> or <input type="submit" />) * * @config * @var string */ private static $default_button_classes = ''; /** * Enable workaround for ListboxField bug in 'framework' 3.3 and below. * When disabled, an exception will be thrown if that bug is detected. * * https://github.com/silverstripe/silverstripe-framework/pull/5775 * * @config * @var boolean */ private static $enable_patch_5775 = false; /** * Defaults to default_config if not set. * * @var array */ protected $config = null; /** * The list object passed into the object. * * @var SS_List */ protected $list; /** * @var FieldList */ protected $actions = null; /** * Field to use for the ToggleCompositeField's heading/title * * @var string */ protected $titleField = ''; /** * Override the field function to call on the record. * * @var function|string */ protected $fieldsFunction = ''; /** * Whether to fallback on 'getDefaultFieldsFunction' if the $fieldsFunction * is a string and doesn't exist on the record. * * @var boolean */ protected $fieldsFunctionFallback = false; /** * Class name of the DataObject that the GridField will display. * * Defaults to the value of $this->list->dataClass. * * @var string */ protected $modelClassName = ''; /** * Whether to override html editor heights * * @var int */ protected $htmlEditorHeight = 6; /** * The field name to sort by. * * @var string|array */ protected $sortFieldName = null; /** * Should we use toggle Composites in layout ? * * @var boolean */ protected $useToggles = true; /** * @var boolean */ protected $preparedForRender = false; /** * @var boolean */ protected $canAddInline = true; /** * @var FieldList */ protected $children, $tabs; /** * If null/false, fallback to default_button_classes config * * @var string|null */ protected $buttonClasses = null; /** * @var array List of additional CSS classes for the form tag. */ protected $extraClasses = array(); /** * How nested inside other MultiRecordField's this field is. * * @var int */ protected $depth = 1; public function __construct($name, $title = null, SS_List $list = null) { parent::__construct($name, $title); $this->children = FieldList::create(); $this->list = $list; } /** * */ public function handleAddInline(SS_HTTPRequest $request) { // Force reset $this->children = FieldList::create(); // Get passed arguments // todo(Jake): Change '->remaining' to '->shift(4)' and test. // remove other ->shift things. $dirParts = explode('/', $request->remaining()); $class = isset($dirParts[0]) ? $dirParts[0] : ''; if (!$class) { return $this->httpError(400, 'No ClassName was supplied.'); } $modelClassNames = $this->getModelClasses(); if (!isset($modelClassNames[$class])) { return $this->httpError(400, 'Invalid ClassName "'.$class.'" was supplied.'); } // Determine sub field action (if executing one) $isSubFieldAction = (isset($dirParts[1])); $recordIDOrNew = (isset($dirParts[1]) && $dirParts[1]) ? $dirParts[1] : null; if ($recordIDOrNew === null || $recordIDOrNew === 'new') { $record = $class::create(); if (!$record->canCreate(Member::currentUser())) { return $this->httpError(400, 'Invalid permissions. Current user (#'.Member::currentUserID().') cannot create "'.$class.'" class type.'); } } else { $recordIDOrNew = (int)$recordIDOrNew; if (!$recordIDOrNew) { return $this->httpError(400, 'Malformed record ID in sub-field action was supplied ('.$class.' #'.$recordIDOrNew.').'); } $record = $class::get()->byID($recordIDOrNew); if (!$record->canEdit(Member::currentUser())) { return $this->httpError(400, 'Invalid permissions. Current user (#'.Member::currentUserID().') cannot edit "'.$class.'" #'.$recordIDOrNew.' class type.'); } } // Check if sub-field exists on requested record (can request new record with 'new') $fields = $this->getRecordDataFields($record); $dataFields = $fields->dataFields(); // $isValidSubFieldAction = (isset($dirParts[2]) && $dirParts[2] === 'field') ? true : false; $subField = null; if ($isSubFieldAction) { $subFieldName = (isset($dirParts[3]) && $dirParts[3]) ? $dirParts[3] : ''; if (!$subFieldName || !isset($dataFields[$subFieldName])) { return $this->httpError(400, 'Invalid sub-field was supplied ('.$class.'::'.$subFieldName.').'); } $subField = $dataFields[$subFieldName]; } $this->applyUniqueFieldNames($fields, $record); // If set a sub-field, execute its action instead. if ($isSubFieldAction) { if ($isValidSubFieldAction && $subField) { // Consume so Silverstripe handles the actions naturally. $request->shift(); // $ClassName $request->shift(); // $ID ('new' or '1') $request->shift(); // field $request->shift(); // $SubFieldName return $subField; } return $this->httpError(400, 'Invalid sub-field action on '.__CLASS__.'::'.__FUNCTION__); } // Allow fields to render, $this->children = $fields; // Remove all actions $actions = $this->Actions(); foreach ($actions as $action) { $actions->remove($action); } return $this->renderWith(array($this->class.'_addinline', __CLASS__.'_addinline')); } public function handleRequest(SS_HTTPRequest $request, DataModel $model) { if ($request->match('addinlinerecord', true)) { // NOTE(Jake): Handling here as I'm not sure how to do a url_handler that allows // infinite parameters after 'addinlinerecord' $result = $this->handleAddInline($request); if ($result && is_object($result) && $result instanceof RequestHandler) { // NOTE(Jake): Logic copied from parent::handleRequest() $returnValue = $result->handleRequest($request, $model); if($returnValue && is_array($returnValue)) { $returnValue = $this->customise($returnValue); } return $returnValue; } // NOTE(Jake): Consume all remaining parts so that 'RequestHandler::handleRequest' // doesn't hit an error. (Use Case: Getting an error with a GridField::handleRequest) // NOTE(Jake): THis is probably due to just CLASSNAME not being consumed/shifted in 'addinlinerecord' // but cbf changing and re-testing everything. $dirParts = explode('/', $request->remaining()); foreach ($dirParts as $dirPart) { $request->shift(); } return $result; } $result = parent::handleRequest($request, $model); return $result; } /** * @param boolean $value * @return \MultiRecordField */ public function setUseToggles($value) { $this->useToggles = $value; return $this; } /** * @return boolean */ public function getUseToggles() { return $this->useToggles; } /** * @param boolean $value * @return \MultiRecordField */ public function setCanAddInline($value) { $this->canAddInline = $value; return $this; } /** * @return boolean */ public function getCanAddInline() { return $this->canAddInline; } /** * @return string */ public function getButtonClasses() { $result = $this->buttonClasses; if ($result === null || $result === false) { return $this->stat('default_button_classes'); } return $result; } /** * Set the classes to be applied on each FormAction field. * (ie. <button> or <input type="submit" />) * * @return \MultiRecordField */ public function setButtonClasses($classes) { $this->buttonClasses = $classes; return $this; } /** * Apply button classes to a fieldlist of actions * * @return \MultiRecordField */ public function applyButtonClasses(FieldList $actions) { $buttonClasses = $this->getButtonClasses(); if ($buttonClasses && $actions) { foreach ($actions as $actionField) { if ($actionField instanceof FormAction) { $actionField->addExtraClass($buttonClasses); } } } return $this; } /** * @param array value * @param array data Passed from Form::loadDataFrom for composite fields to act on data * @return \MultiRecordField */ public function setValue($value, $formData = array()) { if (!$value && $formData && is_array($formData)) { // NOTE(Jake): The call stack is: // $field->setValue($val, $formData); // $form->loadDataFrom($data) // $form->httpSubmission($request); if ($formData) { $relation_class_id_field = array(); $relationFieldName = $this->getName(); foreach ($formData as $name => $value) { // If fieldName is the -very- first part of the string // NOTE(Jake): Check is required here as we're pulling straight from $request if (strpos($name, $relationFieldName) === 0) { static $FIELD_PARAMETERS_SIZE = 5; $fieldParameters = explode('__', $name); $fieldParametersCount = count($fieldParameters); if ($fieldParametersCount < $FIELD_PARAMETERS_SIZE) { // You expect a name like 'ElementArea__MultiRecordField__ElementGallery__new_1__Title' // So ensure 5 parameters exist in the name, otherwise continue. continue; } $signature = $fieldParameters[1]; if ($signature !== 'MultiRecordField') { return $this->httpError(400, 'Invalid signature in "MultiRecordField". Malformed MultiRecordField sub-field or hack attempt.'); } $parentFieldName = $fieldParameters[0]; $class = $fieldParameters[2]; $new_id = $fieldParameters[3]; $fieldName = $fieldParameters[4]; // $fieldData = new MultiRecordFieldData; $fieldData->requestName = $name; $fieldData->value = $value; if ($fieldParametersCount == $FIELD_PARAMETERS_SIZE) { // 1st Nest Level $relation_class_id_field[$parentFieldName][$class][$new_id][$fieldName] = $fieldData; } else { // 2nd, 3rd, nth Nest Level $relationArray = &$relation_class_id_field; for ($i = 0; $i < $fieldParametersCount - 1; $i += $FIELD_PARAMETERS_SIZE - 1) { $parentFieldName = $fieldParameters[$i]; $signature = $fieldParameters[$i+1]; $class = $fieldParameters[$i+2]; $new_id = $fieldParameters[$i+3]; $fieldName = $fieldParameters[$i+4]; if (!isset($relationArray[$parentFieldName][$class][$new_id])) { $relationArray[$parentFieldName][$class][$new_id] = array(); } $relationArray = &$relationArray[$parentFieldName][$class][$new_id]; } $relationArray[$fieldName] = $fieldData; unset($relationArray); } } } // Set value $this->value = reset($relation_class_id_field); return $this; } } $this->value = $value; return $this; } /** * @return \MultiRecordField */ public function getConfig() { return $this->config; } /** * @return \MultiRecordField */ public function setConfig($config) { if ($config && $config instanceof GridFieldConfig) { // NOTE(Jake): Stubbed by design so developers can switch between GridField and this class quickly for test/dev purposes. return $this; } // todo(Jake): Improve API to allow multiple params (ie. setConfigFunction('HtmlEditorField', 'setRows', 6)) $this->config = $config; return $this; } /** * Gets the first model class from list. * This function exists to be identical to the GridField function so developers can * quickly switch between the two. * * @return string */ public function getModelClass() { return reset($this->modelClassNames); } /** * Set one model class. * This function exists to be identical to the GridField function so developers can * quickly switch between the two. * * @param string $modelClass * @return \MultiRecordField */ public function setModelClass($modelClass) { if (is_array($modelClass)) { throw new Exception(__CLASS__.'::'.__FUNCTION__.': Only accepts singular value (not array). Use setModelClasses() instead.'); } return $this->setModelClasses($modelClass); } /** * If array, can be formatted like so: * array('MyClass', 'MyOtherClass') * -or- * array('MyClass' => 'Nice Name 1', 'MyOtherClass' => 'Nice Name 2') * * The classes provided must all extend the same parent. * * @param array|string $modelClassName * * @return \MultiRecordField */ public function setModelClasses($modelClassNames) { if (is_array($modelClassNames)) { $this->modelClassNames = $modelClassNames; } else { $this->modelClassNames = array($modelClassNames); } $this->modelClassNames = static::convert_to_associative($this->modelClassNames); return $this; } /** * @return array */ public function getModelClasses() { if($this->modelClassNames) { return $this->modelClassNames; } if($this->list && method_exists($this->list, 'dataClass')) { $class = $this->list->dataClass(); if ($class) { if(!is_array($class)) { $class = array($class); } return static::convert_to_associative($class); } } return array(); } /** * @return array * * @throws LogicException */ public function getModelClassesOrThrowExceptionIfEmpty() { $modelClasses = $this->getModelClasses(); if (!$modelClasses) { throw new LogicException(__CLASS__.' doesn\'t have any modelClasses set, so it doesn\'t know what class types can be added inline.'); } return $modelClasses; } /** * Convert regular array to associative, making the key * the classname and the value the pretty/front-facing name. * * @return array */ public static function convert_to_associative($modelClasses) { $source = array(); if (isset($modelClasses[0])) { // If regular array, fallback to singular name foreach ($modelClasses as $modelClass) { $source[$modelClass] = singleton($modelClass)->singular_name(); } } else { // If associative/hashmap, make the key the classname foreach ($modelClasses as $modelClass => $niceName) { $source[$modelClass] = $niceName; } } return $source; } /** * @return int|string */ public function getFieldID($record) { if ($record && $record->ID) { return (int)$record->ID; } if ($record && $record->MultiRecordField_NewID) { // Allow setting 'MultiRecordField_NewID' to say 'new_1' or 'new_2' // to restore fields that failed validation. return $record->MultiRecordField_NewID; } // NOTE(Jake): Not '{%=o.multirecordediting.id%}' with tmpl.js because SS strips '{', '}' and replaces '.' with '-' return 'o-multirecordediting-'.$this->depth.'-id'; } /** * @return SS_List */ public function getList() { return $this->list; } /** * @param SS_List $list * @return \MultiRecordField */ public function setList(SS_List $list) { $this->list = $list; return $this; } /** * @param Form $form * @return \MultiRecordField */ public function setForm($form) { parent::setForm($form); foreach ($this->children as $child) { $child->setForm($form); } return $this; } /** * @return boolean */ public function getCanSort() { return (!$this->isReadonly() && $this->getSortFieldName()); } /** * Get the field to use for the ToggleCompositeField's heading/title * * @return string */ public function getTitleField() { return $this->titleField; } /** * Set the field to use for the ToggleCompositeField's heading/title * * @return \MultiRecordField */ public function setTitleField($fieldName) { $this->titleField = $fieldName; return $this; } /** * Get the default function to call on the record if * $this->fieldsFunction isn't set. * * @return string */ public function getDefaultFieldsFunction(DataObjectInterface $record) { if (method_exists($record, 'getMultiRecordFields') || (method_exists($record, 'hasMethod') && $record->hasMethod('getMultiRecordFields'))) { return 'getMultiRecordFields'; } else { return 'getCMSFields'; } } /** * Get closure or string of the function to call for getting record * fields. * * @return function|string */ public function getFieldsFunction() { return $this->fieldsFunction; } /** * Set the function to call on the $record for determining what fields to show. * * If string, then set the method to call on the record to get fields. * If closure, then call the method for the fields with $record as the first parameter. * * @param string|function $functionOrFunctionName * @param boolean $fallback If true, fallback to using 'getMultiRecordFields' and then fallback to 'getCMSFields' * @return MultiRecordField */ public function setFieldsFunction($functionOrFunctionName, $fallback = false) { $this->fieldsFunction = $functionOrFunctionName; $this->fieldsFunctionFallback = $fallback; return $this; } /** * @return FieldList|null */ public function getRecordDataFields(DataObjectInterface $record) { $fieldsFunction = $this->getFieldsFunction(); if (!$fieldsFunction) { $fieldsFunction = $this->getDefaultFieldsFunction($record); } $fields = null; if (is_callable($fieldsFunction)) { $fields = $fieldsFunction($record); } else { if (method_exists($record, $fieldsFunction) || (method_exists($record, 'hasMethod') && $record->hasMethod($fieldsFunction))) { $fields = $record->$fieldsFunction(); } else if ($this->fieldsFunctionFallback) { $fieldsFunction = $this->getDefaultFieldsFunction($record); $fields = $record->$fieldsFunction(); } else { throw new Exception($record->class.'::'.$fieldsFunction.' function does not exist.'); } } if (!$fields || !$fields instanceof FieldList) { throw new Exception('Function callback on '.__CLASS__.' must return a FieldList.'); } $record->extend('updateMultiEditFields', $fields); $dataFields = $fields->dataFields(); if (!$dataFields) { $errorMessage = __CLASS__.' is missing fields for record #'.$record->ID.' on class "'.$record->class.'".'; if (is_callable($fieldsFunction)) { throw new Exception($errorMessage.'. This is due to the closure set with "setFieldsFunction" not returning a populated FieldList.'); } throw new Exception($errorMessage.'. This is due '.$record->class.'::'.$fieldsFunction.' not returning a populated FieldList.'); } // $recordExists = $record->exists(); // Set value from record if it exists or if re-loading data after failed form validation $recordShouldSetValue = ($recordExists || $record->MultiRecordField_NewID); // Setup sort field $sortFieldName = $this->getSortFieldName(); if ($sortFieldName) { $sortField = isset($dataFields[$sortFieldName]) ? $dataFields[$sortFieldName] : null; if ($sortField && !$sortField instanceof HiddenField) { throw new Exception('Cannot utilize drag and drop sort functionality if the sort field is explicitly used on form. Suggestion: $fields->removeByName("'.$sortFieldName.'") in '.$record->class.'::'.$fieldsFunction.'().'); } if (!$sortField) { $sortValue = ($recordShouldSetValue) ? $record->$sortFieldName : self::SORT_INVALID; $sortField = HiddenField::create($sortFieldName); if ($sortField instanceof HiddenField) { $sortField->setAttribute('value', $sortValue); } else { $sortField->setValue($sortValue); } $sortField->setAttribute('data-ignore-delete-check', 1); // NOTE(Jake): Uses array_merge() to prepend the sort field in the $fields associative array. // The sort field is prepended so jQuery.find('.js-multirecordfield-sort-field').first() // finds the related sort field to this, rather than a sort field nested deeply in other // MultiRecordField's. $fields->unshift($sortField); } $sortField->addExtraClass('js-multirecordfield-sort-field'); } // Set heading (ie. 'My Record (Draft)') $titleFieldName = $this->getTitleField(); $status = ''; if (!$titleFieldName) { $recordSectionTitle = $record->MultiRecordEditingTitle; if (!$recordSectionTitle) { $recordSectionTitle = $record->Title; $status = ($recordExists) ? $record->CMSPublishedState : 'New'; } } else { $recordSectionTitle = $record->$titleFieldName; } if (!$recordSectionTitle) { // NOTE(Jake): Ensures no title'd ToggleCompositeField's have a proper height. $recordSectionTitle = ' '; } $recordSectionTitle .= ' <span class="js-multirecordfield-title-status">'; $recordSectionTitle .= ($status) ? '('.$status.')' : ''; $recordSectionTitle .= '</span>'; // Add heading field / Togglable composite field with heading $subRecordField = MultiRecordSubRecordField::create('', $recordSectionTitle, null); $subRecordField->setParent($this); $subRecordField->setRecord($record); if ($this->readonly) { $subRecordField = $subRecordField->performReadonlyTransformation(); } // Modify *certain* dataFields() to work properly with this field foreach ($fields->dataFields() as $field) { $fieldName = $field->getName(); if ($recordShouldSetValue) { if (isset($record->$fieldName) || $record->hasMethod($fieldName) || ($record->hasMethod('hasField') && $record->hasField($fieldName))) { $val = $record->__get($fieldName); $field->setValue($val, $record); } } if ($field instanceof MultiRecordField) { $field->depth = $this->depth + 1; $action = $this->getActionURL($field, $record); $field->setAttribute('data-action', $action); // NOTE(Jake): Unclear at time of writing (17-06-2016) if nested MultiRecordField should // inherit certain settings or not. Might add flag like 'setRecursiveOptions' later // or something. $field->setFieldsFunction($this->getFieldsFunction(), $this->fieldsFunctionFallback); //$field->setTitleField($this->getTitleField()); } else { $config = $this->getConfig(); if ($config === null) { $config = $this->config()->default_config; } // todo(Jake): Make it walk class hierarchy so that things that extend say 'HtmlEditorField' // will also get the config. Make the '*HtmlEditorField' denote that it's only // for that class, sub-classes. if (isset($config[$field->class])) { $fieldConfig = $config[$field->class]; $functionCalls = isset($fieldConfig['functions']) ? $fieldConfig['functions'] : array(); if ($functionCalls) { foreach ($functionCalls as $methodName => $arguments) { $arguments = (array)$arguments; call_user_func_array(array($field, $methodName), $arguments); } } } if ($field instanceof FileAttachmentField) { // fix(Jake) // todo(Jake): Fix deletion // Support for Unclecheese's Dropzone module // @see: https://github.com/unclecheese/silverstripe-dropzone/tree/1.2.3 $action = $this->getActionURL($field, $record); $field = MultiRecordFileAttachmentField::cast($field); $field->multiRecordAction = $action; // Fix $field->Value() if ($recordShouldSetValue && !$val && isset($record->{$fieldName.'ID'})) { // NOTE(Jake): This check was added for 'FileAttachmentField'. // Putting this outside of this 'instanceof' if-statement will break UploadField. $val = $record->__get($fieldName.'ID'); if ($val) { $field->setValue($val, $record); } } } else if (class_exists('MultiRecord'.$field->class)) { // Handle generic case (ie. UploadField) // Where we just want to override value returned from $field->Link() // so FormField actions work. $class = 'MultiRecord'.$field->class; $fieldCopy = $class::create($field->getName(), $field->Title()); $ref = new ReflectionClass($field->class); $propList = $ref->getProperties(); foreach($propList as $propObj) { if ($propObj->isStatic()) { continue; } $property = $propObj->getName(); // Anything inheritting `ViewableData` essentially gives us full // access to set any variable (protected) to a new value. $fieldCopy->setField($property, $field->getField($property)); } $fieldCopy->multiRecordAction = $this->getActionURL($field, $record); $field = $fieldCopy; } else { // NOTE(Jake): This was added for QuickAddNew support (2017-01-05) // Allows non-casted fields to work via extensions. $field->multiRecordAction = $this->getActionURL($field, $record); } // NOTE(Jake): Should probably add an ->extend() so other modules can monkey patch fields. // Will wait to see if its needed. } // NOTE(Jake): Required to support UploadField. Generic so any field can utilize this functionality. if (method_exists($field, 'setRecord') || (method_exists($field, 'hasMethod') && $field->hasMethod('setRecord'))) { $field->setRecord($record); } if ($field instanceof UploadField) { // NOTE(Jake): Hack. Not sure why this value isn't sticking, $field->setAllowedMaxFileNumber($field->getAllowedMaxFileNumber()); } $fields->replaceField($fieldName, $field); } // Add fields to sub-record $stack = $fields->toArray(); while ($stack) { $field = array_shift($stack); if ($field instanceof TabSet) { // NOTE(Jake): TabSet isn't supported in this context, so just // get all the children and insert them in place. // Not using $fields->dataFields() so that 'FieldGroup' // fields and similar are retained. $tabSetChildren = null; if ($this->readonly) { $tabSetChildren = $field->performReadonlyTransformation()->getChildren()->toArray(); } else { $tabSetChildren = $field->getChildren()->toArray(); } $stack = array_merge($tabSetChildren, $stack); continue; } $subRecordField->push($field); } $resultFieldList = new FieldList(); $resultFieldList->push($subRecordField); $resultFieldList->setForm($this->form); return $resultFieldList; } /** * @param FormField $field * @param DataObject $record * @return string */ public function getActionURL($field, $record) { // Example of input data // --------------- // Level 1 Nest: // ------------- // [0] => ElementArea [1] => MultiRecordField [2] => ElementGallery [3] => new_2 [4] => Images // // Level 2 Nest: // ------------- // [5] => MultiRecordField [6] => ElementGallery_Item [7] => new_2 [8] => Items) // // $nameData = $this->getUniqueFieldName($field, $record); $nameData = explode('__', $nameData); $nameDataCount = count($nameData); $action = $nameData[0]; for ($i = 1; $i < $nameDataCount; $i += 4) { $signature = $nameData[$i]; if ($signature !== 'MultiRecordField') { throw new LogicException('Error caused by developer. Invalid signature in "MultiRecordField". Signature: '.$signature); } $class = $nameData[$i + 1]; $id = $nameData[$i + 2]; if ($record->MultiRecordField_NewID || strpos($id, 'o-multirecordediting') !== FALSE) { $id = 'new'; } $subFieldName = $nameData[$i + 3]; $action .= '/addinlinerecord/'.$class.'/'.$id.'/field/'.$subFieldName; } return $action; } /** * Re-write field names to be unique * ie. 'Title' to be 'ElementArea__MultiRecordField__ElementGallery__Title' * * @return \MultiRecordField */ public function applyUniqueFieldNames($fields, $record) { $hasDisplayLogic = $this->hasMethod('getDisplayLogicCriteria'); $isReadonly = $this->isReadonly(); // Loop over all fields (dataFields) INCLUDING CompositeField types $stack = $fields->toArray(); while ($stack) { $field = array_shift($stack); $name = $this->getUniqueFieldName($field, $record); $field->setName($name); if (!$isReadonly && $hasDisplayLogic) { // Support Display Logic module by Unclecheese $displayLogicCriteria = $field->getDisplayLogicCriteria(); if ($displayLogicCriteria !== null) { $displayLogicFieldName = $displayLogicCriteria->getMaster(); $displayLogicFieldName = $this->getUniqueFieldName($displayLogicFieldName, $record); $displayLogicCriteria->setMaster($displayLogicFieldName); } } if ($field->isComposite()) { $stack = array_merge($field->getChildren()->toArray(), $stack); } } if ($isReadonly) { foreach ($fields as $field) { $fields->replaceField($field->getName(), $field = $field->performReadonlyTransformation()); } } return $this; } /** * @param string|FormField $field * @param DataObject $record * @return string */ public function getUniqueFieldName($fieldOrFieldname, $record) { $name = $fieldOrFieldname instanceof FormField ? $fieldOrFieldname->getName() : $fieldOrFieldname; $recordID = $this->getFieldID($record); return sprintf( '%s__%s__%s__%s__%s', $this->getName(), 'MultiRecordField', $record->ClassName, $recordID, $name ); } private static $_new_records_to_write = null; private static $_existing_records_to_write = null; private static $_records_to_delete = null; public function saveInto(\DataObjectInterface $record) { if ($this->depth == 1) { // Reset records to write for top-level MultiRecordField. self::$_new_records_to_write = array(); self::$_existing_records_to_write = array(); self::$_records_to_delete = array(); } $class_id_field = $this->Value(); if (!$class_id_field) { return $this; } $list = $this->list; // Workaround for #5775 - Fix bug where ListboxField writes to $record, making // UnsavedRelationList redundant. // https://github.com/silverstripe/silverstripe-framework/pull/5775 $relationName = $this->getName(); $relation = ($record->hasMethod($relationName)) ? $record->$relationName() : null; if ($relation) { // When ListboxField (or other) has saved a new record in its 'saveInto' function if ($record->ID && $list instanceof UnsavedRelationList) { if ($this->config()->enable_patch_5775 === false) { throw new Exception("ListboxField (in SS 3.4 or lower) or another FormField called DataObject::write() when it wasn't meant to on your unsaved record. https://github.com/silverstripe/silverstripe-framework/pull/5775 ---- Enable 'enable_patch_5775' in your config YML against ".__CLASS__." to enable a workaround."); } if ($relation instanceof ElementalArea) { // Hack to support Elemental $relation = $relation->Elements(); } else if ($relation instanceof DataObject) { throw new Exception("Unable to use enable_patch_5775 workaround as \"".$record->class."\"::\"".$relationName."\"() does not return a DataList."); } $list = $relation; } } $flatList = array(); if ($list instanceof DataList) { $flatList = array(); foreach ($list as $r) { $flatList[$r->ID] = $r; } } else if (!$list instanceof UnsavedRelationList) { throw new Exception('Expected SS_List, but got "'.$list->class.'" in '.__CLASS__); } $sortFieldName = $this->getSortFieldName(); foreach ($class_id_field as $class => $id_field) { // Create and add records to list foreach ($id_field as $idString => $subRecordData) { if (strpos($idString, 'o-multirecordediting') !== FALSE) { throw new Exception('Invalid template ID passed in ("'.$idString.'"). This should have been replaced by MultiRecordField.js. Is your JavaScript broken?'); } $idParts = explode('_', $idString); $id = 0; $subRecord = null; if ($idParts[0] === 'new') { if (!isset($idParts[1])) { throw new Exception('Missing ID part of "new_" identifier.'); } $id = (int)$idParts[1]; if (!$id && $id > 0) { throw new Exception('Invalid ID part of "new_" identifier. Positive Non-Zero Integers only are accepted.'); } // New record $subRecord = $class::create(); } else { $id = $idParts[0]; // Find existing $id = (int)$id; if (!isset($flatList[$id])) { throw new Exception('Record #'.$id.' on "'.$class.'" does not exist in this DataList context. (From ID string: '.$idString.')'); } $subRecord = $flatList[$id]; } // Detect if record was deleted if (isset($subRecordData['multirecordfield_delete']) && $subRecordData['multirecordfield_delete']) { if ($subRecord && $subRecord->exists()) { self::$_records_to_delete[] = $subRecord; } continue; } // maybetodo(Jake): To improve performance, maybe add 'dumb fields' config where it just gets the fields available // on an unsaved record and just re-uses them for each instance. Of course // this means conditional fields based on parent values/db values wont work. $fields = $this->getRecordDataFields($subRecord); $fields = $fields->dataFields(); if (!$fields) { throw new Exception($class.' is returning 0 fields.'); } // Note(Stephen) 2018-05-28: Checkboxes don't return a value // when unchecked. We assume any unset CheckboxField has been // unchecked and set the field to null. foreach ($fields as $field) { if ($field instanceof CheckboxField || $field instanceof CheckboxFieldSet) { if (!isset($subRecordData[$field->Name])) { $field->setValue(null); $field->saveInto($subRecord); $field->MultiRecordField_SavedInto = true; } } } // foreach ($subRecordData as $fieldName => $fieldData) { if ($sortFieldName !== $fieldName && !isset($fields[$fieldName]) && strpos($fieldName, '_ClassName') == false) { // todo(Jake): Say whether its missing the field from getCMSFields or getMultiRecordFields or etc. throw new Exception('Missing field "'.$fieldName.'" from "'.$subRecord->class.'" fields based on data sent from client. (Could be a hack attempt)'); } if(isset($fields[$fieldName])) { $field = $fields[$fieldName]; if (!$field instanceof MultiRecordField) { $value = $fieldData->value; } else { $value = $fieldData; } if($field) { // NOTE(Jake): Added for FileAttachmentField as it uses the name used in the request for // file deletion. $field->MultiRecordEditing_Name = $this->getUniqueFieldName($field->getName(), $subRecord); $field->setValue($value); // todo(Jake): Some field types (ie. UploadField/FileAttachmentField) directly modify the record // on 'saveInto', meaning people -could- circumvent certain permission checks // potentially. Must test this or defer extensions of 'FileField' to 'saveInto' later. $field->saveInto($subRecord); $field->MultiRecordField_SavedInto = true; } } } if ($sortFieldName) { // Handle sort if its not manually handled on the form if (!isset($fields[$sortFieldName])) { $newSortValue = $id; // Default to order added if (isset($subRecordData[$sortFieldName])) { $newSortValue = $subRecordData[$sortFieldName]; } if ($newSortValue) { $subRecord->{$sortFieldName} = $newSortValue; } } // Check if sort value is invalid $sortValue = $subRecord->{$sortFieldName}; if ($sortValue <= 0) { throw new Exception('Invalid sort value ('.$sortValue.') on #'.$subRecord->ID.' for class '.$subRecord->class.'. Sort value must be greater than 0.'); } } if (!$subRecord->doValidate()) { throw new ValidationException('Failed validation on '.$subRecord->class.'::doValidate() on record #'.$subRecord->ID); } if ($subRecord->exists()) { self::$_existing_records_to_write[] = $subRecord; } else { // NOTE(Jake): I used to directly add the record to the list here, but // if it's a HasManyList/ManyManyList, it will create the record // before doing permission checks. self::$_new_records_to_write[] = array( self::NEW_RECORD => $subRecord, self::NEW_LIST => $list, ); } } } // The top-most MutliRecordField handles all the permission checking/saving at once if ($this->depth == 1) { // Remove records from list that haven't been changed to avoid unnecessary // permission check and ->write overhead foreach (self::$_existing_records_to_write as $i => $subRecord) { $hasRecordChanged = false; $changedFields = $subRecord->getChangedFields(true); foreach ($changedFields as $field => $data) { $hasRecordChanged = $hasRecordChanged || ($data['before'] != $data['after']); } if (!$hasRecordChanged) { // Remove from list, stops the record from calling ->write() unset(self::$_existing_records_to_write[$i]); } } // // Check permissions on everything at once // (includes records added in nested-nested-nested-etc MultiRecordField's) // $currentMember = Member::currentUser(); $recordsPermissionUnable = array(); foreach (self::$_new_records_to_write as $subRecordAndList) { $subRecord = $subRecordAndList[self::NEW_RECORD]; // Check each new record to see if you can create them if (!$subRecord->canCreate($currentMember)) { $recordsPermissionUnable['canCreate'][$subRecord->class][$subRecord->ID] = true; } } foreach (self::$_existing_records_to_write as $subRecord) { // Check each existing record to see if you can edit them if (!$subRecord->canEdit($currentMember)) { $recordsPermissionUnable['canEdit'][$subRecord->class][$subRecord->ID] = true; } } foreach (self::$_records_to_delete as $subRecord) { // Check each record deleting to see if you can delete them if (!$subRecord->canDelete($currentMember)) { $recordsPermissionUnable['canDelete'][$subRecord->class][$subRecord->ID] = true; } } if ($recordsPermissionUnable) { /** * Output a nice exception/error message telling you exactly what records/classes * the permissions failed on. * * eg. * Current member #7 does not have permission. * * Unable to "canCreate" records: * - ElementGallery (26) * * Unable to "canEdit" records: * - ElementGallery (24,23,22) * - ElementGallery_Item (16,23,17,18,19,20,22,21) */ $message = ''; foreach ($recordsPermissionUnable as $permissionFunction => $classAndID) { $message .= "\n".'Unable to "'.$permissionFunction.'" records: '."\n"; foreach ($classAndID as $class => $idAsKeys) { $message .= '- '.$class.' ('.implode(',', array_keys($idAsKeys)).')'."\n"; } } throw new Exception('Current member #'.Member::currentUserID().' does not have permission.'."\n".$message); } // Add new records into the appropriate list foreach (self::$_new_records_to_write as $subRecordAndList) { $list = $subRecordAndList[self::NEW_LIST]; if ($list instanceof UnsavedRelationList || $list instanceof RelationList) // ie. HasManyList/ManyManyList { $subRecord = $subRecordAndList[self::NEW_RECORD]; $list->add($subRecord); } else { throw new Exception('Unsupported SS_List type "'.$list->class.'"'); } } // Debugging (for looking at UnsavedRelationList's to ensure $_new_records_to_write is working) // NOTE(Jake): Added to debug Frontend Objects module support //Debug::dump($record); Debug::dump($relation_class_id_field); exit('Exited at: '.__CLASS__.'::'.__FUNCTION__);// Debug raw request information tree // Save existing items foreach (self::$_existing_records_to_write as $subRecord) { // NOTE(Jake): Records are checked above to see if they've been changed. // If they haven't been changed, they're removed from the 'self::$_existing_records_to_write' list. $subRecord->write(); } // Remove deleted items foreach (self::$_records_to_delete as $subRecord) { $subRecord->delete(); } } } /** * @return FieldList */ public function Fields() { return $this->children; } /** * @return FieldList */ public function Actions() { if (!$this->getCanAddInline()) { return new FieldList(); } if ($this->actions) { return $this->actions; } // Setup default actions $this->actions = new FieldList; $this->actions->unshift($inlineAddButton = FormAction::create('AddInlineRecord', 'Add') ->addExtraClass('multirecordfield-addinlinebutton js-multirecordfield-add-inline') ->setAttribute('autocomplete', 'off') ->setUseButtonTag(true)); $this->actions->unshift($classField = DropdownField::create('ClassName', ' ') ->addExtraClass('multirecordfield-classname js-multirecordfield-classname') ->setAttribute('autocomplete', 'off') ->setEmptyString('(Select section type to create)')); $inlineAddButton->addExtraClass('ss-ui-action-constructive ss-ui-button'); $inlineAddButton->setAttribute('data-icon', 'add'); // Update FormAction fields with button classes $this->applyButtonClasses($this->actions); // NOTE(Jake): To add and test later perhaps? If necessary. //$this->extend('updateActions', $this->actions); return $this->actions; } /** * Returns a read-only version of this field. * * @return MultiRecordField_Readonly */ public function performReadonlyTransformation() { $resultField = MultiRecordField_Readonly::create($this->name, $this->title, $this->list); foreach (get_object_vars($this) as $property => $value) { $resultField->$property = $value; } $resultField->readonly = true; return $resultField; } /** * @return FieldList */ public function getChildren() { return $this->children; } /** * @return string */ public function getSortFieldName() { if ($this->sortFieldName || ($this->sortFieldName === '' || $this->sortFieldName === false)) { return $this->sortFieldName; } $modelClasses = $this->getModelClassesOrThrowExceptionIfEmpty(); $class = key($modelClasses); $baseClass = ClassInfo::baseDataClass($class); $sort = Config::inst()->get($baseClass, 'default_sort'); if (!$sort) { return null; } $sort = static::sort_string_to_array($sort); $sort = key($sort); $sort = str_replace(array('"', '\'', '`'), '', $sort); // Strip quotes as core and some modules use them for 'default_sort' if (strpos($sort, '.') !== FALSE) { throw new Exception('Cannot use relational sort field with '.__CLASS__.', default_sort config for "'.$baseClass.'" is incompatible.'); } return $sort; } /** * Set what field to use for sorting the records. * * @param string $name Set to false or empty string to disable sort explicitly. * @return MultiRecordField */ public function setSortFieldName($name) { $this->sortFieldName = $name; return $this; } /** * Parse sort string into an array of sorts * * @return array */ protected static function sort_string_to_array($clauses) { // // NOTE(Jake): The below code is essentially copy-pasted from SQLSelect::addOrderBy. // if(is_string($clauses)) { if(strpos($clauses, '(') !== false) { $sort = preg_split("/,(?![^()]*+\\))/", $clauses); } else { $sort = explode(',', $clauses); } $clauses = array(); $direction = null; foreach($sort as $clause) { list($column, $direction) = static::get_direction_from_string($clause, $direction); $clauses[$column] = $direction; } } $orderby = array(); if(is_array($clauses)) { foreach($clauses as $key => $value) { if(!is_numeric($key)) { $column = trim($key); $columnDir = strtoupper(trim($value)); } else { list($column, $columnDir) = static::get_direction_from_string($value); } $orderby[$column] = $columnDir; } } else { user_error(__CLASS__.'::'.__FUNCTION__.'() incorrect format for default_sort', E_USER_WARNING); } return $orderby; } /** * @return array */ protected static function get_direction_from_string($value, $defaultDirection = null) { // // NOTE(Jake): The below code is essentially copy-pasted from SQLSelect::getDirectionFromString. // if(preg_match('/^(.*)(asc|desc)$/i', $value, $matches)) { $column = trim($matches[1]); $direction = strtoupper($matches[2]); } else { $column = $value; $direction = $defaultDirection ? $defaultDirection : "ASC"; } return array($column, $direction); } /** * Prepares everything just before rendering the field */ protected function prepareForRender() { if (!$this->preparedForRender) { $this->preparedForRender = true; if (!$this->isReadonly() && $this->depth == 1) { // NOTE(Jake): jQuery.ondemand is required to allow FormField classes to add their own // Requirements::javascript on-the-fly. //Requirements::javascript(FRAMEWORK_DIR . "/thirdparty/jquery/jquery.js"); Requirements::css(MULTIRECORDEDITOR_DIR.'/css/MultiRecordField.css'); if (is_subclass_of(Controller::curr(), 'LeftAndMain')) { // NOTE(Jake): Only include in CMS to fix margin issues. Not in the main CSS file // so that the frontend CSS is less in the way. Requirements::css(MULTIRECORDEDITOR_DIR.'/css/MultiRecordFieldCMS.css'); } Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css'); Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery-ui/jquery-ui.js'); Requirements::javascript(FRAMEWORK_DIR . '/javascript/jquery-ondemand/jquery.ondemand.js'); Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js'); Requirements::javascript(MULTIRECORDEDITOR_DIR.'/javascript/MultiRecordField.js'); // If config is set to 'default' but 'default' isn't configured, fallback to 'cms'. // NOTE(Jake): In SS 3.2, 'default' is the default active config but its not configured. $availableConfigs = HtmlEditorConfig::get_available_configs_map(); $activeIdentifier = HtmlEditorConfig::get_active_identifier(); if ($activeIdentifier === 'default' && !isset($availableConfigs[$activeIdentifier])) { HtmlEditorConfig::set_active('cms'); } } // // Setup actions // $actions = $this->Actions(); if ($actions && $actions->count()) { $modelClasses = $this->getModelClassesOrThrowExceptionIfEmpty(); $modelFirstClass = key($modelClasses); $inlineAddButton = $actions->dataFieldByName('action_AddInlineRecord'); if ($inlineAddButton) { // Setup default inline field data attributes //$inlineAddButton->setAttribute('data-name', $this->getName()); $inlineAddButton->setAttribute('data-action', $this->getName()); $inlineAddButton->setAttribute('data-class', $modelFirstClass); $inlineAddButton->setAttribute('data-depth', $this->depth); // Automatically apply all data attributes on this element, to the inline button. foreach ($this->getAttributes() as $name => $value) { if (substr($name, 0, 5) === 'data-') { $inlineAddButton->setAttribute($name, $value); } } if (count($modelClasses) == 1) { $name = singleton($modelFirstClass)->i18n_singular_name(); $inlineAddButton->setTitle('Add '.$name); } } $classField = $actions->dataFieldByName('ClassName'); if ($classField) { if (count($modelClasses) > 1) { if ($inlineAddButton) { $inlineAddButton->setDisabled(true); } $classField->setSource($modelClasses); } else { $actions->removeByName('ClassName'); } } // Allow outside sources to influences the disable state class-wise if ($inlineAddButton && $inlineAddButton->isDisabled()) { $inlineAddButton->addExtraClass('is-disabled'); } // foreach ($actions as $actionField) { // Expand out names $actionField->setName($this->getName().'_'.$actionField->getName()); } } // Get existing records to add fields for $recordArray = array(); if ($this->list && !$this->list instanceof UnsavedRelationList) { foreach ($this->list->toArray() as $record) { $recordArray[$record->ID] = $record; } } // // If the user validation failed, Value() will be populated with some records // that have 'new_' IDs, so handle them. // $value = $this->Value(); if ($value && is_array($value)) { foreach ($value as $class => $recordDatas) { foreach ($recordDatas as $new_id => $fieldData) { if (substr($new_id, 0, 4) === 'new_') { $record = $class::create(); $record->MultiRecordField_NewID = $new_id; $recordArray[$new_id] = $record; } else if ($new_id == (string)(int)$new_id) { // NOTE(Jake): "o-multirecordediting-1-id" == 0 // evaluates true in PHP 5.5.12, // So we need to make it a string again to avoid that dumb case. $new_id = (int)$new_id; if (!isset($recordArray[$new_id])) { throw new Exception('Record #'.$new_id.' does not exist in this context.'); } $record = $recordArray[$new_id]; //throw new Exception('todo, handle existing stuff that fails validation. ('.$new_id.')'); } else { throw new Exception('Validation failed and unable to restore fields with invalid ID. ('.$new_id.')'); } // Update new/existing record with data foreach ($fieldData as $fieldName => $fieldInfo) { if (is_array($fieldInfo)) { $record->$fieldName = $fieldInfo; } else { $record->$fieldName = $fieldInfo->value; } } } } } // Transform into list $recordList = new ArrayList($recordArray); // Ensure all the records are sorted by the sort field $sortFieldName = $this->getSortFieldName(); if ($sortFieldName) { $recordList = $recordList->sort($sortFieldName); } // // Return all fields from the records editing // foreach ($recordList as $record) { $recordFields = $this->getRecordDataFields($record); $this->applyUniqueFieldNames($recordFields, $record); foreach ($recordFields as $field) { $this->children->push($field); } } } } public function FieldHolder($properties = array()) { $this->prepareForRender(); $this->addExtraClass($this->Type().'_holder'); return parent::FieldHolder($properties); } public function Field($properties = array()) { $this->prepareForRender(); $this->removeExtraClass($this->Type().'_holder'); $this->addExtraClass($this->Type().'_field'); return parent::Field($properties); } } class MultiRecordField_Readonly extends MultiRecordField { protected $readonly = true; public function handleRequest(SS_HTTPRequest $request, DataModel $model) { return null; } /** * @return FieldList */ public function Actions() { return new FieldList(); } } |