/** * 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: "", // 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 = $("
"); 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 = $("").addClass('PageListActions PageListSelectActions actions'); var $pageLabel = $("

").addClass("PageListSelectName"); if(options.selectShowPageHeader) $pageLabel.append($loading); var $action = $("") .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($("
  • ").append($action)); $root.append($("
    ").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 = '' + parentPath + ' '; } 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 = $("").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 = $("
  •  
  • ").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 = $("").html(pagination+1).attr('href', pagination).addClass(options.paginationLinkClass); var $item = $("
  • ").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("…"); //if(!$separator) $separator = $blankItem.clone().html("…"); 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("").attr('href', curPagination+1); $list.append($nextBtn); } if(curPagination > 0) { $prevBtn = $blankItem.clone(); $prevBtn.find("a").attr('href', curPagination-1).html(""); $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 = $("").attr('href', nextStart).data('pageId', id).text(options.moreLabel).click(clickMore); $children.append($("").addClass('PageListActions actions').append($("
  • ").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 = $("
    ").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 = $("
    ").data('pageId', child.id).addClass('PageListItem').addClass('PageListTemplate_' + child.template); var $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($("" + child.note + "").addClass('PageListNote detail')); var $actions = $("").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 = $("").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 = $("
  • ").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 = $('').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 = "/"; numLabel = numTotal > 0 && numTotal != numChildren ? numTotal + slash + numChildren : numTotal; break; case 'children/total': var slash = "/"; 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 = $("") .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 = $("").addClass('notes').html(data.message); $msg.prepend("  "); $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 = $("
  • ").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 = $("
    ").addClass('PageListPlaceholder').addClass('PageList'); $ul.append($("
    ").addClass('PageListItem PageListPlaceholderItem').html(' ')); $(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 = $("
    " + options.selectCancelLabel + "").click(function() { return cancelMove($li); }); var $actions = $li.children("ul.PageListActions"); var $moveAction = $(" " + options.moveInstructionLabel + ""); $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 = $("
    "); $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);