1 <?php
2 3 4 5 6 7
8 class TreeDropdownField extends FormField {
9
10 public static $url_handlers = array (
11 '$Action!/$ID' => '$Action'
12 );
13
14 public static $allowed_actions = array (
15 'tree'
16 );
17
18 19 20
21 protected $sourceObject, $keyField, $labelField, $filterCallback, $searchCallback, $baseID = 0;
22
23 24 25
26 protected $searchIds = null, $searchExpanded = array();
27
28 29 30 31 32 33 34 35 36 37 38 39 40
41 public function __construct($name, $title = null, $sourceObject = 'Group', $keyField = 'ID', $labelField = 'Title', $showSearch = false) {
42 $this->sourceObject = $sourceObject;
43 $this->keyField = $keyField;
44 $this->labelField = $labelField;
45 $this->showSearch = $showSearch;
46
47 parent::__construct($name, $title);
48 }
49
50 51 52 53 54
55 public function setTreeBaseID($ID) {
56 $this->baseID = (int) $ID;
57 }
58
59 60 61 62 63
64 public function setFilterFunction($callback) {
65 if(!is_callable($callback, true)) {
66 throw new InvalidArgumentException('TreeDropdownField->setFilterCallback(): not passed a valid callback');
67 }
68
69 $this->filterCallback = $callback;
70 }
71
72 73 74 75 76
77 public function setSearchFunction($callback) {
78 if(!is_callable($callback, true)) {
79 throw new InvalidArgumentException('TreeDropdownField->setSearchFunction(): not passed a valid callback');
80 }
81
82 $this->searchCallback = $callback;
83 }
84
85 86 87
88 public function Field() {
89 Requirements::add_i18n_javascript(SAPPHIRE_DIR . '/javascript/lang');
90
91 Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/prototype/prototype.js');
92 Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/behaviour/behaviour.js');
93 Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/jquery/jquery.js');
94 Requirements::javascript(SAPPHIRE_DIR . '/javascript/jquery_improvements.js');
95 Requirements::javascript(SAPPHIRE_DIR . '/javascript/tree/tree.js');
96
97 Requirements::javascript(SAPPHIRE_DIR . '/javascript/LeftAndMain.js');
98 Requirements::javascript(SAPPHIRE_DIR . '/javascript/TreeSelectorField.js');
99
100 Requirements::css(SAPPHIRE_DIR . '/javascript/tree/tree.css');
101 Requirements::css(SAPPHIRE_DIR . '/css/TreeDropdownField.css');
102
103 if($this->Value() && $record = $this->objectForKey($this->Value())) {
104 $title = $record->{$this->labelField};
105 } else {
106 $title = $this->showSearch ? _t('DropdownField.CHOOSEORTYPE','Choose or type') : _t('DropdownField.CHOOSE', '(Choose)', PR_MEDIUM, 'start value of a dropdown');
107 }
108
109 $options = new DataObjectSet();
110 $attributes = new DataObjectSet();
111 $attributes->push(new ArrayData(array(
112 'Title' => 'id',
113 'Value' => $this->id()
114 )));
115 $attributes->push(new ArrayData(array(
116 'Title' => 'type',
117 'Value' => 'hidden'
118 )));
119 $attributes->push(new ArrayData(array(
120 'Title' => 'name',
121 'Value' => $this->name
122 )));
123 $attributes->push(new ArrayData(array(
124 'Title' => 'value',
125 'Value' => $this->value
126 )));
127 $options->push(new ArrayData(array(
128 'Tag' => 'input',
129 'Attributes' => $attributes
130 )));
131
132 $attributes = new DataObjectSet();
133 if ($this->showSearch) {
134 $tag = 'input';
135 $content = false;
136 $attributes->push(new ArrayData(array(
137 'Title' => 'class',
138 'Value' => 'items'
139 )));
140 $attributes->push(new ArrayData(array(
141 'Title' => 'value',
142 'Value' => $title
143 )));
144 } else {
145 $tag = 'span';
146 $attributes->push(new ArrayData(array(
147 'Title' => 'class',
148 'Value' => 'items'
149 )));
150 $content = $title;
151 }
152 $options->push(new ArrayData(array(
153 'Tag' => $tag,
154 'Attributes' => $attributes,
155 'Content' => $content,
156 )));
157
158 $attributes = new DataObjectSet();
159 $attributes->push(new ArrayData(array(
160 'Title' => 'href',
161 'Value' => '#'
162 )));
163 $attributes->push(new ArrayData(array(
164 'Title' => 'title',
165 'Value' => _t('TreeDropdownField.OPEN','Open')
166 )));
167 $attributes->push(new ArrayData(array(
168 'Title' => 'class',
169 'Value' => 'editLink'
170 )));
171 $options->push(new ArrayData(array(
172 'Tag' => 'a',
173 'Attributes' => $attributes,
174 'Content' => ' '
175 )));
176
177 $attributes = new DataObjectSet();
178 $attributes->push(new ArrayData(array(
179 'Title' => 'href',
180 'Value' => '#'
181 )));
182 $attributes->push(new ArrayData(array(
183 'Title' => 'title',
184 'Value' => _t('TreeDropdownField.CLEAR','Clear')
185 )));
186 $attributes->push(new ArrayData(array(
187 'Title' => 'class',
188 'Value' => 'cleanLink'
189 )));
190 $options->push(new ArrayData(array(
191 'Tag' => 'a',
192 'Attributes' => $attributes,
193 'Content' => ' '
194 )));
195
196
197 return $this->createTag (
198 'div',
199 array (
200 'id' => "TreeDropdownField_{$this->id()}",
201 'class' => 'TreeDropdownField single' . ($this->extraClass() ? " {$this->extraClass()}" : ''),
202 'href' => $this->form ? $this->Link() : "",
203 ),
204 $options
205 );
206 }
207
208 209 210 211 212 213
214 public function tree(SS_HTTPRequest $request) {
215
216 if(is_array($this->sourceObject)) {
217 $output = "<ul class=\"tree\">\n";
218 foreach($this->sourceObject as $k => $v) {
219 $output .= '<li id="selector-' . $this->name . '-' . $k . '"><a>' . $v . '</a>';
220 }
221 $output .= "</ul>";
222 return $output;
223 }
224
225
226 $isSubTree = false;
227
228 $this->search = Convert::Raw2SQL($request->getVar('search'));
229
230 if($ID = (int) $request->latestparam('ID')) {
231 $obj = DataObject::get_by_id($this->sourceObject, $ID);
232 $isSubTree = true;
233
234 if(!$obj) {
235 throw new Exception (
236 "TreeDropdownField->tree(): the object #$ID of type $this->sourceObject could not be found"
237 );
238 }
239 } else {
240 if($this->baseID) {
241 $obj = DataObject::get_by_id($this->sourceObject, $this->baseID);
242 }
243
244 if(!$this->baseID || !$obj) $obj = singleton($this->sourceObject);
245 }
246
247
248 if ( $this->search != "" )
249 $this->populateIDs();
250
251 if ($this->filterCallback || $this->sourceObject == 'Folder' || $this->search != "" )
252 $obj->setMarkingFilterFunction(array($this, "filterMarking"));
253
254 $obj->markPartialTree();
255
256
257 if( isset($_REQUEST['forceValue']) || $this->value ) {
258 $forceValue = ( isset($_REQUEST['forceValue']) ? $_REQUEST['forceValue'] : $this->value);
259 if(($values = preg_split('/,\s*/', $forceValue)) && count($values)) foreach($values as $value) {
260 if($this->keyField == 'ID' && !is_numeric($value)) continue;
261
262 $obj->markToExpose($this->objectForKey($value));
263 }
264 }
265
266 $eval = '"<li id=\"selector-' . $this->Name() . '-{$child->' . $this->keyField . '}\" class=\"$child->class"' .
267 ' . $child->markingClasses() . "\"><a rel=\"$child->ID\">" . $child->' . $this->labelField . ' . "</a>"';
268
269 if($isSubTree) {
270 return substr(trim($obj->getChildrenAsUL('', $eval, null, true)), 4, -5);
271 }
272
273 return $obj->getChildrenAsUL('class="tree"', $eval, null, true);
274 }
275
276 277 278 279 280 281 282
283 function filterMarking($node) {
284 if ($this->labelField == 'Title' && !$node->Title) return false;
285 if ($this->filterCallback && !call_user_func($this->filterCallback, $node)) return false;
286 if ($this->sourceObject == "Folder" && $node->ClassName != 'Folder') return false;
287 if (is_a($this->sourceObject, 'File', true) && ($node->Hidden == 1)) return false;
288 if ($this->search != "") {
289 return isset($this->searchIds[$node->ID]) && $this->searchIds[$node->ID] ? true : false;
290 }
291
292 return true;
293 }
294
295 296 297 298 299
300 protected function populateIDs() {
301
302 if ( $this->searchCallback )
303 $res = call_user_func($this->searchCallback, $this->sourceObject, $this->labelField, $this->search);
304 else
305 $res = DataObject::get($this->sourceObject, "\"$this->labelField\" LIKE '%$this->search%'");
306
307 if( $res ) {
308
309 foreach($res as $row) {
310 if ($row->ParentID) $parents[$row->ParentID] = true;
311 $this->searchIds[$row->ID] = true;
312 }
313 while (!empty($parents)) {
314
315 $res = DB::query('SELECT ParentID, ID FROM ' . $this->sourceObject . ' WHERE ID in ('.implode(',',array_keys($parents)).')');
316 $parents = array();
317
318 foreach($res as $row) {
319 if ($row['ParentID']) $parents[$row['ParentID']] = true;
320 $this->searchIds[$row['ID']] = true;
321 $this->searchExpanded[$row['ID']] = true;
322 }
323 }
324 }
325 }
326
327 328 329 330 331 332
333 protected function objectForKey($key) {
334 if (is_array($this->sourceObject))
335 return new ArrayData(array('ID'=> $key, 'Title' => $this->sourceObject[$key]));
336
337 if($this->keyField == 'ID') {
338 return DataObject::get_by_id($this->sourceObject, Convert::raw2sql($key));
339 } else {
340 return DataObject::get_one($this->sourceObject, "\"{$this->keyField}\" = '" . Convert::raw2sql($key) . "'");
341 }
342 }
343
344 345 346
347 function performReadonlyTransformation() {
348 return new TreeDropdownField_Readonly($this->name, $this->title, $this->sourceObject, $this->keyField, $this->labelField);
349 }
350 }
351
352 353 354 355
356 class TreeDropdownField_Readonly extends TreeDropdownField {
357 protected $readonly = true;
358
359 function Field() {
360 $fieldName = $this->labelField;
361 if($this->value) {
362 $keyObj = $this->objectForKey($this->value);
363 $obj = $keyObj ? $keyObj->$fieldName : '';
364 } else {
365 $obj = null;
366 }
367
368 $source = array(
369 $this->value => $obj
370 );
371
372 $field = new LookupField($this->name, $this->title, $source);
373 $field->setValue($this->value);
374 $field->setForm($this->form);
375 return $field->Field();
376 }
377 }
378
[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the
Silverstripe Forums to ask development related questions.
-