array( 'label' => 'Show', 'icon' => 'framework/images/show.png', 'icon_disabled' => 'framework/images/show_disabled.png', 'class' => 'popuplink showlink', ), 'edit' => array( 'label' => 'Edit', 'icon' => 'framework/images/edit.gif', 'icon_disabled' => 'framework/images/edit_disabled.gif', 'class' => 'popuplink editlink', ), 'delete' => array( 'label' => 'Delete', 'icon' => 'framework/images/delete.gif', 'icon_disabled' => 'framework/images/delete_disabled.gif', 'class' => 'popuplink deletelink', ), ); static $url_handlers = array( 'item/$ID' => 'handleItem', '$Action!' => '$Action', ); public function handleItem($request) { return new ComplexTableField_ItemRequest($this, $request->param('ID')); } public function getViewer() { return new SSViewer($this->template); } public function setPopupSize($width, $height) { $width = (int)$width; $height = (int)$height; if($width < 0 || $height < 0) { user_error("setPopupSize expects non-negative arguments.", E_USER_WARNING); return; } $this->popupWidth = $width; $this->popupHeight = $height; } public function PopupWidth() { return $this->popupWidth; } public function PopupHeight() { return $this->popupHeight; } /** * See class comments * * @param Controller $controller * @param string $name * @param string $sourceClass * @param array $fieldList * @param FieldList $detailFormFields * @param string $sourceFilter * @param string $sourceSort * @param string $sourceJoin */ public function __construct($controller, $name, $sourceClass, $fieldList = null, $detailFormFields = null, $sourceFilter = "", $sourceSort = "", $sourceJoin = "") { $this->detailFormFields = $detailFormFields; $this->controller = $controller; $this->pageSize = 10; parent::__construct($name, $sourceClass, $fieldList, $sourceFilter, $sourceSort, $sourceJoin); } public function isComposite() { return false; } /** * @return String */ public function FieldHolder($properties = array()) { Requirements::javascript(THIRDPARTY_DIR . "/prototype/prototype.js"); Requirements::javascript(THIRDPARTY_DIR . "/behaviour/behaviour.js"); Requirements::javascript(THIRDPARTY_DIR . "/greybox/AmiJS.js"); Requirements::javascript(THIRDPARTY_DIR . "/greybox/greybox.js"); Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/javascript/lang'); Requirements::javascript(FRAMEWORK_DIR . '/javascript/TableListField.js'); Requirements::javascript(FRAMEWORK_DIR . "/javascript/ComplexTableField.js"); Requirements::css(THIRDPARTY_DIR . "/greybox/greybox.css"); Requirements::css(FRAMEWORK_DIR . "/css/TableListField.css"); Requirements::css(FRAMEWORK_DIR . "/css/ComplexTableField.css"); // set caption if required if($this->popupCaption) { $id = $this->id(); if(Director::is_ajax()) { $js = <<sourceItems(); return $this->renderWith($this->template); } /** * @return SS_List */ public function Items() { $sourceItems = $this->sourceItems(); if(!$sourceItems) { return null; } if(isset($_REQUEST['ctf'][$this->getName()]['start'])) { $pageStart = $_REQUEST['ctf'][$this->getName()]['start']; if(!is_numeric($pageStart)) $pageStart = 0; } else { $pageStart = 0; } $output = new ArrayList(); foreach($sourceItems as $pageIndex=>$item) { $output->push(Object::create($this->itemClass,$item, $this, $pageStart+$pageIndex)); } return $output; } /** * Sets the popup-title by javascript. Make sure to use FormResponse in ajax-requests, * otherwise the title-change will only take effect on items existing during page-load. * * @param $caption String */ public function setPopupCaption($caption) { $this->popupCaption = Convert::raw2js($caption); } /** * @param $validator Validator */ public function setDetailFormValidator( Validator $validator ) { $this->detailFormValidator = $validator; } public function setAddTitle($addTitle) { if(is_string($addTitle)) $this->addTitle = $addTitle; } public function Title() { return $this->addTitle ? $this->addTitle : parent::Title(); } /** * Calculates the number of columns needed for colspans * used in template * * @return Int */ public function ItemCount() { return count($this->fieldList); } /** * Used to toggle paging (makes no sense when adding a record) * * @return Boolean */ public function IsAddMode() { return ($this->methodName == "add" || $this->request->param('Action') == 'AddForm'); } public function sourceID() { $idField = $this->form->Fields()->dataFieldByName('ID'); // disabled as it conflicts with scaffolded formfields, and not strictly necessary // if(!$idField) user_error("ComplexTableField needs a formfield named 'ID' to be present", E_USER_ERROR); // because action_callfieldmethod never actually loads data into the form, // we can't rely on $idField being populated, and fall back to the request-params. // this is a workaround for a bug where each subsequent popup-call didn't have ID // of the parent set, and so didn't properly save the relation return ($idField) ? $idField->Value() : (isset($_REQUEST['ctf']['ID']) ? $_REQUEST['ctf']['ID'] : null); } public function AddLink() { return Controller::join_links($this->Link(), 'add'); } /** * @return FieldList */ public function createFieldList() { $fieldset = new FieldList(); foreach($this->fieldTypes as $key => $fieldType){ $fieldset->push(new $fieldType($key)); } return $fieldset; } public function setController($controller) { $this->controller = $controller; return $this; } public function setTemplatePopup($template) { $this->templatePopup = $template; return $this; } //////////////////////////////////////////////////////////////////////////////////////////////////// /** * Return the object-specific fields for the given record, to be shown in the detail pop-up * * This won't include all the CTF-specific 'plumbing; this method is called by self::getFieldsFor() * and the result is then processed further to get the actual FieldList for the form. * * The default implementation of this processes the value of $this->detailFormFields; consequently, if you want to * set the value of the fields to something that $this->detailFormFields doesn't allow, you can do so by overloading * this method. */ public function getCustomFieldsFor($childData) { if($this->detailFormFields instanceof FieldList) { return $this->detailFormFields; } $fieldsMethod = $this->detailFormFields; if(!is_string($fieldsMethod)) { $this->detailFormFields = 'getCMSFields'; $fieldsMethod = 'getCMSFields'; } if(!$childData->hasMethod($fieldsMethod)) { $fieldsMethod = 'getCMSFields'; } return $childData->$fieldsMethod(); } public function getFieldsFor($childData) { $detailFields = $this->getCustomFieldsFor($childData); // the ID field confuses the Controller-logic in finding the right view for ReferencedField $detailFields->removeByName('ID'); // only add childID if we're not adding a record if($childData->ID) { $detailFields->push(new HiddenField('ctf[childID]', '', $childData->ID)); } /* TODO: Figure out how to implement this if($this->getParentClass()) { $detailFields->push(new HiddenField('ctf[parentClass]', '', $this->getParentClass())); // Hack for model admin: model admin will have included a dropdown for the relation itself $parentIdName = $this->getParentIdName($this->getParentClass(), $this->sourceClass()); if($parentIdName) { $detailFields->removeByName($parentIdName); $detailFields->push(new HiddenField($parentIdName, '', $this->sourceID())); } } */ return $detailFields; } public function getValidatorFor($childData) { // if no custom validator is set, and there's on present on the object (e.g. Member), use it if(!isset($this->detailFormValidator) && $childData->hasMethod('getValidator')) { $this->detailFormValidator = $childData->getValidator(); } return $this->detailFormValidator; } //////////////////////////////////////////////////////////////////////////////////////////////////// public function add() { if(!$this->can('add')) return; return $this->customise(array( 'DetailForm' => $this->AddForm(), ))->renderWith($this->templatePopup); } public function AddForm($childID = null) { $className = $this->sourceClass(); $childData = new $className(); $fields = $this->getFieldsFor($childData); $validator = $this->getValidatorFor($childData); $form = new $this->popupClass( $this, 'AddForm', $fields, $validator, false, $childData ); $form->loadDataFrom($childData); return $form; } /** * @deprecated 3.0 */ public function setRelationAutoSetting($value) { Deprecation::notice('3.0', 'Manipulate the DataList instead.'); return $this; } /** * Use the URL-Parameter "action_saveComplexTableField" * to provide a clue to the main controller if the main form has to be rendered, * even if there is no action relevant for the main controller (to provide the instance of ComplexTableField * which in turn saves the record. * * This is for adding new item records. {@link ComplexTableField_ItemRequest::saveComplexTableField()} * * @see Form::ReferencedField */ public function saveComplexTableField($data, $form, $params) { $className = $this->sourceClass(); $childData = new $className(); $form->saveInto($childData); try { $childData->write(); } catch(ValidationException $e) { $form->sessionMessage($e->getResult()->message(), 'bad'); return Controller::curr()->redirectBack(); } // Save this item into the given relationship $this->getDataList()->add($childData); $referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null; $closeLink = sprintf( '(%s)', $referrer, _t('ComplexTableField.CLOSEPOPUP', 'Close Popup') ); $editLink = Controller::join_links($this->Link(), 'item/' . $childData->ID . '/edit'); $message = _t( 'ComplexTableField.SUCCESSADD2', 'Added {name}', array('name' => $childData->singular_name()) ); $message .= '' . $childData->Title . '' . $closeLink; $form->sessionMessage($message, 'good'); return Controller::curr()->redirectBack(); } } /** * @todo Tie this into ComplexTableField_Item better. * @package forms * @subpackage fields-relational */ class ComplexTableField_ItemRequest extends TableListField_ItemRequest { protected $ctf; protected $itemID; protected $methodName; static $url_handlers = array( '$Action!' => '$Action', '' => 'index', ); public function Link($action = null) { return Controller::join_links($this->ctf->Link(), '/item/', $this->itemID, $action); } public function index() { return $this->show(); } /** * Just a hook, processed in {DetailForm()} * * @return String */ public function show() { if($this->ctf->Can('show') !== true) { return false; } $this->methodName = "show"; return $this->renderWith($this->ctf->templatePopup); } /** * Returns a 1-element data object set that can be used for pagination. */ /* this doesn't actually work :-( public function Paginator() { $paginatingSet = new ArrayList(array($this->dataObj())); $start = isset($_REQUEST['ctf']['start']) ? $_REQUEST['ctf']['start'] : 0; $paginatingSet->setPageLimits($start, 1, $this->ctf->TotalCount()); return $paginatingSet; } */ /** * Just a hook, processed in {DetailForm()} * * @return String */ public function edit() { if($this->ctf->Can('edit') !== true) { return false; } $this->methodName = "edit"; return $this->renderWith($this->ctf->templatePopup); } public function delete($request) { // Protect against CSRF on destructive action $token = $this->ctf->getForm()->getSecurityToken(); if(!$token->checkRequest($request)) return $this->httpError(400); if($this->ctf->Can('delete') !== true) { return false; } $this->ctf->getDataList()->removeByID($this->itemID); } /////////////////////////////////////////////////////////////////////////////////////////////////// /** * Return the data object being manipulated */ public function dataObj() { // used to discover fields if requested and for population of field if(is_numeric($this->itemID)) { // we have to use the basedataclass, otherwise we might exclude other subclasses return DataObject::get_by_id( ClassInfo::baseDataClass(Object::getCustomClass($this->ctf->sourceClass())), $this->itemID); } } /** * Renders view, edit and add, depending on the given information. * The form needs several parameters to function independently of its "parent-form", some derived from the context * into a hidden-field, some derived from the parent context (which is not accessible here) and delivered by * GET:ID, Identifier of the currently edited record (only if record is loaded). * , Link back to the correct parent record (e.g. "parentID"). * parentClass, Link back to correct container-class (the parent-record might have many 'has-one'-relationships) * CAUTION: "ID" in the DetailForm would be the "childID" in the overview table. * * @param int $childID */ public function DetailForm($childID = null) { $childData = $this->dataObj(); $fields = $this->ctf->getFieldsFor($childData); $validator = $this->ctf->getValidatorFor($childData); $readonly = ($this->methodName == "show"); $form = new $this->ctf->popupClass( $this, "DetailForm", $fields, $validator, $readonly, $childData ); // Don't use ComplexTableField_Popup.ss $form->setTemplate('Form'); $form->loadDataFrom($childData); if ($readonly) $form->makeReadonly(); return $form; } /** * Use the URL-Parameter "action_saveComplexTableField" * to provide a clue to the main controller if the main form has to be rendered, * even if there is no action relevant for the main controller (to provide the instance of ComplexTableField * which in turn saves the record. * * This is for editing existing item records. {@link ComplexTableField::saveComplexTableField()} * * @see Form::ReferencedField */ public function saveComplexTableField($data, $form, $request) { $dataObject = $this->dataObj(); try { $form->saveInto($dataObject); $dataObject->write(); } catch(ValidationException $e) { $form->sessionMessage($e->getResult()->message(), 'bad'); return Controller::curr()->redirectBack(); } // Save this item into the given relationship $this->ctf->getDataList()->add($dataObject); $referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null; $closeLink = sprintf( '(%s)', $referrer, _t('ComplexTableField.CLOSEPOPUP', 'Close Popup') ); $message = sprintf( _t('ComplexTableField.SUCCESSEDIT', 'Saved %s %s %s'), $dataObject->singular_name(), '"' . htmlspecialchars($dataObject->Title, ENT_QUOTES) . '"', $closeLink ); $form->sessionMessage($message, 'good'); return Controller::curr()->redirectBack(); } public function PopupCurrentItem() { return $_REQUEST['ctf']['start']+1; } public function PopupFirstLink() { $this->ctf->LinkToItem(); if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == 0) { return null; } $start = 0; return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}"); } public function PopupLastLink() { if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == $this->TotalCount()-1) { return null; } $start = $this->TotalCount - 1; return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}"); } public function PopupNextLink() { if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == $this->TotalCount()-1) { return null; } $start = $_REQUEST['ctf']['start'] + 1; return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}"); } public function PopupPrevLink() { if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == 0) { return null; } $start = $_REQUEST['ctf']['start'] - 1; return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}"); } /** * Method handles pagination in asset popup. * * @return Object SS_List */ public function Pagination() { $this->pageSize = 9; $currentItem = $this->PopupCurrentItem(); $result = new ArrayList(); if($currentItem < 6) { $offset = 1; } elseif($this->TotalCount() - $currentItem <= 4) { $offset = $currentItem - (10 - ($this->TotalCount() - $currentItem)); $offset = $offset <= 0 ? 1 : $offset; } else { $offset = $currentItem - 5; } for($i = $offset;$i <= $offset + $this->pageSize && $i <= $this->TotalCount();$i++) { $start = $i - 1; $links['link'] = Controller::join_links($this->Link() . "$this->methodName?ctf[start]={$start}"); $links['number'] = $i; $links['active'] = $i == $currentItem ? false : true; $result->push(new ArrayData($links)); } return $result; } public function ShowPagination() { return false; } /** * ################################# * Utility * ################################# */ /** * Manually overwrites the parent-ID relations. * @see setParentClass() * * @param String $str Example: FamilyID (when one Individual has_one Family) */ public function setParentIdName($str) { throw new Exception("setParentIdName is no longer necessary"); } public function setTemplatePopup($template) { $this->templatePopup = $template; } } /** * Single row of a {@link ComplexTableField}. * @package forms * @subpackage fields-relational */ class ComplexTableField_Item extends TableListField_Item { public function Link($action = null) { return Controller::join_links($this->parent->Link(), '/item/', $this->item->ID, $action); } public function EditLink() { return Controller::join_links($this->Link(), "edit"); } public function ShowLink() { return Controller::join_links($this->Link(), "show"); } public function DeleteLink() { return Controller::join_links($this->Link(), "delete"); } /** * @param String $action * @return boolean */ public function IsDefaultAction($action) { return ($action == $this->parent->defaultAction); } } /** * ComplexTablefield_popup is rendered with a lightbox and can load a more * detailed view of the source class your presenting. * You can customise the fields and requirements as well as any * permissions you might need. * @package forms * @subpackage fields-relational */ class ComplexTableField_Popup extends Form { protected $sourceClass; protected $dataObject; public function __construct($controller, $name, $fields, $validator, $readonly, $dataObject) { $this->dataObject = $dataObject; $actions = new FieldList(); if(!$readonly) { $actions->push( FormAction::create( "saveComplexTableField", _t('CMSMain.SAVE', 'Save') ) ->addExtraClass('save ss-ui-action-constructive') ->setUseButtonTag(true) ->setAttribute('data-icon', 'accept') ); } parent::__construct($controller, $name, $fields, $actions, $validator); if(!$this->dataObject->canEdit()) $this->makeReadonly(); } public function forTemplate() { $ret = parent::forTemplate(); Requirements::css(FRAMEWORK_DIR . '/css/ComplexTableField_popup.css'); Requirements::javascript(FRAMEWORK_DIR . "/thirdparty/prototype/prototype.js"); Requirements::javascript(FRAMEWORK_DIR . "/thirdparty/behaviour/behaviour.js"); Requirements::javascript(FRAMEWORK_DIR . "/thirdparty/scriptaculous/scriptaculous.js"); Requirements::javascript(FRAMEWORK_DIR . "/thirdparty/scriptaculous/scriptaculous/controls.js"); Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/javascript/lang'); Requirements::javascript(FRAMEWORK_DIR . "/javascript/ComplexTableField_popup.js"); // Append requirements from instance callbacks $parent = $this->getParentController(); if($parent instanceof ComplexTableField) { $callback = $parent->requirementsForPopupCallback; } else { $callback = $parent->getParentController()->requirementsForPopupCallback; } if($callback) call_user_func($callback, $this); return $ret; } public function getTemplate() { return 'Form'; } /** * @return ComplexTableField_ItemRequest */ public function getParentController() { return $this->controller; } }