array( 'label' => 'Show', 'icon' => 'cms/images/show.png', 'class' => 'popuplink showlink', ), 'edit' => array( 'label' => 'Edit', 'icon' => 'cms/images/edit.gif', 'class' => 'popuplink editlink', ), 'delete' => array( 'label' => 'Delete', 'icon' => 'cms/images/delete.gif', 'class' => 'popuplink deletelink', ), ); static $url_handlers = array( 'item/$ID' => 'handleItem', '$Action!' => '$Action', ); function handleItem($request) { return new ComplexTableField_ItemRequest($this, $request->param('ID')); } function getViewer() { return new SSViewer($this->template); } /** * See class comments * * @param ContentController $controller * @param string $name * @param string $sourceClass * @param array $fieldList * @param FieldSet $detailFormFields * @param string $sourceFilter * @param string $sourceSort * @param string $sourceJoin */ function __construct($controller, $name, $sourceClass, $fieldList = null, $detailFormFields = null, $sourceFilter = "", $sourceSort = "", $sourceJoin = "") { $this->detailFormFields = $detailFormFields; $this->controller = $controller; $this->pageSize = 10; Requirements::javascript(THIRDPARTY_DIR . "/greybox/AmiJS.js"); Requirements::javascript(THIRDPARTY_DIR . "/greybox/greybox.js"); Requirements::javascript(SAPPHIRE_DIR . '/javascript/TableListField.js'); Requirements::javascript(SAPPHIRE_DIR . "/javascript/ComplexTableField.js"); Requirements::css(THIRDPARTY_DIR . "/greybox/greybox.css"); Requirements::css(SAPPHIRE_DIR . "/css/ComplexTableField.css"); parent::__construct($name, $sourceClass, $fieldList, $sourceFilter, $sourceSort, $sourceJoin); } /** * Return the record filter for this table. * It will automatically add a relation filter if relationAutoSetting is true, and it can determine an appropriate * filter. */ function sourceFilter() { $sourceFilter = parent::sourceFilter(); if($this->relationAutoSetting && $this->getParentClass() && ($filterKey = $this->getParentIdName($this->getParentClass(), $this->sourceClass())) && ($filterValue = $this->sourceID()) ) { $newFilter = "`$filterKey` = '" . Convert::raw2sql($filterValue) . "'"; if($sourceFilter && is_array($sourceFilter)) { // Note that the brackets below are taken into account when building this $sourceFilter = implode(") AND (", $sourceFilter); } $sourceFilter = $sourceFilter ? "($sourceFilter) AND ($newFilter)" : $newFilter; } return $sourceFilter; } function isComposite() { return false; } /** * @return String */ function FieldHolder() { // set caption if required if($this->popupCaption) { $id = $this->id(); if(Director::is_ajax()) { $js = <<sourceItems(); return $this->renderWith($this->template); } function sourceClass() { return $this->sourceClass; } /** * @return DataObjectSet */ function Items() { $this->sourceItems = $this->sourceItems(); if(!$this->sourceItems) { return null; } $pageStart = (isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) ? $_REQUEST['ctf'][$this->Name()]['start'] : 0; $this->sourceItems->setPageLimits($pageStart, $this->pageSize, $this->totalCount); $output = new DataObjectSet(); foreach($this->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 */ function setPopupCaption($caption) { $this->popupCaption = Convert::raw2js($caption); } /** * @param $validator Validator */ function setDetailFormValidator( Validator $validator ) { $this->detailFormValidator = $validator; } /** * Returns the content of this formfield without surrounding layout. Triggered by Javascript * to update content after a DetailForm-save-action. * * @deprecated Use the field link itself, instead - Form/fields/TableFieldName * @return String */ function ajax_render() { user_error("Deprecated; access the field's root link instead", E_USER_NOTICE); return $this->renderWith($this->template); } /** * Calculates the number of columns needed for colspans * used in template * * @return Int */ function ItemCount() { return count($this->fieldList); } /** * Used to toggle paging (makes no sense when adding a record) * * @return Boolean */ function IsAddMode() { return ($this->methodName == "add" || $this->request->param('Action') == 'AddForm'); } function sourceID() { $idField = $this->form->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); } function AddLink() { return $this->Link() . '/add'; } /** * @return FieldSet */ function createFieldSet() { $fieldset = new FieldSet(); foreach($this->fieldTypes as $key => $fieldType){ $fieldset->push(new $fieldType($key)); } return $fieldset; } /** * Determines on which relation-class the DetailForm is saved * by looking at the surrounding form-record. * * @return String */ function getParentClass() { if($this->parentClass === false) { // purposely set parent-relation to false return false; } elseif(!empty($this->parentClass)) { return $this->parentClass; } elseif($this->form && $this->form->getRecord()) { return $this->form->getRecord()->ClassName; } } /** * (Optional) Setter for a correct parent-relation-class. * Defaults to the record loaded into the surrounding form as a fallback. * Caution: Please use the classname, not the actual column-name in the database. * * @param $className string */ function setParentClass($className) { $this->parentClass = $className; } /** * Returns the db-fieldname of the currently used has_one-relationship. */ function getParentIdName( $parentClass, $childClass ) { return $this->getParentIdNameRelation( $childClass, $parentClass, 'has_one' ); } /** * Manually overwrites the parent-ID relations. * @see setParentClass() * * @param String $str Example: FamilyID (when one Individual has_one Family) */ function setParentIdName($str) { $this->parentIdName = $str; } /** * Returns the db-fieldname of the currently used relationship. */ function getParentIdNameRelation( $parentClass, $childClass, $relation ){ if($this->parentIdName) return $this->parentIdName; $relations = singleton( $parentClass )->$relation(); $classes = ClassInfo::ancestry( $childClass ); foreach( $relations as $k => $v ) { if( $v == $childClass ) return $k . 'ID'; } return false; } function setTemplatePopup($template) { $this->templatePopup = $template; } //////////////////////////////////////////////////////////////////////////////////////////////////// /** * 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 FieldSet 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. */ function getCustomFieldsFor($childData) { // If the fieldset is passed, use it if(is_a($this->detailFormFields,"Fieldset")) { return $this->detailFormFields; // Else use the formfields returned from the object via a string method call. } else { if(!is_string($this->detailFormFields)) $this->detailFormFields = "getCMSFields"; $functioncall = $this->detailFormFields; if(!$childData->hasMethod($functioncall)) $functioncall = "getCMSFields"; return $childData->$functioncall(); } } function getFieldsFor($childData) { // Add the relation value to related records if(!$childData->ID && $this->getParentClass()) { // make sure the relation-link is existing, even if we just add the sourceClass and didn't save it $parentIDName = $this->getParentIdName( $this->getParentClass(), $this->sourceClass() ); $childData->$parentIDName = $childData->ID; } $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)); } // add a namespaced ID instead thats "converted" by saveComplexTableField() $detailFields->push(new HiddenField("ctf[ClassName]","",$this->sourceClass())); if($this->getParentClass()) { $parentIdName = $this->getParentIdName($this->getParentClass(), $this->sourceClass()); /* if(!$parentIdName) { user_error("ComplexTableField::DetailForm() Cannot automatically determine 'has-one'-relationship to parent class " . $this->ctf->getParentClass() . ", please use setParentClass() to set it manually", E_USER_WARNING); return; } */ if($parentIdName) { // add relational fields $detailFields->push(new HiddenField("ctf[parentClass]"," ",$this->getParentClass())); if( $this->relationAutoSetting ) { // Hack for model admin: model admin will have included a dropdown for the relation itself $detailFields->removeByName($parentIdName); $detailFields->push(new HiddenField("$parentIdName"," ",$this->sourceID())); } } } return $detailFields; } 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; } //////////////////////////////////////////////////////////////////////////////////////////////////// function add() { if(!$this->can('add')) return; return $this->customise(array( 'DetailForm' => $this->AddForm(), ))->renderWith($this->templatePopup); } function AddForm($childID = null) { $className = $this->sourceClass(); $childData = new $className(); $fields = $this->getFieldsFor($childData); $validator = $this->getValidatorFor($childData); $form = Object::create( $this->popupClass, $this, 'AddForm', $fields, $validator, false, $childData ); return $form; } /** * By default, a ComplexTableField will assume that the field name is the name of a has-many relation on the object being * edited. It will identify the foreign key in the object being listed, and filter on that column, as well as auto-setting * that column for newly created records. * * Calling $this->setRelationAutoSetting(false) will disable this functionality. * * @param boolean $value Should the relation auto-setting functionality be enabled? */ function setRelationAutoSetting($value) { $this->relationAutoSetting = $value; } /** * 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. * * @see Form::ReferencedField */ function saveComplexTableField($data, $form, $params) { $className = $this->sourceClass(); $childData = new $className(); $form->saveInto($childData); $childData->write(); $closeLink = sprintf( '(%s)', _t('ComplexTableField.CLOSEPOPUP', 'Close Popup') ); $message = sprintf( _t('ComplexTableField.SUCCESSADD', 'Added %s %s %s'), $childData->singular_name(), '' . $childData->Title . '', $closeLink ); $form->sessionMessage($message, 'good'); Director::redirectBack(); } } /** * @todo Tie this into ComplexTableField_Item better. */ class ComplexTableField_ItemRequest extends RequestHandlingData { protected $ctf; protected $itemID; protected $methodName; static $url_handlers = array( '$Action!' => '$Action', '' => 'index', ); function Link() { return $this->ctf->Link() . '/item/' . $this->itemID; } function __construct($ctf, $itemID) { $this->ctf = $ctf; $this->itemID = $itemID; } function index() { return $this->show(); } /** * Just a hook, processed in {DetailForm()} * * @return String */ function show() { if($this->ctf->Can('show') !== true) { return false; } $this->methodName = "show"; echo $this->renderWith($this->ctf->templatePopup); } /** * Returns a 1-element data object set that can be used for pagination. */ /* this doesn't actually work :-( function Paginator() { $paginatingSet = new DataObjectSet(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 */ function edit() { if($this->ctf->Can('edit') !== true) { return false; } $this->methodName = "edit"; echo $this->renderWith($this->ctf->templatePopup); } function delete() { if($this->ctf->Can('delete') !== true) { return false; } $this->dataObj()->delete(); } /////////////////////////////////////////////////////////////////////////////////////////////////// /** * Return the data object being manipulated */ 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($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 */ function DetailForm($childID = null) { $childData = $this->dataObj(); $fields = $this->ctf->getFieldsFor($childData); $validator = $this->ctf->getValidatorFor($childData); $readonly = ($this->methodName == "show"); $form = Object::create( $this->ctf->popupClass, $this, "DetailForm", $fields, $validator, $readonly, $childData); $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. * * @see Form::ReferencedField */ function saveComplexTableField($data, $form, $request) { $form->saveInto($this->dataObj()); $this->dataObj()->write(); $closeLink = sprintf( '(%s)', _t('ComplexTableField.CLOSEPOPUP', 'Close Popup') ); $message = sprintf( _t('ComplexTableField.SUCCESSEDIT', 'Saved %s %s %s'), $this->dataObj()->singular_name(), '"' . $this->dataObj()->Title . '"', $closeLink ); $form->sessionMessage($message, 'good'); Director::redirectBack(); } function PopupCurrentItem() { return $_REQUEST['ctf']['start']+1; } function PopupFirstLink() { $this->ctf->LinkToItem(); if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == 0) { return null; } $item = $this->unpagedSourceItems->First(); $start = 0; return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}"); } function PopupLastLink() { if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == $this->totalCount-1) { return null; } $item = $this->unpagedSourceItems->Last(); $start = $this->totalCount - 1; return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}"); } function PopupNextLink() { if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == $this->totalCount-1) { return null; } $item = $this->unpagedSourceItems->getIterator()->getOffset($_REQUEST['ctf']['start'] + 1); $start = $_REQUEST['ctf']['start'] + 1; return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}"); } function PopupPrevLink() { if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == 0) { return null; } $item = $this->unpagedSourceItems->getIterator()->getOffset($_REQUEST['ctf']['start'] - 1); $start = $_REQUEST['ctf']['start'] - 1; return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}"); } /** * Method handles pagination in asset popup. * * @return Object DataObjectSet */ function Pagination() { $this->pageSize = 9; $currentItem = $this->PopupCurrentItem(); $result = new DataObjectSet(); 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; $item = $this->unpagedSourceItems->getIterator()->getOffset($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; } function ShowPagination() { return false; } /** * ################################# * Utility * ################################# */ /** * Get part of class ancestry (even if popup is not subclassed it might be styled differently in css) */ function PopupClasses() { global $_ALL_CLASSES; $items = array(); $parents = $_ALL_CLASSES['parents'][$this->class]; foreach($parents as $parent) { if(!in_array($parent,$_ALL_CLASSES['parents']["TableListField"])) $items[] = $parent . "_Popup"; } $items[] = $this->class . "_Popup"; return implode(" ", $items); } /** * Returns the db-fieldname of the currently used has_one-relationship. */ function getParentIdName( $parentClass, $childClass ) { return $this->getParentIdNameRelation( $childClass, $parentClass, 'has_one' ); } /** * Manually overwrites the parent-ID relations. * @see setParentClass() * * @param String $str Example: FamilyID (when one Individual has_one Family) */ function setParentIdName($str) { $this->parentIdName = $str; } /** * Returns the db-fieldname of the currently used relationship. */ function getParentIdNameRelation($parentClass, $childClass, $relation) { if($this->parentIdName) return $this->parentIdName; $relations = singleton($parentClass)->$relation(); $classes = ClassInfo::ancestry($childClass); foreach($relations as $k => $v) { if(array_key_exists($v, $classes)) return $k . 'ID'; } return false; } function setTemplatePopup($template) { $this->templatePopup = $template; } } /** * Single row of a {@link ComplexTableField}. * @package forms * @subpackage fields-relational */ class ComplexTableField_Item extends TableListField_Item { /** * Needed to transfer pagination-status from overview. */ protected $start; function __construct(DataObject $item, ComplexTableField $parent, $start) { $this->start = $start; parent::__construct($item, $parent); } function Link() { return $this->parent->Link() . '/item/' . $this->item->ID; } function EditLink() { return $this->Link() . "/edit"; } function ShowLink() { return $this->Link() . "/show"; } function DeleteLink() { return $this->Link() . "/delete"; } /** * @param String $action * @return boolean */ 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; function __construct($controller, $name, $fields, $validator, $readonly, $dataObject) { $this->dataObject = $dataObject; /** * WARNING: DO NOT CHANGE THE ORDER OF THESE JS FILES * Some have special requirements. */ //Requirements::css(CMS_DIR . '/css/layout.css'); Requirements::css(THIRDPARTY_DIR . '/tabstrip/tabstrip.css'); Requirements::css(SAPPHIRE_DIR . '/css/Form.css'); Requirements::css(SAPPHIRE_DIR . '/css/ComplexTableField_popup.css'); Requirements::css(CMS_DIR . '/css/typography.css'); Requirements::css(CMS_DIR . '/css/cms_right.css'); Requirements::javascript(THIRDPARTY_DIR . "/prototype.js"); Requirements::javascript(THIRDPARTY_DIR . "/behaviour.js"); Requirements::javascript(THIRDPARTY_DIR . "/prototype_improvements.js"); Requirements::javascript(THIRDPARTY_DIR . "/loader.js"); Requirements::javascript(THIRDPARTY_DIR . "/tabstrip/tabstrip.js"); Requirements::javascript(THIRDPARTY_DIR . "/scriptaculous/scriptaculous.js"); Requirements::javascript(THIRDPARTY_DIR . "/scriptaculous/controls.js"); Requirements::javascript(THIRDPARTY_DIR . "/layout_helpers.js"); Requirements::javascript(CMS_DIR . "/javascript/LeftAndMain.js"); Requirements::javascript(CMS_DIR . "/javascript/LeftAndMain_right.js"); Requirements::javascript(SAPPHIRE_DIR . "/javascript/TableField.js"); Requirements::javascript(SAPPHIRE_DIR . "/javascript/ComplexTableField.js"); Requirements::javascript(SAPPHIRE_DIR . "/javascript/ComplexTableField_popup.js"); if($this->dataObject->hasMethod('getRequirementsForPopup')) { $this->dataObject->getRequirementsForPopup(); } $actions = new FieldSet(); if(!$readonly) { $actions->push( $saveAction = new FormAction("saveComplexTableField", "Save") ); $saveAction->addExtraClass('save'); } parent::__construct($controller, $name, $fields, $actions, $validator); } function FieldHolder() { return $this->renderWith('ComplexTableField_Form'); } } ?>