-
Notifications
You must be signed in to change notification settings - Fork 0
/
ConnectPageFields.module
440 lines (404 loc) · 13.1 KB
/
ConnectPageFields.module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
<?php namespace ProcessWire;
class ConnectPageFields extends WireData implements Module, ConfigurableModule {
/**
* Module information
*/
public static function getModuleInfo() {
return array(
'title' => 'Connect Page Fields',
'summary' => "Allows the connecting of two related Page Reference fields so that changing one updates the other.",
'version' => '0.3.7',
'author' => 'Robin Sallis',
'href' => 'https://github.com/Toutouwai/ConnectPageFields',
'icon' => 'arrows-h',
'autoload' => true,
'requires' => 'ProcessWire>=3.0.166, PHP>=5.4.0',
);
}
/**
* Array of connected Page Reference fields according to module config
*/
protected $connected_fields = [];
/**
* Init
*/
public function init() {
$this->wire()->pages->addHookAfter('saveReady', $this, 'pageSave', ['priority' => 200]);
$this->wire()->pages->addHookAfter('added', $this, 'pageSave', ['priority' => 200]);
$this->wire()->pages->addHookAfter('saveFieldReady', $this, 'fieldSave', ['priority' => 200]);
}
/**
* Ready
*/
public function ready() {
$this->addHookBefore('ProcessModule::executeEdit', $this, 'addConfigAssets');
$this->addHookBefore('Modules::saveModuleConfigData', $this, 'processConfig');
}
/**
* A page is ready to be saved, or was just added
*
* @param HookEvent $event
*/
protected function pageSave(HookEvent $event) {
$page = $event->arguments(0);
// Return if this a new never-before-saved page
if(!$page->id) return;
// Return if page was already saved by this module (prevent recursion)
if($page->cpf_saved) return;
// Set custom property to indicate if page was just added
if($event->method === 'added') $page->cpf_added = true;
// Look for connected fields on this page
$this->determineConnectedFields();
$flds = $page->getFields()->find("id=" . implode('|', $this->connected_fields));
foreach($flds as $field) {
// Skip if this is not a newly added page and field hasn't changed
if(!$page->cpf_added && !$page->isChanged($field->name)) continue;
$this->updateConnectedFields($page, $field);
}
}
/**
* A field on a page is ready to be saved
*
* @param HookEvent $event
*/
protected function fieldSave(HookEvent $event) {
$page = $event->arguments(0);
$field = $event->arguments(1);
// Return if page was already saved by this module (prevent recursion)
if($page->cpf_saved) return;
// Return if this field isn't a Page Reference field
if($field->type != 'FieldtypePage') return;
$this->determineConnectedFields();
$this->updateConnectedFields($page, $field);
}
/**
* Update Page Reference fields that are connected to a field
*
* @param Page $page The page that is being saved or was just added
* @param Field $field A Page Reference field on that page
*/
protected function updateConnectedFields(Page $page, Field $field) {
$this_fname = $field->name;
$this_is_multi = $field->derefAsPage == FieldtypePage::derefAsPageArray ? true : false;
// Find occurrences of field in config
$cfg_matches = array_keys($this->connected_fields, $field->id);
foreach($cfg_matches as $cfg_match) {
$unique = substr($cfg_match, -13); // last 13 characters are the unique code
$num = $cfg_match[10];
// Make sure the pair of fields is populated in module config
if(!$this->connected_fields["pagefield_1_{$unique}"] || !$this->connected_fields["pagefield_2_{$unique}"]) continue;
// Get the other field
$other_num = $num === '1' ? '2' : '1';
$other_field = $this->wire()->fields->get($this->connected_fields["pagefield_{$other_num}_{$unique}"]);
if(!$other_field) continue;
$other_fname = $other_field->name;
$other_is_multi = $other_field->derefAsPage == FieldtypePage::derefAsPageArray ? true : false;
// Old value
if($page->cpf_added) {
// This is a newly added page, so the old value will be an empty field value
$old_value = $this_is_multi ? $this->wire(new PageArray()) : $this->wire(new NullPage());
} else {
// Get unformatted value to support unpublished pages
$old_value = $this->wire()->pages->getById($page->id, [
'cache' => false, // don't let it write to cache
'getFromCache' => false, // don't let it read from cache
'getOne' => true, // return a Page instead of a PageArray
])->getUnformatted($this_fname);
}
// New value
// Get unformatted value to support unpublished pages
$new_value = $page->getUnformatted($this_fname);
// Value changes
if($this_is_multi) {
// For multi Page fields
$added = $new_value->find("id!=$old_value");
$removed = $old_value->find("id!=$new_value");
} else {
// For non-multi Page fields
$added = $this->wire(new PageArray());
$removed = $this->wire(new PageArray());
// If the old value was a Page and not a NullPage
if($old_value && $old_value->id) $removed->add($old_value);
// If the new value is a Page and not a NullPage
if($new_value && $new_value->id) $added->add($new_value);
}
// Apply to other field
foreach($added as $item) {
if(!$item->fields || !$item->fields->get($other_fname)) continue;
if($other_is_multi) {
$not_existing = !$item->$other_fname->has($page);
} else {
$not_existing = !$item->$other_fname || $item->$other_fname->id !== $page->id;
}
if($not_existing) {
$item->of(false);
$other_is_multi ? $item->$other_fname->add($page) : $item->$other_fname = $page;
// Set custom property for preventing recursion
$item->cpf_saved = true;
$item->save($other_fname);
}
}
foreach($removed as $item) {
if(!$item->fields || !$item->fields->get($other_fname)) continue;
if($other_is_multi) {
$existing = $item->$other_fname->has($page);
} else {
$existing = $item->$other_fname && $item->$other_fname->id === $page->id;
}
if($existing) {
$item->of(false);
$other_is_multi ? $item->$other_fname->remove($page) : $item->$other_fname = '';
// Set custom property for preventing recursion
$item->cpf_saved = true;
$item->save($other_fname);
}
}
}
}
/**
* Determine the connected Page Reference fields according to the module config
*/
protected function determineConnectedFields() {
$cfg = $this->modules->getModuleConfigData($this->className());
$connected_fields = [];
foreach($cfg as $key => $value) {
// We only want the data relating to Page Reference connections
if(strpos($key, 'pagefield') === 0) $connected_fields[$key] = $value;
}
$this->connected_fields = array_filter($connected_fields);
}
/**
* Add CSS and JS for module config page
*
* @param HookEvent $event
*/
protected function addConfigAssets(HookEvent $event) {
if($this->wire()->input->get->name !== $this->className) return;
$info = $this->getModuleInfo();
$version = $info['version'];
$config = $this->config;
$config->styles->add($config->urls->$this . "module_config.css?v=$version");
$config->scripts->add($config->urls->$this . "module_config.js?v=$version");
}
/**
* Process the config data on save
*
* @param HookEvent $event
*/
protected function processConfig(HookEvent $event) {
$module_name = $event->arguments(0);
$cfg = $event->arguments(1);
if(is_object($module_name)) $module_name = $module_name->className();
if($module_name !== $this->className()) return;
// If the "Add another row" button was clicked
if($cfg['add_new']) {
$cfg['codes'] .= $cfg['unique'] . '|';
}
// If any rows were marked for deletion
$delete_codes = [];
foreach($cfg as $key => $value) {
if (strpos($key, 'delete_') === 0 && $value === 1) {
$delete_codes[] = substr($key, -13); // last 13 characters are the unique code;
}
}
if(count($delete_codes)) {
foreach($delete_codes as $delete_code) {
$cfg['codes'] = str_replace("$delete_code|", '', $cfg['codes']);
}
foreach($cfg as $key => $value) {
if($this->strposa($key, $delete_codes, 0)) {
unset($cfg[$key]);
}
}
}
// Set processed config
$event->arguments(1, $cfg);
}
/**
* strpos() with array of strings as needle
*
* @param string $haystack
* @param array|string $needle
* @param int $offset
* @return bool
*/
public function strposa($haystack, $needle, $offset = 0) {
if(!is_array($needle)) $needle = [$needle];
foreach($needle as $query) {
if(strpos($haystack, $query, $offset) !== false) return true;
}
return false;
}
/**
* Config inputfields
*
* @param InputfieldWrapper $inputfields
*/
public function getModuleConfigInputfields($inputfields) {
$inputfields->add($this->makeCodes());
$inputfields->add($this->makeUnique());
$codes = explode('|', rtrim($this->codes, '|'));
$i = 1;
foreach($codes as $code) {
if(!$code) break;
$inputfields->add($this->makeFieldset($code, $i));
$i++;
}
$inputfields->add($this->makeAddNew());
}
/**
* Make codes field
*
* @return InputfieldTextarea
*/
protected function makeCodes() {
/* @var InputfieldTextarea $f */
$f_name = 'codes';
$f = $this->modules->get('InputfieldTextarea');
$f->name = $f_name;
$f->value = $this->$f_name;
$f->collapsed = Inputfield::collapsedHidden;
return $f;
}
/**
* Make unique field
*
* @return InputfieldText
*/
protected function makeUnique() {
/* @var InputfieldText $f */
$f = $this->modules->get('InputfieldText');
$f->name = 'unique';
$f->value = uniqid();
$f->collapsed = Inputfield::collapsedHidden;
return $f;
}
/**
* Make 'Add New' button
*
* @return InputfieldCheckbox
*/
protected function makeAddNew() {
/* @var InputfieldCheckbox $f */
$f = $this->modules->get('InputfieldCheckbox');
$f->name = 'add_new';
$f->label = $this->_('Add another row');
return $f;
}
/**
* Make fieldset
*
* @param string $identifier
* @param int $number
* @return InputfieldFieldset
*/
protected function makeFieldset($identifier, $number) {
/* @var InputfieldFieldset $fieldset */
$fieldset = $this->modules->get('InputfieldFieldset');
$fieldset->name = "row_{$identifier}";
$fieldset->label = $this->_('Connected field pair') . " $number";
$f = $this->makePageFieldSelect(1, 'A', $identifier);
$fieldset->add($f);
$f = $this->makePageFieldSelect(2, 'B', $identifier);
$fieldset->add($f);
$f = $this->makeDeleteCheckbox($identifier);
$fieldset->add($f);
return $fieldset;
}
/**
* Make checkbox for row deletion
*
* @param string $identifier
* @return InputfieldCheckbox
*/
protected function makeDeleteCheckbox($identifier) {
/* @var InputfieldCheckbox $f */
$f = $this->modules->get('InputfieldCheckbox');
$f->name = "delete_{$identifier}";
$f->label = $this->_('Delete');
return $f;
}
/**
* Make select for Page field
*
* @param int $number
* @param string $letter
* @param string $identifier
* @return InputfieldSelect
*/
protected function makePageFieldSelect($number, $letter, $identifier) {
$f_name = "pagefield_{$number}_{$identifier}";
/* @var InputfieldSelect $f */
$f = $this->modules->get('InputfieldSelect');
$f->name = $f_name;
$f->label = $this->_('Page field') . " $letter";
$f->required = true;
$f->columnWidth = 50;
$select_options = $this->wire()->fields->find("type=FieldtypePage");
$f->addOption(''); // first item blank
foreach($select_options as $select_option) {
$f->addOption($select_option->id, $select_option->label ?: $select_option->name);
}
$f->value = $this->$f_name;
return $f;
}
/**
* Install
*/
public function ___install() {
// Create one row code to get things started
$this->modules->saveModuleConfigData($this->className, ['codes' => uniqid() . '|']);
}
/**
* Upgrade
* @param $fromVersion
* @param $toVersion
*/
public function ___upgrade($fromVersion, $toVersion) {
// Convert integer version to semantic version
if(is_int($fromVersion)) $fromVersion = '0.0.' . $fromVersion;
// Upgrade from < v0.2.2
if(version_compare($fromVersion, '0.2.2', '<')) {
// Get existing config
$cfg = $this->modules->getModuleConfigData($this->className());
foreach($cfg as $key => $value) {
if(strpos($key, 'pagefield') !== 0) continue;
$f = $this->wire()->fields->get($value);
// Replace field name with field ID
$cfg[$key] = $f->id;
}
// Save config
$this->modules->saveModuleConfigData($this->className(), $cfg);
}
// Upgrade from < v0.0.5
if(version_compare($fromVersion, '0.0.5', '<')) {
// Get existing config
$cfg = $this->modules->getModuleConfigData($this->className());
// Organise into groups
$cfg_grouped = [];
foreach($cfg as $key => $value) {
if(strpos($key, 'pagefield') !== false) { // we only want the pagefield config fields
$num = filter_var($key, FILTER_SANITIZE_NUMBER_INT);
$cfg_grouped[$num][$key] = $value;
}
}
// Set new config
$new_cfg = [];
$new_cfg['codes'] = '';
foreach($cfg_grouped as $key => $value) {
if(is_array($value)) {
$unique = uniqid();
$new_cfg['codes'] .= "$unique|";
$i = 1;
foreach($value as $key2 => $value2) {
$new_key = substr($key2, 0, -2) . $i . "_$unique";
$new_cfg[$new_key] = $value2;
$i++;
}
}
}
$this->modules->saveModuleConfigData($this->className(), []); // clear config
$this->modules->saveModuleConfigData($this->className(), $new_cfg); // save new config
}
}
}