2012-01-25 05:31:27 +01:00
|
|
|
<?php
|
2016-06-15 06:03:16 +02:00
|
|
|
|
2016-08-19 00:51:35 +02:00
|
|
|
namespace SilverStripe\Forms\GridField;
|
|
|
|
|
2016-09-09 08:43:05 +02:00
|
|
|
use SilverStripe\Control\HTTPRequest;
|
2017-01-17 11:03:07 +01:00
|
|
|
use SilverStripe\Control\HTTPResponse;
|
2016-08-19 00:51:35 +02:00
|
|
|
use SilverStripe\Core\Config\Config;
|
|
|
|
use SilverStripe\Core\Convert;
|
|
|
|
use SilverStripe\Control\Controller;
|
|
|
|
use SilverStripe\Forms\FieldList;
|
|
|
|
use SilverStripe\Forms\TextField;
|
2016-06-15 06:03:16 +02:00
|
|
|
use SilverStripe\ORM\SS_List;
|
|
|
|
use SilverStripe\ORM\DataObject;
|
|
|
|
use SilverStripe\ORM\DataList;
|
2016-08-19 00:51:35 +02:00
|
|
|
use SilverStripe\View\ArrayData;
|
|
|
|
use SilverStripe\View\SSViewer;
|
|
|
|
use LogicException;
|
|
|
|
|
2012-01-25 05:31:27 +01:00
|
|
|
/**
|
2014-08-15 08:53:05 +02: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
|
2013-05-20 12:18:07 +02:00
|
|
|
* {@link GridField} constructor.
|
|
|
|
*
|
2014-08-15 08:53:05 +02:00
|
|
|
* Objects can be searched through an input field (partially matching one or
|
2013-05-20 12:18:07 +02:00
|
|
|
* more fields).
|
|
|
|
*
|
2012-03-01 14:43:42 +01:00
|
|
|
* Selecting from the results will add the object to the relation.
|
2013-05-20 12:18:07 +02:00
|
|
|
*
|
2014-08-15 08:53:05 +02:00
|
|
|
* Often used alongside {@link GridFieldDeleteAction} for detaching existing
|
2013-05-20 12:18:07 +02:00
|
|
|
* records from a relationship.
|
|
|
|
*
|
2014-08-15 08:53:05 +02:00
|
|
|
* For easier setup, have a look at a sample configuration in
|
2013-05-20 12:18:07 +02:00
|
|
|
* {@link GridFieldConfig_RelationEditor}.
|
2012-01-25 05:31:27 +01:00
|
|
|
*/
|
2016-11-29 00:31:16 +01:00
|
|
|
class GridFieldAddExistingAutocompleter implements GridField_HTMLProvider, GridField_ActionProvider, GridField_DataManipulator, GridField_URLHandler
|
|
|
|
{
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The HTML fragment to write this component into
|
|
|
|
*/
|
|
|
|
protected $targetFragment;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var SS_List
|
|
|
|
*/
|
|
|
|
protected $searchList;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Define column names which should be included in the search.
|
|
|
|
* By default, they're searched with a {@link StartsWithFilter}.
|
|
|
|
* To define custom filters, use the same notation as {@link DataList->filter()},
|
|
|
|
* e.g. "Name:EndsWith".
|
|
|
|
*
|
|
|
|
* If multiple fields are provided, the filtering is performed non-exclusive.
|
|
|
|
* If no fields are provided, tries to auto-detect fields from
|
|
|
|
* {@link DataObject->searchableFields()}.
|
|
|
|
*
|
|
|
|
* The fields support "dot-notation" for relationships, e.g.
|
|
|
|
* a entry called "Team.Name" will search through the names of
|
|
|
|
* a "Team" relationship.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* array(
|
|
|
|
* 'Name',
|
|
|
|
* 'Email:StartsWith',
|
|
|
|
* 'Team.Name'
|
|
|
|
* )
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
protected $searchFields = array();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string SSViewer template to render the results presentation
|
|
|
|
*/
|
|
|
|
protected $resultsFormat = '$Title';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string Text shown on the search field, instructing what to search for.
|
|
|
|
*/
|
|
|
|
protected $placeholderText;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var int
|
|
|
|
*/
|
|
|
|
protected $resultsLimit = 20;
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param string $targetFragment
|
|
|
|
* @param array $searchFields Which fields on the object in the list should be searched
|
|
|
|
*/
|
|
|
|
public function __construct($targetFragment = 'before', $searchFields = null)
|
|
|
|
{
|
|
|
|
$this->targetFragment = $targetFragment;
|
|
|
|
$this->searchFields = (array)$searchFields;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param GridField $gridField
|
|
|
|
* @return string[] - HTML
|
|
|
|
*/
|
|
|
|
public function getHTMLFragments($gridField)
|
|
|
|
{
|
|
|
|
$dataClass = $gridField->getModelClass();
|
|
|
|
|
|
|
|
$forTemplate = new ArrayData(array());
|
|
|
|
$forTemplate->Fields = new FieldList();
|
|
|
|
|
2017-04-20 03:15:24 +02:00
|
|
|
$searchField = new TextField('gridfield_relationsearch', _t('SilverStripe\\Forms\\GridField\\GridField.RelationSearch', "Relation search"));
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
$searchField->setAttribute('data-search-url', Controller::join_links($gridField->Link('search')));
|
|
|
|
$searchField->setAttribute('placeholder', $this->getPlaceholderText($dataClass));
|
|
|
|
$searchField->addExtraClass('relation-search no-change-track action_gridfield_relationsearch');
|
|
|
|
|
|
|
|
$findAction = new GridField_FormAction(
|
|
|
|
$gridField,
|
|
|
|
'gridfield_relationfind',
|
2017-04-20 03:15:24 +02:00
|
|
|
_t('SilverStripe\\Forms\\GridField\\GridField.Find', "Find"),
|
2016-11-29 00:31:16 +01:00
|
|
|
'find',
|
|
|
|
'find'
|
|
|
|
);
|
|
|
|
$findAction->setAttribute('data-icon', 'relationfind');
|
|
|
|
$findAction->addExtraClass('action_gridfield_relationfind');
|
|
|
|
|
|
|
|
$addAction = new GridField_FormAction(
|
|
|
|
$gridField,
|
|
|
|
'gridfield_relationadd',
|
2017-04-20 03:15:24 +02:00
|
|
|
_t('SilverStripe\\Forms\\GridField\\GridField.LinkExisting', "Link Existing"),
|
2016-11-29 00:31:16 +01:00
|
|
|
'addto',
|
|
|
|
'addto'
|
|
|
|
);
|
|
|
|
$addAction->setAttribute('data-icon', 'chain--plus');
|
2017-08-10 06:35:09 +02:00
|
|
|
$addAction->addExtraClass('btn btn-outline-secondary font-icon-link action_gridfield_relationadd');
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
// If an object is not found, disable the action
|
|
|
|
if (!is_int($gridField->State->GridFieldAddRelation(null))) {
|
|
|
|
$addAction->setReadonly(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
$forTemplate->Fields->push($searchField);
|
|
|
|
$forTemplate->Fields->push($findAction);
|
|
|
|
$forTemplate->Fields->push($addAction);
|
|
|
|
if ($form = $gridField->getForm()) {
|
|
|
|
$forTemplate->Fields->setForm($form);
|
|
|
|
}
|
|
|
|
|
|
|
|
$template = SSViewer::get_templates_by_class($this, '', __CLASS__);
|
|
|
|
return array(
|
|
|
|
$this->targetFragment => $forTemplate->renderWith($template)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param GridField $gridField
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getActions($gridField)
|
|
|
|
{
|
|
|
|
return array('addto', 'find');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Manipulate the state to add a new relation
|
|
|
|
*
|
|
|
|
* @param GridField $gridField
|
|
|
|
* @param string $actionName Action identifier, see {@link getActions()}.
|
|
|
|
* @param array $arguments Arguments relevant for this
|
|
|
|
* @param array $data All form data
|
|
|
|
*/
|
|
|
|
public function handleAction(GridField $gridField, $actionName, $arguments, $data)
|
|
|
|
{
|
|
|
|
switch ($actionName) {
|
|
|
|
case 'addto':
|
|
|
|
if (isset($data['relationID']) && $data['relationID']) {
|
|
|
|
$gridField->State->GridFieldAddRelation = $data['relationID'];
|
|
|
|
}
|
|
|
|
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)
|
|
|
|
{
|
|
|
|
$objectID = $gridField->State->GridFieldAddRelation(null);
|
|
|
|
if (empty($objectID)) {
|
|
|
|
return $dataList;
|
|
|
|
}
|
|
|
|
$object = DataObject::get_by_id($gridField->getModelClass(), $objectID);
|
|
|
|
if ($object) {
|
|
|
|
$dataList->add($object);
|
|
|
|
}
|
|
|
|
$gridField->State->GridFieldAddRelation = null;
|
|
|
|
return $dataList;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param GridField $gridField
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getURLHandlers($gridField)
|
|
|
|
{
|
|
|
|
return array(
|
|
|
|
'search' => 'doSearch',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a json array of a search results that can be used by for example Jquery.ui.autosuggestion
|
|
|
|
*
|
|
|
|
* @param GridField $gridField
|
|
|
|
* @param HTTPRequest $request
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function doSearch($gridField, $request)
|
|
|
|
{
|
|
|
|
$dataClass = $gridField->getModelClass();
|
|
|
|
$allList = $this->searchList ? $this->searchList : DataList::create($dataClass);
|
|
|
|
|
|
|
|
$searchFields = ($this->getSearchFields())
|
|
|
|
? $this->getSearchFields()
|
|
|
|
: $this->scaffoldSearchFields($dataClass);
|
|
|
|
if (!$searchFields) {
|
|
|
|
throw new LogicException(
|
|
|
|
sprintf(
|
|
|
|
'GridFieldAddExistingAutocompleter: No searchable fields could be found for class "%s"',
|
|
|
|
$dataClass
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$params = array();
|
|
|
|
foreach ($searchFields as $searchField) {
|
|
|
|
$name = (strpos($searchField, ':') !== false) ? $searchField : "$searchField:StartsWith";
|
|
|
|
$params[$name] = $request->getVar('gridfield_relationsearch');
|
|
|
|
}
|
|
|
|
$results = $allList
|
|
|
|
->subtract($gridField->getList())
|
|
|
|
->filterAny($params)
|
|
|
|
->sort(strtok($searchFields[0], ':'), 'ASC')
|
|
|
|
->limit($this->getResultsLimit());
|
|
|
|
|
|
|
|
$json = array();
|
|
|
|
Config::nest();
|
|
|
|
SSViewer::config()->update('source_file_comments', false);
|
|
|
|
$viewer = SSViewer::fromString($this->resultsFormat);
|
|
|
|
foreach ($results as $result) {
|
2017-10-20 03:43:19 +02:00
|
|
|
$title = Convert::html2raw($viewer->process($result));
|
2016-11-29 00:31:16 +01:00
|
|
|
$json[] = array(
|
|
|
|
'label' => $title,
|
|
|
|
'value' => $title,
|
|
|
|
'id' => $result->ID,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
Config::unnest();
|
2018-10-28 22:06:04 +01:00
|
|
|
$response = new HTTPResponse(json_encode($json));
|
2018-08-15 13:10:39 +02:00
|
|
|
$response->addHeader('Content-Type', 'application/json');
|
2017-01-17 11:03:07 +01:00
|
|
|
return $response;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $format
|
|
|
|
*
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setResultsFormat($format)
|
|
|
|
{
|
|
|
|
$this->resultsFormat = $format;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getResultsFormat()
|
|
|
|
{
|
|
|
|
return $this->resultsFormat;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the base list instance which will be used for the autocomplete
|
|
|
|
* search.
|
|
|
|
*
|
|
|
|
* @param SS_List $list
|
|
|
|
*/
|
|
|
|
public function setSearchList(SS_List $list)
|
|
|
|
{
|
|
|
|
$this->searchList = $list;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param array $fields
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setSearchFields($fields)
|
|
|
|
{
|
|
|
|
$this->searchFields = $fields;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getSearchFields()
|
|
|
|
{
|
|
|
|
return $this->searchFields;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Detect searchable fields and searchable relations.
|
|
|
|
* Falls back to {@link DataObject->summaryFields()} if
|
|
|
|
* no custom search fields are defined.
|
|
|
|
*
|
|
|
|
* @param string $dataClass The class name
|
|
|
|
* @return array|null names of the searchable fields
|
|
|
|
*/
|
|
|
|
public function scaffoldSearchFields($dataClass)
|
|
|
|
{
|
2017-02-22 04:14:53 +01:00
|
|
|
$obj = DataObject::singleton($dataClass);
|
2016-11-29 00:31:16 +01:00
|
|
|
$fields = null;
|
|
|
|
if ($fieldSpecs = $obj->searchableFields()) {
|
2017-08-22 23:42:10 +02:00
|
|
|
$customSearchableFields = $obj->config()->get('searchable_fields');
|
2016-11-29 00:31:16 +01:00
|
|
|
foreach ($fieldSpecs as $name => $spec) {
|
|
|
|
if (is_array($spec) && array_key_exists('filter', $spec)) {
|
|
|
|
// The searchableFields() spec defaults to PartialMatch,
|
|
|
|
// so we need to check the original setting.
|
|
|
|
// If the field is defined $searchable_fields = array('MyField'),
|
|
|
|
// then default to StartsWith filter, which makes more sense in this context.
|
|
|
|
if (!$customSearchableFields || array_search($name, $customSearchableFields)) {
|
|
|
|
$filter = 'StartsWith';
|
|
|
|
} else {
|
|
|
|
$filter = preg_replace('/Filter$/', '', $spec['filter']);
|
|
|
|
}
|
|
|
|
$fields[] = "{$name}:{$filter}";
|
|
|
|
} else {
|
|
|
|
$fields[] = $name;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (is_null($fields)) {
|
|
|
|
if ($obj->hasDatabaseField('Title')) {
|
|
|
|
$fields = array('Title');
|
|
|
|
} elseif ($obj->hasDatabaseField('Name')) {
|
|
|
|
$fields = array('Name');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $fields;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $dataClass The class of the object being searched for
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getPlaceholderText($dataClass)
|
|
|
|
{
|
|
|
|
$searchFields = ($this->getSearchFields())
|
|
|
|
? $this->getSearchFields()
|
|
|
|
: $this->scaffoldSearchFields($dataClass);
|
|
|
|
|
|
|
|
if ($this->placeholderText) {
|
|
|
|
return $this->placeholderText;
|
|
|
|
} else {
|
|
|
|
$labels = array();
|
|
|
|
if ($searchFields) {
|
|
|
|
foreach ($searchFields as $searchField) {
|
|
|
|
$searchField = explode(':', $searchField);
|
|
|
|
$label = singleton($dataClass)->fieldLabel($searchField[0]);
|
|
|
|
if ($label) {
|
|
|
|
$labels[] = $label;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ($labels) {
|
|
|
|
return _t(
|
2017-04-20 03:15:24 +02:00
|
|
|
'SilverStripe\\Forms\\GridField\\GridField.PlaceHolderWithLabels',
|
2016-11-29 00:31:16 +01:00
|
|
|
'Find {type} by {name}',
|
|
|
|
array('type' => singleton($dataClass)->i18n_plural_name(), 'name' => implode(', ', $labels))
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return _t(
|
2017-04-20 03:15:24 +02:00
|
|
|
'SilverStripe\\Forms\\GridField\\GridField.PlaceHolder',
|
2016-11-29 00:31:16 +01:00
|
|
|
'Find {type}',
|
|
|
|
array('type' => singleton($dataClass)->i18n_plural_name())
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $text
|
|
|
|
*
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setPlaceholderText($text)
|
|
|
|
{
|
|
|
|
$this->placeholderText = $text;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the maximum number of autocomplete results to display.
|
|
|
|
*
|
|
|
|
* @return int
|
|
|
|
*/
|
|
|
|
public function getResultsLimit()
|
|
|
|
{
|
|
|
|
return $this->resultsLimit;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param int $limit
|
|
|
|
*
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setResultsLimit($limit)
|
|
|
|
{
|
|
|
|
$this->resultsLimit = $limit;
|
|
|
|
return $this;
|
|
|
|
}
|
2012-03-24 04:04:52 +01:00
|
|
|
}
|