Removing deprecated TableListField and subclasses

These have been moved to a module called "legacytablefields"
located at https://github.com/silverstripe-labs/legacytablefields
This commit is contained in:
Sean Harvey 2012-11-16 11:04:28 +13:00
parent aeef4d6e84
commit 77337ae58c
38 changed files with 0 additions and 9443 deletions

View File

@ -1,6 +0,0 @@
/* table */
.ComplexTableField { margin-bottom: 10px; }
.ComplexTableField tbody td { cursor: pointer; }
.ComplexTableField tbody td.markingcheckbox { cursor: default; }
.ui-dialog .ctf-dialog.ui-dialog-content { padding-right: 0; /* scrollbars */ }

View File

@ -1,30 +0,0 @@
#right form .hasmanyfile a.addFile, #right form .hasmanyfile a.uploadFile, #right form .hasmanyfile .currentFiles li { font-size: 1.2em; padding-left: 3px; }
#right form .hasmanyfile a.uploadFile { border-color: #cccccc #999999 #999999 #cccccc; border-style: solid; border-width: 2px; color: #333333; cursor: pointer; font-size: 11px; font-weight: bold; position: relative; top: -29px; left: 344px; text-decoration: none; overflow: visible; padding: 3px 5px; float: left; width: auto; }
#right form .hasmanyfile a.removeFile { border-color: #cccccc #999999 #999999 #cccccc; border-style: solid; border-width: 2px; color: #333333; cursor: pointer; font-size: 10px; font-weight: bold; text-decoration: none; overflow: visible; padding: 3px 5px; width: auto; margin: -2px 0 0 10px; }
#right form .hasmanyfile a.removeFile:hover { background: #CE0000; color: #fff; }
#right form .hasmanyfile a.addFile { border-color: #cccccc #999999 #999999 #cccccc; border-style: solid; border-width: 2px; color: #333333; cursor: pointer; font-size: 11px; font-weight: bold; position: relative; top: -29px; left: 340px; text-decoration: none; overflow: visible; padding: 3px 5px; float: left; width: auto; }
#right form .hasmanyfile a.addFile:hover, #right form .hasmanyfile a.uploadFile:hover { background: #fff; }
#right form .hasmanyfile ul.currentFiles { padding-bottom: 5px; }
#right form .hasmanyfile .currentFiles li { height: 30px; line-height: 30px; }
#right form .hasmanyfile .clear { clear: both; }
/* ICONS */
#right form .hasmanyfile .currentFiles a[href$=".pdf"], #right form .hasmanyfile .currentFiles a[href$=".PDF"], #right form .hasmanyfile .currentFiles a.pdf { padding: 2px; padding-left: 20px; background: url(../images/icons/page_white_acrobat.png) no-repeat left center; }
#right form .hasmanyfile .currentFiles a[href$=".doc"], #right form .hasmanyfile .currentFiles a[href$=".DOC"], #right form .hasmanyfile .currentFiles a.doc { padding: 2px; padding-left: 20px; background: url(../images/icons/page_word.png) no-repeat left center; }
#right form .hasmanyfile .currentFiles a[href$=".xls"], #right form .hasmanyfile .currentFiles a[href$=".XLS"], #right form .hasmanyfile .currentFiles a.xls { padding: 2px; padding-left: 20px; background: url(../images/icons/page_excel.png) no-repeat left center; }
#right form .hasmanyfile .currentFiles a[href$=".gz"], #right form .hasmanyfile .currentFiles a[href$=".GZ"], #right form .hasmanyfile .currentFiles a[href$=".gzip"], #right form .hasmanyfile .currentFiles a[href$=".GZIP"], #right form .hasmanyfile .currentFiles a[href$=".zip"], #right form .hasmanyfile .currentFiles a[href$=".ZIP"], #right form .hasmanyfile .currentFiles a.archive { padding: 2px; padding-left: 20px; background: url(../images/icons/page_white_zip.png) no-repeat left center; }
#right form .hasmanyfile .currentFiles a[href$=".jpg"], #right form .hasmanyfile .currentFiles a[href$=".JPG"], #right form .hasmanyfile .currentFiles a[href$=".gif"], #right form .hasmanyfile .currentFiles a[href$=".GIF"], #right form .hasmanyfile .currentFiles a[href$=".png"], #right form .hasmanyfile .currentFiles a[href$=".PNG"], #right form .hasmanyfile .currentFiles a.image { padding: 2px; padding-left: 20px; background: url(../images/icons/icon-jpg.gif) no-repeat left center; }
#right form .hasmanyfile .currentFiles a[href$=".exe"], #right form .hasmanyfile .currentFiles a[href$=".EXE"], #right form .hasmanyfile .currentFiles a.application { padding: 2px; padding-left: 20px; background: url(../images/icons/application.png) no-repeat left center; }

View File

@ -1,76 +0,0 @@
table.TableField, table.TableListField, .TableListField table.data, table.CMSList { border-collapse: collapse; border-spacing: 0; width: 100%; }
/* Preventing IE6 from showing double borders */
body > div table.TableField, body > div table.TableListField, body > div .TableListField table.data, body > div table.CMSList { border-collapse: separate; }
table.TableField td, table.TableListField td, .TableListField table.data td, table.CMSList td { border-style: none; }
table.TableField th, table.TableListField th, .TableListField table.data th, table.CMSList th { white-space: nowrap; }
table.TableField thead th, .TableListField table.data thead th, table.CMSList thead th { white-space: nowrap; padding: 3px; font-size: 12px; text-align: left; }
table.TableField thead th span, .TableListField table.data thead th span { font-size: 12px; }
table.TableField thead th span.sortLink, .TableListField table.data thead th span.sortLink, table.CMSList thead th span.sortLink { overflow: hidden; }
table.TableField tbody td, table.TableField tfoot td, .TableListField table.data tbody td, .TableListField table.data tfoot td, table.CMSList tbody td, table.CMSList tfoot td { padding: 2px 4px; }
.TableListField table.data tfoot tr.addtogrouprow td { padding: 3px; }
.TableListField table.data tfoot .actions { float: none; }
.TableListField table.data tfoot tr.addtogrouprow input { width: 94%; }
.TableField td input, .TableListField td input { width: 98%; }
table.data tbody td input, table.data tbody td textarea { border: 0 !important; }
table.TableField tbody td.checkbox, .TableListField table.data tbody td.checkbox, table.CMSList tbody td.checkbox { padding-left: 5px; background-image: url(../images/checkbox.png); background-repeat: repeat-x; background-position: left bottom; }
.TableListField table.data tfoot .addlink img { vertical-align: middle; margin: 3px 6px 3px 3px; }
.TableListField table.data tfoot tr td a { text-decoration: none; }
.TableListField table.data tbody tr td a:hover, .TableListField table.data tfoot tr td a:hover { background: none; }
/** Show a loading indication on a TableListField row */
.TableListField tr.loading td.first { padding-left: 22px; background: url(../images/network-save.gif) 3px 2px no-repeat; }
.right form .TableField span.readonly { border: 0; background: none; padding: 0; margin-bottom: 0; }
.right form .TableListField td { background: #fff; }
.TableListField div.utility { overflow: auto; }
.TableListField div.utility .item { margin-top: 1em; padding: 3px 0 6px 0; display: block; float: left; }
.TableListField div.utility a { text-decoration: none; color: #333; cursor: pointer; font-size: 11px; margin-right: 2px; overflow: visible; padding: 3px 2px; width: auto; }
form .TableField .message { width: auto; }
.TableListField .selectOptions { overflow: auto; font: 1.3em; margin: 0; padding: 0; }
.TableListField .selectOptions li { float: left; margin: 0px 5px; }
.TableListField .PageControls { margin: 5px 0; text-align: center; display: block; margin-bottom: 5px; position: relative; }
.TableListField .PageControls * { display: inline; vertical-align: middle; font-weight: bold; }
.TableListField .PageControls .Last { display: block; width: 40px; text-align: right; position: absolute; right: 0px; top: 0px; }
.TableListField .PageControls .First { float: left; display: block; width: 40px; text-align: left; }
#Pagination { margin-top: 10px; margin-left: auto; margin-right: auto; }
#Pagination a { font-size: 14px; width: 1px; height: 1px; margin: 1px; }
#Pagination span { display: inline; font-size: 14px; }
#Pagination div { display: inline; }
#Pagination_Next a { text-decoration: none; }
#Pagination_Next a div { position: relative; left: -20px; }
#Pagination_Next a img { position: relative; top: -15px; left: 5px; }
#Pagination_Previous a { text-decoration: none; }
#Pagination_Previous a img { position: relative; top: -15px; left: 35px; }

View File

@ -1,883 +0,0 @@
<?php
/**
* Provides a tabuar list in your form with view, edit and add links to edit records
* with a "has-one"-relationship. Detail-views are shown in a greybox-iframe.
* Features pagination in the overview as well as the detail-views.
*
* CAUTION: You need to make sure that the original form-call to the main controller (e.g. EditForm())
* returns a form which includes this field even if no data is loaded,
* to provide a "starting point" for action_callfieldmethod and ReferencedField.
*
* All URL data sent to and from ComplexTableField is encapsulated in $_REQUEST['ctf']
* to avoid side-effects with the main controller.
*
* Example-URL for a "DetailForm"-call explained:
* "/admin/family/?executeForm=EditForm&action_callfieldmethod&fieldName=Individual&childID=7&methodName=edit"
* - executeForm Name of the form on the main rendering page (e.g. "FamilyAdmin")
* - action_callfieldmethod Trigger to call a method of a single field in "EditForm" instead of rendering the
* whole thing
* - fieldName Name of the targeted formField
* - methodName Method on the formfield (e.g. "ComplexTableField")
* - childID Identifier of the database-record (the targeted table is determined by the $sourceClass
* parameter)
*
* @deprecated 3.1 Use GridField with GridFieldConfig_RecordEditor
*
* @todo Control width/height of popup by constructor (hardcoded at the moment)
* @package forms
* @subpackage fields-relational
*/
class ComplexTableField extends TableListField {
/**
* Determines the fields of the detail pop-up form. It can take many forms:
* - A FieldList object: Use that field set directly.
* - A method name, eg, 'getCMSFields': Call that method on the child object to get the fields.
*/
protected $addTitle;
protected $detailFormFields;
protected $viewAction;
/**
* @var Controller
*/
protected $controller;
/**
* @var string Classname of the parent-relation to correctly link new records.
*/
public $parentClass;
/**
* @var string Database column name for the used relation (e.g. FamilyID
* if one Family has_many Individuals).
*/
protected $parentIdName;
/**
* @var array Influence output without having to subclass the template.
*/
protected $permissions = array(
"add",
"edit",
"show",
"delete",
//"export",
);
/**
* Template for main rendering
*
* @var string
*/
protected $template = "ComplexTableField";
/**
* Template for popup (form rendering)
*
* @var string
*/
public $templatePopup = "ComplexTableField_popup";
/**
* Classname for each row/item
*
* @var string
*/
public $itemClass = 'ComplexTableField_Item';
/**
* Classname for the popup form
*
* @var string
*/
public $popupClass = 'ComplexTableField_Popup';
/**
* @var boolean Trigger pagination (defaults to true for ComplexTableField)
*/
protected $showPagination = true;
/**
* @var string Caption the popup will show (defaults to the selected action).
* This is set by javascript and used by greybox.
*/
protected $popupCaption = null;
/**
* @var callback A function callback invoked
* after initializing the popup and its base calls to
* the {@link Requirements} class.
*/
public $requirementsForPopupCallback = null;
/**
* @var $detailFormValidator Validator
*/
protected $detailFormValidator = null;
/**
* Default size for the popup box
*/
protected $popupWidth = 560;
protected $popupHeight = 390;
public $defaultAction = 'show';
public $actions = array(
'show' => 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 = <<<JS
$('$id').GB_Caption = '$this->popupCaption';
JS;
// FormResponse::add($js);
} else {
$js = <<<JS
Event.observe(window, 'load', function() { \$('$id').GB_Caption = '$this->popupCaption'; });
JS;
Requirements::customScript($js);
}
}
// compute sourceItems here instead of Items() to ensure that
// pagination and filters are respected on template accessors
$this->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.1
*/
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(
'<small><a href="%s" onclick="javascript:window.top.GB_hide(); return false;">(%s)</a></small>',
$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 .= '<a href="' . $editLink . '">' . $childData->Title . '</a>' . $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).
* <parentIDName>, 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(
'<small><a href="%s" onclick="javascript:window.top.GB_hide(); return false;">(%s)</a></small>',
$referrer,
_t('ComplexTableField.CLOSEPOPUP', 'Close Popup')
);
$message = sprintf(
_t('ComplexTableField.SUCCESSEDIT', 'Saved %s %s %s'),
$dataObject->singular_name(),
'<a href="' . $this->Link('edit') . '">"' . htmlspecialchars($dataObject->Title, ENT_QUOTES) . '"</a>',
$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;
}
}

View File

@ -1,181 +0,0 @@
<?php
/**
* ComplexTableField designed to edit a has_many join.
*
* This field allows you to show a 1-to-many relation with a group of DataObjects as a (readonly) tabular list. Its
* most useful when you want to manage the relationship itself thanks the **check boxes** present on each line of the
* table.
*
* Moreover, you can not do any mistake anymore in the relation by checking a DataObject already linked with another
* of the parent class.
*
* See {@link ComplexTableField} for more documentation on the base-class.
*
* <b>Usage</b>
*
* <code>
* $tablefield = new HasManyComplexTableField(
* $this,
* 'MyFruits',
* 'Fruit',
* array(
* 'Name' => 'Name',
* 'Color' => 'Color'
* ),
* 'getCMSFields_forPopup'
* );
* </code>
*
* Notice: You still have different ways to customize the popup window as in the parent-class
* {@link ComplexTableField}.
*
* @see http://doc.silverstripe.org/tutorial/5-dataobject-relationship-management
*
* @deprecated 3.1 Use GridField with GridFieldConfig_RelationEditor
*
* @package forms
* @subpackage fields-relational
*/
class HasManyComplexTableField extends ComplexTableField {
public $joinField;
protected $addTitle;
// If you change the value, do not forget to change it also in the JS file
protected $htmlListEndName = 'CheckedList';
// If you change the value, do not forget to change it also in the JS file
protected $htmlListField = 'selected';
public $template = 'RelationComplexTableField';
public $itemClass = 'HasManyComplexTableField_Item';
protected $relationAutoSetting = false;
public function __construct($controller, $name, $sourceClass, $fieldList = null, $detailFormFields = null,
$sourceFilter = "", $sourceSort = "", $sourceJoin = "") {
parent::__construct($controller, $name, $sourceClass, $fieldList, $detailFormFields,
$sourceFilter, $sourceSort, $sourceJoin);
Deprecation::notice('3.0', 'Use GridField with GridFieldConfig_RelationEditor', Deprecation::SCOPE_CLASS);
$this->Markable = true;
if($controllerClass = $this->controllerClass()) {
$this->joinField = $this->getParentIdName($controllerClass, $this->sourceClass);
if(!$this->joinField) {
user_error("Can't find a has_one relationship from '$this->sourceClass' to '$controllerClass'",
E_USER_WARNING);
}
} else {
user_error("Can't figure out the data class of $controller", E_USER_WARNING);
}
}
public function FieldHolder($properties = array()) {
$ret = parent::FieldHolder($properties);
Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/javascript/lang');
Requirements::javascript(FRAMEWORK_DIR . "/javascript/HasManyFileField.js");
Requirements::javascript(FRAMEWORK_DIR . '/javascript/RelationComplexTableField.js');
Requirements::css(FRAMEWORK_DIR . '/css/HasManyFileField.css');
return $ret;
}
/**
* Try to determine the DataObject that this field is built on top of
*/
public function controllerClass() {
if($this->controller instanceof DataObject) return $this->controller->class;
elseif($this->controller instanceof Controller) return $this->controller->data()->class;
}
public function getControllerID() {
return $this->controller->ID;
}
public function saveInto(DataObjectInterface $record) {
$fieldName = $this->name;
$saveDest = $record->$fieldName();
if(!$saveDest) {
user_error("HasManyComplexTableField::saveInto() Field $fieldName not found on $record->class.$record->ID"
, E_USER_ERROR);
}
$items = array();
if($list = $this->value[ $this->htmlListField ]) {
if($list != 'undefined')
$items = explode(',', $list);
}
$saveDest->setByIDList($items);
}
public function setAddTitle($addTitle) {
if(is_string($addTitle))
$this->addTitle = $addTitle;
}
public function Title() {
return $this->addTitle ? $this->addTitle : parent::Title();
}
/**
* Get the IDs of the selected items, in a has_many or many_many relation
*/
public function selectedItemIDs() {
$fieldName = $this->name;
$selectedItems = $this->form->getRecord()->$fieldName();
$itemIDs = array();
foreach($selectedItems as $item) $itemIDs[] = $item->ID;
return $itemIDs;
}
public function ExtraData() {
$items = array();
$list = implode(',', $this->selectedItemIDs());
$inputId = $this->id() . '_' . $this->htmlListEndName;
return <<<HTML
<input id="$inputId" name="{$this->name}[{$this->htmlListField}]" type="hidden" value="$list"/>
HTML;
}
}
/**
* Single record of a {@link HasManyComplexTableField} field.
* @package forms
* @subpackage fields-relational
*/
class HasManyComplexTableField_Item extends ComplexTableField_Item {
public function MarkingCheckbox() {
$name = $this->parent->getName() . '[]';
if(!$this->parent->joinField) {
user_error("joinField not set in HasManyComplexTableField '{$this->parent->name}'", E_USER_WARNING);
return null;
}
$joinVal = $this->item->{$this->parent->joinField};
$parentID = $this->parent->getControllerID();
if($this->parent->IsReadOnly || ($joinVal > 0 && $joinVal != $parentID))
return "<input class=\"checkbox\" type=\"checkbox\" name=\"$name\" value=\"{$this->item->ID}\"
disabled=\"disabled\"/>";
else if($joinVal == $parentID)
return "<input class=\"checkbox\" type=\"checkbox\" name=\"$name\" value=\"{$this->item->ID}\"
checked=\"checked\"/>";
else
return "<input class=\"checkbox\" type=\"checkbox\" name=\"$name\" value=\"{$this->item->ID}\"/>";
}
}

View File

@ -1,109 +0,0 @@
<?php
/**
* ComplexTableField with a radio button column, designed to edit a has_one join.
*
* This [RelationTable](RelationTable) allows you to show a **1-to-1** or **1-to-many** relation with a group of
* DataObjects as a (readonly) tabular list (similiar to [ComplexTableField](ComplexTableField)). Its most useful when
* you want to manage the relationship itself thanks the **radio buttons** present on each line of the table.
*
* Moreover, you have the possibility to uncheck a radio button in order to make the relation as null.
*
* <b>Usage</b>
*
* <code>
* $tablefield = new HasOneComplexTableField(
* $this,
* 'MyOnlyFruit',
* 'Fruit',
* array(
* 'Name' => 'Name',
* 'Color' => 'Color'
* ),
* 'getCMSFields_forPopup'
* );
* </code>
*
* **Notice** : You still have different ways to customize the popup window as in the parent-class
* [ComplexTableField](ComplexTableField).
*
* This field is made to manage a **has_one** relation. In the SilverStripe relation between DataObjects, you can use
* this relation for **1-to-1** and **1-to-many** relations.
* By default, a HasOneComplexTableField manages a **1-to-many** relation. If you want to specify that the relation
* that you manage is a **1-to-1** relation, add this code :
*
* <code>
* $tablefield->setOneToOne();
* </code>
*
* @package forms
* @subpackage fields-relational
*/
class HasOneComplexTableField extends HasManyComplexTableField {
public $itemClass = 'HasOneComplexTableField_Item';
public $isOneToOne = false;
public function getParentIdName($parentClass, $childClass) {
return $this->getParentIdNameRelation($parentClass, $childClass, 'has_one');
}
public function getControllerJoinID() {
return $this->controller->{$this->joinField};
}
public function saveInto(DataObjectInterface $record) {
$fieldName = $this->name;
$fieldNameID = $fieldName . 'ID';
$record->$fieldNameID = 0;
if($val = $this->value[ $this->htmlListField ]) {
if($val != 'undefined')
$record->$fieldNameID = $val;
}
$record->write();
}
public function setOneToOne() {
$this->isOneToOne = true;
}
public function isChildSet($childID) {
return DataObject::get($this->controllerClass(), '"' . $this->joinField . "\" = '$childID'");
}
public function ExtraData() {
$val = $this->getControllerJoinID() ? $this->getControllerJoinID() : '';
$inputId = $this->id() . '_' . $this->htmlListEndName;
return <<<HTML
<input id="$inputId" name="{$this->name}[{$this->htmlListField}]" type="hidden" value="$val"/>
HTML;
}
}
/**
* Single record of a {@link HasOneComplexTableField} field.
* @package forms
* @subpackage fields-relational
*/
class HasOneComplexTableField_Item extends ComplexTableField_Item {
public function MarkingCheckbox() {
$name = $this->parent->getName() . '[]';
$isOneToOne = $this->parent->isOneToOne;
$joinVal = $this->parent->getControllerJoinID();
$childID = $this->item->ID;
if($this->parent->IsReadOnly || ($isOneToOne && $joinVal != $childID && $this->parent->isChildSet($childID)))
return "<input class=\"radio\" type=\"radio\" name=\"$name\" value=\"{$this->item->ID}\"
disabled=\"disabled\"/>";
else if($joinVal == $childID)
return "<input class=\"radio\" type=\"radio\" name=\"$name\" value=\"{$this->item->ID}\"
checked=\"checked\"/>";
else
return "<input class=\"radio\" type=\"radio\" name=\"$name\" value=\"{$this->item->ID}\"/>";
}
}

View File

@ -1,116 +0,0 @@
<?php
/**
* Special ComplexTableField for editing a many_many relation.
*
* This field allows you to show a **many-to-many** relation with a group of
* DataObjects as a (readonly) tabular list (similiar to {@link ComplexTableField}).
* Its most useful when you want to manage the relationship itself
* thanks to the check boxes present on each line of the table.
*
* See {@link ComplexTableField} for more documentation on the base-class.
* See {@link HasManyComplexTableField} for more documentation on the relation table base-class.
*
* Note: This class relies on the fact that both sides of the relation have database tables.
* If you are only creating a class as a logical extension (that is, it doesn't have any database fields),
* then you will need to create a dummy static $db array because SilverStripe won't create a database
* table unless needed.
*
* <b>Usage</b>
*
* <code>
* $tablefield = new ManyManyComplexTableField(
* $this,
* 'MyFruits',
* 'Fruit',
* array(
* 'Name' => 'Name',
* 'Color' => 'Color'
* ),
* 'getCMSFields_forPopup'
* );
* </code>
*
* @deprecated 3.1 Use GridField with GridFieldConfig_RelationEditor
*
* @package forms
* @subpackage fields-relational
*/
class ManyManyComplexTableField extends HasManyComplexTableField {
private $manyManyParentClass;
public $itemClass = 'ManyManyComplexTableField_Item';
public function __construct($controller, $name, $sourceClass, $fieldList = null, $detailFormFields = null,
$sourceFilter = "", $sourceSort = "", $sourceJoin = "") {
Deprecation::notice('3.0', 'Use GridField with GridFieldConfig_RelationEditor', Deprecation::SCOPE_CLASS);
parent::__construct($controller, $name, $sourceClass, $fieldList, $detailFormFields,
$sourceFilter, $sourceSort, $sourceJoin);
$classes = array_reverse(ClassInfo::ancestry($this->controllerClass()));
foreach($classes as $class) {
$singleton = singleton($class);
$manyManyRelations = $singleton->uninherited('many_many', true);
if(isset($manyManyRelations) && array_key_exists($this->name, $manyManyRelations)) {
$this->manyManyParentClass = $class;
$manyManyTable = $class . '_' . $this->name;
break;
}
$belongsManyManyRelations = $singleton->uninherited( 'belongs_many_many', true );
if( isset( $belongsManyManyRelations ) && array_key_exists( $this->name, $belongsManyManyRelations ) ) {
$this->manyManyParentClass = $class;
$manyManyTable = $belongsManyManyRelations[$this->name] . '_' . $this->name;
break;
}
}
$tableClasses = ClassInfo::dataClassesFor($this->sourceClass);
$source = array_shift($tableClasses);
$sourceField = $this->sourceClass;
if($this->manyManyParentClass == $sourceField)
$sourceField = 'Child';
$parentID = $this->controller->ID;
$this->sourceJoin .= " LEFT JOIN \"$manyManyTable\"
ON (\"$source\".\"ID\" = \"$manyManyTable\".\"{$sourceField}ID\"
AND \"{$this->manyManyParentClass}ID\" = '$parentID')";
$this->joinField = 'Checked';
}
public function getQuery() {
$query = parent::getQuery();
$query->selectField("CASE WHEN \"{$this->manyManyParentClass}ID\" IS NULL THEN '0' ELSE '1' END", "Checked");
$query->groupby[] = "\"{$this->manyManyParentClass}ID\""; // necessary for Postgres
return $query;
}
public function getParentIdName($parentClass, $childClass) {
return $this->getParentIdNameRelation($parentClass, $childClass, 'many_many');
}
}
/**
* One record in a {@link ManyManyComplexTableField}.
* @package forms
* @subpackage fields-relational
*/
class ManyManyComplexTableField_Item extends ComplexTableField_Item {
public function MarkingCheckbox() {
$name = $this->parent->getName() . '[]';
if($this->parent->IsReadOnly)
return "<input class=\"checkbox\" type=\"checkbox\" name=\"$name\" value=\"{$this->item->ID}\"
disabled=\"disabled\"/>";
else if($this->item->{$this->parent->joinField})
return "<input class=\"checkbox\" type=\"checkbox\" name=\"$name\" value=\"{$this->item->ID}\"
checked=\"checked\"/>";
else
return "<input class=\"checkbox\" type=\"checkbox\" name=\"$name\" value=\"{$this->item->ID}\"/>";
}
}

View File

@ -1,724 +0,0 @@
<?php
/**
* TableField behaves in the same manner as TableListField, however allows the addition of
* fields and editing of attributes specified, and filtering results.
*
* Caution: If you insert DropdownFields in the fieldTypes-array, make sure they have an empty first option.
* Otherwise the saving can't determine if a new row should really be saved.
*
* Caution: TableField relies on {@FormResponse} to reload the field after it is saved.
* A TableField-instance should never be saved twice without reloading, because otherwise it
* can't determine if a field is new (=create) or existing (=update), and will produce duplicates.
*
* IMPORTANT: This class is about to be deprecated in favour of a new GridFieldEditableColumns component,
* see http://open.silverstripe.org/ticket/7045
*
* @package forms
* @subpackage fields-relational
*/
class TableField extends TableListField {
protected $fieldList;
/**
* A "Field = Value" filter can be specified by setting $this->filterField and $this->filterValue. This has the
* advantage of auto-populating new records
*/
protected $filterField = null;
/**
* A "Field = Value" filter can be specified by setting $this->filterField and $this->filterValue. This has the
* advantage of auto-populating new records
*/
protected $filterValue = null;
/**
* @var $fieldTypes FieldList
* Caution: Use {@setExtraData()} instead of manually adding HiddenFields if you want to
* preset relations or other default data.
*/
protected $fieldTypes;
/**
* @var $template string Template-Overrides
*/
protected $template = "TableField";
/**
* @var $extraData array Any extra data that need to be included, e.g. to retain
* has-many relations. Format: array('FieldName' => 'Value')
*/
protected $extraData;
protected $tempForm;
/**
* Influence output without having to subclass the template.
*/
protected $permissions = array(
"edit",
"delete",
"add",
//"export",
);
public $transformationConditions = array();
/**
* @var $requiredFields array Required fields as a numerical array.
* Please use an instance of Validator on the including
* form.
*/
protected $requiredFields = null;
/**
* Shows a row of empty fields for adding a new record
* (turned on by default).
* Please use {@link TableField::$permissions} to control
* if the "add"-functionality incl. button is shown at all.
*
* @param boolean $showAddRow
*/
public $showAddRow = true;
/**
* @param $name string The fieldname
* @param $sourceClass string The source class of this field
* @param $fieldList array An array of field headings of Fieldname => Heading Text (eg. heading1)
* @param $fieldTypes array An array of field types of fieldname => fieldType (eg. formfield). Do not use for
* extra data/hiddenfields.
* @param $filterField string The field to filter by. Give the filter value in $sourceFilter. The value will
* automatically be set on new records.
* @param $sourceFilter string If $filterField has a value, then this is the value to filter by. Otherwise, it is
* a SQL filter expression.
* @param $editExisting boolean (Note: Has to stay on this position for legacy reasons)
* @param $sourceSort string
* @param $sourceJoin string
*/
public function __construct($name, $sourceClass, $fieldList = null, $fieldTypes, $filterField = null,
$sourceFilter = null, $editExisting = true, $sourceSort = null, $sourceJoin = null) {
$this->fieldTypes = $fieldTypes;
$this->filterField = $filterField;
$this->editExisting = $editExisting;
// If we specify filterField, then an implicit source filter of "filterField = sourceFilter" is used.
if($filterField) {
$this->filterValue = $sourceFilter;
$sourceFilter = "\"$filterField\" = '" . Convert::raw2sql($sourceFilter) . "'";
}
parent::__construct($name, $sourceClass, $fieldList, $sourceFilter, $sourceSort, $sourceJoin);
}
/**
* Displays the headings on the template
*
* @return SS_List
*/
public function Headings() {
$i=0;
foreach($this->fieldList as $fieldName => $fieldTitle) {
$extraClass = "col".$i;
$class = $this->fieldTypes[$fieldName];
if(is_object($class)) $class = "";
$class = $class." ".$extraClass;
$headings[] = new ArrayData(array("Name" => $fieldName, "Title" => $fieldTitle, "Class" => $class));
$i++;
}
return new ArrayList($headings);
}
/**
* Calculates the number of columns needed for colspans
* used in template
*
* @return int
*/
public function ItemCount() {
return count($this->fieldList);
}
/**
* Displays the items from {@link sourceItems()} using the encapsulation object.
* If the field value has been set as an array (e.g. after a failed validation),
* it generates the rows from array data instead.
* Used in the formfield template to iterate over each row.
*
* @return SS_List Collection of {@link TableField_Item}
*/
public function Items() {
// holds TableField_Item instances
$items = new ArrayList();
$sourceItems = $this->sourceItems();
// either load all rows from the field value,
// (e.g. when validation failed), or from sourceItems()
if($this->value) {
if(!$sourceItems) $sourceItems = new ArrayList();
// get an array keyed by rows, rather than values
$rows = $this->sortData(ArrayLib::invert($this->value));
// ignore all rows which are already saved
if(isset($rows['new'])) {
if($sourceItems instanceof DataList) {
$sourceItems = new ArrayList($sourceItems->toArray());
}
$newRows = $this->sortData($rows['new']);
// iterate over each value (not each row)
$i = 0;
foreach($newRows as $idx => $newRow){
// set a pseudo-ID
$newRow['ID'] = "new";
// unset any extradata
foreach($newRow as $k => $v){
if($this->extraData && array_key_exists($k, $this->extraData)){
unset($newRow[$k]);
}
}
// generate a temporary DataObject container (not saved in the database)
$sourceClass = $this->sourceClass();
$sourceItems->add(new $sourceClass($newRow));
$i++;
}
}
}
// generate a new TableField_Item instance from each collected item
if($sourceItems) foreach($sourceItems as $sourceItem) {
$items->push($this->generateTableFieldItem($sourceItem));
}
// add an empty TableField_Item for a single "add row"
if($this->showAddRow && $this->Can('add')) {
$items->push(new TableField_Item(null, $this, null, $this->fieldTypes, true));
}
return $items;
}
/**
* Generates a new {@link TableField} instance
* by loading a FieldList for this row into a temporary form.
*
* @param DataObject $dataObj
* @return TableField_Item
*/
protected function generateTableFieldItem($dataObj) {
// Load the data in to a temporary form (for correct field types)
$form = new Form(
$this,
null,
$this->FieldSetForRow(),
new FieldList()
);
$form->loadDataFrom($dataObj);
// Add the item to our new ArrayList, with a wrapper class.
return new TableField_Item($dataObj, $this, $form, $this->fieldTypes);
}
/**
* @return array
*/
public function FieldList() {
return $this->fieldList;
}
/**
* Saves the Dataobjects contained in the field
*/
public function saveInto(DataObjectInterface $record) {
// CMS sometimes tries to set the value to one.
if(is_array($this->value)){
$newFields = array();
// Sort into proper array
$value = ArrayLib::invert($this->value);
$dataObjects = $this->sortData($value, $record->ID);
// New fields are nested in their own sub-array, and need to be sorted separately
if(isset($dataObjects['new']) && $dataObjects['new']) {
$newFields = $this->sortData($dataObjects['new'], $record->ID);
}
// Update existing fields
// @todo Should this be in an else{} statement?
$savedObjIds = $this->saveData($dataObjects, $this->editExisting);
// Save newly added record
if($savedObjIds || $newFields) {
$savedObjIds = $this->saveData($newFields,false);
}
// Add the new records to the DataList
if($savedObjIds) foreach($savedObjIds as $id => $status) {
$this->getDataList()->add($id);
}
// Update the internal source items cache
$this->value = null;
$items = $this->sourceItems();
// FormResponse::update_dom_id($this->id(), $this->FieldHolder());
}
}
/**
* Get all {@link FormField} instances necessary for a single row,
* respecting the casting set in {@link $fieldTypes}.
* Doesn't populate with any data. Optionally performs a readonly
* transformation if {@link $IsReadonly} is set, or the current user
* doesn't have edit permissions.
*
* @return FieldList
*/
public function FieldSetForRow() {
$fieldset = new FieldList();
if($this->fieldTypes){
foreach($this->fieldTypes as $key => $fieldType) {
if(isset($fieldType->class) && is_subclass_of($fieldType, 'FormField')) {
// using clone, otherwise we would just add stuff to the same field-instance
$field = clone $fieldType;
} elseif(strpos($fieldType, '(') === false) {
$field = new $fieldType($key);
} else {
$fieldName = $key;
$fieldTitle = "";
$field = eval("return new $fieldType;");
}
if($this->IsReadOnly || !$this->Can('edit')) {
$field = $field->performReadonlyTransformation();
}
$fieldset->push($field);
}
}else{
USER_ERROR("TableField::FieldSetForRow() - Fieldtypes were not specified",E_USER_WARNING);
}
return $fieldset;
}
public function performReadonlyTransformation() {
$clone = clone $this;
$clone->permissions = array('show');
$clone->setReadonly(true);
return $clone;
}
public function performDisabledTransformation() {
$clone = clone $this;
$clone->setPermissions(array('show'));
$clone->setDisabled(true);
return $clone;
}
/**
* Needed for Form->callfieldmethod.
*/
public function getField($fieldName, $combinedFieldName = null) {
$fieldSet = $this->FieldSetForRow();
$field = $fieldSet->dataFieldByName($fieldName);
if(!$field) {
return false;
}
if($combinedFieldName) {
$field->Name = $combinedFieldName;
}
return $field;
}
/**
* Called on save, it creates the appropriate objects and writes them
* to the database.
*
* @param SS_List $dataObjects
* @param boolean $existingValues If set to TRUE, it tries to find existing objects
* based on the database IDs passed as array keys in $dataObjects parameter.
* If set to FALSE, it will always create new object (default: TRUE)
* @return array Array of saved object IDs in the key, and the status ("Updated") in the value
*/
public function saveData($dataObjects, $existingValues = true) {
if(!$dataObjects) return false;
$savedObjIds = array();
$fieldset = $this->FieldSetForRow();
// add hiddenfields
if($this->extraData) {
foreach($this->extraData as $fieldName => $fieldValue) {
$fieldset->push(new HiddenField($fieldName));
}
}
$form = new Form($this, null, $fieldset, new FieldList());
foreach ($dataObjects as $objectid => $fieldValues) {
// 'new' counts as an empty column, don't save it
if($objectid === "new") continue;
// extra data was creating fields, but
if($this->extraData) {
$fieldValues = array_merge( $this->extraData, $fieldValues );
}
// either look for an existing object, or create a new one
if($existingValues) {
$obj = DataObject::get_by_id($this->sourceClass(), $objectid);
} else {
$sourceClass = $this->sourceClass();
$obj = new $sourceClass();
}
// Legacy: Use the filter as a predefined relationship-ID
if($this->filterField && $this->filterValue) {
$filterField = $this->filterField;
$obj->$filterField = $this->filterValue;
}
// Determine if there is changed data for saving
$dataFields = array();
foreach($fieldValues as $type => $value) {
// if the field is an actual datafield (not a preset hiddenfield)
if(is_array($this->extraData)) {
if(!in_array($type, array_keys($this->extraData))) {
$dataFields[$type] = $value;
}
// all fields are real
} else {
$dataFields[$type] = $value;
}
}
$dataValues = ArrayLib::array_values_recursive($dataFields);
// determine if any of the fields have a value (loose checking with empty())
$hasData = false;
foreach($dataValues as $value) {
if(!empty($value)) $hasData = true;
}
if($hasData) {
$form->loadDataFrom($fieldValues, true);
$form->saveInto($obj);
$objectid = $obj->write();
$savedObjIds[$objectid] = "Updated";
}
}
return $savedObjIds;
}
/**
* Organises the data in the appropriate manner for saving
*
* @param array $data
* @param int $recordID
* @return array Collection of maps suitable to construct DataObjects
*/
public function sortData($data, $recordID = null) {
if(!$data) return false;
$sortedData = array();
foreach($data as $field => $rowData) {
$i = 0;
if(!is_array($rowData)) continue;
foreach($rowData as $id => $value) {
if($value == '$recordID') $value = $recordID;
if($value) $sortedData[$id][$field] = $value;
$i++;
}
// TODO ADD stuff for removing rows with incomplete data
}
return $sortedData;
}
/**
* @param $extraData array
*/
public function setExtraData($extraData) {
$this->extraData = $extraData;
return $this;
}
/**
* @return array
*/
public function getExtraData() {
return $this->extraData;
}
/**
* Sets the template to be rendered with
*/
public function FieldHolder($properties = array()) {
Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
Requirements::javascript(THIRDPARTY_DIR . "/prototype/prototype.js");
Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/behaviour/behaviour.js');
Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/javascript/lang');
Requirements::javascript(FRAMEWORK_DIR . '/javascript/TableListField.js');
Requirements::javascript(FRAMEWORK_DIR . '/javascript/TableField.js');
Requirements::css(FRAMEWORK_DIR . '/css/TableListField.css');
$obj = $properties ? $this->customise($properties) : $this;
return $obj->renderWith($this->template);
}
public function setTransformationConditions($conditions) {
$this->transformationConditions = $conditions;
return $this;
}
public function php($data) {
$valid = true;
if($data['methodName'] != 'delete') {
$items = $this->Items();
if($items) foreach($items as $item) {
foreach($item->Fields() as $field) {
$valid = $field->validate($this) && $valid;
}
}
return $valid;
} else {
return $valid;
}
}
public function validate($validator) {
$errorMessage = '';
$valid = true;
// @todo should only contain new elements
$items = $this->Items();
if($items) foreach($items as $item) {
foreach($item->Fields() as $field) {
$valid = $field->validate($validator) && $valid;
}
}
//debug::show($this->form->Message());
if($this->requiredFields&&$sourceItemsNew&&$sourceItemsNew->count()) {
foreach ($this->requiredFields as $field) {
foreach($sourceItemsNew as $item){
$cellName = $this->getName().'['.$item->ID.']['.$field.']';
$cName = $this->getName().'[new]['.$field.'][]';
if($fieldObj = $fields->dataFieldByName($cellName)) {
if(!trim($fieldObj->Value())){
$title = $fieldObj->Title();
$errorMessage .= sprintf(
_t('TableField.ISREQUIRED', "In %s '%s' is required"),
$this->name,
$title
);
$errorMessage .= "<br />";
}
}
}
}
}
if($errorMessage){
$messageType .= "validation";
$message .= "<br />".$errorMessage;
$validator->validationError($this->name, $message, $messageType);
}
return $valid;
}
public function setRequiredFields($fields) {
$this->requiredFields = $fields;
return $this;
}
}
/**
* Single record in a TableField.
* @package forms
* @subpackage fields-relational
* @see TableField
*/
class TableField_Item extends TableListField_Item {
/**
* @var FieldList $fields
*/
protected $fields;
protected $data;
protected $fieldTypes;
protected $isAddRow;
protected $extraData;
/**
* Each row contains a dataobject with any number of attributes
* @param $ID int The ID of the record
* @param $form Form A Form object containing all of the fields for this item. The data should be loaded in
* @param $fieldTypes array An array of name => fieldtype for use when creating a new field
* @param $parent TableListField The parent table for quick reference of names, and id's for storing values.
*/
public function __construct($item = null, $parent, $form, $fieldTypes, $isAddRow = false) {
$this->data = $form;
$this->fieldTypes = $fieldTypes;
$this->isAddRow = $isAddRow;
$this->item = $item;
parent::__construct(($this->item) ? $this->item : new DataObject(), $parent);
$this->fields = $this->createFields();
}
/**
* Represents each cell of the table with an attribute.
*
* @return FieldList
*/
public function createFields() {
// Existing record
if($this->item && $this->data) {
$form = $this->data;
$this->fieldset = $form->Fields();
$this->fieldset->removeByName('SecurityID');
if($this->fieldset) {
$i=0;
foreach($this->fieldset as $field) {
$origFieldName = $field->getName();
// set unique fieldname with id
$combinedFieldName = $this->parent->getName() . "[" . $this->ID() . "][" . $origFieldName . "]";
// ensure to set field to nested array notation
// if its an unsaved row, or the "add row" which is present by default
if($this->isAddRow || $this->ID() == 'new') $combinedFieldName .= '[]';
// get value
if(strpos($origFieldName,'.') === false) {
$value = $field->dataValue();
} else {
// this supports the syntax fieldName = Relation.RelatedField
$fieldNameParts = explode('.', $origFieldName) ;
$tmpItem = $this->item;
for($j=0;$j<sizeof($fieldNameParts);$j++) {
$relationMethod = $fieldNameParts[$j];
$idField = $relationMethod . 'ID';
if($j == sizeof($fieldNameParts)-1) {
$value = $tmpItem->$relationMethod;
} else {
$tmpItem = $tmpItem->$relationMethod();
}
}
}
$field->Name = $combinedFieldName;
$field->setValue($field->dataValue());
$field->addExtraClass('col'.$i);
$field->setForm($this->data);
// transformation
if(isset($this->parent->transformationConditions[$origFieldName])) {
$transformation = $this->parent->transformationConditions[$origFieldName]['transformation'];
$rule = str_replace("\$","\$this->item->",
$this->parent->transformationConditions[$origFieldName]['rule']);
$ruleApplies = null;
eval('$ruleApplies = ('.$rule.');');
if($ruleApplies) {
$field = $field->$transformation();
}
}
// formatting
$item = $this->item;
$value = $field->Value();
if(array_key_exists($origFieldName, $this->parent->fieldFormatting)) {
$format = str_replace('$value', "__VAL__", $this->parent->fieldFormatting[$origFieldName]);
$format = preg_replace('/\$([A-Za-z0-9-_]+)/','$item->$1', $format);
$format = str_replace('__VAL__', '$value', $format);
eval('$value = "' . $format . '";');
$field->dontEscape = true;
$field->setValue($value);
}
$this->fields[] = $field;
$i++;
}
}
// New record
} else {
$list = $this->parent->FieldList();
$i=0;
foreach($list as $fieldName => $fieldTitle) {
if(strpos($fieldName, ".")) {
$shortFieldName = substr($fieldName, strpos($fieldName, ".")+1, strlen($fieldName));
} else {
$shortFieldName = $fieldName;
}
$combinedFieldName = $this->parent->getName() . "[new][" . $shortFieldName . "][]";
$fieldType = $this->fieldTypes[$fieldName];
if(isset($fieldType->class) && is_subclass_of($fieldType, 'FormField')) {
$field = clone $fieldType; // we can't use the same instance all over, as we change names
$field->Name = $combinedFieldName;
} elseif(strpos($fieldType, '(') === false) {
//echo ("<li>Type: ".$fieldType." fieldName: ". $filedName. " Title: ".$fieldTitle."</li>");
$field = new $fieldType($combinedFieldName,$fieldTitle);
} else {
$field = eval("return new " . $fieldType . ";");
}
$field->addExtraClass('col'.$i);
$this->fields[] = $field;
$i++;
}
}
return new FieldList($this->fields);
}
public function Fields($xmlSafe = true) {
return $this->fields;
}
public function ExtraData() {
$content = "";
$id = ($this->item->ID) ? $this->item->ID : "new";
if($this->parent->getExtraData()) {
foreach($this->parent->getExtraData() as $fieldName=>$fieldValue) {
$name = $this->parent->getName() . "[" . $id . "][" . $fieldName . "]";
if($this->isAddRow) $name .= '[]';
$field = new HiddenField($name, null, $fieldValue);
$content .= $field->FieldHolder() . "\n";
}
}
return $content;
}
/**
* Get the flag isAddRow of this item,
* to indicate if the item is that blank last row in the table which is not in the database
*
* @return boolean
*/
public function IsAddRow(){
return $this->isAddRow;
}
}

View File

@ -1,1644 +0,0 @@
<?php
/**
* @package forms
* @subpackage fields-relational
*/
/**
* Form field that embeds a list into a form, such as a member list or a file list.
*
* All get variables are namespaced in the format ctf[MyFieldName][MyParameter] to avoid collisions
* when multiple TableListFields are present in a form.
*
* @deprecated 3.1 Use GridField with GridFieldConfig_RecordEditor
*
* @package forms
* @subpackage fields-relational
*/
class TableListField extends FormField {
/**
* The {@link DataList} object defining the source data for this view/
*/
protected $dataList;
protected $fieldList;
protected $disableSorting = false;
/**
* @var $fieldListCsv array
*/
protected $fieldListCsv;
/**
* @var $clickAction
*/
protected $clickAction;
/**
* @var bool
*/
public $IsReadOnly;
/**
* Called method (needs to be retained for AddMode())
*/
protected $methodName;
/**
* @var $summaryFieldList array Shows a row which summarizes the contents of a column by a predefined
* Javascript-function
*/
protected $summaryFieldList;
/**
* @var $summaryTitle string The title which will be shown in the first column of the summary-row.
* Accordingly, the first column can't be used for summarizing.
*/
protected $summaryTitle;
/**
* @var $template string Template-Overrides
*/
protected $template = "TableListField";
/**
* @var $itemClass string Class name for each item/row
*/
public $itemClass = 'TableListField_Item';
/**
* @var bool Do we use checkboxes to mark records, or delete them one by one?
*/
public $Markable;
public $MarkableTitle = null;
/**
* @var array See {@link SelectOptions()}
*/
protected $selectOptions = array();
/**
* @var $readOnly boolean Deprecated, please use $permssions instead
*/
protected $readOnly;
/**
* @var $permissions array Influence output without having to subclass the template.
* See $actions for adding your custom actions/permissions.
*/
protected $permissions = array(
//"print",
//"export",
"delete"
);
/**
* @var $actions array Action that can be performed on a single row-entry.
* Has to correspond to a method in a TableListField-class (or subclass).
* Actions can be disabled through $permissions.
* Format (key is used for the methodname and CSS-class):
* array(
* 'delete' => array(
* 'label' => 'Delete',
* 'icon' => 'framework/images/delete.gif',
* 'icon_disabled' => 'framework/images/delete_disabled.gif',
* 'class' => 'deletelink',
* )
* )
*/
public $actions = array(
'delete' => array(
'label' => 'Delete',
'icon' => 'framework/images/delete.gif',
'icon_disabled' => 'framework/images/delete_disabled.gif',
'class' => 'deletelink'
)
);
/**
* @var $defaultAction String Action being executed when clicking on table-row (defaults to "show").
* Mostly needed in ComplexTableField-subclass.
*/
public $defaultAction = '';
/**
* @var $customCsvQuery Query for CSV-export (might need different fields or further filtering)
*/
protected $customCsvQuery;
/**
* Character to seperate exported columns in the CSV file
*/
protected $csvSeparator = ",";
/*
* Boolean deciding whether to include a header row in the CSV file
*/
protected $csvHasHeader = true;
/**
* @var array Specify custom escape for the fields.
*
* <code>
* array("\""=>"\"\"","\r"=>"", "\r\n"=>"", "\n"=>"")
* </code>
*/
public $csvFieldEscape = array(
"\""=>"\"\"",
"\r\n"=>"",
"\r"=>"",
"\n"=>"",
);
/**
* @var boolean Trigger pagination
*/
protected $showPagination = false;
/**
* @var string Override the {@link Link()} method
* for all pagination. Useful to force rendering of the field
* in a different context.
*/
public $paginationBaseLink = null;
/**
* @var int Number of items to show on a single page (needed for pagination)
*/
protected $pageSize = 10;
/**
* @var array Definitions for highlighting table-rows with a specific class. You can use all column-names
* in the result of a query. Use in combination with {@setCustomQuery} to select custom properties and joined
* objects.
*
* Example:
* array(
* array(
* "rule" => '$Flag == "red"',
* "class" => "red"
* ),
* array(
* "rule" => '$Flag == "orange"',
* "class" => "orange"
* )
* )
*/
public $highlightConditions = array();
/**
* @var array Specify castings with fieldname as the key, and the desired casting as value.
* Example: array("MyCustomDate"=>"Date","MyShortText"=>"Text->FirstSentence")
*/
public $fieldCasting = array();
/**
* @var array Specify custom formatting for fields, e.g. to render a link instead of pure text.
* Caution: Make sure to escape special php-characters like in a normal php-statement.
* Example: "myFieldName" => '<a href=\"custom-admin/$ID\">$ID</a>'
*/
public $fieldFormatting = array();
public $csvFieldFormatting = array();
/**
* @var string
*/
public $exportButtonLabel = 'Export as CSV';
/**
* @var string $groupByField Used to group by a specific column in the DataObject
* and create partial summaries.
*/
public $groupByField = null;
/**
* @var array
*/
protected $extraLinkParams;
protected $__cachedQuery;
/**
* This is a flag that enables some backward-compatibility helpers.
*/
private $getDataListFromForm;
/**
* @param $name string The fieldname
* @param $sourceClass string The source class of this field
* @param $fieldList array An array of field headings of Fieldname => Heading Text (eg. heading1)
* @param $sourceFilter string The filter field you wish to limit the objects by (eg. parentID)
* @param $sourceSort string
* @param $sourceJoin string
*/
public function __construct($name, $sourceClass = null, $fieldList = null, $sourceFilter = null,
$sourceSort = null, $sourceJoin = null) {
if($sourceClass) {
// You can optionally pass a list
if($sourceClass instanceof SS_List) {
$this->dataList = $sourceClass;
} else {
$this->dataList = DataObject::get($sourceClass)->where($sourceFilter)->sort($sourceSort);
if($sourceJoin) $this->dataList = $this->dataList->join($sourceJoin);
// Grab it from the form relation, if available.
$this->getDataListFromForm = true;
}
}
$this->fieldList = ($fieldList) ? $fieldList : singleton($this->sourceClass())->summaryFields();
$this->readOnly = false;
parent::__construct($name);
}
public function index() {
return $this->FieldHolder();
}
static $url_handlers = array(
'item/$ID' => 'handleItem',
'$Action' => '$Action',
);
public function sourceClass() {
$list = $this->getDataList();
if(method_exists($list, 'dataClass')) return $list->dataClass();
// Failover for SS_List
else return get_class($list->First());
}
public function handleItem($request) {
return new TableListField_ItemRequest($this, $request->param('ID'));
}
public function FieldHolder($properties = array()) {
Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/prototype/prototype.js');
Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/behaviour/behaviour.js');
Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/javascript/lang');
Requirements::javascript(FRAMEWORK_DIR . '/javascript/TableListField.js');
Requirements::css(FRAMEWORK_DIR . '/css/TableListField.css');
if($this->clickAction) {
$id = $this->id();
Requirements::customScript(<<<JS
Behaviour.register({
'#$id tr' : {
onclick : function() {
$this->clickAction
return false;
}
}
});
JS
);}
$obj = $properties ? $this->customise($properties) : $this;
return $obj->renderWith($this->template);
}
public function Headings() {
$headings = array();
foreach($this->fieldList as $fieldName => $fieldTitle) {
$isSorted = (isset($_REQUEST['ctf'][$this->getName()]['sort'])
&& $fieldName == $_REQUEST['ctf'][$this->getName()]['sort']);
// we can't allow sorting with partial summaries (groupByField)
$isSortable = ($this->form && $this->isFieldSortable($fieldName) && !$this->groupByField);
// sorting links (only if we have a form to refresh with)
if($this->form) {
$sortLink = $this->Link();
$sortLink = HTTP::setGetVar("ctf[{$this->getName()}][sort]", $fieldName, $sortLink,'&');
// Apply sort direction to the current sort field
if(!empty($_REQUEST['ctf'][$this->getName()]['sort'])
&& ($_REQUEST['ctf'][$this->getName()]['sort'] == $fieldName)) {
if(isset($_REQUEST['ctf'][$this->getName()]['dir'])) {
$dir = $_REQUEST['ctf'][$this->getName()]['dir'];
} else {
$dir = null;
}
$dir = trim(strtolower($dir));
$newDir = ($dir == 'desc') ? null : 'desc';
$sortLink = HTTP::setGetVar("ctf[{$this->getName()}][dir]", Convert::raw2xml($newDir),
$sortLink,'&');
}
if(isset($_REQUEST['ctf'][$this->getName()]['search'])
&& is_array($_REQUEST['ctf'][$this->getName()]['search'])) {
foreach($_REQUEST['ctf'][$this->getName()]['search'] as $parameter => $value) {
$XML_search = Convert::raw2xml($value);
$sortLink = HTTP::setGetVar("ctf[{$this->getName()}][search][$parameter]", $XML_search,
$sortLink,'&');
}
}
} else {
$sortLink = '#';
}
$headings[] = new ArrayData(array(
"Name" => $fieldName,
"Title" => ($this->sourceClass())
? singleton($this->sourceClass())->fieldLabel($fieldTitle)
: $fieldTitle,
"IsSortable" => $isSortable,
"SortLink" => $sortLink,
"SortBy" => $isSorted,
"SortDirection" => (isset($_REQUEST['ctf'][$this->getName()]['dir']))
? $_REQUEST['ctf'][$this->getName()]['dir']
: null
));
}
return new ArrayList($headings);
}
public function disableSorting($to = true) {
$this->disableSorting = $to;
}
/**
* Determines if a field is "sortable".
* If the field is generated by a custom getter, we can't sort on it
* without generating all objects first (which would be a huge performance impact).
*
* @param string $fieldName
* @return bool
*/
public function isFieldSortable($fieldName) {
if($this->disableSorting) return false;
$list = $this->getDataList();
if(method_exists($list,'canSortBy')) return $list->canSortBy($fieldName);
else return false;
}
/**
* Dummy function to get number of actions originally generated in
* TableListField_Item.
*
* @return SS_List
*/
public function Actions() {
$allowedActions = new ArrayList();
foreach($this->actions as $actionName => $actionSettings) {
if($this->Can($actionName)) {
$allowedActions->push(new ViewableData());
}
}
return $allowedActions;
}
/**
* Provide a custom query to compute sourceItems. This is the preferred way to using
* {@setSourceItems}, because we can still paginate.
* Please use this only as a fallback for really complex queries (e.g. involving HAVING and GROUPBY).
*
* @param $query DataList
*/
public function setCustomQuery(DataList $dataList) {
$this->dataList = $dataList;
return $this;
}
public function setCustomCsvQuery(DataList $dataList) {
$this->customCsvQuery = $query;
return $this;
}
public function setCustomSourceItems(SS_List $items) {
user_error('TableList::setCustomSourceItems() deprecated, just pass the items into the constructor',
E_USER_WARNING);
// The type-hinting above doesn't seem to work consistently
if($items instanceof SS_List) {
$this->dataList = $items;
} else {
user_error('TableList::setCustomSourceItems() should be passed a SS_List', E_USER_WARNING);
}
return $this;
}
/**
* Get items, with sort & limit applied
*/
public function sourceItems() {
// get items (this may actually be a SS_List)
$items = clone $this->getDataList();
// TODO: Sorting could be implemented on regular SS_Lists.
if(method_exists($items,'canSortBy') && isset($_REQUEST['ctf'][$this->getName()]['sort'])) {
$sort = $_REQUEST['ctf'][$this->getName()]['sort'];
// TODO: sort direction
if($items->canSortBy($sort)) $items = $items->sort($sort);
}
// Determine pagination limit, offset
// To disable pagination, set $this->showPagination to false.
if($this->showPagination && $this->pageSize) {
$SQL_limit = (int)$this->pageSize;
if(isset($_REQUEST['ctf'][$this->getName()]['start'])
&& is_numeric($_REQUEST['ctf'][$this->getName()]['start'])) {
if(isset($_REQUEST['ctf'][$this->getName()]['start'])) {
$SQL_start = intval($_REQUEST['ctf'][$this->getName()]['start']);
} else {
$SQL_start = "0";
}
} else {
$SQL_start = 0;
}
$items = $items->limit($SQL_limit, $SQL_start);
}
return $items;
}
/**
* Return a SS_List of TableListField_Item objects, suitable for display in the template.
*/
public function Items() {
$fieldItems = new ArrayList();
if($items = $this->sourceItems()) foreach($items as $item) {
if($item) $fieldItems->push(new $this->itemClass($item, $this));
}
return $fieldItems;
}
/**
* Returns the DataList for this field.
*/
public function getDataList() {
// If we weren't passed in a DataList to begin with, try and get the datalist from the form
if($this->form && $this->getDataListFromForm) {
$this->getDataListFromForm = false;
$relation = $this->name;
if($record = $this->form->getRecord()) {
if($record->hasMethod($relation)) $this->dataList = $record->$relation();
}
}
if(!$this->dataList) {
user_error(get_class($this). ' is missing a DataList', E_USER_ERROR);
}
return $this->dataList;
}
public function getCsvDataList() {
if($this->customCsvQuery) return $this->customCsvQuery;
else return $this->getDataList();
}
/**
* @deprecated Use getDataList() instead.
*/
public function getQuery() {
Deprecation::notice('3.0', 'Use getDataList() instead.');
$list = $this->getDataList();
if(method_exists($list,'dataQuery')) {
return $this->getDataList()->dataQuery()->query();
}
}
/**
* @deprecated Use getCsvDataList() instead.
*/
public function getCsvQuery() {
Deprecation::notice('3.0', 'Use getCsvDataList() instead.');
$list = $this->getCsvDataList();
if(method_exists($list,'dataQuery')) {
return $list->dataQuery()->query();
}
}
public function FieldList() {
return $this->fieldList;
}
/**
* Configure this table to load content into a subform via ajax
*/
public function setClick_AjaxLoad($urlBase, $formID) {
$this->clickAction = "this.ajaxRequest('" . addslashes($urlBase) . "', '" . addslashes($formID) . "')";
return $this;
}
/**
* Configure this table to open a popup window
*/
public function setClick_PopupLoad($urlBase) {
$this->clickAction = "var w = window.open(baseHref()+'$urlBase'+this.id.replace(/.*-(\d*)$/,'$1'), 'popup');"
. " w.focus();";
return $this;
}
public function performReadonlyTransformation() {
$clone = clone $this;
$clone->setShowPagination(false);
// Only include the show action if it was in the original CTF.
$clone->setPermissions(in_array('show', $this->permissions) ? array('show') : array());
$clone->addExtraClass( 'readonly' );
$clone->setReadonly(true);
return $clone;
}
/**
* #################################
* CRUD
* #################################
*/
/**
* @return String
*/
public function delete($request) {
// Protect against CSRF on destructive action
$token = $this->getForm()->getSecurityToken();
if(!$token->checkRequest($request)) return $this->httpError('400');
if($this->Can('delete') !== true) {
return false;
}
$this->methodName = "delete";
$childId = Convert::raw2sql($_REQUEST['ctf']['childID']);
if (is_numeric($childId)) {
$this->getDataList()->removeById($childId);
}
// TODO return status in JSON etc.
//return $this->renderWith($this->template);
}
/**
* #################################
* Summary-Row
* #################################
*/
/**
* Can utilize some built-in summary-functions, with optional casting.
* Currently supported:
* - sum
* - avg
*
* @param $summaryTitle string
* @param $summaryFields array
* Simple Format: array("MyFieldName"=>"sum")
* With Casting: array("MyFieldname"=>array("sum","Currency->Nice"))
*/
public function addSummary($summaryTitle, $summaryFieldList) {
$this->summaryTitle = $summaryTitle;
$this->summaryFieldList = $summaryFieldList;
}
public function removeSummary() {
$this->summaryTitle = null;
$this->summaryFields = null;
}
public function HasSummary() {
return (isset($this->summaryFieldList));
}
public function SummaryTitle() {
return $this->summaryTitle;
}
/**
* @param SS_List $items Only used to pass grouped sourceItems for creating
* partial summaries.
*/
public function SummaryFields($items = null) {
if(!isset($this->summaryFieldList)) {
return false;
}
$summaryFields = array();
$fieldListWithoutFirst = $this->fieldList;
if(!empty($this->summaryTitle)) {
array_shift($fieldListWithoutFirst);
}
foreach($fieldListWithoutFirst as $fieldName => $fieldTitle) {
if(in_array($fieldName, array_keys($this->summaryFieldList))) {
if(is_array($this->summaryFieldList[$fieldName])) {
$summaryFunction = "colFunction_{$this->summaryFieldList[$fieldName][0]}";
$casting = $this->summaryFieldList[$fieldName][1];
} else {
$summaryFunction = "colFunction_{$this->summaryFieldList[$fieldName]}";
$casting = null;
}
// fall back to integrated sourceitems if not passed
if(!$items) $items = $this->sourceItems();
$summaryValue = ($items) ? $this->$summaryFunction($items->column($fieldName)) : null;
// Optional casting, Format: array('MyFieldName'=>array('sum','Currency->Nice'))
if(isset($casting)) {
$summaryValue = $this->getCastedValue($summaryValue, $casting);
}
} else {
$summaryValue = null;
$function = null;
}
$summaryFields[] = new ArrayData(array(
'Function' => $function,
'SummaryValue' => $summaryValue,
'Name' => DBField::create_field('Varchar', $fieldName),
'Title' => DBField::create_field('Varchar', $fieldTitle),
));
}
return new ArrayList($summaryFields);
}
public function HasGroupedItems() {
return ($this->groupByField);
}
public function GroupedItems() {
if(!$this->groupByField) {
return false;
}
$items = $this->sourceItems();
if(!$items || !$items->Count()) {
return false;
}
$groupedItems = $items->groupBy($this->groupByField);
$groupedArrItems = new ArrayList();
foreach($groupedItems as $key => $group) {
$fieldItems = new ArrayList();
foreach($group as $item) {
if($item) $fieldItems->push(new $this->itemClass($item, $this));
}
$groupedArrItems->push(new ArrayData(array(
'Items' => $fieldItems,
'SummaryFields' => $this->SummaryFields($group)
)));
}
return $groupedArrItems;
}
public function colFunction_sum($values) {
return array_sum($values);
}
public function colFunction_avg($values) {
return array_sum($values)/count($values);
}
/**
* #################################
* Permissions
* #################################
*/
/**
* Template accessor for Permissions.
* See {@link TableListField_Item->Can()} for object-specific
* permissions.
*
* @return boolean
*/
public function Can($mode) {
if($mode == 'add' && $this->isReadonly()) {
return false;
} else if($mode == 'delete' && $this->isReadonly()) {
return false;
} else if($mode == 'edit' && $this->isReadonly()) {
return false;
} else {
return (in_array($mode, $this->permissions));
}
}
public function setPermissions($arr) {
$this->permissions = $arr;
return $this;
}
/**
* @return array
*/
public function getPermissions() {
return $this->permissions;
}
/**
* #################################
* Pagination
* #################################
*/
public function setShowPagination($bool) {
$this->showPagination = (bool)$bool;
return $this;
}
/**
* @return boolean
*/
public function ShowPagination() {
if($this->showPagination && !empty($this->summaryFieldList)) {
user_error("You can't combine pagination and summaries - please disable one of them.", E_USER_ERROR);
}
return $this->showPagination;
}
public function setPageSize($pageSize) {
$this->pageSize = $pageSize;
return $this;
}
public function PageSize() {
return $this->pageSize;
}
public function ListStart() {
return $_REQUEST['ctf'][$this->getName()]['start'];
}
/**
* @param array
* @deprecated Put the query string onto your form's link instead :-)
*/
public function setExtraLinkParams($params){
Deprecation::notice('2.4', 'Put the query string onto your FormAction instead().');
$this->extraLinkParams = $params;
return $this;
}
/**
* @return array
*/
public function getExtraLinkParams(){
return $this->extraLinkParams;
}
public function FirstLink() {
$start = 0;
if(!isset($_REQUEST['ctf'][$this->getName()]['start'])
|| !is_numeric($_REQUEST['ctf'][$this->getName()]['start'])
|| $_REQUEST['ctf'][$this->getName()]['start'] == 0) {
return null;
}
$baseLink = ($this->paginationBaseLink) ? $this->paginationBaseLink : $this->Link();
$link = Controller::join_links($baseLink, "?ctf[{$this->getName()}][start]={$start}");
if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
// preserve sort options
if(isset($_REQUEST['ctf'][$this->getName()]['sort'])) {
$link .= "&ctf[{$this->getName()}][sort]=" . $_REQUEST['ctf'][$this->getName()]['sort'];
// direction
if(isset($_REQUEST['ctf'][$this->getName()]['dir'])) {
$link .= "&ctf[{$this->getName()}][dir]=" . $_REQUEST['ctf'][$this->getName()]['dir'];
}
}
return $link;
}
public function PrevLink() {
if(isset($_REQUEST['ctf'][$this->getName()]['start'])) {
$currentStart = $_REQUEST['ctf'][$this->getName()]['start'];
} else {
$currentStart = 0;
}
if($currentStart == 0) {
return null;
}
if($_REQUEST['ctf'][$this->getName()]['start'] - $this->pageSize < 0) {
$start = 0;
} else {
$start = $_REQUEST['ctf'][$this->getName()]['start'] - $this->pageSize;
}
$baseLink = ($this->paginationBaseLink) ? $this->paginationBaseLink : $this->Link();
$link = Controller::join_links($baseLink, "?ctf[{$this->getName()}][start]={$start}");
if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
// preserve sort options
if(isset($_REQUEST['ctf'][$this->getName()]['sort'])) {
$link .= "&ctf[{$this->getName()}][sort]=" . $_REQUEST['ctf'][$this->getName()]['sort'];
// direction
if(isset($_REQUEST['ctf'][$this->getName()]['dir'])) {
$link .= "&ctf[{$this->getName()}][dir]=" . $_REQUEST['ctf'][$this->getName()]['dir'];
}
}
return $link;
}
public function NextLink() {
$currentStart = isset($_REQUEST['ctf'][$this->getName()]['start'])
? $_REQUEST['ctf'][$this->getName()]['start']
: 0;
$start = ($currentStart + $this->pageSize < $this->TotalCount())
? $currentStart + $this->pageSize
: $this->TotalCount() % $this->pageSize > 0;
if($currentStart >= $start-1) {
return null;
}
$baseLink = ($this->paginationBaseLink) ? $this->paginationBaseLink : $this->Link();
$link = Controller::join_links($baseLink, "?ctf[{$this->getName()}][start]={$start}");
if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
// preserve sort options
if(isset($_REQUEST['ctf'][$this->getName()]['sort'])) {
$link .= "&ctf[{$this->getName()}][sort]=" . $_REQUEST['ctf'][$this->getName()]['sort'];
// direction
if(isset($_REQUEST['ctf'][$this->getName()]['dir'])) {
$link .= "&ctf[{$this->getName()}][dir]=" . $_REQUEST['ctf'][$this->getName()]['dir'];
}
}
return $link;
}
public function LastLink() {
$pageSize = ($this->TotalCount() % $this->pageSize > 0)
? $this->TotalCount() % $this->pageSize
: $this->pageSize;
$start = $this->TotalCount() - $pageSize;
// Check if there is only one page, or if we are on last page
if($this->TotalCount() <= $pageSize || (isset($_REQUEST['ctf'][$this->getName()]['start'])
&& $_REQUEST['ctf'][$this->getName()]['start'] >= $start)) {
return null;
}
$baseLink = ($this->paginationBaseLink) ? $this->paginationBaseLink : $this->Link();
$link = Controller::join_links($baseLink, "?ctf[{$this->getName()}][start]={$start}");
if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
// preserve sort options
if(isset($_REQUEST['ctf'][$this->getName()]['sort'])) {
$link .= "&ctf[{$this->getName()}][sort]=" . $_REQUEST['ctf'][$this->getName()]['sort'];
// direction
if(isset($_REQUEST['ctf'][$this->getName()]['dir'])) {
$link .= "&ctf[{$this->getName()}][dir]=" . $_REQUEST['ctf'][$this->getName()]['dir'];
}
}
return $link;
}
public function FirstItem() {
if ($this->TotalCount() < 1) return 0;
return isset($_REQUEST['ctf'][$this->getName()]['start'])
? $_REQUEST['ctf'][$this->getName()]['start'] + 1
: 1;
}
public function LastItem() {
if(isset($_REQUEST['ctf'][$this->getName()]['start'])) {
$pageStep = min($this->pageSize, $this->TotalCount() - $_REQUEST['ctf'][$this->getName()]['start']);
return $_REQUEST['ctf'][$this->getName()]['start'] + $pageStep;
} else {
return min($this->pageSize, $this->TotalCount());
}
}
/**
* @ignore
*/
private $_cache_TotalCount;
/**
* Return the total number of items in the source DataList
*/
public function TotalCount() {
if($this->_cache_TotalCount === null) {
$this->_cache_TotalCount = $this->getDataList()->Count();
}
return $this->_cache_TotalCount;
}
/**
* #################################
* Search
* #################################
*
* @todo Not fully implemented at the moment
*/
/**
* Compile all request-parameters for search and pagination
* (except the actual list-positions) as a query-string.
*
* @return String URL-parameters
*/
public function filterString() {
}
/**
* #################################
* CSV Export
* #################################
*/
public function setFieldListCsv($fields) {
$this->fieldListCsv = $fields;
return $this;
}
/**
* Set the CSV separator character. Defaults to ,
*/
public function setCsvSeparator($csvSeparator) {
$this->csvSeparator = $csvSeparator;
return $this;
}
/**
* Get the CSV separator character. Defaults to ,
*/
public function getCsvSeparator() {
return $this->csvSeparator;
}
/**
* Remove the header row from the CSV export
*/
public function removeCsvHeader() {
$this->csvHasHeader = false;
return $this;
}
/**
* Exports a given set of comma-separated IDs (from a previous search-query, stored in a HiddenField).
* Uses {$csv_columns} if present, and falls back to {$result_columns}.
* We move the most filedata generation code to the function {@link generateExportFileData()} so that a child class
* could reuse the filedata generation code while overwrite export function.
*
* @todo Make relation-syntax available (at the moment you'll have to use custom sql)
*/
public function export() {
$now = date("d-m-Y-H-i");
$fileName = "export-$now.csv";
// No pagination for export
$oldShowPagination = $this->showPagination;
$this->showPagination = false;
$result = $this->renderWith(array($this->template . '_printable', 'TableListField_printable'));
$this->showPagination = $oldShowPagination;
if($fileData = $this->generateExportFileData($numColumns, $numRows)){
return SS_HTTPRequest::send_file($fileData, $fileName, 'text/csv');
} else {
user_error("No records found", E_USER_ERROR);
}
}
public function generateExportFileData(&$numColumns, &$numRows) {
$separator = $this->csvSeparator;
$csvColumns = ($this->fieldListCsv) ? $this->fieldListCsv : $this->fieldList;
$fileData = '';
$columnData = array();
$fieldItems = new ArrayList();
if($this->csvHasHeader) {
$fileData .= "\"" . implode("\"{$separator}\"", array_values($csvColumns)) . "\"";
$fileData .= "\n";
}
if(isset($this->customSourceItems)) {
$items = $this->customSourceItems;
} else {
$items = $this->getCsvDataList();
}
// temporary override to adjust TableListField_Item behaviour
$this->setFieldFormatting(array());
$this->fieldList = $csvColumns;
if($items) {
foreach($items as $item) {
if(is_array($item)) {
$className = isset($item['RecordClassName']) ? $item['RecordClassName'] : $item['ClassName'];
$item = new $className($item);
}
$fieldItem = new $this->itemClass($item, $this);
$fields = $fieldItem->Fields(false);
$columnData = array();
if($fields) foreach($fields as $field) {
$value = $field->Value;
// TODO This should be replaced with casting
if(array_key_exists($field->Name, $this->csvFieldFormatting)) {
$format = str_replace('$value', "__VAL__", $this->csvFieldFormatting[$field->Name]);
$format = preg_replace('/\$([A-Za-z0-9-_]+)/','$item->$1', $format);
$format = str_replace('__VAL__', '$value', $format);
eval('$value = "' . $format . '";');
}
$value = str_replace(array("\r", "\n"), "\n", $value);
$tmpColumnData = '"' . str_replace('"', '\"', $value) . '"';
$columnData[] = $tmpColumnData;
}
$fileData .= implode($separator, $columnData);
$fileData .= "\n";
$item->destroy();
unset($item);
unset($fieldItem);
}
$numColumns = count($columnData);
$numRows = $fieldItems->count();
return $fileData;
} else {
return null;
}
}
/**
* We need to instanciate this button manually as a normal button has no means of adding inline onclick-behaviour.
*/
public function ExportLink() {
$exportLink = Controller::join_links($this->Link(), 'export');
if($this->extraLinkParams) $exportLink .= "?" . http_build_query($this->extraLinkParams);
return $exportLink;
}
public function printall() {
Requirements::clear();
if(defined('CMS_DIR')) {
Requirements::css(CMS_DIR . '/css/typography.css');
Requirements::css(CMS_DIR . '/css/cms_right.css');
}
$this->cachedSourceItems = null;
$oldShowPagination = $this->showPagination;
$this->showPagination = false;
increase_time_limit_to();
$this->Print = true;
$result = $this->renderWith(array($this->template . '_printable', 'TableListField_printable'));
$this->showPagination = $oldShowPagination;
return $result;
}
public function PrintLink() {
$link = Controller::join_links($this->Link(), 'printall');
if(isset($_REQUEST['ctf'][$this->getName()]['sort'])) {
$link = HTTP::setGetVar("ctf[{$this->getName()}][sort]",
Convert::raw2xml($_REQUEST['ctf'][$this->getName()]['sort']), $link);
}
return $link;
}
/**
* #################################
* Utilty
* #################################
*/
public function Utility() {
$links = new ArrayList();
if($this->can('export')) {
$links->push(new ArrayData(array(
'Title' => _t('TableListField.CSVEXPORT', 'Export to CSV'),
'Link' => $this->ExportLink()
)));
}
if($this->can('print')) {
$links->push(new ArrayData(array(
'Title' => _t('TableListField.PRINT', 'Print'),
'Link' => $this->PrintLink()
)));
}
return $links;
}
public function setFieldCasting($casting) {
$this->fieldCasting = $casting;
return $this;
}
public function setFieldFormatting($formatting) {
$this->fieldFormatting = $formatting;
return $this;
}
public function setCSVFieldFormatting($formatting) {
$this->csvFieldFormatting = $formatting;
return $this;
}
/**
* Edit the field list
*/
public function setFieldList($fieldList) {
$this->fieldList = $fieldList;
return $this;
}
/**
* @return String
*/
public function Name() {
return $this->name;
}
public function Title() {
// adding translating functionality
// this is a bit complicated, because this parameter is passed to this class
// and should come here translated already
// adding this to TODO probably add a method to the classes
// to return they're translated string
// added by ruibarreiros @ 27/11/2007
return $this->sourceClass() ? singleton($this->sourceClass())->singular_name() : $this->getName();
}
public function NameSingular() {
// same as Title()
// added by ruibarreiros @ 27/11/2007
return $this->sourceClass() ? singleton($this->sourceClass())->singular_name() : $this->getName();
}
public function NamePlural() {
// same as Title()
// added by ruibarreiros @ 27/11/2007
return $this->sourceClass() ? singleton($this->sourceClass())->plural_name() : $this->getName();
}
public function setTemplate($template) {
$this->template = $template;
return $this;
}
public function CurrentLink() {
$link = $this->Link();
if(isset($_REQUEST['ctf'][$this->getName()]['start'])
&& is_numeric($_REQUEST['ctf'][$this->getName()]['start'])) {
$start = ($_REQUEST['ctf'][$this->getName()]['start'] < 0)
? 0
: $_REQUEST['ctf'][$this->getName()]['start'];
$link = Controller::join_links($link, "?ctf[{$this->getName()}][start]={$start}");
}
if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
return $link;
}
/**
* Overloaded to automatically add security token.
*
* @param String $action
* @return String
*/
public function Link($action = null) {
$form = $this->getForm();
if($form) {
$token = $form->getSecurityToken();
$parentUrlParts = parse_url(parent::Link($action));
$queryPart = (isset($parentUrlParts['query'])) ? '?' . $parentUrlParts['query'] : null;
// Ensure that URL actions not routed through Form->httpSubmission() are protected against CSRF attacks.
if($token->isEnabled()) $queryPart = $token->addtoUrl($queryPart);
return Controller::join_links($parentUrlParts['path'], $queryPart);
} else {
// allow for instanciation of this FormField outside of a controller/form
// context (e.g. for unit tests)
return false;
}
}
public function BaseLink() {
user_error("TableListField::BaseLink() deprecated, use Link() instead", E_USER_NOTICE);
return $this->Link();
}
/**
* Helper method to determine permissions for a scaffolded
* TableListField (or subclasses) - currently used in {@link ModelAdmin} and
* {@link DataObject->scaffoldFormFields()}.
*
* Returns true for each permission that doesn't have an explicit getter.
*
* @todo Temporary method, implement directly in FormField subclasses with object-level permissions.
*
* @param string $class
* @param numeric $id
* @return array
*/
public static function permissions_for_object($class, $id = null) {
$permissions = array();
$obj = ($id) ? DataObject::get_by_id($class, $id) : singleton($class);
if(!$obj->hasMethod('canView') || $obj->canView()) $permissions[] = 'show';
if(!$obj->hasMethod('canEdit') || $obj->canEdit()) $permissions[] = 'edit';
if(!$obj->hasMethod('canDelete') || $obj->canDelete()) $permissions[] = 'delete';
if(!$obj->hasMethod('canCreate') || $obj->canCreate()) $permissions[] = 'add';
return $permissions;
}
/**
* @param $value
*
*/
public function getCastedValue($value, $castingDefinition) {
if(is_array($castingDefinition)) {
$castingParams = $castingDefinition;
array_shift($castingParams);
$castingDefinition = array_shift($castingDefinition);
} else {
$castingParams = array();
}
if(strpos($castingDefinition,'->') === false) {
$castingFieldType = $castingDefinition;
$castingField = DBField::create_field($castingFieldType, $value);
$value = call_user_func_array(array($castingField,'XML'),$castingParams);
} else {
$fieldTypeParts = explode('->', $castingDefinition);
$castingFieldType = $fieldTypeParts[0];
$castingMethod = $fieldTypeParts[1];
$castingField = DBField::create_field($castingFieldType, $value);
$value = call_user_func_array(array($castingField,$castingMethod),$castingParams);
}
return $value;
}
public function setHighlightConditions($conditions) {
$this->highlightConditions = $conditions;
return $this;
}
/**
* See {@link SelectOptions()} for introduction.
*
* @param $options array Options to add, key being a unique identifier of the action,
* and value a title for the rendered link element (can contain HTML).
* The keys for 'all' and 'none' have special behaviour associated
* through TableListField.js JavaScript.
* For any other key, the JavaScript automatically checks all checkboxes contained in
* <td> elements with a matching classname.
*/
public function addSelectOptions($options){
foreach($options as $k => $title)
$this->selectOptions[$k] = $title;
}
/**
* Remove one all more table's {@link $selectOptions}
*
* @param $optionsNames array
*/
public function removeSelectOptions($names){
foreach($names as $name){
unset($this->selectOptions[trim($name)]);
}
}
/**
* Return the table's {@link $selectOptions}.
* Used to toggle checkboxes for each table row through button elements.
*
* Requires {@link Markable()} to return TRUE.
* This is only functional with JavaScript enabled.
*
* @return SS_List of ArrayData objects
*/
public function SelectOptions(){
if(!$this->selectOptions) return;
$selectOptionsSet = new ArrayList();
foreach($this->selectOptions as $k => $v) {
$selectOptionsSet->push(new ArrayData(array(
'Key' => $k,
'Value' => $v
)));
}
return $selectOptionsSet;
}
}
/**
* A single record in a TableListField.
* @package forms
* @subpackage fields-relational
* @see TableListField
*/
class TableListField_Item extends ViewableData {
/**
* @var DataObject The underlying data record,
* usually an element of {@link TableListField->sourceItems()}.
*/
protected $item;
/**
* @var TableListField
*/
protected $parent;
public function __construct($item, $parent) {
$this->failover = $this->item = $item;
$this->parent = $parent;
parent::__construct();
}
public function ID() {
return $this->item->ID;
}
public function Parent() {
return $this->parent;
}
public function Fields($xmlSafe = true) {
$list = $this->parent->FieldList();
foreach($list as $fieldName => $fieldTitle) {
$value = "";
// This supports simple FieldName syntax
if(strpos($fieldName,'.') === false) {
$value = ($this->item->XML_val($fieldName) && $xmlSafe)
? $this->item->XML_val($fieldName)
: $this->item->RAW_val($fieldName);
// This support the syntax fieldName = Relation.RelatedField
} else {
$fieldNameParts = explode('.', $fieldName) ;
$tmpItem = $this->item;
for($j=0;$j<sizeof($fieldNameParts);$j++) {
$relationMethod = $fieldNameParts[$j];
$idField = $relationMethod . 'ID';
if($j == sizeof($fieldNameParts)-1) {
if($tmpItem) $value = $tmpItem->$relationMethod;
} else {
if($tmpItem) $tmpItem = $tmpItem->$relationMethod();
}
}
}
// casting
if(array_key_exists($fieldName, $this->parent->fieldCasting)) {
$value = $this->parent->getCastedValue($value, $this->parent->fieldCasting[$fieldName]);
} elseif(is_object($value) && method_exists($value, 'Nice')) {
$value = $value->Nice();
}
// formatting
$item = $this->item;
if(array_key_exists($fieldName, $this->parent->fieldFormatting)) {
$format = str_replace('$value', "__VAL__", $this->parent->fieldFormatting[$fieldName]);
$format = preg_replace('/\$([A-Za-z0-9-_]+)/','$item->$1', $format);
$format = str_replace('__VAL__', '$value', $format);
eval('$value = "' . $format . '";');
}
//escape
if($escape = $this->parent->fieldEscape){
foreach($escape as $search => $replace){
$value = str_replace($search, $replace, $value);
}
}
$fields[] = new ArrayData(array(
"Name" => $fieldName,
"Title" => $fieldTitle,
"Value" => $value,
"CsvSeparator" => $this->parent->getCsvSeparator(),
));
}
return new ArrayList($fields);
}
public function Markable() {
return $this->parent->Markable;
}
/**
* Checks global permissions for field in {@link TableListField->Can()}.
* If they are allowed, it checks for object permissions by assuming
* a method with "can" + $mode parameter naming, e.g. canDelete().
*
* @param string $mode See {@link TableListField::$permissions} array.
* @return boolean
*/
public function Can($mode) {
$canMethod = "can" . ucfirst($mode);
if(!$this->parent->Can($mode)) {
// check global settings for the field instance
return false;
} elseif($this->item->hasMethod($canMethod)) {
// if global allows, check object specific permissions (e.g. canDelete())
return $this->item->$canMethod();
} else {
// otherwise global allowed this action, so return TRUE
return true;
}
}
public function Link($action = null) {
$form = $this->parent->getForm();
if($form) {
$token = $form->getSecurityToken();
$parentUrlParts = parse_url($this->parent->Link());
$queryPart = (isset($parentUrlParts['query'])) ? '?' . $parentUrlParts['query'] : null;
// Ensure that URL actions not routed through Form->httpSubmission() are protected against CSRF attacks.
if($token->isEnabled()) $queryPart = $token->addtoUrl($queryPart);
return Controller::join_links($parentUrlParts['path'], 'item', $this->item->ID, $action, $queryPart);
} else {
// allow for instanciation of this FormField outside of a controller/form
// context (e.g. for unit tests)
return false;
}
}
/**
* Returns all row-based actions not disallowed through permissions.
* See TableListField->Action for a similiar dummy-function to work
* around template-inheritance issues.
*
* @return SS_List
*/
public function Actions() {
$allowedActions = new ArrayList();
foreach($this->parent->actions as $actionName => $actionSettings) {
if($this->parent->Can($actionName)) {
$allowedActions->push(new ArrayData(array(
'Name' => $actionName,
'Link' => $this->{ucfirst($actionName).'Link'}(),
'Icon' => $actionSettings['icon'],
'IconDisabled' => $actionSettings['icon_disabled'],
'Label' => $actionSettings['label'],
'Class' => $actionSettings['class'],
'Default' => ($actionName == $this->parent->defaultAction),
'IsAllowed' => $this->Can($actionName),
)));
}
}
return $allowedActions;
}
public function BaseLink() {
user_error("TableListField_Item::BaseLink() deprecated, use Link() instead", E_USER_NOTICE);
return $this->Link();
}
public function DeleteLink() {
return Controller::join_links($this->Link(), "delete");
}
public function MarkingCheckbox() {
$name = $this->parent->getName() . '[]';
if($this->parent->isReadonly())
return "<input class=\"checkbox\" type=\"checkbox\" name=\"$name\" value=\"{$this->item->ID}\""
. " disabled=\"disabled\" />";
else
return "<input class=\"checkbox\" type=\"checkbox\" name=\"$name\" value=\"{$this->item->ID}\" />";
}
/**
* According to {@link TableListField->selectOptions}, each record will check if the options' key on the object is
* true, if it is true, add the key as a class to the record
*
* @return string Value for a 'class' HTML attribute.
*/
public function SelectOptionClasses(){
$tagArray = array('markingcheckbox');
$options = $this->parent->SelectOptions();
if($options && $options->exists()){
foreach($options as $option){
if($option->Key !== 'all' && $option->Key !== 'none'){
if($this->{$option->Key}) {
$tagArray[] = $option->Key;
}
}
}
}
return implode(" ",$tagArray);
}
public function HighlightClasses() {
$classes = array();
foreach($this->parent->highlightConditions as $condition) {
$rule = str_replace("\$","\$this->item->", $condition['rule']);
$ruleApplies = null;
eval('$ruleApplies = ('.$rule.');');
if($ruleApplies) {
if(isset($condition['exclusive']) && $condition['exclusive']) {
return $condition['class'];
} else {
$classes[] = $condition['class'];
}
}
}
return (count($classes) > 0) ? " " . implode(" ", $classes) : false;
}
/**
* Legacy: Please use permissions instead
*/
public function isReadonly() {
return $this->parent->Can('delete');
}
}
/**
* @package forms
* @subpackage fields-relational
*/
class TableListField_ItemRequest extends RequestHandler {
protected $ctf;
protected $itemID;
protected $methodName;
static $url_handlers = array(
'$Action!' => '$Action',
'' => 'index',
);
public function Link() {
return Controller::join_links($this->ctf->Link(), 'item/' . $this->itemID);
}
public function __construct($ctf, $itemID) {
$this->ctf = $ctf;
$this->itemID = $itemID;
parent::__construct();
}
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->dataObj()->delete();
}
///////////////////////////////////////////////////////////////////////////////////////////////////
/**
* 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 $this->ctf->getDataList()->byId($this->itemID);
}
}
/**
* @return TableListField
*/
public function getParentController() {
return $this->ctf;
}
}

View File

@ -1,156 +0,0 @@
// Shortcut-function (until we update to Prototye v1.5)
if(typeof $$ != "Function") $$ = document.getElementsBySelector;
GB_OpenerObj = {};
GB_RefreshLink = "";
ComplexTableField = Class.create();
ComplexTableField.prototype = {
// These are defaults used if setPopupSize encounters errors
defaultPopupWidth: 560,
defaultPopupHeight: 390,
initialize: function() {
var rules = {};
rules['#'+this.id+' table.data a.popuplink'] = {onclick: this.openPopup.bind(this)};
// Assume that the delete link uses the deleteRecord method
rules['#'+this.id+' table.data a.deletelink'] = {onclick: this.deleteRecord.bind(this)};
// invoke row action-link based on default-action set in classname
var defaultAction = this.getDefaultAction();
if(defaultAction) {
rules['#'+this.id+' table.data tbody td'] = {
onclick: function(e) {
var elt = Event.element(e);
// Check the tag, as otherwise this
// function can take over checkbox
// click actions etc. See ticket #4737
if (elt.tagName != 'TD' && elt.tagName != 'TR') {
return;
}
var link = $$('.'+defaultAction, Event.element(e).parentNode)[0].href;
this.openPopup(null, link);
return false;
}.bind(this)
};
}
Behaviour.register('ComplexTableField_'+this.id,rules);
this.setPopupSize();
// HACK If already in a popup, we can't allow add (doesn't save existing relation correctly)
if(window != top) $$('#'+this.id+' table.data a.addlink').each(function(el) {Element.hide(el);});
},
setPopupSize: function() {
try {
this.popupHeight = parseInt(document.getElementById(this.id + '_PopupHeight').value);
this.popupWidth = parseInt(document.getElementById(this.id + '_PopupWidth').value);
} catch (ex) {
this.popupHeight = this.defaultPopupHeight;
this.popupWidth = this.defaultPopupWidth;
}
},
getDefaultAction: function() {
// try to get link class from <td class="action default"><a href="...
var links = $$('#'+this.id+' table.data tbody .default a');
// fall back to first given link
if(!links || !links[0]) links = $$('#'+this.id+' table.data tbody .action a');
return (links && links[0]) ? $A(Element.classNames(links[0])).last() : false;
},
/**
* @param href, table Optional dom object (use for external triggering without an event)
*/
openPopup: function(e, _popupLink, _table) {
// If already in a popup, simply open the link instead
// of opening a nested lightwindow
if(window != top) return true;
this.setPopupSize();
var el,type;
var popupLink = "";
if(_popupLink) {
popupLink = _popupLink;
table = _table;
} else {
// if clicked item is an input-element, don't trigger popup
var el = Event.element(e);
var input = Event.findElement(e,"input");
var tr = Event.findElement(e, "tr");
// stop on non-found lines
if(tr && Element.hasClassName(tr, 'notfound')) {
Event.stop(e);
return false;
}
// normal behaviour for input elements
if(el.nodeName == "INPUT" || input.length > 0) {
return true;
}
try {
var table = Event.findElement(e,"table");
if(Event.element(e).nodeName == "IMG") {
link = Event.findElement(e,"a");
popupLink = link.href + (link.href.match(/\?/) ? "&ajax=1" : "?ajax=1");
} else {
el = Event.findElement(e,"tr");
var link = $$("a",el)[0];
popupLink = link.href;
}
} catch(err) {
// no link found
Event.stop(e);
return false;
}
// no link found
if(!link || popupLink.length == 0) {
Event.stop(e);
return false;
}
}
if(this.GB_Caption) {
var title = this.GB_Caption;
} else {
// Getting the title from the URL is pretty ugly, but it works for now
type = popupLink.match(/[0-9]+\/([^\/?&]*)([?&]|$)/);
var title = (type && type[1]) ? type[1].ucfirst() : "";
}
// reset internal greybox callbacks, they are not properly unregistered
// and fire multiple times on each subsequent popup close action otherwise
if(GB_ONLY_ONE) GB_ONLY_ONE.callback_fn = [];
GB_show(
title,
popupLink,
this.popupHeight,
this.popupWidth,
this.refresh.bind(this)
);
if(e) {
Event.stop(e);
}
return false;
}
}
ComplexTableField.applyTo('div.ComplexTableField');
/**
* Get first letter as uppercase
*/
String.prototype.ucfirst = function () {
var firstLetter = this.substr(0,1).toUpperCase()
return this.substr(0,1).toUpperCase() + this.substr(1,this.length);
}

View File

@ -1,11 +0,0 @@
ComplexTableFieldPopupForm = Class.create();
ComplexTableFieldPopupForm.prototype = {
initialize: function() {
var rules = {};
Behaviour.register(rules);
}
}
ComplexTableFieldPopupForm.applyTo('#ComplexTableField_Popup_DetailForm');
ComplexTableFieldPopupForm.applyTo('#ComplexTableField_Popup_AddForm');

View File

@ -1,137 +0,0 @@
HasManyFileField = Class.create();
HasManyFileField.applyTo('div.hasmanyfile');
HasManyFileField.prototype = {
initialize: function() {
HasManyFileFieldAddButton.applyToChildren(this, 'a.addFile');
HasManyFileFieldRemoveButton.applyToChildren(this, 'a.removeFile');
HasManyFileFieldUploadButton.applyToChildren(this, 'a.uploadFile');
this.tree = document.getElementsByClassName('TreeDropdownField', this)[0];
this.list = this.getElementsByTagName('ul')[0];
this.uploadFolderID = this.getElementsByTagName('input')[0].value;
this.uploadMessage = document.getElementsByClassName('uploadMessage')[0];
}
}
HasManyFileFieldAddButton = Class.create();
HasManyFileFieldAddButton.prototype = {
onclick: function() {
tree = this.parentNode.parentNode.tree;
list = this.parentNode.parentNode.list;
fieldid = this.parentNode.parentNode.id;
fileid = tree.getElementsByTagName('input')[0].value;
name = tree.getElementsByTagName('span')[0].innerHTML;
input = document.createElement('input');
input.className = 'hidden';
input.type = 'hidden';
input.name = fieldid + '[]';
input.value = fileid;
text = document.createTextNode(name);
link = document.createElement('a');
link.appendChild(text);
removelink = document.createElement('a');
removelink.className = 'removeFile';
removelink.innerHTML = 'Remove file';
li = document.createElement('li');
li.appendChild(input);
li.appendChild(link);
li.appendChild(removelink);
list.appendChild(li);
HasManyFileFieldRemoveButton.applyTo(removelink);
return false;
}
}
HasManyFileFieldRemoveButton = Class.create();
HasManyFileFieldRemoveButton.prototype = {
onclick: function() {
li = this.parentNode;
list = this.parentNode.parentNode;
list.removeChild(li);
return false;
}
}
HasManyFileFieldUploadButton = Class.create();
HasManyFileFieldUploadButton.prototype = {
initialize: function() {
this.upload = new Upload({
fileUploadLimit : '6',
securityID : document.getElementById('SecurityID').value,
beginUploadOnQueue : true,
fileQueued : this.uploadFileQueuedCallback.bind(this),
fileComplete : this.uploadFileCompleteCallback.bind(this),
queueComplete : this.uploadQueueCompleteCallback.bind(this)
});
this.upload.setFolderID(this.parentNode.parentNode.uploadFolderID);
},
buildUI: function() {
},
onclick: function(event) {
Event.stop(event);
this.upload.browse();
},
uploadFileQueuedCallback: function(file,queueLength) {
var message = ss.i18n.sprintf(
ss.i18n._t('HASMANYFILEFIELD.UPLOADING', 'Uploading... %s'),
this.upload.getFilesToUpload()
);
this.parentNode.parentNode.uploadMessage.innerHTML = message;
},
uploadFileCompleteCallback: function(file,serverData) {
var message = ss.i18n.sprintf(
ss.i18n._t('HASMANYFILEFIELD.UPLOADING', 'Uploading... %s'),
this.upload.getFilesUploaded() + "/" + this.upload.getFilesToUpload()
);
this.parentNode.parentNode.uploadMessage.innerHTML = message;
idregex = /\/\* IDs: ([0-9,]+) \*\//;
ids = serverData.match(idregex);
fileid = ids[1];
nameregex = /\/\* Names: ([^\s]+) \*\//;
names = serverData.match(nameregex);
name = names[1];
fieldid = this.parentNode.parentNode.id;
list = this.parentNode.parentNode.list;
input = document.createElement('input');
input.className = 'hidden';
input.type = 'hidden';
input.name = fieldid + '[]';
input.value = fileid;
text = document.createTextNode(name);
link = document.createElement('a');
link.appendChild(text);
removelink = document.createElement('a');
removelink.className = 'removeFile';
removelink.innerHTML = 'Remove file';
li = document.createElement('li');
li.appendChild(input);
li.appendChild(link);
li.appendChild(removelink);
list.appendChild(li);
HasManyFileFieldRemoveButton.applyTo(removelink);
},
uploadQueueCompleteCallback: function() {
this.parentNode.parentNode.uploadMessage.innerHTML = '';
}
}

View File

@ -1,117 +0,0 @@
Event.observe( window, 'load', function() {
if(document.getElementById('sitetree')){
if(typeof document.getElementById('sitetree').observeMethod != 'undefined') {
document.getElementById('sitetree').observeMethod( 'NodeClicked' , function() {
checkedListNameArray = null;
checkedArray = null;
} );
}
}
} );
RelationComplexTableField = Class.create();
RelationComplexTableField.prototype = {
initialize: function() {
var checkedListNameArray = null;
var checkedListEndName = 'CheckedList';
var checkedListField = 'selected';
var checkedArray = null;
// 1) Find The Hidden Field Where The IDs Will Be Stored
var checkedList = document.getElementById( this.id + '_' + checkedListEndName );
// 2) Initialize The Array Or Update The Hidden Input Field And The HTML Table
var checkedListName = checkedList.getAttribute( 'name' );
//if( checkedListNameArray == null ) {
checkedListNameArray = [];
checkedListNameArray.push( checkedListName );
checkedArray = [];
if( checkedList.getAttribute( 'value' ) )
checkedArray.push( checkedList.getAttribute( 'value' ).split( ',' ) );
//}
/*
else if( checkedListNameArray.indexOf( checkedListName ) < 0 ) {
checkedListNameArray.push( checkedListName );
if( checkedList.getAttribute( 'value' ) )
checkedArray[ checkedListNameArray.length - 1 ] = checkedList.getAttribute( 'value' ).split( ',' );
}
else {
var index = checkedListNameArray.indexOf( checkedListName );
// a) Update The Hidden Input Field
checkedList.setAttribute( 'value', checkedArray[ index ] );
// b) Update The HTML Table
markingInputs = document.getElementsByName( checkedListName.substring( 0, checkedListName.indexOf( '[' ) ) + '[]' );
for( var i = 0; i < markingInputs.length; i++ ) {
markingInput = markingInputs[ i ];
if( checkedArray[ index ] && checkedArray[ index ].indexOf( markingInput.getAttribute( 'value' ) ) > -1 ) {
markingInput.setAttribute( 'checked', 'checked' );}
else
markingInput.removeAttribute( 'checked' );
}
} */
// 3) Create The Rules
var rules = {};
rules[ '#' + this.id + ' table.data tbody td.markingcheckbox input' ] = {
onclick : function() {
// 1) Find The Hidden Field Where The IDs Will Be Stored
var checkedListName = this.getAttribute( 'name' );
checkedListName = checkedListName.substring( 0, checkedListName.length - 1 ) + checkedListField + ']';
var inputs = document.getElementsByTagName( 'input' );
for( var i = 0; i < inputs.length; i++ ) {
var checkedList = inputs[ i ];
if( checkedList.getAttribute( 'name' ) == checkedListName )
break;
}
var index = checkedListNameArray.indexOf( checkedListName );
// 2) Update The Array
if( checkedArray[ index ] && checkedArray[ index ].indexOf( this.getAttribute( 'value' ) ) > -1 ) {
index2 = checkedArray[ index ].indexOf( this.getAttribute( 'value' ) );
var previousCheckedArray = checkedArray[ index ];
checkedArray[ index ] = [];
for( var i = 0; i < previousCheckedArray.length; i++ ) {
if( i != index2 )
checkedArray[ index ].push( previousCheckedArray[ i ] );
}
if( this.getAttribute( 'type' ) == 'radio' )
this.checked = false;
}
else if( checkedArray[ index ] ) {
if( this.getAttribute( 'type' ) == 'radio' )
checkedArray[ index ] = [];
checkedArray[ index ].push( this.getAttribute( 'value' ) );
}
else {
checkedArray[ index ] = [];
checkedArray[ index ].push( this.getAttribute( 'value' ) );
}
// 3) Update The Hidden Input Field
checkedList.setAttribute( 'value', checkedArray[ index ] );
}
};
Behaviour.register( rules );
}
}
RelationComplexTableField.applyTo('#Form_EditForm div.HasOneComplexTableField');
RelationComplexTableField.applyTo('#Form_EditForm div.HasManyComplexTableField');
RelationComplexTableField.applyTo('#Form_EditForm div.ManyManyComplexTableField');

View File

@ -1,191 +0,0 @@
/**
* Javascript for TableField. allows the deletion of records via
* AJAX, and the addition of rows via javasript.
*
* TODO relies on include-order at the moment to override actions :/
*/
TableField = Class.create();
TableField.prototype = {
newRowID: 1,
/**
* Applies behaviour to the delete button for deleting objects via ajax.
*/
initialize: function() {
var rules = {};
rules['#'+this.id+' table.data a.deletelink'] = {
onclick: this.deleteRecord.bind(this)
};
rules['#'+this.id+' table.data a.addrow'] = {
onclick: this.addRow.bind(this)
};
Behaviour.register('TableField_'+this.id,rules);
},
/**
* Deletes the given dataobject record via an ajax request. If the record doesn't have any
* information in it, it just removes it from the form.
* to tablefield->Delete()
* @param {Object} e
*/
deleteRecord: function(e) {
var img = Event.element(e);
var link = Event.findElement(e,"a");
var row = Event.findElement(e,"tr");
var params = link.getAttribute("href").toQueryParams();
var isEmpty = true;
var recordID = row.getRecordId();
var self = this;
// Check to see if there is a dataobject to delete first, otherwise remove the row.
// or: Check if a childID is set (not present on new items)
if(
(this.hasNoValues(row,"input") && this.hasNoValues(row,"select") && this.hasNoValues(row,"textarea"))
|| params["childID"] <= 0 || (recordID <= 0 || recordID == false)
){
if( row.parentNode.getElementsByTagName('tr').length > 1 ) {
jQuery(row).fadeOut();
} else {
// clear all fields in the row
var fields = row.getElementsByTagName('input');
if( fields )
for( var i = 0; i < fields.length; i++ ) {
fields[i].value = '';
}
}
Event.stop(e);
return false;
}
// TODO ajaxErrorHandler and loading-image are dependent on cms, but formfield is in framework
var confirmed = confirm(ss.i18n._t('TABLEFIELD.DELETECONFIRMMESSAGE', 'Are you sure you want to delete this record?'));
if(confirmed){
img.setAttribute("src",'framework/images/network-save.gif'); // TODO doesn't work
jQuery.ajax({
'url': link.getAttribute("href"),
'method': 'post',
'data': {ajax: 1, 'SecurityID': document.getElementById('SecurityID') ? document.getElementById('SecurityID').value : null},
'success': function(response){
jQuery(row).fadeOut('fast', function() {
// remove row from DOM
this.element.parentNode.removeChild(obj.element);
// recalculate summary if needed (assumes that TableListField.js is present)
// TODO Proper inheritance
if(self._summarise) self._summarise();
// custom callback
if(self.callback_deleteRecord) self.callback_deleteRecord(e);
});
},
'error': ajaxErrorHandler
});
}
Event.stop(e);
return false;
},
/**
*
*
* @param {Object} element
* @param {Object} tagName
*/
hasNoValues: function(element,tagName){
elements = element.getElementsByTagName(tagName);
if(elements.length >= 1){
var isEmpty = true;
for(var i = 0; i < elements.length;i++){
if(elements[i].type != "hidden"){
if(elements[i].value != null && elements[i].value != ""){
isEmpty = false;
}
}
}
return isEmpty;
}else{
return true;
}
},
/**
* Appends a new row to the DOM.
*
* @param {Object} tableid
*/
addRow: function (e){
var table = Event.findElement(e,"table");
if(table){
// Clone the last TR
var tbody = table.tBodies[0];
var numRows = tbody.rows.length;
var newRow = tbody.rows[0].cloneNode(true);
// Get the input elements in this new row
var inputs = newRow.getElementsByTagName('input');
// For every input, set it's value to blank if it is not hidden
for(var i = 0; i < inputs.length; i++) {
if(inputs[i].type != 'hidden') {
inputs[i].value = ""
};
}
this.newRowID++;
if(newRow.id != "new"){
this.resetNames(newRow);
}
// Make sure all inputs have unique IDs
for(var i = 0; i < inputs.length; i++) {
inputs[i].id += "-" + this.newRowID;
}
// Change the ID to a unique one
newRow.id = "New_" + this.newRowID;
// Append the new row to the DOM
table.tBodies[0].appendChild(newRow);
Behaviour.apply(table);
}
Event.stop(e);
},
/**
* resets the names for all elements inside a row.
* @param {Object} row
*/
resetNames: function(row){
// Support for addressing the ID's appropriately.
for(i = 0; i < row.cells.length;i++){
for(b=0; b < row.cells[i].childNodes.length;b++){
inputElement = row.cells[i].childNodes[b];
if(inputElement.type != 'hidden') inputElement.value = "";
if(inputElement.name != null){
if(inputElement.name.substr(inputElement.name.length - 2,inputElement.name.length) != "[]"){
inputElement.name =
inputElement.name.substr(0,inputElement.name.indexOf('[')+1) + "new" +
inputElement.name.substr(inputElement.name.indexOf(']'),inputElement.name.length) + "[]";
}else{
inputElement.name =
inputElement.name.substr(0,inputElement.name.indexOf('[')+1) + "new" +
inputElement.name.substr(inputElement.name.indexOf(']'),inputElement.name.length);
}
}
}
}
}
}
TableField.applyTo('div.TableField');
if(typeof ajaxErrorHandler == 'undefined'){
ajaxErrorHandler = function(response) {
alert(response.responseText);
}
}

View File

@ -1,374 +0,0 @@
// Shortcut-function (until we update to Prototye v1.5)
if(typeof $$ != "Function") $$ = document.getElementsBySelector;
TableListField = Class.create();
TableListField.prototype = {
errorMessage: "Error talking to server",
initialize: function() {
var rules = {};
rules['#'+this.id+' table.data a.deletelink'] = {
onclick: this.deleteRecord.bind(this)
};
rules['#'+this.id+' th a'] = {
onclick: this.refresh.bind(this)
};
rules['#'+this.id+' th'] = {
initialize: function() {
var sortLinks = $$('span.sortLinkHidden a', this);
if(sortLinks) sortLinks[0].style.visibility = 'hidden';
},
onmouseover: function(e) {
var sortLinks = $$('span.sortLinkHidden a', this);
if(sortLinks) sortLinks[0].style.visibility = 'visible';
},
onmouseout: function(e) {
var sortLinks = $$('span.sortLinkHidden a', this);
if(sortLinks) sortLinks[0].style.visibility = 'hidden';
}
};
rules['#'+this.id+' div.PageControls a'] = {onclick: this.refresh.bind(this)};
rules['#'+this.id+' table.data tr td.markingcheckbox'] = {
onclick : function(e) {
// do nothing for clicks in marking box cells (e.g. if checkbox is missed)
}
};
// rules for selection options on click event
rules['#'+this.id+' .selectOptions a'] = {
onclick: this.markRecords.bind(this)
};
// initialize summary (if needed)
// TODO Breaks with nested divs
var summaryCols = $$('tfoot tr.summary td', this);
this._summaryDefs = [];
//should check summaryCols.length, because summaryCols will always be an array, though its length could be 0.
if(summaryCols && summaryCols.length) {
rules['#'+this.id+' table.data tbody input'] = {
onchange: function(e) {
if (!e) e = window.event; // stupid IE
// workaround for wrong scope with bind(this) and applyTo()
var root = Event.findElement(e,'div');
// TODO Fix slow $$()-calls and re-enable clientside summaries
//root._summarise();
}
};
rules['#'+this.id+' table.data tbody select'] = {
onchange: function(e) {
if (!e) e = window.event; // stupid IE
// workaround for wrong scope with bind(this) and applyTo()
var root = Event.findElement(e,'div');
// TODO Fix slow $$()-calls and re-enable clientside summaries
//root._summarise();
}.bind(this)
};
}
Behaviour.register('TableListField_'+this.id,rules);
/*
if(summaryCols.length) {
this._getSummaryDefs(summaryCols);
}
*/
},
/**
* Deletes the given dataobject record via an ajax request
* to complextablefield->Delete()
* @param {Object} e
*/
deleteRecord: function(e) {
var img = Event.element(e);
var link = Event.findElement(e,"a");
var row = Event.findElement(e,"tr");
var self = this;
// TODO ajaxErrorHandler and loading-image are dependent on cms, but formfield is in framework
var confirmed = confirm(ss.i18n._t('TABLEFIELD.DELETECONFIRMMESSAGE', 'Are you sure you want to delete this record?'));
if(confirmed)
{
img.setAttribute("src",'framework/images/network-save.gif'); // TODO doesn't work
jQuery.ajax({
'url': link.getAttribute("href"),
'method': 'post',
'data': {forceajax: 1, SecurityID: jQuery('input[name=SecurityID]').val()},
'success': function(){
jQuery(row).remove();
// recalculate summary if needed (assumes that TableListField.js is present)
// TODO Proper inheritance
if(self._summarise) self._summarise();
// custom callback
if(self.callback_deleteRecord) self.callback_deleteRecord(e);
},
'error': this.ajaxErrorHandler
});
}
Event.stop(e);
},
removeById: function(id) {
var el = jQuery('#record-' + this.id + '-' + id)[0];
if(el) el.parentNode.removeChild(el);
this._summarise();
},
/**
* according to the clicked element in "Select bar", mark records that have same class as the element.
*/
markRecords: function(e){
var el = Event.element(e);
if(el.nodeName != "a") el = Event.findElement(e,"a");
if(el.rel == "all"){
this.markAll();
}else if(el.rel == 'none') {
this.unmarkAll();
}else{
this.unmarkAll();
var records = jQuery('#' + this.id + ' td.' + el.rel + ' input.checkbox');
var i=0;
for(i; i<records.length; i++){
records[i].checked = 'checked';
}
}
return false;
},
/**
* mark all record in current view of the table
*/
markAll: function(e){
var records = $$('#'+this.id+' td.markingcheckbox input.checkbox');
var i=0;
for(i; i<records.length; i++){
records[i].checked = 'checked';
}
},
/**
* unmark all records in current view of the table
*/
unmarkAll: function(e){
var records = $$('#'+this.id+' td.markingcheckbox input.checkbox');
var i=0;
for(i; i<records.length; i++){
records[i].checked = '';
}
},
refresh: function(e) {
var self = this;
if(e) {
var el = Event.element(e);
if(el.nodeName != "a") el = Event.findElement(e,"a");
} else {
var el = jQuery('#' + this.id)[0];
}
if(el.getAttribute('href')) {
jQuery.ajax({
'url': el.getAttribute('href'),
'data': {'update': 1},
'success': function(response) {
jQuery('#' + self.id).replaceWith(response)
// reapply behaviour and reattach methods to TF container node
// e.g. <div class="TableListField">
Behaviour.apply(jQuery('#' + self.id)[0], true);
}
});
}
if(e) Event.stop(e);
return false;
},
ajaxErrorHandler: function(response) {
if(typeof(window.ajaxErrorHandler) == 'function') {
window.ajaxErrorHandler();
} else {
alert(this.errorMessage);
}
},
_getSummaryDefs: function(summaryCols) {
summaryCols.each(function(col, pos) {
if( col ) {
var func = this._getSummaryFunction(col.className);
this._summaryDefs[pos] = {col: col, pos: pos, func: func};
}
}.bind(this));
this._summarise();
},
_summarise: function() {
var rows = $$('tbody tr', this);
if(!rows) return false;
var columnData = [];
// prepare the array (gets js-errors otherwise)
var cols = $$('td', rows[0]);
for(colPos=0; colPos<cols.length; colPos++) {
columnData[colPos] = [];
}
for(rowPos=0; rowPos<rows.length; rowPos++) {
// avoid wrong calculations for nested lists
if(Element.hasClassName(rows[rowPos], "subitem")) continue;
var cols = $$('td', rows[rowPos]);
var colPos = 0;
for(colPos; colPos<cols.length; colPos++) {
//if(!columnData[colPos]) columnData[colPos] = [];
if(this._summaryDefs[colPos] && this._summaryDefs[colPos].func) {
columnData[colPos][rowPos] = this._getValue(cols[colPos]);
} else {
columnData[colPos][rowPos] = "";
}
}
}
for(colPos=0; colPos<columnData.length; colPos++) {
if(this._summaryDefs[colPos] && this._summaryDefs[colPos].func) {
var summaryVal = this._summaryDefs[colPos].func.apply(this,[columnData[colPos]]);
this._summaryDefs[colPos].col.innerHTML = summaryVal;
}
}
},
_getSummaryFunction: function(func) {
if(this[func] && typeof this[func] == "function") {
// local
return this[func];
} else if(window[func] && typeof window[func] == "function") {
// global
return window[func];
} else {
// not existing
return false
}
},
_getValue: function(col) {
var inputNode = $$('input', col);
if(inputNode[0]) {
return $F(inputNode[0]);
}
var selectNode = $$('select', col);
if(selectNode[0]) {
return $F(selectNode[0]);
}
return col.innerHTML.stripTags();
},
/**
* ############# Summary Functions ##############
*/
sum: function(arr) {
var sum = 0;
arr.each(function(val) {
sum += val*1; // convert to float
});
return sum;
},
sumCurrency: function(arr) {
var sum = 0;
arr.each(function(val) {
if(!val) return;
val = val.replace(/\$/,'');
val = val.replace(/\,/,'');
sum+= val*1; // convert to float
});
return sum.toCurrency();
},
max: function(arr) {
return arr.max();
},
min: function(arr) {
return arr.min();
}
}
TableListRecord = Class.create();
TableListRecord.prototype = {
onmouseover : function() {
Element.addClassName(this,'over');
},
onmouseout : function() {
Element.removeClassName(this,'over');
},
ajaxRequest : function(url, subform) {
var self = this;
// Highlight the new row
if(this.parentNode.selectedRow) {
Element.removeClassName(this.parentNode.selectedRow,'current');
}
this.parentNode.selectedRow = this;
Element.addClassName(this,'current');
this.subform = document.getElementById(subform);
Element.addClassName(this, 'loading');
statusMessage('loading');
jQuery.ajax({
'url': url + this.id.replace('record-',''),
'method' : 'post',
'data' : {'ajax': 1},
success : function() {
self.select_success();
},
failure : ajaxErrorHandler
});
},
getRecordId: function(){
parts = this.id.match( /.*[\-]{1}(\d+)$/ );
if(parts) return parts[1];
else return false;
},
select_success : function(response) {
Element.removeClassName(this, 'loading');
this.subform.loadNewPage(response.responseText);
statusMessage('loaded','good');
}
}
TableListRecord.applyTo('div.TableListField tr');
TableListField.applyTo('div.TableListField');
Number.prototype.CURRENCIES = {
en_GB: '$ ###,###.##'
};
/**
* Caution: Not finished!
* @param iso string (Not used) Please use in combination with Number.CURRENCIES to achieve i18n
* @return string
*
* @see http://www.jibbering.com/faq/faq_notes/type_convert.html
* @see http://www.rgagnon.com/jsdetails/js-0063.html
* @see http://www.mredkj.com/javascript/nfdocs.html
*/
Number.prototype.toCurrency = function(iso) {
if(!iso) iso = SS_DEFAULT_ISO;
// TODO stub, please implement properly
return "$" + this.toFixed(2);
}

View File

@ -81,18 +81,6 @@ en:
CheckboxField:
NO: No
YES: Yes
ComplexTableField:
CLOSEPOPUP: 'Close Popup'
SUCCESSADD2: 'Added {name}'
SUCCESSEDIT: 'Saved %s %s %s'
ComplexTableField.ss:
ADDITEM: 'Add %s'
NOITEMSFOUND: 'No items found'
SORTASC: 'Sort ascending'
SORTDESC: 'Sort descending'
ComplexTableField_popup.ss:
NEXT: Next
PREVIOUS: Previous
ConfirmedPasswordField:
ATLEAST: 'Passwords must be at least {min} characters long.'
BETWEEN: 'Passwords must be {min} to {max} characters long.'
@ -175,18 +163,7 @@ en:
XlsType: 'Excel spreadsheet'
ZipType: 'ZIP compressed file'
FileIFrameField:
ATTACH: 'Attach {type}'
ATTACHONCESAVED: '{type}s can be attached once you have saved the record for the first time.'
ATTACHONCESAVED2: 'Files can be attached once you have saved the record for the first time.'
DELETE: 'Delete {type}'
DISALLOWEDFILETYPE: 'This filetype is not allowed to be uploaded'
FILE: File
FROMCOMPUTER: 'From your Computer'
FROMFILESTORE: 'From the File Store'
NOSOURCE: 'Please select a source file to attach'
REPLACE: 'Replace {type}'
FileIFrameField_iframe.ss:
TITLE: 'Image Uploading Iframe'
Filesystem:
SYNCRESULTS: 'Sync complete: {createdcount} items created, {deletedcount} items deleted'
Folder:
@ -312,8 +289,6 @@ en:
Image:
PLURALNAME: Files
SINGULARNAME: File
ImageField:
IMAGE: Image
Image_Cached:
PLURALNAME: Files
SINGULARNAME: File
@ -476,10 +451,6 @@ en:
UserPermissionsIntro: 'Assigning groups to this user will adjust the permissions they have. See the groups section for details of permissions on individual groups.'
PhoneNumberField:
VALIDATION: 'Please enter a valid phone number'
RelationComplexTableField.ss:
ADD: Add
CSVEXPORT: 'Export to CSV'
NOTFOUND: 'No items found'
Security:
ALREADYLOGGEDIN: 'You don''t have access to this page. If you have another account that can access that page, you can log in again below.'
BUTTONSEND: 'Send me the password reset link'
@ -517,24 +488,12 @@ en:
FileFieldLabel: 'CSV File <small>(Allowed extensions: *.csv)</small>'
SilverStripeNavigator:
Edit: Edit
SimpleImageField:
NOUPLOAD: 'No Image Uploaded'
SiteTree:
TABMAIN: Main
TableField:
ISREQUIRED: 'In %s ''%s'' is required'
TableField.ss:
ADD: 'Add a new row'
ADDITEM: 'Add %s'
TableListField:
CSVEXPORT: 'Export to CSV'
PRINT: Print
Print: Print
SELECT: 'Select:'
TableListField.ss:
NOITEMSFOUND: 'No items found'
SORTASC: 'Sort in ascending order'
SORTDESC: 'Sort in descending order'
TableListField_PageControls.ss:
DISPLAYING: Displaying
OF: of

View File

@ -1,16 +0,0 @@
/* table */
.ComplexTableField {
margin-bottom: 10px;
tbody td {
cursor: pointer;
}
tbody td.markingcheckbox {
cursor: default;
}
}
.ui-dialog .ctf-dialog.ui-dialog-content {
padding-right: 0; /* scrollbars */
}

View File

@ -1,124 +0,0 @@
#right form .hasmanyfile a.addFile,
#right form .hasmanyfile a.uploadFile,
#right form .hasmanyfile .currentFiles li {
font-size: 1.2em;
padding-left: 3px;
}
#right form .hasmanyfile a.uploadFile {
border-color:#CCCCCC rgb(153, 153, 153) rgb(153, 153, 153) rgb(204, 204, 204);
border-style:solid;
border-width: 2px;
color:#333333;
cursor:pointer;
font-size:11px;
font-weight: bold;
position: relative;
top: -29px;
left: 344px;
text-decoration: none;
overflow:visible;
padding:3px 5px;
float:left;
width:auto;
}
#right form .hasmanyfile a.removeFile {
border-color:#CCCCCC rgb(153, 153, 153) rgb(153, 153, 153) rgb(204, 204, 204);
border-style:solid;
border-width: 2px;
color:#333333;
cursor:pointer;
font-size:10px;
font-weight: bold;
text-decoration: none;
overflow:visible;
padding:3px 5px;
width:auto;
margin: -2px 0 0 10px;
}
#right form .hasmanyfile a.removeFile:hover {
background: #CE0000;
color: #fff;
}
#right form .hasmanyfile a.addFile {
border-color:#CCCCCC rgb(153, 153, 153) rgb(153, 153, 153) rgb(204, 204, 204);
border-style:solid;
border-width: 2px;
color:#333333;
cursor:pointer;
font-size:11px;
font-weight: bold;
position: relative;
top: -29px;
left: 340px;
text-decoration: none;
overflow:visible;
padding:3px 5px;
float:left;
width:auto;
}
#right form .hasmanyfile a.addFile:hover,
#right form .hasmanyfile a.uploadFile:hover {
background: #fff;
}
#right form .hasmanyfile ul.currentFiles {
padding-bottom: 5px;
}
#right form .hasmanyfile .currentFiles li {
height: 30px;
line-height: 30px;
}
#right form .hasmanyfile .clear {
clear: both;
}
/* ICONS */
#right form .hasmanyfile .currentFiles a[href$=".pdf"],
#right form .hasmanyfile .currentFiles a[href$=".PDF"],
#right form .hasmanyfile .currentFiles a.pdf {
padding: 2px;
padding-left: 20px;
background: url(../images/icons/page_white_acrobat.png) no-repeat left center;
}
#right form .hasmanyfile .currentFiles a[href$=".doc"],
#right form .hasmanyfile .currentFiles a[href$=".DOC"],
#right form .hasmanyfile .currentFiles a.doc {
padding: 2px;
padding-left: 20px;
background: url(../images/icons/page_word.png) no-repeat left center;
}
#right form .hasmanyfile .currentFiles a[href$=".xls"],
#right form .hasmanyfile .currentFiles a[href$=".XLS"],
#right form .hasmanyfile .currentFiles a.xls {
padding: 2px;
padding-left: 20px;
background: url(../images/icons/page_excel.png) no-repeat left center;
}
#right form .hasmanyfile .currentFiles a[href$=".gz"],
#right form .hasmanyfile .currentFiles a[href$=".GZ"],
#right form .hasmanyfile .currentFiles a[href$=".gzip"],
#right form .hasmanyfile .currentFiles a[href$=".GZIP"],
#right form .hasmanyfile .currentFiles a[href$=".zip"],
#right form .hasmanyfile .currentFiles a[href$=".ZIP"],
#right form .hasmanyfile .currentFiles a.archive {
padding: 2px;
padding-left: 20px;
background: url(../images/icons/page_white_zip.png) no-repeat left center;
}
#right form .hasmanyfile .currentFiles a[href$=".jpg"],
#right form .hasmanyfile .currentFiles a[href$=".JPG"],
#right form .hasmanyfile .currentFiles a[href$=".gif"],
#right form .hasmanyfile .currentFiles a[href$=".GIF"],
#right form .hasmanyfile .currentFiles a[href$=".png"],
#right form .hasmanyfile .currentFiles a[href$=".PNG"],
#right form .hasmanyfile .currentFiles a.image {
padding: 2px;
padding-left: 20px;
background: url(../images/icons/icon-jpg.gif) no-repeat left center;
}
#right form .hasmanyfile .currentFiles a[href$=".exe"],
#right form .hasmanyfile .currentFiles a[href$=".EXE"],
#right form .hasmanyfile .currentFiles a.application {
padding: 2px;
padding-left: 20px;
background: url(../images/icons/application.png) no-repeat left center;
}

View File

@ -1,226 +0,0 @@
table.TableField,
table.TableListField,
.TableListField table.data,
table.CMSList {
border-collapse: collapse;
border-spacing: 0;
width : 100%;
}
/* Preventing IE6 from showing double borders */
body>div table.TableField,
body>div table.TableListField,
body>div .TableListField table.data,
body>div table.CMSList {
border-collapse: separate;
}
table.TableField td,
table.TableListField td,
.TableListField table.data td,
table.CMSList td {
border-style:none;
}
table.TableField th,
table.TableListField th,
.TableListField table.data th,
table.CMSList th {
white-space: nowrap;
}
table.TableField thead th,
.TableListField table.data thead th,
table.CMSList thead th {
white-space: nowrap;
padding: 3px;
font-size: 12px;
text-align: left;
}
table.TableField thead th span,
.TableListField table.data thead th span {
font-size: 12px;
}
table.TableField thead th span.sortLink,
.TableListField table.data thead th span.sortLink,
table.CMSList thead th span.sortLink {
overflow: hidden;
}
table.TableField tbody td,
table.TableField tfoot td,
.TableListField table.data tbody td,
.TableListField table.data tfoot td,
table.CMSList tbody td,
table.CMSList tfoot td {
padding: 2px 4px;
}
.TableListField table.data tfoot tr.addtogrouprow td {
padding:3px;
}
.TableListField table.data tfoot .actions {
float: none;
}
.TableListField table.data tfoot tr.addtogrouprow input {
width: 94%;
}
.TableField td input,
.TableListField td input {
width: 98%;
}
table.data tbody td input,
table.data tbody td textarea {
border:0 !important;
}
table.TableField tbody td.checkbox,
.TableListField table.data tbody td.checkbox,
table.CMSList tbody td.checkbox {
padding-left : 5px;
background-image : url(../images/checkbox.png);
background-repeat : repeat-x;
background-position : left bottom;
}
.TableListField table.data tfoot .addlink img {
vertical-align: middle;
margin: 3px 6px 3px 3px;
}
.TableListField table.data tfoot tr td a {
text-decoration: none;
}
.TableListField table.data tbody tr td a:hover,
.TableListField table.data tfoot tr td a:hover {
background: none;
}
/**
* Show a loading indication on a TableListField row
*/
.TableListField tr.loading td.first {
padding-left: 22px;
background: url(../images/network-save.gif) 3px 2px no-repeat;
}
.right form .TableField span.readonly {
border: 0;
background: none;
padding: 0;
margin-bottom: 0;
}
.right form .TableListField td {
background: #fff;
}
.TableListField div.utility {
overflow: auto;
}
.TableListField div.utility .item {
margin-top: 1em;
padding: 3px 0 6px 0;
display: block;
float: left;
}
.TableListField div.utility a {
text-decoration: none;
color: #333;
cursor: pointer;
font-size: 11px;
margin-right: 2px;
overflow: visible;
padding: 3px 2px;
width: auto;
}
form .TableField .message {
width: auto;
}
.TableListField .selectOptions {
overflow: auto;
font: 1.3em;
margin: 0;
padding: 0;
}
.TableListField .selectOptions li {
float: left;
margin: 0px 5px;
}
.TableListField .PageControls {
margin: 5px 0;
text-align:center;
display:block;
margin-bottom: 5px;
position: relative;
}
.TableListField .PageControls * {
display:inline;
vertical-align: middle;
font-weight: bold;
}
.TableListField .PageControls .Last{
display: block;
width: 40px;
text-align: right;
position: absolute;
right: 0px;
top: 0px;
}
.TableListField .PageControls .First{
float:left; display:block;
width:40px; text-align:left;
}
#Pagination {
margin-top: 10px;
margin-left: auto;
margin-right: auto;
a {
font-size: 14px;
width: 1px;
height: 1px;
margin: 1px;
}
span {
display: inline;
font-size: 14px;
}
div {
display: inline;
}
}
#Pagination_Next a {
text-decoration: none;
}
#Pagination_Next a div {
position: relative;
left: -20px;
}
#Pagination_Next a img {
position: relative;
top: -15px;
left: 5px;
}
#Pagination_Previous a {
text-decoration: none;
}
#Pagination_Previous a img {
position: relative;
top: -15px;
left: 35px;
}

View File

@ -1,82 +0,0 @@
<div id="$id" class="$CSSClasses $extraClass field nolabel" href="$CurrentLink">
<div class="middleColumn">
<% if Markable %>
<% include TableListField_SelectOptions %>
<% end_if %>
<% include TableListField_PageControls %>
<table class="data">
<thead>
<tr>
<% if Markable %><th width="18">&nbsp;</th><% end_if %>
<% loop Headings %>
<th class="$Name">
<% if IsSortable %>
<span class="sortTitle">
<a href="$SortLink">$Title</a>
</span>
<span class="sortLink <% if SortBy %><% else %>sortLinkHidden<% end_if %>">
<a href="$SortLink">
<% if SortDirection = desc %>
<img src="$ModulePath(framework)/images/bullet_arrow_up.png" alt="<% _t('ComplexTableField.ss.SORTASC', 'Sort ascending') %>" />
<% else %>
<img src="$ModulePath(framework)/images/bullet_arrow_down.png" alt="<% _t('ComplexTableField.ss.SORTDESC', 'Sort descending') %>" />
<% end_if %>
</a>
&nbsp;
</span>
<% else %>
$Title
<% end_if %>
</th>
<% end_loop %>
<% loop Actions %><th width="18">&nbsp;</th><% end_loop %>
</tr>
</thead>
<tfoot>
<% if HasSummary %>
<tr class="summary">
<% if Markable %><th width="18">&nbsp;</th><% end_if %>
<td><i>$SummaryTitle</i></td>
<% loop SummaryFields %>
<td<% if Function %> class="$Function"<% end_if %>>$SummaryValue</td>
<% end_loop %>
<% loop Actions %><td width="18">&nbsp;</td><% end_loop %>
</tr>
<% end_if %>
<% if Can(add) %>
<tr>
<% if Markable %><td width="18">&nbsp;</td><% end_if %>
<td colspan="$ItemCount">
<input type="hidden" id="{$id}_PopupHeight" value="$PopupHeight" disabled="disabled">
<input type="hidden" id="{$id}_PopupWidth" value="$PopupWidth" disabled="disabled">
<a class="popuplink addlink" href="$AddLink" alt="add"><img src="$ModulePath(framework)/images/add.gif" alt="<% _t('ComplexTableField.ss.ADDITEM', 'add') %>" />
<% sprintf(_t('ADDITEM', 'Add %s', 'Add [name]'),$Title) %>
</a>
</td>
<% loop Actions %><td width="18">&nbsp;</td><% end_loop %>
</tr>
<% end_if %>
</tfoot>
<tbody>
<% if Items %>
<% loop Items %>
<% include TableListField_Item %>
<% end_loop %>
<% else %>
<tr class="notfound">
<% if Markable %><th width="18">&nbsp;</th><% end_if %>
<td colspan="$Headings.Count"><i><% _t('ComplexTableField.ss.NOITEMSFOUND', 'No items found') %></i></td>
<% loop Actions %><td width="18">&nbsp;</td><% end_loop %>
</tr>
<% end_if %>
</tbody>
</table>
<% if Utility %>
<div class="utility">
<% loop Utility %>
<span class="item"><a href="$Link" target="_blank">$Title</a></span>
<% end_loop %>
</div>
<% end_if %>
</div>
</div>

View File

@ -1,42 +0,0 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7" />
<% base_tag %>
</head>
<body class="cms" style="overflow: auto;">
<div class="right $PopupClasses">
$DetailForm
</div>
<% if ShowPagination %>
<table id="ComplexTableField_Pagination">
<tr>
<% if Paginator.PrevLink %>
<td id="ComplexTableField_Pagination_Previous">
<a href="$Paginator.PrevLink"><img src="$ModulePath(framework)/images/pagination/record-prev.png" /><% _t('ComplexTableField_popup.ss.PREVIOUS', 'Previous') %></a>
</td>
<% end_if %>
<% if xdsfdsf %>
<% else %>
<td>
<% loop Paginator.Pages %>
<% if active %>
<a href="$link">$number</a>
<% else %>
<span>$number</span>
<% end_if %>
<% end_loop %>
</td>
<% end_if %>
<% if Paginator.NextLink %>
<td id="ComplexTableField_Pagination_Next">
<a href="$Paginator.NextLink"><% _t('ComplexTableField_popup.ss.NEXT', 'Next') %><img src="$ModulePath(framework)/images/pagination/record-next.png" /></a>
</td>
<% end_if %>
</tr>
</table>
<% end_if %>
</body>
</html>

View File

@ -1,67 +0,0 @@
<div id="$id" class="$CSSClasses $extraClass field" href="$CurrentLink">
<% if Markable %>
<% include TableListField_SelectOptions %>
<% end_if %>
<% include TableListField_PageControls %>
<table class="data">
<thead>
<tr>
<% if Markable %><th width="18">&nbsp;</th><% end_if %>
<% loop Headings %>
<th class="$Name">$Title</th>
<% end_loop %>
<% if Can(show) %><th width="18">&nbsp;</th><% end_if %>
<% if Can(edit) %><th width="18">&nbsp;</th><% end_if %>
<% if Can(delete) %><th width="18">&nbsp;</th><% end_if %>
</tr>
</thead>
<tfoot>
<% if HasSummary %>
<tr class="summary">
<% if Markable %><th width="18">&nbsp;</th><% end_if %>
<td><i>$SummaryTitle</i></td>
<% loop SummaryFields %>
<td<% if Function %> class="$Function"<% end_if %>>&nbsp;</td>
<% end_loop %>
<% if Can(show) %><td width="18">&nbsp;</td><% end_if %>
<% if Can(edit) %><td width="18">&nbsp;</td><% end_if %>
<% if Can(delete) %><td width="18">&nbsp;</td><% end_if %>
</tr>
<% end_if %>
<% if Can(add) %>
<tr>
<% if Markable %><td width="18">&nbsp;</td><% end_if %>
<td colspan="$ItemCount">
<input type="hidden" id="{$id}_PopupHeight" value="$PopupHeight" disabled="disabled">
<input type="hidden" id="{$id}_PopupWidth" value="$PopupWidth" disabled="disabled">
<a class="popuplink addlink" href="$AddLink" alt="<% _t('RelationComplexTableField.ss.ADD', 'Add') %>"><img src="$ModulePath(framework)/images/add.gif" alt="<% _t('ADD', 'Add') %>" /><% _t('RelationComplexTableField.ss.ADD', 'Add') %> $Title</a>
</td>
<% if Can(show) %><td width="18">&nbsp;</td><% end_if %>
<% if Can(edit) %><td width="18">&nbsp;</td><% end_if %>
<% if Can(delete) %><td width="18">&nbsp;</td><% end_if %>
</tr>
<% end_if %>
</tfoot>
<tbody>
<% if Items %>
<% loop Items %>
<% include TableListField_Item %>
<% end_loop %>
<% else %>
<tr class="notfound">
<% if Markable %><th width="18">&nbsp;</th><% end_if %>
<td colspan="$Headings.Count"><i><% _t('RelationComplexTableField.ss.NOTFOUND', 'No items found') %></i></td>
<% if Can(show) %><td width="18">&nbsp;</td><% end_if %>
<% if Can(edit) %><td width="18">&nbsp;</td><% end_if %>
<% if Can(delete) %><td width="18">&nbsp;</td><% end_if %>
</tr>
<% end_if %>
</tbody>
</table>
$ExtraData
<div class="utility">
<% if Can(export) %>
<a href="$ExportLink" target="_blank"><% _t('RelationComplexTableField.ss.CSVEXPORT', 'Export to CSV' ) %></a>
<% end_if %>
</div>
</div>

View File

@ -1,71 +0,0 @@
<div id="$id" class="$CSSClasses field">
<div class="middleColumn">
<% include TableListField_PageControls %>
<% if Message %>
<p id="{$id}_error" class="message $MessageType">$Message</p>
<% else %>
<p id="{$id}_error" class="message $MessageType" style="display: none"></p>
<% end_if %>
<table class="data">
<thead>
<tr>
<% loop Headings %>
<th class="$Name $Class" scope="col">$Title</th>
<% end_loop %>
<th style="display: none"></th>
<% if Can(delete) %><th width="18">&nbsp;</th><% end_if %>
</tr>
</thead>
<tfoot>
<% if HasSummary %>
<tr class="summary">
<td><i>$SummaryTitle</i></td>
<% loop SummaryFields %>
<td<% if Function %> class="$Function"<% end_if %>>$SummaryValue</td>
<% end_loop %>
<th style="display: none"></th>
<% if Can(delete) %><td width="18">&nbsp;</td><% end_if %>
</tr>
<% end_if %>
<% if Can(add) %>
<tr>
<td colspan="$ItemCount">
<a href="#" class="addrow" title="<% _t('TableField.ss.ADD', 'Add a new row') %>"><img src="$ModulePath(framework)/images/add.gif" alt="<% _t('TableField.ss.ADD','Add a new row') %>" />
<% sprintf(_t('TableField.ss.ADDITEM','Add %s'),$Title) %>
</a>
</td>
<td style="display: none"></td>
<% if Can(delete) %><td width="18">&nbsp;</td><% end_if %>
</tr>
<% end_if %>
</tfoot>
<tbody>
<% if Items %>
<% loop Items %>
<tr id="record-$Parent.id-$ID" class="row<% if HighlightClasses %> $HighlightClasses<% end_if %>">
<% loop Fields %>
<td class="$FieldClass $extraClass $ClassName $Title tablecolumn">$Field</td>
<% end_loop %>
<td style="display: none">$ExtraData</td>
<% if Can(delete) %><td width="18"><a class="deletelink" href="$DeleteLink" title="<% _t('TableField.ss.DELETEROW') %>"><img src="$ModulePath(framework)/images/delete.gif" alt="<% _t('TableField.ss.DELETE') %>" /></a></td><% end_if %>
</tr>
<% end_loop %>
<% else %>
<tr class="notfound">
<% if Markable %><th width="18">&nbsp;</th><% end_if %>
<td colspan="$Headings.Count"><i><% _t('TableField.ss.NOITEMSFOUND') %></i></td>
<% if Can(delete) %><td width="18">&nbsp;</td><% end_if %>
</tr>
<% end_if %>
</tbody>
</table>
<% if Print %><% else %><div class="utility">
<% loop Utility %>
<span class="item"><a href="$Link">$Title</a></span>
<% end_loop %>
</div><% end_if %>
<% if Message %>
<span class="message $MessageType">$Message</span>
<% end_if %>
</div>
</div>

View File

@ -1,85 +0,0 @@
<div id="$id" class="$CSSClasses $extraClass field nolabel">
<% if Print %><% else %>
<% if Markable %>
<% include TableListField_SelectOptions %>
<% end_if %>
<% include TableListField_PageControls %>
<% end_if %>
<table class="data">
<thead>
<tr>
<% if Markable %><th width="16"><% if MarkableTitle %>$MarkableTitle<% else %>&nbsp;<% end_if %></th><% end_if %>
<% if Print %>
<% loop Headings %>
<th class="$Name">
$Title
</th>
<% end_loop %>
<% else %>
<% loop Headings %>
<th class="$Name">
<% if IsSortable %>
<span class="sortTitle">
<a href="$SortLink">$Title</a>
</span>
<span class="sortLink <% if SortBy %><% else %>sortLinkHidden<% end_if %>">
<% if SortDirection = desc %>
<a href="$SortLink"><img src="$ModulePath(framework)/images/bullet_arrow_up.png" alt="<% _t('TableListField.ss.SORTDESC', 'Sort in descending order') %>" /></a>
<% else %>
<a href="$SortLink"><img src="$ModulePath(framework)/images/bullet_arrow_down.png" alt="<% _t('TableListField.ss.SORTASC', 'Sort in ascending order') %>" /></a>
<% end_if %>
</a>
&nbsp;
</span>
<% else %>
<span>$Title</span>
<% end_if %>
</th>
<% end_loop %>
<% end_if %>
<% if Can(delete) %><th width="18">&nbsp;</th><% end_if %>
</tr>
</thead>
<% if HasSummary %>
<tfoot>
<tr class="summary">
<% include TableListField_Summary %>
</tr>
</tfoot>
<% end_if %>
<tbody>
<% if HasGroupedItems %>
<% loop GroupedItems %>
<% loop Items %>
<% include TableListField_Item %>
<% end_loop %>
<tr class="summary partialSummary">
<% include TableListField_Summary %>
</tr>
<% end_loop %>
<% else %>
<% if Items %>
<% loop Items %>
<% include TableListField_Item %>
<% end_loop %>
<% else %>
<tr class="notfound">
<% if Markable %><th width="18">&nbsp;</th><% end_if %>
<td colspan="$Headings.Count"><i><% _t('TableListField.ss.NOITEMSFOUND','No items found') %></i></td>
<% if Can(delete) %><td width="18">&nbsp;</td><% end_if %>
</tr>
<% end_if %>
<% if Can(add) %>
$AddRecordAsTableRow
<% end_if %>
<% end_if %>
</tbody>
</table>
<% if Print %><% else %><div class="utility">
<% loop Utility %>
<span class="item"><a href="$Link">$Title</a></span>
<% end_loop %>
</div><% end_if %>
</div>

View File

@ -1,12 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<% base_tag %>
<title>Print</title>
</head>
<body onload="window.print();">
<% include TableListField %>
</body>
</html>

View File

@ -1,202 +0,0 @@
<?php
/**
* @package framework
* @subpackage tests
*/
class ComplexTableFieldTest extends FunctionalTest {
static $fixture_file = 'ComplexTableFieldTest.yml';
static $use_draft_site = true;
protected $extraDataObjects = array(
'ComplexTableFieldTest_Player',
'ComplexTableFieldTest_Team',
'ComplexTableFieldTest_Sponsor',
);
/**
* An instance of {@link Controller} used for
* running tests against.
*
* @var Controller object
*/
protected $controller;
protected $autoFollowRedirection = false;
public function setUp() {
parent::setUp();
$this->controller = new ComplexTableFieldTest_Controller();
$this->manyManyForm = $this->controller->ManyManyForm();
}
public function testCorrectNumberOfRowsInTable() {
$field = $this->manyManyForm->Fields()->dataFieldByName('Players');
$parser = new CSSContentParser($field->FieldHolder());
$this->assertEquals(count($parser->getBySelector('tbody tr')), 2, 'There are 2 players (rows) in the table');
$this->assertEquals($field->Items()->Count(), 2, 'There are 2 CTF items in the SS_List');
}
public function testAddingManyManyNewPlayer() {
$this->logInWithPermission('ADMIN');
$team = DataObject::get_one('ComplexTableFieldTest_Team', "\"Name\" = 'The Awesome People'");
$this->post('ComplexTableFieldTest_Controller/ManyManyForm/field/Players/AddForm', array(
'Name' => 'Bobby Joe',
'ctf' => array(
'ClassName' => 'ComplexTableFieldTest_Player',
'manyManyRelation' => 'Players',
'parentClass' => 'ComplexTableFieldTest_Team',
'sourceID' => $team->ID
)
));
$newPlayer = DataObject::get_one('ComplexTableFieldTest_Player', "\"Name\" = 'Bobby Joe'");
$this->assertNotNull($newPlayer, 'A new ComplexTableFieldTest_Player record was created, Name = "Bobby Joe"');
$teams = $newPlayer->getManyManyComponents('Teams');
$this->assertEquals($teams->Count(), 1, 'Automatic many-many relation was set correctly on the new player');
}
public function testAddingHasManyData() {
$this->logInWithPermission('ADMIN');
$team = DataObject::get_one('ComplexTableFieldTest_Team', "\"Name\" = 'The Awesome People'");
$this->post('ComplexTableFieldTest_Controller/HasManyForm/field/Sponsors/AddForm', array(
'Name' => 'Jim Beam',
'ctf' => array(
'ClassName' => 'ComplexTableFieldTest_Sponsor',
'hasManyRelation' => 'Sponsors',
'parentClass' => 'ComplexTableFieldTest_Team',
'sourceID' => $team->ID
)
));
$newSponsor = DataObject::get_one('ComplexTableFieldTest_Sponsor', "\"Name\" = 'Jim Beam'");
$this->assertNotNull($newSponsor, 'A new ComplexTableFieldTest_Sponsor record was created, Name = "Jim Beam"');
$this->assertEquals($newSponsor->TeamID, $team->ID,
'Automatic has-many/has-one relation was set correctly on the sponsor');
$this->assertEquals($newSponsor->getComponent('Team')->ID, $team->ID,
'Automatic has-many/has-one relation was set correctly on the sponsor');
$team = DataObject::get_by_id('ComplexTableFieldTest_Team', $team->ID);
$sponsor = DataObject::get_by_id('ComplexTableFieldTest_Sponsor', $newSponsor->ID);
$this->assertEquals($newSponsor->ID, $sponsor->ID, 'The sponsor is the same as the one we added');
$foundTeam = $sponsor->getComponent('Team');
$this->assertEquals($team->ID, $foundTeam->ID, 'The team ID matches on the other side of the relation');
}
}
class ComplexTableFieldTest_Controller extends Controller {
public function Link($action = null) {
return "ComplexTableFieldTest_Controller/$action";
}
public function ManyManyForm() {
$team = DataObject::get_one('ComplexTableFieldTest_Team', "\"Name\" = 'The Awesome People'");
$playersField = new ComplexTableField(
$this,
'Players',
$team->Players(),
ComplexTableFieldTest_Player::$summary_fields,
'getCMSFields'
);
$form = new Form(
$this,
'ManyManyForm',
new FieldList(
new HiddenField('ID', '', $team->ID),
$playersField
),
new FieldList(
new FormAction('doSubmit', 'Submit')
)
);
$form->loadDataFrom($team);
$form->disableSecurityToken();
return $form;
}
public function HasManyForm() {
$team = DataObject::get_one('ComplexTableFieldTest_Team', "\"Name\" = 'The Awesome People'");
$sponsorsField = new ComplexTableField(
$this,
'Sponsors',
$team->Sponsors(),
ComplexTableFieldTest_Sponsor::$summary_fields,
'getCMSFields'
);
$form = new Form(
$this,
'HasManyForm',
new FieldList(
new HiddenField('ID', '', $team->ID),
$sponsorsField
),
new FieldList(
new FormAction('doSubmit', 'Submit')
)
);
$form->loadDataFrom($team);
$form->disableSecurityToken();
return $form;
}
}
class ComplexTableFieldTest_Player extends DataObject implements TestOnly {
public static $db = array(
'Name' => 'Varchar(100)'
);
public static $many_many = array(
'Teams' => 'ComplexTableFieldTest_Team'
);
public static $many_many_extraFields = array(
'Teams' => array(
'Role' => 'Varchar(100)',
'Position' => "Enum('Admin,Player,Coach','Admin')",
'DateJoined' => 'Date'
)
);
}
class ComplexTableFieldTest_Team extends DataObject implements TestOnly {
public static $db = array(
'Name' => 'Varchar(100)'
);
public static $belongs_many_many = array(
'Players' => 'ComplexTableFieldTest_Player'
);
public static $has_many = array(
'Sponsors' => 'ComplexTableFieldTest_Sponsor'
);
}
class ComplexTableFieldTest_Sponsor extends DataObject implements TestOnly {
public static $db = array(
'Name' => 'Varchar(100)'
);
public static $has_one = array(
'Team' => 'ComplexTableFieldTest_Team'
);
}

View File

@ -1,16 +0,0 @@
ComplexTableFieldTest_Player:
p1:
Name: Joe Bloggs
p2:
Name: Some Guy
ComplexTableFieldTest_Team:
t1:
Name: The Awesome People
Players: =>ComplexTableFieldTest_Player.p1,=>ComplexTableFieldTest_Player.p2
t2:
Name: Incredible Four
ComplexTableFieldTest_Sponsor:
s1:
Name: Coca Cola
s2:
Name: Pepsi

View File

@ -1,263 +0,0 @@
<?php
class TableFieldTest extends SapphireTest {
static $fixture_file = 'TableFieldTest.yml';
protected $extraDataObjects = array(
'TableFieldTest_Object',
'TableFieldTest_HasManyRelation',
);
public function testAdd() {
$group = $this->objFromFixture('Group','group1_no_perms');
$tableField = new TableField(
"Permissions",
"Permission",
array(
"Code" => 'Code',
"Arg" => 'Arg',
),
array(
"Code" => "TextField",
"Arg" => "TextField",
),
"GroupID",
$group->ID
);
$form = new Form(
new TableFieldTest_Controller(),
"Form",
new FieldList($tableField),
new FieldList()
);
// Test Insert
// The field starts emppty. Save some new data.
// We have replicated the array structure that the specific layout of the form generates.
$tableField->setValue(array(
'new' => array(
'Code' => array(
'CustomPerm1',
'CustomPerm2',
),
'Arg' => array(
'1',
'2'
),
),
));
$tableField->saveInto($group);
// Let's check that the 2 permissions entries have been saved
$permissions = $group->Permissions()->map('Arg', 'Code');
$this->assertEquals(array(
1 => 'CustomPerm1',
2 => 'CustomPerm2',
), $permissions->toArray());
// Test repeated insert
$value = array();
foreach($group->Permissions() as $permission) {
$value[$permission->ID] = array("Code" => $permission->Code, "Arg" => $permission->Arg);
}
$value['new'] = array(
'Code' => array(
'CustomPerm3',
),
'Arg' => array(
'3',
),
);
$tableField->setValue($value);
$tableField->saveInto($group);
// Let's check that the 2 existing permissions entries, and the 1 new one, have been saved
$permissions = $group->Permissions()->map('Arg', 'Code');
$this->assertEquals(array(
1 => 'CustomPerm1',
2 => 'CustomPerm2',
3 => 'CustomPerm3',
), $permissions->toArray());
}
public function testEdit() {
$group = $this->objFromFixture('Group','group2_existing_perms');
$perm1 = $this->objFromFixture('Permission', 'perm1');
$perm2 = $this->objFromFixture('Permission', 'perm2');
$tableField = new TableField(
"Permissions",
"Permission",
array(
"Code" => 'Code',
"Arg" => 'Arg',
),
array(
"Code" => "TextField",
"Arg" => "TextField",
),
"GroupID",
$group->ID
);
$form = new Form(
new TableFieldTest_Controller(),
"Form",
new FieldList($tableField),
new FieldList()
);
$this->assertEquals(2, $tableField->sourceItems()->Count());
// We have replicated the array structure that the specific layout of the form generates.
$tableField->setValue(array(
$perm1->ID => array(
'Code' => 'Perm1 Modified',
'Arg' => '101'
),
$perm2->ID => array(
'Code' => 'Perm2 Modified',
'Arg' => '102'
)
));
$tableField->saveInto($group);
// Let's check that the 2 permissions entries have been saved
$permissions = $group->Permissions()->map('Arg', 'Code');
$this->assertEquals(array(
101 => 'Perm1 Modified',
102 => 'Perm2 Modified',
), $permissions->toArray());
}
public function testDelete() {
$group = $this->objFromFixture('Group','group2_existing_perms');
$perm1 = $this->objFromFixture('Permission', 'perm1');
$perm2 = $this->objFromFixture('Permission', 'perm2');
$tableField = new TableField(
"Permissions",
"Permission",
array(
"Code" => 'Code',
"Arg" => 'Arg',
),
array(
"Code" => "TextField",
"Arg" => "TextField",
),
"GroupID",
$group->ID
);
$form = new Form(
new TableFieldTest_Controller(),
"Form",
new FieldList($tableField),
new FieldList()
);
$this->assertContains($perm1->ID, $tableField->sourceItems()->column('ID'));
$response = $tableField->Items()->find('ID', $perm1->ID)->delete();
$this->assertNotContains($perm1->ID, $tableField->sourceItems()->column('ID'));
}
/**
* Relation auto-setting is now the only option
*/
public function testAutoRelationSettingOn() {
$o = new TableFieldTest_Object();
$o->write();
$tf = new TableField(
'HasManyRelations',
'TableFieldTest_HasManyRelation',
array(
'Value' => 'Value'
),
array(
'Value' => 'TextField'
)
);
// Test with auto relation setting
$form = new Form(new TableFieldTest_Controller(), "Form", new FieldList($tf), new FieldList());
$form->loadDataFrom($o);
$tf->setValue(array(
'new' => array(
'Value' => array('one','two',)
)
));
$form->saveInto($o);
$this->assertEquals(2, $o->HasManyRelations()->Count());
}
public function testHasItemsWhenSetAsArray() {
$tf = new TableField(
'TestTableField',
'TableFieldTest_HasManyRelation',
array(
'Value' => 'Value'
),
array(
'Value' => 'TextField'
)
);
$tf->setValue(array(
'new' => array(
'Value' => array(
'One',
'Two',
)
)
));
$items = $tf->Items();
$itemsArr = $items->toArray();
// includes the two values and an "add" row
$this->assertEquals($items->Count(), 3);
// first row
$this->assertEquals(
$itemsArr[0]->Fields()->fieldByName('TestTableField[new][Value][]')->Value(),
'One'
);
// second row
$this->assertEquals(
$itemsArr[1]->Fields()->fieldByName('TestTableField[new][Value][]')->Value(),
'Two'
);
}
}
/**
* Stub controller
*/
class TableFieldTest_Controller extends Controller implements TestOnly {
public function Link($action = null) {
return Controller::join_links('TableFieldTest/', $action);
}
}
class TableFieldTest_Object extends DataObject implements TestOnly {
static $has_many = array(
"HasManyRelations" => 'TableFieldTest_HasManyRelation'
);
}
class TableFieldTest_HasManyRelation extends DataObject implements TestOnly {
static $db = array(
'Value' => 'Text',
);
static $has_one = array(
'HasOneRelation' => 'TableFieldTest_Object'
);
}

View File

@ -1,13 +0,0 @@
Permission:
perm1:
Code: Perm1
Arg: 1
perm2:
Code: Perm2
Arg: 2
Group:
group1_no_perms:
Title: Group A
group2_existing_perms:
Title: Group B
Permissions: =>Permission.perm1,=>Permission.perm2

View File

@ -1,375 +0,0 @@
<?php
class TableListFieldTest extends SapphireTest {
static $fixture_file = 'TableListFieldTest.yml';
protected $extraDataObjects = array(
'TableListFieldTest_Obj',
'TableListFieldTest_CsvExport',
);
public function testCanReferenceCustomMethodsAndFieldsOnObject() {
$table = new TableListField("Tester", "TableListFieldTest_Obj", array(
"A" => "Col A",
"B" => "Col B",
"C" => "Col C",
"D" => "Col D",
"E" => "Col E",
));
// A TableListField must be inside a form for its links to be generated
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList(
$table
), new FieldList());
$result = $table->FieldHolder();
// Do a quick check to ensure that some of the D() and getE() values got through
$this->assertRegExp('/>\s*a2\s*</', $result);
$this->assertRegExp('/>\s*a2\/b2\/c2\s*</', $result);
$this->assertRegExp('/>\s*a2-e</', $result);
}
public function testUnpaginatedSourceItemGeneration() {
$item1 = $this->objFromFixture('TableListFieldTest_Obj', 'one');
$item2 = $this->objFromFixture('TableListFieldTest_Obj', 'two');
$item3 = $this->objFromFixture('TableListFieldTest_Obj', 'three');
$item4 = $this->objFromFixture('TableListFieldTest_Obj', 'four');
$item5 = $this->objFromFixture('TableListFieldTest_Obj', 'five');
// In this simple case, the source items should just list all the data objects specified
$table = new TableListField("Tester", "TableListFieldTest_Obj", array(
"A" => "Col A",
"B" => "Col B",
"C" => "Col C",
"D" => "Col D",
"E" => "Col E",
));
// A TableListField must be inside a form for its links to be generated
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList(
$table
), new FieldList());
$items = $table->sourceItems();
$this->assertNotNull($items);
$itemMap = $items->map("ID", "A") ;
$this->assertEquals(array(
$item1->ID => "a1",
$item2->ID => "a2",
$item3->ID => "a3",
$item4->ID => "a4",
$item5->ID => "a5"
), $itemMap->toArray());
}
public function testFirstPageOfPaginatedSourceItemGeneration() {
$item1 = $this->objFromFixture('TableListFieldTest_Obj', 'one');
$item2 = $this->objFromFixture('TableListFieldTest_Obj', 'two');
$item3 = $this->objFromFixture('TableListFieldTest_Obj', 'three');
$item4 = $this->objFromFixture('TableListFieldTest_Obj', 'four');
$item5 = $this->objFromFixture('TableListFieldTest_Obj', 'five');
// With pagination enabled, only the first page of items should be shown
$table = new TableListField("Tester", "TableListFieldTest_Obj", array(
"A" => "Col A",
"B" => "Col B",
"C" => "Col C",
"D" => "Col D",
"E" => "Col E",
));
// A TableListField must be inside a form for its links to be generated
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList(
$table
), new FieldList());
$table->ShowPagination = true;
$table->PageSize = 2;
$items = $table->sourceItems();
$this->assertNotNull($items);
$itemMap = $items->map("ID", "A") ;
$this->assertEquals(array(
$item1->ID => "a1",
$item2->ID => "a2"
), $itemMap->toArray());
}
public function testSecondPageOfPaginatedSourceItemGeneration() {
$item1 = $this->objFromFixture('TableListFieldTest_Obj', 'one');
$item2 = $this->objFromFixture('TableListFieldTest_Obj', 'two');
$item3 = $this->objFromFixture('TableListFieldTest_Obj', 'three');
$item4 = $this->objFromFixture('TableListFieldTest_Obj', 'four');
$item5 = $this->objFromFixture('TableListFieldTest_Obj', 'five');
// With pagination enabled, only the first page of items should be shown
$table = new TableListField("Tester", "TableListFieldTest_Obj", array(
"A" => "Col A",
"B" => "Col B",
"C" => "Col C",
"D" => "Col D",
"E" => "Col E",
));
// A TableListField must be inside a form for its links to be generated
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList(
$table
), new FieldList());
$table->ShowPagination = true;
$table->PageSize = 2;
$_REQUEST['ctf']['Tester']['start'] = 2;
$items = $table->sourceItems();
$this->assertNotNull($items);
$itemMap = $items->map("ID", "A") ;
$this->assertEquals(array($item3->ID => "a3", $item4->ID => "a4"), $itemMap->toArray());
}
public function testSelectOptionsAddRemove() {
$table = new TableListField("Tester", "TableListFieldTest_Obj", array(
"A" => "Col A",
));
$this->assertNull($table->SelectOptions(), 'Empty by default');
$table->addSelectOptions(array("F"=>"FieldF", 'G'=>'FieldG'));
$this->assertEquals($table->SelectOptions()->map('Key', 'Value'), array("F"=>"FieldF",'G'=>'FieldG'));
$table->removeSelectOptions(array("F"));
$this->assertEquals($table->SelectOptions()->map('Key', 'Value'), array("G"=>"FieldG"));
}
public function testSelectOptionsRendering() {
$obj1 = $this->objFromFixture('TableListFieldTest_Obj', 'one');
$obj2 = $this->objFromFixture('TableListFieldTest_Obj', 'two');
$obj3 = $this->objFromFixture('TableListFieldTest_Obj', 'three');
$table = new TableListField("Tester", "TableListFieldTest_Obj", array(
"A" => "Col A",
));
$table->Markable = true;
$table->addSelectOptions(array("F"=>"FieldF"));
$tableHTML = $table->FieldHolder();
$p = new CSSContentParser($tableHTML);
$this->assertContains('rel="F"', $tableHTML);
$tbody = $p->getByXpath('//tbody');
$this->assertContains('markingcheckbox F', (string)$tbody[0]->tr[0]->td[0]['class']);
$this->assertContains('markingcheckbox', (string)$tbody[0]->tr[1]->td[0]['class']);
$this->assertContains('markingcheckbox F', (string)$tbody[0]->tr[2]->td[0]['class']);
}
/**
* Get that visiting the field's URL returns the content of the field.
* This capability is used by ajax
*/
public function testAjaxRefreshing() {
$controller = new TableListFieldTest_TestController();
$table = $controller->TestForm()->Fields()->First();
$ajaxResponse = Director::test($table->Link())->getBody();
// Check that the column headings have been rendered
$this->assertRegExp('/<th[^>]*>.*Col A.*<\/th>/si', $ajaxResponse);
$this->assertRegExp('/<th[^>]*>.*Col B.*<\/th>/si', $ajaxResponse);
$this->assertRegExp('/<th[^>]*>.*Col C.*<\/th>/si', $ajaxResponse);
$this->assertRegExp('/<th[^>]*>.*Col D.*<\/th>/si', $ajaxResponse);
$this->assertRegExp('/<th[^>]*>.*Col E.*<\/th>/si', $ajaxResponse);
}
public function testCsvExport() {
$table = new TableListField("Tester", "TableListFieldTest_CsvExport", array(
"A" => "Col A",
"B" => "Col B"
));
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList(
$table
), new FieldList());
$csvResponse = $table->export();
$csvOutput = $csvResponse->getBody();
$this->assertNotEquals($csvOutput, false);
// Create a temporary file and write the CSV to it.
$csvFileName = tempnam(TEMP_FOLDER, 'csv-export');
$csvFile = fopen($csvFileName, 'wb');
fwrite($csvFile, $csvOutput);
fclose($csvFile);
$csvFile = fopen($csvFileName, 'rb');
$csvRow = fgetcsv($csvFile);
$this->assertEquals(
$csvRow,
array('Col A', 'Col B')
);
// fgetcsv doesn't handle escaped quotes in the string in PHP 5.2, so we're asserting the
// raw string instead.
$this->assertEquals(
'"\"A field, with a comma\"","A second field"',
trim(fgets($csvFile))
);
fclose($csvFile);
unlink($csvFileName);
}
public function testLink() {
// A TableListField must be inside a form for its links to be generated
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList(
new TableListField("Tester", "TableListFieldTest_Obj", array(
"A" => "Col A",
"B" => "Col B",
"C" => "Col C",
"D" => "Col D",
"E" => "Col E",
))
), new FieldList());
$table = $form->Fields()->dataFieldByName('Tester');
$this->assertEquals(
$table->Link('test'),
sprintf('TableListFieldTest_TestController/TestForm/field/Tester/test?SecurityID=%s',
$form->Fields()->dataFieldByName('SecurityID')->Value())
);
}
public function testPreservedSortOptionsInPaginationLink() {
$item1 = $this->objFromFixture('TableListFieldTest_Obj', 'one');
$item2 = $this->objFromFixture('TableListFieldTest_Obj', 'two');
$item3 = $this->objFromFixture('TableListFieldTest_Obj', 'three');
$item4 = $this->objFromFixture('TableListFieldTest_Obj', 'four');
$item5 = $this->objFromFixture('TableListFieldTest_Obj', 'five');
/* With pagination enabled, only the first page of items should be shown */
$table = new TableListField("Tester", "TableListFieldTest_Obj", array(
"A" => "Col A",
"B" => "Col B",
"C" => "Col C",
"D" => "Col D",
"E" => "Col E",
));
// A TableListField must be inside a form for its links to be generated
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList(
$table
), new FieldList());
$table->ShowPagination = true;
$table->PageSize = 2;
// first page & sort A column by ASC
$_REQUEST['ctf']['Tester']['start'] = 0;
$_REQUEST['ctf']['Tester']['sort'] = 'A';
$this->assertContains('&ctf[Tester][sort]=A', $table->NextLink());
$this->assertNotContains('ctf[Tester][dir]', $table->NextLink());
$this->assertContains('&ctf[Tester][sort]=A', $table->LastLink());
$this->assertNotContains('ctf[Tester][dir]', $table->LastLink());
// second page & sort A column by ASC
$_REQUEST['ctf']['Tester']['start'] = 2;
$this->assertContains('&ctf[Tester][sort]=A', $table->PrevLink());
$this->assertNotContains('&ctf[Tester][dir]', $table->PrevLink());
$this->assertContains('&ctf[Tester][sort]=A', $table->FirstLink());
$this->assertNotContains('&ctf[Tester][dir]', $table->FirstLink());
// first page & sort A column by DESC
$_REQUEST['ctf']['Tester']['start'] = 0;
$_REQUEST['ctf']['Tester']['sort'] = 'A';
$_REQUEST['ctf']['Tester']['dir'] = 'desc';
$this->assertContains('&ctf[Tester][sort]=A', $table->NextLink());
$this->assertContains('&ctf[Tester][dir]=desc', $table->NextLink());
$this->assertContains('&ctf[Tester][sort]=A', $table->LastLink());
$this->assertContains('&ctf[Tester][dir]=desc', $table->LastLink());
// second page & sort A column by DESC
$_REQUEST['ctf']['Tester']['start'] = 2;
$this->assertContains('&ctf[Tester][sort]=A', $table->PrevLink());
$this->assertContains('&ctf[Tester][dir]=desc', $table->PrevLink());
$this->assertContains('&ctf[Tester][sort]=A', $table->FirstLink());
$this->assertContains('&ctf[Tester][dir]=desc', $table->FirstLink());
unset($_REQUEST['ctf']);
}
/**
* Check that a SS_List can be passed to TableListField
*/
public function testDataObjectSet() {
$one = new TableListFieldTest_Obj;
$one->A = "A-one";
$two = new TableListFieldTest_Obj;
$two->A = "A-two";
$three = new TableListFieldTest_Obj;
$three->A = "A-three";
$list = new ArrayList(array($one, $two, $three));
// A TableListField must be inside a form for its links to be generated
$form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList(
new TableListField("Tester", $list, array(
"A" => "Col A",
"B" => "Col B",
"C" => "Col C",
"D" => "Col D",
"E" => "Col E",
))
), new FieldList());
$table = $form->Fields()->dataFieldByName('Tester');
$rendered = $table->FieldHolder();
$this->assertContains('A-one', $rendered);
$this->assertContains('A-two', $rendered);
$this->assertContains('A-three', $rendered);
}
}
class TableListFieldTest_Obj extends DataObject implements TestOnly {
static $db = array(
"A" => "Varchar",
"B" => "Varchar",
"C" => "Varchar",
"F" => "Boolean",
);
static $default_sort = "A";
public function D() {
return $this->A . '/' . $this->B . '/' . $this->C;
}
public function getE() {
return $this->A . '-e';
}
}
class TableListFieldTest_CsvExport extends DataObject implements TestOnly {
static $db = array(
"A" => "Varchar",
"B" => "Varchar"
);
static $default_sort = "A";
}
class TableListFieldTest_TestController extends Controller {
public function Link($action = null) {
return Controller::join_links("TableListFieldTest_TestController/", $action);
}
public function TestForm() {
$table = new TableListField("Table", "TableListFieldTest_Obj", array(
"A" => "Col A",
"B" => "Col B",
"C" => "Col C",
"D" => "Col D",
"E" => "Col E",
));
$table->disableSorting();
// A TableListField must be inside a form for its links to be generated
return new Form($this, "TestForm", new FieldList(
$table
), new FieldList());
}
}

View File

@ -1,31 +0,0 @@
TableListFieldTest_Obj:
one:
A: a1
B: b1
C: c1
F: true
two:
A: a2
B: b2
C: c2
F: false
three:
A: a3
B: b3
C: c3
F: true
four:
A: a4
B: b4
C: c4
D: false
five:
A: a5
B: b5
C: c5
F: true
TableListFieldTest_CsvExport:
exportone:
A: "\"A field, with a comma\""
B: A second field

View File

@ -1,101 +0,0 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// See scriptaculous.js for full license.
var Builder = {
NODEMAP: {
AREA: 'map',
CAPTION: 'table',
COL: 'table',
COLGROUP: 'table',
LEGEND: 'fieldset',
OPTGROUP: 'select',
OPTION: 'select',
PARAM: 'object',
TBODY: 'table',
TD: 'table',
TFOOT: 'table',
TH: 'table',
THEAD: 'table',
TR: 'table'
},
// note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken,
// due to a Firefox bug
node: function(elementName) {
elementName = elementName.toUpperCase();
// try innerHTML approach
var parentTag = this.NODEMAP[elementName] || 'div';
var parentElement = document.createElement(parentTag);
try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
parentElement.innerHTML = "<" + elementName + "></" + elementName + ">";
} catch(e) {}
var element = parentElement.firstChild || null;
// see if browser added wrapping tags
if(element && (element.tagName != elementName))
element = element.getElementsByTagName(elementName)[0];
// fallback to createElement approach
if(!element) element = document.createElement(elementName);
// abort if nothing could be created
if(!element) return;
// attributes (or text)
if(arguments[1])
if(this._isStringOrNumber(arguments[1]) ||
(arguments[1] instanceof Array)) {
this._children(element, arguments[1]);
} else {
var attrs = this._attributes(arguments[1]);
if(attrs.length) {
try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
parentElement.innerHTML = "<" +elementName + " " +
attrs + "></" + elementName + ">";
} catch(e) {}
element = parentElement.firstChild || null;
// workaround firefox 1.0.X bug
if(!element) {
element = document.createElement(elementName);
for(attr in arguments[1])
element[attr == 'class' ? 'className' : attr] = arguments[1][attr];
}
if(element.tagName != elementName)
element = parentElement.getElementsByTagName(elementName)[0];
}
}
// text, or array of children
if(arguments[2])
this._children(element, arguments[2]);
return element;
},
_text: function(text) {
return document.createTextNode(text);
},
_attributes: function(attributes) {
var attrs = [];
for(attribute in attributes)
attrs.push((attribute=='className' ? 'class' : attribute) +
'="' + attributes[attribute].toString().escapeHTML() + '"');
return attrs.join(" ");
},
_children: function(element, children) {
if(typeof children=='object') { // array can hold nodes and text
children.flatten().each( function(e) {
if(typeof e=='object')
element.appendChild(e)
else
if(Builder._isStringOrNumber(e))
element.appendChild(Builder._text(e));
});
} else
if(Builder._isStringOrNumber(children))
element.appendChild(Builder._text(children));
},
_isStringOrNumber: function(param) {
return(typeof param=='string' || typeof param=='number');
}
}

View File

@ -1,750 +0,0 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
// (c) 2005 Jon Tirsen (http://www.tirsen.com)
// Contributors:
// Richard Livsey
// Rahul Bhargava
// Rob Wills
//
// See scriptaculous.js for full license.
// Autocompleter.Base handles all the autocompletion functionality
// that's independent of the data source for autocompletion. This
// includes drawing the autocompletion menu, observing keyboard
// and mouse events, and similar.
//
// Specific autocompleters need to provide, at the very least,
// a getUpdatedChoices function that will be invoked every time
// the text inside the monitored textbox changes. This method
// should get the text for which to provide autocompletion by
// invoking this.getToken(), NOT by directly accessing
// this.element.value. This is to allow incremental tokenized
// autocompletion. Specific auto-completion logic (AJAX, etc)
// belongs in getUpdatedChoices.
//
// Tokenized incremental autocompletion is enabled automatically
// when an autocompleter is instantiated with the 'tokens' option
// in the options parameter, e.g.:
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
// will incrementally autocomplete with a comma as the token.
// Additionally, ',' in the above example can be replaced with
// a token array, e.g. { tokens: [',', '\n'] } which
// enables autocompletion on multiple tokens. This is most
// useful when one of the tokens is \n (a newline), as it
// allows smart autocompletion after linebreaks.
var Autocompleter = {}
Autocompleter.Base = function() {};
Autocompleter.Base.prototype = {
baseInitialize: function(element, update, options) {
this.element = $(element);
this.update = $(update);
this.hasFocus = false;
this.changed = false;
this.active = false;
this.index = 0;
this.entryCount = 0;
if (this.setOptions)
this.setOptions(options);
else
this.options = options || {};
this.options.paramName = this.options.paramName || this.element.name;
this.options.tokens = this.options.tokens || [];
this.options.frequency = this.options.frequency || 0.4;
this.options.minChars = this.options.minChars || 1;
this.options.onShow = this.options.onShow ||
function(element, update){
if(!update.style.position || update.style.position=='absolute') {
update.style.position = 'absolute';
Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
}
Effect.Appear(update,{duration:0.15});
};
this.options.onHide = this.options.onHide ||
function(element, update){ new Effect.Fade(update,{duration:0.15}) };
if (typeof(this.options.tokens) == 'string')
this.options.tokens = new Array(this.options.tokens);
this.observer = null;
this.element.setAttribute('autocomplete','off');
Element.hide(this.update);
Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
},
show: function() {
if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
if(!this.iefix &&
(navigator.appVersion.indexOf('MSIE')>0) &&
(navigator.userAgent.indexOf('Opera')<0) &&
(Element.getStyle(this.update, 'position')=='absolute')) {
new Insertion.After(this.update,
'<iframe id="' + this.update.id + '_iefix" '+
'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
this.iefix = $(this.update.id+'_iefix');
}
if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
},
fixIEOverlapping: function() {
Position.clone(this.update, this.iefix);
this.iefix.style.zIndex = 1;
this.update.style.zIndex = 2;
Element.show(this.iefix);
},
hide: function() {
this.stopIndicator();
if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
if(this.iefix) Element.hide(this.iefix);
},
startIndicator: function() {
if(this.options.indicator) Element.show(this.options.indicator);
},
stopIndicator: function() {
if(this.options.indicator) Element.hide(this.options.indicator);
},
onKeyPress: function(event) {
if(this.active)
switch(event.keyCode) {
case Event.KEY_TAB:
case Event.KEY_RETURN:
this.selectEntry();
Event.stop(event);
case Event.KEY_ESC:
this.hide();
this.active = false;
Event.stop(event);
return;
case Event.KEY_LEFT:
case Event.KEY_RIGHT:
return;
case Event.KEY_UP:
this.markPrevious();
this.render();
if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
return;
case Event.KEY_DOWN:
this.markNext();
this.render();
if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
return;
}
else
if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN)
return;
this.changed = true;
this.hasFocus = true;
if(this.observer) clearTimeout(this.observer);
this.observer =
setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
},
onHover: function(event) {
var element = Event.findElement(event, 'LI');
if(this.index != element.autocompleteIndex)
{
this.index = element.autocompleteIndex;
this.render();
}
Event.stop(event);
},
onClick: function(event) {
var element = Event.findElement(event, 'LI');
this.index = element.autocompleteIndex;
this.selectEntry();
this.hide();
},
onBlur: function(event) {
// needed to make click events working
setTimeout(this.hide.bind(this), 250);
this.hasFocus = false;
this.active = false;
},
render: function() {
if(this.entryCount > 0) {
for (var i = 0; i < this.entryCount; i++)
this.index==i ?
Element.addClassName(this.getEntry(i),"selected") :
Element.removeClassName(this.getEntry(i),"selected");
if(this.hasFocus) {
this.show();
this.active = true;
}
} else {
this.active = false;
this.hide();
}
},
markPrevious: function() {
if(this.index > 0) this.index--
else this.index = this.entryCount-1;
},
markNext: function() {
if(this.index < this.entryCount-1) this.index++
else this.index = 0;
},
getEntry: function(index) {
return this.update.firstChild.childNodes[index];
},
getCurrentEntry: function() {
return this.getEntry(this.index);
},
selectEntry: function() {
this.active = false;
this.updateElement(this.getCurrentEntry());
},
updateElement: function(selectedElement) {
if (this.options.updateElement) {
this.options.updateElement(selectedElement);
return;
}
var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
var lastTokenPos = this.findLastToken();
if (lastTokenPos != -1) {
var newValue = this.element.value.substr(0, lastTokenPos + 1);
var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
if (whitespace)
newValue += whitespace[0];
this.element.value = newValue + value;
} else {
this.element.value = value;
}
this.element.focus();
if (this.options.afterUpdateElement)
this.options.afterUpdateElement(this.element, selectedElement);
},
updateChoices: function(choices) {
if(!this.changed && this.hasFocus) {
this.update.innerHTML = choices;
Element.cleanWhitespace(this.update);
Element.cleanWhitespace(this.update.firstChild);
if(this.update.firstChild && this.update.firstChild.childNodes) {
this.entryCount =
this.update.firstChild.childNodes.length;
for (var i = 0; i < this.entryCount; i++) {
var entry = this.getEntry(i);
entry.autocompleteIndex = i;
this.addObservers(entry);
}
} else {
this.entryCount = 0;
}
this.stopIndicator();
this.index = 0;
this.render();
}
},
addObservers: function(element) {
Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
Event.observe(element, "click", this.onClick.bindAsEventListener(this));
},
onObserverEvent: function() {
this.changed = false;
if(this.getToken().length>=this.options.minChars) {
this.startIndicator();
this.getUpdatedChoices();
} else {
this.active = false;
this.hide();
}
},
getToken: function() {
var tokenPos = this.findLastToken();
if (tokenPos != -1)
var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
else
var ret = this.element.value;
return /\n/.test(ret) ? '' : ret;
},
findLastToken: function() {
var lastTokenPos = -1;
for (var i=0; i<this.options.tokens.length; i++) {
var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
if (thisTokenPos > lastTokenPos)
lastTokenPos = thisTokenPos;
}
return lastTokenPos;
}
}
Ajax.Autocompleter = Class.create();
Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
initialize: function(element, update, url, options) {
this.baseInitialize(element, update, options);
this.options.asynchronous = true;
this.options.onComplete = this.onComplete.bind(this);
this.options.defaultParams = this.options.parameters || null;
this.url = url;
},
getUpdatedChoices: function() {
entry = encodeURIComponent(this.options.paramName) + '=' +
encodeURIComponent(this.getToken());
this.options.parameters = this.options.callback ?
this.options.callback(this.element, entry) : entry;
if(this.options.defaultParams)
this.options.parameters += '&' + this.options.defaultParams;
new Ajax.Request(this.url, this.options);
},
onComplete: function(request) {
this.updateChoices(request.responseText);
}
});
// The local array autocompleter. Used when you'd prefer to
// inject an array of autocompletion options into the page, rather
// than sending out Ajax queries, which can be quite slow sometimes.
//
// The constructor takes four parameters. The first two are, as usual,
// the id of the monitored textbox, and id of the autocompletion menu.
// The third is the array you want to autocomplete from, and the fourth
// is the options block.
//
// Extra local autocompletion options:
// - choices - How many autocompletion choices to offer
//
// - partialSearch - If false, the autocompleter will match entered
// text only at the beginning of strings in the
// autocomplete array. Defaults to true, which will
// match text at the beginning of any *word* in the
// strings in the autocomplete array. If you want to
// search anywhere in the string, additionally set
// the option fullSearch to true (default: off).
//
// - fullSsearch - Search anywhere in autocomplete array strings.
//
// - partialChars - How many characters to enter before triggering
// a partial match (unlike minChars, which defines
// how many characters are required to do any match
// at all). Defaults to 2.
//
// - ignoreCase - Whether to ignore case when autocompleting.
// Defaults to true.
//
// It's possible to pass in a custom function as the 'selector'
// option, if you prefer to write your own autocompletion logic.
// In that case, the other options above will not apply unless
// you support them.
Autocompleter.Local = Class.create();
Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
initialize: function(element, update, array, options) {
this.baseInitialize(element, update, options);
this.options.array = array;
},
getUpdatedChoices: function() {
this.updateChoices(this.options.selector(this));
},
setOptions: function(options) {
this.options = Object.extend({
choices: 10,
partialSearch: true,
partialChars: 2,
ignoreCase: true,
fullSearch: false,
selector: function(instance) {
var ret = []; // Beginning matches
var partial = []; // Inside matches
var entry = instance.getToken();
var count = 0;
for (var i = 0; i < instance.options.array.length &&
ret.length < instance.options.choices ; i++) {
var elem = instance.options.array[i];
var foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase()) :
elem.indexOf(entry);
while (foundPos != -1) {
if (foundPos == 0 && elem.length != entry.length) {
ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
elem.substr(entry.length) + "</li>");
break;
} else if (entry.length >= instance.options.partialChars &&
instance.options.partialSearch && foundPos != -1) {
if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
foundPos + entry.length) + "</li>");
break;
}
}
foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
elem.indexOf(entry, foundPos + 1);
}
}
if (partial.length)
ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
return "<ul>" + ret.join('') + "</ul>";
}
}, options || {});
}
});
// AJAX in-place editor
//
// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
// Use this if you notice weird scrolling problems on some browsers,
// the DOM might be a bit confused when this gets called so do this
// waits 1 ms (with setTimeout) until it does the activation
Field.scrollFreeActivate = function(field) {
setTimeout(function() {
Field.activate(field);
}, 1);
}
Ajax.InPlaceEditor = Class.create();
Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
Ajax.InPlaceEditor.prototype = {
initialize: function(element, url, options) {
this.url = url;
this.element = $(element);
this.options = Object.extend({
okText: "ok",
cancelText: "cancel",
savingText: "Saving...",
clickToEditText: "Click to edit",
okText: "ok",
rows: 1,
onComplete: function(transport, element) {
new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
},
onFailure: function(transport) {
alert("Error communicating with the server: " + transport.responseText.stripTags());
},
callback: function(form) {
return Form.serialize(form);
},
handleLineBreaks: true,
loadingText: 'Loading...',
savingClassName: 'inplaceeditor-saving',
loadingClassName: 'inplaceeditor-loading',
formClassName: 'inplaceeditor-form',
highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
highlightendcolor: "#FFFFFF",
externalControl: null,
ajaxOptions: {}
}, options || {});
if(!this.options.formId && this.element.id) {
this.options.formId = this.element.id + "-inplaceeditor";
if ($(this.options.formId)) {
// there's already a form with that name, don't specify an id
this.options.formId = null;
}
}
if (this.options.externalControl) {
this.options.externalControl = $(this.options.externalControl);
}
this.originalBackground = Element.getStyle(this.element, 'background-color');
if (!this.originalBackground) {
this.originalBackground = "transparent";
}
this.element.title = this.options.clickToEditText;
this.onclickListener = this.enterEditMode.bindAsEventListener(this);
this.mouseoverListener = this.enterHover.bindAsEventListener(this);
this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
Event.observe(this.element, 'click', this.onclickListener);
Event.observe(this.element, 'mouseover', this.mouseoverListener);
Event.observe(this.element, 'mouseout', this.mouseoutListener);
if (this.options.externalControl) {
Event.observe(this.options.externalControl, 'click', this.onclickListener);
Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
}
},
enterEditMode: function(evt) {
if (this.saving) return;
if (this.editing) return;
this.editing = true;
this.onEnterEditMode();
if (this.options.externalControl) {
Element.hide(this.options.externalControl);
}
Element.hide(this.element);
this.createForm();
this.element.parentNode.insertBefore(this.form, this.element);
Field.scrollFreeActivate(this.editField);
// stop the event to avoid a page refresh in Safari
if (evt) {
Event.stop(evt);
}
return false;
},
createForm: function() {
this.form = document.createElement("form");
this.form.id = this.options.formId;
Element.addClassName(this.form, this.options.formClassName)
this.form.onsubmit = this.onSubmit.bind(this);
this.createEditField();
if (this.options.textarea) {
var br = document.createElement("br");
this.form.appendChild(br);
}
okButton = document.createElement("input");
okButton.type = "submit";
okButton.value = this.options.okText;
this.form.appendChild(okButton);
cancelLink = document.createElement("a");
cancelLink.href = "#";
cancelLink.appendChild(document.createTextNode(this.options.cancelText));
cancelLink.onclick = this.onclickCancel.bind(this);
this.form.appendChild(cancelLink);
},
hasHTMLLineBreaks: function(string) {
if (!this.options.handleLineBreaks) return false;
return string.match(/<br/i) || string.match(/<p>/i);
},
convertHTMLLineBreaks: function(string) {
return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
},
createEditField: function() {
var text;
if(this.options.loadTextURL) {
text = this.options.loadingText;
} else {
text = this.getText();
}
if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
this.options.textarea = false;
var textField = document.createElement("input");
textField.type = "text";
textField.name = "value";
textField.value = text;
textField.style.backgroundColor = this.options.highlightcolor;
var size = this.options.size || this.options.cols || 0;
if (size != 0) textField.size = size;
this.editField = textField;
} else {
this.options.textarea = true;
var textArea = document.createElement("textarea");
textArea.name = "value";
textArea.value = this.convertHTMLLineBreaks(text);
textArea.rows = this.options.rows;
textArea.cols = this.options.cols || 40;
this.editField = textArea;
}
if(this.options.loadTextURL) {
this.loadExternalText();
}
this.form.appendChild(this.editField);
},
getText: function() {
return this.element.innerHTML;
},
loadExternalText: function() {
Element.addClassName(this.form, this.options.loadingClassName);
this.editField.disabled = true;
new Ajax.Request(
this.options.loadTextURL,
Object.extend({
asynchronous: true,
onComplete: this.onLoadedExternalText.bind(this)
}, this.options.ajaxOptions)
);
},
onLoadedExternalText: function(transport) {
Element.removeClassName(this.form, this.options.loadingClassName);
this.editField.disabled = false;
this.editField.value = transport.responseText.stripTags();
},
onclickCancel: function() {
this.onComplete();
this.leaveEditMode();
return false;
},
onFailure: function(transport) {
this.options.onFailure(transport);
if (this.oldInnerHTML) {
this.element.innerHTML = this.oldInnerHTML;
this.oldInnerHTML = null;
}
return false;
},
onSubmit: function() {
// onLoading resets these so we need to save them away for the Ajax call
var form = this.form;
var value = this.editField.value;
// do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
// which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
// to be displayed indefinitely
this.onLoading();
new Ajax.Updater(
{
success: this.element,
// don't update on failure (this could be an option)
failure: null
},
this.url,
Object.extend({
parameters: this.options.callback(form, value),
onComplete: this.onComplete.bind(this),
onFailure: this.onFailure.bind(this)
}, this.options.ajaxOptions)
);
// stop the event to avoid a page refresh in Safari
if (arguments.length > 1) {
Event.stop(arguments[0]);
}
return false;
},
onLoading: function() {
this.saving = true;
this.removeForm();
this.leaveHover();
this.showSaving();
},
showSaving: function() {
this.oldInnerHTML = this.element.innerHTML;
this.element.innerHTML = this.options.savingText;
Element.addClassName(this.element, this.options.savingClassName);
this.element.style.backgroundColor = this.originalBackground;
Element.show(this.element);
},
removeForm: function() {
if(this.form) {
if (this.form.parentNode) Element.remove(this.form);
this.form = null;
}
},
enterHover: function() {
if (this.saving) return;
this.element.style.backgroundColor = this.options.highlightcolor;
if (this.effect) {
this.effect.cancel();
}
Element.addClassName(this.element, this.options.hoverClassName)
},
leaveHover: function() {
if (this.options.backgroundColor) {
this.element.style.backgroundColor = this.oldBackground;
}
Element.removeClassName(this.element, this.options.hoverClassName)
if (this.saving) return;
this.effect = new Effect.Highlight(this.element, {
startcolor: this.options.highlightcolor,
endcolor: this.options.highlightendcolor,
restorecolor: this.originalBackground
});
},
leaveEditMode: function() {
Element.removeClassName(this.element, this.options.savingClassName);
this.removeForm();
this.leaveHover();
this.element.style.backgroundColor = this.originalBackground;
Element.show(this.element);
if (this.options.externalControl) {
Element.show(this.options.externalControl);
}
this.editing = false;
this.saving = false;
this.oldInnerHTML = null;
this.onLeaveEditMode();
},
onComplete: function(transport) {
this.leaveEditMode();
this.options.onComplete.bind(this)(transport, this.element);
},
onEnterEditMode: function() {},
onLeaveEditMode: function() {},
dispose: function() {
if (this.oldInnerHTML) {
this.element.innerHTML = this.oldInnerHTML;
}
this.leaveEditMode();
Event.stopObserving(this.element, 'click', this.onclickListener);
Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
if (this.options.externalControl) {
Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
}
}
};
// Delayed observer, like Form.Element.Observer,
// but waits for delay after last key input
// Ideal for live-search fields
Form.Element.DelayedObserver = Class.create();
Form.Element.DelayedObserver.prototype = {
initialize: function(element, delay, callback) {
this.delay = delay || 0.5;
this.element = $(element);
this.callback = callback;
this.timer = null;
this.lastValue = $F(this.element);
Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
},
delayedListener: function(event) {
if(this.lastValue == $F(this.element)) return;
if(this.timer) clearTimeout(this.timer);
this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
this.lastValue = $F(this.element);
},
onTimerEvent: function() {
this.timer = null;
this.callback(this.element, $F(this.element));
}
};

View File

@ -1,581 +0,0 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// See scriptaculous.js for full license.
/*--------------------------------------------------------------------------*/
var Droppables = {
drops: [],
remove: function(element) {
this.drops = this.drops.reject(function(d) { return d.element==$(element) });
},
add: function(element) {
element = $(element);
var options = Object.extend({
greedy: true,
hoverclass: null
}, arguments[1] || {});
// cache containers
if(options.containment) {
options._containers = [];
var containment = options.containment;
if((typeof containment == 'object') &&
(containment.constructor == Array)) {
containment.each( function(c) { options._containers.push($(c)) });
} else {
options._containers.push($(containment));
}
}
if(options.accept) options.accept = [options.accept].flatten();
// Element.makePositioned(element); // fix IE
options.element = element;
this.drops.push(options);
},
isContained: function(element, drop) {
var parentNode = element.parentNode;
return drop._containers.detect(function(c) { return parentNode == c });
},
isAffected: function(pX, pY, element, drop) {
return (
(drop.element!=element) && (drop.element.parentNode) &&
((!drop._containers) ||
this.isContained(element, drop)) &&
((!drop.accept) ||
(Element.classNames(element).detect(
function(v) { return drop.accept.include(v) } ) )) &&
((!drop.checkDroppableIsntContained) || !Element.contains(element, drop.element)) &&
Position.within(drop.element, pX, pY) );
},
deactivate: function(drop) {
if(drop.hoverclass)
Element.removeClassName(drop.element, drop.hoverclass);
this.last_active = null;
},
activate: function(drop) {
if(this.last_active) this.deactivate(this.last_active);
if(drop.hoverclass)
Element.addClassName(drop.element, drop.hoverclass);
this.last_active = drop;
},
show: function(event, element) {
if(!this.drops.length) return;
var pX = Event.pointerX(event);
var pY = Event.pointerY(event);
Position.prepare();
var i = this.drops.length-1; do {
var drop = this.drops[i];
if(this.isAffected(pX, pY, element, drop)) {
if(drop.onHover)
drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
if(drop.greedy) {
this.activate(drop);
return;
}
}
} while (i--);
if(this.last_active) this.deactivate(this.last_active);
},
fire: function(event, element) {
if(!this.last_active) return;
Position.prepare();
if (this.isAffected(Event.pointerX(event), Event.pointerY(event), element, this.last_active))
if (this.last_active.onDrop)
this.last_active.onDrop(element, this.last_active.element, event);
},
reset: function() {
if(this.last_active)
this.deactivate(this.last_active);
}
}
var Draggables = {
observers: [],
addObserver: function(observer) {
this.observers.push(observer);
this._cacheObserverCallbacks();
},
removeObserver: function(element) { // element instead of observer fixes mem leaks
this.observers = this.observers.reject( function(o) { return o.element==element });
this._cacheObserverCallbacks();
},
notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
if(this[eventName+'Count'] > 0)
this.observers.each( function(o) {
if(o[eventName]) o[eventName](eventName, draggable, event);
});
},
_cacheObserverCallbacks: function() {
['onStart','onEnd','onDrag'].each( function(eventName) {
Draggables[eventName+'Count'] = Draggables.observers.select(
function(o) { return o[eventName]; }
).length;
});
}
}
/*--------------------------------------------------------------------------*/
var Draggable = Class.create();
Draggable.prototype = {
initialize: function(element) {
var options = Object.extend({
handle: false,
starteffect: function(element) {
new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7});
},
reverteffect: function(element, top_offset, left_offset) {
var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
new Effect.MoveBy(element, -top_offset, -left_offset, {duration:dur});
},
endeffect: function(element) {
new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0});
},
zindex: 1000,
revert: false,
snap: false // false, or xy or [x,y] or function(x,y){ return [x,y] }
}, arguments[1] || {});
this.element = $(element);
if(options.handle && (typeof options.handle == 'string'))
this.handle = Element.childrenWithClassName(this.element, options.handle)[0];
if(!this.handle) this.handle = $(options.handle);
if(!this.handle) this.handle = this.element;
this.offsetX = 0;
this.offsetY = 0;
this.originalLeft = this.currentLeft();
this.originalTop = this.currentTop();
this.originalX = this.element.offsetLeft;
this.originalY = this.element.offsetTop;
this.options = options;
this.active = false;
this.dragging = false;
this.eventMouseDown = this.startDrag.bindAsEventListener(this);
this.eventMouseUp = this.endDrag.bindAsEventListener(this);
this.eventMouseMove = this.update.bindAsEventListener(this);
this.eventKeypress = this.keyPress.bindAsEventListener(this);
this.registerEvents();
},
destroy: function() {
Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
this.unregisterEvents();
},
registerEvents: function() {
//if(!Draggable.eventsRegistered) {
//Draggable.eventsRegistered = true;
Event.observe(document, "mouseup", this.eventMouseUp);
Event.observe(document, "mousemove", this.eventMouseMove);
Event.observe(document, "keypress", this.eventKeypress);
Event.observe(this.handle, "mousedown", this.eventMouseDown);
//}
},
unregisterEvents: function() {
//if(!Draggable.eventsRegistered) {
//Draggable.eventsRegistered = false;
//if(!this.active) return;
Event.stopObserving(document, "mouseup", this.eventMouseUp);
Event.stopObserving(document, "mousemove", this.eventMouseMove);
Event.stopObserving(document, "keypress", this.eventKeypress);
//}
},
currentLeft: function() {
return parseInt(this.element.style.left || '0');
},
currentTop: function() {
return parseInt(this.element.style.top || '0')
},
startDrag: function(event) {
if(Event.isLeftClick(event)) {
// abort on form elements, fixes a Firefox issue
var src = Event.element(event);
if(src.tagName && (
src.tagName=='INPUT' ||
src.tagName=='SELECT' ||
src.tagName=='BUTTON' ||
src.tagName=='TEXTAREA')) return;
// this.registerEvents();
Element.makePositioned(this.element); // fix IE
this.element.wasDragged = false;
this.active = true;
var pointer = [Event.pointerX(event), Event.pointerY(event)];
var offsets = Position.cumulativeOffset(this.element);
this.offsetX = (pointer[0] - offsets[0]);
this.offsetY = (pointer[1] - offsets[1]);
Event.stop(event);
}
},
finishDrag: function(event, success) {
// this.unregisterEvents();
this.active = false;
this.dragging = false;
this.element.wasDragged = true;
if(this.options.ghosting) {
Position.relativize(this.element);
Element.remove(this._clone);
this._clone = null;
}
if(success) Droppables.fire(event, this.element);
Draggables.notify('onEnd', this, event);
var revert = this.options.revert;
if(revert && typeof revert == 'function') revert = revert(this.element);
if(revert && this.options.reverteffect) {
this.options.reverteffect(this.element,
this.currentTop()-this.originalTop,
this.currentLeft()-this.originalLeft);
} else {
this.originalLeft = this.currentLeft();
this.originalTop = this.currentTop();
}
if(this.options.zindex)
this.element.style.zIndex = this.originalZ;
if(this.options.endeffect)
this.options.endeffect(this.element);
Droppables.reset();
},
keyPress: function(event) {
if(this.active) {
if(event.keyCode==Event.KEY_ESC) {
Element.undoPositioned(this.element);
this.finishDrag(event, false);
Event.stop(event);
}
}
},
endDrag: function(event) {
Element.undoPositioned(this.element);
if(this.active && this.dragging) {
this.finishDrag(event, true);
Event.stop(event);
}
this.active = false;
this.dragging = false;
},
draw: function(event) {
var pointer = [Event.pointerX(event), Event.pointerY(event)];
var offsets = Position.cumulativeOffset(this.element);
offsets[0] -= this.currentLeft();
offsets[1] -= this.currentTop();
var style = this.element.style;
var pos = [
(pointer[0] - offsets[0] - this.offsetX),
(pointer[1] - offsets[1] - this.offsetY)];
if(this.options.snap) {
if(typeof this.options.snap == 'function') {
pos = this.options.snap(pos[0],pos[1]);
} else {
var draggable = this;
if(this.options.snap instanceof Array) {
pos = pos.collect( function(v, i) {
return Math.round(v/draggable.options.snap[i])*draggable.options.snap[i] })
} else {
pos = pos.collect( function(v) {
return Math.round(v/draggable.options.snap)*draggable.options.snap })
}
}}
if((!this.options.constraint) || (this.options.constraint=='horizontal'))
style.left = pos[0] + "px";
if((!this.options.constraint) || (this.options.constraint=='vertical'))
style.top = pos[1] + "px";
if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
},
update: function(event) {
if(this.active) {
if(!this.dragging) {
if(this.options.onStartDrag) this.options.onStartDrag(this.element);
var style = this.element.style;
this.dragging = true;
if(Element.getStyle(this.element,'position')=='')
style.position = "relative";
if(this.options.zindex) {
this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
style.zIndex = this.options.zindex;
}
if(this.options.ghosting) {
this._clone = this.element.cloneNode(true);
Position.absolutize(this.element);
this.element.parentNode.insertBefore(this._clone, this.element);
}
Draggables.notify('onStart', this, event);
if(this.options.starteffect) this.options.starteffect(this.element);
}
Droppables.show(event, this.element);
Draggables.notify('onDrag', this, event);
this.draw(event);
if(this.options.change) this.options.change(this);
// fix AppleWebKit rendering
if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
Event.stop(event);
}
}
}
function rollingStatus(chr, maxLength) {
var txt = window.status + chr;
if(txt.length > maxLength) txt = txt.substr(txt.length - maxLength);
window.status = txt;
}
/*--------------------------------------------------------------------------*/
var SortableObserver = Class.create();
SortableObserver.prototype = {
initialize: function(element, observer) {
this.element = $(element);
this.observer = observer;
this.lastValue = Sortable.serialize(this.element);
},
onStart: function() {
this.lastValue = Sortable.serialize(this.element);
},
onEnd: function() {
Sortable.unmark();
if(this.lastValue != Sortable.serialize(this.element))
this.observer(this.element)
}
}
var Sortable = {
sortables: new Array(),
options: function(element){
element = $(element);
return this.sortables.detect(function(s) { return s.element == element });
},
destroy: function(element){
element = $(element);
this.sortables.findAll(function(s) { return s.element == element }).each(function(s){
Draggables.removeObserver(s.element);
s.droppables.each(function(d){ Droppables.remove(d) });
s.draggables.invoke('destroy');
});
this.sortables = this.sortables.reject(function(s) { return s.element == element });
},
create: function(element) {
element = $(element);
var options = Object.extend({
element: element,
tag: 'li', // assumes li children, override with tag: 'tagname'
dropOnEmpty: false,
tree: false, // fixme: unimplemented
overlap: 'vertical', // one of 'vertical', 'horizontal'
constraint: 'vertical', // one of 'vertical', 'horizontal', false
containment: element, // also takes array of elements (or id's); or false
handle: false, // or a CSS class
only: false,
hoverclass: null,
ghosting: false,
format: null,
onChange: Prototype.emptyFunction,
onUpdate: Prototype.emptyFunction
}, arguments[1] || {});
// clear any old sortable with same element
this.destroy(element);
// build options for the draggables
var options_for_draggable = {
revert: true,
ghosting: options.ghosting,
constraint: options.constraint,
handle: options.handle };
if(options.starteffect)
options_for_draggable.starteffect = options.starteffect;
if(options.reverteffect)
options_for_draggable.reverteffect = options.reverteffect;
else
if(options.ghosting) options_for_draggable.reverteffect = function(element) {
element.style.top = 0;
element.style.left = 0;
};
if(options.endeffect)
options_for_draggable.endeffect = options.endeffect;
if(options.zindex)
options_for_draggable.zindex = options.zindex;
// build options for the droppables
var options_for_droppable = {
overlap: options.overlap,
containment: options.containment,
hoverclass: options.hoverclass,
onHover: Sortable.onHover,
greedy: !options.dropOnEmpty
}
// fix for gecko engine
Element.cleanWhitespace(element);
options.draggables = [];
options.droppables = [];
// make it so
// drop on empty handling
if(options.dropOnEmpty) {
Droppables.add(element,
{containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false});
options.droppables.push(element);
}
(this.findElements(element, options) || []).each( function(e) {
// handles are per-draggable
var handle = options.handle ?
Element.childrenWithClassName(e, options.handle)[0] : e;
options.draggables.push(
new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
Droppables.add(e, options_for_droppable);
options.droppables.push(e);
});
// keep reference
this.sortables.push(options);
// for onupdate
Draggables.addObserver(new SortableObserver(element, options.onUpdate));
},
// return all suitable-for-sortable elements in a guaranteed order
findElements: function(element, options) {
if(!element.hasChildNodes()) return null;
var elements = [];
$A(element.childNodes).each( function(e) {
if(e.tagName && e.tagName==options.tag.toUpperCase() &&
(!options.only || (Element.hasClassName(e, options.only))))
elements.push(e);
if(options.tree) {
var grandchildren = this.findElements(e, options);
if(grandchildren) elements.push(grandchildren);
}
});
return (elements.length>0 ? elements.flatten() : null);
},
onHover: function(element, dropon, overlap) {
if(overlap>0.5) {
Sortable.mark(dropon, 'before');
if(dropon.previousSibling != element) {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering
dropon.parentNode.insertBefore(element, dropon);
if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
} else {
Sortable.mark(dropon, 'after');
var nextElement = dropon.nextSibling || null;
if(nextElement != element) {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering
dropon.parentNode.insertBefore(element, nextElement);
if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
}
},
onEmptyHover: function(element, dropon) {
if(element.parentNode!=dropon) {
var oldParentNode = element.parentNode;
dropon.appendChild(element);
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon).onChange(element);
}
},
unmark: function() {
if(Sortable._marker) Element.hide(Sortable._marker);
},
mark: function(dropon, position) {
// mark on ghosting only
var sortable = Sortable.options(dropon.parentNode);
if(sortable && !sortable.ghosting) return;
if(!Sortable._marker) {
Sortable._marker = $('dropmarker') || document.createElement('DIV');
Element.hide(Sortable._marker);
Element.addClassName(Sortable._marker, 'dropmarker');
Sortable._marker.style.position = 'absolute';
document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
}
var offsets = Position.cumulativeOffset(dropon);
Sortable._marker.style.left = offsets[0] + 'px';
Sortable._marker.style.top = offsets[1] + 'px';
if(position=='after')
if(sortable.overlap == 'horizontal')
Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px';
else
Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
Element.show(Sortable._marker);
},
serialize: function(element) {
element = $(element);
var sortableOptions = this.options(element);
var options = Object.extend({
tag: sortableOptions.tag,
only: sortableOptions.only,
name: element.id,
format: sortableOptions.format || /^[^_]*_(.*)$/
}, arguments[1] || {});
var ret = $(this.findElements(element, options) || []).collect( function(item) {
return (encodeURIComponent(options.name) + "[]=" +
encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : ''));
}).join("&");
return ret;
}
}

View File

@ -1,904 +0,0 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// Contributors:
// Justin Palmer (http://encytemedia.com/)
// Mark Pilgrim (http://diveintomark.org/)
// Martin Bialasinki
//
// See scriptaculous.js for full license.
/* ------------- element ext -------------- */
// converts rgb() and #xxx to #xxxxxx format,
// returns self (or first argument) if not convertable
String.prototype.parseColor = function() {
color = "#";
if(this.slice(0,4) == "rgb(") {
var cols = this.slice(4,this.length-1).split(',');
var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
} else {
if(this.slice(0,1) == '#') {
if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
if(this.length==7) color = this.toLowerCase();
}
}
return(color.length==7 ? color : (arguments[0] || this));
}
Element.collectTextNodesIgnoreClass = function(element, ignoreclass) {
var children = $(element).childNodes;
var text = "";
var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i");
for (var i = 0; i < children.length; i++) {
if(children[i].nodeType==3) {
text+=children[i].nodeValue;
} else {
if((!children[i].className.match(classtest)) && children[i].hasChildNodes())
text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass);
}
}
return text;
}
Element.setContentZoom = function(element, percent) {
element = $(element);
element.style.fontSize = (percent/100) + "em";
if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
}
Element.getOpacity = function(element){
var opacity;
if (opacity = Element.getStyle(element, "opacity"))
return parseFloat(opacity);
if (opacity = (Element.getStyle(element, "filter") || '').match(/alpha\(opacity=(.*)\)/))
if(opacity[1]) return parseFloat(opacity[1]) / 100;
return 1.0;
}
Element.setOpacity = function(element, value){
element= $(element);
var els = element.style;
if (value == 1){
els.opacity = '0.999999';
if(/MSIE/.test(navigator.userAgent))
els.filter = Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'');
} else {
if(value < 0.00001) value = 0;
els.opacity = value;
if(/MSIE/.test(navigator.userAgent))
els.filter = Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') +
"alpha(opacity="+value*100+")";
}
}
Element.getInlineOpacity = function(element){
element= $(element);
var op;
op = element.style.opacity;
if (typeof op != "undefined" && op != "") return op;
return "";
}
Element.setInlineOpacity = function(element, value){
element= $(element);
var els = element.style;
els.opacity = value;
}
Element.childrenWithClassName = function(element, className) {
return $A($(element).getElementsByTagName('*')).select(
function(c) { return Element.hasClassName(c, className) });
}
/*--------------------------------------------------------------------------*/
var Effect = {
tagifyText: function(element) {
var tagifyStyle = "position:relative";
if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ";zoom:1";
element = $(element);
$A(element.childNodes).each( function(child) {
if(child.nodeType==3) {
child.nodeValue.toArray().each( function(character) {
element.insertBefore(
Builder.node('span',{style: tagifyStyle},
character == " " ? String.fromCharCode(160) : character),
child);
});
Element.remove(child);
}
});
},
multiple: function(element, effect) {
var elements;
if(((typeof element == 'object') ||
(typeof element == 'function')) &&
(element.length))
elements = element;
else
elements = $(element).childNodes;
var options = Object.extend({
speed: 0.1,
delay: 0.0
}, arguments[2] || {});
var speed = options.speed;
var delay = options.delay;
$A(elements).each( function(element, index) {
new effect(element, Object.extend(options, { delay: delay + index * speed }));
});
}
};
var Effect2 = Effect; // deprecated
/* ------------- transitions ------------- */
Effect.Transitions = {}
Effect.Transitions.linear = function(pos) {
return pos;
}
Effect.Transitions.sinoidal = function(pos) {
return (-Math.cos(pos*Math.PI)/2) + 0.5;
}
Effect.Transitions.reverse = function(pos) {
return 1-pos;
}
Effect.Transitions.flicker = function(pos) {
return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
}
Effect.Transitions.wobble = function(pos) {
return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
}
Effect.Transitions.pulse = function(pos) {
return (Math.floor(pos*10) % 2 == 0 ?
(pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10)));
}
Effect.Transitions.none = function(pos) {
return 0;
}
Effect.Transitions.full = function(pos) {
return 1;
}
/* ------------- core effects ------------- */
Effect.Queue = {
effects: [],
_each: function(iterator) {
this.effects._each(iterator);
},
interval: null,
add: function(effect) {
var timestamp = new Date().getTime();
switch(effect.options.queue) {
case 'front':
// move unstarted effects after this effect
this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
e.startOn += effect.finishOn;
e.finishOn += effect.finishOn;
});
break;
case 'end':
// start effect after last queued effect has finished
timestamp = this.effects.pluck('finishOn').max() || timestamp;
break;
}
effect.startOn += timestamp;
effect.finishOn += timestamp;
this.effects.push(effect);
if(!this.interval)
this.interval = setInterval(this.loop.bind(this), 40);
},
remove: function(effect) {
this.effects = this.effects.reject(function(e) { return e==effect });
if(this.effects.length == 0) {
clearInterval(this.interval);
this.interval = null;
}
},
loop: function() {
var timePos = new Date().getTime();
this.effects.invoke('loop', timePos);
}
}
Object.extend(Effect.Queue, Enumerable);
Effect.Base = function() {};
Effect.Base.prototype = {
position: null,
setOptions: function(options) {
this.options = Object.extend({
transition: Effect.Transitions.sinoidal,
duration: 1.0, // seconds
fps: 25.0, // max. 25fps due to Effect.Queue implementation
sync: false, // true for combining
from: 0.0,
to: 1.0,
delay: 0.0,
queue: 'parallel'
}, options || {});
},
start: function(options) {
this.setOptions(options || {});
this.currentFrame = 0;
this.state = 'idle';
this.startOn = this.options.delay*1000;
this.finishOn = this.startOn + (this.options.duration*1000);
this.event('beforeStart');
if(!this.options.sync) Effect.Queue.add(this);
},
loop: function(timePos) {
if(timePos >= this.startOn) {
if(timePos >= this.finishOn) {
this.render(1.0);
this.cancel();
this.event('beforeFinish');
if(this.finish) this.finish();
this.event('afterFinish');
return;
}
var pos = (timePos - this.startOn) / (this.finishOn - this.startOn);
var frame = Math.round(pos * this.options.fps * this.options.duration);
if(frame > this.currentFrame) {
this.render(pos);
this.currentFrame = frame;
}
}
},
render: function(pos) {
if(this.state == 'idle') {
this.state = 'running';
this.event('beforeSetup');
if(this.setup) this.setup();
this.event('afterSetup');
}
if(this.options.transition) pos = this.options.transition(pos);
pos *= (this.options.to-this.options.from);
pos += this.options.from;
this.position = pos;
this.event('beforeUpdate');
if(this.update) this.update(pos);
this.event('afterUpdate');
},
cancel: function() {
if(!this.options.sync) Effect.Queue.remove(this);
this.state = 'finished';
},
event: function(eventName) {
if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
if(this.options[eventName]) this.options[eventName](this);
}
}
Effect.Parallel = Class.create();
Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
initialize: function(effects) {
this.effects = effects || [];
this.start(arguments[1]);
},
update: function(position) {
this.effects.invoke('render', position);
},
finish: function(position) {
this.effects.each( function(effect) {
effect.render(1.0);
effect.cancel();
effect.event('beforeFinish');
if(effect.finish) effect.finish(position);
effect.event('afterFinish');
});
}
});
Effect.Opacity = Class.create();
Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
// make this work on IE on elements without 'layout'
if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout))
this.element.style.zoom = 1;
var options = Object.extend({
from: Element.getOpacity(this.element) || 0.0,
to: 1.0
}, arguments[1] || {});
this.start(options);
},
update: function(position) {
Element.setOpacity(this.element, position);
}
});
Effect.MoveBy = Class.create();
Object.extend(Object.extend(Effect.MoveBy.prototype, Effect.Base.prototype), {
initialize: function(element, toTop, toLeft) {
this.element = $(element);
this.toTop = toTop;
this.toLeft = toLeft;
this.start(arguments[3]);
},
setup: function() {
// Bug in Opera: Opera returns the "real" position of a static element or
// relative element that does not have top/left explicitly set.
// ==> Always set top and left for position relative elements in your stylesheets
// (to 0 if you do not need them)
Element.makePositioned(this.element);
this.originalTop = parseFloat(Element.getStyle(this.element,'top') || '0');
this.originalLeft = parseFloat(Element.getStyle(this.element,'left') || '0');
},
update: function(position) {
var topd = this.toTop * position + this.originalTop;
var leftd = this.toLeft * position + this.originalLeft;
this.setPosition(topd, leftd);
},
setPosition: function(topd, leftd) {
this.element.style.top = topd + "px";
this.element.style.left = leftd + "px";
}
});
Effect.Scale = Class.create();
Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
initialize: function(element, percent) {
this.element = $(element)
var options = Object.extend({
scaleX: true,
scaleY: true,
scaleContent: true,
scaleFromCenter: false,
scaleMode: 'box', // 'box' or 'contents' or {} with provided values
scaleFrom: 100.0,
scaleTo: percent
}, arguments[2] || {});
this.start(options);
},
setup: function() {
var effect = this;
this.restoreAfterFinish = this.options.restoreAfterFinish || false;
this.elementPositioning = Element.getStyle(this.element,'position');
effect.originalStyle = {};
['top','left','width','height','fontSize'].each( function(k) {
effect.originalStyle[k] = effect.element.style[k];
});
this.originalTop = this.element.offsetTop;
this.originalLeft = this.element.offsetLeft;
var fontSize = Element.getStyle(this.element,'font-size') || "100%";
['em','px','%'].each( function(fontSizeType) {
if(fontSize.indexOf(fontSizeType)>0) {
effect.fontSize = parseFloat(fontSize);
effect.fontSizeType = fontSizeType;
}
});
this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
this.dims = null;
if(this.options.scaleMode=='box')
this.dims = [this.element.clientHeight, this.element.clientWidth];
if(/^content/.test(this.options.scaleMode))
this.dims = [this.element.scrollHeight, this.element.scrollWidth];
if(!this.dims)
this.dims = [this.options.scaleMode.originalHeight,
this.options.scaleMode.originalWidth];
},
update: function(position) {
var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
if(this.options.scaleContent && this.fontSize)
this.element.style.fontSize = this.fontSize*currentScale + this.fontSizeType;
this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
},
finish: function(position) {
if (this.restoreAfterFinish) {
var effect = this;
['top','left','width','height','fontSize'].each( function(k) {
effect.element.style[k] = effect.originalStyle[k];
});
}
},
setDimensions: function(height, width) {
var els = this.element.style;
if(this.options.scaleX) els.width = width + 'px';
if(this.options.scaleY) els.height = height + 'px';
if(this.options.scaleFromCenter) {
var topd = (height - this.dims[0])/2;
var leftd = (width - this.dims[1])/2;
if(this.elementPositioning == 'absolute') {
if(this.options.scaleY) els.top = this.originalTop-topd + "px";
if(this.options.scaleX) els.left = this.originalLeft-leftd + "px";
} else {
if(this.options.scaleY) els.top = -topd + "px";
if(this.options.scaleX) els.left = -leftd + "px";
}
}
}
});
Effect.Highlight = Class.create();
Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
var options = Object.extend({
startcolor: "#ffff99"
}, arguments[1] || {});
this.start(options);
},
setup: function() {
// Prevent executing on elements not in the layout flow
if(this.element.style.display=='none') { this.cancel(); return; }
// Disable background image during the effect
this.oldBgImage = this.element.style.backgroundImage;
this.element.style.backgroundImage = "none";
if(!this.options.endcolor)
this.options.endcolor = Element.getStyle(this.element, 'background-color').parseColor('#ffffff');
if (typeof this.options.restorecolor == "undefined")
this.options.restorecolor = this.element.style.backgroundColor;
// init color calculations
this.colors_base = [
parseInt(this.options.startcolor.slice(1,3),16),
parseInt(this.options.startcolor.slice(3,5),16),
parseInt(this.options.startcolor.slice(5),16) ];
this.colors_delta = [
parseInt(this.options.endcolor.slice(1,3),16)-this.colors_base[0],
parseInt(this.options.endcolor.slice(3,5),16)-this.colors_base[1],
parseInt(this.options.endcolor.slice(5),16)-this.colors_base[2]];
},
update: function(position) {
var effect = this; var colors = $R(0,2).map( function(i){
return Math.round(effect.colors_base[i]+(effect.colors_delta[i]*position))
});
this.element.style.backgroundColor = "#" +
colors[0].toColorPart() + colors[1].toColorPart() + colors[2].toColorPart();
},
finish: function() {
this.element.style.backgroundColor = this.options.restorecolor;
this.element.style.backgroundImage = this.oldBgImage;
}
});
Effect.ScrollTo = Class.create();
Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
this.start(arguments[1] || {});
},
setup: function() {
Position.prepare();
var offsets = Position.cumulativeOffset(this.element);
var max = window.innerHeight ?
window.height - window.innerHeight :
document.body.scrollHeight -
(document.documentElement.clientHeight ?
document.documentElement.clientHeight : document.body.clientHeight);
this.scrollStart = Position.deltaY;
this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
},
update: function(position) {
Position.prepare();
window.scrollTo(Position.deltaX,
this.scrollStart + (position*this.delta));
}
});
/* ------------- combination effects ------------- */
Effect.Fade = function(element) {
var oldOpacity = Element.getInlineOpacity(element);
var options = Object.extend({
from: Element.getOpacity(element) || 1.0,
to: 0.0,
afterFinishInternal: function(effect)
{ if (effect.options.to == 0) {
Element.hide(effect.element);
Element.setInlineOpacity(effect.element, oldOpacity);
}
}
}, arguments[1] || {});
return new Effect.Opacity(element,options);
}
Effect.Appear = function(element) {
var options = Object.extend({
from: (Element.getStyle(element, "display") == "none" ? 0.0 : Element.getOpacity(element) || 0.0),
to: 1.0,
beforeSetup: function(effect)
{ Element.setOpacity(effect.element, effect.options.from);
Element.show(effect.element); }
}, arguments[1] || {});
return new Effect.Opacity(element,options);
}
Effect.Puff = function(element) {
element = $(element);
var oldOpacity = Element.getInlineOpacity(element);
var oldPosition = element.style.position;
return new Effect.Parallel(
[ new Effect.Scale(element, 200,
{ sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
Object.extend({ duration: 1.0,
beforeSetupInternal: function(effect)
{ effect.effects[0].element.style.position = 'absolute'; },
afterFinishInternal: function(effect)
{ Element.hide(effect.effects[0].element);
effect.effects[0].element.style.position = oldPosition;
Element.setInlineOpacity(effect.effects[0].element, oldOpacity); }
}, arguments[1] || {})
);
}
Effect.BlindUp = function(element) {
element = $(element);
Element.makeClipping(element);
return new Effect.Scale(element, 0,
Object.extend({ scaleContent: false,
scaleX: false,
restoreAfterFinish: true,
afterFinishInternal: function(effect)
{
Element.hide(effect.element);
Element.undoClipping(effect.element);
}
}, arguments[1] || {})
);
}
Effect.BlindDown = function(element) {
element = $(element);
var oldHeight = element.style.height;
var elementDimensions = Element.getDimensions(element);
return new Effect.Scale(element, 100,
Object.extend({ scaleContent: false,
scaleX: false,
scaleFrom: 0,
scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
restoreAfterFinish: true,
afterSetup: function(effect) {
Element.makeClipping(effect.element);
effect.element.style.height = "0px";
Element.show(effect.element);
},
afterFinishInternal: function(effect) {
Element.undoClipping(effect.element);
effect.element.style.height = oldHeight;
}
}, arguments[1] || {})
);
}
Effect.SwitchOff = function(element) {
element = $(element);
var oldOpacity = Element.getInlineOpacity(element);
return new Effect.Appear(element, {
duration: 0.4,
from: 0,
transition: Effect.Transitions.flicker,
afterFinishInternal: function(effect) {
new Effect.Scale(effect.element, 1, {
duration: 0.3, scaleFromCenter: true,
scaleX: false, scaleContent: false, restoreAfterFinish: true,
beforeSetup: function(effect) {
Element.makePositioned(effect.element);
Element.makeClipping(effect.element);
},
afterFinishInternal: function(effect) {
Element.hide(effect.element);
Element.undoClipping(effect.element);
Element.undoPositioned(effect.element);
Element.setInlineOpacity(effect.element, oldOpacity);
}
})
}
});
}
Effect.DropOut = function(element) {
element = $(element);
var oldTop = element.style.top;
var oldLeft = element.style.left;
var oldOpacity = Element.getInlineOpacity(element);
return new Effect.Parallel(
[ new Effect.MoveBy(element, 100, 0, { sync: true }),
new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
Object.extend(
{ duration: 0.5,
beforeSetup: function(effect) {
Element.makePositioned(effect.effects[0].element); },
afterFinishInternal: function(effect) {
Element.hide(effect.effects[0].element);
Element.undoPositioned(effect.effects[0].element);
effect.effects[0].element.style.left = oldLeft;
effect.effects[0].element.style.top = oldTop;
Element.setInlineOpacity(effect.effects[0].element, oldOpacity); }
}, arguments[1] || {}));
}
Effect.Shake = function(element) {
element = $(element);
var oldTop = element.style.top;
var oldLeft = element.style.left;
return new Effect.MoveBy(element, 0, 20,
{ duration: 0.05, afterFinishInternal: function(effect) {
new Effect.MoveBy(effect.element, 0, -40,
{ duration: 0.1, afterFinishInternal: function(effect) {
new Effect.MoveBy(effect.element, 0, 40,
{ duration: 0.1, afterFinishInternal: function(effect) {
new Effect.MoveBy(effect.element, 0, -40,
{ duration: 0.1, afterFinishInternal: function(effect) {
new Effect.MoveBy(effect.element, 0, 40,
{ duration: 0.1, afterFinishInternal: function(effect) {
new Effect.MoveBy(effect.element, 0, -20,
{ duration: 0.05, afterFinishInternal: function(effect) {
Element.undoPositioned(effect.element);
effect.element.style.left = oldLeft;
effect.element.style.top = oldTop;
}}) }}) }}) }}) }}) }});
}
Effect.SlideDown = function(element) {
element = $(element);
Element.cleanWhitespace(element);
// SlideDown need to have the content of the element wrapped in a container element with fixed height!
var oldInnerBottom = element.firstChild.style.bottom;
var elementDimensions = Element.getDimensions(element);
return new Effect.Scale(element, 100,
Object.extend({ scaleContent: false,
scaleX: false,
scaleFrom: 0,
scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
restoreAfterFinish: true,
afterSetup: function(effect) {
Element.makePositioned(effect.element.firstChild);
if (window.opera) effect.element.firstChild.style.top = "";
Element.makeClipping(effect.element);
element.style.height = '0';
Element.show(element);
},
afterUpdateInternal: function(effect) {
effect.element.firstChild.style.bottom =
(effect.dims[0] - effect.element.clientHeight) + 'px'; },
afterFinishInternal: function(effect) {
Element.undoClipping(effect.element);
Element.undoPositioned(effect.element.firstChild);
effect.element.firstChild.style.bottom = oldInnerBottom; }
}, arguments[1] || {})
);
}
Effect.SlideUp = function(element) {
element = $(element);
Element.cleanWhitespace(element);
var oldInnerBottom = element.firstChild.style.bottom;
return new Effect.Scale(element, 0,
Object.extend({ scaleContent: false,
scaleX: false,
scaleMode: 'box',
scaleFrom: 100,
restoreAfterFinish: true,
beforeStartInternal: function(effect) {
Element.makePositioned(effect.element.firstChild);
if (window.opera) effect.element.firstChild.style.top = "";
Element.makeClipping(effect.element);
Element.show(element);
},
afterUpdateInternal: function(effect) {
effect.element.firstChild.style.bottom =
(effect.dims[0] - effect.element.clientHeight) + 'px'; },
afterFinishInternal: function(effect) {
Element.hide(effect.element);
Element.undoClipping(effect.element);
Element.undoPositioned(effect.element.firstChild);
effect.element.firstChild.style.bottom = oldInnerBottom; }
}, arguments[1] || {})
);
}
Effect.Squish = function(element) {
// Bug in opera makes the TD containing this element expand for a instance after finish
return new Effect.Scale(element, window.opera ? 1 : 0,
{ restoreAfterFinish: true,
beforeSetup: function(effect) {
Element.makeClipping(effect.element); },
afterFinishInternal: function(effect) {
Element.hide(effect.element);
Element.undoClipping(effect.element); }
});
}
Effect.Grow = function(element) {
element = $(element);
var options = arguments[1] || {};
var elementDimensions = Element.getDimensions(element);
var originalWidth = elementDimensions.width;
var originalHeight = elementDimensions.height;
var oldTop = element.style.top;
var oldLeft = element.style.left;
var oldHeight = element.style.height;
var oldWidth = element.style.width;
var oldOpacity = Element.getInlineOpacity(element);
var direction = options.direction || 'center';
var moveTransition = options.moveTransition || Effect.Transitions.sinoidal;
var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal;
var opacityTransition = options.opacityTransition || Effect.Transitions.full;
var initialMoveX, initialMoveY;
var moveX, moveY;
switch (direction) {
case 'top-left':
initialMoveX = initialMoveY = moveX = moveY = 0;
break;
case 'top-right':
initialMoveX = originalWidth;
initialMoveY = moveY = 0;
moveX = -originalWidth;
break;
case 'bottom-left':
initialMoveX = moveX = 0;
initialMoveY = originalHeight;
moveY = -originalHeight;
break;
case 'bottom-right':
initialMoveX = originalWidth;
initialMoveY = originalHeight;
moveX = -originalWidth;
moveY = -originalHeight;
break;
case 'center':
initialMoveX = originalWidth / 2;
initialMoveY = originalHeight / 2;
moveX = -originalWidth / 2;
moveY = -originalHeight / 2;
break;
}
return new Effect.MoveBy(element, initialMoveY, initialMoveX, {
duration: 0.01,
beforeSetup: function(effect) {
Element.hide(effect.element);
Element.makeClipping(effect.element);
Element.makePositioned(effect.element);
},
afterFinishInternal: function(effect) {
new Effect.Parallel(
[ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: opacityTransition }),
new Effect.MoveBy(effect.element, moveY, moveX, { sync: true, transition: moveTransition }),
new Effect.Scale(effect.element, 100, {
scaleMode: { originalHeight: originalHeight, originalWidth: originalWidth },
sync: true, scaleFrom: window.opera ? 1 : 0, transition: scaleTransition, restoreAfterFinish: true})
], Object.extend({
beforeSetup: function(effect) {
effect.effects[0].element.style.height = 0;
Element.show(effect.effects[0].element);
},
afterFinishInternal: function(effect) {
var el = effect.effects[0].element;
var els = el.style;
Element.undoClipping(el);
Element.undoPositioned(el);
els.top = oldTop;
els.left = oldLeft;
els.height = oldHeight;
els.width = originalWidth + 'px';
Element.setInlineOpacity(el, oldOpacity);
}
}, options)
)
}
});
}
Effect.Shrink = function(element) {
element = $(element);
var options = arguments[1] || {};
var originalWidth = element.clientWidth;
var originalHeight = element.clientHeight;
var oldTop = element.style.top;
var oldLeft = element.style.left;
var oldHeight = element.style.height;
var oldWidth = element.style.width;
var oldOpacity = Element.getInlineOpacity(element);
var direction = options.direction || 'center';
var moveTransition = options.moveTransition || Effect.Transitions.sinoidal;
var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal;
var opacityTransition = options.opacityTransition || Effect.Transitions.none;
var moveX, moveY;
switch (direction) {
case 'top-left':
moveX = moveY = 0;
break;
case 'top-right':
moveX = originalWidth;
moveY = 0;
break;
case 'bottom-left':
moveX = 0;
moveY = originalHeight;
break;
case 'bottom-right':
moveX = originalWidth;
moveY = originalHeight;
break;
case 'center':
moveX = originalWidth / 2;
moveY = originalHeight / 2;
break;
}
return new Effect.Parallel(
[ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: opacityTransition }),
new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: scaleTransition, restoreAfterFinish: true}),
new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: moveTransition })
], Object.extend({
beforeStartInternal: function(effect) {
Element.makePositioned(effect.effects[0].element);
Element.makeClipping(effect.effects[0].element);
},
afterFinishInternal: function(effect) {
var el = effect.effects[0].element;
var els = el.style;
Element.hide(el);
Element.undoClipping(el);
Element.undoPositioned(el);
els.top = oldTop;
els.left = oldLeft;
els.height = oldHeight;
els.width = oldWidth;
Element.setInlineOpacity(el, oldOpacity);
}
}, options)
);
}
Effect.Pulsate = function(element) {
element = $(element);
var options = arguments[1] || {};
var oldOpacity = Element.getInlineOpacity(element);
var transition = options.transition || Effect.Transitions.sinoidal;
var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) };
reverser.bind(transition);
return new Effect.Opacity(element,
Object.extend(Object.extend({ duration: 3.0, from: 0,
afterFinishInternal: function(effect) { Element.setInlineOpacity(effect.element, oldOpacity); }
}, options), {transition: reverser}));
}
Effect.Fold = function(element) {
element = $(element);
var originalTop = element.style.top;
var originalLeft = element.style.left;
var originalWidth = element.style.width;
var originalHeight = element.style.height;
Element.makeClipping(element);
return new Effect.Scale(element, 5, Object.extend({
scaleContent: false,
scaleX: false,
afterFinishInternal: function(effect) {
new Effect.Scale(element, 1, {
scaleContent: false,
scaleY: false,
afterFinishInternal: function(effect) {
Element.hide(effect.element);
Element.undoClipping(effect.element);
effect.element.style.top = originalTop;
effect.element.style.left = originalLeft;
effect.element.style.width = originalWidth;
effect.element.style.height = originalHeight;
} });
}}, arguments[1] || {}));
}

View File

@ -1,48 +0,0 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
var Scriptaculous = {
Version: '1.5_rc5',
require: function(libraryName) {
// inserting via DOM fails in Safari 2.0, so brute force approach
document.write('<script type="text/javascript" src="'+libraryName+'"></script>');
},
load: function() {
if((typeof Prototype=='undefined') ||
parseFloat(Prototype.Version.split(".")[0] + "." +
Prototype.Version.split(".")[1]) < 1.4)
throw("script.aculo.us requires the Prototype JavaScript framework >= 1.4.0");
var scriptTags = document.getElementsByTagName("script");
for(var i=0;i<scriptTags.length;i++) {
if(scriptTags[i].src && scriptTags[i].src.match(/scriptaculous\.js(\?.*)?$/)) {
var path = scriptTags[i].src.replace(/scriptaculous\.js(\?.*)?$/,'');
this.require(path + 'builder.js');
this.require(path + 'effects.js');
this.require(path + 'dragdrop.js');
this.require(path + 'controls.js');
this.require(path + 'slider.js');
break;
}
}
}
}
Scriptaculous.load();

View File

@ -1,275 +0,0 @@
// Copyright (c) 2005 Marty Haught, Thomas Fuchs
//
// See http://script.aculo.us for more info
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
if(!Control) var Control = {};
Control.Slider = Class.create();
// options:
// axis: 'vertical', or 'horizontal' (default)
//
// callbacks:
// onChange(value)
// onSlide(value)
Control.Slider.prototype = {
initialize: function(handle, track, options) {
var slider = this;
if(handle instanceof Array) {
this.handles = handle.collect( function(e) { return $(e) });
} else {
this.handles = [$(handle)];
}
this.track = $(track);
this.options = options || {};
this.axis = this.options.axis || 'horizontal';
this.increment = this.options.increment || 1;
this.step = parseInt(this.options.step || '1');
this.range = this.options.range || $R(0,1);
this.value = 0; // assure backwards compat
this.values = this.handles.map( function() { return 0 });
this.spans = this.options.spans ? this.options.spans.map(function(s){ return $(s) }) : false;
this.options.startSpan = $(this.options.startSpan || null);
this.options.endSpan = $(this.options.endSpan || null);
this.restricted = this.options.restricted || false;
this.maximum = this.options.maximum || this.range.end;
this.minimum = this.options.minimum || this.range.start;
// Will be used to align the handle onto the track, if necessary
this.alignX = parseInt(this.options.alignX || '0');
this.alignY = parseInt(this.options.alignY || '0');
this.trackLength = this.maximumOffset() - this.minimumOffset();
this.active = false;
this.dragging = false;
this.disabled = false;
if(this.options.disabled) this.setDisabled();
// Allowed values array
this.allowedValues = this.options.values ? this.options.values.sortBy(Prototype.K) : false;
if(this.allowedValues) {
this.minimum = this.allowedValues.min();
this.maximum = this.allowedValues.max();
}
this.eventMouseDown = this.startDrag.bindAsEventListener(this);
this.eventMouseUp = this.endDrag.bindAsEventListener(this);
this.eventMouseMove = this.update.bindAsEventListener(this);
// Initialize handles in reverse (make sure first handle is active)
this.handles.each( function(h,i) {
i = slider.handles.length-1-i;
slider.setValue(parseFloat(
(slider.options.sliderValue instanceof Array ?
slider.options.sliderValue[i] : slider.options.sliderValue) ||
slider.range.start), i);
Element.makePositioned(h); // fix IE
Event.observe(h, "mousedown", slider.eventMouseDown);
});
Event.observe(this.track, "mousedown", this.eventMouseDown);
Event.observe(document, "mouseup", this.eventMouseUp);
Event.observe(document, "mousemove", this.eventMouseMove);
this.initialized = true;
},
dispose: function() {
var slider = this;
Event.stopObserving(this.track, "mousedown", this.eventMouseDown);
Event.stopObserving(document, "mouseup", this.eventMouseUp);
Event.stopObserving(document, "mousemove", this.eventMouseMove);
this.handles.each( function(h) {
Event.stopObserving(h, "mousedown", slider.eventMouseDown);
});
},
setDisabled: function(){
this.disabled = true;
},
setEnabled: function(){
this.disabled = false;
},
getNearestValue: function(value){
if(this.allowedValues){
if(value >= this.allowedValues.max()) return(this.allowedValues.max());
if(value <= this.allowedValues.min()) return(this.allowedValues.min());
var offset = Math.abs(this.allowedValues[0] - value);
var newValue = this.allowedValues[0];
this.allowedValues.each( function(v) {
var currentOffset = Math.abs(v - value);
if(currentOffset <= offset){
newValue = v;
offset = currentOffset;
}
});
return newValue;
}
if(value > this.range.end) return this.range.end;
if(value < this.range.start) return this.range.start;
return value;
},
setValue: function(sliderValue, handleIdx){
if(!this.active) {
this.activeHandle = this.handles[handleIdx];
this.activeHandleIdx = handleIdx;
this.updateStyles();
}
handleIdx = handleIdx || this.activeHandleIdx || 0;
if(this.initialized && this.restricted) {
if((handleIdx>0) && (sliderValue<this.values[handleIdx-1]))
sliderValue = this.values[handleIdx-1];
if((handleIdx < (this.handles.length-1)) && (sliderValue>this.values[handleIdx+1]))
sliderValue = this.values[handleIdx+1];
}
sliderValue = this.getNearestValue(sliderValue);
this.values[handleIdx] = sliderValue;
this.value = this.values[0]; // assure backwards compat
this.handles[handleIdx].style[this.isVertical() ? 'top' : 'left'] =
this.translateToPx(sliderValue);
this.drawSpans();
if(!this.dragging || !this.event) this.updateFinished();
},
setValueBy: function(delta, handleIdx) {
this.setValue(this.values[handleIdx || this.activeHandleIdx || 0] + delta,
handleIdx || this.activeHandleIdx || 0);
},
translateToPx: function(value) {
return Math.round((this.trackLength / (this.range.end - this.range.start)) * (value - this.range.start)) + "px";
},
translateToValue: function(offset) {
return ((offset/this.trackLength) * (this.range.end - this.range.start)) + this.range.start;
},
getRange: function(range) {
var v = this.values.sortBy(Prototype.K);
range = range || 0;
return $R(v[range],v[range+1]);
},
minimumOffset: function(){
return(this.isVertical() ? this.alignY : this.alignX);
},
maximumOffset: function(){
return(this.isVertical() ?
this.track.offsetHeight - this.alignY : this.track.offsetWidth - this.alignX);
},
isVertical: function(){
return (this.axis == 'vertical');
},
drawSpans: function() {
var slider = this;
if(this.spans)
$R(0, this.spans.length-1).each(function(r) { slider.setSpan(slider.spans[r], slider.getRange(r)) });
if(this.options.startSpan)
this.setSpan(this.options.startSpan,
$R(0, this.values.length>1 ? this.getRange(0).min() : this.value ));
if(this.options.endSpan)
this.setSpan(this.options.endSpan,
$R(this.values.length>1 ? this.getRange(this.spans.length-1).max() : this.value, this.maximum));
},
setSpan: function(span, range) {
if(this.isVertical()) {
span.style.top = this.translateToPx(range.start);
span.style.height = this.translateToPx(range.end - range.start);
} else {
span.style.left = this.translateToPx(range.start);
span.style.width = this.translateToPx(range.end - range.start);
}
},
updateStyles: function() {
this.handles.each( function(h){ Element.removeClassName(h, 'selected') });
Element.addClassName(this.activeHandle, 'selected');
},
startDrag: function(event) {
if(Event.isLeftClick(event)) {
if(!this.disabled){
this.active = true;
var handle = Event.element(event);
var pointer = [Event.pointerX(event), Event.pointerY(event)];
if(handle==this.track) {
var offsets = Position.cumulativeOffset(this.track);
this.event = event;
this.setValue(this.translateToValue(
this.isVertical() ? pointer[1]-offsets[1] : pointer[0]-offsets[0]
));
} else {
// find the handle (prevents issues with Safari)
while((this.handles.indexOf(handle) == -1) && handle.parentNode)
handle = handle.parentNode;
this.activeHandle = handle;
this.activeHandleIdx = this.handles.indexOf(this.activeHandle);
this.updateStyles();
var offsets = Position.cumulativeOffset(this.activeHandle);
this.offsetX = (pointer[0] - offsets[0]);
this.offsetY = (pointer[1] - offsets[1]);
}
}
Event.stop(event);
}
},
update: function(event) {
if(this.active) {
if(!this.dragging) this.dragging = true;
this.draw(event);
// fix AppleWebKit rendering
if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
Event.stop(event);
}
},
draw: function(event) {
var pointer = [Event.pointerX(event), Event.pointerY(event)];
var offsets = Position.cumulativeOffset(this.track);
pointer[0] -= this.offsetX + offsets[0];
pointer[1] -= this.offsetY + offsets[1];
this.event = event;
this.setValue(this.translateToValue( this.isVertical() ? pointer[1] : pointer[0] ));
if(this.initialized && this.options.onSlide) this.options.onSlide(this.values.length>1 ? this.values : this.value, this);
},
endDrag: function(event) {
if(this.active && this.dragging) {
this.finishDrag(event, true);
Event.stop(event);
}
this.active = false;
this.dragging = false;
},
finishDrag: function(event, success) {
this.active = false;
this.dragging = false;
this.updateFinished();
},
updateFinished: function() {
if(this.initialized && this.options.onChange)
this.options.onChange(this.values.length>1 ? this.values : this.value, this);
this.event = null;
}
}

View File

@ -1,363 +0,0 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005 Jon Tirsen (http://www.tirsen.com)
// (c) 2005 Michael Schuerig (http://www.schuerig.de/michael/)
//
// See scriptaculous.js for full license.
// experimental, Firefox-only
Event.simulateMouse = function(element, eventName) {
var options = Object.extend({
pointerX: 0,
pointerY: 0,
buttons: 0
}, arguments[2] || {});
var oEvent = document.createEvent("MouseEvents");
oEvent.initMouseEvent(eventName, true, true, document.defaultView,
options.buttons, options.pointerX, options.pointerY, options.pointerX, options.pointerY,
false, false, false, false, 0, $(element));
if(this.mark) Element.remove(this.mark);
this.mark = document.createElement('div');
this.mark.appendChild(document.createTextNode(" "));
document.body.appendChild(this.mark);
this.mark.style.position = 'absolute';
this.mark.style.top = options.pointerY + "px";
this.mark.style.left = options.pointerX + "px";
this.mark.style.width = "5px";
this.mark.style.height = "5px;";
this.mark.style.borderTop = "1px solid red;"
this.mark.style.borderLeft = "1px solid red;"
if(this.step)
alert('['+new Date().getTime().toString()+'] '+eventName+'/'+Test.Unit.inspect(options));
$(element).dispatchEvent(oEvent);
};
// Note: Due to a fix in Firefox 1.0.5/6 that probably fixed "too much", this doesn't work in 1.0.6 or DP2.
// You need to downgrade to 1.0.4 for now to get this working
// See https://bugzilla.mozilla.org/show_bug.cgi?id=289940 for the fix that fixed too much
Event.simulateKey = function(element, eventName) {
var options = Object.extend({
ctrlKey: false,
altKey: false,
shiftKey: false,
metaKey: false,
keyCode: 0,
charCode: 0
}, arguments[2] || {});
var oEvent = document.createEvent("KeyEvents");
oEvent.initKeyEvent(eventName, true, true, window,
options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
options.keyCode, options.charCode );
$(element).dispatchEvent(oEvent);
};
Event.simulateKeys = function(element, command) {
for(var i=0; i<command.length; i++) {
Event.simulateKey(element,'keypress',{charCode:command.charCodeAt(i)});
}
};
var Test = {}
Test.Unit = {};
// security exception workaround
Test.Unit.inspect = function(obj) {
var info = [];
if(typeof obj=="string" ||
typeof obj=="number") {
return obj;
} else {
for(property in obj)
if(typeof obj[property]!="function")
info.push(property + ' => ' +
(typeof obj[property] == "string" ?
'"' + obj[property] + '"' :
obj[property]));
}
return ("'" + obj + "' #" + typeof obj +
": {" + info.join(", ") + "}");
}
Test.Unit.Logger = Class.create();
Test.Unit.Logger.prototype = {
initialize: function(log) {
this.log = $(log);
if (this.log) {
this._createLogTable();
}
},
start: function(testName) {
if (!this.log) return;
this.testName = testName;
this.lastLogLine = document.createElement('tr');
this.statusCell = document.createElement('td');
this.nameCell = document.createElement('td');
this.nameCell.appendChild(document.createTextNode(testName));
this.messageCell = document.createElement('td');
this.lastLogLine.appendChild(this.statusCell);
this.lastLogLine.appendChild(this.nameCell);
this.lastLogLine.appendChild(this.messageCell);
this.loglines.appendChild(this.lastLogLine);
},
finish: function(status, summary) {
if (!this.log) return;
this.lastLogLine.className = status;
this.statusCell.innerHTML = status;
this.messageCell.innerHTML = this._toHTML(summary);
},
message: function(message) {
if (!this.log) return;
this.messageCell.innerHTML = this._toHTML(message);
},
summary: function(summary) {
if (!this.log) return;
this.logsummary.innerHTML = this._toHTML(summary);
},
_createLogTable: function() {
this.log.innerHTML =
'<div id="logsummary"></div>' +
'<table id="logtable">' +
'<thead><tr><th>Status</th><th>Test</th><th>Message</th></tr></thead>' +
'<tbody id="loglines"></tbody>' +
'</table>';
this.logsummary = $('logsummary')
this.loglines = $('loglines');
},
_toHTML: function(txt) {
return txt.escapeHTML().replace(/\n/g,"<br/>");
}
}
Test.Unit.Runner = Class.create();
Test.Unit.Runner.prototype = {
initialize: function(testcases) {
this.options = Object.extend({
testLog: 'testlog'
}, arguments[1] || {});
this.options.resultsURL = this.parseResultsURLQueryParameter();
if (this.options.testLog) {
this.options.testLog = $(this.options.testLog) || null;
}
if(this.options.tests) {
this.tests = [];
for(var i = 0; i < this.options.tests.length; i++) {
if(/^test/.test(this.options.tests[i])) {
this.tests.push(new Test.Unit.Testcase(this.options.tests[i], testcases[this.options.tests[i]], testcases["setup"], testcases["teardown"]));
}
}
} else {
if (this.options.test) {
this.tests = [new Test.Unit.Testcase(this.options.test, testcases[this.options.test], testcases["setup"], testcases["teardown"])];
} else {
this.tests = [];
for(var testcase in testcases) {
if(/^test/.test(testcase)) {
this.tests.push(new Test.Unit.Testcase(testcase, testcases[testcase], testcases["setup"], testcases["teardown"]));
}
}
}
}
this.currentTest = 0;
this.logger = new Test.Unit.Logger(this.options.testLog);
setTimeout(this.runTests.bind(this), 1000);
},
parseResultsURLQueryParameter: function() {
return window.location.search.parseQuery()["resultsURL"];
},
// Returns:
// "ERROR" if there was an error,
// "FAILURE" if there was a failure, or
// "SUCCESS" if there was neither
getResult: function() {
var hasFailure = false;
for(var i=0;i<this.tests.length;i++) {
if (this.tests[i].errors > 0) {
return "ERROR";
}
if (this.tests[i].failures > 0) {
hasFailure = true;
}
}
if (hasFailure) {
return "FAILURE";
} else {
return "SUCCESS";
}
},
postResults: function() {
if (this.options.resultsURL) {
new Ajax.Request(this.options.resultsURL,
{ method: 'get', parameters: 'result=' + this.getResult(), asynchronous: false });
}
},
runTests: function() {
var test = this.tests[this.currentTest];
if (!test) {
// finished!
this.postResults();
this.logger.summary(this.summary());
return;
}
if(!test.isWaiting) {
this.logger.start(test.name);
}
test.run();
if(test.isWaiting) {
this.logger.message("Waiting for " + test.timeToWait + "ms");
setTimeout(this.runTests.bind(this), test.timeToWait || 1000);
} else {
this.logger.finish(test.status(), test.summary());
this.currentTest++;
// tail recursive, hopefully the browser will skip the stackframe
this.runTests();
}
},
summary: function() {
var assertions = 0;
var failures = 0;
var errors = 0;
var messages = [];
for(var i=0;i<this.tests.length;i++) {
assertions += this.tests[i].assertions;
failures += this.tests[i].failures;
errors += this.tests[i].errors;
}
return (
this.tests.length + " tests, " +
assertions + " assertions, " +
failures + " failures, " +
errors + " errors");
}
}
Test.Unit.Assertions = Class.create();
Test.Unit.Assertions.prototype = {
initialize: function() {
this.assertions = 0;
this.failures = 0;
this.errors = 0;
this.messages = [];
},
summary: function() {
return (
this.assertions + " assertions, " +
this.failures + " failures, " +
this.errors + " errors" + "\n" +
this.messages.join("\n"));
},
pass: function() {
this.assertions++;
},
fail: function(message) {
this.failures++;
this.messages.push("Failure: " + message);
},
error: function(error) {
this.errors++;
this.messages.push(error.name + ": "+ error.message + "(" + Test.Unit.inspect(error) +")");
},
status: function() {
if (this.failures > 0) return 'failed';
if (this.errors > 0) return 'error';
return 'passed';
},
assert: function(expression) {
var message = arguments[1] || 'assert: got "' + Test.Unit.inspect(expression) + '"';
try { expression ? this.pass() :
this.fail(message); }
catch(e) { this.error(e); }
},
assertEqual: function(expected, actual) {
var message = arguments[2] || "assertEqual";
try { (expected == actual) ? this.pass() :
this.fail(message + ': expected "' + Test.Unit.inspect(expected) +
'", actual "' + Test.Unit.inspect(actual) + '"'); }
catch(e) { this.error(e); }
},
assertNotEqual: function(expected, actual) {
var message = arguments[2] || "assertNotEqual";
try { (expected != actual) ? this.pass() :
this.fail(message + ': got "' + Test.Unit.inspect(actual) + '"'); }
catch(e) { this.error(e); }
},
assertNull: function(obj) {
var message = arguments[1] || 'assertNull'
try { (obj==null) ? this.pass() :
this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); }
catch(e) { this.error(e); }
},
assertHidden: function(element) {
var message = arguments[1] || 'assertHidden';
this.assertEqual("none", element.style.display, message);
},
assertNotNull: function(object) {
var message = arguments[1] || 'assertNotNull';
this.assert(object != null, message);
},
assertInstanceOf: function(expected, actual) {
var message = arguments[2] || 'assertInstanceOf';
try {
(actual instanceof expected) ? this.pass() :
this.fail(message + ": object was not an instance of the expected type"); }
catch(e) { this.error(e); }
},
assertNotInstanceOf: function(expected, actual) {
var message = arguments[2] || 'assertNotInstanceOf';
try {
!(actual instanceof expected) ? this.pass() :
this.fail(message + ": object was an instance of the not expected type"); }
catch(e) { this.error(e); }
},
_isVisible: function(element) {
element = $(element);
if(!element.parentNode) return true;
this.assertNotNull(element);
if(element.style && Element.getStyle(element, 'display') == 'none')
return false;
return this._isVisible(element.parentNode);
},
assertNotVisible: function(element) {
this.assert(!this._isVisible(element), Test.Unit.inspect(element) + " was not hidden and didn't have a hidden parent either. " + ("" || arguments[1]));
},
assertVisible: function(element) {
this.assert(this._isVisible(element), Test.Unit.inspect(element) + " was not visible. " + ("" || arguments[1]));
}
}
Test.Unit.Testcase = Class.create();
Object.extend(Object.extend(Test.Unit.Testcase.prototype, Test.Unit.Assertions.prototype), {
initialize: function(name, test, setup, teardown) {
Test.Unit.Assertions.prototype.initialize.bind(this)();
this.name = name;
this.test = test || function() {};
this.setup = setup || function() {};
this.teardown = teardown || function() {};
this.isWaiting = false;
this.timeToWait = 1000;
},
wait: function(time, nextPart) {
this.isWaiting = true;
this.test = nextPart;
this.timeToWait = time;
},
run: function() {
try {
try {
if (!this.isWaiting) this.setup.bind(this)();
this.isWaiting = false;
this.test.bind(this)();
} finally {
if(!this.isWaiting) {
this.teardown.bind(this)();
}
}
}
catch(e) { this.error(e); }
}
});