2012-01-25 05:31:27 +01:00
|
|
|
<?php
|
|
|
|
/**
|
2012-03-05 12:27:25 +01:00
|
|
|
* This class is is responsible for adding objects to another object's has_many and many_many relation,
|
|
|
|
* as defined by the {@link RelationList} passed to the GridField constructor.
|
2012-03-01 14:43:42 +01:00
|
|
|
* Objects can be searched through an input field (partially matching one or more fields).
|
|
|
|
* Selecting from the results will add the object to the relation.
|
2012-03-09 00:54:02 +01:00
|
|
|
* Often used alongside {@link GridFieldRemoveButton} for detaching existing records from a relatinship.
|
2012-03-05 12:27:25 +01:00
|
|
|
* For easier setup, have a look at a sample configuration in {@link GridFieldConfig_RelationEditor}.
|
2012-01-25 05:31:27 +01:00
|
|
|
*/
|
2012-03-09 00:54:02 +01:00
|
|
|
class GridFieldAddExistingAutocompleter implements GridField_HTMLProvider, GridField_ActionProvider, GridField_DataManipulator, GridField_URLHandler {
|
2012-01-25 05:31:27 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Which template to use for rendering
|
|
|
|
*
|
|
|
|
* @var string $itemClass
|
|
|
|
*/
|
2012-03-09 00:54:02 +01:00
|
|
|
protected $itemClass = 'GridFieldAddExistingAutocompleter';
|
2012-03-09 04:50:05 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The HTML fragment to write this component into
|
|
|
|
*/
|
|
|
|
protected $targetFragment;
|
|
|
|
|
2012-01-25 05:31:27 +01:00
|
|
|
/**
|
2012-03-01 14:46:43 +01:00
|
|
|
* Which columns that should be used for doing a "StartsWith" search.
|
|
|
|
* If multiple fields are provided, the filtering is performed non-exclusive.
|
2012-03-05 12:27:00 +01:00
|
|
|
* If no fields are provided, tries to auto-detect a "Title" or "Name" field,
|
|
|
|
* and falls back to the first textual field defined on the object.
|
2012-01-25 05:31:27 +01:00
|
|
|
*
|
2012-03-01 14:46:43 +01:00
|
|
|
* @var Array
|
2012-01-25 05:31:27 +01:00
|
|
|
*/
|
2012-03-01 14:46:43 +01:00
|
|
|
protected $searchFields = array();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string SSViewer template to render the results presentation
|
|
|
|
*/
|
|
|
|
protected $resultsFormat = '$Title';
|
2012-03-01 16:48:44 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var String Text shown on the search field, instructing what to search for.
|
|
|
|
*/
|
|
|
|
protected $placeholderText;
|
2012-07-17 11:56:18 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var int
|
|
|
|
*/
|
|
|
|
protected $resultsLimit = 20;
|
|
|
|
|
2012-01-25 05:31:27 +01:00
|
|
|
/**
|
|
|
|
*
|
2012-03-01 14:46:43 +01:00
|
|
|
* @param array $searchFields Which fields on the object in the list should be searched
|
2012-01-25 05:31:27 +01:00
|
|
|
*/
|
2012-03-09 04:50:05 +01:00
|
|
|
public function __construct($targetFragment = 'before', $searchFields = null) {
|
|
|
|
$this->targetFragment = $targetFragment;
|
2012-03-01 14:46:43 +01:00
|
|
|
$this->searchFields = (array)$searchFields;
|
2012-01-25 05:31:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param GridField $gridField
|
|
|
|
* @return string - HTML
|
|
|
|
*/
|
|
|
|
public function getHTMLFragments($gridField) {
|
|
|
|
$searchState = $gridField->State->GridFieldSearchRelation;
|
2012-03-05 12:27:00 +01:00
|
|
|
$dataClass = $gridField->getList()->dataClass();
|
2012-01-25 05:31:27 +01:00
|
|
|
|
|
|
|
$forTemplate = new ArrayData(array());
|
|
|
|
$forTemplate->Fields = new ArrayList();
|
2012-03-05 12:27:00 +01:00
|
|
|
|
|
|
|
$searchFields = ($this->getSearchFields()) ? $this->getSearchFields() : $this->scaffoldSearchFields($dataClass);
|
2012-01-25 05:31:27 +01:00
|
|
|
|
2012-03-05 12:27:00 +01:00
|
|
|
$value = $this->findSingleEntry($gridField, $searchFields, $searchState, $dataClass);
|
2012-01-24 18:15:49 +01:00
|
|
|
$searchField = new TextField('gridfield_relationsearch', _t('GridField.RelationSearch', "Relation search"), $value);
|
2012-01-25 05:31:27 +01:00
|
|
|
// Apparently the data-* needs to be double qouted for the jQuery.meta data plugin
|
|
|
|
$searchField->setAttribute('data-search-url', '\''.Controller::join_links($gridField->Link('search').'\''));
|
2012-03-05 12:27:00 +01:00
|
|
|
$searchField->setAttribute('placeholder', $this->getPlaceholderText($dataClass));
|
2012-01-24 18:37:00 +01:00
|
|
|
$searchField->addExtraClass('relation-search');
|
2012-01-25 05:31:27 +01:00
|
|
|
|
2012-03-06 12:51:57 +01:00
|
|
|
$findAction = new GridField_FormAction($gridField, 'gridfield_relationfind', _t('GridField.Find', "Find"), 'find', 'find');
|
2012-03-06 16:58:13 +01:00
|
|
|
$findAction->setAttribute('data-icon', 'relationfind');
|
2012-03-09 04:50:05 +01:00
|
|
|
$addAction = new GridField_FormAction($gridField, 'gridfield_relationadd', _t('GridField.LinkExisting', "Link Existing"), 'addto', 'addto');
|
2012-03-08 02:55:23 +01:00
|
|
|
$addAction->setAttribute('data-icon', 'chain--plus');
|
2012-01-25 05:31:27 +01:00
|
|
|
|
|
|
|
// If an object is not found, disable the action
|
|
|
|
if(!is_int($gridField->State->GridFieldAddRelation)) {
|
|
|
|
$addAction->setReadonly(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
$forTemplate->Fields->push($searchField);
|
|
|
|
$forTemplate->Fields->push($findAction);
|
|
|
|
$forTemplate->Fields->push($addAction);
|
2012-03-09 04:50:05 +01:00
|
|
|
|
|
|
|
return array(
|
|
|
|
$this->targetFragment => $forTemplate->renderWith($this->itemClass)
|
|
|
|
);
|
2012-01-25 05:31:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param GridField $gridField
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getActions($gridField) {
|
|
|
|
return array('addto', 'find');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Manipulate the state to either add a new relation, or doing a small search
|
|
|
|
*
|
|
|
|
* @param GridField $gridField
|
|
|
|
* @param string $actionName
|
|
|
|
* @param string $arguments
|
|
|
|
* @param string $data
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function handleAction(GridField $gridField, $actionName, $arguments, $data) {
|
|
|
|
switch($actionName) {
|
|
|
|
case 'addto':
|
|
|
|
if(isset($data['relationID']) && $data['relationID']){
|
|
|
|
$gridField->State->GridFieldAddRelation = $data['relationID'];
|
|
|
|
}
|
|
|
|
$gridField->State->GridFieldSearchRelation = '';
|
|
|
|
break;
|
|
|
|
case 'find' && isset($data['autosuggest_search']):
|
|
|
|
$gridField->State->GridFieldSearchRelation = $data['autosuggest_search'];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If an object ID is set, add the object to the list
|
|
|
|
*
|
|
|
|
* @param GridField $gridField
|
|
|
|
* @param SS_List $dataList
|
|
|
|
* @return SS_List
|
|
|
|
*/
|
|
|
|
public function getManipulatedData(GridField $gridField, SS_List $dataList) {
|
|
|
|
if(!$gridField->State->GridFieldAddRelation) {
|
|
|
|
return $dataList;
|
|
|
|
}
|
|
|
|
$objectID = Convert::raw2sql($gridField->State->GridFieldAddRelation);
|
|
|
|
if($objectID) {
|
|
|
|
$object = DataObject::get_by_id($dataList->dataclass(), $objectID);
|
|
|
|
if($object) {
|
|
|
|
$dataList->add($object);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$gridField->State->GridFieldAddRelation = null;
|
|
|
|
return $dataList;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param GridField $gridField
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getURLHandlers($gridField) {
|
|
|
|
return array(
|
|
|
|
'search/$ID' => 'doSearch',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a json array of a search results that can be used by for example Jquery.ui.autosuggestion
|
|
|
|
*
|
|
|
|
* @param GridField $gridField
|
|
|
|
* @param SS_HTTPRequest $request
|
|
|
|
*/
|
|
|
|
public function doSearch($gridField, $request) {
|
2012-03-05 12:27:00 +01:00
|
|
|
$dataClass = $gridField->getList()->dataClass();
|
|
|
|
$allList = DataList::create($dataClass);
|
2012-03-01 14:46:43 +01:00
|
|
|
$filters = array();
|
|
|
|
$stmts = array();
|
2012-03-05 12:27:00 +01:00
|
|
|
|
|
|
|
$searchFields = ($this->getSearchFields()) ? $this->getSearchFields() : $this->scaffoldSearchFields($dataClass);
|
|
|
|
if(!$searchFields) {
|
|
|
|
throw new LogicException(
|
2012-03-09 00:54:02 +01:00
|
|
|
sprintf('GridFieldAddExistingAutocompleter: No searchable fields could be found for class "%s"', $dataClass)
|
2012-03-05 12:27:00 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2012-03-01 14:46:43 +01:00
|
|
|
// TODO Replace with DataList->filterAny() once it correctly supports OR connectives
|
2012-03-05 12:27:00 +01:00
|
|
|
foreach($searchFields as $searchField) {
|
2012-03-01 14:46:43 +01:00
|
|
|
$stmts[] .= sprintf('"%s" LIKE \'%s%%\'', $searchField, $request->param('ID'));
|
|
|
|
}
|
|
|
|
$results = $allList->where(implode(' OR ', $stmts))->subtract($gridField->getList());
|
2012-06-15 06:08:54 +02:00
|
|
|
$results = $results->sort($searchFields[0], 'ASC');
|
2012-07-17 11:56:18 +02:00
|
|
|
$results = $results->limit($this->getResultsLimit());
|
2012-06-15 06:08:54 +02:00
|
|
|
|
2012-01-25 05:31:27 +01:00
|
|
|
$json = array();
|
|
|
|
foreach($results as $result) {
|
2012-03-01 14:46:43 +01:00
|
|
|
$json[$result->ID] = SSViewer::fromString($this->resultsFormat)->process($result);
|
2012-01-25 05:31:27 +01:00
|
|
|
}
|
|
|
|
return Convert::array2json($json);
|
|
|
|
}
|
2012-03-01 14:46:43 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param String
|
|
|
|
*/
|
|
|
|
public function setResultsFormat($format) {
|
|
|
|
$this->resultsFormat = $format;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return String
|
|
|
|
*/
|
|
|
|
public function getResultsFormat() {
|
|
|
|
return $this->resultsFormat;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param Array
|
|
|
|
*/
|
|
|
|
public function setSearchFields($fields) {
|
|
|
|
$this->searchFields = $fields;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return Array
|
|
|
|
*/
|
|
|
|
public function getSearchFields() {
|
|
|
|
return $this->searchFields;
|
|
|
|
}
|
2012-03-01 16:48:44 +01:00
|
|
|
|
2012-03-05 12:27:00 +01:00
|
|
|
/**
|
|
|
|
* Detect searchable
|
|
|
|
*
|
|
|
|
* @param String
|
|
|
|
* @return Array
|
|
|
|
*/
|
|
|
|
protected function scaffoldSearchFields($dataClass) {
|
|
|
|
$obj = singleton($dataClass);
|
|
|
|
if($obj->hasDatabaseField('Title')) {
|
|
|
|
return array('Title');
|
|
|
|
} else if($obj->hasDatabaseField('Name')) {
|
|
|
|
return array('Name');
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-03-01 16:48:44 +01:00
|
|
|
/**
|
|
|
|
* @param String The class of the object being searched for
|
|
|
|
* @return String
|
|
|
|
*/
|
|
|
|
public function getPlaceholderText($dataClass) {
|
2012-03-05 12:27:00 +01:00
|
|
|
$searchFields = ($this->getSearchFields()) ? $this->getSearchFields() : $this->scaffoldSearchFields($dataClass);
|
|
|
|
|
2012-03-01 16:48:44 +01:00
|
|
|
if($this->placeholderText) {
|
|
|
|
return $this->placeholderText;
|
|
|
|
} else {
|
|
|
|
$labels = array();
|
2012-03-08 23:56:12 +01:00
|
|
|
if($searchFields) foreach($searchFields as $searchField) {
|
2012-03-01 16:48:44 +01:00
|
|
|
$label = singleton($dataClass)->fieldLabel($searchField);
|
|
|
|
if($label) $labels[] = $label;
|
|
|
|
}
|
|
|
|
if($labels) {
|
2012-05-01 21:44:54 +02:00
|
|
|
return _t(
|
|
|
|
'GridField.PlaceHolderWithLabels',
|
|
|
|
'Find {type} by {name}',
|
|
|
|
array('type' => singleton($dataClass)->plural_name(), 'name' => implode(', ', $labels))
|
2012-03-01 16:48:44 +01:00
|
|
|
);
|
|
|
|
} else {
|
2012-05-01 21:44:54 +02:00
|
|
|
return _t(
|
|
|
|
'GridField.PlaceHolder', 'Find {type}',
|
|
|
|
array('type' => singleton($dataClass)->plural_name())
|
2012-03-01 16:48:44 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param String
|
|
|
|
*/
|
|
|
|
public function setPlaceholderText($text) {
|
|
|
|
$this->placeholderText = $text;
|
|
|
|
}
|
2012-07-17 11:56:18 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the maximum number of autocomplete results to display.
|
|
|
|
*
|
|
|
|
* @return int
|
|
|
|
*/
|
|
|
|
public function getResultsLimit() {
|
|
|
|
return $this->resultsLimit;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param int $limit
|
|
|
|
*/
|
|
|
|
public function setResultsLimit($limit) {
|
|
|
|
$this->resultsLimit = $limit;
|
|
|
|
}
|
|
|
|
|
2012-01-25 05:31:27 +01:00
|
|
|
/**
|
|
|
|
* This will provide a StartsWith search that only returns a value if we are
|
|
|
|
* matching ONE object only. We wouldn't want to attach used any object to
|
|
|
|
* the list.
|
|
|
|
*
|
|
|
|
* @param GridField $gridField
|
|
|
|
* @param string $field
|
|
|
|
* @param string $searchTerm
|
|
|
|
* @param string $dataclass
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
protected function findSingleEntry($gridField, $field, $searchTerm, $dataclass) {
|
|
|
|
$fullList = DataList::create($dataclass);
|
|
|
|
$searchTerm = Convert::raw2sql($searchTerm);
|
|
|
|
if(!$searchTerm) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
$existingList = clone $gridField->getList();
|
|
|
|
$searchResults = $fullList->subtract($existingList->limit(0))->filter($field.':StartsWith', $searchTerm);
|
|
|
|
|
|
|
|
// If more than one, skip
|
|
|
|
if($searchResults->count() != 1) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
$gridField->State->GridFieldAddRelation = $searchResults->first()->ID;
|
|
|
|
return $searchResults->first()->$field;
|
|
|
|
}
|
2012-03-24 04:04:52 +01:00
|
|
|
}
|