Subversion Repositories web.active

Rev

Details | Last modification | View Log

Rev Author Line No. Line
1 mjordaan 1
<?php namespace ProcessWire;
2
 
3
/**
4
* Process Menu Builder Module for ProcessWire
5
* This module enables you to create custom menus for your website using drag and drop in the ProcessWire Admin Panel
6
*
7
* @author Francis Otieno (Kongondo)
8
*
9
* https://github.com/kongondo/ProcessMenuBuilder
10
* Created 4 August 2013
11
* Major update in March 2015
12
*
13
* ProcessWire 3.x
14
* Copyright (C) 2016 by Ryan Cramer
15
*
16
* Licensed under GNU/GPL v2, see LICENSE.TXT
17
*
18
* http://www.processwire.com
19
*
20
*/
21
 
22
class ProcessMenuBuilder extends Process implements Module {
23
 
24
	/**
25
	 * Return information about this module (required)
26
	 *
27
	 * @access public
28
	 *
29
	 */
30
	public static function getModuleInfo() {
31
 
32
		// @User role needs 'menu-builder' permission
33
		// @$permission = 'menu-builder';
34
		// @Installs MarkupMenuBuilder
35
 
36
		return array(
37
			'title' => 'Menu Builder: Process',
38
			'summary' => 'Easy, drag and drop menu builder',
39
			'author' => 'Francis Otieno (Kongondo)',
40
			'version' => '0.2.7',
41
			'href' => 'http:// processwire.com/talk/topic/4451-module-menu-builder/',
42
			'singular' => true,
43
			'autoload' => false,
44
			'permission' => 'menu-builder',
45
			'installs' => 'MarkupMenuBuilder'
46
		);
47
 
48
	}
49
 
50
 
51
	const PAGE_NAME = 'menu-builder';
52
 
53
	/**
54
	 * Property to return this module's admin page (parent of all menus).
55
	 *
56
	 */
57
	protected $menusParent;
58
 
59
	/**
60
	 * Property to store include children setting (boolean).
61
	 *
62
	 */
63
	private $includeChildren;
64
 
65
	/**
66
	 * Property to store disable items setting (boolean).
67
	 *
68
	 */
69
	private $disableItems;
70
 
71
	/**
72
	 * string name of the cookie used to save limit of posts to show per page in posts dashboard.
73
	 *
74
	 */
75
	private $cookieName;
76
 
77
	/**
78
	 * int value of number of menus to show per dashboard page.
79
	 *
80
	 */
81
	private $showLimit;
82
 
83
 
84
	// other single menu properties
85
	private $menuItems;
86
	private $menuPages;
87
	private $menuSettings;
88
 
89
	// multilingual
90
	private $multilingual;// bool to check if in multilingual environment
91
	private $menuItemsLanguages;
92
 
93
 
94
	/**
95
	 * Initialise the module. This is an optional initialisation method called before any execute methods.
96
	 *
97
	 * Initialises various class properties ready for use throughout the class.
98
	 *
99
	 * @access public
100
	 *
101
	 */
102
	public function init() {
103
 
104
		$user = $this->wire('user');
105
 
106
		if ($this->wire('permissions')->get('menu-builder')->id && !$user->hasPermission('menu-builder'))
107
			 throw new WirePermissionException("You have no permission to use this module");
108
 
109
		$this->wire('modules')->get('JqueryWireTabs');
110
		$config = $this->wire('config');
111
		$config->scripts->add($this->config->urls->ProcessMenuBuilder . 'scripts/jquery.mjs.nestedSortable.js');
112
		$config->scripts->add($this->config->urls->ProcessMenuBuilder . 'scripts/jquery.asmselect-mb.js');
113
 
114
		$this->multilingual = $this->wire('languages') ? true : false;
115
 
116
		$this->menusParent = $this->wire('page');
117
 
118
		// cookie per user to save state of number of menus to display per pagination screen in execute()
119
		$this->cookieName = $user->id . '-menubuilder';
120
 
121
		// default number of menus to show in menu builder landing page if no custom limit set (via post/session cookie).
122
		$this->showLimit = 10;
123
 
124
		parent::init();
125
 
126
	}
127
 
128
	/* ######################### - MARKUP BUILDERS - ######################### */
129
 
130
	/**
131
	 * Displays a list of the menus.
132
	 *
133
	 * This function is executed when a menu with Menu Builder Process assigned is accessed.
134
	 *
135
	 * @access public
136
	 * @return string $form Form markup.
137
	 *
138
	 */
139
	public function ___execute() {
140
 
141
		$modules = $this->wire('modules');
142
		$post = $this->wire('input')->post;
143
 
144
		// CREATE A NEW FORM
145
		$form = $modules->get('InputfieldForm');
146
		$form->attr('id', 'menu-builder');
147
		$form->action = './';
148
		$form->method = 'post';
149
 
150
		// CREATE A NEW WRAPPER
151
		$w = new InputfieldWrapper;
152
 
153
		// quick create menu markup
154
		$w->add($this->buildQuickCreateMenuMarkup());
155
		// menus table/list markup
156
		$w->add($this->buildMenusTableMarkup());
157
		// actions markup
158
		if ($this->menusTotal !=0) $w->add($this->buildMenusActionsMarkup());
159
 
160
		// add to form for rendering
161
		$form->add($w);
162
 
163
		// send input->post values to the Method save();
164
		if($post->menus_action_btn || $post->menu_new_unpublished_btn || $post->menu_new_published_btn) $this->save($form);
165
 
166
		// render the final form
167
		return $form->render();
168
 
169
	}
170
 
171
	/**
172
	 * Renders a single menu for editing.
173
	 *
174
	 * Called when the URL is Menu Builders page URL + "/edit/"
175
	 * note: matches what is appended after ___execute below.
176
	 *
177
	 * @access public
178
	 * @return string $form Form markup.
179
	 *
180
	 */
181
	public function ___executeEdit() {
182
 
183
 
184
		$modules = $this->wire('modules');
185
		$post = $this->input->post;
186
 
187
		// get the menu (page) we are editing
188
		$menuID = (int) $this->wire('input')->get->id;
189
		$menu = $this->wire('pages')->get("id=$menuID, parent=$this->menusParent, include=all");// only get menu pages!
190
 
191
		$form = $modules->get('InputfieldForm');
192
 
193
		// if we found a valid menu page
194
		if($menu->id) {
195
 
196
			// menu settings
197
			$this->menuSettings = $menu->menu_settings ? json_decode($menu->menu_settings, true) : array();
198
			// fetch this menu's JSON string with menu pages properties (pages find selector and inputfield to use)
199
			$this->menuPages = $menu->menu_pages ? json_decode($menu->menu_pages, true) : array();
200
			// fetch this menu's JSON string with menu items properties
201
			$this->menuItems = $menu->menu_items ? json_decode($menu->menu_items, true) : array();
202
			// if multilingual, active MB languages
203
			$this->menuItemsLanguages = isset($this->menuPages['menu_items_languages']) ? $this->menuPages['menu_items_languages'] : null;
204
 
205
			##############################
206
 
207
			$this->nestedSortableConfigs();
208
			$this->menuConfigs();// @note: we check if user has right permission in the method itself
209
 
210
			// check if menu is published or not
211
			$menu->is(Page::statusUnpublished) ? $pubStatus = 1 : $pubStatus = '';
212
 
213
			// check if menu is locked for editing
214
			$menu->is(Page::statusLocked) ? $editStatus = 1 : $editStatus = '';
215
 
216
			$editStatusNote = $editStatus ? $this->_(' (locked)') : '';
217
 
218
			// add a breadcrumb that returns to our main page @todo - don't show non-superadmins breadcrumbs?
219
			$this->breadcrumbs->add(new Breadcrumb('../', $this->wire('page')->title));
220
			 // headline when editing a menu
221
			 // @todo: delete this old style since now we show warning message?
222
			//$this->headline(sprintf(__('Edit menu: %s'), $menu->title) . $editStatusNote);
223
			$this->headline(sprintf(__('Edit menu: %s'), $menu->title));
224
 
225
			$form->attr('id', 'MenuBuilderEdit');
226
			$form->action = './';
227
			$form->method = 'post';
228
 
229
			############################################ - prep for tabs - ############################################
230
 
231
			$menuPages = $this->menuPages;
232
 
233
			// set include children + disable items status + for users with right permission
234
			if(!empty($menuPages)) {
235
				// enable include children feature
236
				if(isset($menuPages['children']) && $this->wire('user')->hasPermission('menu-builder-include-children')) {
237
					$this->includeChildren = $menuPages['children'];
238
				}
239
				// enable 'enable/disable' items feature
240
				if(isset($menuPages['disable_items']) && $this->wire('user')->hasPermission('menu-builder-disable-items')) {
241
					$this->disableItems = $menuPages['disable_items'];
242
				}
243
			}
244
 
245
			############################################ - First Tab (build menu) - ############################################
246
			// @note: only shown for menu that are not locked
247
			if(!$menu->is(Page::statusLocked)) $form->add($this->editTabBuild());
248
			############################################ - Second Tab (menu items overview) - ####################################
249
			$form->add($this->editTabOverview());
250
			############################################ - Third Tab (menu settings) - ###########################################
251
			// @note: only shown for menu that are not locked
252
			if(!$menu->is(Page::statusLocked)) $form->add($this->editTabMenuSettings($menu, $pubStatus, $editStatus));
253
			############################################ - Fourth Tab (delete) - ############################################
254
			// @note: only shown for menu that are not locked
255
			if(!$menu->is(Page::statusLocked)) $form->add($this->editTabDelete($menu->id));
256
 
257
 
258
			/***************** Add input buttons to Fourth tab *****************/
259
 
260
			$m = $modules->get('InputfieldHidden');
261
			$m->attr('name', 'menu_id');
262
			$m->attr('value', $menuID);
263
			$form->add($m);
264
 
265
			// @note: only shown for menu that are not locked
266
			if(!$menu->is(Page::statusLocked)) {
267
				$m = $modules->get('InputfieldSubmit');
268
				$m->class .= ' head_button_clone';
269
				$m->attr('id+name', 'menu_save');
270
				$m->class .= " menu_save";// add a custom class to this submit button
271
				$m->attr('value', $this->_('Save'));
272
				$form->add($m);
273
 
274
				$m = $modules->get('InputfieldSubmit');
275
				$m->attr('id+name', 'menu_save_exit');
276
				$m->class .= " ui-priority-secondary";
277
				$m->class .= " menu_save";// add a custom class to this submit button
278
				$m->attr('value', $this->_('Save & Exit'));
279
				$form->add($m);
280
			}
281
 
282
			// show an exit button for locked menus
283
			else {
284
				$m = $modules->get('InputfieldSubmit');
285
				$m->attr('id+name', 'menu_locked_exit');
286
				$m->class .= " ui-priority-secondary";
287
				$m->class .= " menu_save";// add a custom class to this submit button
288
				$m->attr('value', $this->_('Exit'));
289
				$form->add($m);
290
 
291
 
292
				// show menu locked warning
293
				$this->warning($this->_('Menu Builder: This menu is locked for edits.'));
294
 
295
			}
296
 
297
 
298
 
299
			return $form->render();
300
 
301
		}// end if $menu
302
 
303
 
304
		############################################ - if input->post - ############################################
305
 
306
		// if saving menu
307
		elseif($post->menu_save || $post->menu_save_exit || $post->menu_delete) $this->save($form);
308
		// else invalid menu ID or no ID provided (e.g. /edit/) or exiting 'view' a locked menu
309
		else $this->wire('session')->redirect($this->wire('page')->url);// redirect to landing page
310
 
311
	}
312
 
313
	/**
314
	 * First tab contents for executeEdit()
315
	 *
316
	 * @access protected
317
	 * @return object $tab To render as markup.
318
	 *
319
	 */
320
	protected function editTabBuild() {
321
 
322
		$modules = $this->wire('modules');
323
		$menuPages = $this->menuPages;
324
 
325
		// First Tab - Drag & Drop + add menu items. Only show if a menu exists
326
 
327
		$tab = new InputfieldWrapper();
328
		$tab->attr('title', $this->_('Build Menu'));
329
		$id = $this->className() . 'Build';
330
		$tab->attr('id', $id);
331
		$tab->class .= " WireTab";
332
 
333
		$m = $modules->get('InputfieldMarkup');
334
		$m->label = $this->_('Add menu items');
335
		$m->description = '<span id="add_menu_items"><a href="#" id="add_page_menu_items">' . $this->_('Pages') . '</a> ';
336
		$m->description .= '<a href="#" id="add_custom_menu_items">' . $this->_('Custom') . '</a>';
337
		if($this->user->hasPermission('menu-builder-selector')) $m->description .= '<a href="#" id="add_selector_menu_items">' . $this->_('Selector') . '</a>';
338
		$m->description .= '</span>';
339
		$m->textFormat = Inputfield::textFormatNone;// make sure ProcessWire renders the HTML
340
 
341
		$tab->add($m);
342
 
343
		// if user specified (hence limited) the pages selectable for adding to this menu, we use that
344
		// if user has not specified pages to return, we find all pages (except admin pages [including trash]) but limit to 50.
345
		// @note: user can override the 50 pages limit by adding their own 'limit=n' in $menuPages['sel']
346
		$defaultSelector = "template!=admin, has_parent!=2, parent!=7, id!=27, limit=50";
347
 
348
		if(isset($menuPages['sel'])) $pagesSelector = $defaultSelector . ', ' . $menuPages['sel'];
349
		else $pagesSelector = $defaultSelector;
350
 
351
		$description = $this->_('Select Pages to add to your menu. Optionally enter a CSS ID and single/multiple Classes.');
352
 
353
		$extraNotes = $this->includeChildren ?
354
						$this->_("The setting 'Level' is for use in conjunction with 'include children' with respect to your menu, i.e. the selections 'Menu' or 'Both'. The default level (i.e. how 'deep' to fetch descendant children, granchildren, etc. is 1. If that is what you want then you do not have to enter a level.") :
355
							'';
356
 
357
		$menuPagesInput = 1;
358
 
359
		if (isset($menuPages['input']) && $menuPages['input'] == 2) {
360
 
361
			// modify PageAutoComplete output
362
			$this->wire('page')->addHookBefore("InputfieldPageAutocomplete::renderListItem", $this, "customAc");
363
 
364
			// if we are using page autocomplete inputfield to find pages for menu items selection
365
			$menuAddPageItems = $modules->get('InputfieldPageAutocomplete');
366
			$menuAddPageItems->set('findPagesSelector', $pagesSelector);
367
			$menuAddPageItems->notes = $this->_('Start typing to search for pages.' . "\n");
368
			$menuAddPageItems->notes .= $extraNotes;
369
 
370
			// we'll use this variable when saving PageAutocomplete values
371
			// especially important for non-superusers since the radio input 'menu_pages_select' will not be output for them
372
			$menuPagesInput = 2;
373
		}
374
 
375
		elseif (isset($menuPages['input']) && $menuPages['input'] == 3) {
376
 
377
			// modify PageListSelectMultiple output
378
			$this->wire('page')->addHookAfter("InputfieldPageListSelectMultiple::render", $this, "customPls");
379
 
380
			$menuAddPageItems = $modules->get('InputfieldPageListSelectMultiple');
381
			$menuAddPageItems->label = $this->_('Pages');
382
			//$menuAddPageItems->set('parent_id', 1300);// @todo: configurable?
383
 
384
			// see notes above about this variable
385
			$menuPagesInput = 3;
386
		}
387
 
388
		// else we default to AsmSelect inputfield
389
		else {
390
 
391
			$opts = $this->wire('pages')->find($pagesSelector);
392
			if(empty($opts)) {
393
				$this->error($this->_('Menu Builder: Your selector did not find any selectable pages for your menu! Confirm its validity.'));
394
				$description = $this->_('No pages were found to add to your menu. Rectify the specified error first.');
395
			}
396
 
397
			// modify AsmSelect output
398
			$this->wire('page')->addHookAfter("InputfieldAsmSelect::render", $this, "customAsm");
399
 
400
			$menuAddPageItems = $modules->get('InputfieldAsmSelect');
401
			$menuAddPageItems->notes = $extraNotes;
402
 
403
			foreach($opts as $opt) $menuAddPageItems->addOption($opt->id, $opt->title);
404
 
405
		}
406
 
407
		// Page Select to add menu items from pages
408
		$menuAddPageItems->label = $this->_('Pages');
409
		$menuAddPageItems->attr('name+id', 'item_addpages');
410
		$menuAddPageItems->description = $description;
411
 
412
		$tab->add($menuAddPageItems);// add page asmSelect/autocomplete/page list select multiple to markup
413
 
414
		// hidden field to store value if using PageAutocomplete to select menu items from pages
415
		$h = $modules->get('InputfieldHidden');
416
		$h->attr('name', 'menu_pages_input');
417
		$h->attr('value', $menuPagesInput);
418
 
419
		$tab->add($h);// add hidden field to markup
420
 
421
		// Add Custom menu items
422
		$t = $modules->get('MarkupAdminDataTable');
423
		$t->setEncodeEntities(false);
424
		$t->setSortable(false);
425
		$t->setClass('menu_add_custom_items_table');
426
 
427
		$t->headerRow(array(
428
			$this->_('Title'),
429
			$this->_('Link'),
430
			$this->_('CSS ID'),
431
			$this->_('CSS Class'),
432
			$this->_('New Tab'),
433
		));
434
 
435
		$n = $modules->get('InputfieldName');
436
		$n->required = true;
437
		$n->attr('name', 'new_item_custom_title[]');
438
		$n->attr('class', 'new_custom');
439
 
440
		$u = $modules->get('InputfieldURL');
441
		$u->attr('name', 'new_item_custom_url[]');
442
		$u->attr('class', 'new_custom');
443
 
444
		$n2 = $modules->get('InputfieldName');
445
		$n2->attr('name', 'new_css_itemid[]');
446
		$n2->attr('class', 'new_custom');
447
 
448
		$n3 = $modules->get('InputfieldName');
449
		$n3->attr('name', 'new_css_itemclass[]');
450
		$n3->attr('class', 'new_custom');
451
 
452
		$itemCustomNewTab = "<input type='checkbox' name='new_newtab[]' value='0' class='newtab'>";
453
		$itemCustomNewTabHidden = "<input type='hidden' name='new_newtab_hidden[]' value='0' class='newtabhidden'>";// force send a value for new tabs
454
 
455
		$t->row(array(
456
			$n->render(),
457
			$u->render(),
458
			$n2->render(),
459
			$n3->render(),
460
			$itemCustomNewTab . $itemCustomNewTabHidden,
461
			'<a href="#" class="remove_row"><i class="fa fa-trash"></i></a>',
462
		));
463
 
464
		$addRow = "<a class='addrow' href='#'>" . $this->_('add row') . "</a>";
465
 
466
		$m = $modules->get('InputfieldMarkup');
467
		$m->attr('id', 'item_addcustom');
468
		$m->label = $this->_('Custom links');
469
		$m->description = $this->_('Add custom menu items. Title and Link are required.');
470
		$m->attr('value', $addRow . $t->render());
471
 
472
		$tab->add($m);
473
 
474
		// Add menu items from pages returned by a selector
475
		// only add for users with the permission 'menu-builder-selector' (it means that permission has to be created if it doesn't exist)
476
		if ($this->wire('user')->hasPermission('menu-builder-selector')) {
477
 
478
			$tx = $modules->get('InputfieldText');
479
			$tx->label = $this->_('Pages search');
480
			$tx->attr('name+id', 'item_addselector');
481
			$tx->description = $this->_('Use a ProcessWire selector to find and add menu items.');
482
 
483
			$tab->add($tx);
484
 
485
		}
486
 
487
		// Drag and drop to sort + reorder menu items area
488
		$m = $modules->get('InputfieldMarkup');
489
		$m->label = $this->_('Drag & Drop');
490
		$m->skipLabel = Inputfield::skipLabelHeader;// we don't want a label displayed here
491
		$m->attr('id', 'dragdrop');
492
 
493
		$m->notes = $this->_('Add items to start building your menu. You can add both Pages (internal links) and Custom (external) links. Drag and drop each item in the order you wish.') . "\n";
494
 
495
		$m->notes .= $this->_('Advanced optional settings can be edited by clicking the "down-arrow" button or the menu item label.');
496
 
497
 
498
		// if menu populated, create nested list
499
		$markup = '<h4>' . $this->_('No items have been added to this menu yet') . '</h4>';
500
		if(!empty($this->menuItems)) {
501
			$markup = '<div id="menu_sortable_wrapper">' .
502
							'<a href="#" id="remove_menus">' . $this->_('Delete All') . '</a>' .
503
							$this->listMenu(0) .
504
						'</div>';
505
 
506
				// add hidden markup for extra labels for all page select methods
507
				$markup.= $this->buildExtraLabels();
508
				// add hidden markup for extra input for page autocomplete
509
				$markup.= $this->buildAsmExtraInputs();
510
		}
511
 
512
		$m->attr('value', $markup);
513
 
514
		$tab->add($m);
515
 
516
		return $tab;
517
 
518
	}
519
 
520
	/**
521
	 * Second tab contents for executeEdit().
522
	 *
523
	 * @access protected
524
	 * @return object $tab To render as markup.
525
	 *
526
	 */
527
	protected function editTabOverview() {
528
 
529
		$modules = $this->wire('modules');
530
		$menuItems = $this->menuItems;
531
 
532
		// Third Tab - Menu item properties overview [read only]. Only show if a menu exists
533
		$tab = new InputfieldWrapper();
534
		$tab->attr('title', $this->_('Items Overview'));
535
		$id = $this->className() . 'Overview';
536
		$tab->attr('id', $id);
537
		$tab->class .= ' WireTab';
538
 
539
		// we'll use this to wrap the table below
540
		$m = $modules->get('InputfieldMarkup');
541
		$m->label = $this->_('Menu items');
542
 
543
		$t = $modules->get('MarkupAdminDataTable');
544
		$t->setEncodeEntities(false);
545
		$t->setClass('menu_items_table no_disable');
546
 
547
		$t->headerRow(array(
548
			// $this->_('ID'),// id of item in menu; not PW page id!!!
549
			$this->_('Title'),// for PW pages, actual title saved. The title can also be edited in the add menu item settings
550
			$this->_('URL'),// path to PW pages + normal url for custom menu items
551
			$this->_('Parent'),// parent in this menu! NOT PW PAGE PARENT!
552
			$this->_('CSS ID'),
553
			$this->_('CSS Class'),
554
			$this->_('New Tab'),
555
			$this->_('Type'),// custom or PW page
556
 
557
		));
558
 
559
		// fetch menu items and display their properties in the overview table
560
		if(!empty($menuItems)) {
561
 
562
			foreach ($menuItems as $menu => $menuItem) {
563
 
564
				// if an internal PW page
565
				if (isset($menuItem['pages_id'])) {
566
					$itemURL = $this->wire('pages')->get($menuItem['pages_id'])->url;
567
					$itemType = 'Page';
568
				}
569
 
570
				// else it is a custom menu
571
				else {
572
					$itemURL = $menuItem['url'];
573
					$itemType = 'Custom';
574
				}
575
 
576
				// check if top tier menu item (has no parent) or below (has a parent)
577
				if (isset($menuItem['parent_id'])) $itemParent = $menuItems[$menuItem['parent_id']]['title'];
578
				else $itemParent = '';
579
 
580
				// does this menu item link open in a new window or not (i.e. target='_blank') - for custom menu items only
581
				$itemNewTab = isset($menuItem['newtab']) ? $this->_('Yes') : $this->_('No');
582
 
583
				$itemCSSID = isset($menuItem['css_itemid']) ? $menuItem['css_itemid'] :'';
584
				$itemCSSClass = isset($menuItem['css_itemclass']) ? $menuItem['css_itemclass'] : '';
585
 
586
				$t->row(array(
587
					$menuItem['title'],
588
					$itemURL,
589
					$itemParent,
590
					$itemCSSID,
591
					$itemCSSClass,
592
					$itemNewTab,
593
					$itemType)
594
				);
595
 
596
			}// end foreach
597
 
598
			$m->attr('value', $t->render());
599
 
600
		}// end if count $menuItems
601
 
602
		else {
603
			// give user feedback that no menu items have been added to this menu
604
			$m->description = '<h4>' . $this->_('No items have been added to this menu yet.') . '</h4>';
605
			$m->textFormat = Inputfield::textFormatNone;// make sure ProcessWire renders the HTML
606
		}
607
 
608
		$tab->add($m);
609
 
610
		return $tab;
611
 
612
	}
613
 
614
	/**
615
	 * Third tab contents for executeEdit().
616
	 *
617
	 * @access protected
618
	 * @param Page $menu The menu being edited.
619
	 * @param string $unpublished String to show whether the menu being edited is unpublished.
620
	 * @param string $locked String to show whether the menu being edited is locked.
621
	 * @return object $tab To render as markup.
622
	 *
623
	 */
624
	protected function editTabMenuSettings($menu, $unpublished = null, $locked = null) {
625
 
626
		$modules = $this->wire('modules');
627
		$user = $this->wire('user');
628
		$menuPages = $this->menuPages;
629
 
630
		// Third Tab - Settings
631
		$tab = new InputfieldWrapper();
632
		$tab->attr('title', $this->_('Settings'));
633
		$id = $this->className() . 'Settings';
634
		$tab->attr('id', $id);
635
		$tab->class .= ' WireTab';
636
 
637
		// menu title
638
		// @note: multi-lingual aware title field
639
		$f = $modules->get('InputfieldPageTitle');
640
		$f->attr('name', 'menu_title');
641
		$f->label = $this->_('Menu title');
642
		$f->useLanguages = true;
643
		$f->required = true;
644
		$f->attr('value', $menu->title);
645
 
646
		// different description if in multi-lingual setting
647
		if($this->multilingual) {
648
			$description = $this->_('A menu title is required for at least the default language.');
649
		}
650
		else $description = $this->_('A menu title is required.');
651
 
652
		$f->description = $description;
653
 
654
		$notes = ($unpublished || $locked) ? $this->_('Menu status: ') : '';
655
		if ($unpublished) $notes .= $this->_('Unpublished, ');
656
		if ($locked) $notes .= $this->_('Locked');
657
 
658
		$f->notes = rtrim($notes, ', ');
659
 
660
		// if in multilingual site, set respective languages description values where available
661
		if($this->multilingual) {
662
			foreach ($this->wire('languages') as $language) {
663
				// skip default language as already set above
664
				if($language->name == 'default') continue;
665
				$langTitle = $menu->getLanguageValue($language, 'title');
666
				// set title in the language
667
				$f->set("value$language->id", $langTitle);
668
			}
669
		}
670
 
671
		$tab->add($f);
672
 
673
		// display configurable backend menu settings for users with right permissions
674
		// options for nestedSortable + ProcessWire selector for pages selectable in $menuAddPageItems AsmSelect
675
 
676
		// if this user has permission to SPECIFY pages selectable as menu items in AsmSelect and PageAutocomplete
677
		if($user->hasPermission('menu-builder-selectable')) {
678
 
679
			// if selector to find pages to add to menu specified
680
			$selectorValue = isset($menuPages['sel']) ? $menuPages['sel'] : '';
681
 
682
			$tx = $modules->get('InputfieldText');
683
			$tx->attr('name', 'menu_pages');
684
			$tx->label = $this->_('Pages selectable in menu');
685
			$tx->attr('value', $selectorValue);
686
			$tx->description = $this->_('Optionally, you can specify a valid ProcessWire selector to limit the Pages that can be added to this menu (see Build Menu Tab). Otherwise, all valid pages will be available to add to the menu. By default, returned pages are limited to 50. You can override this by setting your own limit here. NOTE: This feature only works with Asm Select and Page Auto Complete.');
687
			$tx->notes =  $this->_('Example: parent=/products/, template=product, sort=title');
688
 
689
			$tab->add($tx);
690
 
691
		}// end if user has permission menu-builder-selectable
692
 
693
		// if user has permission to allow changing of page field type used to select pages to add as menu items [AsmSelect vs PageAutocomplete]
694
		if($user->hasPermission('menu-builder-page-field')) {
695
 
696
			// only 'PageAutocomplete' and PageListSelectMultiple options are saved in the field menu_pages (JSON): 'input'=> 2 || 3
697
			// else we assume default 'input' => 1 (AsmSelect)
698
			$pageSel = isset($menuPages['input']) && (($menuPages['input'] == 2) || ($menuPages['input'] == 3)) ? $menuPages['input'] : 1;
699
 
700
			// radios: page inputfield selection
701
			$r = new InputfieldRadios();
702
			$r->attr('id+name', 'menu_pages_select');
703
			$r->label =  $this->_('Choose a method for selecting pages to add to your menu');
704
			$r->notes = $this->_('If you will have a large selection of pages to choose from, you may want to use Page Auto Complete.');
705
 
706
			$radioOptions = array (
707
				1 => $this->_('Asm Select'),
708
				2 => $this->_('Page Auto Complete'),
709
				3 => $this->_('Page List Select Multiple'),
710
		 	);
711
 
712
			$r->addOptions($radioOptions);
713
			$r->value = $pageSel;
714
 
715
			$tab->add($r);
716
 
717
		}// end if user has permision menu-builder-page-field
718
 
719
		// if user can change and use allow markup/HTML setting
720
		if($user->hasPermission('menu-builder-markup')) {
721
 
722
			// only 'Allow Markup' (Yes) option is saved in the field menu_pages (JSON): 'markup'=> 2. Else we assume default 'markup' => 1 (No)
723
			$allowMarkup = isset($menuPages['markup']) ? 1 : 2;
724
 
725
			// radios: allow markup in menu item title/label
726
			$r = new InputfieldRadios();
727
			$r->attr('id+name', 'menu_item_title_markup');
728
			$r->label =  $this->_('Allow HTML in menu items title');
729
			$r->notes = $this->_('Example: <span>Home</span>. If you allow this, the HTML will be run through HTML purifier before saving. Take care not to input malformed HTML.');
730
 
731
			$radioOptions = array (
732
				1 => $this->_('Yes'),
733
				2 => $this->_('No'),
734
		 	);
735
 
736
			$r->addOptions($radioOptions);
737
			$r->value = $allowMarkup;
738
 
739
			$tab->add($r);
740
 
741
		}// end if user can change and use allow markup/HTML setting
742
 
743
		// if user can edit and use 'include children' feature
744
		if($user->hasPermission('menu-builder-include-children')) {
745
 
746
			// only 'Allow Include Children' (Yes) option is saved in the field menu_pages (JSON): 'markup'=> 2. Else we assume default 'markup' => 1 (No)
747
			$includeChildren = isset($menuPages['children']) ? 1 : 2;
748
 
749
			// radios: enable include children feature
750
			$r = new InputfieldRadios();
751
			$r->attr('id+name', 'menu_item_include_children');
752
			$r->label =  $this->_('Use include children feature');
753
			$r->notes = $this->_('This feature allows you to designate menu items that can have their natural ProcessWire pages descendants included in the menu/breadcrumbs output in the frontend without actually including those pages here in Menu Builder. Be careful when using the feature as you could potentially output a very large amount of menu items than intended.');
754
 
755
			$radioOptions = array (
756
				1 => $this->_('Yes'),
757
				2 => $this->_('No'),
758
		 	);
759
 
760
			$r->addOptions($radioOptions);
761
			$r->value = $includeChildren;
762
 
763
			$tab->add($r);
764
 
765
		}// end if user can change and use allow markup/HTML setting
766
 
767
		// if user can edit and use 'disable menu items' feature
768
		if($user->hasPermission('menu-builder-disable-items')) {
769
 
770
			// only 'Enable Disable Item' options
771
			$disableItems = isset($menuPages['disable_items']) ? 1 : 2;
772
 
773
			$r = new InputfieldRadios();
774
			$r->attr('id+name', 'menu_item_disable_items');
775
			$r->label =  $this->_('Use enable/disable menu items feature');
776
			$r->notes = $this->_('Allows you to set some menu items as disabled. If an item is disabled, the item together will all of its descendants will be set as disabled after you save the menu settings. Disabled items will not be output when the menu is viewed in the frontend.');
777
 
778
			$radioOptions = array (
779
				1 => $this->_('Yes'),
780
				2 => $this->_('No'),
781
		 	);
782
 
783
			$r->addOptions($radioOptions);
784
			$r->value = $disableItems;
785
 
786
			$tab->add($r);
787
 
788
		}// end if user can edit and use 'disable menu items' feature
789
 
790
		// if user can use 'multi-lingual menu items' feature
791
		if($user->hasPermission('menu-builder-multi-lingual-items') && !is_null($user->language)) {
792
 
793
			// active languages select checkboxes
794
			$menuItemsLanguages = isset($menuPages['menu_items_languages']) ? $menuPages['menu_items_languages'] : array();
795
			$languages = $this->getLanguages();// @note: grabs all available languages
796
 
797
			// active languages select checkboxes
798
			$c = $modules->get('InputfieldCheckboxes');
799
			$c->label = $this->_('Other active languages for this menu');
800
			$c->attr('id+name', 'menu_items_languages');
801
			$c->attr('value', $menuItemsLanguages);
802
			$c->description = $this->_('Optionally, you can choose other languages other than the default for which you want to save values for your menu items.');
803
			#$c->addOptions($languageOptions);
804
			foreach ($languages as $langName => $langTitle) {
805
				if($langName == 'default') continue;
806
				$c->addOption($langName, $langTitle);
807
			}
808
			$c->notes = $this->_('When building the menu, you will see tabs for other active languages selected here to input titles and URLs. If a title or URL is left blank, in the frontend, the respective values for the default language will be used instead.');
809
 
810
			$tab->add($c);
811
 
812
 
813
		}// user can use 'multi-lingual menu items' feature
814
 
815
 
816
		// if user has permission to allow editing of nestedSortable settings
817
		if($user->hasPermission('menu-builder-settings')) {
818
 
819
			$t = $modules->get('MarkupAdminDataTable');
820
			$t->setEncodeEntities(false);
821
			$t->setSortable(false);
822
			$t->setClass('menu_items_table');
823
 
824
			$t->headerRow(array(
825
				$this->_('Name'),
826
				$this->_('Default'),// for PW pages, actual title saved. The title can also be edited in the add menu item settings
827
				$this->_('Setting'),// path to PW pages + normal url for custom menu items
828
				$this->_('Notes'),// parent in this menu! NOT PW PAGE PARENT!
829
			));
830
 
831
			// advanced/optional settings for nestedSortable
832
			$mergedMenuSettings = $this->nestedSortableMenuSettings();
833
 
834
			foreach ($mergedMenuSettings as $key => $value) {
835
				if($key == 'includeChildren') continue;// setting not for nestedSortable
836
				$t->row(array(
837
					$key,// name
838
					$value['default'],// default value
839
					"<input type='text' name='menu_settings[" . $key . "]' value='" . $value['setting'] . "'>",// setting - saved in menu_settings as JSON
840
					$value['notes'],
841
				));
842
 
843
			}// end foreach $menuSettings
844
 
845
			$m = $modules->get('InputfieldMarkup');
846
			$m->attr('id', 'menu_settings');
847
			$m->label = $this->_('Menu settings');
848
			$m->textFormat = Inputfield::textFormatNone;// make sure ProcessWire renders the HTML
849
			$m->description = $this->_('These are optional settings for') .  ' <a href="https:// github.com/ilikenwf/nestedSortable" target="_blank">nestedSortable</a> ' .
850
			$this->_('(the Drag and Drop menu functionality in Build Menu Tab).');
851
			$m->notes = $this->_('Note: These settings do not affect how your menu is displayed in the frontend.');
852
			$m->collapsed = Inputfield::collapsedYes;
853
			$m->attr('value', $t->render());
854
 
855
			$tab->add($m);
856
 
857
		}// end if user has permission to edit nestedSortable settings
858
 
859
		return $tab;
860
 
861
	}
862
 
863
	/**
864
	 * Fourth tab contents for executeEdit()
865
	 *
866
	 * @access protected
867
	 * @param integer $menuID ID of the menu being edited
868
	 * @return object $tab To render as markup.
869
	 *
870
	 */
871
	protected function editTabDelete($menuID) {
872
 
873
		$modules = $this->wire('modules');
874
 
875
		// Fourth Tab - Delete Menu. Only show if a menu exists
876
 
877
		$tab = new InputfieldWrapper();
878
		$tab->attr('title', $this->_('Delete'));
879
		$id = $this->className() . 'Delete';
880
		$tab->attr('id', $id);
881
		$tab->class .= " WireTab";
882
 
883
		$f = $modules->get('InputfieldCheckbox');
884
		$f->attr('id+name', 'menu_delete_confirm');
885
		$f->attr('value', $menuID);
886
		$f->icon = 'trash-o';
887
		$f->label = $this->_('Move to Trash');
888
		$f->description = $this->_('Check the box to confirm you want to do this.');
889
		$f->label2 = $this->_('Confirm');
890
		$tab->add($f);
891
 
892
		$f = $modules->get('InputfieldButton');
893
		$f->attr('id+name', 'menu_delete');
894
		$f->value = $this->_('Move to Trash');
895
		$tab->append($f);
896
 
897
		return $tab;
898
 
899
	}
900
 
901
	/**
902
	 * Displays a nested list (menu items) of a single menu.
903
	 *
904
	 * This is a recursive function to display list of menu items.
905
	 * Also displays each menu item's settings.
906
	 *
907
	 * @access private
908
	 * @param integer $parent ID of menu items.
909
	 * @param integer $first Helper variable to designate first menu item. Ensures CSS Class 'sortable' is output only once.
910
	 * @return string $out Menu items markup.
911
	 *
912
	 */
913
	private function listMenu($parent = 0, $first = 0) {
914
 
915
		$menuID = (int) $this->wire('input')->get->id;
916
 
917
		if($menuID) {
918
 
919
			/*
920
				INPUTS
921
 
922
					- id: item id of the menu item in relation to the menu (not same as pages_id!)
923
					- title: the menu item title as saved in Build Menu (note: even PW native page->title can be customised)
924
					- parent_id: the parent of this menu item in relation to the menu (note: does not have to reflect PW tree!; top tier items have parent_id = 0)
925
					- url: the url of the menu item (if PW, use native $page->url; if custom use provided url)
926
					- css_itemid: this menu item's CSS ID (optional)
927
					- css_itemclass: this menu items's CSS Class (optional)
928
					- pages_id: for PW pages items = $page->id; for custom menu items = 0 (note: this is different from id!)
929
					- optional include children feature
930
					- opitional disable menu items feature
931
 
932
			 */
933
 
934
 
935
			$out = '';
936
 
937
			$has_child = false;
938
 
939
			// $id is = id; $item = arrays of title, url, newtab, etc
940
			foreach ($this->menuItems as $id => $item) {
941
 
942
				## - MENU ITEM PROPERTIES - ##
943
 
944
				// set on the fly properties
945
				$this->itemID = $id;
946
 
947
				$this->itemTitle = $item['title'];
948
				$this->itemTitle2 = $this->wire('sanitizer')->entities($this->itemTitle);// for value of title input
949
				$this->itemURL = isset($item['url']) ? $item['url'] : '';
950
 
951
				// if multilingual, also set language specific titles and urls
952
				// @note: format is $this->itemTitle_de; $this->itemURL_de; $this->itemTitle2_de, etc...
953
				if(!is_null($this->menuItemsLanguages)) $this->setLanguageTitlesAndURLs($item);
954
 
955
				// items without parent ids are top level items
956
				// we give them an ID of 0 for display purposes (we won't save the value [see wireEncodeJSON()])
957
				$this->itemParentID = isset($item['parent_id']) ? $item['parent_id'] : 0;
958
				$this->cssItemID = isset($item['css_itemid']) ? $item['css_itemid'] : '';
959
				$this->cssItemClass = isset($item['css_itemclass']) ? $item['css_itemclass'] : '';
960
				$this->itemPagesID = isset($item['pages_id']) ? $item['pages_id'] : 0;// only PW pages will have a pages_id > 0 (equal to their PW page->id)
961
				$this->newTab = isset($item['newtab']) ? $item['newtab'] : 0;
962
				$this->itemIncludeChildren = isset($item['include_children']) ? $item['include_children'] : '';
963
				$this->itemMenuMaxLevel = isset($item['m_max_level']) ? $item['m_max_level'] : '';
964
				$this->disabledItem = isset($item['disabled_item']) ? $item['disabled_item'] : '';
965
 
966
				// custom menu items
967
				if(!$this->itemPagesID) {
968
					$this->itemType = $this->_('Custom');
969
					$this->readOnly = '';
970
				}
971
				// pw page menu items
972
				else {
973
					$this->itemType = $this->_('Page');
974
					$this->readOnly = ' readonly';
975
					$this->itemURL = $this->wire('pages')->get($this->itemPagesID)->path;
976
				}
977
 
978
				## - BUILD MENU - ##
979
 
980
				######################### item is a parent #########################
981
				// if this menu item is a parent; create the inner-items/child-menu-items
982
				if ($this->itemParentID == $parent) {
983
					// if this is the first child output '<ol>' with the class 'sortable'
984
					if ($has_child === false) {
985
						$has_child = true;// This is a parent
986
						if ($first == 0){
987
							$out .= "<ol id='sortable_main' class='sortable'>\n";
988
							$first = 1;
989
						}
990
						else $out .= "\n<ol>\n";
991
					}
992
 
993
					######################### menu item drag n drop handle #########################
994
					$out .= $this->buildMenuItemDragDropHandleMarkup();
995
					######################### item settings #########################
996
					$out .=  $this->buildMenuItemSettingsPanel();
997
					######################### generate sub-menu items [recursion] #########################
998
					// call function again to generate nested list for sub-menu items belonging to this menu item.
999
					$out .= $this->listMenu($id, $first);
1000
					// close the <li>
1001
					$out .= "</li>\n";
1002
				}// end if parent
1003
 
1004
			}// end foreach $this->menuItems as $id => $item
1005
 
1006
			if ($has_child === true) $out .= "</ol>\n";
1007
 
1008
			return $out;
1009
 
1010
		}// end if menuID
1011
 
1012
	}
1013
 
1014
	/**
1015
	 * Builds panel for quick create menus.
1016
	 *
1017
	 * @access private
1018
	 * @return object $m To render as InputfieldMarkup.
1019
	 *
1020
	 */
1021
	private function buildQuickCreateMenuMarkup() {
1022
 
1023
		$modules = $this->wire('modules');
1024
 
1025
		// markup module
1026
		$m = $modules->get('InputfieldMarkup');
1027
		$m->label = $this->_('Add new menu');
1028
		//$m->description = $this->_('A title is required.');
1029
		$m->collapsed = Inputfield::collapsedYes;
1030
 
1031
		// @note: multi-lingual aware title field
1032
		$f = $modules->get('InputfieldPageTitle');
1033
		$f->attr('name', 'menus_add_title');
1034
		$f->label = $this->_('Title');
1035
		$f->useLanguages = true;
1036
 
1037
		// different description if in multi-lingual setting
1038
		if($this->multilingual) {
1039
			$description = $this->_('A menu title is required for at least the default language.');
1040
		}
1041
 
1042
		else $description = $this->_('A menu title is required.');
1043
 
1044
		$f->description = $description;
1045
 
1046
		$m->add($f);
1047
 
1048
		// submit button to save quick menus create [save unpublished!]
1049
		$f = $modules->get('InputfieldSubmit');
1050
		$f->attr('id+name', 'menu_new_unpublished_btn');
1051
		$f->attr('value', $this->_('Save Unpublished'));
1052
		$f->class .= " menu_new_unpublished";// add a custom class to this submit button
1053
 
1054
		$m->add($f);
1055
 
1056
		// submit button to save AND publish quick menus create
1057
		$f = $modules->get('InputfieldSubmit');
1058
		$f->attr('id+name', 'menu_new_published_btn');
1059
		$f->attr('value', $this->_('Publish'));
1060
		$f->class .= " menu_new_publish";// add a custom class to this submit button
1061
 
1062
		$m->add($f);
1063
 
1064
		return $m;
1065
 
1066
	}
1067
 
1068
	/**
1069
	 * Builds panel showing tabular list of menus.
1070
	 *
1071
	 * @access private
1072
	 * @return object $m To render as InputfieldMarkup.
1073
	 *
1074
	 */
1075
	private function buildMenusTableMarkup() {
1076
 
1077
		// Determine number of menus to show per page in menus tab. Default = 10 {see $this->showLimit}
1078
		$this->setShowLimit();
1079
 
1080
		$table = '';
1081
		$modules = $this->wire('modules');
1082
		$m = $modules->get('InputfieldMarkup');
1083
 
1084
		// grab a limited number of menus to show in menus tab. Limit is determined in $this->setShowLimit() above
1085
		$menus = $this->menusParent->children("include=all, sort=title, limit={$this->showLimit}");
1086
		if (!empty($menus)) 	$table = $this->buildTable($menus);
1087
		// display a headline indicating quantities
1088
		$m->description = $this->buildMenusCountHeadline($menus);
1089
		// pagination
1090
		$pagination = $this->buildPagination($menus);
1091
		// add to markup
1092
		$m->attr('value', $pagination . $table . $pagination);// wrap our table with pagination
1093
		$m->textFormat = Inputfield::textFormatNone;// make sure ProcessWire renders the HTML
1094
 
1095
		return $m;
1096
 
1097
	}
1098
 
1099
	/**
1100
	 * Builds selects for limiting number of menus to show per tabular list.
1101
	 *
1102
	 * @access private
1103
	 * @return string $out Markup of selects.
1104
	 *
1105
	 */
1106
	private function buildLimitSelect() {
1107
		$out = '<span class="limit-select">' . $this->_('Show ') . '<select id="limit" name="show_limit">';
1108
		$limits = array( '', 5, 10, 15, 25, 50, 75, 100);
1109
		foreach ($limits as $limit) {
1110
					$out .='<option value="' . $limit . '"' . ($this->showLimit == $limit ? 'selected="selected"':'') . '>' .
1111
								$limit .
1112
							'</option>';
1113
		}
1114
		$out .= '</select>'. $this->_(' Items') . '</span>';
1115
		return $out;
1116
	}
1117
 
1118
	/**
1119
	 * Builds pagination for tabular list of menus.
1120
	 *
1121
	 * @access private
1122
	 * @return string $out Markup of pagination.
1123
	 *
1124
	 */
1125
	private function buildPagination($menus) {
1126
		$currentUrl = $this->wire('page')->url . $this->wire('input')->urlSegmentsStr."/";// get the url segment string.
1127
		$out = $menus->renderPager(array('baseUrl' => $currentUrl));// just foolproofing
1128
		return $out;
1129
	}
1130
 
1131
	/**
1132
	 * Builds headline for tabular list of menus.
1133
	 *
1134
	 * Headline shows number of items per paginated view.
1135
	 *
1136
	 * @access private
1137
	 * @param PageArray $menus Menu items that will be show in tabular list.
1138
	 * @return string $out Markup of headline for tabular list of menus.
1139
	 *
1140
	 */
1141
	private function buildMenusCountHeadline($menus) {
1142
 
1143
		// display a headline indicating quantities. We'll add this to menus tab
1144
		$start = $menus->getStart()+1;
1145
		$end = $start + count($menus)-1;
1146
		$total = $this->menusTotal = $menus->getTotal();
1147
 
1148
		if($total) {
1149
			$out = '<h4>' . sprintf(__('Menus %1$d to %2$d of %3$d'), $start, $end, $total) . '</h4>';
1150
			$out .= $this->_('Click on a title to edit the menu.') . $this->buildLimitSelect();
1151
		}
1152
 
1153
		else $out = $this->_('No menus found.');
1154
 
1155
		return $out;
1156
 
1157
	}
1158
 
1159
	/**
1160
	 * Builds the actual table that shows list of menus.
1161
	 *
1162
	 * @access private
1163
	 * @param PageArray $menus Menu items to display in the table.
1164
	 * @return string $out Markup of table.
1165
	 *
1166
	 */
1167
	private function buildTable($menus) {
1168
 
1169
		$modules = $this->wire('modules');
1170
 
1171
		// CREATE A NEW TABLE: for menus
1172
		$t = $modules->get('MarkupAdminDataTable');
1173
		$t->setEncodeEntities(false);
1174
		$t->setClass('menus_table');
1175
 
1176
		// set header rows
1177
		$t->headerRow(array(
1178
			'<input type="checkbox" class="toggle_all">',
1179
			$this->_('Title'),
1180
			$this->_('Menu Items'),
1181
			$this->_('Published'),
1182
			$this->_('Locked'),
1183
			$this->_('Modified'),
1184
		));
1185
 
1186
		foreach ($menus as $menu) {
1187
 
1188
			// count number of menu items in each menu
1189
			$menuItemsJSON = $menu->menu_items;
1190
			$menuItemsArray = json_decode($menuItemsJSON, true);
1191
			$menuItemsCnt = !empty($menuItemsArray) ? count($menuItemsArray): 0;
1192
			// check if menu is published or not
1193
			$menu->is(Page::statusUnpublished) ? $pubStatus = '<span class="unpublished">' . $this->_('No') . '</span>' : $pubStatus = $this->_('Yes');
1194
			// check if menu is locked for editing
1195
			$menu->is(Page::statusLocked) ? $editStatus = '<span class="locked">' . $this->_('Yes') . '</span>' : $editStatus = $this->_('No');
1196
			$modified = wireRelativeTimeStr($menu->modified);
1197
 
1198
			// set table rows
1199
			$menusTable = array(
1200
				// @note: disabled sorting on this checkbox in .js file
1201
				'<input type="checkbox" name="menus_action[]" value="' . $menu->id . '" class="toggle">',
1202
				'<a href="' . $this->menusParent->url . 'edit/?id=' . $menu->id . '">' . $menu->title . '</a>',
1203
				$menuItemsCnt,
1204
				$pubStatus,// menu published status
1205
				$editStatus,// menu locked status
1206
				$modified,// last modified status
1207
			);
1208
 
1209
			// render the table rows with variables set above
1210
			$t->row($menusTable);
1211
 
1212
		}// end foreach $menus as $menu
1213
 
1214
		$out = $t->render();
1215
 
1216
		return $out;
1217
 
1218
	}
1219
 
1220
	/**
1221
	 * Builds panel for actions for menu items bulk editing.
1222
	 *
1223
	 * @access private
1224
	 * @return object $m To render as InputfieldMarkup.
1225
	 *
1226
	 */
1227
	private function buildMenusActionsMarkup() {
1228
 
1229
		$modules = $this->wire('modules');
1230
		$user = $this->wire('user');
1231
		$permissions = $this->wire('permissions');
1232
 
1233
		// the menus bulk actions panel
1234
		$actions = array(
1235
			'publish' => $this->_('Publish'),
1236
			'unpublish' => $this->_('Unpublish'),
1237
			'lock' => $this->_('Lock'),
1238
			'unlock' => $this->_('Unlock'),
1239
			'trash' => $this->_('Trash'),
1240
			'delete' => $this->_('Delete'),
1241
		);
1242
 
1243
		// check for Menu Builder 'lock' and 'delete' permissions
1244
		// if they exist and user doesn't have these permissions, remove the actions
1245
		if ($permissions->get('menu-builder-lock')->id && !$user->hasPermission('menu-builder-lock')) {
1246
			unset($actions['lock']);
1247
			unset($actions['unlock']);
1248
		}
1249
		if ($permissions->get('menu-builder-delete')->id && !$user->hasPermission('menu-builder-delete')) {
1250
			unset($actions['trash']);
1251
			unset($actions['delete']);
1252
		}
1253
 
1254
		$m = $modules->get('InputfieldMarkup');
1255
		$m->label = $this->_('Actions');
1256
		$m->collapsed = 1;
1257
		$m->description = $this->_('Choose an Action to be applied to the selected menus.');
1258
 
1259
		// input select
1260
		$f = $modules->get('InputfieldSelect');
1261
		$f->label = $this->_('Action');
1262
		$f->attr('name+id', 'menus_action_select');
1263
		$f->addOptions($actions);
1264
 
1265
		$m->add($f);
1266
 
1267
		// apply button
1268
		$f = $modules->get('InputfieldSubmit');
1269
		$f->attr('id+name', 'menus_action_btn');
1270
		$f->class .= " posts_action";// add a custom class to this submit button
1271
		$f->attr('value', $this->_('Apply'));
1272
 
1273
		$m->add($f);
1274
 
1275
		return $m;
1276
 
1277
	}
1278
 
1279
	/**
1280
	 * Builds handle for drag and drop of a menu item for use in singe menu edit.
1281
	 *
1282
	 * @access private
1283
	 * @return string $out.
1284
	 *
1285
	 */
1286
	private function buildMenuItemDragDropHandleMarkup() {
1287
 
1288
		$id = $this->itemID;
1289
		$disabledClass = $this->disabledItem ? ' menu_item_disabled' : '';
1290
 
1291
		$out =	'<li id="item_' . $id . '" class="menu_item">' .
1292
					'<div class="handle">' .
1293
						'<a href="#" data-id="' . $id . '" class="item_expand_settings">' .
1294
							'<i data-id="' . $id . '" class="fa fa-caret-down"></i>' .
1295
						'</a>' .
1296
						'<span class="item_title_main' . $disabledClass . '" data-id="' . $id . '">' . $this->itemTitle . '</span>' .
1297
						'<span class="item_type_wrapper">' .
1298
							'<span class="item_type">' . $this->itemType . '</span>' .
1299
							'<a href="#" class="remove_menu"><i class="fa fa-trash"></i></a>' .
1300
						'</span>' .
1301
					'</div>' .
1302
					"\n";
1303
 
1304
		return $out;
1305
 
1306
	}
1307
 
1308
	/**
1309
	 * Builds panel for a menu item's settings.
1310
	 *
1311
	 * @access private
1312
	 * @return string $out Markup of settings panel.
1313
	 *
1314
	 */
1315
	private function buildMenuItemSettingsPanel() {
1316
 
1317
		$out = '';
1318
 
1319
		// build title and url inputs in the context of single or multi-lingual sites
1320
		// also  for use in either context
1321
		if(is_null($this->menuItemsLanguages)) $out .= $this->buildSingleLanguageTitleURLMarkup();
1322
		else $out .= $this->buildMultiLanguageTitleURLMarkup();
1323
 
1324
		## - OTHER INPUTS - ##
1325
		$out .='<div class="menu_edit_item_other_wrapper">';
1326
 
1327
		######################### add CSS ID and CSS Classes inputs #########################
1328
		$out .= $this->buildCSSMarkup();
1329
		######################### include children markup #########################
1330
		if($this->includeChildren == 1 && $this->itemPagesID != 0 && $this->itemPagesID != 1) $out .= $this->buildMenuItemIncludeChildrenMarkup();
1331
		######################### item disabled markup #########################
1332
		if($this->disableItems == 1) $out .= $this->buildMenuItemDisabledMarkup();
1333
		######################### custom menu item new tab markup #########################
1334
		if(0 == $this->itemPagesID) $out .= $this->buildMenuItemNewTabMarkup();
1335
		######################### item hidden inputs markup #########################
1336
		$out .= $this->buildMenuItemHiddenInputs();
1337
 
1338
		$out .='</div>';// end div.menu_edit_item_other_wrapper
1339
		## - END OTHER INPOUTS - ##
1340
 
1341
		// wrap it all up
1342
		$out = '<div id="menu_edit' . $this->itemID . '" class="settings">' . $out . '</div>' .
1343
				"\n";
1344
 
1345
		return $out;
1346
 
1347
	}
1348
 
1349
	/**
1350
	 * Builds title and URL inputs for a menu item's settings.
1351
	 *
1352
	 * This is for use in a non-multi-lingual setup.
1353
	 *
1354
	 * @access private
1355
	 * @return string $out Markup of title and URL inputs.
1356
	 *
1357
	 */
1358
	private function buildSingleLanguageTitleURLMarkup() {
1359
 
1360
		$out = '';
1361
 
1362
		$id = $this->itemID;
1363
		$labelsAndInputs = $this->getMenuSettingsPanelTitleURLInputs();
1364
 
1365
		foreach ($labelsAndInputs as $key => $value) {
1366
			$r = $key == 'item_url' ? $this->readOnly : '';// read only
1367
			$out .= '<label for="' . $key . $id . '">' . $value[0] . '</label>' .
1368
					'<input type="text" value="' . $value[1] . '" name="' . $key .'[' . $id . ']" class="menu_settings' . $r . '" id="' . $key . $id . '"' . $r . '>';
1369
		}
1370
 
1371
		return $out;
1372
 
1373
	}
1374
 
1375
	/**
1376
	 * Builds title and URL inputs for a menu item's settings.
1377
	 *
1378
	 * This is for use in a multi-lingual setup.
1379
	 *
1380
	 * @access private
1381
	 * @return string $out Markup of title and URL inputs.
1382
	 *
1383
	 */
1384
	private function buildMultiLanguageTitleURLMarkup() {
1385
 
1386
		$out = '';
1387
		$id = $this->itemID;
1388
		$r = $this->readOnly;
1389
		$languageSelector = '';// for language selector '<span>'s
1390
		$languageInputs = '';// fro language title and url <input>s
1391
 
1392
		// get MB active languages (minus default)
1393
		foreach($this->getLanguages(1) as $langName => $langTitle) {// @note: 1 means skip non-active languages
1394
			if($langName == 'default') {
1395
				$suffix = '';
1396
				$title2 = $this->itemTitle2;
1397
				$url = $this->itemURL;
1398
			}
1399
 
1400
			else  {
1401
				$suffix = '_' . $langName;
1402
				$title2 = $this->{"itemTitle2_{$langName}"};
1403
				$url = $this->{"itemURL_{$langName}"};
1404
			}
1405
 
1406
			// language selector
1407
			$active = $this->wire('user')->language->name == $langName ? ' menu_language_active' : ''; // @note: active language
1408
			$languageSelector .=
1409
				'<span class="menu_language_selector' . $active . '" data-language="menu_edit' . $suffix . $id . '">' .
1410
					$langTitle .
1411
				'</span>';
1412
 
1413
			// wrapper for individual language inputs
1414
			$languageInputs .= '<div id="menu_edit' . $suffix . $id . '" class="menu_language' . $active . '">';
1415
 
1416
			// item titles
1417
			$languageInputs .=
1418
				'<label for="item_title' . $suffix . $id . '">' . $this->_('Title') . '</label>' .
1419
				'<input type="text" value="' . $title2 . '" name="item_title' . $suffix .'[' . $id . ']" class="menu_settings" id="item_title' . $suffix . $id . '">';
1420
			// item urls
1421
			$languageInputs .=
1422
				'<label for="item_url' . $suffix . $id . '">' . $this->_('URL') . '</label>' .
1423
				'<input type="text" value="' . $url . '" name="item_url' . $suffix .'[' . $id . ']" class="menu_settings' . $r . '" id="item_url' . $suffix . $id . '"' . $r . '>';
1424
 
1425
			$languageInputs .= '</div>';// end div.menu_language
1426
 
1427
 
1428
		}// end foreach
1429
 
1430
		// language selector <span>s wrapper
1431
		$languageSelector = '<div class="menu_edit_language_selector">' . $languageSelector . '</div>';
1432
		// language inputs wrapper
1433
		$languageInputs = '<div class="menu_language_wrapper">' . $languageInputs . '</div>';
1434
 
1435
		$out = $languageSelector . $languageInputs;
1436
 
1437
		return $out;
1438
 
1439
	}
1440
 
1441
	/**
1442
	 * Builds CSS ID and Classes inputs for a menu item's settings.
1443
	 *
1444
	 * @access private
1445
	 * @return string $out Markup of inputs.
1446
	 *
1447
	 */
1448
	private function buildCSSMarkup() {
1449
 
1450
		$out = '';
1451
 
1452
		$id = $this->itemID;
1453
		$labelsAndInputs = $this->getMenuSettingsPanelCSSInputs();
1454
 
1455
		foreach ($labelsAndInputs as $key => $value) {
1456
			$out .= '<label for="' . $key . $id . '">' . $value[0] . '</label>' .
1457
					'<input type="text" value="' . $value[1] . '" name="' . $key .'[' . $id . ']" class="menu_settings" id="' . $key . $id . '">';
1458
		}
1459
 
1460
		return $out;
1461
 
1462
	}
1463
 
1464
	/**
1465
	 * Builds include children markup inputs for a menu item's settings.
1466
	 *
1467
	 * @access private
1468
	 * @return string $out Markup of include children inputs.
1469
	 *
1470
	 */
1471
	private function buildMenuItemIncludeChildrenMarkup() {
1472
 
1473
		// include children options
1474
		$options = array(
1475
			4 => $this->_('No'),
1476
			1 => $this->_('Menu'),
1477
			2 => $this->_('Breadcrumbs'),
1478
			3 => $this->_('Both'),
1479
			5 => $this->_('Never'),
1480
		);
1481
 
1482
		$opts = '';
1483
		foreach ($options as $key => $value) {
1484
			$selected = $key == $this->itemIncludeChildren ? ' selected' : '';
1485
			$opts .=  '<option value="' . $key . '"' . $selected . '>' . $value . '</option>';
1486
		}
1487
 
1488
		$out =	'<span class="include_children">' . $this->_('Include natural children') . '</span>' .
1489
				'<select name="include_children[' . $this->itemID . ']" class="include_children">' .
1490
					$opts .
1491
				'</select>' .
1492
				'<label class="include_children">' . $this->_('Level') .
1493
					'<input type="text" value="' . $this->itemMenuMaxLevel . '" name="mb_max_level[' . $this->itemID . ']">' .
1494
				'</label>';
1495
 
1496
		return $out;
1497
 
1498
	}
1499
 
1500
	/**
1501
	 * Builds input for disabling a menu item for use in its settings.
1502
	 *
1503
	 * @access private
1504
	 * @return string $out Markup of input.
1505
	 *
1506
	 */
1507
	private function buildMenuItemDisabledMarkup() {
1508
		$id = $this->itemID;
1509
		$checked = $this->disabledItem ? ' checked' : '';
1510
		$out = '<label for="disabled_item' . $id . '">' .
1511
				'<input type="checkbox" name="disabled_item[' . $id . ']" value="' . $id . '" class="menu_disabled" id="disabled_item' . $id . '"' . $checked . '>' .
1512
					$this->_('Disabled') .
1513
				'</label>';
1514
		return $out;
1515
	}
1516
 
1517
	/**
1518
	 * Builds input for specifying whether custom menu item should open in a new tab.
1519
	 *
1520
	 * Used in a menu item's settings.
1521
	 *
1522
	 * @access private
1523
	 * @return string $out Markup of input.
1524
	 *
1525
	 */
1526
	private function buildMenuItemNewTabMarkup() {
1527
		$id = $this->itemID;
1528
		$checked = $this->newTab ? ' checked' : '';
1529
		$out = '<label for="newtab' . $id . '">' .
1530
					'<input type="checkbox" name="newtab[' . $id . ']" value="' . $id . '" class="menu_settings" id="newtab' . $id . '"' . $checked . '>' .
1531
					$this->_('Open link in a new tab/window') .
1532
				'</label>';
1533
		return $out;
1534
	}
1535
 
1536
	/**
1537
	 * Builds hiden inputs for tracking menu items settings.
1538
	 *
1539
	 * @access private
1540
	 * @return string $out Markup of hidden inputs.
1541
	 *
1542
	 */
1543
	private function buildMenuItemHiddenInputs() {
1544
		$out = '';
1545
		$id = $this->itemID;
1546
		// build label-input pairs..
1547
		$hiddenInputs = array('pages_id' => $this->itemPagesID, 'item_id' => $id, 'item_parent' => $this->itemParentID);
1548
		foreach ($hiddenInputs as $key => $value) {
1549
			$out .= '<input type="hidden" value="' . $value . '" name="' . $key . '[' . $id . ']" id="' . $key . $id . '">';
1550
		}
1551
		return $out;
1552
	}
1553
 
1554
	/**
1555
	 * Builds hidden markup to add to page select panel.
1556
	 *
1557
	 * Used by page list select panels.
1558
	 * Added to panels via JS.
1559
	 *
1560
	 * @access private
1561
	 * @return string $out Markup of extra labels.
1562
	 *
1563
	 */
1564
	private function buildExtraLabels() {
1565
 
1566
		$out =
1567
			'<div id="menu_items_headers_template" class="hide">' .
1568
				'<div id="menu_items_headers">' .
1569
				'<span id="ac_title">' . $this->_('Title') . '</span>' .
1570
				'<span id="ac_css_id">' . $this->_('CSS ID') . '</span>' .
1571
				'<span id="ac_css_class">' . $this->_('CSS Class') . '</span>';
1572
			// check for include children
1573
			if(isset($this->menuPages['children'])) {
1574
				if(1 == $this->menuPages['children'] && $this->wire('user')->hasPermission('menu-builder-include-children')) {
1575
					$out .=
1576
						'<span id="ac_children">' . $this->_('Children'). '</span>' .
1577
						'<span id="ac_level">' . $this->_('Level') . '</span>';
1578
				}
1579
			}
1580
		$out .=
1581
				'</div>' .
1582
			'</div>';
1583
 
1584
		return $out;
1585
 
1586
	}
1587
 
1588
	/**
1589
	 * Builds hidden markup to add to page autocomplete.
1590
	 *
1591
	 * Used by our custom page autocomplete page select.
1592
	 *
1593
	 * @return string $out Markup of extra inputs.
1594
	 *
1595
	 */
1596
	private function buildAsmExtraInputs() {
1597
 
1598
		$out = '';
1599
 
1600
		// build only if using Asm Select (i.e. nothing set for input, hence defaults to Asm)
1601
		if(!isset($this->menuPages['input'])) {
1602
 
1603
			$out .=
1604
				'<div id="menu_items_new_asm_extra_inputs_template" class="hide">' .
1605
					'<span class="asmMB">' .
1606
						'<input name="new_page_css_itemid[]" type="text" class="asm_itemid">' .
1607
						'<input name="new_page_css_itemclass[]" type="text" class="asm_itemclass">';
1608
				// check for include children
1609
				if(isset($this->menuPages['children'])) {
1610
					if(1 == $this->menuPages['children'] && $this->wire('user')->hasPermission('menu-builder-include-children')) {
1611
						$out .=
1612
							'<select name="new_page_include_children[]" class="asm_include_children">' .
1613
								'<option value="4">' . $this->_('No') . '</option>' .
1614
								'<option value="1">' . $this->_('Menu') . '</option>' .
1615
								'<option value="2">' . $this->_('Breadcrumbs') . '</option>' .
1616
								'<option value="3">' . $this->_('Both') . '</option>' .
1617
								'<option value="5">' . $this->_('Never') . '</option>' .
1618
							'</select>' .
1619
							'<input type="text" name="new_page_mb_max_level[]" class="asm_mb_max_level">';
1620
					}
1621
				}
1622
			$out .= '</span>' .
1623
				'</div>';
1624
		}
1625
 
1626
		return $out;
1627
 
1628
	}
1629
 
1630
 
1631
	/* ######################### - GETTERS - ######################### */
1632
 
1633
	/**
1634
	 * Get the languages for use in a multi-lingual setup.
1635
	 *
1636
	 * @access private
1637
	 * @param integer $mode If 1, return only MB active languages, else return all available.
1638
	 * @return array $languages Array of language-name => language-title pairs.
1639
	 *
1640
	 */
1641
	private function getLanguages($mode='') {
1642
		$languages = array();
1643
		if($this->multilingual) {
1644
			foreach ($this->wire('languages') as $language) {
1645
				if(1 == $mode && is_array($this->menuItemsLanguages)) {
1646
					// skip non-active languages (in respect of MB)
1647
					if($language->name!='default' && !in_array($language->name, $this->menuItemsLanguages))	continue;
1648
				}
1649
				$languages[(string) $language->name] = (string) $language->title;
1650
			}
1651
		}
1652
		return $languages;
1653
	}
1654
 
1655
	/**
1656
	 * Get array with key value pairs to build title and URL inputs for a menu item settings.
1657
	 *
1658
	 * @access private
1659
	 * @return string $labelsAndInputs Array of key=>value pairs for building title and URL inputs.
1660
	 *
1661
	 */
1662
	private function getMenuSettingsPanelTitleURLInputs() {
1663
		// build label-input pairs..
1664
		$labelsAndInputs = array(
1665
			'item_title' => array($this->_('Title'), $this->itemTitle2),
1666
			'item_url' => array($this->_('URL'), $this->itemURL),
1667
		);
1668
		return $labelsAndInputs;
1669
	}
1670
 
1671
	/**
1672
	 * Get array with key value pairs to build CSS inputs for a menu item settings.
1673
	 *
1674
	 * @access private
1675
	 * @return string $labelsAndInputs Array of key=>value pairs for building CSS inputs.
1676
	 *
1677
	 */
1678
	private function getMenuSettingsPanelCSSInputs() {
1679
		// build label-input pairs..
1680
		$labelsAndInputs = array(
1681
			'css_itemid' => array($this->_('CSS ID (single value)'), $this->cssItemID),
1682
			'css_itemclass' => array($this->_('CSS Class (single or multiple values separated by space)'), $this->cssItemClass)
1683
		);
1684
		return $labelsAndInputs;
1685
	}
1686
 
1687
	/* ######################### - SETTERS - ######################### */
1688
 
1689
	/**
1690
	 * Sets cookie for limiting number of menu items to show per page in tabular list.
1691
	 *
1692
	 * @access private
1693
	 *
1694
	 */
1695
	private function setShowLimit() {
1696
 
1697
		$post = $this->wire('input')->post;
1698
		$cookie = $this->wire('input')->cookie;
1699
 
1700
		// Determine number of menus to show per page in menus tab. Default = 10 {see $this->showLimit}
1701
		// if user selects a limit ($input->post->show_limit) we set that as the limit and set a cookie {see $this->cookieName} with that value to save state for session.
1702
		if ($post->show_limit) {
1703
			$this->showLimit = $post->show_limit;
1704
			setcookie($this->cookieName, $this->showLimit , 0, '/');
1705
		}
1706
 
1707
		// if no custom limit selected but there is a cookie set, we use the cookie value
1708
		elseif ($cookie[$this->cookieName]) {
1709
			$this->showLimit = (int) $cookie[$this->cookieName];
1710
		}
1711
 
1712
	}
1713
 
1714
	/**
1715
	 * Set values to title and URL properties for multi-lingual setups.
1716
	 *
1717
	 * @access private
1718
	 * @param array $item Array with a menu item's settings.
1719
	 *
1720
	 */
1721
	private function setLanguageTitlesAndURLs($item) {
1722
 
1723
		// set values to required class properties
1724
		foreach($this->getLanguages(1) as $langName => $langTitle) {// @note: 1 means skip non-active languages
1725
 
1726
			if($langName == 'default') continue;
1727
 
1728
			// pw page is menu item
1729
			if(isset($item['pages_id'])) {
1730
				$p = $this->wire('pages')->get($item['pages_id']);
1731
				$title =  isset($item['title_' . $langName]) ? $item['title_' . $langName] : $p->title->getLanguageValue($langName);
1732
				$url = $p->getLanguageValue($langName, 'url');
1733
			}
1734
 
1735
			// custom menu item
1736
			else {
1737
 
1738
				$title = isset($item['title_' . $langName]) ? $item['title_' . $langName] : '';
1739
				$url = isset($item['url_' . $langName]) ? $item['url_' . $langName] : '';
1740
			}
1741
 
1742
			$title2 = $this->wire('sanitizer')->entities($title);// if using <html> in title
1743
 
1744
			$this->{"itemTitle_{$langName}"} = $title;
1745
			$this->{"itemTitle2_{$langName}"} = $title2;
1746
			$this->{"itemURL_{$langName}"} = $url;
1747
 
1748
		}
1749
 
1750
	}
1751
 
1752
	/**
1753
	 * Sets value to various properties for overall settings of a menu.
1754
	 *
1755
	 * @access private
1756
	 * @param object $post A post input to process.
1757
	 *
1758
	 */
1759
	private function setSingleMenuItemsNewPagesArrays($post) {
1760
 
1761
		// Process NEW menu items from PW Pages
1762
		$addPages = $post->item_addpages;
1763
		$pagesCSSID = is_array($post->new_page_css_itemid) ? $post->new_page_css_itemid : array();
1764
		$pagesCSSClass = is_array($post->new_page_css_itemclass) ? $post->new_page_css_itemclass : array();
1765
		$pagesIncludeChildren = is_array($post->new_page_include_children) ? $post->new_page_include_children : array();
1766
		$pagesMBMaxLevel = is_array($post->new_page_mb_max_level) ? $post->new_page_mb_max_level : array();
1767
		$menuPagesInput = (int) $post->menu_pages_input;
1768
 
1769
		// if using PageAutocomplete OR PageListSelectMultiple in the 'add pages to menu select'
1770
		if($menuPagesInput == 2 || $menuPagesInput == 3) {
1771
 
1772
			// In the PageAutocomplete select array, there is only one index with a string of numbers, e.g. ,1087,1364,7895 as a value
1773
			// It is similar in PageListSelectMultiple except it has no first empty string, e.g. 1087,1364,7895 as a value
1774
			$addPages = explode(",", $addPages[0]);
1775
 
1776
			// if PageAutocomplete, we remove the first item in the array since it will be an empty string.
1777
			if($menuPagesInput == 2) array_splice($addPages, 0, 1);
1778
			// @TODO..PROBLEM HERE! SEE TRACY DIFFICULT TO REPLICATE CONSISTENTLY! NOT SURE IF ASM SELECT OR NOT?! SO, MAYBE CHECK IF $pagesCSSClass etc exist?
1779
			// we also remove the other corresponding first item values in the array since they will be empty strings.
1780
			array_splice($pagesCSSID, 0, 1);
1781
			array_splice($pagesCSSClass, 0, 1);
1782
			array_splice($pagesIncludeChildren, 0, 1);
1783
			array_splice($pagesMBMaxLevel, 0, 1);
1784
 
1785
		}
1786
 
1787
		$this->addPages = $addPages ;
1788
		$this->pagesCSSID = $pagesCSSID;
1789
		$this->pagesCSSClass = $pagesCSSClass;
1790
		$this->pagesIncludeChildren = $pagesIncludeChildren;
1791
		$this->pagesMBMaxLevel = $pagesMBMaxLevel;
1792
 
1793
	}
1794
 
1795
	/**
1796
	 * Set the name of user's language as suffix for use for finding cached menus.
1797
	 *
1798
	 * @access private
1799
	 * @return string $value Hyphenated language name.
1800
	 *
1801
	 */
1802
	private function getLanguageSuffixes() {
1803
		$languageNames = array();
1804
		$language = $this->wire('user')->language ? true : false;
1805
		if($language) {
1806
			foreach($this->wire('languages') as $language) $languageNames[] = $language->name;
1807
		}
1808
		return $languageNames;
1809
	}
1810
 
1811
	/* ######################### - HOOKS - ######################### */
1812
 
1813
	/**
1814
	 * Hooks into InputfieldAsmSelect::render().
1815
	 *
1816
	 * Hook modifies AsmSelect output to allow for the use of a custom jquery.asmselect.js
1817
	 * The custom js allows to inject extra HTML input tags for a selected page menu item.
1818
	 * Inputs are for CSS ID and CSS Class of the page selected in the AsmSelect page field and optionally an include children feature.
1819
	 *
1820
	 * @access protected
1821
	 * @param object $event The object returned by the hook
1822
	 * @return object $event The modified event.
1823
	 *
1824
	 */
1825
	protected function customAsm(HookEvent $event) {
1826
		// $value contains the full rendered markup returned by InputfieldAsmSelect ___render()
1827
		$value = $event->return;
1828
		$value = str_replace("\"multiple\"", "\"multipleMB\"", $value);
1829
		// set the modified value back to the return value
1830
		$event->return = $value;
1831
	}
1832
 
1833
	/**
1834
	 * Hooks into InputfieldPageAutocomplete::renderListItem().
1835
	 *
1836
	 * Hook modifies PageAutocomplete output to append extra HTML inputs.
1837
	 * The hook complete replaces the method.
1838
	 * Inputs are for CSS ID and CSS Class of the page selected in the Autocomplete page field  and optionally an include children feature.
1839
	 *
1840
	 * @access protected
1841
	 * @param object $event The object returned by the hook
1842
	 * @return object $event The modified event.
1843
	 *
1844
	 */
1845
	protected function customAc(HookEvent $event) {
1846
 
1847
		/*
1848
			- Every ProcessWire hook is passed an object called $event (of type HookEvent).
1849
			- This object contains an arguments() method that you can access to retrieve the arguments of the method either by index or name.
1850
			- renderListItem() accepts three arguments:
1851
			- renderListItem($label, $value, $class = '')
1852
		 */
1853
 
1854
		$class = " " . $event->arguments('class');// note the space! This will be appended to other CSS classes
1855
		$label = $event->arguments('label');// label () to display for the sortable li
1856
		$value = $event->arguments('value');// selected items values (typically page->id)
1857
 
1858
		// here we just add the <span><input></span> to the default renderListItem() Markup
1859
		$extraInput =
1860
			'<span class="acMB"><input name="new_page_css_itemid[]" type="text" class="ac_itemid">
1861
				<input name="new_page_css_itemclass[]" type="text" class="ac_itemclass">';
1862
 
1863
		// output 'include children' extras only if specified and for users with right credentials
1864
		if($this->includeChildren == 1) {
1865
 
1866
			$extraInput .=
1867
				'<select name="new_page_include_children[]" class="ac_include_children">
1868
					<option value="4">' . $this->_('No') . '</option>
1869
					<option value="1">' . $this->_('Menu') . '</option>
1870
					<option value="2">' . $this->_('Breadcrumbs') . '</option>
1871
					<option value="3">' . $this->_('Both') . '</option>
1872
					<option value="5">' . $this->_('Never') . '</option>
1873
				</select>
1874
				<input type="text" name="new_page_mb_max_level[]" class="ac_mb_max_level">';
1875
		}
1876
 
1877
		$extraInput .= '</span>';
1878
 
1879
		// we don't want extra input in the default template (one with $label='Label', $class='itemTemplate', $value='1')
1880
		if($label =='Label' && $class == 'itemTemplate') $extraInput = '';
1881
 
1882
		$event->replace = true;// we want to entirely replace the method
1883
 
1884
		$out =
1885
			"\n<li class='ui-state-default" . $class . "'>" .
1886
			"<i class='fa fa-sort fa-fw'></i> " .
1887
			"<span class='itemValue'>" . $value . "</span>" .
1888
			"<span class='itemLabel'>" . $label . "</span>
1889
			<a class='itemRemove' title='Remove' href='#'><i class='fa fa-trash'></i></a>" .
1890
			$extraInput .	"</li>";
1891
 
1892
		// set the modified value back to the return value
1893
		$event->return = $out;
1894
 
1895
	}
1896
 
1897
	/**
1898
	 * Hooks into InputfieldPageListSelectMultiple::render().
1899
	 *
1900
	 * Hook modifies PageListSelectMultiple output to append extra HTML inputs.
1901
	 * Inputs are for CSS ID and CSS Class of the page selected in the Autocomplete page field  and optionally an include children feature.
1902
	 *
1903
	 * @access protected
1904
	 * @param object $event The object returned by the hook
1905
	 * @return object $event The modified event.
1906
	 *
1907
	 */
1908
	protected function customPls(HookEvent $event) {
1909
 
1910
		// $value contains the full rendered markup returned by InputfieldPageListSelectMultiple ___render()
1911
		$value = $event->return;
1912
 
1913
		// @note: PW CHANGED FROM 'fa-sort' to 'fa-arrows' somewhere in PW3.x; we now simply the search
1914
		// this is the string we want to replace (the itemTemplate)
1915
		/* $searchStr = "<li class='ui-state-default itemTemplate'><i class='itemSort fa fa-arrows'></i> <span class='itemValue'>1</span><span class='itemLabel'>Label</span> <a class='itemRemove' title='Remove' href='#'><i class='fa fa-trash'></i></a></li>"; */
1916
 
1917
		// @note: PW CHANGED FROM 'fa-sort' to 'fa-arrows' somewhere in PW3.x; we have now simplified the search
1918
		$searchStr = "</i></a></li>";
1919
 
1920
		// part of replacement string
1921
		$extraInput =
1922
			"<span class='plsMB'><input name='new_page_css_itemid[]' type='text' class='pls_itemid'>" .
1923
			"<input name='new_page_css_itemclass[]' type='text' class='pls_itemclass'>";
1924
 
1925
		// output 'include children' extras only if specified and for users with right credentials
1926
		if($this->includeChildren == 1) {
1927
 
1928
			$extraInput .=
1929
				'<select name="new_page_include_children[]" class="pls_include_children">
1930
					<option value="4">' . $this->_('No') . '</option>
1931
					<option value="1">' . $this->_('Menu') . '</option>
1932
					<option value="2">' . $this->_('Breadcrumbs') . '</option>
1933
					<option value="3">' . $this->_('Both') . '</option>
1934
					<option value="5">' . $this->_('Never') . '</option>
1935
				</select>
1936
				<input type="text" name="new_page_mb_max_level[]" class="pls_mb_max_level">';
1937
		}
1938
 
1939
		$extraInput .= '</span>';
1940
 
1941
		// @note: PW CHANGED FROM 'fa-sort' to 'fa-arrows' somewhere in PW3.x; we simplify the replacement
1942
		/* $replacementStr =
1943
			"\n<li class='ui-state-default itemTemplate'>" .
1944
			// "<span class='ui-icon ui-icon-arrowthick-2-n-s'></span>" .
1945
			"<i class='itemSort fa fa-sort'></i> " .
1946
			"<span class='itemValue'>1</span>" .
1947
			"<span class='itemLabel'>Label</span> " .
1948
			"<a class='itemRemove' title='Remove' href='#'><i class='fa fa-trash'></i></a>" .
1949
			$extraInput .
1950
			"</li>"; */
1951
 
1952
			// @note: PW CHANGED FROM 'fa-sort' to 'fa-arrows' somewhere in PW3.x; we have now simplified the replacement
1953
		$replacementStr =
1954
			"</i></a>" .
1955
			$extraInput .
1956
			"</li>";
1957
 
1958
		$value = str_replace($searchStr, $replacementStr, $value);
1959
 
1960
		// set the modified value back to the return value
1961
		$event->return = $value;
1962
 
1963
	}
1964
 
1965
	/* ######################### - OTHER - ######################### */
1966
 
1967
	/**
1968
	 * Outputs javascript configuration values for nestedSortable.
1969
	 *
1970
	 * @access protected
1971
	 * @return object $scripts Object with array of configurations to pass to JS.
1972
	 *
1973
	 */
1974
	protected function nestedSortableConfigs() {
1975
 
1976
		// our default nestedSortable settings
1977
		$nestedSortableOptions = array(
1978
			'config' => array(
1979
				'maxLevels' => 0,
1980
				'disableParentChange' => 'false',
1981
				'expandOnHover' => 700,
1982
				'protectRoot' => 'false',
1983
				'rtl' => 'false',
1984
				'startCollapsed'=>'false',
1985
				'tabSize' => 20,
1986
				'doNotClear' => 'false',
1987
				'isTree' => 'true',
1988
			)
1989
		);
1990
 
1991
		// if custom settings found, we overwrite default ones
1992
		if(!empty($this->menuSettings)) {
1993
			foreach ($this->menuSettings as $key => $value) $nestedSortableOptions['config'][$key] = $value['setting'];
1994
		}
1995
		// ProcessMenuBuilderNestedSortable
1996
		$scripts = $this->wire('config')->js($this->className() . 'NestedSortable', $nestedSortableOptions);
1997
 
1998
		return $scripts;
1999
 
2000
	}
2001
 
2002
	/**
2003
	 * Outputs javascript configuration value for other menu features.
2004
	 *
2005
	 * @access protected
2006
	 * @return object $scripts Object with array of configurations to pass to JS.
2007
	 *
2008
	 */
2009
	protected function menuConfigs() {
2010
 
2011
		// our default include children setting
2012
		$options = array('config' => array('children' => 0));// do not include children
2013
 
2014
		// @todo: this could be refactored!
2015
		if(!empty($this->menuPages)) {
2016
			// if a custom 'include children' setting found, we overwrite the default one
2017
			foreach ($this->menuPages as $key => $value) {
2018
				if($key == 'children' && $this->wire('user')->hasPermission('menu-builder-include-children')) {
2019
					$options['config'][$key] = $value;
2020
					break;
2021
				}
2022
			}
2023
 
2024
			// set multilingual status
2025
			$options['config']['multilingual'] = isset($this->menuPages['menu_items_languages']) ? 1 : 0;
2026
		}
2027
		// ProcessMenuBuilder
2028
		$scripts = $this->wire('config')->js($this->className(), $options);
2029
 
2030
		return $scripts;
2031
 
2032
	}
2033
 
2034
	/**
2035
	 * Outputs saved menu settings for editing configuration values for nestedSortable.
2036
	 *
2037
	 * Settings will only be available to supersusers.
2038
	 *
2039
	 * @access protected
2040
	 * @return array $mergedMenuSettings Merge menu settings.
2041
	 *
2042
	 */
2043
	protected function nestedSortableMenuSettings() {
2044
 
2045
		$mlNote = $this->_('The maximum depth of nested items the list can accept. If set to \'0\' the levels are unlimited.');
2046
		$dpcNote = $this->_('Set this to') . ' true ';
2047
		$dpcNote .= $this->_('to lock the parentship of items. They can only be re-ordered within their current parent container.');
2048
		$eonNote = $this->_('How long (in milliseconds) to wait before expanding a collapsed node (useful only if') . ' isTree: true).';
2049
		$prNote = $this->_('Whether to protect the root level (i.e. root items can be sorted but not nested, sub-items cannot become root items.)');
2050
		$rltNote = $this->_('Set this to') . ' true ';
2051
		$rltNote .= $this->_('if you have a right-to-left page.');
2052
		$scNote = $this->_('Set this to') . ' true ';
2053
		$scNote .= $this->_('if you want the plugin to collapse the tree on page load.');
2054
		$tsNote = $this->_('How far right or left (in pixels) the item has to travel in order to be nested or to be sent outside its current list.');
2055
		$dncNote = $this->_('Set this to') .  ' true ';
2056
		$dncNote .= $this->_('if you do not want empty lists to be removed.');
2057
		$treeNote = $this->_('Nested list to behave as a tree with expand/collapse functionality.');
2058
 
2059
		// default menu settings array to be merged and displayed in menu settings table in 'Settings' Tab
2060
		$defaultMenuSettings = array(
2061
			'maxLevels' => array('default'=>0, 'setting'=>'', 'notes'=> $mlNote),
2062
			'disableParentChange' => array('default'=>'false', 'setting'=>'', 'notes'=> $dpcNote),
2063
			'expandOnHover' => array('default'=>700, 'setting'=>'', 'notes'=> $eonNote),
2064
			'protectRoot' => array('default'=>'false', 'setting'=>'', 'notes'=> $prNote),
2065
			'rtl' => array('default'=>'false', 'setting'=>'', 'notes'=> $rltNote),
2066
			'startCollapsed' => array('default'=>'false', 'setting'=>'', 'notes'=> $scNote),
2067
			'tabSize' => array('default'=>20, 'setting'=>'', 'notes'=> $tsNote),
2068
			'doNotClear' => array('default'=>'false', 'setting'=>'', 'notes'=> $dncNote),
2069
			'isTree' => array('default' =>'true', 'setting'=>'', 'notes'=> $treeNote),
2070
		);
2071
 
2072
		$mergedMenuSettings = array_replace_recursive($defaultMenuSettings, $this->menuSettings);
2073
 
2074
		return $mergedMenuSettings;
2075
 
2076
	}
2077
 
2078
 
2079
	/* ######################### - CRUD ACTIONS - ######################### */
2080
 
2081
	/**
2082
	 * Processes ProcessMenuBuilder form inputs (CRUD).
2083
	 *
2084
	 * CRUD - Processes all the form input sent from execute() and executeEdit().
2085
	 *
2086
	 * @access private
2087
	 * @param object $form Sent form values.
2088
	 *
2089
	 */
2090
	private function save($form) {
2091
 
2092
		$post = $this->wire('input')->post;
2093
 
2094
		// process form
2095
		$form->processInput($post);
2096
 
2097
		$menuID = (int) $post->menu_id;
2098
		$menuDeleteConfirm = (int) $post->menu_delete_confirm;// checkbox to confirm trash
2099
 
2100
		// save new menu(s)
2101
		if ($post->menu_new_unpublished_btn || $post->menu_new_published_btn)$this->saveNewMenu($post);
2102
		// menus bulk actions: lock/unlock and trash/delete are controlled by permissions
2103
		elseif($post->menus_action_btn) $this->bulkActionsMenu($post);
2104
		// save single specified menu
2105
		elseif($post->menu_save || $post->menu_save_exit) $this->saveSingleMenu($menuID, $post);
2106
		// delete menu
2107
		elseif ($post->menu_delete) $this->menuDelete($menuDeleteConfirm);
2108
 
2109
	}
2110
 
2111
	/**
2112
	 * Delete a single menu item.
2113
	 *
2114
	 * @access private
2115
	 * @param integer $menuID ID of the Menu to delete.
2116
	 *
2117
	 */
2118
	private function menuDelete($menuID) {
2119
 
2120
		if($menuID) {
2121
 
2122
			$page = $this->wire('page');
2123
			$pages = $this->wire('pages');
2124
 
2125
			// if user does not have permission to trash/delete a menu, exit with an error
2126
			if ($this->wire('permissions')->get('menu-builder-delete')->id && !$this->wire('user')->hasPermission('menu-builder-delete')) {
2127
				$this->error($this->_('Menu Builder: You have no permission to delete menus.'));
2128
				$this->session->redirect($page->url. 'edit/?id=' . $menuID);// redirect back to the menu we were editing
2129
			}
2130
 
2131
			$menu = $pages->get("id=$menuID, parent=$this->menusParent, include=all");
2132
 
2133
			// if menu is locked for editing, exit with an error
2134
			if($menu->is(Page::statusLocked)) {
2135
				$this->error($this->_('Menu Builder: This menu is locked for edits.'));
2136
				$this->session->redirect($page->url. 'edit/?id=' . $menuID);// redirect back to the menu we were editing
2137
			}
2138
 
2139
			if($pages->trash($menu)) {
2140
				// also delete cache of menu if present
2141
				$this->deleteMenuCache($menu->id);
2142
				$this->message(sprintf($this->_('Menu Builder: Moved menu %1$s to trash: %2$s'), $menu->title, $menu->url));// tell user menu trashed
2143
				$this->session->redirect($page->url);
2144
			}
2145
 
2146
			else {
2147
				$this->error($this->_('Menu Builder: Unable to move menu to trash'));// menu can't be moved to the trash error
2148
				return false;
2149
			}
2150
 
2151
		}
2152
 
2153
	}
2154
 
2155
	/**
2156
	 * Save a single menu item.
2157
	 *
2158
	 * @param integer $menuID ID of the Menu to save.
2159
	 * @param array $post Post input with a menu's settings.
2160
	 * @access private
2161
	 *
2162
	 */
2163
	private function saveSingleMenu($menuID, $post) {
2164
 
2165
		//  ================ SAVE SINGLE EXISTING MENU (executeEdit()) =====================
2166
 
2167
		$user = $this->wire('user');
2168
		$sanitizer = $this->wire('sanitizer');
2169
		$page = $this->wire('page');
2170
		$pages = $this->wire('pages');
2171
		$session = $this->wire('session');
2172
 
2173
		$menu = $pages->get($menuID);
2174
 
2175
		// if we didn't get a menu, exit with an error
2176
		if(!$menu->id) {
2177
			$this->error($this->_('Menu Builder: Error saving Menu.'));
2178
			return false;
2179
		}
2180
 
2181
		// @todo: if locked, maybe hide link or hide save buttons!
2182
		// if menu is locked for editing, exit with an error
2183
		if($menu->is(Page::statusLocked)) {
2184
			$this->error($this->_('Menu Builder: This menu is locked for edits.'));
2185
			$this->wire('session')->redirect($page->url . 'edit/?id=' . $menuID);// redirect back to the menu we were editing
2186
		}
2187
 
2188
		################# process menu #################
2189
 
2190
		$menuTitle = $sanitizer->text($post->menu_title);
2191
 
2192
		// if no title provided, halt proceedings and show error message
2193
		if (!$menuTitle) {
2194
			$this->error($this->_('Menu Builder: A title is required.'));
2195
			return false;
2196
		}
2197
 
2198
		$menu->title = $menuTitle;
2199
		$menu->name = $sanitizer->pageName($menuTitle);
2200
 
2201
		// check if name already taken
2202
		// @note: we use ID since if we checked name, we might just be checking the name of the menu we are editing!
2203
		$child = $menu->parent->child("name={$menu->name}, include=all");
2204
		// if different ID, it means there is a menu sibling; abort!
2205
		if($child->id && $child->id !== $menuID) {
2206
			$this->error($this->_("Menu Builder: A menu with that title already exists."));
2207
			// redirect back to the menu we were editing
2208
			$session->redirect($page->url . 'edit/?id=' . $menuID);
2209
		}
2210
 
2211
		// else process menu
2212
		else {
2213
 
2214
			// save other languages' titles if in multi-lingual environment
2215
 
2216
			if ($this->multilingual) {
2217
				foreach ($this->wire('languages') as $language) {
2218
 
2219
				// skip default language as already set above
2220
					if($language->name == 'default') continue;
2221
 
2222
					// set values for other languages
2223
					else {
2224
 
2225
						$id = $language->id;
2226
						$title = $sanitizer->text($post->{"menu_title__$id"});
2227
						$name = $sanitizer->pageName($title);
2228
						// set language page title
2229
						$menu->title->setLanguageValue($language, $title);
2230
						// @note: name is not a field, so we set this way
2231
						$menu->set("name$language", $name);
2232
					}
2233
				}
2234
			} // end if languages
2235
 
2236
 
2237
 
2238
			#################	01. Process menu 'pages'	#################
2239
			$menu->menu_pages = $this->saveSingleMenuPages($menu, $post);
2240
			#################	02. Process EXISTING menu items 	#################
2241
			$menu->menu_items = $this->saveSingleMenuItems($post);
2242
			#################	03. Process menu settings 	#################
2243
			if($user->hasPermission('menu-builder-settings')) $menu->menu_settings = $this->saveSingleMenuSettings($post);// only save for users with right permission
2244
 
2245
			#################	Save menu 	#################
2246
			$menu->save();
2247
 
2248
			// also delete cache of menu if present so that can be refreshed
2249
			$this->deleteMenuCache($menu->id);
2250
 
2251
			$this->message($this->_('Menu Builder: Saved Menu '. $menu->title));
2252
			if($post->menu_save_exit) $session->redirect($page->url);
2253
			else $session->redirect($page->url . 'edit/?id=' . $menuID);// redirect back to the menu we were editing
2254
		}
2255
 
2256
	}
2257
 
2258
	/**
2259
	 * Save a single menu 'pages' settings.
2260
	 *
2261
	 * Here pages refer mainly to settings that affect the whole menu.
2262
	 * These include, allow markup, etc.
2263
	 *
2264
	 * @access private
2265
	 * @param object $menu Page representing the menu being edited.
2266
	 * @param object $post The Post containing all 'pages' values to be saved for this menu.
2267
	 * @return string $menuPagesJSON JSON String to save as settings for this menu.
2268
	 *
2269
	 */
2270
	private function saveSingleMenuPages($menu, $post) {
2271
 
2272
		$user = $this->wire('user');
2273
		$sanitizer = $this->wire('sanitizer');
2274
 
2275
		// array for newly set menuPages settings ('sel', 'input', 'markup' and 'children')
2276
		$menuPagesNew = array();
2277
 
2278
		// only save for users with correct permissions
2279
		// ensures their settings are not overwritten (although hidden for other users)
2280
 
2281
		// if this user has permission to SPECIFY pages selectable as menu items in AsmSelect and PageAutocomplete
2282
		if($user->hasPermission('menu-builder-selectable')) {
2283
			// selector for finding pages that can be added to the menu (for AsmSelect/Autocomplete)
2284
			$menuPagesNew['sel'] = $sanitizer->text($post->menu_pages);
2285
		}
2286
 
2287
		// if user has permission to allow changing of page field type used to select pages to add as menu items [AsmSelect vs PageAutocomplete]
2288
		if($user->hasPermission('menu-builder-page-field')) {
2289
			// page inputfield type for finding pages that can be added to the menu (AsmSelect vs. Autocomplete)
2290
			// we only save this if user selects Autocomplete; otherwise defaults to AsmSelect
2291
			//$menuPagesNew['input'] = (int) $post->menu_pages_select == 2 ? 2 : '';
2292
			$menuPagesNew['input'] = '';
2293
			$selPageField = (int) $post->menu_pages_select;
2294
			if($selPageField == 2) $menuPagesNew['input'] = 2;
2295
			elseif($selPageField == 3) $menuPagesNew['input'] = 3;
2296
		}
2297
 
2298
		// if user can change and use allow markup/HTML setting
2299
		if($user->hasPermission('menu-builder-markup')) {
2300
			// whether to allow HTML markup in menu item titles/lables -> e.g. <span>Home</span>
2301
			// we only save this if user selects Yes; otherwise defaults to No (don't allow markup)
2302
			// we'll then use the correct sanitizer below
2303
			$this->allowMarkup = $menuPagesNew['markup'] = (int) $post->menu_item_title_markup == 1 ? 1 : '';
2304
		}
2305
 
2306
		// if user can change and use include children setting
2307
		if($user->hasPermission('menu-builder-include-children')) {
2308
			// we only save this if user selects Yes; otherwise defaults to No (don't allow inclusion of children)
2309
			// we'll then use the correct sanitizer below
2310
			$this->includeChildren = $menuPagesNew['children'] = (int) $post->menu_item_include_children == 1 ? 1 : '';
2311
		}
2312
 
2313
		// if user can change and use disable items setting
2314
		if($user->hasPermission('menu-builder-disable-items')) {
2315
			// we only save this if user selects Yes; otherwise defaults to No (don't allow disabling of menu items)
2316
			$this->disableItems = $menuPagesNew['disable_items'] = (int) $post->menu_item_disable_items == 1 ? 1 : '';
2317
		}
2318
 
2319
		// if user can change and use multi-lingual menu items feature
2320
		if($user->hasPermission('menu-builder-multi-lingual-items')) {
2321
			$this->menuItemsLanguages = $menuPagesNew['menu_items_languages'] = is_array($post->menu_items_languages) && !empty($post->menu_items_languages) ? $post->menu_items_languages : '';
2322
		}
2323
 
2324
		// merge newly set menuPages values with (any) existing ones
2325
		$menuPagesSaved = json_decode($menu->menu_pages, true);
2326
		if(!is_array($menuPagesSaved)) $menuPagesSaved = array();
2327
		$menuPages = array_merge($menuPagesSaved, $menuPagesNew);
2328
 
2329
		// JSON string of menu pages and menu items to save
2330
		$menuPagesJSON = !empty($menuPages) ? wireEncodeJSON($menuPages) : '';// using wireEncodeJSON ensures we only save non-empty values
2331
 
2332
		return $menuPagesJSON;
2333
 
2334
	}
2335
 
2336
	/**
2337
	 * Save a single menu's menu items.
2338
	 *
2339
	 * @access private
2340
	 * @param object $post The Post containing all menu items and their properties.
2341
	 * @return string $menuitemsJSON JSON String to save as menu items for this menu.
2342
	 *
2343
	 */
2344
	private function saveSingleMenuItems($post) {
2345
 
2346
		#################	01. Process existing menu items 	#################
2347
		$menuItems = $this->saveSingleMenuItemsExisting($post);
2348
		// we'll need this to auto-increment menu IDs for new menu items (to ensure uniqueness)
2349
		$lastID = !empty($menuItems) ? max(array_keys($menuItems)) : 0;// will give us the highest numbered array key (the itemID)
2350
 
2351
		$this->menuItemID = $lastID + 1;
2352
 
2353
		#################	02: Process NEW custom menu items 	#################
2354
		$menuItems = $this->saveSingleMenuItemsNewCustom($post, $menuItems);
2355
		#################	03: Process NEW menu items from PW Pages 	#################
2356
		$this->setSingleMenuItemsNewPagesArrays($post);
2357
		$menuItems = $this->saveSingleMenuItemsNewPages($menuItems);
2358
		#################	04: Process NEW menu items from Selector 	#################
2359
		$menuItems = $this->saveSingleMenuItemsNewSelector($post, $menuItems);
2360
 
2361
		$menuitemsJSON = !empty($menuItems) ? wireEncodeJSON($menuItems) : '';
2362
 
2363
		return $menuitemsJSON;
2364
 
2365
	}
2366
 
2367
	/**
2368
	 * Prepare data for existing menu items within a menu being saved.
2369
	 *
2370
	 * @access private
2371
	 * @param object $post The Post containing all menu items and their properties.
2372
	 * @return array $menuItems Array populated with data for existing menu items for menu being saved.
2373
	 *
2374
	 */
2375
	private function saveSingleMenuItemsExisting($post) {
2376
 
2377
		$user = $this->wire('user');
2378
		$sanitizer = $this->wire('sanitizer');
2379
 
2380
		// array to hold our all our menu items
2381
		$menuItems = array();
2382
		// for mutlilingual titles and custom URLs if needed
2383
		$menuItemsLanguage = array();
2384
 
2385
		// to hold IDs of disabled items to action cascading same status to descendants
2386
		$disabledItemsIDs = array();
2387
 
2388
		// loop through the existing, updated menu items sent from nestedSortable
2389
		// only loop if we have existing menu times. we check the hidden field with IDs of menu items
2390
		if(!empty($post->item_id)) {
2391
 
2392
			//$ml = $this->multiLingual ? true : false;
2393
			$ml = !is_null($this->menuItemsLanguages) ? true : false;
2394
 
2395
			//$itemIncludeChildren = '';
2396
			//$itemMMaxLevel = '';
2397
 
2398
			if($this->allowMarkup) $purifier = $this->wire('modules')->get('MarkupHTMLPurifier');
2399
 
2400
			foreach($post->item_id as $itemID) {
2401
 
2402
				$itemMMaxLevel = '';
2403
 
2404
				$itemID = (int) $itemID;
2405
				if(!$itemID) continue;
2406
 
2407
				// if menu items titles allow HTML (markup) && user has correct permission, we run them though HTML purifier
2408
				if($this->allowMarkup && $user->hasPermission('menu-builder-markup')) $itemTitle = $purifier->purify($post->item_title[$itemID]);
2409
				// else we sanitize menu item titles as text
2410
				else $itemTitle =  $sanitizer->text($post->item_title[$itemID]);
2411
 
2412
				if(!$itemTitle) continue;
2413
 
2414
				$itemURL = $sanitizer->url($post->item_url[$itemID]);
2415
				if(!$itemURL) continue;
2416
 
2417
				$itemParent = (int) $post->item_parent[$itemID];// the item's parent in relation to the menu (not PW page menu!)
2418
				$itemPagesID = (int) $post->pages_id[$itemID];
2419
				$itemURL = $itemPagesID == 0 ? $itemURL : '';// only save custom (external to PW) items links
2420
 
2421
				// add multilingual titles and URLs (for custom menu items only)
2422
				if($ml) $menuItemsLanguage = $this->saveSingleMenuItemsExistingLanguagesTitleURL($itemID, $itemPagesID, $post, $menuItemsLanguage);
2423
 
2424
				$itemCSSID = $sanitizer->name($post->css_itemid[$itemID]);// single value
2425
				$itemCSSClass = $sanitizer->text($post->css_itemclass[$itemID]);// sanitizer->text to accept multiple classes
2426
				$itemNewTab = isset($post->newtab[$itemID]) ? 1 : '';// only save for custom menu items with target='_blank'
2427
 
2428
				// if current user can edit include children values + change include children setting
2429
				$itemIncludeChildren = '';
2430
				if( $this->includeChildren && isset($post->include_children[$itemID]) ) {
2431
					// no need to save default value '4'
2432
					$itemIncludeChildren =  (int) $post->include_children[$itemID] == 4 ? '' : (int) $post->include_children[$itemID];
2433
					// @todo: For now, only m_max_level can be individually set
2434
					$itemMMaxLevel = $itemIncludeChildren == 1 || $itemIncludeChildren == 3 ? (int) $post->mb_max_level[$itemID] : '';
2435
				}
2436
 
2437
				// if current user can edit enable/disable menu items feature + change items enabled status
2438
				$itemDisabled = $this->disableItems && isset($post->disabled_item[$itemID]) ? 1 : '';
2439
 
2440
				// if parent is disabled, then disable all descendants as well
2441
				if(in_array($itemParent, $disabledItemsIDs)) $itemDisabled = 1;
2442
 
2443
				$menuItems[$itemID] = array(
2444
					'title' => $itemTitle,
2445
					'parent_id' => $itemParent,
2446
					'url' => $itemURL,
2447
					'pages_id' => $itemPagesID,
2448
					'css_itemid' => $itemCSSID,
2449
					'css_itemclass' => $itemCSSClass,
2450
					'newtab' => $itemNewTab,
2451
					'include_children' => $itemIncludeChildren,
2452
					'm_max_level' => $itemMMaxLevel,
2453
					'disabled_item' => $itemDisabled,
2454
				);
2455
 
2456
				// add disabled item to array to check if to apply same status to descendants
2457
				if($itemDisabled) $disabledItemsIDs[] = $itemID;
2458
 
2459
			}// end foreach loop for existing menu items
2460
 
2461
			// merge menu items with multilingual titles and custom URLs if applicable
2462
			if(!empty($menuItemsLanguage)) $menuItems = array_replace_recursive($menuItems, $menuItemsLanguage);
2463
 
2464
		}// end if !empty $post->item_id
2465
 
2466
		return $menuItems;
2467
 
2468
	}
2469
 
2470
	/**
2471
	 * Prepare multi-lingual data for existing menu items within a menu being saved.
2472
	 *
2473
	 * @access private
2474
	 * @param integer $itemID The ID of the menu item being prepared for saving.
2475
	 * @param integer $itemPagesID The pages ID of the menu item. If 0, it means a custom menu item.
2476
	 * @param object $post The Post containing the menu item's multi-lingual properties.
2477
	 * @param array $menuItemsLanguage Array with data for existing menu items multi-lingual titles and URLs for menu being saved.
2478
	 * @return array $menuItemsLanguage Updated array with data for existing menu items multi-lingual titles and URLs for menu being saved.
2479
	 *
2480
	 */
2481
	 private function saveSingleMenuItemsExistingLanguagesTitleURL($itemID, $itemPagesID, $post, $menuItemsLanguage) {
2482
 
2483
		 $user = $this->wire('user');
2484
		 $sanitizer = $this->wire('sanitizer');
2485
		 if($this->allowMarkup) $purifier = $this->wire('modules')->get('MarkupHTMLPurifier');
2486
 
2487
		 foreach($this->getLanguages(1) as $langName => $langTitle) {// @note: 1 means skip non-active languages
2488
 
2489
		 	if($langName == 'default') continue;
2490
 
2491
		 	$suffix = '_' . $langName;
2492
 
2493
		 	## language title ##
2494
		 	// if menu items titles allow HTML (markup) && user has correct permission, we run them though HTML purifier
2495
			if($this->allowMarkup && $user->hasPermission('menu-builder-markup')){
2496
				$itemLanguageTitle = $purifier->purify($post->{"item_title{$suffix}"}[$itemID]);
2497
			}
2498
			// else we sanitize menu item title as text
2499
			else $itemLanguageTitle = $sanitizer->text($post->{"item_title{$suffix}"}[$itemID]);
2500
 
2501
			## language url ##
2502
			$itemLanguageURL = $itemPagesID == 0 ? $sanitizer->url($post->{"item_url{$suffix}"}[$itemID]) : '';// only save custom (external to PW) items links
2503
 
2504
			##################
2505
 
2506
		 	$titleIndex = 'title' . $suffix;
2507
		 	$urlIndex = 'url' . $suffix;
2508
 
2509
		 	$menuItemsLanguage[$itemID][$titleIndex] = $itemLanguageTitle;
2510
		 	$menuItemsLanguage[$itemID][$urlIndex] = $itemLanguageURL;
2511
 
2512
		 }// end foreach
2513
 
2514
		 return $menuItemsLanguage;
2515
 
2516
	}
2517
 
2518
	/**
2519
	 * Prepare data for new custom menu items for the menu being saved.
2520
	 *
2521
	 * @access private
2522
	 * @param object $post The Post containing all custom menu items and their properties.
2523
	 * @param array $menuItems Array with data for menu items being prepared for saving.
2524
	 * @return array $menuItems Updated array with data for menu items to save.
2525
	 *
2526
	 */
2527
	private function saveSingleMenuItemsNewCustom($post, $menuItems) {
2528
 
2529
		/*	Values coming from two sources: Custom menu links & PW pages added to menu
2530
		 *	Tack these at the bottom of the menuItems array
2531
		 *	Give them parent = 0 (i.e. top tier until drag & drop later)
2532
		 *
2533
		 */
2534
 
2535
		$menuItemID = $this->menuItemID;
2536
 
2537
		$sanitizer = $this->wire('sanitizer');
2538
 
2539
		// add the new custom menu item links. Cannot add new pages here since their count may be different
2540
		$count = count($post->new_item_custom_title);
2541
 
2542
		for ($i = 0; $i < $count; $i++) {
2543
 
2544
			$itemTitle = $sanitizer->text($post->new_item_custom_title[$i]);
2545
			if (!$itemTitle) continue;
2546
 
2547
			// @TODO..MAKE URL A REQUIRED INPUT! + DON'T SUBMIT (JS) UNTIL COMPLETED!?
2548
			// $newpages_id = '';// not needed. New items, hence new $ids will be auto-created
2549
			$itemURL = $sanitizer->url($post->new_item_custom_url[$i]);
2550
			if (!$itemURL) continue;// only accept new menu items with URLs. @todo - should this be the case? What if they want a divider-like item?
2551
 
2552
			$itemCSSID = $sanitizer->name($post->new_css_itemid[$i]);
2553
			$itemCSSClass = $sanitizer->name($post->new_css_itemclass[$i]);
2554
			//$itemNewTab = (!isset($post->new_newtab[$i])) ? '' : 1;// using checkbox unreliable; use hidden input instead (below)
2555
			$itemNewTab = (int) $post->new_newtab_hidden[$i] ? 1 : '';// hidden input to resolve above
2556
 
2557
			// add custom (external) menu items to our menu
2558
			$menuItems[$menuItemID] = array(
2559
				'title' => $itemTitle,
2560
				'parent_id' => 0,// for new items (before potentially moved to other tiers in drag & drop)
2561
				'url' => $itemURL,
2562
				'css_itemid' => $itemCSSID,
2563
				'css_itemclass' => $itemCSSClass,
2564
				'pages_id' => '',
2565
				'newtab' => $itemNewTab,
2566
			);
2567
 
2568
			$menuItemID++;
2569
 
2570
		}// end for loop for new custom items
2571
 
2572
		$this->menuItemID = $menuItemID;
2573
 
2574
		return $menuItems;
2575
 
2576
	}
2577
 
2578
	/**
2579
	 * Prepare data for new (pw) pages menu items from added pages for the menu being saved.
2580
	 *
2581
	 * @access private
2582
	 * @param array $menuItems Array with data for menu items being prepared for saving.
2583
	 * @return array $menuItems Updated array with data for menu items to save.
2584
	 *
2585
	 */
2586
	private function saveSingleMenuItemsNewPages($menuItems) {
2587
 
2588
		$menuItemID = $this->menuItemID;
2589
		$pages = $this->wire('pages');
2590
		$sanitizer = $this->wire('sanitizer');
2591
		// for multilingual environments
2592
		$language = $this->wire('user')->language; // save the current user's language
2593
 
2594
		$count = is_array($this->addPages) ? count($this->addPages) : 0;
2595
 
2596
		for ($i = 0; $i < $count; $i++) {
2597
 
2598
			// if there are menu items added from the AsmSelect, add them to the menu
2599
			$itemID = (int) $this->addPages[$i];// sanitize: we need this to be an integer
2600
 
2601
			// multilingual environments
2602
			if($language != null && method_exists($pages->get($itemID)->title, 'getLanguageValue')) $itemTitle = $pages->get($itemID)->title->getLanguageValue($language);// title of each PW page in this array
2603
			else $itemTitle = $pages->get($itemID)->title;// title of each PW page in this array
2604
			if(!$itemTitle) continue;// if no new pages posted, move on...[otherwise one iteration with empty strings is added to array!]
2605
 
2606
			$itemCSSID = isset($this->pagesCSSID[$i]) ? $sanitizer->name($this->pagesCSSID[$i]) : '';
2607
			$itemCSSClass = isset($this->pagesCSSClass[$i]) ? $sanitizer->text($this->pagesCSSClass[$i]) : '';
2608
 
2609
			// include children (but not for custom menu items or 'Home')
2610
			$itemIncludeChildren = '';
2611
			if(isset($this->pagesIncludeChildren[$i])) {
2612
				$itemIncludeChildren = (int) $this->pagesIncludeChildren[$i] == 4 || $itemID == 1 ? '' : (int) $this->pagesIncludeChildren[$i];
2613
			}
2614
 
2615
			// @todo: only m_max_level can be individually set for now
2616
			$itemMMaxLevel = $itemIncludeChildren == 1 || $itemIncludeChildren == 3 ? (int) $this->pagesMBMaxLevel[$i] : '';
2617
			#$itemBMaxLevel = $itemIncludeChildren == 2 ? (int) $this->pagesMBMaxLevel[$i] : '';
2618
 
2619
			// @todo - not setting individually for now
2620
			// determine m and b_max_levels when 'Both' selection made in include children level (and if there's need for separate levels)
2621
			/*if($itemIncludeChildren == 3) {
2622
				$itemMBMaxLevels = explode(',', $pagesMBMaxLevel[$i]);
2623
				$itemMMaxLevel = (int) $itemMBMaxLevels['0'];
2624
				$itemBMaxLevel = isset($itemMBMaxLevels['1']) && $itemMBMaxLevels['1'] ? (int) $itemMBMaxLevels['1'] : $itemMMaxLevel;
2625
			}*/
2626
 
2627
			// add PW pages (internal) menu items to our menu
2628
			$menuItems[$menuItemID] = array(
2629
				'title' => $itemTitle,
2630
				'parent_id' => 0,// for new items before they are sorted in drag & drop
2631
				// 'url' => '',// empty since these are PW pages; no needed to copy URL here + need to make sure always have latest
2632
				'css_itemid' => $itemCSSID,
2633
				'css_itemclass' => $itemCSSClass,
2634
				'pages_id' => $itemID,// the PW page ID
2635
				// 'newtab' => '',// NOT necessary for PW pages
2636
				'include_children' => $itemIncludeChildren,
2637
				'm_max_level' => $itemMMaxLevel,
2638
				// 'b_max_level' => $itemBMaxLevel,// @todo - not setting individually for now
2639
			);
2640
 
2641
			$menuItemID++;
2642
 
2643
		}// end for loop for new page items
2644
 
2645
 
2646
		$this->menuItemID = $menuItemID;
2647
 
2648
		return $menuItems;
2649
 
2650
	}
2651
 
2652
	/**
2653
	 * Prepare data for new (pw) pages menu items from selector for the menu being saved.
2654
	 *
2655
	 * @access private
2656
	 * @param object $post The Post containing the selector for adding menu items.
2657
	 * @param array $menuItems Array with data for menu items being prepared for saving.
2658
	 * @return array $menuItems Updated array with data for menu items to save.
2659
	 *
2660
	 */
2661
	private function saveSingleMenuItemsNewSelector($post, $menuItems) {
2662
 
2663
		$menuItemID = $this->menuItemID;
2664
		$pages = $this->wire('pages');
2665
		$sanitizer = $this->wire('sanitizer');
2666
		$language = $this->wire('user')->language; // save the current user's language
2667
 
2668
		$items = array();
2669
 
2670
		$selectorPages = $sanitizer->text($post->item_addselector);
2671
		if($selectorPages) {
2672
			$sel = ", template!=admin, has_parent!=2, parent!=7, id!=27";// prevent accidental addition of admin|trash|404 pages
2673
			$items = $pages->find($selectorPages . $sel);
2674
		}
2675
 
2676
		if (!empty($items)) {
2677
 
2678
			foreach ($items as $item) {
2679
 
2680
				// add PW pages (internal) menu items from the selector to our menu
2681
				$menuItems[$menuItemID] = array(
2682
					// multilingual environments
2683
					'title' => $title = is_null($language) ? $item->title : $item->title->getLanguageValue($language),
2684
					'parent_id' => 0,// for new items before they are sorted in drag & drop
2685
					// 'url' => '',// empty since these are PW pages; no needed to copy URL here + need to make sure always have latest
2686
					// 'css_itemid' => ''// empty until edited
2687
					// 'css_itemclass' => ''// empty until edited
2688
					'pages_id' => $item->id,// the PW page ID
2689
					// 'newtab' => ''// NOT necessary for PW pages
2690
				);
2691
 
2692
				$menuItemID++;
2693
 
2694
			}// end foreach $items as $item
2695
 
2696
		}// end if !empty($items)
2697
 
2698
		return $menuItems;
2699
 
2700
	}
2701
 
2702
	/**
2703
	 * Save a single menu nestedSortable settings.
2704
	 *
2705
	 * @access private
2706
	 * @param object $post The Post containing all menu settings.
2707
	 * @return string $menuSettingsJSON JSON String to save as settings for this menu.
2708
	 *
2709
	 */
2710
	private function saveSingleMenuSettings($post) {
2711
 
2712
		$user = $this->wire('user');
2713
		$sanitizer = $this->wire('sanitizer');
2714
 
2715
		// if user has permission to edit nestedSortable settings
2716
		if($user->hasPermission('menu-builder-settings')) {
2717
			// nestedSortable settings for this menu. we'll save this as JSON in menu_settings field
2718
			$menuSettings = array();
2719
			// nestedSortable settings
2720
			foreach ($post->menu_settings as $key => $value) {
2721
				// only save non-empty $key => $values
2722
				if($value) {
2723
					if($key == 'maxLevels' || $key == 'expandOnHover' || $key == 'tabSize') $value = (int) $value;
2724
					else $value = $sanitizer->text($value);
2725
					$menuSettings[$key]['setting'] = $value;
2726
				}
2727
			}// end foreach
2728
		}// end if user has menu-builder-settings permission
2729
 
2730
		// JSON string of menu settings to save
2731
		$menuSettingsJSON = !empty($menuSettings) ? json_encode($menuSettings) : '';
2732
 
2733
		return $menuSettingsJSON;
2734
 
2735
	}
2736
 
2737
	/**
2738
	 * Apply bulk actions to selected menu items.
2739
	 *
2740
	 * @access private
2741
	 * @param object $post Input Post with action to apply and menu items to apply them to.
2742
	 * @access private
2743
	 *
2744
	 */
2745
	private function bulkActionsMenu($post) {
2746
 
2747
		$action = $this->wire('sanitizer')->fieldName($post->menus_action_select);
2748
 
2749
		if (!$action) {
2750
			$this->error($this->_("Menu Builder: You need to select an action."));
2751
			return false;
2752
		}
2753
 
2754
		$actionMenus = $post->menus_action;// checkbox array name
2755
 
2756
		// check if menus were selected.
2757
		if (!empty($actionMenus)) {
2758
 
2759
			// prepare sent menu IDs to find and TRASH the menu pages
2760
			$menuIds = implode('|', $actionMenus);// split array elements, joining them with pipe (I) to use in selector
2761
			$menus = $this->wire('pages')->find("id={$menuIds}, include=all");
2762
 
2763
			$i = 0;
2764
			# publish
2765
			if ($action == 'publish') {
2766
				foreach ($menus as $m) {
2767
					$m->removeStatus(Page::statusUnpublished);
2768
					$m->save();
2769
					$i++;
2770
				}
2771
 
2772
				$msg = sprintf(_n("Published %d menu.", "Published %d menus.", $i), $i);
2773
 
2774
			}// end publish menus
2775
 
2776
			# unpublish
2777
			elseif ($action == 'unpublish') {
2778
				foreach ($menus as $m) {
2779
					$m->addStatus(Page::statusUnpublished);
2780
					$m->save();
2781
					$i++;
2782
				}
2783
 
2784
				$msg = sprintf(_n("Unpublished %d menu.", "Unpublished %d menus.", $i), $i);
2785
 
2786
			}// end unpublish menus
2787
 
2788
			# lock
2789
			elseif ($action == 'lock') {
2790
				foreach ($menus as $m) {
2791
					$m->addStatus(Page::statusLocked);
2792
					$m->save();
2793
					$i++;
2794
				}
2795
 
2796
				$msg = sprintf(_n("Locked %d menu.", "Locked %d menus.", $i), $i);
2797
 
2798
			}// end lock menus
2799
 
2800
			# unlock
2801
			elseif ($action == 'unlock') {
2802
				foreach ($menus as $m) {
2803
					$m->removeStatus(Page::statusLocked);
2804
					$m->save();
2805
					$i++;
2806
				}
2807
 
2808
				$msg = sprintf(_n("Unlocked %d menu.", "Unlocked %d menus.", $i), $i);
2809
 
2810
			}// end unlock menus
2811
 
2812
			# trash
2813
			elseif ($action == 'trash') {
2814
				foreach ($menus as $m) {
2815
					$m->trash();
2816
					$i++;
2817
					// also delete cache of menu if present
2818
					$this->deleteMenuCache($m->id);
2819
				}
2820
 
2821
				$msg = sprintf(_n("Trashed %d menu.", "Trashed %d menus.", $i), $i);
2822
 
2823
			}// end trash menus
2824
 
2825
			# delete
2826
			elseif ($action == 'delete') {
2827
				foreach ($menus as $m) {
2828
					$m->delete();
2829
					$i++;
2830
					// also delete cache of menu if present
2831
					$this->deleteMenuCache($m->id);
2832
				}
2833
 
2834
				$msg = sprintf(_n("Deleted %d menu.", "Deleted %d menus.", $i), $i);
2835
 
2836
			}// end delete menus
2837
 
2838
			// messages
2839
			$msg = $this->_('Menu Builder') . ': ' . $msg;
2840
 
2841
			$this->message($msg);// tell user how many menus were 'actioned'
2842
			$this->session->redirect($this->wire('page')->url);// redirect to page where we were
2843
 
2844
		}
2845
 
2846
		// error
2847
		else {
2848
			// show error message if apply action button clicked without first selecting menus
2849
			$this->error($this->_('Menu Builder: You need to select at least one menu before applying an action.'));
2850
			return false;
2851
		}
2852
 
2853
	}
2854
 
2855
	/**
2856
	 * Save new menus.
2857
	 *
2858
	 * @access private
2859
	 * @param array $post Input Post with new menus to save.
2860
	 * @access private
2861
	 *
2862
	 */
2863
	private function saveNewMenu($post) {
2864
 
2865
		$sanitizer = $this->wire('sanitizer');
2866
 
2867
		// default/main language title
2868
		$title = $sanitizer->text($post->menus_add_title);
2869
		$newUnpublishedBtn = $post->menu_new_unpublished_btn;
2870
 
2871
		if($title) {
2872
 
2873
			$page = new Page();
2874
			$page->parent = $this->menusParent;
2875
			$page->template = $this->wire('templates')->get("menus");
2876
			$page->title = $title;
2877
			// sanitize and convert to a URL friendly page name
2878
			$page->name = $sanitizer->pageName($page->title);
2879
			// check if name already taken
2880
			if($page->parent->child("name={$page->name}, include=all")->id) {
2881
				$this->error($this->_("Menu Builder: A menu with that title already exists."));
2882
			}
2883
			// save new menu  + also check multi-lingual
2884
			else {
2885
				if ($this->multilingual) {
2886
					foreach ($this->wire('languages') as $language) {
2887
 
2888
					// skip default language as already set above
2889
						if($language->name == 'default') continue;
2890
 
2891
						// set values for other languages
2892
						else {
2893
							// @note: we set language as active
2894
							$page->set("status$language", 1);
2895
							$id = $language->id;
2896
							$title = $sanitizer->text($post->{"menus_add_title__$id"});
2897
							$name = $sanitizer->pageName($title);
2898
							// set language page title
2899
							$page->title->setLanguageValue($language, $title);
2900
							// @note: name is not a field, so we set this way
2901
							$page->set("name$language", $name);
2902
						}
2903
					}
2904
				} // end if languages
2905
 
2906
				// if user pressed 'save unpublished', we save new menus unpublished
2907
				if ($newUnpublishedBtn) $page->addStatus(Page::statusUnpublished);
2908
 
2909
				// save
2910
				$page->save();
2911
 
2912
				// success message
2913
				$this->message(sprintf(__('Added new menu: %s'), $page->title));
2914
 
2915
				// redirect to landing page
2916
				$this->session->redirect($this->wire('page')->url);
2917
 
2918
			}
2919
 
2920
		}// end if menu title provided
2921
 
2922
		// show error message if add button clicked without first entering a menu title
2923
		else $this->error($this->_("Menu Builder: You need to specify a menu title."));
2924
 
2925
	}
2926
 
2927
	/**
2928
	 * Delete given Menu's cache.
2929
	 *
2930
	 * @access private
2931
	 * @param integer $menuID ID of the Menu to delete.
2932
	 *
2933
	 */
2934
	private function deleteMenuCache($menuID) {
2935
		$languageNames = $this->getLanguageSuffixes();
2936
		// multi-lingual site
2937
		if(!empty($languageNames)) {
2938
			foreach ($languageNames as $name) {
2939
				$cacheName = 'menu-builder-' . $menuID . '-' . $name;
2940
				$this->wire('cache')->delete($cacheName);// delete the cache
2941
			}
2942
		}
2943
 
2944
		// non-multi-lingual site
2945
		else {
2946
			$cacheName = 'menu-builder-' . $menuID;
2947
			$this->wire('cache')->delete($cacheName);// delete the cache
2948
		}
2949
 
2950
	}
2951
 
2952
	/* ######################### - INSTALLERS - ######################### */
2953
 
2954
 
2955
	/**
2956
	 * Called only when the module is installed.
2957
	 *
2958
	 * A new page with this Process module assigned is created.
2959
	 * A new permission 'menu-builder' is created.
2960
	 * 3 fields are created.
2961
	 * A new template 'menu_pages' is created.
2962
	 *
2963
	 * @access public
2964
	 *
2965
	 */
2966
	public function ___install() {
2967
 
2968
		// installer for templates and fields + their tags  to be used by Menu Builder
2969
		$pages = $this->wire('pages');
2970
		$fields = array(
2971
 
2972
		'menu_pages' => array('name'=>'menu_pages', 'type'=> 'FieldtypeText', 'label'=>'Menu Pages', 'description'=>'JSON formatted values of optional ProcessWire selector to limit pages that can be added to this menu, whether to allow HTML (markup) in menu item titles and whether to use AsmSelect or PageAutocomplete in adding menu items. Example JSON: {"sel":"template=colours, limit=20, sort=title","input":2}. You don\'t need to edit these directly. Use Menu Builder instead.', 'maxlength'=>2048),
2973
		'menu_items' => array('name'=>'menu_items', 'type'=> 'FieldtypeTextarea', 'label'=>'Menu Items', 'description'=>'JSON values of the items in this menu. You don\'t need to edit these directly. Use Menu Builder instead.'),
2974
		'menu_settings' => array('name'=>'menu_settings', 'type'=> 'FieldtypeTextarea', 'label'=>'Menu Settings', 'description'=>'JSON values of this menu\'s settings. You don\'t need to edit these directly. Use Menu Builder instead.'),
2975
 
2976
		);
2977
 
2978
		// first check that we don't already have fields named same as menu builderss
2979
		foreach ($fields as $field) {
2980
			// if we do, we abort before installing the module
2981
			if($this->wire('fields')->get($field['name'])) {
2982
				throw new WireException($this->_("Aborted installation. Confirm that you do not have fields called 'menu_pages', 'menu_settings' and 'menu_items' before installing this module."));
2983
			}
2984
		}
2985
 
2986
		// check that we already don't have a template named same as menu builder's
2987
		// if we do, we abort before installing the module
2988
		if($this->wire('templates')->get('menus')) {
2989
				throw new WireException($this->_("Aborted installation. Confirm that you do not have a template called 'menus' before installing this module."));
2990
		}
2991
 
2992
		// if no errors, we are good to go
2993
 
2994
		// create our 3 fields
2995
		foreach ($fields as $field) {
2996
 
2997
			$f = new Field(); // create new field object
2998
			$f->type = $this->wire('modules')->get($field['type']); // get a field type
2999
			$f->name = $field['name'];
3000
			$f->label = $field['label'];
3001
			$f->description = $field['description'];
3002
			$f->collapsed = 5;
3003
			if ($field['name'] == 'menu_pages') $f->maxlength = $field['maxlength'];
3004
			if ($field['name'] != 'menu_pages') $f->rows = 10;
3005
 
3006
			$f->tags = '-menu';
3007
			$f->save();
3008
 
3009
		}// end foreach fields
3010
 
3011
 
3012
		// create our 1 template + add above fields
3013
		// new fieldgroup
3014
		$fg = new Fieldgroup();
3015
		$fg->name = 'menus';
3016
 
3017
		// add title field
3018
		$title = $this->wire('fields')->get('title');
3019
		$fg->add($title);
3020
 
3021
		foreach ($fields as $key => $value) {
3022
				$f = $this->wire('fields')->get($key);
3023
				$fg->add($f);
3024
		}
3025
 
3026
		// save fieldgroup
3027
		$fg->save();
3028
		$this->message('Created Fields: menu_pages, menu_items, menu_settings');
3029
 
3030
		// create a new template to use with this fieldgroup
3031
		$t = new Template();
3032
		$t->name = 'menus';
3033
		$t->fieldgroup = $fg;// add the fieldgroup
3034
 
3035
		// add template settings we need
3036
		$t->label = 'Menus';
3037
		$t->noChildren = 1;// the pages using this template should not have children
3038
		$t->parentTemplates = array($this->wire('templates')->get('admin')->id);// needs to be added as array of template IDs. Allowed template for parents = 'admin'
3039
		$t->tags = '-menu';
3040
 
3041
		// save new template with fields and settings now added
3042
		$t->save();
3043
		$this->message('Created Template: menus');
3044
 
3045
		// create menu builder page and permission
3046
		$p = $pages->get('template=admin, name='.self::PAGE_NAME);
3047
		if (!$p->id) {
3048
			$page = new Page();
3049
			$page->template = 'admin';
3050
			$page->parent = $pages->get($this->config->adminRootPageID)->child('name=setup');
3051
			$page->title = 'Menu Builder';
3052
			$page->name = self::PAGE_NAME;
3053
			$page->process = $this;
3054
			$page->save();
3055
 
3056
			// tell the user we created this page
3057
			$this->message("Created Page: {$page->path}");
3058
		}
3059
 
3060
		$permission = $this->wire('permissions')->get('menu-builder');
3061
		if (!$permission->id) {
3062
			$p = new Permission();
3063
			$p->name = 'menu-builder';
3064
			$p->title = $this->_('View Menu Builder Page');
3065
			$p->save();
3066
 
3067
			// tell the user we created this module's permission
3068
			$this->message('Created New Permission: menu-builder');
3069
		}
3070
 
3071
	}
3072
 
3073
	/**
3074
	 * Called only when the module is uninstalled.
3075
	 *
3076
	 * This should return the site to the same state it was in before the module was installed.
3077
	 * Deletes 3 fields, template and permission created on install as well as created menu pages.
3078
	 *
3079
	 * @access public
3080
	 *
3081
	 */
3082
	public function ___uninstall() {
3083
 
3084
		$pages = $this->wire('pages');
3085
 
3086
		// find and delete the page we installed, locating it by the process field (which has the module ID)
3087
		// it would probably be sufficient just to locate by name, but this is just to be extra sure.
3088
		$moduleID = $this->wire('modules')->getModuleID($this);
3089
		$mbPage = $pages->get("template=admin, process=$moduleID, name=" . self::PAGE_NAME);
3090
		// $page = $pages->get('template=admin, name='.self::PAGE_NAME);
3091
 
3092
		if($mbPage->id) {
3093
			// if we found the page, let the user know and delete it
3094
			$this->message($this->_('Deleted Page: ') . $mbPage->path);
3095
			// recursively delete the menu builder page (i.e. including all its children (the menus))
3096
			$pages->delete($mbPage, true);
3097
			// also delete any menu pages that may have been left in the trash
3098
			foreach ($pages->find('template=menus, status>=' . Page::statusTrash) as $p) $p->delete();
3099
		}
3100
 
3101
		// find and delete the menu builder permission and let the user know
3102
		$permission = $this->wire('permissions')->get('menu-builder');
3103
		if ($permission->id){
3104
			$permission->delete();
3105
			$this->message('Deleted Permission: menu-builder');
3106
 
3107
		}
3108
 
3109
		// find and delete our menus template
3110
		$t = $this->wire('templates')->get('menus');
3111
 
3112
		if ($t->id) {
3113
			$this->wire('templates')->delete($t);
3114
			$this->wire('fieldgroups')->delete($t->fieldgroup);// delete the associated fieldgroups
3115
			$this->message('Deleted Template: menus');
3116
		}
3117
 
3118
		// find and delete the 3 fields used by our menus
3119
		$fields = array('menu_pages', 'menu_items', 'menu_settings');
3120
		foreach ($fields as $field) {
3121
				$f = $this->wire('fields')->get($field);
3122
				if($f->id) $this->wire('fields')->delete($f);
3123
				$this->message('Deleted Fields: menu_pages, menu_items, menu_settings');
3124
		}
3125
 
3126
	}
3127
 
3128
 
3129
}