Passed
Push — master ( 21c694...dd5bf3 )
by Garion
04:35 queued 11s
created
src/TagField.php 1 patch
Indentation   +525 added lines, -525 removed lines patch added patch discarded remove patch
@@ -26,529 +26,529 @@
 block discarded – undo
26 26
  */
27 27
 class TagField extends MultiSelectField
28 28
 {
29
-    /**
30
-     * @var array
31
-     */
32
-    private static $allowed_actions = [
33
-        'suggest',
34
-    ];
35
-
36
-    /**
37
-     * @var bool
38
-     */
39
-    protected $shouldLazyLoad = false;
40
-
41
-    /**
42
-     * @var int
43
-     */
44
-    protected $lazyLoadItemLimit = 10;
45
-
46
-    /**
47
-     * @var bool
48
-     */
49
-    protected $canCreate = true;
50
-
51
-    /**
52
-     * @var string
53
-     */
54
-    protected $titleField = 'Title';
55
-
56
-    /**
57
-     * @var DataList
58
-     */
59
-    protected $sourceList;
60
-
61
-    /**
62
-     * @var bool
63
-     */
64
-    protected $isMultiple = true;
65
-
66
-    /** @skipUpgrade */
67
-    protected $schemaComponent = 'TagField';
68
-
69
-    /**
70
-     * @param string $name
71
-     * @param string $title
72
-     * @param null|DataList|array $source
73
-     * @param null|DataList $value
74
-     * @param string $titleField
75
-     */
76
-    public function __construct($name, $title = '', $source = [], $value = null, $titleField = 'Title')
77
-    {
78
-        $this->setTitleField($titleField);
79
-        parent::__construct($name, $title, $source, $value);
80
-
81
-        $this->addExtraClass('ss-tag-field');
82
-    }
83
-
84
-    /**
85
-     * @return bool
86
-     */
87
-    public function getShouldLazyLoad()
88
-    {
89
-        return $this->shouldLazyLoad;
90
-    }
91
-
92
-    /**
93
-     * @param bool $shouldLazyLoad
94
-     *
95
-     * @return static
96
-     */
97
-    public function setShouldLazyLoad($shouldLazyLoad)
98
-    {
99
-        $this->shouldLazyLoad = $shouldLazyLoad;
100
-
101
-        return $this;
102
-    }
103
-
104
-    /**
105
-     * @return int
106
-     */
107
-    public function getLazyLoadItemLimit()
108
-    {
109
-        return $this->lazyLoadItemLimit;
110
-    }
111
-
112
-    /**
113
-     * @param int $lazyLoadItemLimit
114
-     *
115
-     * @return static
116
-     */
117
-    public function setLazyLoadItemLimit($lazyLoadItemLimit)
118
-    {
119
-        $this->lazyLoadItemLimit = $lazyLoadItemLimit;
120
-
121
-        return $this;
122
-    }
123
-
124
-    /**
125
-     * @return bool
126
-     */
127
-    public function getIsMultiple()
128
-    {
129
-        return $this->isMultiple;
130
-    }
131
-
132
-    /**
133
-     * @param bool $isMultiple
134
-     *
135
-     * @return static
136
-     */
137
-    public function setIsMultiple($isMultiple)
138
-    {
139
-        $this->isMultiple = $isMultiple;
140
-
141
-        return $this;
142
-    }
143
-
144
-    /**
145
-     * @return bool
146
-     */
147
-    public function getCanCreate()
148
-    {
149
-        return $this->canCreate;
150
-    }
151
-
152
-    /**
153
-     * @param bool $canCreate
154
-     *
155
-     * @return static
156
-     */
157
-    public function setCanCreate($canCreate)
158
-    {
159
-        $this->canCreate = $canCreate;
160
-
161
-        return $this;
162
-    }
163
-
164
-    /**
165
-     * @return string
166
-     */
167
-    public function getTitleField()
168
-    {
169
-        return $this->titleField;
170
-    }
171
-
172
-    /**
173
-     * @param string $titleField
174
-     *
175
-     * @return $this
176
-     */
177
-    public function setTitleField($titleField)
178
-    {
179
-        $this->titleField = $titleField;
180
-
181
-        return $this;
182
-    }
183
-
184
-    /**
185
-     * Get the DataList source. The 4.x upgrade for SelectField::setSource starts to convert this to an array.
186
-     * If empty use getSource() for array version
187
-     *
188
-     * @return DataList
189
-     */
190
-    public function getSourceList()
191
-    {
192
-        return $this->sourceList;
193
-    }
194
-
195
-    /**
196
-     * Set the model class name for tags
197
-     *
198
-     * @param DataList $sourceList
199
-     * @return self
200
-     */
201
-    public function setSourceList($sourceList)
202
-    {
203
-        $this->sourceList = $sourceList;
204
-        return $this;
205
-    }
206
-
207
-    /**
208
-     * {@inheritdoc}
209
-     */
210
-    public function Field($properties = [])
211
-    {
212
-        $this->addExtraClass('entwine');
213
-
214
-        return $this->customise($properties)->renderWith(self::class);
215
-    }
216
-
217
-    /**
218
-     * Provide TagField data to the JSON schema for the frontend component
219
-     *
220
-     * @return array
221
-     */
222
-    public function getSchemaDataDefaults()
223
-    {
224
-        $options = $this->getOptions(true);
225
-        $schema = array_merge(
226
-            parent::getSchemaDataDefaults(),
227
-            [
228
-                'name' => $this->getName() . '[]',
229
-                'lazyLoad' => $this->getShouldLazyLoad(),
230
-                'creatable' => $this->getCanCreate(),
231
-                'multi' => $this->getIsMultiple(),
232
-                'value' => $options->count() ? $options->toNestedArray() : null,
233
-                'disabled' => $this->isDisabled() || $this->isReadonly(),
234
-            ]
235
-        );
236
-
237
-        if (!$this->getShouldLazyLoad()) {
238
-            $schema['options'] = array_values($this->getOptions()->toNestedArray());
239
-        } else {
240
-            $schema['optionUrl'] = $this->getSuggestURL();
241
-        }
242
-
243
-        return $schema;
244
-    }
245
-
246
-    /**
247
-     * @return string
248
-     */
249
-    protected function getSuggestURL()
250
-    {
251
-        return Controller::join_links($this->Link(), 'suggest');
252
-    }
253
-
254
-    /**
255
-     * @return ArrayList
256
-     */
257
-    protected function getOptions($onlySelected = false)
258
-    {
259
-        $options = ArrayList::create();
260
-        $source = $this->getSourceList();
261
-
262
-        // No source means we have no options
263
-        if (!$source) {
264
-            return ArrayList::create();
265
-        }
266
-
267
-        $dataClass = $source->dataClass();
268
-
269
-        $values = $this->Value();
270
-
271
-        // If we have no values and we only want selected options we can bail here
272
-        if (empty($values) && $onlySelected) {
273
-            return ArrayList::create();
274
-        }
275
-
276
-        // Convert an array of values into a datalist of options
277
-        if (is_array($values) && !empty($values)) {
278
-            $values = DataList::create($dataClass)
279
-                ->filter($this->getTitleField(), $values);
280
-        } else {
281
-            $values = ArrayList::create();
282
-        }
283
-
284
-        // Prep a function to parse a dataobject into an option
285
-        $addOption = function (DataObject $item) use ($options, $values) {
286
-            $titleField = $this->getTitleField();
287
-            $option = $item->$titleField;
288
-            $options->push(ArrayData::create([
289
-                'Title' => $option,
290
-                'Value' => $option,
291
-                'Selected' => (bool) $values->find('ID', $item->ID)
292
-            ]));
293
-        };
294
-
295
-        // Only parse the values if we only want the selected items in the values list (this is for lazy-loading)
296
-        if ($onlySelected) {
297
-            $values->each($addOption);
298
-            return $options;
299
-        }
300
-
301
-        $source->each($addOption);
302
-        return $options;
303
-    }
304
-
305
-    /**
306
-     * {@inheritdoc}
307
-     */
308
-    public function setValue($value, $source = null)
309
-    {
310
-        if ($source instanceof DataObject) {
311
-            $name = $this->getName();
312
-
313
-            if ($source->hasMethod($name)) {
314
-                $value = $source->$name()->column($this->getTitleField());
315
-            }
316
-        }
317
-
318
-        if (!is_array($value)) {
319
-            return parent::setValue($value);
320
-        }
321
-
322
-        return parent::setValue(array_filter($value));
323
-    }
324
-
325
-    /**
326
-     * Gets the source array if required
327
-     *
328
-     * Note: this is expensive for a SS_List
329
-     *
330
-     * @return array
331
-     */
332
-    public function getSource()
333
-    {
334
-        if (is_null($this->source)) {
335
-            $this->source = $this->getListMap($this->getSourceList());
336
-        }
337
-        return $this->source;
338
-    }
339
-
340
-    /**
341
-     * Intercept DataList source
342
-     *
343
-     * @param mixed $source
344
-     * @return $this
345
-     */
346
-    public function setSource($source)
347
-    {
348
-        // When setting a datalist force internal list to null
349
-        if ($source instanceof DataList) {
350
-            $this->source = null;
351
-            $this->setSourceList($source);
352
-        } else {
353
-            parent::setSource($source);
354
-        }
355
-        return $this;
356
-    }
357
-
358
-    /**
359
-     * @param DataObject|DataObjectInterface $record DataObject to save data into
360
-     * @throws Exception
361
-     */
362
-    public function getAttributes()
363
-    {
364
-        return array_merge(
365
-            parent::getAttributes(),
366
-            [
367
-                'name' => $this->getName() . '[]',
368
-                'style' => 'width: 100%',
369
-                'data-schema' => json_encode($this->getSchemaData()),
370
-            ]
371
-        );
372
-    }
373
-
374
-    /**
375
-     * {@inheritdoc}
376
-     */
377
-    public function saveInto(DataObjectInterface $record)
378
-    {
379
-        $name = $this->getName();
380
-        $titleField = $this->getTitleField();
381
-        $values = $this->Value();
382
-
383
-        /** @var Relation $relation */
384
-        $relation = $record->$name();
385
-        $ids = [];
386
-
387
-        if (!$values) {
388
-            $values = [];
389
-        }
390
-
391
-        if (empty($record) || empty($titleField)) {
392
-            return;
393
-        }
394
-
395
-        if (!$record->hasMethod($name)) {
396
-            throw new Exception(
397
-                sprintf("%s does not have a %s method", get_class($record), $name)
398
-            );
399
-        }
400
-
401
-        foreach ($values as $key => $value) {
402
-            // Get or create record
403
-            $record = $this->getOrCreateTag($value);
404
-            if ($record) {
405
-                $ids[] = $record->ID;
406
-                $values[$key] = $record->Title;
407
-            }
408
-        }
409
-
410
-        $relation->setByIDList(array_filter($ids));
411
-    }
412
-
413
-    /**
414
-     * Get or create tag with the given value
415
-     *
416
-     * @param  string $term
417
-     * @return DataObject|bool
418
-     */
419
-    protected function getOrCreateTag($term)
420
-    {
421
-        // Check if existing record can be found
422
-        $source = $this->getSourceList();
423
-        if (!$source) {
424
-            return false;
425
-        }
426
-
427
-        $titleField = $this->getTitleField();
428
-        $record = $source
429
-            ->filter($titleField, $term)
430
-            ->first();
431
-        if ($record) {
432
-            return $record;
433
-        }
434
-
435
-        // Create new instance if not yet saved
436
-        if ($this->getCanCreate()) {
437
-            $dataClass = $source->dataClass();
438
-            $record = Injector::inst()->create($dataClass);
439
-
440
-            if (is_array($term)) {
441
-                $term = $term['Value'];
442
-            }
443
-
444
-            $record->{$titleField} = $term;
445
-            $record->write();
446
-            if ($source instanceof SS_List) {
447
-                $source->add($record);
448
-            }
449
-            return $record;
450
-        }
451
-
452
-        return false;
453
-    }
454
-
455
-    /**
456
-     * Returns a JSON string of tags, for lazy loading.
457
-     *
458
-     * @param  HTTPRequest $request
459
-     * @return HTTPResponse
460
-     */
461
-    public function suggest(HTTPRequest $request)
462
-    {
463
-        $tags = $this->getTags($request->getVar('term'));
464
-
465
-        $response = HTTPResponse::create();
466
-        $response->addHeader('Content-Type', 'application/json');
467
-        $response->setBody(json_encode(['items' => $tags]));
468
-
469
-        return $response;
470
-    }
471
-
472
-    /**
473
-     * Returns array of arrays representing tags.
474
-     *
475
-     * @param  string $term
476
-     * @return array
477
-     */
478
-    protected function getTags($term)
479
-    {
480
-        $source = $this->getSourceList();
481
-        if (!$source) {
482
-            return [];
483
-        }
484
-
485
-        $titleField = $this->getTitleField();
486
-
487
-        $query = $source
488
-            ->filter($titleField . ':PartialMatch:nocase', $term)
489
-            ->sort($titleField)
490
-            ->limit($this->getLazyLoadItemLimit());
491
-
492
-        // Map into a distinct list
493
-        $items = [];
494
-        $titleField = $this->getTitleField();
495
-        foreach ($query->map('ID', $titleField) as $id => $title) {
496
-            $items[$title] = [
497
-                'Title' => $title,
498
-                'Value' => $title,
499
-            ];
500
-        }
501
-
502
-        return array_values($items);
503
-    }
504
-
505
-    /**
506
-     * DropdownField assumes value will be a scalar so we must
507
-     * override validate. This only applies to Silverstripe 3.2+
508
-     *
509
-     * @param Validator $validator
510
-     * @return bool
511
-     */
512
-    public function validate($validator)
513
-    {
514
-        return true;
515
-    }
516
-
517
-    /**
518
-     * Converts the field to a readonly variant.
519
-     *
520
-     * @return ReadonlyTagField
521
-     */
522
-    public function performReadonlyTransformation()
523
-    {
524
-        /** @var ReadonlyTagField $copy */
525
-        $copy = $this->castedCopy(ReadonlyTagField::class);
526
-        $copy->setSourceList($this->getSourceList());
527
-        return $copy;
528
-    }
529
-
530
-    /**
531
-     * Prevent the default, which would return "tag"
532
-     *
533
-     * @return string
534
-     */
535
-    public function Type()
536
-    {
537
-        return '';
538
-    }
539
-
540
-    public function getSchemaStateDefaults()
541
-    {
542
-        $data = parent::getSchemaStateDefaults();
543
-
544
-        // Add options to 'data'
545
-        $data['lazyLoad'] = $this->getShouldLazyLoad();
546
-        $data['multi'] = $this->getIsMultiple();
547
-        $data['optionUrl'] = $this->getSuggestURL();
548
-        $data['creatable'] = $this->getCanCreate();
549
-        $options = $this->getOptions(true);
550
-        $data['value'] = $options->count() ? $options->toNestedArray() : null;
551
-
552
-        return $data;
553
-    }
29
+	/**
30
+	 * @var array
31
+	 */
32
+	private static $allowed_actions = [
33
+		'suggest',
34
+	];
35
+
36
+	/**
37
+	 * @var bool
38
+	 */
39
+	protected $shouldLazyLoad = false;
40
+
41
+	/**
42
+	 * @var int
43
+	 */
44
+	protected $lazyLoadItemLimit = 10;
45
+
46
+	/**
47
+	 * @var bool
48
+	 */
49
+	protected $canCreate = true;
50
+
51
+	/**
52
+	 * @var string
53
+	 */
54
+	protected $titleField = 'Title';
55
+
56
+	/**
57
+	 * @var DataList
58
+	 */
59
+	protected $sourceList;
60
+
61
+	/**
62
+	 * @var bool
63
+	 */
64
+	protected $isMultiple = true;
65
+
66
+	/** @skipUpgrade */
67
+	protected $schemaComponent = 'TagField';
68
+
69
+	/**
70
+	 * @param string $name
71
+	 * @param string $title
72
+	 * @param null|DataList|array $source
73
+	 * @param null|DataList $value
74
+	 * @param string $titleField
75
+	 */
76
+	public function __construct($name, $title = '', $source = [], $value = null, $titleField = 'Title')
77
+	{
78
+		$this->setTitleField($titleField);
79
+		parent::__construct($name, $title, $source, $value);
80
+
81
+		$this->addExtraClass('ss-tag-field');
82
+	}
83
+
84
+	/**
85
+	 * @return bool
86
+	 */
87
+	public function getShouldLazyLoad()
88
+	{
89
+		return $this->shouldLazyLoad;
90
+	}
91
+
92
+	/**
93
+	 * @param bool $shouldLazyLoad
94
+	 *
95
+	 * @return static
96
+	 */
97
+	public function setShouldLazyLoad($shouldLazyLoad)
98
+	{
99
+		$this->shouldLazyLoad = $shouldLazyLoad;
100
+
101
+		return $this;
102
+	}
103
+
104
+	/**
105
+	 * @return int
106
+	 */
107
+	public function getLazyLoadItemLimit()
108
+	{
109
+		return $this->lazyLoadItemLimit;
110
+	}
111
+
112
+	/**
113
+	 * @param int $lazyLoadItemLimit
114
+	 *
115
+	 * @return static
116
+	 */
117
+	public function setLazyLoadItemLimit($lazyLoadItemLimit)
118
+	{
119
+		$this->lazyLoadItemLimit = $lazyLoadItemLimit;
120
+
121
+		return $this;
122
+	}
123
+
124
+	/**
125
+	 * @return bool
126
+	 */
127
+	public function getIsMultiple()
128
+	{
129
+		return $this->isMultiple;
130
+	}
131
+
132
+	/**
133
+	 * @param bool $isMultiple
134
+	 *
135
+	 * @return static
136
+	 */
137
+	public function setIsMultiple($isMultiple)
138
+	{
139
+		$this->isMultiple = $isMultiple;
140
+
141
+		return $this;
142
+	}
143
+
144
+	/**
145
+	 * @return bool
146
+	 */
147
+	public function getCanCreate()
148
+	{
149
+		return $this->canCreate;
150
+	}
151
+
152
+	/**
153
+	 * @param bool $canCreate
154
+	 *
155
+	 * @return static
156
+	 */
157
+	public function setCanCreate($canCreate)
158
+	{
159
+		$this->canCreate = $canCreate;
160
+
161
+		return $this;
162
+	}
163
+
164
+	/**
165
+	 * @return string
166
+	 */
167
+	public function getTitleField()
168
+	{
169
+		return $this->titleField;
170
+	}
171
+
172
+	/**
173
+	 * @param string $titleField
174
+	 *
175
+	 * @return $this
176
+	 */
177
+	public function setTitleField($titleField)
178
+	{
179
+		$this->titleField = $titleField;
180
+
181
+		return $this;
182
+	}
183
+
184
+	/**
185
+	 * Get the DataList source. The 4.x upgrade for SelectField::setSource starts to convert this to an array.
186
+	 * If empty use getSource() for array version
187
+	 *
188
+	 * @return DataList
189
+	 */
190
+	public function getSourceList()
191
+	{
192
+		return $this->sourceList;
193
+	}
194
+
195
+	/**
196
+	 * Set the model class name for tags
197
+	 *
198
+	 * @param DataList $sourceList
199
+	 * @return self
200
+	 */
201
+	public function setSourceList($sourceList)
202
+	{
203
+		$this->sourceList = $sourceList;
204
+		return $this;
205
+	}
206
+
207
+	/**
208
+	 * {@inheritdoc}
209
+	 */
210
+	public function Field($properties = [])
211
+	{
212
+		$this->addExtraClass('entwine');
213
+
214
+		return $this->customise($properties)->renderWith(self::class);
215
+	}
216
+
217
+	/**
218
+	 * Provide TagField data to the JSON schema for the frontend component
219
+	 *
220
+	 * @return array
221
+	 */
222
+	public function getSchemaDataDefaults()
223
+	{
224
+		$options = $this->getOptions(true);
225
+		$schema = array_merge(
226
+			parent::getSchemaDataDefaults(),
227
+			[
228
+				'name' => $this->getName() . '[]',
229
+				'lazyLoad' => $this->getShouldLazyLoad(),
230
+				'creatable' => $this->getCanCreate(),
231
+				'multi' => $this->getIsMultiple(),
232
+				'value' => $options->count() ? $options->toNestedArray() : null,
233
+				'disabled' => $this->isDisabled() || $this->isReadonly(),
234
+			]
235
+		);
236
+
237
+		if (!$this->getShouldLazyLoad()) {
238
+			$schema['options'] = array_values($this->getOptions()->toNestedArray());
239
+		} else {
240
+			$schema['optionUrl'] = $this->getSuggestURL();
241
+		}
242
+
243
+		return $schema;
244
+	}
245
+
246
+	/**
247
+	 * @return string
248
+	 */
249
+	protected function getSuggestURL()
250
+	{
251
+		return Controller::join_links($this->Link(), 'suggest');
252
+	}
253
+
254
+	/**
255
+	 * @return ArrayList
256
+	 */
257
+	protected function getOptions($onlySelected = false)
258
+	{
259
+		$options = ArrayList::create();
260
+		$source = $this->getSourceList();
261
+
262
+		// No source means we have no options
263
+		if (!$source) {
264
+			return ArrayList::create();
265
+		}
266
+
267
+		$dataClass = $source->dataClass();
268
+
269
+		$values = $this->Value();
270
+
271
+		// If we have no values and we only want selected options we can bail here
272
+		if (empty($values) && $onlySelected) {
273
+			return ArrayList::create();
274
+		}
275
+
276
+		// Convert an array of values into a datalist of options
277
+		if (is_array($values) && !empty($values)) {
278
+			$values = DataList::create($dataClass)
279
+				->filter($this->getTitleField(), $values);
280
+		} else {
281
+			$values = ArrayList::create();
282
+		}
283
+
284
+		// Prep a function to parse a dataobject into an option
285
+		$addOption = function (DataObject $item) use ($options, $values) {
286
+			$titleField = $this->getTitleField();
287
+			$option = $item->$titleField;
288
+			$options->push(ArrayData::create([
289
+				'Title' => $option,
290
+				'Value' => $option,
291
+				'Selected' => (bool) $values->find('ID', $item->ID)
292
+			]));
293
+		};
294
+
295
+		// Only parse the values if we only want the selected items in the values list (this is for lazy-loading)
296
+		if ($onlySelected) {
297
+			$values->each($addOption);
298
+			return $options;
299
+		}
300
+
301
+		$source->each($addOption);
302
+		return $options;
303
+	}
304
+
305
+	/**
306
+	 * {@inheritdoc}
307
+	 */
308
+	public function setValue($value, $source = null)
309
+	{
310
+		if ($source instanceof DataObject) {
311
+			$name = $this->getName();
312
+
313
+			if ($source->hasMethod($name)) {
314
+				$value = $source->$name()->column($this->getTitleField());
315
+			}
316
+		}
317
+
318
+		if (!is_array($value)) {
319
+			return parent::setValue($value);
320
+		}
321
+
322
+		return parent::setValue(array_filter($value));
323
+	}
324
+
325
+	/**
326
+	 * Gets the source array if required
327
+	 *
328
+	 * Note: this is expensive for a SS_List
329
+	 *
330
+	 * @return array
331
+	 */
332
+	public function getSource()
333
+	{
334
+		if (is_null($this->source)) {
335
+			$this->source = $this->getListMap($this->getSourceList());
336
+		}
337
+		return $this->source;
338
+	}
339
+
340
+	/**
341
+	 * Intercept DataList source
342
+	 *
343
+	 * @param mixed $source
344
+	 * @return $this
345
+	 */
346
+	public function setSource($source)
347
+	{
348
+		// When setting a datalist force internal list to null
349
+		if ($source instanceof DataList) {
350
+			$this->source = null;
351
+			$this->setSourceList($source);
352
+		} else {
353
+			parent::setSource($source);
354
+		}
355
+		return $this;
356
+	}
357
+
358
+	/**
359
+	 * @param DataObject|DataObjectInterface $record DataObject to save data into
360
+	 * @throws Exception
361
+	 */
362
+	public function getAttributes()
363
+	{
364
+		return array_merge(
365
+			parent::getAttributes(),
366
+			[
367
+				'name' => $this->getName() . '[]',
368
+				'style' => 'width: 100%',
369
+				'data-schema' => json_encode($this->getSchemaData()),
370
+			]
371
+		);
372
+	}
373
+
374
+	/**
375
+	 * {@inheritdoc}
376
+	 */
377
+	public function saveInto(DataObjectInterface $record)
378
+	{
379
+		$name = $this->getName();
380
+		$titleField = $this->getTitleField();
381
+		$values = $this->Value();
382
+
383
+		/** @var Relation $relation */
384
+		$relation = $record->$name();
385
+		$ids = [];
386
+
387
+		if (!$values) {
388
+			$values = [];
389
+		}
390
+
391
+		if (empty($record) || empty($titleField)) {
392
+			return;
393
+		}
394
+
395
+		if (!$record->hasMethod($name)) {
396
+			throw new Exception(
397
+				sprintf("%s does not have a %s method", get_class($record), $name)
398
+			);
399
+		}
400
+
401
+		foreach ($values as $key => $value) {
402
+			// Get or create record
403
+			$record = $this->getOrCreateTag($value);
404
+			if ($record) {
405
+				$ids[] = $record->ID;
406
+				$values[$key] = $record->Title;
407
+			}
408
+		}
409
+
410
+		$relation->setByIDList(array_filter($ids));
411
+	}
412
+
413
+	/**
414
+	 * Get or create tag with the given value
415
+	 *
416
+	 * @param  string $term
417
+	 * @return DataObject|bool
418
+	 */
419
+	protected function getOrCreateTag($term)
420
+	{
421
+		// Check if existing record can be found
422
+		$source = $this->getSourceList();
423
+		if (!$source) {
424
+			return false;
425
+		}
426
+
427
+		$titleField = $this->getTitleField();
428
+		$record = $source
429
+			->filter($titleField, $term)
430
+			->first();
431
+		if ($record) {
432
+			return $record;
433
+		}
434
+
435
+		// Create new instance if not yet saved
436
+		if ($this->getCanCreate()) {
437
+			$dataClass = $source->dataClass();
438
+			$record = Injector::inst()->create($dataClass);
439
+
440
+			if (is_array($term)) {
441
+				$term = $term['Value'];
442
+			}
443
+
444
+			$record->{$titleField} = $term;
445
+			$record->write();
446
+			if ($source instanceof SS_List) {
447
+				$source->add($record);
448
+			}
449
+			return $record;
450
+		}
451
+
452
+		return false;
453
+	}
454
+
455
+	/**
456
+	 * Returns a JSON string of tags, for lazy loading.
457
+	 *
458
+	 * @param  HTTPRequest $request
459
+	 * @return HTTPResponse
460
+	 */
461
+	public function suggest(HTTPRequest $request)
462
+	{
463
+		$tags = $this->getTags($request->getVar('term'));
464
+
465
+		$response = HTTPResponse::create();
466
+		$response->addHeader('Content-Type', 'application/json');
467
+		$response->setBody(json_encode(['items' => $tags]));
468
+
469
+		return $response;
470
+	}
471
+
472
+	/**
473
+	 * Returns array of arrays representing tags.
474
+	 *
475
+	 * @param  string $term
476
+	 * @return array
477
+	 */
478
+	protected function getTags($term)
479
+	{
480
+		$source = $this->getSourceList();
481
+		if (!$source) {
482
+			return [];
483
+		}
484
+
485
+		$titleField = $this->getTitleField();
486
+
487
+		$query = $source
488
+			->filter($titleField . ':PartialMatch:nocase', $term)
489
+			->sort($titleField)
490
+			->limit($this->getLazyLoadItemLimit());
491
+
492
+		// Map into a distinct list
493
+		$items = [];
494
+		$titleField = $this->getTitleField();
495
+		foreach ($query->map('ID', $titleField) as $id => $title) {
496
+			$items[$title] = [
497
+				'Title' => $title,
498
+				'Value' => $title,
499
+			];
500
+		}
501
+
502
+		return array_values($items);
503
+	}
504
+
505
+	/**
506
+	 * DropdownField assumes value will be a scalar so we must
507
+	 * override validate. This only applies to Silverstripe 3.2+
508
+	 *
509
+	 * @param Validator $validator
510
+	 * @return bool
511
+	 */
512
+	public function validate($validator)
513
+	{
514
+		return true;
515
+	}
516
+
517
+	/**
518
+	 * Converts the field to a readonly variant.
519
+	 *
520
+	 * @return ReadonlyTagField
521
+	 */
522
+	public function performReadonlyTransformation()
523
+	{
524
+		/** @var ReadonlyTagField $copy */
525
+		$copy = $this->castedCopy(ReadonlyTagField::class);
526
+		$copy->setSourceList($this->getSourceList());
527
+		return $copy;
528
+	}
529
+
530
+	/**
531
+	 * Prevent the default, which would return "tag"
532
+	 *
533
+	 * @return string
534
+	 */
535
+	public function Type()
536
+	{
537
+		return '';
538
+	}
539
+
540
+	public function getSchemaStateDefaults()
541
+	{
542
+		$data = parent::getSchemaStateDefaults();
543
+
544
+		// Add options to 'data'
545
+		$data['lazyLoad'] = $this->getShouldLazyLoad();
546
+		$data['multi'] = $this->getIsMultiple();
547
+		$data['optionUrl'] = $this->getSuggestURL();
548
+		$data['creatable'] = $this->getCanCreate();
549
+		$options = $this->getOptions(true);
550
+		$data['value'] = $options->count() ? $options->toNestedArray() : null;
551
+
552
+		return $data;
553
+	}
554 554
 }
Please login to merge, or discard this patch.