/**
 * ProcessWire Page List Process, JQuery Plugin
 *
 * Provides the Javascript/jQuery implementation of the PageList process when used with the JSON renderer
 * 
 * ProcessWire 3.x (development), Copyright 2016 by Ryan Cramer
 * https://processwire.com
 *
 */

function ProcessPageListInit() {
	if(ProcessWire.config.ProcessPageList) {
		$('#' + ProcessWire.config.ProcessPageList.containerID).ProcessPageList(ProcessWire.config.ProcessPageList);
	}
}

$(document).ready(function() {
	ProcessPageListInit();
}); 

(function($) {
	
	$.fn.ProcessPageList = function(customOptions) {

		/**
	 	 * List of options that may be passed to the plugin
		 *
		 */
		var options = {

			// mode: 'select' or 'actions', currently this is automatically determined based on the element the PageList is attached to
			mode: '',		

			// default number of pages to show before pagination
			limit: 35,		

			// the page ID that starts the list
			rootPageID: 0,			 

			// show the page identified by 'rootPageID' ?	
			showRootPage: true,

			// the page ID currently selected
			selectedPageID: 0, 
		
			// id of the admin page
			adminPageID: 2, 
		
			// id of the trash page
			trashPageID: 7, 
		
			// language ID, if applicable
			langID: 0,
			
			// in 'select' mode, allow no value to be selected (to abort a selected value)
			selectAllowUnselect: false,

			// show the 'currently selected' page header? (should be false on multi-selection)
			selectShowPageHeader: true, 

			// show the parent path in the selected page label?
			selectShowPath: true, 

			// the label to click on to change the currently selected page
			selectStartLabel: 'Change', 

			// the label to click on to cancel selecting a page
			selectCancelLabel: 'Cancel',

			// the label to click on to select a given page
			selectSelectLabel: 'Select',

			// the label to click on to unselect a selected page
			selectUnselectLabel: 'Unselect',

			// label used for 'more' in paginated lists
			moreLabel: 'More',
			
			// label used for 'trash' button in Move mode
			trashLabel: 'Trash', 

			// label used for the move instruction
			moveInstructionLabel: "Click and drag to move", 

			// href attribute of 'select' link
			selectSelectHref: '#', 

			// href attribute of 'unselect' link
			selectUnselectHref: '#',
	
			// URL where page lists are loaded from 	
			ajaxURL: ProcessWire.config.urls.admin + 'page/list/', 	

			// URL where page move's should be posted
			ajaxMoveURL: ProcessWire.config.urls.admin + 'page/sort/',
		
			// classes for pagination
			paginationClass: 'PageListPagination',
			paginationCurrentClass: 'PageListPaginationCurrent',
			paginationLinkClass: 'ui-state-default',
			paginationLinkCurrentClass: 'ui-state-active',
			paginationHoverClass: 'ui-state-hover',
			paginationDisabledClass: 'ui-priority-secondary',

			// pagination number that you want to open (to correspond with openPageIDs)
			openPagination: 0, 

			// IDs of the pages that we want to automatically open (default none) 
			openPageIDs: [],
		
			// pre-rendered data corresponding to openPageIDs, indexed by 'id-start', i.e. 123-0
			openPageData: {},

			// speed at which the slideUp/slideDown run (in ms)
			speed: 200,
			
			// whether or not hovering an item reveals its actions
			useHoverActions: false, 
		
			// milliseconds delay between hovering an item and it revealing actions 
			hoverActionDelay: 250,
			
			// milliseconds in fade time to reveal or hide hover actions
			hoverActionFade: 150,
		
			// show only edit and view actions, and make everything else extra actions
			useNarrowActions: $('body').hasClass('pw-narrow-width'), 
		
			// markup for the spinner used when ajax calls are made
			spinnerMarkup: "<span class='PageListLoading'><i class='ui-priority-secondary fa fa-fw fa-spin fa-spinner'></i></span>",
		
			// session field name that holds page label format, when used
			labelName: '',
		
			// what to show in the PageListNumChildren quantity: 'children', 'total', 'children/total', 'total/children', or 'id'
			// default is blank, which implies 'children'
			qtyType: '', 
		};
	
		// array of "123.0" (page_id.start) that are currently open (used in non-select mode only)
		var currentOpenPageIDs = [];
	
		// true when operations are occurring where we want to ignore clicks
		var ignoreClicks = false;
		
		var isModal = $("body").hasClass("modal") || $("body").hasClass("pw-iframe");
		
		if(typeof ProcessWire.config.ProcessPageList != "undefined") {
			$.extend(options, ProcessWire.config.ProcessPageList);
		}
	
		$.extend(options, customOptions);

		return this.each(function(index) {

			var $container = $(this);
			var $outer;
			var $root; 
			var $loading = $(options.spinnerMarkup); 
			var firstPagination = 0; // used internally by the getPaginationList() function
			var curPagination = 0; // current page number used by getPaginationList() function

			/**
	 		 * Initialize the Page List
			 *
			 */
			function init() {

				$root = $("<div class='PageListRoot'></div>"); 
				
				if($container.is(":input")) {
					options.selectedPageID = $container.val();
					if(!options.selectedPageID.length) options.selectedPageID = 0;
					options.mode = 'select';
					$container.before($root); 
					$outer = $container.closest('.InputfieldContent');
					setupSelectMode();
				} else {
					options.mode = 'actions'; 
					$container.append($root); 
					$outer = $container;
					loadChildren(options.rootPageID > 0 ? options.rootPageID : 1, $root, 0, true); 
					/*
					// longclick to initiate sort, still marinating on whether to support this
					$(document).on('longclick', 'a.PageListPage', function() {
						$(this).parent().find('.PageListActionMove > a').click();
					});
					*/
				}
				
				$(document).on('pageListRefresh', function(e, pageID) {
					// i.e. $(document).trigger('pageListRefresh', pageID); 
					refreshList(pageID);
				});
				
				if(options.useHoverActions) { 
					$root.addClass('PageListUseHoverActions');
					setupHoverActions();
				}
			}

			/**
			 * If hover actions enabled, setup events to hide/show hover actions
			 * 
 			 */
			function setupHoverActions() {
				
				var hoverTimeout = null;
				var hoverOutTimeout = null;
				var $hoveredItem = null;
				
				function showItem($item) {
					var $actions = $item.find('.PageListActions');
					if(!$actions.is(":visible") || $item.hasClass('PageListItemOpen')) {
						// we confirm :visible so that we don't iterfere with admin themes that already
						// make the PageList items visible with css hover states
						$item.addClass('PageListItemHover');
						$actions.css('display', 'inline').css('opacity', 0)
							.animate({opacity: 1.0}, options.hoverActionFade);
					}
				}
				
				function hideItem($item) {
					var $actions = $item.find('.PageListActions');
					$item.removeClass('PageListItemHover');
					if($actions.is(":visible")) { // || $hoveredItem.hasClass('PageListItemOpen')) {
						$actions.animate({opacity: 0}, options.hoverActionFade, function () {
							$actions.hide();
						});
					}
				}

				$outer.on('keydown', '.PageListItem', function(e) {
					// PR#1 makes page-list keyboard accessible
					e = e || window.event;
					if(e.keyCode == 0 || e.keyCode == 32) {
						// spacebar
						var $actions = $(this).find('.PageListActions');
						if($actions.is(":visible")) {
							$actions.css('display', 'none');
						} else {
							$actions.css('display', 'inline-block');
						}
						return false;
					}
				});

				$outer.on('mouseover', '.PageListItem', function(e) {

					if($root.is(".PageListSorting") || $root.is(".PageListSortSaving")) return;
					if(!$(this).children('a:first').is(":hover")) return;
					
					$hoveredItem = $(this);
					//console.log('pageX=' + e.pageX);
					//console.log('offsetX=' + $hoveredItem.offset().left);
					
					//var maxPageX = $(this).children('.PageListNumChildren').offset().left + 100;
					//if(e.pageX > maxPageX) return;
					
					if($hoveredItem.hasClass('PageListItemHover')) return;
					var $item = $(this);
					if(hoverTimeout) clearTimeout(hoverTimeout);
					var delay = options.hoverActionDelay;
					
					
					hoverTimeout = setTimeout(function() {
						if($hoveredItem.attr('class') == $item.attr('class')) {
							if(!$hoveredItem.children('a:first').is(":hover")) return;
							var $hideItems = $outer.find(".PageListItemHover");
							showItem($hoveredItem);
							$hideItems.each(function() { hideItem($(this)); });
						}
					}, delay); 

				}).on('mouseout', '.PageListItem', function(e) {
					if($root.is(".PageListSorting") || $root.is(".PageListSortSaving")) return;
					var $item = $(this);
					if($item.hasClass('PageListItemOpen')) return;
					if(!$item.hasClass('PageListItemHover')) return;
					var delay = options.hoverActionDelay * 0.7;
					hoverOutTimeout = setTimeout(function() {
						if($item.is(":hover")) return;
						if($item.attr('class') == $hoveredItem.attr('class')) return;
						hideItem($item);
					}, delay);
				});
			
			}
			
			/**
	 		 * Sets up a mode where the user is given a "select" link for each page, rather than a list of actions
			 * 
			 * When they hit "select" the list collapses and the selected page ID is populated into an input
			 *
			 */
			function setupSelectMode() {

				var $actions = $("<ul></ul>").addClass('PageListActions PageListSelectActions actions'); 
				var $pageLabel = $("<p></p>").addClass("PageListSelectName"); 
				if(options.selectShowPageHeader) $pageLabel.append($loading); 

				var $action = $("<a></a>")
					.addClass("PageListSelectActionToggle")
					.addClass("PageListSelectActionToggleStart")
					.attr('href', '#')
					.text(options.selectStartLabel)
					.click(function() {

						if($(this).text() == options.selectStartLabel) {
							loadChildren(options.rootPageID > 0 ? options.rootPageID : 1, $root, 0, true); 
							$(this).text(options.selectCancelLabel)
								.removeClass('PageListSelectActionToggleStart').addClass('PageListSelectActionToggleCancel');
						} else {
							$(this).addClass('PageListSelectActionToggleStart').removeClass('PageListSelectActionToggleCancel');
							$root.children(".PageList").slideUp(options.speed, function() {
								$(this).remove();
							}); 
							$(this).text(options.selectStartLabel); 
						}
						return false; 
					}); 

				$actions.append($("<li></li>").append($action)); 

				$root.append($("<div></div>").addClass('PageListSelectHeader').append($pageLabel).append($actions)); 

				if(options.selectShowPageHeader) { 
					var ajaxURL = options.ajaxURL + 
						"?id=" + options.selectedPageID + 
						"&render=JSON&start=0&limit=0&lang=" + options.langID + 
						"&mode=" + options.mode;
					if(options.labelName.length) ajaxURL += '&labelName=' + options.labelName;
					$.getJSON(ajaxURL, function(data) {
						var parentPath = '';
						if(options.selectShowPath) {
							parentPath = data.page.path;
							if(parentPath.substring(-1) == '/') parentPath = parentPath.substring(0, parentPath.length-1); 
							parentPath = parentPath.substring(0, parentPath.lastIndexOf('/')+1); 
							parentPath = '<span class="detail">' + parentPath + '</span> ';
						} 
						var label = options.selectedPageID > 0 ? parentPath + data.page.label : '';
						$root.children(".PageListSelectHeader").find(".PageListSelectName").html(label); 
					}); 
				}
			}

			/**
			 * Method that is triggered when the processChildren() method completes
			 *
			 */
			function loaded() {
				ignoreClicks = false;
			}

			/**
			 * Handles pagination of PageList items
			 *
			 * @param int id ID of the page having children to show
			 * @param start Index that we are starting with in the current list
		 	 * @param int limit The limit being applied to the list (items per page)
			 * @param int total The total number of items in the list (excluding any limits)
			 * @return jQuery $list The pagination list ready for insertion
			 *
			 */
			function getPaginationList(id, start, limit, total) {

				// console.log('getPaginationList(id=' + id + ', start=' + start + ", limit=" + limit + ", total=" + total + ')'); 

				var maxPaginationLinks = 9; 
				var numPaginations = Math.ceil(total / limit); 
				curPagination = start >= limit ? Math.floor(start / limit) : 0;

				if(curPagination == 0) {		
					firstPagination = 0; 
				
				} else if((curPagination-maxPaginationLinks+1) > firstPagination) {
					firstPagination = curPagination - Math.floor(maxPaginationLinks / 2); 

				} else if(firstPagination > 0 && curPagination == firstPagination) {
					firstPagination = curPagination - Math.ceil(maxPaginationLinks / 2); 
				}


				// if we're on the last page of pagination links, then make the firstPagination static at the end
				if(firstPagination > numPaginations - maxPaginationLinks) firstPagination = numPaginations - maxPaginationLinks; 

				if(firstPagination < 0) firstPagination = 0;

				var $list = $("<ul></ul>").addClass(options.paginationClass).data('paginationInfo', {
					start: start,
					limit: limit,
					total: total
				}); 

				/**
				 * paginationClick is the event function called when an item in the pagination nav is clicked
				 *
				 * It loads the new pages (via loadChildren) and then replaces the old pageList with the new one
				 *
				 */
				var paginationClick = function(e) {
					var $curList = $(this).parents("ul." + options.paginationClass);
					var info = $curList.data('paginationInfo'); 
					if(!info) return false;
					var start = parseInt($(this).attr('href')) * info.limit;
					if(start === NaN) start = 0;
					var $newList = getPaginationList(id, start, info.limit, info.total);
					var $spinner = $(options.spinnerMarkup);
					var $loading = $("<li>&nbsp;</li>").addClass(options.paginationDisabledClass).append($spinner.hide());
					$curList.siblings(".PageList").remove(); // remove any open lists below current
					$curList.replaceWith($newList); 
					$newList.append($loading); 
					$spinner.fadeIn('fast');
					var $siblings = $newList.siblings().css('opacity', 0.5);
					loadChildren(id, $newList.parent(), start, false, false, true, function() {
						$spinner.fadeOut('fast', function() {
							$loading.remove();
						});
						$newList.parent('.PageList').prev('.PageListItem').data('start', start);
						updateOpenPageIDs();
					}); 
					return false;	
				}
		
				var $separator = null;
				var $blankItem = null;
	
				for(var pagination = firstPagination, cnt = 0; pagination < numPaginations; pagination++, cnt++) {

					var $a = $("<a></a>").html(pagination+1).attr('href', pagination).addClass(options.paginationLinkClass); 
					var $item = $("<li></li>").addClass(options.paginationClass + cnt).append($a); // .addClass('ui-state-default');

					if(pagination == curPagination) {
						//$item.addClass("PageListPaginationCurrent ui-state-focus"); 
						$item.addClass(options.paginationCurrentClass).find("a")
							.removeClass(options.paginationLinkClass)
							.addClass(options.paginationLinkCurrentClass); 
					}

					$list.append($item); 

					if(!$blankItem) {
						$blankItem = $item.clone().removeClass(options.paginationCurrentClass + ' ' + options.paginationLinkCurrentClass); 
						$blankItem.find('a').removeClass(options.paginationLinkCurrentClass).addClass(options.paginationLinkClass);  
					}
					// if(!$blankItem) $blankItem = $item.clone().removeClass('PageListPaginationCurrent').find('a').removeClass('ui-state-focus').addClass('ui-state-default'); 
					if(!$separator) $separator = $blankItem.clone().removeClass(options.paginationLinkClass)
						.addClass(options.paginationDisabledClass).html("&hellip;"); 
					//if(!$separator) $separator = $blankItem.clone().html("&hellip;"); 

					if(cnt >= maxPaginationLinks && pagination < numPaginations) {
						$lastItem = $blankItem.clone();
						$lastItem.find("a").text(numPaginations).attr('href', numPaginations-1);
						$list.append($separator.clone()).append($lastItem); 
						break;
					} 
				}


				if(firstPagination > 0) {
					$firstItem = $blankItem.clone();
					$firstItem.find("a").text("1").attr('href', '0').click(paginationClick); 
					$list.prepend($separator.clone()).prepend($firstItem); 
				}

				//if(curPagination+1 < maxPaginationLinks && curPagination+1 < numPaginations) {
				if(curPagination+1 < numPaginations) {
					$nextBtn = $blankItem.clone();
					$nextBtn.find("a").html("<i class='fa fa-angle-right'></i>").attr('href', curPagination+1); 
					$list.append($nextBtn);
				}

				if(curPagination > 0) {
					$prevBtn = $blankItem.clone();
					$prevBtn.find("a").attr('href', curPagination-1).html("<i class='fa fa-angle-left'></i>"); 
					$list.prepend($prevBtn); 
				}

				$list.find("a").click(paginationClick)
					.hover(function() { 
						$(this).addClass(options.paginationHoverClass); 
					}, function() { 
						$(this).removeClass(options.paginationHoverClass); 
					}); 

				return $list;
			}

			/**
	 		 * Load children via ajax call, attach them to $target and show. 
			 *
			 * @param int id ID of the page having children to show
			 * @param jQuery $target Item to attach children to
		 	 * @param int start If not starting from first item, num of item to start with
			 * @param bool beginList Set to true if this is the first call to create the list
			 * @param bool pagination Set to false if you don't want pagination, otherwise leave it out
			 * @param bool replace Should any existing list be replaced (true) or appended (false)
			 *
			 */
			function loadChildren(id, $target, start, beginList, pagination, replace, callback) {
				
				if(pagination == undefined) pagination = true; 
				if(replace == undefined) replace = false;

				var processChildren = function(data) {

					if(data && data.error) {
						ProcessWire.alert(data.message); 
						$loading.hide();
						ignoreClicks = false;
						return; 
					}

					var $children = listChildren($(data.children)); 
					var nextStart = data.start + data.limit; 
					//var openPageKey = id + '-' + start;
				
					if($target.hasClass('PageListItem')) {
						setNumChildren($target, data.page.numChildren, data.page.numTotal);
					}

					if(data.page.numChildren > nextStart) {
						var $a = $("<a></a>").attr('href', nextStart).data('pageId', id).text(options.moreLabel).click(clickMore); 
						$children.append($("<ul></ul>").addClass('PageListActions actions').append($("<li></li>").addClass('PageListActionMore').append($a)));
					}
					if(pagination && (data.page.numChildren > nextStart || data.start > 0)) {
						$children.prepend(getPaginationList(id, data.start, data.limit, data.page.numChildren));
					}

					$children.hide();

					if(beginList) {
						var $listRoot; 
						$listRoot = listChildren($(data.page)); 
						if(options.showRootPage) $listRoot.children(".PageListItem").addClass("PageListItemOpen"); 
							else $listRoot.children('.PageListItem').hide().parent('.PageList').addClass('PageListRootHidden'); 
						$listRoot.append($children); 
						$target.append($listRoot);

					} else if($target.hasClass('PageList')) {
					
						var $newChildren = $children.children(".PageListItem, .PageListActions"); 
						if(replace) {
							// $target.children(".PageListItem, .PageListActions").replaceWith($newChildren); // doesn't work w/jQuery 1.9+
							$target.children('.PageListItem, .PageListActions').remove();
						}
						$target.append($newChildren);

					} else {
						$target.after($children); 
					}

					if($loading.parent().is('.PageListRoot')) {
						$loading.hide();
					} else {
						$loading.fadeOut('fast');
					}

					if(replace) {
						$children.show();
						loaded();
						if(callback != undefined) callback();
					} else { 
						$children.slideDown(options.speed, function() {
							loaded();
							if(callback != undefined) callback();
						}); 
					}
					
					$children.prev('.PageListItem').data('start', data.start);

					// if a pagination is requested to be opened, and it exists, then open it
					/*
					if(options.openPagination > 1) {
						//var $a = $(".PageListPagination" + (options.openPagination-1) + ">a");
						var $a = $(".PageListPagination a[href=" + (options.openPagination-1) + "]");
						if($a.size() > 0) {
							$a.click();	
							options.openPagination = 0;
						} else {
							// last pagination link
							$(".PageListPagination9 a").click();
						}
					}
					*/
				}; 

				if(!replace) $target.append($loading.fadeIn('fast')); 
			
		
				var key = id + '-' + start;
				if(typeof options.openPageData[key] != "undefined" 
					&& !$target.hasClass('PageListID7') // trash
					&& !$target.hasClass('PageListForceReload')) {
					processChildren(options.openPageData[key]);
					return;
				} 
				
				// @teppokoivula PR #1052
				var ajaxURL = options.ajaxURL + 
					"?id=" + id + 
					"&render=JSON&start=" + start + 
					"&lang=" + options.langID + 
					"&open=" + options.openPageIDs[0] + 
					"&mode=" + options.mode;
				if(options.labelName.length) ajaxURL += '&labelName=' + options.labelName;
				$.getJSON(ajaxURL)
					.done(function(data, textStatus, jqXHR) {
						processChildren(data);
					})
					.fail(function(jqXHR, textStatus, errorThrown) {
						processChildren({
							error: 1,
							message: !jqXHR.status ? options.ajaxNetworkError : options.ajaxUnknownError
						});
					});
				// end #1052
			}

			/**
			 * Given a list of pages, generates a list of them
			 *
			 * @param jQuery $children
			 *
			 */ 
			function listChildren($children) {

				var $list = $("<div></div>").addClass("PageList");
				var $ul = $list;

				$children.each(function(n, child) {
					$ul.append(listChild(child)); 
				}); 	
				
				addClickEvents($ul);
				
				return $list; 
			}

			/**
			 * Set a parent to reload its children on next access rather than using cached data
			 * 
			 * @param $pageListItem .PageList or .PageListItem element
			 * @param reloadNow Reload the list now? (default=false)
			 *
			 */
			function setForceReload($pageListItem, reloadNow) {
				
				if($pageListItem.hasClass('PageList')) $pageListItem = $pageListItem.prev('.PageListItem');
				
				$pageListItem.addClass('PageListForceReload');
			
				var id = $pageListItem.data('pageId');
				var prefix = id + '-';
				
				if(typeof options.openPageData != "undefined") {
					// remove cached items from openPageData
					var openPageData = {};
					for(var key in options.openPageData) {
						if(key.indexOf(prefix) === 0) {
							// skip it
						} else {
							openPageData[key] = options.openPageData[key];
						}
					}
					options.openPageData = openPageData;
				}
			
				if(typeof reloadNow != "undefined" && reloadNow) {
					var $a = $pageListItem.children('a.PageListPage');
					if($pageListItem.hasClass('PageListItemOpen')) {
						$a.click();
						setTimeout(function() { $a.click(); }, 250);
					} else {
						$a.click();
					}
				}
			}

			/**
			 * 
			 * @param $ul Any element that contains items needing click events attached
			 * 
			 */
			function addClickEvents($ul) {

				$("a.PageListPage", $ul).click(clickChild);
				$ul.on('click', '.PageListActionMove a', clickMove); 
				// $(".PageListActionMove a", $ul).click(clickMove);
				$(".PageListActionSelect a", $ul).click(clickSelect);
				$(".PageListTriggerOpen:not(.PageListID1) > a.PageListPage", $ul).click();
				$(".PageListActionExtras > a:not(.clickExtras)", $ul).addClass('clickExtras').on('click', clickExtras);
				
				// if(options.useHoverActions) $(".PageListActionExtras > a", $ul).on('mouseover', clickExtras);
			}

			/**
			 * Given a single page, generates the list item for it
			 *
			 * @param map child
			 *
			 */
			function listChild(child) {
				
				var $li = $("<div></div>").data('pageId', child.id).addClass('PageListItem').addClass('PageListTemplate_' + child.template); 
				var $a = $("<a></a>")
					.attr('href', '#')
					.attr('title', child.path)
					.html(child.label)
					.addClass('PageListPage label'); 
				
				$li.addClass(child.numChildren > 0 ? 'PageListHasChildren' : 'PageListNoChildren').addClass('PageListID' + child.id); 
				if(child.status == 0) $li.addClass('PageListStatusOff disabled');
				if(child.status & 2048) $li.addClass('PageListStatusUnpublished secondary'); 
				if(child.status & 1024) $li.addClass('PageListStatusHidden secondary'); 
				if(child.status & 512) $li.addClass('PageListStatusTemp secondary'); // typically combined with PageListStatusUnpublished
				if(child.status & 16) $li.addClass('PageListStatusSystem'); 
				if(child.status & 8) $li.addClass('PageListStatusSystem'); 
				if(child.status & 4) $li.addClass('PageListStatusLocked'); 
				if(child.addClass && child.addClass.length) $li.addClass(child.addClass); 
				if(child.type && child.type.length > 0) if(child.type == 'System') $li.addClass('PageListStatusSystem'); 

				$(options.openPageIDs).each(function(n, id) {
					id = parseInt(id);
					if(child.id == id) $li.addClass('PageListTriggerOpen'); 
				}); 

				$li.append($a); 
				setNumChildren($li, child.numChildren, child.numTotal, true);
		
				if(child.note && child.note.length) $li.append($("<span>" + child.note + "</span>").addClass('PageListNote detail')); 	
				
				var $actions = $("<ul></ul>").addClass('PageListActions actions'); 
				var links = options.rootPageID == child.id ? [] : [{ name: options.selectSelectLabel, url: options.selectSelectHref }]; 
				if(options.mode == 'actions') {
					links = child.actions; 
				} else if(options.selectAllowUnselect) {
					if(child.id == $container.val()) links = [{ name: options.selectUnselectLabel, url: options.selectUnselectHref }]; 
				}

				var $lastAction = null;
				var $extrasLink = null; // link that toggles extra actions
				var extras = {}; // extra actions
				var hasExtrasLink = false; // only referenced if options.useNarrowActions
		
				if(options.useNarrowActions) {
					for(var n = 0; n < links.length; n++) {
						if(typeof links[n].extras != "undefined") hasExtrasLink = true;
					}
				}
				
				$(links).each(function(n, action) {
					var actionName;
					if(action.name == options.selectSelectLabel) {
						actionName = 'Select';
					} else if(action.name == options.selectUnselectLabel) {
						actionName = 'Select';
					} else {
						actionName = action.cn; // cn = className
						if(options.useNarrowActions && hasExtrasLink
							&& (actionName != 'Edit' && actionName != 'View' && actionName != 'Extras')) {
							// move non-edit/view actions to extras when in narrow mode
							extras[actionName] = action;
							return;
						}
					}
					
					var $a = $("<a></a>").html(action.name).attr('href', action.url);
					
					if(!isModal) {
						if(action.cn == 'Edit') {
							$a.addClass('pw-modal pw-modal-large pw-modal-longclick');
							$a.attr('data-buttons', '#ProcessPageEdit > .Inputfields > .InputfieldSubmit .ui-button');
						} else if(action.cn == 'View') {
							$a.addClass('pw-modal pw-modal-large pw-modal-longclick');
						}
					}
					if(typeof action.extras != "undefined") {
						for(var key in action.extras) {
							extras[key] = action.extras[key];
						}
						$extrasLink = $a;
					}
					
					var $action = $("<li></li>").addClass('PageListAction' + actionName).append($a);
					if(actionName == 'Extras') $lastAction = $action; 
						else $actions.append($action);
				});
				
				if($extrasLink) $extrasLink.data('extras', extras);
				
				if($lastAction) {
					$actions.append($lastAction);
					$lastAction.addClass('ui-priority-secondary');
				}

				$li.append($actions); 
				return $li;
			}

			/**
			 * Get number of children for given .PageListItem
			 * 
			 */
			function getNumChildren($item, getTotal) {
				if(typeof getTotal == "undefined") var getTotal = false;
				if(getTotal) {
					var n = $item.attr('data-numTotal');
				} else {
					var n = $item.attr('data-numChild');
				}
				return n && n.length > 0 ? parseInt(n) : 0;
			}
			
			function getNumTotal($item) {
				return getNumChildren($item, true);
			}

			/**
			 * Set number of children for given PageListItem
			 * 
			 */
			function setNumChildren($item, numChildren, numTotal, addNew) {
				
				if(typeof numTotal == "undefined") var numTotal = numChildren;
				if(typeof addNew == "undefined") var addNew = false;
			
				var $numChildren = addNew ? '' : $item.children('.PageListNumChildren');
				var n = numChildren === false ? numTotal : numChildren;
				
				if(addNew || !$numChildren.length) {
					$numChildren = $('<span></span>').addClass('PageListNumChildren detail');
					addNew = true;
				}	
				
				if(n < 1) {
					$item.removeClass('PageListHasChildren').addClass('PageListNoChildren');
					if(numTotal !== false) numTotal = 0;
				} else {
					$item.removeClass('PageListNoChildren').addClass('PageListHasChildren');
				}
				
				if(numTotal === false) {
					numTotal = getNumTotal($item);
				} else {
					$item.attr('data-numTotal', numTotal);
				}
				
				if(numChildren === false) {
					numChildren = getNumChildren($item);
				} else {
					if(numChildren < 0) numChildren = 0;
					$item.attr('data-numChild', numChildren);
				}

				var numLabel = '';
				switch(options.qtyType) {
					case 'total':
						numLabel = numTotal;
						break;
					case 'total/children':
						var slash = "<span class='ui-priority-secondary'>/</span>";
						numLabel = numTotal > 0 && numTotal != numChildren ? numTotal + slash + numChildren : numTotal;
						break;
					case 'children/total':
						var slash = "<span class='ui-priority-secondary'>/</span>";
						numLabel = numTotal > 0 && numTotal != numChildren ? numChildren + slash + numTotal : numTotal;
						break;
					case 'id':
						numLabel = $item.data('pageId');
						break;
					default:
						numLabel = numChildren;
				}
				if(!numLabel) numLabel = '';
				$numChildren.html(numLabel);
				
				if(addNew) $item.append($numChildren);
			}

			/**
			 * Set total/descendants
			 * 
			 */
			function setNumTotal($item, numTotal) {
				setNumChildren($item, false, numTotal);
			}

			/**
			 * Recursively adjust total/descendants number up the tree by given amount (n)
			 * 
			 */
			function adjustNumTotal($item, n) {
				var numTotal = getNumTotal($item);
				numTotal += n;
				if(numTotal < 0) numTotal = 0;
				setNumTotal($item, numTotal);
				var $parentItem = $item.closest('.PageList').prev('.PageListItem');
				if($parentItem.length) adjustNumTotal($parentItem, n);
			}

			/**
			 * Extra actions button click handler
			 * 
			 */
			function clickExtras(e) {

				var $a = $(this);
				var extras = $a.data('extras');
				if(typeof extras == "undefined") return false;
			
				var $li = $a.closest('.PageListItem');
				var $actions = $a.closest('.PageListActions');
				var $lastItem = null;
				var $icon = $a.children('i.fa');
				var $extraActions = $actions.find("li.PageListActionExtra");
			
				/*
				if($extraActions.length && e.type != 'click') {
					// mouseover only opens, but a click is required to close
					return;
				}
				*/
				
				$icon.toggleClass('fa-flip-horizontal');
			
				if($extraActions.length) {
					$extraActions.fadeOut(100, function() {
						$extraActions.remove();
					}); 
					return false;
				}
				
				for(var extraKey in extras) {
					
					var extra = extras[extraKey];
					var $extraLink = $("<a />")
						.addClass('PageListActionExtra PageListAction' + extra.cn)
						.attr('href', extra.url)
						.html(extra.name);
				
					/*
					if(extra.cn == 'Trash') {
						// handler for trash action
						$extraLink.click(function() {
							trashPage($li);
							return false;
						});
						
					} else 
					*/
					if(typeof extra.ajax != "undefined" && extra.ajax == true) {
						// ajax action
						$extraLink.click(function () {
							
							$li.find('.PageListActions').hide();
							var $spinner = $(options.spinnerMarkup);
							var href = $(this).attr('href');
							var actionName = href.match(/[\?&]action=([-_a-zA-Z0-9]+)/)[1];
							var pageID = parseInt(href.match(/[\?&]id=([0-9]+)/)[1]);
							var tokenName = $("#PageListContainer").attr('data-token-name');
							var tokenValue = $("#PageListContainer").attr('data-token-value');
							var postData = {
								action: actionName,
								id: pageID, 
							};
							postData[tokenName] = tokenValue;
							$li.append($spinner);
							
							$.post(href + '&render=json', postData, function (data) {
								
								if (data.success) {
									
									$li.fadeOut('fast', function() {
										
										var addNew = false;
										var removeItem = data.remove;
										var refreshChildren = data.refreshChildren;
										var $liNew = false;
										
										if(typeof data.child != "undefined") {
											// prepare update existing item
											$liNew = listChild(data.child);
										} else if(typeof data.newChild != "undefined") {
											// prepare append new item
											$liNew = listChild(data.newChild);
											addNew = true; 
										}
										
										// display a message for a second to let them know what was done
										if($liNew) {
											var $msg = $("<span />").addClass('notes').html(data.message);
											$msg.prepend("&nbsp;<i class='fa fa-check-square ui-priority-secondary'></i>&nbsp;");
											$liNew.append($msg);
											addClickEvents($liNew);
										}
										
										if(addNew) {
											// append new item
											$spinner.fadeOut('normal', function() { $spinner.remove() }); 
											$liNew.hide();
											$li.after($liNew);
											$liNew.slideDown();
											setForceReload($liNew.closest('.PageList'));
										} else if($liNew) {
											// update existing item
											if($li.hasClass('PageListItemOpen')) $liNew.addClass('PageListItemOpen');
											$li.replaceWith($liNew);
										}
										
										$li.fadeIn('fast', function () {
											// display message for 1 second, then remove
											setTimeout(function () {
												$msg.fadeOut('normal', function () { 
													var $parentItem = $liNew.closest('.PageList').prev('.PageListItem');
													var numChildren = getNumChildren($parentItem);
													var numTotal = getNumTotal($parentItem);
													if(removeItem) {
														numChildren--;
														adjustNumTotal($parentItem, -1);
													} else if(addNew) {
														numChildren++;
														adjustNumTotal($parentItem, 1);
													}
													setNumChildren($parentItem, numChildren, false);
													setForceReload($parentItem);
													if(removeItem) {	
														$liNew.next('.PageList').fadeOut('fast');
														$liNew.fadeOut('fast', function() {
															$liNew.remove();
														});
													} else {
														$msg.remove();
													}
												});
											}, 1000);
										});
									
										// refresh the children of the page represented by refreshChildren
										if(refreshChildren) {
											var $refreshParent = $(".PageListID" + refreshChildren);
											if($refreshParent.length) setForceReload($refreshParent, true);
										}
									});
									
								} else {
									// data.success === false, so display error
									$spinner.remove();
									ProcessWire.alert(data.message);
								}
							});
							return false;
						});
					} else {
						// some other action where the direct URL can be used, so we don't need to do anything
					}
					
					var $extraLinkItem = $("<li />").addClass('PageListActionExtra PageListAction' + extra.cn).append($extraLink);
					$extraLink.hide();
					
					if(extra.cn == 'Trash') {
						$li.addClass('trashable');
						// ensure the Trash item is always the last one
						$lastItem = $extraLinkItem;
					} else {
						$actions.append($extraLinkItem);
					}
				}
				
				if($lastItem) $actions.append($lastItem);
				
				$actions.find(".PageListActionExtra a").fadeIn(50, function() {
					$(this).css('display', 'inline-block');
				});
				
				return false;
			}

			/**
			 * Event called when a page label is clicked on
			 *
			 * @param event e
			 *
			 */
			function clickChild(e) {

				var $t = $(this); 
				var $li = $t.parent('.PageListItem'); 
				var id = $li.data('pageId');

				if(ignoreClicks && !$li.hasClass("PageListTriggerOpen")) return false; 

				if($root.is(".PageListSorting") || $root.is(".PageListSortSaving")) {
					return false; 
				}

				if($li.hasClass("PageListItemOpen")) {
					var collapseThis = true;
					if($li.hasClass('PageListID1') && !$li.hasClass('PageListForceReload') && options.mode != 'select') {
						var $collapseItems = $(this).closest('.PageListRoot').find('.PageListItemOpen:not(.PageListID1)');
						if($collapseItems.length) {
							// collapse all open items, except homepage, when homepage link is collapsed
							$root.find('.PageListItemOpen:not(.PageListID1)').each(function() {
								$(this).children('a.PageListPage').click();
							});
							collapseThis = false;
						}
					}
					if(collapseThis) {	
						$li.removeClass('PageListItemOpen').next(".PageList").slideUp(options.speed, function() {
							$(this).remove();
						});
					}
				} else {
					$li.addClass('PageListItemOpen');
					var numChildren = getNumChildren($li); 
					if(numChildren > 0 || $li.hasClass('PageListForceReload')) {
						ignoreClicks = true; 
						var start = getOpenPageStart(id);
						loadChildren(id, $li, start, false); 
					}
				}
	
				if(options.mode != 'select') {
					setTimeout(function() { updateOpenPageIDs() }, 250); 
				}

				return false;
			}

			/**
			 * Get the pagination "start" index for the given open page ID 
			 * 
			 * @param id
			 * @returns {number}
			 * 
			 */
			function getOpenPageStart(id) {
				var start = 0;
				for(n = 0; n < options.openPageIDs.length; n++) {
					var key = options.openPageIDs[n];
					if(key.indexOf('-') == -1) continue;
					var parts = options.openPageIDs[n].split('-');
					var _id = parseInt(parts[0]);
					if(_id == id) {
						start = parseInt(parts[1]);
						break;
					}
				}
				return start;
			}

			/**
			 * Update the currentOpenPageIDs list and cookie to reflect the current open pages
			 * 
			 */
			function updateOpenPageIDs() {
				currentOpenPageIDs = [];
				$('.PageListItemOpen').each(function() {
					var id = $(this).data('pageId');
					var start = $(this).data('start');
					if(typeof start == "undefined" || start === null) {
						start = 0;
					} else {
						var start = parseInt(start);
					}
					if(jQuery.inArray(id, currentOpenPageIDs) == -1) {
						currentOpenPageIDs.push(id + '-' + start); // id.start
					}
				});
				// console.log(currentOpenPageIDs);
				$.cookie('pagelist_open', currentOpenPageIDs);
			}

			/**
			 * Force refresh the list that pageID appears in
			 * 
			 * @param pageID
			 * @param animate
			 * 
			 */
			function refreshList(pageID, animate) {
				
				var $parentList = $('.PageListID' + pageID).parent('.PageList');
				var $parentItem = $parentList.prev('.PageListItem');
				if(!$parentItem.length) return;
				
				var parentID = $parentItem.attr('class').match(/PageListID(\d+)/)[1];
				var start = 0;
				var $pagination = $parentList.children("ul." + options.paginationClass);
				var paginationInfo = $pagination.data('paginationInfo');
				var $removeItems = null;
				
				if(paginationInfo) {
					var $a = $pagination.find('.pw-link-active');
					if($a.length) start = parseInt($a.attr('href')) * paginationInfo.limit;
					if(start === NaN) start = 0;
				}
				if(parentID == 1) {
					$removeItems = $parentList.children();
					$parentList = $parentItem;
				}
				setForceReload($parentList);
				loadChildren(parentID, $parentList, start, false, true, true, function() {
					if($removeItems && $removeItems.length) $removeItems.remove();
				});
			}
			
			/**
			 * Event called when the 'more' action/link is clicked on
			 *
			 * @param event e
			 *
			 */
			function clickMore(e) {

				var $t = $(this); 
				var $actions = $t.parent('li').parent('ul.PageListActions'); 
				var $pageList = $actions.parent('.PageList'); 
				var id = $t.data('pageId');
				var nextStart = parseInt($t.attr('href')); 
		
				loadChildren(id, $pageList, nextStart, false); 
				$actions.remove();
				return false; 
			}

			/**
			 * Event called when the 'move' action/link is clicked on
			 *
			 * @param event e
			 *
			 */
			function clickMove() {

				if(ignoreClicks) return false;
				

				var $t = $(this);
				
				if($(".PageListItem:visible").length == 1) {
					// no other items to sort/move to
					$t.css('text-decoration', 'line-through').addClass('ui-state-disabled');
					return false;
				}
				
				var $li = $t.parent('li').parent('ul.PageListActions').parent('.PageListItem'); 

				// $li.children(".PageListPage").click(); 
				if($li.hasClass("PageListItemOpen")) $li.children(".PageListPage").click(); // @somatonic PR163

				// make an invisible PageList placeholder that allows 'move' action to create a child below this
				$root.find('.PageListItemOpen').each(function() {
					var numChildren = getNumChildren($(this)); 
					// if there are children and the next sibling doesn't contain a visible .PageList, then don't add a placeholder
					if(parseInt(numChildren) > 1 && $(this).next().find(".PageList:visible").length == 0) {
						return; 
					}
					var $ul = $("<div></div>").addClass('PageListPlaceholder').addClass('PageList');
					$ul.append($("<div></div>").addClass('PageListItem PageListPlaceholderItem').html('&nbsp;'));
					$(this).after($ul);
					//$(this).prepend($ul.clone()); 
					//$(this).addClass('PageListItemNoSort'); 
				}); 

				var sortOptions = {
					stop: stopMove, 
					helper: 'PageListItemHelper', 
					items: '.PageListItem:not(.PageListItemOpen)',
					placeholder: 'PageListSortPlaceholder',
					start: function(e, ui) {
						$(".PageListSortPlaceholder").css('width', ui.item.children(".PageListPage").outerWidth() + 'px'); 
					}
				};

				var $sortRoot = $root.children('.PageList').children('.PageList');

				var $cancelLink = $("<a href='#'>" + options.selectCancelLabel + "</a>").click(function() { 
					return cancelMove($li); 
				});
				
				var $actions = $li.children("ul.PageListActions");
				var $moveAction = $("<span class='PageListMoveNote detail'><i class='fa fa-fw fa-sort'></i> " + options.moveInstructionLabel + "<i class='fa fa-fw fa-angle-left'></i></span>");
				$moveAction.append($cancelLink);
			
				$actions.before($moveAction); 
				
				$li.addClass('PageListSortItem'); 
				$li.parent('.PageList').attr('id', 'PageListMoveFrom'); 

				$root.addClass('PageListSorting'); 
				$sortRoot.addClass('PageListSortingList').sortable(sortOptions); 

				return false; 

			}

			/**
			 * Remove everything setup from an active 'move' 
			 *
			 * @param jQuery $li List item that initiated the 'move'
			 * @return bool
			 *
			 */
			function cancelMove($li) {
				var $sortRoot = $root.find('.PageListSortingList'); 
				$sortRoot.sortable('destroy').removeClass('PageListSortingList'); 
				$li.removeClass('PageListSortItem').parent('.PageList').removeAttr('id'); 
				$li.find('.PageListMoveNote').remove();
				$root.find(".PageListPlaceholder").remove();
				$root.removeClass('PageListSorting'); 
				return false; 
			}
			
			/**
			 * Remove everything setup from an active 'move'
			 *
			 * @param jQuery $li List item that initiated the 'move'
			 * @return bool
			 *
			 */
			function trashPage($li) {
				var $trash = $root.find('.PageListID' + options.trashPageID);
				if(!$trash.hasClass('PageListItemOpen')) {
					$root.removeClass('PageListSorting'); 
					$trash.children('a').click();
					$root.addClass('PageListSorting'); 
				}
				var $trashList = $trash.next('.PageList');
				if($trashList.length == 0) {
					$trashList = $("<div class='PageList'></div>");
					$trash.after($trashList);
				}
				$trashList.prepend($li);
				var ui = { item: $li };
				stopMove(null, ui);
			}
			
			/**
			 * Event called when the mouse stops after performing a 'move'
			 *
			 * @param event e
			 * @param jQueryUI ui
			 * @return bool
			 *
			 */
			function stopMove(e, ui) {

				var $li = ui.item; 
				var $a = $li.children('.PageListPage'); 
				var id = parseInt($li.data('pageId')); 
				var $ul = $li.parent('.PageList'); 
				var $from = $("#PageListMoveFrom")

				// get the previous sibling .PageListItem, and skip over the pagination list if it's there
				var $ulPrev = $ul.prev().is('.PageListItem') ? $ul.prev() : $ul.prev().prev();
				var parent_id = parseInt($ulPrev.data('pageId')); 

				// check if item was moved to an invalid spot
				// in this case, a spot between another open PageListItem and its PageList
				var $liPrev = $li.prev(".PageListItem"); 
				if($liPrev.is(".PageListItemOpen")) return false; 

				// check if item was moved into an invisible parent placeholder PageList
				if($ul.is('.PageListPlaceholder')) {
					var $ulNext = $ul.next();
					if($ulNext.is('.PageList:visible')) {
						// first item at top of list ended up in placeholder, but belongs in next sibling PageList
						$ulNext.prepend($li);
						$ul = $ulNext;
					} else {
						// if so, it's no longer a placeholder, but a real PageList
						$ul.removeClass('PageListPlaceholder').children('.PageListPlaceholderItem').remove();
					}
				}

				$root.addClass('PageListSortSaving'); 
				cancelMove($li); 

				// setup to save the change
				$li.append($loading.fadeIn('fast')); 
				var sortCSV = '';
			
				// create a CSV string containing the order of Page IDs	
				$ul.children(".PageListItem").each(function() {
					sortCSV += $(this).data('pageId') + ','; 
				}); 

				var postData = {
					id: id, 
					parent_id: parent_id, 
					sort: sortCSV
				}; 

				postData[$('#PageListContainer').attr('data-token-name')] = $('#PageListContainer').attr('data-token-value'); // CSRF Token

				var success = 'unknown'; 
			
				// save the change	
				$.post(options.ajaxMoveURL, postData, function(data) {

					$loading.fadeOut('fast');

					$a.fadeOut('fast', function() {
						$(this).fadeIn("fast")
						$li.removeClass('PageListSortItem'); 
						$root.removeClass('PageListSorting');
					}); 

					if(data && data.error) {
						ProcessWire.alert(data.message); 
					}

					// if item moved from one list to another, then update the numChildren counts
					if(!$ul.is("#PageListMoveFrom")) {
						// update count where item came from
						var $fromItem = $from.prev(".PageListItem"); 	
						var numChildren = getNumChildren($fromItem);
						var numTotal = getNumTotal($fromItem);
						if(numChildren > 0) {
							numChildren--;
							adjustNumTotal($fromItem, -1);
						} else {
							$from.remove(); // empty list, no longer needed
						}
						setNumChildren($fromItem, numChildren, false);
						setForceReload($fromItem);
				
						// update count where item went to	
						var $toItem = $ul.prev(".PageListItem"); 
						numChildren = getNumChildren($toItem) + 1;
						adjustNumTotal($toItem, 1);
						setNumChildren($toItem, numChildren, false);
						setForceReload($toItem);
					}
					$from.attr('id', ''); // remove tempoary #PageListMoveFrom
					$root.removeClass('PageListSortSaving'); 

				}, 'json'); 

				// trigger pageMoved event: @teppokoivula
				$li.trigger('pageMoved'); 

				return true; // whether or not to allow the sort
			}

			/**
			 * Event called when the "select" link is clicked on in select mode
			 *
			 * This also triggers a 'pageSelected' event on the attached text input. 
			 *
			 * @see setupSelectMode()
			 *
			 */
			function clickSelect() {

				var $t = $(this); 
				var $li = $t.parent('li').parent('ul.PageListActions').parent('.PageListItem'); 
				var id = $li.data('pageId');
				var $a = $li.children(".PageListPage"); 
				var title = $a.text();
				var url = $a.attr('title'); 
				var $header = $root.children(".PageListSelectHeader"); 

				if($t.text() == options.selectUnselectLabel) {
					// if unselect link clicked, then blank out the values
					id = 0; 
					title = '';
				}

				if(id != $container.val()) $container.val(id).change();

				if(options.selectShowPageHeader) { 
					$header.children(".PageListSelectName").text(title); 
				}
				
				// trigger pageSelected event	
				$container.trigger('pageSelected', { 
					id: id, 
					url: url, 
					title: title, 
					a: $a 
				}); 	


				$header.find(".PageListSelectActionToggle").click(); // close the list

				// jump to specified anchor, if provided
				if(options.selectSelectHref == '#') return false; 
				return true; 
			}


			// initialize the plugin
			init(); 

		}); 
	};
})(jQuery); 

