diff --git a/wire/modules/AdminTheme/AdminThemeDefault/scripts/main.js b/wire/modules/AdminTheme/AdminThemeDefault/scripts/main.js index 59e94b63..9f24bbc0 100644 --- a/wire/modules/AdminTheme/AdminThemeDefault/scripts/main.js +++ b/wire/modules/AdminTheme/AdminThemeDefault/scripts/main.js @@ -96,9 +96,19 @@ var ProcessWireAdminTheme = { }, _renderItem: function(ul, item) { if(item.label == item.template) item.template = ''; - return $("
  • ") - .append("" + item.label + " " + item.template + "") - .appendTo(ul); + var $label = $("").text(item.label).css('margin-right', '3px'); + if(item.unpublished) $label.css('text-decoration', 'line-through'); + if(item.hidden) $label.addClass('ui-priority-secondary'); + if(typeof item.icon != "undefined" && item.icon.length) { + var $icon = $('').addClass('fa fa-fw fa-' + item.icon).css('margin-right', '2px'); + $label.prepend($icon); + } + var $a = $("") + .attr('href', item.edit_url) + .attr('title', item.tip) + .append($label) + .append($("").text(item.template)); + return $("
  • ").append($a).appendTo(ul); } }); @@ -118,7 +128,7 @@ var ProcessWireAdminTheme = { $("#topnav").show(); }, source: function(request, response) { - var url = $input.parents('form').attr('data-action') + 'for?get=template_label,title&include=all&admin_search=' + request.term; + var url = $input.parents('form').attr('action') + '?q=' + request.term; $.getJSON(url, function(data) { var len = data.matches.length; if(len < data.total) $status.text(data.matches.length + '/' + data.total); @@ -130,7 +140,12 @@ var ProcessWireAdminTheme = { page_id: item.id, template: item.template_label ? item.template_label : '', edit_url: item.editUrl, - type: item.type + type: item.type, + tip: item.tip, + unpublished: (typeof item.unpublished != "undefined" ? item.unpublished : false), + hidden: (typeof item.hidden != "undefined" ? item.hidden : false), + locked: (typeof item.locked != "undefined" ? item.locked : false), + icon: (typeof item.icon != "undefined" ? item.icon : '') } })); }); diff --git a/wire/modules/AdminTheme/AdminThemeDefault/scripts/main.min.js b/wire/modules/AdminTheme/AdminThemeDefault/scripts/main.min.js index 2bd328f1..4ce0f1ca 100644 --- a/wire/modules/AdminTheme/AdminThemeDefault/scripts/main.min.js +++ b/wire/modules/AdminTheme/AdminThemeDefault/scripts/main.min.js @@ -1 +1 @@ -var ProcessWireAdminTheme={init:function(){var b=$("#head_button > button.pw-dropdown-toggle").hide();this.setupCloneButton();ProcessWireAdmin.init();this.setupSearch();this.setupMobile();var a=$("body");if(a.hasClass("hasWireTabs")&&$("ul.WireTabs").length==0){a.removeClass("hasWireTabs")}$("#content").removeClass("pw-fouc-fix");a.removeClass("pw-init").addClass("pw-ready");if(b.length>0){b.show()}},setupCloneButton:function(){if($("body").is(".modal")){return}var b=$("button.pw-head-button, button.head_button_clone");if(b.length==0){return}var a=$("#head_button");if(a.length==0){a=$("
    ").prependTo("#breadcrumbs .pw-container")}b.each(function(){var e=$(this);var d=e.parent("a");var c;if(d.length>0){c=e.parent("a").clone(true);a.prepend(c)}else{if(e.hasClass("pw-head-button")||e.hasClass("head_button_clone")){c=e.clone(true);c.attr("data-from_id",e.attr("id")).attr("id",e.attr("id")+"_copy");c.click(function(){$("#"+$(this).attr("data-from_id")).click();return false});a.prepend(c)}}});a.show()},setupSearch:function(){$.widget("custom.adminsearchautocomplete",$.ui.autocomplete,{_renderMenu:function(e,c){var f=this;var d="";e.attr("id","ProcessPageSearchAutocomplete");$.each(c,function(g,h){if(h.type!=d){$("
  • "+h.type+"
  • ").addClass("ui-widget-header").appendTo(e);d=h.type}f._renderItemData(e,h)})},_renderItem:function(c,d){if(d.label==d.template){d.template=""}return $("
  • ").append(""+d.label+" "+d.template+"").appendTo(c)}});var b=$("#ProcessPageSearchQuery");var a=$("#ProcessPageSearchStatus");b.adminsearchautocomplete({minLength:2,position:{my:"right top",at:"right bottom"},search:function(c,d){a.html("")},open:function(c,d){$("#topnav").hide()},close:function(c,d){$("#topnav").show()},source:function(e,c){var d=b.parents("form").attr("data-action")+"for?get=template_label,title&include=all&admin_search="+e.term;$.getJSON(d,function(g){var f=g.matches.length;if(f50){if(!g.hasClass("collapse-topnav")){g.addClass("collapse-topnav");a=g.width()}}else{if(a>0){var f=g.width();if(g.hasClass("collapse-topnav")&&f>a){g.removeClass("collapse-topnav");a=0}}}h.children(".collapse-topnav-menu").children("a").click(function(){if($(this).is(".hover")){$(this).mouseleave()}else{$(this).mouseenter()}return false});var d=$(".WireTabs");if(d.length<1){return}d.each(function(){var j=$(this);var i=j.height();if(i>65){if(!g.hasClass("collapse-wiretabs")){g.addClass("collapse-wiretabs");c=g.width()}}else{if(c>0){var k=g.width();if(g.hasClass("collapse-wiretabs")&&k>c){g.removeClass("collapse-wiretabs");c=0}}}})};b();$(window).resize(b)}};$(document).ready(function(){ProcessWireAdminTheme.init();$("a.notice-remove","#notices").click(function(){$("#notices").slideUp("fast",function(){$(this).remove()});return false})}); \ No newline at end of file +var ProcessWireAdminTheme={init:function(){var b=$("#head_button > button.pw-dropdown-toggle").hide();this.setupCloneButton();ProcessWireAdmin.init();this.setupSearch();this.setupMobile();var a=$("body");if(a.hasClass("hasWireTabs")&&$("ul.WireTabs").length==0){a.removeClass("hasWireTabs")}$("#content").removeClass("pw-fouc-fix");a.removeClass("pw-init").addClass("pw-ready");if(b.length>0){b.show()}},setupCloneButton:function(){if($("body").is(".modal")){return}var b=$("button.pw-head-button, button.head_button_clone");if(b.length==0){return}var a=$("#head_button");if(a.length==0){a=$("
    ").prependTo("#breadcrumbs .pw-container")}b.each(function(){var e=$(this);var d=e.parent("a");var c;if(d.length>0){c=e.parent("a").clone(true);a.prepend(c)}else{if(e.hasClass("pw-head-button")||e.hasClass("head_button_clone")){c=e.clone(true);c.attr("data-from_id",e.attr("id")).attr("id",e.attr("id")+"_copy");c.click(function(){$("#"+$(this).attr("data-from_id")).click();return false});a.prepend(c)}}});a.show()},setupSearch:function(){$.widget("custom.adminsearchautocomplete",$.ui.autocomplete,{_renderMenu:function(e,c){var f=this;var d="";e.attr("id","ProcessPageSearchAutocomplete");$.each(c,function(g,h){if(h.type!=d){$("
  • "+h.type+"
  • ").addClass("ui-widget-header").appendTo(e);d=h.type}f._renderItemData(e,h)})},_renderItem:function(e,f){if(f.label==f.template){f.template=""}var c=$("").text(f.label).css("margin-right","3px");if(f.unpublished){c.css("text-decoration","line-through")}if(f.hidden){c.addClass("ui-priority-secondary")}if(typeof f.icon!="undefined"&&f.icon.length){var d=$("").addClass("fa fa-fw fa-"+f.icon).css("margin-right","2px");c.prepend(d)}var g=$("").attr("href",f.edit_url).attr("title",f.tip).append(c).append($("").text(f.template));return $("
  • ").append(g).appendTo(e)}});var b=$("#ProcessPageSearchQuery");var a=$("#ProcessPageSearchStatus");b.adminsearchautocomplete({minLength:2,position:{my:"right top",at:"right bottom"},search:function(c,d){a.html("")},open:function(c,d){$("#topnav").hide()},close:function(c,d){$("#topnav").show()},source:function(e,c){var d=b.parents("form").attr("action")+"?q="+e.term;$.getJSON(d,function(g){var f=g.matches.length;if(f50){if(!g.hasClass("collapse-topnav")){g.addClass("collapse-topnav");a=g.width()}}else{if(a>0){var f=g.width();if(g.hasClass("collapse-topnav")&&f>a){g.removeClass("collapse-topnav");a=0}}}h.children(".collapse-topnav-menu").children("a").click(function(){if($(this).is(".hover")){$(this).mouseleave()}else{$(this).mouseenter()}return false});var d=$(".WireTabs");if(d.length<1){return}d.each(function(){var j=$(this);var i=j.height();if(i>65){if(!g.hasClass("collapse-wiretabs")){g.addClass("collapse-wiretabs");c=g.width()}}else{if(c>0){var k=g.width();if(g.hasClass("collapse-wiretabs")&&k>c){g.removeClass("collapse-wiretabs");c=0}}}})};b();$(window).resize(b)}};$(document).ready(function(){ProcessWireAdminTheme.init();$("a.notice-remove","#notices").click(function(){$("#notices").slideUp("fast",function(){$(this).remove()});return false})}); \ No newline at end of file diff --git a/wire/modules/AdminTheme/AdminThemeReno/scripts/main.js b/wire/modules/AdminTheme/AdminThemeReno/scripts/main.js index eeed00b8..151f918c 100644 --- a/wire/modules/AdminTheme/AdminThemeReno/scripts/main.js +++ b/wire/modules/AdminTheme/AdminThemeReno/scripts/main.js @@ -326,9 +326,19 @@ var ProcessWireAdminTheme = { }, _renderItem: function(ul, item) { if(item.label == item.template) item.template = ''; - return $("
  • ") - .append("" + item.label + " " + item.template + "
  • ") - .appendTo(ul); + var $label = $("").text(item.label).css('margin-right', '3px'); + if(item.unpublished) $label.css('text-decoration', 'line-through'); + if(item.hidden) $label.css('opacity', 0.7); + if(item.icon.length) { + var $icon = $('').addClass('fa fa-fw fa-' + item.icon).css('margin-right', '2px'); + $label.prepend($icon); + } + var $a = $("") + .attr('href', item.edit_url) + .attr('title', item.tip) + .append($label) + .append($("").text(item.template)); + return $("
  • ").append($a).appendTo(ul); } }); @@ -342,7 +352,7 @@ var ProcessWireAdminTheme = { $status.html(""); }, source: function(request, response) { - var url = $input.parents('form').attr('action') + 'for?get=template_label,title&include=all&admin_search=' + request.term; + var url = $input.parents('form').attr('action') + '?q=' + request.term; $.getJSON(url, function(data) { var len = data.matches.length; if(len < data.total) $status.text(data.matches.length + '/' + data.total); @@ -354,7 +364,12 @@ var ProcessWireAdminTheme = { page_id: item.id, template: item.template_label ? item.template_label : '', edit_url: item.editUrl, - type: item.type + type: item.type, + tip: item.tip, + unpublished: (typeof item.unpublished != "undefined" ? item.unpublished : false), + hidden: (typeof item.hidden != "undefined" ? item.hidden : false), + locked: (typeof item.locked != "undefined" ? item.locked : false), + icon: (typeof item.icon != "undefined" ? item.icon : '') } })); }); diff --git a/wire/modules/AdminTheme/AdminThemeReno/scripts/main.min.js b/wire/modules/AdminTheme/AdminThemeReno/scripts/main.min.js index cb75fdc3..892b813d 100644 --- a/wire/modules/AdminTheme/AdminThemeReno/scripts/main.min.js +++ b/wire/modules/AdminTheme/AdminThemeReno/scripts/main.min.js @@ -1 +1 @@ -var ProcessWireAdminTheme={init:function(){this.setupCloneButton();ProcessWireAdmin.init();this.setupSearch();this.setupDropdowns();this.setupSidebarNav();this.setupSideBarState();this.setupSideBarToggle();var b=$("body");var a=$("html");if(b.hasClass("hasWireTabs")&&$("ul.WireTabs").length==0){b.removeClass("hasWireTabs")}$("#content").removeClass("pw-fouc-fix");b.removeClass("pw-init").addClass("pw-ready");a.removeClass("pw-init").addClass("pw-ready");$("a.notice-remove","#notices").click(function(){$("#notices").slideUp("fast",function(){$(this).remove();return false})})},setupSidebarNav:function(){var a=window.location.toString();$(document).mouseup(function(g){var f=$("ul.quicklinks");if(!f.is(g.target)&&f.has(g.target).length===0){f.hide();$(".quicklink-open").removeClass("active");$("#main-nav .current").removeClass("no-arrow")}});$(document).keydown(function(i){var g=i.target.tagName.toLowerCase();var f=i.target.className.split(" ")[0];var h;if(g=="input"||g=="textarea"||f=="InputfieldCKEditorInline"){return}switch(i.which){case 37:h="open";break;case 39:h="closed";break;default:return}ProcessWireAdminTheme.setupSideBarState(true,h);i.preventDefault()});function e(){$("#main-nav > li > a.open:not(.hover-temp):not(.just-clicked)").each(function(){var g=$(this);var f=g.next("ul:visible");if(f.length>0){if(f.find(".quicklinks-open").length>0){f.find(".quicklink-close").click()}}})}var c=null,b=0;$("#main-nav a.parent").dblclick(function(f){f.preventDefault()}).click(function(){var f=$(this);f.addClass("just-clicked");b++;if(b===1){c=setTimeout(function(){e();f.toggleClass("open").next("ul").slideToggle(200,function(){f.removeClass("just-clicked")});b=0},200)}else{clearTimeout(c);b=0;window.location.href=f.attr("href");return true}return false});var d=null;$(".quicklink-open").click(function(j){e();var k=$(this);k.parent().addClass("quicklinks-open");k.toggleClass("active").parent().next("ul.quicklinks").toggle();k.parent().parent().siblings().find("ul.quicklinks").hide();k.parent().parent().siblings().find(".quicklink-open").removeClass("active").parent("a").removeClass("quicklinks-open");k.effect("pulsate",100);j.stopPropagation();$("#main-nav .current:not(.open)").addClass("no-arrow");var f=$(this).parent().next("ul.quicklinks");var i=f.attr("data-json");if(i.length>0&&!f.hasClass("json-loaded")){f.addClass("json-loaded");var h=f.find(".quicklinks-spinner");var g=h.attr("class");h.removeClass(g).addClass("fa fa-fw fa-spin fa-spinner");$.getJSON(i,function(l){if(l.add){var m=$("
  • "+l.add.label+"
  • ");f.append(m)}$.each(l.list,function(r){var p="";var o=this.url.indexOf("/")===0?this.url:l.url+this.url;var q=$("
  • "+p+this.label+"
  • ");if(typeof this.className!="undefined"&&this.className&&this.className.length){q.addClass(this.className)}f.append(q)});h.removeClass("fa-spin fa-spinner").addClass(g);if(l.icon.length>0){h.removeClass("fa-bolt").addClass("fa-"+l.icon)}})}return false}).mouseover(function(){var f=$(this);if(f.parent().hasClass("quicklinks-open")){return}f.addClass("hover-temp");clearTimeout(d);d=setTimeout(function(){if(f.parent().hasClass("quicklinks-open")){return}if(f.hasClass("hover-temp")){f.click()}},500)}).mouseout(function(){$(this).removeClass("hover-temp")});$(".quicklink-close").click(function(){$(this).parent().removeClass("quicklinks-open");$(this).closest("ul.quicklinks").hide().prev("a").removeClass("quicklinks-open");$(".quicklink-open").removeClass("active");$("#main-nav .current").removeClass("no-arrow");return false});$("#main-nav .parent").each(function(){var f=$(this).attr("href");if(a.match(f)){$(this).next("ul").show();$(this).addClass("open")}})},setupCloneButton:function(){if($("body").is(".modal")){return}var b=$("button.pw-head-button, button.head_button_clone");if(b.length==0||$.browser.msie){return}var a=$("
    ").prependTo("#headline").show();b.each(function(){var e=$(this);var d=e.parent("a");var c;if(d.length){c=e.parent("a").clone();a.append(c)}else{if(e.hasClass("head_button_clone")||e.hasClass("pw-head-button")){c=e.clone();c.attr("data-from_id",e.attr("id")).attr("id",e.attr("id")+"_copy");c.click(function(){$("#"+$(this).attr("data-from_id")).click();return false});a.prepend(c)}}})},setupSearch:function(){$.widget("custom.adminsearchautocomplete",$.ui.autocomplete,{_renderMenu:function(f,d){var g=this;var e="";f.attr("id","ProcessPageSearchAutocomplete");$.each(d,function(h,i){if(i.type!=e){$("
  • "+i.type+"
  • ").addClass("ui-widget-header").appendTo(f);e=i.type}g._renderItemData(f,i)})},_renderItem:function(d,e){if(e.label==e.template){e.template=""}return $("
  • ").append(""+e.label+" "+e.template+"
  • ").appendTo(d)}});var c=$("#ProcessPageSearchQuery");var a=$("#ProcessPageSearchStatus");c.adminsearchautocomplete({minLength:2,position:{my:"right top",at:"right bottom"},search:function(d,e){a.html("")},source:function(f,d){var e=c.parents("form").attr("action")+"for?get=template_label,title&include=all&admin_search="+f.term;$.getJSON(e,function(h){var g=h.matches.length;if(g a").on("click",function(a){$(this).next("ul").toggleClass("open");$(this).parent().siblings().find("ul.open").removeClass("open");return false});$("#masthead li.pw-dropdown > ul li a").on("click",function(a){a.stopPropagation()});$(document).on("click",function(){$("#masthead li.pw-dropdown ul").removeClass("open")})},setupSideBarToggle:function(){$(".main-nav-toggle").on("click",function(){ProcessWireAdminTheme.setupSideBarState(true);return false})},setupSideBarState:function(j,c){if($("body").hasClass("id-23")||$("body").hasClass("modal")){return false}var e="pw_sidebar_state";var c=c||localStorage.getItem(e);var i=$(".main-nav-toggle");var a=$("#sidebar");var h=$("#main");var d=$("#masthead");var g=$("#branding");var f=$("#NotificationBug");var b=i.add(a).add(h).add(d).add(g).add(f);if(c===null&&$(window).width()>=690){localStorage.setItem(e,"open")}if(!j&&$(window).width()<690){localStorage.setItem(e,"closed");c="closed"}if(j==true){if(c=="open"){localStorage.setItem(e,"closed");b.addClass("closed")}else{if(c=="closed"){localStorage.setItem(e,"open");b.removeClass("closed")}}a.removeClass("hide");g.removeClass("hide");h.removeClass("full");d.removeClass("full");i.removeClass("full")}else{if(c=="closed"){b.addClass("closed");a.addClass("hide");g.addClass("hide");h.addClass("full");d.addClass("full");i.addClass("full")}}},browserCheck:function(){if($.browser.msie&&$.browser.version<8){$("#content .pw-container").html("

    ProcessWire does not support IE7 and below at this time. Please try again with a newer browser.

    ").show()}}};$(document).ready(function(){ProcessWireAdminTheme.init()}); \ No newline at end of file +var ProcessWireAdminTheme={init:function(){this.setupCloneButton();ProcessWireAdmin.init();this.setupSearch();this.setupDropdowns();this.setupSidebarNav();this.setupSideBarState();this.setupSideBarToggle();var b=$("body");var a=$("html");if(b.hasClass("hasWireTabs")&&$("ul.WireTabs").length==0){b.removeClass("hasWireTabs")}$("#content").removeClass("pw-fouc-fix");b.removeClass("pw-init").addClass("pw-ready");a.removeClass("pw-init").addClass("pw-ready");$("a.notice-remove","#notices").click(function(){$("#notices").slideUp("fast",function(){$(this).remove();return false})})},setupSidebarNav:function(){var a=window.location.toString();$(document).mouseup(function(g){var f=$("ul.quicklinks");if(!f.is(g.target)&&f.has(g.target).length===0){f.hide();$(".quicklink-open").removeClass("active");$("#main-nav .current").removeClass("no-arrow")}});$(document).keydown(function(i){var g=i.target.tagName.toLowerCase();var f=i.target.className.split(" ")[0];var h;if(g=="input"||g=="textarea"||f=="InputfieldCKEditorInline"){return}switch(i.which){case 37:h="open";break;case 39:h="closed";break;default:return}ProcessWireAdminTheme.setupSideBarState(true,h);i.preventDefault()});function e(){$("#main-nav > li > a.open:not(.hover-temp):not(.just-clicked)").each(function(){var g=$(this);var f=g.next("ul:visible");if(f.length>0){if(f.find(".quicklinks-open").length>0){f.find(".quicklink-close").click()}}})}var c=null,b=0;$("#main-nav a.parent").dblclick(function(f){f.preventDefault()}).click(function(){var f=$(this);f.addClass("just-clicked");b++;if(b===1){c=setTimeout(function(){e();f.toggleClass("open").next("ul").slideToggle(200,function(){f.removeClass("just-clicked")});b=0},200)}else{clearTimeout(c);b=0;window.location.href=f.attr("href");return true}return false});var d=null;$(".quicklink-open").click(function(j){e();var k=$(this);k.parent().addClass("quicklinks-open");k.toggleClass("active").parent().next("ul.quicklinks").toggle();k.parent().parent().siblings().find("ul.quicklinks").hide();k.parent().parent().siblings().find(".quicklink-open").removeClass("active").parent("a").removeClass("quicklinks-open");k.effect("pulsate",100);j.stopPropagation();$("#main-nav .current:not(.open)").addClass("no-arrow");var f=$(this).parent().next("ul.quicklinks");var i=f.attr("data-json");if(i.length>0&&!f.hasClass("json-loaded")){f.addClass("json-loaded");var h=f.find(".quicklinks-spinner");var g=h.attr("class");h.removeClass(g).addClass("fa fa-fw fa-spin fa-spinner");$.getJSON(i,function(l){if(l.add){var m=$("
  • "+l.add.label+"
  • ");f.append(m)}$.each(l.list,function(r){var p="";var o=this.url.indexOf("/")===0?this.url:l.url+this.url;var q=$("
  • "+p+this.label+"
  • ");if(typeof this.className!="undefined"&&this.className&&this.className.length){q.addClass(this.className)}f.append(q)});h.removeClass("fa-spin fa-spinner").addClass(g);if(l.icon.length>0){h.removeClass("fa-bolt").addClass("fa-"+l.icon)}})}return false}).mouseover(function(){var f=$(this);if(f.parent().hasClass("quicklinks-open")){return}f.addClass("hover-temp");clearTimeout(d);d=setTimeout(function(){if(f.parent().hasClass("quicklinks-open")){return}if(f.hasClass("hover-temp")){f.click()}},500)}).mouseout(function(){$(this).removeClass("hover-temp")});$(".quicklink-close").click(function(){$(this).parent().removeClass("quicklinks-open");$(this).closest("ul.quicklinks").hide().prev("a").removeClass("quicklinks-open");$(".quicklink-open").removeClass("active");$("#main-nav .current").removeClass("no-arrow");return false});$("#main-nav .parent").each(function(){var f=$(this).attr("href");if(a.match(f)){$(this).next("ul").show();$(this).addClass("open")}})},setupCloneButton:function(){if($("body").is(".modal")){return}var b=$("button.pw-head-button, button.head_button_clone");if(b.length==0||$.browser.msie){return}var a=$("
    ").prependTo("#headline").show();b.each(function(){var e=$(this);var d=e.parent("a");var c;if(d.length){c=e.parent("a").clone();a.append(c)}else{if(e.hasClass("head_button_clone")||e.hasClass("pw-head-button")){c=e.clone();c.attr("data-from_id",e.attr("id")).attr("id",e.attr("id")+"_copy");c.click(function(){$("#"+$(this).attr("data-from_id")).click();return false});a.prepend(c)}}})},setupSearch:function(){$.widget("custom.adminsearchautocomplete",$.ui.autocomplete,{_renderMenu:function(f,d){var g=this;var e="";f.attr("id","ProcessPageSearchAutocomplete");$.each(d,function(h,i){if(i.type!=e){$("
  • "+i.type+"
  • ").addClass("ui-widget-header").appendTo(f);e=i.type}g._renderItemData(f,i)})},_renderItem:function(f,g){if(g.label==g.template){g.template=""}var d=$("").text(g.label).css("margin-right","3px");if(g.unpublished){d.css("text-decoration","line-through")}if(g.hidden){d.css("opacity",0.7)}if(g.icon.length){var e=$("").addClass("fa fa-fw fa-"+g.icon).css("margin-right","2px");d.prepend(e)}var h=$("").attr("href",g.edit_url).attr("title",g.tip).append(d).append($("").text(g.template));return $("
  • ").append(h).appendTo(f)}});var c=$("#ProcessPageSearchQuery");var a=$("#ProcessPageSearchStatus");c.adminsearchautocomplete({minLength:2,position:{my:"right top",at:"right bottom"},search:function(d,e){a.html("")},source:function(f,d){var e=c.parents("form").attr("action")+"?q="+f.term;$.getJSON(e,function(h){var g=h.matches.length;if(g a").on("click",function(a){$(this).next("ul").toggleClass("open");$(this).parent().siblings().find("ul.open").removeClass("open");return false});$("#masthead li.pw-dropdown > ul li a").on("click",function(a){a.stopPropagation()});$(document).on("click",function(){$("#masthead li.pw-dropdown ul").removeClass("open")})},setupSideBarToggle:function(){$(".main-nav-toggle").on("click",function(){ProcessWireAdminTheme.setupSideBarState(true);return false})},setupSideBarState:function(j,c){if($("body").hasClass("id-23")||$("body").hasClass("modal")){return false}var e="pw_sidebar_state";var c=c||localStorage.getItem(e);var i=$(".main-nav-toggle");var a=$("#sidebar");var h=$("#main");var d=$("#masthead");var g=$("#branding");var f=$("#NotificationBug");var b=i.add(a).add(h).add(d).add(g).add(f);if(c===null&&$(window).width()>=690){localStorage.setItem(e,"open")}if(!j&&$(window).width()<690){localStorage.setItem(e,"closed");c="closed"}if(j==true){if(c=="open"){localStorage.setItem(e,"closed");b.addClass("closed")}else{if(c=="closed"){localStorage.setItem(e,"open");b.removeClass("closed")}}a.removeClass("hide");g.removeClass("hide");h.removeClass("full");d.removeClass("full");i.removeClass("full")}else{if(c=="closed"){b.addClass("closed");a.addClass("hide");g.addClass("hide");h.addClass("full");d.addClass("full");i.addClass("full")}}},browserCheck:function(){if($.browser.msie&&$.browser.version<8){$("#content .pw-container").html("

    ProcessWire does not support IE7 and below at this time. Please try again with a newer browser.

    ").show()}}};$(document).ready(function(){ProcessWireAdminTheme.init()}); \ No newline at end of file diff --git a/wire/modules/AdminTheme/AdminThemeUikit/_search-form.php b/wire/modules/AdminTheme/AdminThemeUikit/_search-form.php index 1ad05731..236f14e6 100644 --- a/wire/modules/AdminTheme/AdminThemeUikit/_search-form.php +++ b/wire/modules/AdminTheme/AdminThemeUikit/_search-form.php @@ -5,7 +5,7 @@ if(!defined("PROCESSWIRE")) die(); /** @var Paths $urls */ /** @var AdminThemeUikit $adminTheme */ -$searchURL = $urls->admin . 'page/search/'; +$searchURL = $urls->admin . 'page/search/live/'; if($adminTheme->isEditor): ?>
    diff --git a/wire/modules/AdminTheme/AdminThemeUikit/config.php b/wire/modules/AdminTheme/AdminThemeUikit/config.php index 114430bc..3b73c584 100644 --- a/wire/modules/AdminTheme/AdminThemeUikit/config.php +++ b/wire/modules/AdminTheme/AdminThemeUikit/config.php @@ -561,6 +561,16 @@ class AdminThemeUikitConfigHelper extends Wire { */ public function configTests(InputfieldWrapper $inputfields) { + $form = $inputfields->getForm(); + if($form) { + $form->action .= '&tests=1'; + $this->wire('session')->addHookBefore('redirect', function(HookEvent $event) { + $url = $event->arguments(0); + $url .= '&tests=1'; + $event->arguments(0, $url); + }); + } + /** @var Modules $modules */ $modules = $this->wire('modules'); @@ -705,7 +715,6 @@ class AdminThemeUikitConfigHelper extends Wire { $f->showIf = 'test_select=2'; $f->notes = $f->showIf . " ($f->columnWidth%)"; $fieldset->add($f); - } } diff --git a/wire/modules/AdminTheme/AdminThemeUikit/scripts/main.js b/wire/modules/AdminTheme/AdminThemeUikit/scripts/main.js index cd851c0a..8503044a 100644 --- a/wire/modules/AdminTheme/AdminThemeUikit/scripts/main.js +++ b/wire/modules/AdminTheme/AdminThemeUikit/scripts/main.js @@ -301,12 +301,20 @@ var ProcessWireAdminTheme = { }, _renderItem: function(ul, item) { if(item.label == item.template) item.template = ''; - return $("
  • ") - .append( - "" + item.label + " " + - "" + item.template + "" - ) - .appendTo(ul); + var $label = $("").text(item.label).css('margin-right', '3px'); + if(item.unpublished) $label.css('text-decoration', 'line-through'); + if(item.hidden) $label.addClass('ui-priority-secondary'); + if(item.icon.length) { + var $icon = $('').addClass('fa fa-fw fa-' + item.icon).css('margin-right', '2px'); + $label.prepend($icon); + } + var $a = $("") + .attr('href', item.edit_url) + .attr('title', item.tip) + .append($label) + .append($("").text(item.template)); + + return $("
  • ").append($a).appendTo(ul); } }); @@ -338,8 +346,7 @@ var ProcessWireAdminTheme = { close: function(event, ui) { }, source: function(request, response) { - var url = $input.parents('form').attr('data-action') + - 'for?get=template_label,title&include=all&admin_search=' + request.term; + var url = $input.parents('form').attr('data-action') + '?q=' + request.term; $.getJSON(url, function(data) { var len = data.matches.length; if(len < data.total) { @@ -356,7 +363,12 @@ var ProcessWireAdminTheme = { page_id: item.id, template: item.template_label ? item.template_label : '', edit_url: item.editUrl, - type: item.type + type: item.type, + tip: item.tip, + unpublished: (typeof item.unpublished != "undefined" ? item.unpublished : false), + hidden: (typeof item.hidden != "undefined" ? item.hidden : false), + locked: (typeof item.locked != "undefined" ? item.locked : false), + icon: (typeof item.icon != "undefined" ? item.icon : '') } })); }); diff --git a/wire/modules/AdminTheme/AdminThemeUikit/scripts/main.min.js b/wire/modules/AdminTheme/AdminThemeUikit/scripts/main.min.js index 7b0360e2..3eb4bc3e 100644 --- a/wire/modules/AdminTheme/AdminThemeUikit/scripts/main.min.js +++ b/wire/modules/AdminTheme/AdminThemeUikit/scripts/main.min.js @@ -1 +1 @@ -var ProcessWireAdminTheme={init:function(){this.setupInputfields();this.setupTooltips();this.checkLayout()},ready:function(){this.setupCloneButton();ProcessWireAdmin.init();this.setupSearch();this.setupSideNav();var b=$("body");$(document).on("wiretabclick opened",function(c){$("body").addClass("pw-fake-resize");$(window).resize();setTimeout(function(){$("body").removeClass("pw-fake-resize")},100)});$("a.notice-remove","#notices").click(function(){$("#notices").slideUp("fast",function(){$(this).remove()});return false});$("a.pw-logo-link").click(this.logoClickEvent);$("#_ProcessPageEditView").click(function(c){c.stopPropagation()});var a=null;$(window).resize(function(){if(a){return}a=setTimeout(function(){ProcessWireAdminTheme.windowResized();a=null},250)});this.setupMasthead();this.setupWireTabs();b.removeClass("pw-init").addClass("pw-ready")},setupWireTabs:function(){var a=$(".WireTabs");if(a.length){$(document).on("wiretabclick",function(b,c){ProcessWireAdminTheme.wireTabClick(c)});setTimeout(function(){var c=a.children(".uk-active");if(c.length){var b=$(c.find("a").attr("href"));if(b.length){ProcessWireAdminTheme.wireTabClick(b)}}},500)}},wireTabClick:function(a){if(!a.length){return}var d=null;var b=null;if(a.hasClass("InputfieldWrapper")){b=a.children(".Inputfields").children(".Inputfield:eq(0)");d=b.children(".InputfieldHeader")}else{if(a.hasClass("Inputfield")){b=a;d=a.children(".InputfieldHeader")}}if(!d||!d.length){return}var i=false;var c=["InputfieldIsPrimary","InputfieldIsWarning","InputfieldIsError","InputfieldIsHighlight","InputfieldIsSuccess"];for(var f=0;f-1){a=a.replace(/([?&]layout)=[-_a-zA-Z0-9]+/,"$1=sidenav-init")}else{a+=(a.indexOf("?")>0?"&":"?")+"layout=sidenav-init"}window.location.href=a}},windowResized:function(){if($("body").hasClass("pw-fake-resize")){return}this.setupMasthead()},setupMasthead:function(){var b=$("#pw-masthead");var e=$("#pw-masthead-mobile");var c=$(window).width();var a=0;var d=0;if(c>767){d=parseInt(b.data("pw-height"));a=b.children(".pw-container").height()}else{a=999}if(b.hasClass("uk-hidden")){b.removeClass("uk-hidden")}if(a>d){if(!b.hasClass("pw-masthead-hidden")){b.addClass("pw-masthead-hidden").css({position:"absolute",top:"-9999px"});e.removeClass("uk-hidden");$("#offcanvas-toggle").removeClass("uk-hidden")}}else{if(b.hasClass("pw-masthead-hidden")){e.addClass("uk-hidden");b.removeClass("pw-masthead-hidden").css({position:"relative",top:0});$("#offcanvas-toggle").addClass("uk-hidden")}}},setupCloneButton:function(){if($("body").is(".modal")){return}var c=$("button.pw-head-button, button.head_button_clone");if(c.length==0){return}var a=$("#pw-content-head-buttons");var e=null;var h=null;var d={};c.each(function(){var m=$(this);var k=m.parent("a");var j;if(k.length>0){j=m.parent("a").clone(true);a.prepend(j)}else{if(m.hasClass("pw-head-button")||m.hasClass("head_button_clone")){j=m.clone(true);j.attr("data-from_id",m.attr("id")).attr("id",m.attr("id")+"_copy").addClass("pw-head-button");j.click(function(){$("#"+$(this).attr("data-from_id")).click();return false});if(j.hasClass("pw-button-dropdown-toggle")){var l=j.attr("id").replace("pw-dropdown-toggle-","");d[l]=j}else{if(j.hasClass("pw-button-dropdown-main")){var i=$("").addClass("pw-button-dropdown-wrap");i.append(j).addClass("uk-float-right");a.prepend(i)}else{j.addClass("uk-float-right");a.prepend(j)}}}}});for(var g in d){var b=d[g];var f=$("#"+g);f.after(b)}},setupSearch:function(){$.widget("custom.adminsearchautocomplete",$.ui.autocomplete,{_renderMenu:function(c,a){var d=this;var b="";c.addClass("pw-dropdown-menu-shorter uk-nav uk-nav-default");c.css("z-index",9999);$.each(a,function(e,f){if(f.type!=b){if(b.length){$("
  • ").appendTo(c)}$("
  • "+f.type+"
  • ").addClass("uk-nav-header").appendTo(c);b=f.type}d._renderItemData(c,f)})},_renderItem:function(a,b){if(b.label==b.template){b.template=""}return $("
  • ").append(""+b.label+" "+b.template+"").appendTo(a)}});$(".pw-search-form").each(function(){var b=$(this);var c=b.find(".pw-search-input");var a={my:"right top",at:"right bottom"};if(b.closest(".uk-offcanvas-bar").length){a.my="left top";a.at="left bottom"}c.click(function(d){d.stopPropagation()});c.adminsearchautocomplete({minLength:2,position:a,search:function(d,e){b.find(".pw-search-icon").addClass("uk-hidden");b.find(".pw-spinner-icon").removeClass("uk-hidden")},open:function(d,e){},close:function(d,e){},source:function(f,d){var e=c.parents("form").attr("data-action")+"for?get=template_label,title&include=all&admin_search="+f.term;$.getJSON(e,function(h){var g=h.matches.length;if(g");b.append(c);$.getJSON(a,function(g){var i=e.clone();var f=i.find("i");if(!f.length){f=$("");i.prepend(f)}f.attr("class","fa fa-fw fa-arrow-circle-right pw-nav-icon");i.removeAttr("data-json").removeAttr("class");i.find("small").remove();var h=$("
  • ").addClass("pw-nav-dup").append(i);b.append(h);if(g.add){var h=$("
  • "+g.add.label+"
  • ");b.append(h)}$.each(g.list,function(k){if(this.label.indexOf("-1){this.label=this.label.replace(/<\/?span[^>]*>/g,"")}var q="";var r=$("
    "+this.label+"
    ");var s=r.text();if(s.length>30){var o=r.find("small");if(o.length){o.remove()}s=r.text();s=s.substring(0,30);var k=s.lastIndexOf(" ");if(k>3){s=s.substring(0,k)+"… "}r.html(s);if(o.length){r.append(o)}}s=r.html().replace(" "," ");if(this.icon){q=""}var j=this.url.indexOf("/")===0?this.url:g.url+this.url;var l=$(""+q+s+"");var p=$("
  • ").append(l);if(this.navJSON!="undefined"&&this.navJSON){l.addClass("pw-has-items pw-has-ajax-items").attr("data-json",this.navJSON);var m=$("
      ");p.addClass("uk-parent").append(m);UIkit.nav(m,{multiple:true})}if(typeof this.className!="undefined"&&this.className&&this.className.length){p.addClass(this.className)}if(p.hasClass("pw-nav-add")||p.hasClass("pw-pagelist-show-all")){b.children(".pw-nav-dup").after(p.removeClass("separator").addClass("pw-nav-add"))}else{b.append(p)}});c.remove();b.addClass("navJSON").addClass("length"+parseInt(g.list.length)).hide();if(b.children().length){b.css("opacity",1).fadeIn("fast")}});return false})},setupInputfields:function(){var d=$("body").hasClass("AdminThemeUikitNoGrid");function e(j){$("form.uk-form-horizontal").each(function(){$(this).find(".InputfieldContent > .Inputfields").each(function(){var l=$(this);l.addClass("uk-form-vertical");l.find(".uk-form-label").removeClass("uk-form-label");l.find(".uk-form-controls").removeClass("uk-form-controls")});$(this).find(".InputfieldSubmit, .InputfieldButton").each(function(){$(this).find(".InputfieldContent").before("
       
      ")})});$(".InputfieldNoBorder.uk-card").removeClass("uk-card uk-card-default");$(".InputfieldIsOffset.InputfieldColumnWidthFirst").each(function(){var m=$(this);var l;do{l=m.next(".InputfieldColumnWidth");if(!l.length||l.hasClass("InputfieldColumnWidthFirst")){break}l.addClass("InputfieldIsOffset");m=l}while(true)});$(".Inputfields").each(function(){b($(this))});$(".ui-widget.Inputfield, .ui-widget-header.InputfieldHeader, .ui-widget-content.InputfieldContent").removeClass("ui-widget ui-widget-header ui-widget-content");$(".MarkupPagerNav:not(.uk-pagination)").each(function(){$(this).addClass("uk-pagination")});if(typeof j=="undefined"){j=$(".InputfieldForm")}var k=$("select:not([multiple]):not(.uk-select)",j);k.addClass("uk-select")}function b(j){$(".InputfieldRowFirst",j).removeClass("InputfieldRowFirst");$(".InputfieldRowLast",j).removeClass("InputfieldRowLast");var k=j.children(".Inputfield:not(.InputfieldStateHidden):eq(0)");if(!k.length){return}do{k.addClass("InputfieldRowFirst");k=k.next(".Inputfield:not(.InputfieldStateHidden)")}while(k.hasClass("InputfieldColumnWidth")&&!k.hasClass("InputfieldColumnWidthFirst"));k=j.children(".Inputfield:last-child");while(k.length&&k.hasClass("InputfieldStateHidden")){k=k.prev(".Inputfield")}do{k.addClass("InputfieldRowLast");if(!k.hasClass("InputfieldColumnWidth")||k.hasClass("InputfieldColumnWidthFirst")){break}k=k.prev(".Inputfield:not(.InputfieldStateHidden)")}while(k.hasClass("InputfieldColumnWidth"))}var i=[];function c(l,p){if(d&&typeof p!="undefined"){if(typeof l=="string"){p.addClass(l)}else{p.css("width",l+"%")}return""}var k="uk-width-1-1";var j=k;var n=false;if(typeof l=="string"&&typeof p!="undefined"){j=l;n=true}else{if(!l||l>=100){j=k}else{if(typeof i[l]!="undefined"){j="uk-width-"+i[l]}else{for(var m in ProcessWire.config.ukGridWidths){var o=ProcessWire.config.ukGridWidths[m];m=parseInt(m);if(l>=m){j=o;break}}if(j.length){i[l]=j;j="uk-width-"+j}}}}if(!n&&j&&j!=k){j+="@m"}if(typeof p!="undefined"){if(j&&p.hasClass(j)){}else{g(p);if(j){p.addClass(j)}}}return j}function g(k){var l=null;if(typeof k!="string"){l=k;k=l.attr("class")}if(k.indexOf("uk-width-")>-1){var j=k.replace(/uk-width-(\d-\d|expand)[@smxl]*\s*/g,"");if(l!==null){l.attr("class",j)}}return k}function a(j){if(!j){return}var q=j.parent().children(".Inputfield");var r=null;var l=0;var p=0;var t=0;var m=0;var k=false;function u(v,x){if(!k){return}if(typeof x=="undefined"){x=j}var w=x.attr("id");w=w.replace("wrap_Inputfield_","");console.log(w+" (combined width="+l+", w="+t+"): "+v)}function o(v){if(typeof v=="undefined"){v=r}if(v){if(d){v.addClass("InputfieldColumnWidthLast")}else{c("InputfieldColumnWidthLast uk-width-expand",v)}}}function n(){if(k){u("A: hidden",j)}m+=t;l+=t;if(r&&l>=95){m+=p;if(k){u("Updating last visible Inputfield to width="+m,r)}c(m,r);l=0;m=0;p=0;r=null}else{p+=t}}function s(){if(k){u("Skipping because full-width",j)}if(l<100&&r){o(r)}r=null;p=0;m=0;l=0}q.each(function(){j=$(this);var y=false;var x=false;var w=j.hasClass("InputfieldColumnWidth");var v=!w||j.hasClass("InputfieldColumnWidthFirst");if(v&&r&&l<100){o(r)}t=w?parseInt(j.attr("data-colwidth")):0;if(!t||t>=95){s();return}if(j.hasClass("InputfieldStateHidden")){n();return}if(!l||l>=100){l=0;x=true;y=false;if(k){u("B: starting new row",j)}}else{if(l+t>100){if(r){o(r)}l=0;x=true;if(k){u("C: start new row because width would exceed 100%",j)}}else{if(l+t==100){y=true;if(k){u("D: width is exactly 100%, so this is the last column",j)}}else{if(l+t>=95){y=true;t=100-l;if(k){u("D2: width is close enough to 100%, so this is the last column",j)}}else{if(k){u("E: not first or last column",j)}}}}}if(y){j.addClass("InputfieldColumnWidthLast")}else{j.removeClass("InputfieldColumnWidthLast")}if(x){j.addClass("InputfieldColumnWidthFirst");p=0}else{j.removeClass("InputfieldColumnWidthFirst")}if(y){r=null;l=0;m=0;if(p){t+=p}p=0}else{r=j;l+=t;m=t}c(t,j)});if(l<100&&r){o(r)}}var h=null;var f=function(l,k){var j=$(k);if(l.type=="showInputfield"){j.removeClass("uk-hidden")}else{j.show();j.addClass("uk-hidden")}a(j);if(h){return}h=setTimeout(function(){b(j.closest(".Inputfields"));var m=j.find(".Inputfields");if(m.length){m.each(function(){b($(this))})}h=null},100)};$(document).on("reloaded",function(){e($(this))});$(document).on("hideInputfield",f);$(document).on("showInputfield",f);$("body").addClass("InputfieldColumnWidthsInit");e()},setupTooltips:function(){$(".tooltip, .pw-tooltip").each(function(){$(this).removeClass("tooltip pw-tooltip");UIkit.tooltip($(this))})},linkTargetMainMouseoverEvent:function(){var b=$(this);var a=b.attr("href");if(a.length<2){return}if(b.attr("target")){return}if(b.parent("li").hasClass("PageListActionView")){b.attr("target","_top")}else{b.attr("target","main")}},logoClickEvent:function(){if($("body").hasClass("pw-layout-sidenav-init")){if($("#pw-admin-side").length){toggleSidebarPane()}else{UIkit.toggle("#offcanvas-nav").toggle()}}else{if(ProcessWire.config.adminTheme.logoAction==1){UIkit.toggle("#offcanvas-nav").toggle()}else{return true}}return false}};$(document).ready(function(){ProcessWireAdminTheme.ready()}); \ No newline at end of file +var ProcessWireAdminTheme={init:function(){this.setupInputfields();this.setupTooltips();this.checkLayout()},ready:function(){this.setupCloneButton();ProcessWireAdmin.init();this.setupSearch();this.setupSideNav();var b=$("body");$(document).on("wiretabclick opened",function(c){$("body").addClass("pw-fake-resize");$(window).resize();setTimeout(function(){$("body").removeClass("pw-fake-resize")},100)});$("a.notice-remove","#notices").click(function(){$("#notices").slideUp("fast",function(){$(this).remove()});return false});$("a.pw-logo-link").click(this.logoClickEvent);$("#_ProcessPageEditView").click(function(c){c.stopPropagation()});var a=null;$(window).resize(function(){if(a){return}a=setTimeout(function(){ProcessWireAdminTheme.windowResized();a=null},250)});this.setupMasthead();this.setupWireTabs();b.removeClass("pw-init").addClass("pw-ready")},setupWireTabs:function(){var a=$(".WireTabs");if(a.length){$(document).on("wiretabclick",function(b,c){ProcessWireAdminTheme.wireTabClick(c)});setTimeout(function(){var c=a.children(".uk-active");if(c.length){var b=$(c.find("a").attr("href"));if(b.length){ProcessWireAdminTheme.wireTabClick(b)}}},500)}},wireTabClick:function(a){if(!a.length){return}var d=null;var b=null;if(a.hasClass("InputfieldWrapper")){b=a.children(".Inputfields").children(".Inputfield:eq(0)");d=b.children(".InputfieldHeader")}else{if(a.hasClass("Inputfield")){b=a;d=a.children(".InputfieldHeader")}}if(!d||!d.length){return}var i=false;var c=["InputfieldIsPrimary","InputfieldIsWarning","InputfieldIsError","InputfieldIsHighlight","InputfieldIsSuccess"];for(var f=0;f-1){a=a.replace(/([?&]layout)=[-_a-zA-Z0-9]+/,"$1=sidenav-init")}else{a+=(a.indexOf("?")>0?"&":"?")+"layout=sidenav-init"}window.location.href=a}},windowResized:function(){if($("body").hasClass("pw-fake-resize")){return}this.setupMasthead()},setupMasthead:function(){var b=$("#pw-masthead");var e=$("#pw-masthead-mobile");var c=$(window).width();var a=0;var d=0;if(c>767){d=parseInt(b.data("pw-height"));a=b.children(".pw-container").height()}else{a=999}if(b.hasClass("uk-hidden")){b.removeClass("uk-hidden")}if(a>d){if(!b.hasClass("pw-masthead-hidden")){b.addClass("pw-masthead-hidden").css({position:"absolute",top:"-9999px"});e.removeClass("uk-hidden");$("#offcanvas-toggle").removeClass("uk-hidden")}}else{if(b.hasClass("pw-masthead-hidden")){e.addClass("uk-hidden");b.removeClass("pw-masthead-hidden").css({position:"relative",top:0});$("#offcanvas-toggle").addClass("uk-hidden")}}},setupCloneButton:function(){if($("body").is(".modal")){return}var c=$("button.pw-head-button, button.head_button_clone");if(c.length==0){return}var a=$("#pw-content-head-buttons");var e=null;var h=null;var d={};c.each(function(){var m=$(this);var k=m.parent("a");var j;if(k.length>0){j=m.parent("a").clone(true);a.prepend(j)}else{if(m.hasClass("pw-head-button")||m.hasClass("head_button_clone")){j=m.clone(true);j.attr("data-from_id",m.attr("id")).attr("id",m.attr("id")+"_copy").addClass("pw-head-button");j.click(function(){$("#"+$(this).attr("data-from_id")).click();return false});if(j.hasClass("pw-button-dropdown-toggle")){var l=j.attr("id").replace("pw-dropdown-toggle-","");d[l]=j}else{if(j.hasClass("pw-button-dropdown-main")){var i=$("").addClass("pw-button-dropdown-wrap");i.append(j).addClass("uk-float-right");a.prepend(i)}else{j.addClass("uk-float-right");a.prepend(j)}}}}});for(var g in d){var b=d[g];var f=$("#"+g);f.after(b)}},setupSearch:function(){$.widget("custom.adminsearchautocomplete",$.ui.autocomplete,{_renderMenu:function(c,a){var d=this;var b="";c.addClass("pw-dropdown-menu-shorter uk-nav uk-nav-default");c.css("z-index",9999);$.each(a,function(e,f){if(f.type!=b){if(b.length){$("
    • ").appendTo(c)}$("
    • "+f.type+"
    • ").addClass("uk-nav-header").appendTo(c);b=f.type}d._renderItemData(c,f)})},_renderItem:function(c,d){if(d.label==d.template){d.template=""}var a=$("").text(d.label).css("margin-right","3px");if(d.unpublished){a.css("text-decoration","line-through")}if(d.hidden){a.addClass("ui-priority-secondary")}if(d.icon.length){var b=$("").addClass("fa fa-fw fa-"+d.icon).css("margin-right","2px");a.prepend(b)}var e=$("").attr("href",d.edit_url).attr("title",d.tip).append(a).append($("").text(d.template));return $("
    • ").append(e).appendTo(c)}});$(".pw-search-form").each(function(){var b=$(this);var c=b.find(".pw-search-input");var a={my:"right top",at:"right bottom"};if(b.closest(".uk-offcanvas-bar").length){a.my="left top";a.at="left bottom"}c.click(function(d){d.stopPropagation()});c.adminsearchautocomplete({minLength:2,position:a,search:function(d,e){b.find(".pw-search-icon").addClass("uk-hidden");b.find(".pw-spinner-icon").removeClass("uk-hidden")},open:function(d,e){},close:function(d,e){},source:function(f,d){var e=c.parents("form").attr("data-action")+"?q="+f.term;$.getJSON(e,function(h){var g=h.matches.length;if(g");b.append(c);$.getJSON(a,function(g){var i=e.clone();var f=i.find("i");if(!f.length){f=$("");i.prepend(f)}f.attr("class","fa fa-fw fa-arrow-circle-right pw-nav-icon");i.removeAttr("data-json").removeAttr("class");i.find("small").remove();var h=$("
    • ").addClass("pw-nav-dup").append(i);b.append(h);if(g.add){var h=$("
    • "+g.add.label+"
    • ");b.append(h)}$.each(g.list,function(k){if(this.label.indexOf("-1){this.label=this.label.replace(/<\/?span[^>]*>/g,"")}var q="";var r=$("
      "+this.label+"
      ");var s=r.text();if(s.length>30){var o=r.find("small");if(o.length){o.remove()}s=r.text();s=s.substring(0,30);var k=s.lastIndexOf(" ");if(k>3){s=s.substring(0,k)+"… "}r.html(s);if(o.length){r.append(o)}}s=r.html().replace(" "," ");if(this.icon){q=""}var j=this.url.indexOf("/")===0?this.url:g.url+this.url;var l=$(""+q+s+"");var p=$("
    • ").append(l);if(this.navJSON!="undefined"&&this.navJSON){l.addClass("pw-has-items pw-has-ajax-items").attr("data-json",this.navJSON);var m=$("
        ");p.addClass("uk-parent").append(m);UIkit.nav(m,{multiple:true})}if(typeof this.className!="undefined"&&this.className&&this.className.length){p.addClass(this.className)}if(p.hasClass("pw-nav-add")||p.hasClass("pw-pagelist-show-all")){b.children(".pw-nav-dup").after(p.removeClass("separator").addClass("pw-nav-add"))}else{b.append(p)}});c.remove();b.addClass("navJSON").addClass("length"+parseInt(g.list.length)).hide();if(b.children().length){b.css("opacity",1).fadeIn("fast")}});return false})},setupInputfields:function(){var d=$("body").hasClass("AdminThemeUikitNoGrid");function e(j){$("form.uk-form-horizontal").each(function(){$(this).find(".InputfieldContent > .Inputfields").each(function(){var l=$(this);l.addClass("uk-form-vertical");l.find(".uk-form-label").removeClass("uk-form-label");l.find(".uk-form-controls").removeClass("uk-form-controls")});$(this).find(".InputfieldSubmit, .InputfieldButton").each(function(){$(this).find(".InputfieldContent").before("
         
        ")})});$(".InputfieldNoBorder.uk-card").removeClass("uk-card uk-card-default");$(".InputfieldIsOffset.InputfieldColumnWidthFirst").each(function(){var m=$(this);var l;do{l=m.next(".InputfieldColumnWidth");if(!l.length||l.hasClass("InputfieldColumnWidthFirst")){break}l.addClass("InputfieldIsOffset");m=l}while(true)});$(".Inputfields").each(function(){b($(this))});$(".ui-widget.Inputfield, .ui-widget-header.InputfieldHeader, .ui-widget-content.InputfieldContent").removeClass("ui-widget ui-widget-header ui-widget-content");$(".MarkupPagerNav:not(.uk-pagination)").each(function(){$(this).addClass("uk-pagination")});if(typeof j=="undefined"){j=$(".InputfieldForm")}var k=$("select:not([multiple]):not(.uk-select)",j);k.addClass("uk-select")}function b(j){$(".InputfieldRowFirst",j).removeClass("InputfieldRowFirst");$(".InputfieldRowLast",j).removeClass("InputfieldRowLast");var k=j.children(".Inputfield:not(.InputfieldStateHidden):eq(0)");if(!k.length){return}do{k.addClass("InputfieldRowFirst");k=k.next(".Inputfield:not(.InputfieldStateHidden)")}while(k.hasClass("InputfieldColumnWidth")&&!k.hasClass("InputfieldColumnWidthFirst"));k=j.children(".Inputfield:last-child");while(k.length&&k.hasClass("InputfieldStateHidden")){k=k.prev(".Inputfield")}do{k.addClass("InputfieldRowLast");if(!k.hasClass("InputfieldColumnWidth")||k.hasClass("InputfieldColumnWidthFirst")){break}k=k.prev(".Inputfield:not(.InputfieldStateHidden)")}while(k.hasClass("InputfieldColumnWidth"))}var i=[];function c(l,p){if(d&&typeof p!="undefined"){if(typeof l=="string"){p.addClass(l)}else{p.css("width",l+"%")}return""}var k="uk-width-1-1";var j=k;var n=false;if(typeof l=="string"&&typeof p!="undefined"){j=l;n=true}else{if(!l||l>=100){j=k}else{if(typeof i[l]!="undefined"){j="uk-width-"+i[l]}else{for(var m in ProcessWire.config.ukGridWidths){var o=ProcessWire.config.ukGridWidths[m];m=parseInt(m);if(l>=m){j=o;break}}if(j.length){i[l]=j;j="uk-width-"+j}}}}if(!n&&j&&j!=k){j+="@m"}if(typeof p!="undefined"){if(j&&p.hasClass(j)){}else{g(p);if(j){p.addClass(j)}}}return j}function g(k){var l=null;if(typeof k!="string"){l=k;k=l.attr("class")}if(k.indexOf("uk-width-")>-1){var j=k.replace(/uk-width-(\d-\d|expand)[@smxl]*\s*/g,"");if(l!==null){l.attr("class",j)}}return k}function a(j){if(!j){return}var q=j.parent().children(".Inputfield");var r=null;var l=0;var p=0;var t=0;var m=0;var k=false;function u(v,x){if(!k){return}if(typeof x=="undefined"){x=j}var w=x.attr("id");w=w.replace("wrap_Inputfield_","");console.log(w+" (combined width="+l+", w="+t+"): "+v)}function o(v){if(typeof v=="undefined"){v=r}if(v){if(d){v.addClass("InputfieldColumnWidthLast")}else{c("InputfieldColumnWidthLast uk-width-expand",v)}}}function n(){if(k){u("A: hidden",j)}m+=t;l+=t;if(r&&l>=95){m+=p;if(k){u("Updating last visible Inputfield to width="+m,r)}c(m,r);l=0;m=0;p=0;r=null}else{p+=t}}function s(){if(k){u("Skipping because full-width",j)}if(l<100&&r){o(r)}r=null;p=0;m=0;l=0}q.each(function(){j=$(this);var y=false;var x=false;var w=j.hasClass("InputfieldColumnWidth");var v=!w||j.hasClass("InputfieldColumnWidthFirst");if(v&&r&&l<100){o(r)}t=w?parseInt(j.attr("data-colwidth")):0;if(!t||t>=95){s();return}if(j.hasClass("InputfieldStateHidden")){n();return}if(!l||l>=100){l=0;x=true;y=false;if(k){u("B: starting new row",j)}}else{if(l+t>100){if(r){o(r)}l=0;x=true;if(k){u("C: start new row because width would exceed 100%",j)}}else{if(l+t==100){y=true;if(k){u("D: width is exactly 100%, so this is the last column",j)}}else{if(l+t>=95){y=true;t=100-l;if(k){u("D2: width is close enough to 100%, so this is the last column",j)}}else{if(k){u("E: not first or last column",j)}}}}}if(y){j.addClass("InputfieldColumnWidthLast")}else{j.removeClass("InputfieldColumnWidthLast")}if(x){j.addClass("InputfieldColumnWidthFirst");p=0}else{j.removeClass("InputfieldColumnWidthFirst")}if(y){r=null;l=0;m=0;if(p){t+=p}p=0}else{r=j;l+=t;m=t}c(t,j)});if(l<100&&r){o(r)}}var h=null;var f=function(l,k){var j=$(k);if(l.type=="showInputfield"){j.removeClass("uk-hidden")}else{j.show();j.addClass("uk-hidden")}a(j);if(h){return}h=setTimeout(function(){b(j.closest(".Inputfields"));var m=j.find(".Inputfields");if(m.length){m.each(function(){b($(this))})}h=null},100)};$(document).on("reloaded",function(){e($(this))});$(document).on("hideInputfield",f);$(document).on("showInputfield",f);$("body").addClass("InputfieldColumnWidthsInit");e()},setupTooltips:function(){$(".tooltip, .pw-tooltip").each(function(){$(this).removeClass("tooltip pw-tooltip");UIkit.tooltip($(this))})},linkTargetMainMouseoverEvent:function(){var b=$(this);var a=b.attr("href");if(a.length<2){return}if(b.attr("target")){return}if(b.parent("li").hasClass("PageListActionView")){b.attr("target","_top")}else{b.attr("target","main")}},logoClickEvent:function(){if($("body").hasClass("pw-layout-sidenav-init")){if($("#pw-admin-side").length){toggleSidebarPane()}else{UIkit.toggle("#offcanvas-nav").toggle()}}else{if(ProcessWire.config.adminTheme.logoAction==1){UIkit.toggle("#offcanvas-nav").toggle()}else{return true}}return false}};$(document).ready(function(){ProcessWireAdminTheme.ready()}); \ No newline at end of file diff --git a/wire/modules/Process/ProcessField/ProcessField.module b/wire/modules/Process/ProcessField/ProcessField.module index bc08f759..f27cc214 100644 --- a/wire/modules/Process/ProcessField/ProcessField.module +++ b/wire/modules/Process/ProcessField/ProcessField.module @@ -2651,6 +2651,12 @@ class ProcessField extends Process implements ConfigurableModule { if($property == 'description' || $property == 'all') $search[] = $item->description; if($property == 'notes' || $property == 'all') $search[] = $item->notes; } + + if($property == 'data') { + foreach($item->getArray() as $k => $v) { + $search[] = (string) $v; + } + } $search = implode(' ', $search); $pos = stripos($search, $text); diff --git a/wire/modules/Process/ProcessPageSearch/ProcessPageSearch.module b/wire/modules/Process/ProcessPageSearch/ProcessPageSearch.module index 8bb29ca4..86abb237 100644 --- a/wire/modules/Process/ProcessPageSearch/ProcessPageSearch.module +++ b/wire/modules/Process/ProcessPageSearch/ProcessPageSearch.module @@ -12,6 +12,14 @@ * https://processwire.com * * @method string findReady($selector) + * @property string $searchFields + * @property string $searchFields2 + * @property string $displayField + * @property string $operator Single-word/partial match operator + * @property string $operator2 Multi-word operator + * @property array $searchTypesOrder + * @property array $noSearchTypes + * * */ @@ -89,15 +97,32 @@ class ProcessPageSearch extends Process implements ConfigurableModule { * */ protected $lister = null; - + /** - * Mode indicating admin ajax search, set by GET var admin_search=1 + * Debug mode? * - * This mode typically focuses on just searching the 'title' field. - * This mode also includes data specific to ajax requests. + * @var bool * */ - protected $adminSearchMode = false; + protected $debug = true; + + public function __construct() { + parent::__construct(); + $this->set('searchFields', 'title body'); + $this->set('searchFields2', 'title'); + $this->set('displayField', 'name'); + $this->set('operator', self::defaultOperator); + $this->set('operator2', '~='); + $this->set('searchTypesOrder', array('fields', 'templates', 'modules', 'pages', 'trash')); + $this->set('noSearchTypes', array()); // search types that have been removed + + // make nativeSorts indexed by value + $sorts = array(); + foreach($this->nativeSorts as $sort) { + $sorts[$sort] = $sort; + } + $this->nativeSorts = $sorts; + } /** * Initialize module @@ -108,12 +133,22 @@ class ProcessPageSearch extends Process implements ConfigurableModule { foreach($this->fields as $field) { if($field->type instanceof FieldtypeFieldsetOpen) continue; if($field->type instanceof FieldtypePassword) continue; - $this->fieldOptions[] = $field->name; + // @todo add field access control checking + $this->fieldOptions[$field->name] = $field->name; } - sort($this->fieldOptions); + ksort($this->fieldOptions); parent::init(); } + + public function set($key, $value) { + if($key == 'searchFields' || $key == 'searchFields2') { + if(is_array($value)) $value = implode(' ', $value); + } else if($key == 'noSearchTypes' && !is_array($value)) { + $value = explode(' ', $value); + } + return parent::set($key, $value); + } /** * Get operators used for searches, where key is operator and value is description @@ -123,6 +158,7 @@ class ProcessPageSearch extends Process implements ConfigurableModule { */ static public function getOperators() { $f = __FILE__; + $anyOrder = __('(in any order)', $f); return array( '=' => __('Equals', $f), '!=' => __('Does not equal', $f), @@ -130,11 +166,11 @@ class ProcessPageSearch extends Process implements ConfigurableModule { '>=' => __('Greater than or equal to', $f), '<' => __('Less than', $f), '<=' => __('Less than or equal to', $f), - '*=' => __('Contains phrase or partial word', $f), - '%=' => __('Contains phrase/word using LIKE', $f), - '~=' => __('Contains all the words', $f), - '^=' => __('Starts with', $f), - '$=' => __('Ends with', $f), + '*=' => __('Contains phrase or partial word', $f) . '*', + '%=' => __('Contains phrase/word using LIKE', $f) . '*', + '~=' => __('Contains all the words', $f) . ' ' . $anyOrder, + '^=' => __('Starts with', $f) . '*', + '$=' => __('Ends with', $f) . '*', ); } @@ -163,7 +199,7 @@ class ProcessPageSearch extends Process implements ConfigurableModule { * @return string Must return the selector (optionally modified) * */ - protected function ___findReady($selector) { + public function ___findReady($selector) { return $selector; } @@ -180,7 +216,6 @@ class ProcessPageSearch extends Process implements ConfigurableModule { } $ajax = $this->wire('config')->ajax; - if($this->lister && $ajax) { // we will just let Lister do it's thing, since it remembers settings in session return $this->lister->execute(); @@ -216,24 +251,55 @@ class ProcessPageSearch extends Process implements ConfigurableModule { * */ public function ___executeFor() { + + /** @var Languages $languages */ + $languages = $this->wire('languages'); + /** @var User $user */ + $user = $this->wire('user'); + /** @var WireInput $input */ + $input = $this->wire('input'); + /** @var Sanitizer $sanitizer */ + $sanitizer = $this->wire('sanitizer'); + + if($input->get('admin_search')) return $this->executeLive(); $this->fullSetup(); - $selector = ''; + $selectors = array(); $limit = $this->resultLimit; $start = 0; $status = 0; $names = array(); - $adminSearchStr = ''; - $languages = $this->wire('languages'); $userLanguage = null; - $user = $this->wire('user'); $superuser = $user->isSuperuser(); $checkEditAccess = false; + $hasInclude = ''; - foreach($this->input->get as $name => $value) { + // names to skip (must be lowercase) + $skipNames = array( + 'get', + 'display', + 'format_name', + 'admin_search' + ); + + // names to convert (keys must be lowercase) + $convertNames = array( + 'hasparent' => 'has_parent', + 'checkaccess' => 'check_access', + ); + + foreach($input->get as $name => $value) { + + $lowerName = strtolower(trim($name)); - if($name == 'get' || $name == 'display' || $name == 'format_name') continue; - if($name == 'lang_id') { + if(isset($convertNames[$lowerName])) { + $name = $convertNames[$lowerName]; + $lowerName = strtolower($name); + } + + if(in_array($lowerName, $skipNames)) continue; + + if($lowerName == 'lang_id') { if($languages) { // force results for specific language $language = $languages->get((int) $value); @@ -245,7 +311,7 @@ class ProcessPageSearch extends Process implements ConfigurableModule { } continue; } - + // operator has no '=', so we'll get the value from the name // so that you can do something like: bedrooms>5 rather than bedrooms>=5 if(!strlen($value) && preg_match('/([^<>]+)\s*([<>])\s*([^<>]+)/', $name, $matches)) { @@ -253,7 +319,7 @@ class ProcessPageSearch extends Process implements ConfigurableModule { $name = $matches[1]; $operator = $matches[2]; $value = $matches[3]; - + } else { $operator = substr($name, -1) . '='; @@ -268,49 +334,51 @@ class ProcessPageSearch extends Process implements ConfigurableModule { // replace '-' with '.' since '.' is not allowed in URL variable names if(strpos($name, '-')) $name = str_replace('-', '.', $name); - if(strpos($name, ',')) $name = $this->sanitizer->names($name, ',', array('_', '.')); - else $name = $this->sanitizer->pageName($name); // note: switch to pageName over fieldName to support "." + if(strpos($name, ',')) { + $name = $sanitizer->names($name, ',', array('_', '.')); + } else { + $name = $sanitizer->pageName($name); // note: switch to pageName over fieldName to support "." + } if(!$name) continue; + $lowerName = strtolower($name); - if($name == 'limit') { + if($lowerName == 'limit') { $limit = (int) $value; - $this->input->whitelist('limit', $value); + $input->whitelist('limit', $value); continue; } - if($name == 'start') { + if($lowerName == 'start') { $start = (int) $value; - $this->input->whitelist('start', $value); + $input->whitelist('start', $value); continue; } // if dealing with a user other than superuser, only allow include=hidden - if($name == 'include' && $value != 'hidden' && !$superuser) { - if($user->hasPermission('page-edit') && $this->input->get('admin_search')) { - $value = 'unpublished'; - $checkEditAccess = true; - } else { - $value = 'hidden'; + if($lowerName == 'include') { + $name = $lowerName; + $value = strtolower($value); + if($value != 'hidden' && !$superuser) { + if($user->hasPermission('page-edit') && $input->get('admin_search')) { + $value = 'unpublished'; + $checkEditAccess = true; + } else { + $value = 'hidden'; + } } + $hasInclude = $value; } // don't allow setting of check_access property, except for superuser - if($name == 'check_access' && !$superuser) continue; + if($lowerName == 'check_access' && !$superuser) continue; // don't allow setting of the 'status' property, except for superuser - if($name == 'status') { + if($lowerName == 'status') { if(!$superuser) continue; $status = (int) $value; } - // check if adminSearchMode should be enabled (for ajax search) - if($name == 'admin_search') { - $adminSearchStr = $this->sanitizer->selectorValue($value); - $this->adminSearchMode = true; - continue; - } - // replace URL-compatible comma separators with selector-compatible pipes if(strpos($name, ',')) $name = str_replace(',', '|', $name); @@ -327,57 +395,39 @@ class ProcessPageSearch extends Process implements ConfigurableModule { if(strpos($val, '|')) { $valuesOR = explode('|', $val); foreach($valuesOR as $k => $v) { - $valuesOR[$k] = $this->sanitizer->selectorValue($v); + $valuesOR[$k] = $sanitizer->selectorValue($v); } $val = implode('|', $valuesOR); } else { - $val = $this->sanitizer->selectorValue($val); + $val = $sanitizer->selectorValue($val); } $valuesAND[$key] = $val; } + $value = implode(',', $valuesAND); - - $this->input->whitelist($name . rtrim($operator, '='), trim($value, '"\'')); + $input->whitelist($name . rtrim($operator, '='), trim($value, '"\'')); + foreach($valuesAND as $val) { - $selector .= "$name$operator$val, "; + $selectors[] = "$name$operator$val"; } + $names[] = $name; - } - - if(strlen($adminSearchStr)) { - // adminSearchMode active, auto populate a search, like title%=value - $fields = $this->searchFields2 ? explode(' ', $this->searchFields2) : array('title'); - $operator = strlen($adminSearchStr) < 4 ? '%=' : '*='; - $selector .= implode('|', $fields) . $operator . $adminSearchStr . ", "; - } - - if($start) $selector .= "start=$start, "; - $selector .= "limit=$limit, "; - - $selector = rtrim($selector, ", "); - $displaySelector = $selector; - - if($this->adminSearchMode && !in_array('has_parent', $names)) { - // exclude repeaters from matching, when present - $admin = $this->wire('pages')->get($this->wire('config')->adminRootPageID); - $repeaters = $admin->children("name=repeaters, include=all"); - if(count($repeaters)) $selector .= ", has_parent!=" . $repeaters->first()->id; - } - - if(!$status && !preg_match('/\binclude=/', $selector)) { - if($superuser) { - // superuser only - $selector .= ", include=all, status<" . Page::statusTrash; - } else if($this->adminSearchMode && $user->hasPermission('page-edit')) { - // admin search mode and user has some kind of page-edit permission - $selector .= ", include=unpublished, status<" . Page::statusTrash; - $checkEditAccess = true; - } - } + + } // foreach input - $selector = $this->findReady($selector); + if($start) $selectors[] = "start=$start"; + $selectors[] = "limit=$limit"; + $displaySelector = implode(',', $selectors); + + if(!$status && !$hasInclude && $superuser) { + // superuser only + $selectors[] = "include=all, status<" . Page::statusTrash; + } + + $selector = implode(', ', $selectors); + $selector = $this->findReady($selector); $items = $this->pages->find($selector); - + if(!$superuser && $checkEditAccess) { // filter out non-editable pages, since some may be included via include=unpublished foreach($items as $item) { @@ -391,6 +441,51 @@ class ProcessPageSearch extends Process implements ConfigurableModule { return $out; } + /** + * Execute live search + * + * @return string + * + */ + public function executeLive() { + require_once(dirname(__FILE__) . '/ProcessPageSearchLive.php'); + $liveSearch = new ProcessPageSearchLive($this); + $liveSearch->setSearchTypesOrder($this->searchTypesOrder); + $liveSearch->setNoSearchTypes($this->noSearchTypes); + $liveSearch->setDefaultOperators($this->operator, $this->operator2); + if($this->wire('config')->ajax) { + header('Content-type: application/json'); + return $liveSearch->execute(); + } else { + return $liveSearch->executeViewAll(); + } + } + + /** + * Get ID of the repeaters root page ID or 0 if not installed + * + * @return int + * + */ + public function getRepeatersPageID() { + /** @var Session $session */ + $session = $this->wire('session'); + $repeaterID = $session->getFor($this, 'repeaterID'); + if(is_int($repeaterID)) return $repeaterID; + if($this->wire('modules')->isInstalled('FieldtypeRepeater')) { + $repeaterPage = $this->wire('pages')->get( + "parent_id=" . $this->wire('config')->adminRootPageID . ", " . + "name=repeaters, " . + "include=all" + ); + $repeaterID = $repeaterPage->id; + $session->setFor($this, 'repeaterID', (int) $repeaterID); + } else { + $repeaterID = 0; + } + return $repeaterID; + } + /** * Return array of fields to display in results * @@ -463,7 +558,7 @@ class ProcessPageSearch extends Process implements ConfigurableModule { * @return string * */ - protected function render(PageArray $matches, $displaySelector) { + protected function render(PageArray $matches, $displaySelector = '') { $out = ''; @@ -525,13 +620,20 @@ class ProcessPageSearch extends Process implements ConfigurableModule { */ protected function buildSelector() { $selector = ''; // for regular ProcessPageSearch - + $input = $this->wire('input'); + // search query text - $q = $this->input->whitelist('q'); + $q = $input->whitelist('q'); if(strlen($q)) { - $searchFields = $this->searchFields; - if(is_string($searchFields)) $searchFields = explode(' ', $searchFields); - + // GET vars "property" or "field" can used interchangably + if($input->whitelist('property')) { + $searchFields = array($input->whitelist('property')); + } else if($input->whitelist('field')) { + $searchFields = explode(' ', $input->whitelist('field')); + } else { + $searchFields = $input->get('live') ? $this->searchFields2 : $this->searchFields; + if(is_string($searchFields)) $searchFields = explode(' ', $searchFields); + } foreach($searchFields as $fieldName) { $fieldName = $this->sanitizer->fieldName($fieldName); $selector .= "$fieldName|"; @@ -540,13 +642,13 @@ class ProcessPageSearch extends Process implements ConfigurableModule { } // determine if results are sorted by something other than relevance - $sort = $this->input->whitelist('sort'); + $sort = $input->whitelist('sort'); if($sort && $sort != 'relevance') { - $reverse = $this->input->whitelist('reverse') ? "-" : ''; + $reverse = $input->whitelist('reverse') ? "-" : ''; $selector .= ", sort=$reverse$sort"; // if a specific template isn't requested, then locate the templates that use this field and confine the search to them - if(!$this->input->whitelist('template') && !in_array($sort, $this->nativeSorts)) { + if(!$input->whitelist('template') && !isset($this->nativeSorts[$sort])) { $templates = array(); foreach($this->templates as $template) { if($template->fieldgroup->has($sort)) $templates[] = $template->name; @@ -556,8 +658,17 @@ class ProcessPageSearch extends Process implements ConfigurableModule { } // determine if search limited to a specific template - if($this->input->whitelist('template')) { - $selector .= ", template=" . $this->input->whitelist('template'); + if($input->whitelist('template')) { + $selector .= ", template=" . $input->whitelist('template'); + } + + $trash = $input->whitelist('trash'); + if($trash !== null && $this->wire('user')->isSuperuser()) { + if($trash === 0) { + $selector .= ", status!=trash"; + } else if($trash === 1) { + $selector .= ", status=trash, include=all"; + } } if(!$selector) { @@ -578,7 +689,7 @@ class ProcessPageSearch extends Process implements ConfigurableModule { $adminRootPage = $this->wire('pages')->get($this->wire('config')->adminRootPageID); // exclude admin repeater pages unless the admin template is chosen - if(!$this->input->whitelist('template')) { + if(!$input->whitelist('template')) { // but only for superuser, as we're excluding all admin pages for non-superusers if($this->user->isSuperuser()) { $repeaters = $adminRootPage->child('name=repeaters, include=all'); @@ -607,29 +718,41 @@ class ProcessPageSearch extends Process implements ConfigurableModule { * */ protected function processInput() { + + /** @var WireInput $input */ + $input = $this->wire('input'); + /** @var Sanitizer $sanitizer */ + $sanitizer = $this->wire('sanitizer'); - // search fields - if($this->input->get->field) { - $field = str_replace(',', ' ', $this->input->get->field); + // search query + $q = $input->get('q'); + if($q !== null) $this->processInputQuery($q); + + // search fields (can optionally contain multiple CSV field names) + $field = $input->get('field'); + if($field) { + $field = str_replace(',', ' ', $field); $fieldArray = explode(' ', $field); $field = ''; foreach($fieldArray as $f) { - $f = $this->sanitizer->fieldName($f); - if(!in_array($f, $this->fieldOptions) && !in_array($f, $this->nativeSorts)) continue; + $f = $sanitizer->fieldName($f); + if(!isset($this->fieldOptions[$f]) && !isset($this->nativeSorts[$f])) continue; $field .= $f . " "; } $field = rtrim($field, " "); if($field) { $this->searchFields = $field; - $this->input->whitelist('field', $field); + $input->whitelist('field', $field); } + } else if($input->get('live')) { + $input->whitelist('field', $this->searchFields2); } else { - $this->input->whitelist('field', $this->searchFields); + $input->whitelist('field', $this->searchFields); } // operator, search type if(empty($this->operator)) $this->operator = self::defaultOperator; - $operator = $this->input->get->operator; + $operator = $input->get('operator'); if(!is_null($operator)) { if(array_key_exists($operator, $this->operators)) { $this->operator = substr($this->input->get->operator, 0, 3); @@ -637,73 +760,212 @@ class ProcessPageSearch extends Process implements ConfigurableModule { $operators = array_keys($this->operators); if(isset($operators[$operator])) $this->operator = $operators[$operator]; } - $this->input->whitelist('operator', $this->operator); + $input->whitelist('operator', $this->operator); } - // search query - $q = $this->sanitizer->text(substr($this->input->get->q, 0, 128)); - $this->input->whitelist('q', $q); - // sort - $this->input->whitelist('sort', 'relevance'); - if($this->input->get->sort) { - $sort = $this->sanitizer->fieldName($this->input->get->sort); - if($sort && (in_array($sort, $this->nativeSorts) || in_array($sort, $this->fieldOptions))) { - $this->input->whitelist('sort', $sort); + $input->whitelist('sort', 'relevance'); + $sort = $input->get('sort'); + if($sort) { + $sort = $sanitizer->fieldName($sort); + if($sort && (isset($this->nativeSorts[$sort]) || isset($this->fieldOptions[$sort]))) { + $input->whitelist('sort', $sort); } - if($this->input->get->reverse) { - $this->input->whitelist('reverse', 1); + if($input->get('reverse')) { + $input->whitelist('reverse', 1); } } // template - if($this->input->get->template) { - $template = $this->sanitizer->name($this->input->get->template); - if(!$this->templates->get($template)) $template = ''; - if($template) $this->input->whitelist('template', $template); + $template = $input->get('template'); + if($template) { + $template = $sanitizer->templateName($template); + $template = $this->wire('templates')->get($template); + if($template && $this->wire('user')->hasPermission('page-view', $template)) { + $input->whitelist('template', $template->name); + } + } + + // trash (liveSearch) + $trash = $input->get('trash'); + if($trash !== null && $this->wire('user')->isSuperuser()) { + $trash = (int) $trash; + if($trash === 0 || $trash === 1) { + $input->whitelist('trash', $trash); + } + } + + // custom property (like 'field', except can contain only one name) + $property = $input->get('property'); + if($property !== null) { + $property = $sanitizer->fieldName($property); + if($this->isSelectableFieldName($property)) { + $input->whitelist('property', $property); + } + } + } + + /** + * Process input for the $q query variable + * + * Since $q can also have type, field/property, operator and search text embedded within it, + * this function separates all of those out and populates GET variables for them, when present. + * + * @param $q + * + */ + protected function processInputQuery($q) { + + /** @var WireInput $input */ + $input = $this->wire('input'); + /** @var Sanitizer $sanitizer */ + $sanitizer = $this->wire('sanitizer'); + + $q = trim($sanitizer->text($q)); + $redirectUrl = ''; + $operator = ''; + $operators = $this->operators; + $type = ''; + + $operators['=='] = 'Equals'; + $operators[':'] = 'Auto'; // alternative to '=' + + // handle cases where search type (template), property, and operator are bundled in with the $q + if(!$this->operator) $this->operator = '%='; + + // deetermine which operator (if any) is present in $q + foreach($operators as $operator => $description) { + if(strpos($q, $operator) === false) continue; + if(!preg_match('/^([^=%$*<>~^:]+)' . $operator . '([^=%$*<>~^:]+)$/', $q, $matches)) continue; + if($operator === '=') $operator = '?'; // operator to be determined on factors search text + if($operator === '==') $operator = '='; + $type = $sanitizer->name($matches[1]); + $q = trim($matches[2]); + break; } + if($operator === '?') { + // operator was '=': use 'contains words' operator if there is more than one word in $q + $operator = strpos($q, ' ') ? '~=' : $this->operator; + } else if(empty($operator)) { + // operator was not present, only query text was, so use default operator + $operator = $this->operator; + } + $input->get->operator = $operator; + + if(strpos($type, '.')) { + // type with property/field + list($type, $field) = explode('.', $type, 2); + $field = $this->wire('sanitizer')->fieldName(trim($field)); + $input->get->field = $field; + } else { + $field = ''; + } + + if($type == 'pages') { + // okay + } else if($type == 'trash') { + $input->get->trash = 1; + } else if($type) { + $template = $type ? $this->wire('templates')->get($type) : ''; + if($template) { + // defined template + $input->get->template = $template->name; + } else { + // some other non-page type + $redirectUrl = $this->wire('page')->url . 'live/' . + '?q=' . urlencode($q) . + '&type=' . urlencode($type) . + '&property=' . urlencode($field) . + '&operator=' . urlencode($operator); + } + } + + if($redirectUrl) $this->wire('session')->redirect($redirectUrl); + + $input->whitelist('q', $q); } /** * Is the given field name selectable? * - * @param string $name - * @param int $level + * @param string $name Field "name" or "name1|name2|name3" + * @param int $level Greater than 0 when recursive * @return bool * */ protected function isSelectableFieldName($name, $level = 0) { $selectable = array( - 'parent', 'template', 'template_label', 'has_parent', 'hasParent', - 'children', 'numChildren', 'num_children', 'count', 'path', + 'parent', + 'template', + 'template_label', + 'has_parent', + 'hasParent', + 'children', + 'numChildren', + 'num_children', + 'count', + 'path', + ); + + $notSelectable = array( + // must be lowercase + 'pass', + 'config', + 'it', + 'display', + ); + + $noSubnames = array( + // must be lowercase + 'include', + 'check_access', + 'checkaccess', ); $is = false; - if(!$level && strpos($name, '|')) { + if(!$level && strpos($name, '|') !== false) { $names = explode('|', $name); $cnt = 0; - foreach($names as $n) if(!$this->isSelectableFieldName($n, $level+1)) $cnt++; + foreach($names as $n) { + if(!$this->isSelectableFieldName($n, $level + 1)) $cnt++; + } return $cnt == 0; } else if(strpos($name, '.')) { - list($name, $subname) = explode('.', $name); + list($name, $subname) = explode('.', $name, 2); + if(strpos($subname, '.') !== false) return false; + if(in_array(strtolower($subname), $noSubnames)) return false; if(!$this->isSelectableFieldName($subname, $level)) return false; } - if($name == 'path' && (!$this->wire('modules')->isInstalled('PagePaths') || $this->wire('languages'))) { + $lowerName = strtolower($name); + + if($lowerName == 'path' && (!$this->wire('modules')->isInstalled('PagePaths') || $this->wire('languages'))) { $name = 'name'; + $lowerName = $name; } - if(in_array($name, $this->nativeSorts)) $is = true; - else if(in_array($name, $selectable)) $is = true; - else if(!$level && in_array($name, array('include', 'status', 'check_access'))) $is = true; - else if(in_array($name, $this->fieldOptions)) $is = true; + if(isset($this->nativeSorts[$name])) { + // native sort properties + $is = true; + } else if(in_array($name, $selectable)) { + // always selectable properties + $is = true; + } else if(!$level && in_array($name, array('include', 'status', 'check_access'))) { + // selectable, but only if not OR’d with other fields (level=0), and must be access checked outside this method + $is = true; + } else if(isset($this->fieldOptions[$name])) { + // custom fields + $is = true; + } - if($name == 'pass' || $name == 'config' || $name == 'it' || $name == 'display') $is = false; + if(in_array($lowerName, $notSelectable)) { + $is = false; + } return $is; } @@ -839,7 +1101,6 @@ class ProcessPageSearch extends Process implements ConfigurableModule { $form->add($field); return $form->render(); - } @@ -897,62 +1158,6 @@ class ProcessPageSearch extends Process implements ConfigurableModule { return $out; } - /** - * Find other types of ProcessWire assets that may be useful in search - * - * Applicable to adminSearchMode only. - * - * @param string $q Text to find - * @return array Array of matches - * - */ - protected function findOtherTypes($q) { - - $language = $this->wire('user')->language; - $language = $language && $language->id && !$language->isDefault() ? $language->id : ''; - $results = array(); - - foreach($this->wire('fields') as $field) { - $name = $field->name; - $label = $field->{"label$language"}; - if(stripos($name . $label, $q) !== false) $results[] = array( - 'id' => $field->id, - 'template_label' => str_replace('Fieldtype', '', $field->type), - 'title' => $field->name, - 'editUrl' => $this->wire('config')->urls->admin . "setup/field/edit?id=$field->id", - 'type' => $this->_x('Fields', 'match-type') - ); - } - - foreach($this->wire('templates') as $template) { - $name = $template->name; - $label = $template->{"label$language"}; - if(stripos($name . $label, $q) !== false) $results[] = array( - 'id' => $template->id, - 'template_label' => $template->name, - 'title' => $label ? $label : $template->name, - 'editUrl' => $this->wire('config')->urls->admin . "setup/template/edit?id=$template->id", - 'type' => $this->_x('Templates', 'match-type') - ); - } - - foreach($this->wire('modules') as $module) { - //if($module instanceof ModulePlaceholder) continue; - $name = $module->className(); - $info = $this->wire('modules')->getModuleInfo($module); - $title = $module instanceof ModulePlaceholder ? $name : $info['title']; - if(stripos($name . $title, $q) !== false) $results[] = array( - 'id' => $module->id, - 'template_label' => $name, - 'title' => $title, - 'editUrl' => $this->wire('config')->urls->admin . "module/edit?name=$name", - 'type' => $this->_x('Modules', 'match-type') - ); - } - - return $results; - } - /** * Render the provided matches as a JSON string for AJAX use * @@ -979,14 +1184,6 @@ class ProcessPageSearch extends Process implements ConfigurableModule { if($language && !$language->isDefault()) $templateLabel = "label$language"; } - if($this->adminSearchMode && $this->user->isSuperuser()) { - // enable search to include users when adminSearchMode and superuser - $a['matches'] = $this->findOtherTypes($this->input->get('admin_search')); - $users = $this->wire('users')->find("name%=" . - $this->wire('sanitizer')->pageName($this->input->get('admin_search'), Sanitizer::toAscii)); - if(count($users)) $matches->prepend($users); - } - foreach($matches as $page) { /** @var Page $page */ @@ -1106,9 +1303,11 @@ class ProcessPageSearch extends Process implements ConfigurableModule { } else { $placeholder = ''; } - + + $action = $adminURL . 'page/search/live/'; + $out = - "\n" . + "\n" . "\n\t" . "\n\t" . "\n\t" . //" . $this->_x('Search', 'input') . "' />" . // Text that appears as the placeholder text in the top search submit input @@ -1124,82 +1323,132 @@ class ProcessPageSearch extends Process implements ConfigurableModule { $inputfields = $this->wire(new InputfieldWrapper()); $modules = $this->wire('modules'); - - $inputfield = $modules->get("InputfieldText"); - $inputfield->attr('name', 'searchFields'); - if(!isset($data['searchFields'])) $data['searchFields'] = 'title body'; - if(is_array($data['searchFields'])) $data['searchFields'] = implode(' ', $data['searchFields']); - $inputfield->attr('value', $data['searchFields']); - $inputfield->label = $this->_("Default fields to search"); - $description = $this->_("Enter the names for one or more text-based fields that you want to search, separating each by a space."); // Default fields description - $inputfield->description = $description; - $inputfields->append($inputfield); + $textFields = array(); + $allSearchTypes = array('pages', 'trash', 'modules'); + $textOperators = array('%=', '~=', '*=', '^=', '='); + $allOperators = self::getOperators(); - $inputfield = $modules->get("InputfieldText"); - $inputfield->attr('name', 'searchFields2'); - if(!isset($data['searchFields2'])) $data['searchFields2'] = 'title'; - if(is_array($data['searchFields2'])) $data['searchFields2'] = implode(' ', $data['searchFields2']); - $inputfield->attr('value', $data['searchFields2']); - $inputfield->label = $this->_("Field(s) to search in admin search (ajax) mode"); - $inputfield->description = $description; - $inputfield->notes = $this->_("We recommend limiting this to 1 or 2 fields at the most since results populate a live autocomplete field. Typically you would just search the title."); // Fields to search description - $inputfields->append($inputfield); - - $inputfield = $modules->get("InputfieldText"); - $inputfield->attr('name', 'displayField'); - $inputfield->attr('value', isset($data['displayField']) ? $data['displayField'] : 'name'); - $inputfield->label = $this->_("Default field name(s) to display in search results"); - $inputfield->description = $this->_("If specifying more than one field, separate each with a space."); - $inputfields->append($inputfield); - - $inputfield = $modules->get("InputfieldSelect"); - $inputfield->attr('name', 'operator'); - $inputfield->attr('value', isset($data['operator']) ? $data['operator'] : self::defaultOperator); - $inputfield->label = $this->_("Default search operator"); - foreach(self::getOperators() as $operator => $label) { - $inputfield->addOption($operator, "$operator $label"); + if(!isset($data['searchTypesOrder'])) $data['searchTypesOrder'] = array(); + if(!isset($data['noSearchTypes'])) $data['noSearchTypes'] = array(); + $searchTypesOrder = &$data['searchTypesOrder']; + $noSearchTypes = &$data['noSearchTypes']; + + // find all text fields + foreach($this->wire('fields') as $field) { + if(!$field->type instanceof FieldtypeText) continue; + $textFields[$field->name] = $field; } - $inputfields->append($inputfield); + + // ensure that base/built-in search types are present + foreach($allSearchTypes as $key) { + if(!in_array($key, $searchTypesOrder)) $searchTypesOrder[] = $key; + } + + // find searchable modules + foreach($this->wire('modules') as $module) { + $info = $this->wire('modules')->getModuleInfoVerbose($module); + if(empty($info['searchable'])) continue; + $name = $info['searchable']; + if(is_bool($name) || ctype_digit($name)) $name = $info['name']; + $allSearchTypes[$name] = $name; + if(!in_array($name, $searchTypesOrder)) $searchTypesOrder[] = $name; + } + + $fieldset = $modules->get('InputfieldFieldset'); + $fieldset->label = $this->_('Admin live search'); + $fieldset->icon = 'search'; + $inputfields->add($fieldset); + + $f = $modules->get('InputfieldAsmSelect'); + $f->attr('name', 'searchTypesOrder'); + $f->label = $this->_('Search order'); + $f->description = + $this->_('These are the types of searches that will be performed during an admin live search.') . ' ' . + $this->_('Drag them to the order you want the search results to be listed in.'); + foreach($allSearchTypes as $name) { + $label = $name; + if(in_array($name, $noSearchTypes)) $label .= ' ' . $this->_('(excluded)'); + $f->addOption($name, $label); + } + $f->attr('value', $searchTypesOrder); + $f->setAsmSelectOption('deletable', false); + $f->setAsmSelectOption('addable', false); + $fieldset->add($f); + + $f = $modules->get('InputfieldAsmSelect'); + $f->attr('name', 'noSearchTypes'); + $f->label = $this->_('Exclude search types'); + $f->description = + $this->_('Select any search types that you want to exclude from live search. These might be types you don’t often need to search.') . ' ' . + $this->_('The more types excluded, the faster the live search will perform.') . ' ' . + $this->_('Any selected types can still be searched if asked for specifically in the search.') . ' ' . + $this->_('For example, if you excluded the “trash” type, it could still be searched if you prefixed your search with “trash=”, like “trash=hello”.'); + foreach($allSearchTypes as $name) { + $f->addOption($name); + } + $f->attr('value', $noSearchTypes); + $fieldset->add($f); + + $f = $modules->get('InputfieldAsmSelect'); + $f->attr('name', 'searchFields2'); + $f->label = $this->_('Page fields to search'); + $f->description = + $this->_('This applies to search results from “pages” and “trash” only.') . ' ' . + $this->_("We recommend limiting this to 1 or 2 fields at the most to ensure the live search is fast. Typically you would just search the “title” field."); // Fields to search description + foreach($textFields as $field) $f->addOption($field->name); + $value = isset($data['searchFields2']) ? $data['searchFields2'] : array('title'); + $value = !is_array($value) ? explode(' ', $value) : $value; + $f->value = $value; + $fieldset->add($f); + + $f = $modules->get('InputfieldAsmSelect'); + $f->attr('name', 'searchFields'); + $f->label = $this->_('Page fields to search if user hits “enter” in the search box'); + $f->description = + $this->_('Typically this would be the same as above, but you might also want to add additional field(s).') . ' ' . + $this->_('For instance, rather than just searching the “title” field, you might want to also search a “body” field as well.'); + foreach($textFields as $field) $f->addOption($field->name); + $value = isset($data['searchFields']) ? $data['searchFields'] : array('title', 'body'); + $value = !is_array($value) ? explode(' ', $value) : $value; + $f->value = $value; + $fieldset->append($f); + + $f = $modules->get("InputfieldSelect"); + $f->attr('name', 'operator'); + $f->attr('value', isset($data['operator']) ? $data['operator'] : self::defaultOperator); + $f->label = $this->_('Default search operator for single and partial word searches'); + $f->columnWidth = 50; + foreach($textOperators as $operator) { + $label = $allOperators[$operator]; + $f->addOption($operator, "$operator $label"); + } + $partialLabel = '*' . $this->_('Indicates support of partial word matches'); + $f->notes = $partialLabel; + $fieldset->append($f); + + $f = $modules->get("InputfieldSelect"); + $f->attr('name', 'operator2'); + $f->attr('value', isset($data['operator2']) ? $data['operator2'] : '~='); + $f->label = $this->_('Default search operator for multi-word (phrase) searches'); + $f->columnWidth = 50; + foreach($textOperators as $operator) { + $label = $allOperators[$operator]; + $f->addOption($operator, "$operator $label"); + } + $f->notes = $partialLabel; + $fieldset->append($f); + + // displayField: no longer used, except if user lacks page-lister permission + $f = $modules->get("InputfieldHidden"); + $f->attr('name', 'displayField'); + $f->attr('value', isset($data['displayField']) ? $data['displayField'] : 'name'); + $f->label = $this->_("Default field name(s) to display in search results"); + $f->description = $this->_("If specifying more than one field, separate each with a space."); + $inputfields->append($f); return $inputfields; } - /* - * No longer in use, but here for reference: - * - protected function renderShortcuts() { - $out = ''; - $links = array( - 'Quick Links', - "All by creation date" => '?q=&submit=Search&display=title+path+created&sort=created&reverse=1' , - "All by latest edit date" => '?q=&submit=Search&display=title+path+created&sort=modified&reverse=1', - "Users by creation date" => '?q=&template=user&submit=Search&operator=~%3D&display=name+email+created&sort=created&reverse=1', - 'New pages by template', - ); - - foreach($this->templates as $template) { - // Quick links only for content with more than one page - // if($template->getNumPages() < 2) continue; - - // Users get own quick link earlier, others are rather irrelevant - if($template->flags & Template::flagSystem) continue; - - $links[$template->name] = "?q=&template={$template->name}&submit=Search&operator=~%3D&display=title+path+created&sort=created&reverse=1"; - } - - foreach($links as $label => $value) { - if(is_int($label)) { - $out .= "

        $value

        "; - } else { - $value .= "&show_options=1"; - $value = htmlspecialchars($value); - $out .= "$label"; - } - } - - return $out; - } - */ } diff --git a/wire/modules/Process/ProcessPageSearch/ProcessPageSearchLive.php b/wire/modules/Process/ProcessPageSearch/ProcessPageSearchLive.php new file mode 100644 index 00000000..56e9ef40 --- /dev/null +++ b/wire/modules/Process/ProcessPageSearch/ProcessPageSearchLive.php @@ -0,0 +1,971 @@ + '', // type of search, if not pages, i.e. "templates", "fields", "modules", "comments", etc. + 'property' => '', // property to search for within type, or blank if no specific property + 'operator' => '%=', + 'q' => '', // query text to find + 'selectors' => array(), + 'template' => null, + 'multilang' => true, + 'language' => '', // language name + 'edit' => true, + 'start' => 0, + 'limit' => 15, + 'verbose' => false, + 'debug' => false, + ); + + /** + * Template for individual live search result items + * + * @var array + * + */ + protected $itemTemplate = array( + 'id' => 0, + 'url' => '', // required + 'name' => '', + 'title' => '', // required + 'subtitle' => '', + 'summary' => '', + 'icon' => '', + 'group' => '', + 'status' => 0, + 'modified' => 0, + ); + + /** + * Allowed operators + * + * @var array + * + */ + protected $allowOperators = array( + '=', '==', '!=', '*=', '~=', '%=', '^=', '$=', '<=', '>=', '<', '>' + ); + + /** + * Operator to use for single-word matches (if not overridden) + * + * @var string + * + */ + protected $singleWordOperator = '%='; + + /** + * Operator to use for multi-word matches (if not overridden) + * + * @var string + * + */ + protected $multiWordOperator = '~='; + + /** + * Default fields to search for pages + * + * @var array + * + */ + protected $defaultPageSearchFields = array('title'); + + /** + * Are we currently in “view all” mode? + * + * @var bool + * + */ + protected $isViewAll = false; + + /** + * Order to render results in, by search type + * + * @var array + * + */ + protected $searchTypesOrder = array(); + + /** + * Search types that are specifically excluded + * + * @var array + * + */ + protected $noSearchTypes = array(); + + /** + * PaginatedArray to use for pagination, when applicable for “view all” mode + * + * @var null|PaginatedArray + * + */ + protected $pagination = null; + + /** + * Shared translation labels, defined in constructor + * + * @var array + * + */ + protected $labels = array(); + + /** + * Construct + * + * @param Process|ProcessPageSearch $process + * @param array $liveSearch + * + */ + public function __construct(Process $process = null, array $liveSearch = array()) { + + if($process) { + $process->wire($this); + if($process instanceof ProcessPageSearch) $this->process = $process; + $a = explode(' ', $process->searchFields2); + if(count($a)) $this->defaultPageSearchFields = $a; + } + + if(!empty($liveSearch)) { + $this->liveSearchDefaults = array_merge($this->liveSearchDefaults, $liveSearch); + } + + $this->labels = array( + 'missing-query' => $this->_('No search specified'), + 'pages' => $this->_('Pages'), + 'trash' => $this->_('Trash'), + 'modules' => $this->_('Modules'), + 'view-all' => $this->_('View All'), + 'search-results' => $this->_('Search Results'), + ); + + parent::__construct(); + } + + /** + * Set order of search types + * + * @param array $types Names of types, in order + * + */ + public function setSearchTypesOrder(array $types) { + $this->searchTypesOrder = $types; + } + + /** + * Set types that should be excluded unless specifically asked for + * + * @param array $types Names of types to exclude + * + */ + public function setNoSearchTypes(array $types) { + $this->noSearchTypes = $types; + } + + /** + * Set default operators to use for searches (if query does not specify operator) + * + * @param string $singleWordOperator + * @param string $multiWordOperator + * + */ + public function setDefaultOperators($singleWordOperator, $multiWordOperator = '') { + $this->singleWordOperator = $singleWordOperator; + $this->multiWordOperator = empty($multiWordOperator) ? $singleWordOperator : $multiWordOperator; + } + + /** + * Initialize live search + * + * @param array $presets Additional info to populate in liveSearchInfo + * @return array Current liveSearchInfo + * + */ + protected function init(array $presets = array()) { + + /** @var WireInput $input */ + $input = $this->wire('input'); + /** @var Sanitizer $sanitizer */ + $sanitizer = $this->wire('sanitizer'); + /** @var Fields $fields */ + $fields = $this->wire('fields'); + /** @var Templates $templates */ + $templates = $this->wire('templates'); + /** @var User $user */ + $user = $this->wire('user'); + /** @var Languages $languages */ + $languages = $this->wire('languages'); + + $type = isset($presets['type']) ? $presets['type'] : ''; + $language = isset($presets['language']) ? $presets['language'] : ''; + $property = isset($presets['property']) ? $presets['property'] : ''; + $operator = isset($presets['operator']) ? $presets['operator'] : ''; + $template = isset($presets['template']) ? $presets['template'] : ''; + $limit = isset($presets['limit']) ? (int) $presets['limit'] : $this->liveSearchDefaults['limit']; + $start = isset($presets['start']) ? (int) $presets['start'] : ($input->pageNum() - 1) * $limit; + $selectors = array(); + $replaceOperator = ''; + $opHolders = array('<=' => '~@LT=', '>=' => '~@GT=', '<' => '~@LT', '>' => '~@GT'); // operator placeholders + + $q = empty($presets['q']) ? $input->get('q') : $presets['q']; + if(empty($q)) $q = $input->get('admin_search'); // legacy name + if(strpos($q, '~@') !== false) $q = str_replace('~@', '', $q); // disallow placeholder prefix + if(empty($operator)) $q = str_replace(array_keys($opHolders), array_values($opHolders), $q); + $q = $sanitizer->text($q, array('reduceSpace' => true)); + + if($user->isSuperuser() && strpos($q, 'DEBUG') !== false) { + $q = str_replace('DEBUG', '', $q); + $presets['debug'] = true; + } + + if(empty($q)) { + // if no query, we've got nothing to do + return $this->liveSearchDefaults; + } + + if(empty($operator)) { + // operator may be bundled into query: $q + if(strpos($q, '~@') !== false) { + foreach($opHolders as $op => $placeholder) { // <=, >=, <, > + if(strpos($q, $placeholder) === false) continue; + $replaceOperator = $placeholder; + $operator = $op; + break; + } + + } else if(strpos($q, '==') !== false) { + // forced equals operator + $replaceOperator = '=='; + $operator = '='; + + } else if(strpos($q, '=') !== false) { + // regular equals or other w/equals + $replaceOperator = '='; + if(preg_match('/([%~*^$<>!]{1,2}=)/', $q, $matches)) { + if(in_array($matches[1], $this->allowOperators)) { + $operator = $matches[1]; + $replaceOperator = $operator; + } + } else { + // regular equals, use default operator + } + } + + if($replaceOperator) { + $q = str_replace($replaceOperator, ':', $q); + } + } + + if(empty($operator) || !in_array($operator, $this->allowOperators)) { + $operator = strpos($q, ' ') ? $this->multiWordOperator : $this->singleWordOperator; + } + + // check if type and property may be part of query: $q + if(empty($type) && empty($property) && strpos($q, ':')) { + // Search specifies a specific type "type:text", i.e. "users:ryan" + list($type, $q) = explode(':', $q, 2); + // live search type: pages, users, modules, fields, templates, comments, etc. + $type = $sanitizer->name($type); + if(strpos($type, '.') !== false) { + // live search type includes a property, i.e. "pages.body", "users.first_name", etc. + list($type, $property) = explode('.', $type, 2); + } + if($type === 'pages') { + // ok + } else if($type) { + // check if type refers to a template name or language + $template = true; + $language = true; + } else { + // search all types + } + } else if($type) { + $template = true; + $language = true; + } + + if($language === true) { + // check if type refers to a language + $language = $languages ? $languages->get($type) : null; + if($language && $language->id) { + $language = $language->name; + $template = null; + $type = ''; + } else { + $language = ''; + } + } + + if($template === true) { + // check if type refers to template name or language + $template = $templates->get($type); + } + + if($template && $template instanceof Template) { + // does search type match the name of a template? + $selectors[] = "template=$template->name"; + $type = ''; + // $type = 'pages'; + } + + $type = $sanitizer->name($type); + $property = $sanitizer->fieldName($property); + $q = trim($q); + $value = $sanitizer->selectorValue($q); + $lp = strtolower($property); + + if($property && ($fields->isNative($property) || $fields->get($property)) && !in_array($lp, $this->skipProperties)) { + // we recognize this property as searchable, so add it to the selector + if($lp == 'status' && !$user->isSuperuser() && $value > Page::statusHidden) $value = Page::statusHidden; + $selectors[] = $property . $operator . $value; + } else { + // we did not recognize the property, so use field(s) defined in module instead + $selectors[] = implode('|', $this->defaultPageSearchFields) . $operator . $value; + } + + $liveSearch = array_merge($this->liveSearchDefaults, $presets, array( + 'type' => $type, + 'property' => $property, + 'operator' => $operator, + 'q' => $q, + 'selectors' => $selectors, + 'template' => $template, + 'multilang' => $this->wire('languages') ? true : false, + 'language' => $language, + 'start' => $start, + 'limit' => $limit + )); + + if($this->isViewAll) { + // variables for pagination + $input->whitelist('type', $type); + $input->whitelist('property', $property); + $input->whitelist('operator', $operator); + $input->whitelist('q', $q); + if(!empty($liveSearch['language'])) $input->whitelist('language', $liveSearch['language']); + } + + return $liveSearch; + } + + /** + * Execute live search and return JSON result + * + * @param bool $getJSON Get results as JSON string? Specify false to get array instead. + * @return string|array + * + */ + public function execute($getJSON = true) { + + /** @var WireInput $input */ + $input = $this->wire('input'); + + $liveSearch = $this->init(); + + if((int) $input->get('version') > 1) { + // version 2+ keep results in native format, for future use + $items = $this->find($liveSearch); + } else { + // version 1 is currently used by PW admin themes + $items = $this->convertItemsFormat($this->find($liveSearch)); + } + + $result = array( + 'matches' => &$items + ); + + return $getJSON ? json_encode($result) : $items; + } + + /** + * Render output for landing page to view all items of a particular type + * + * Expects these GET vars to be present: + * - type + * - operator + * - property + * - q + * + * @return string + * @throws WireException + * + */ + public function executeViewAll() { + + /** @var WireInput $input */ + $input = $this->wire('input'); + $this->isViewAll = true; + + $type = $input->get->pageName('type'); + $operator = $input->get('operator'); + $property = $input->get->fieldName('property'); + $language = $input->get->pageName('language'); + $q = $input->get->text('q'); + $this->pagination = new PaginatedArray(); + $this->wire($this->pagination); + + if(empty($q)) { + $this->error($this->labels['missing-query']); + return ''; + } + + if(false && ($type == 'pages' || $type == 'trash')) { + // let Lister handle it + $results = array(); + } else { + $liveSearch = $this->init(array( + 'type' => $type, + 'property' => $property, + 'operator' => $operator, + 'q' => $q, + 'limit' => $this->liveSearchDefaults['limit'], + 'verbose' => true, + 'language' => $language, + )); + $results = $this->find($liveSearch); + } + + if($this->process) { + $headline = $this->labels['search-results']; + if($type) $headline .= " - " . ucfirst($type); + $this->process->headline($this->pagination->getPaginationString(array( + 'label' => $headline, + 'count' => count($results) + ))); + } + + $out = $this->renderList($results); + + return $out; + } + + /** + * Perform find of types, pages, modules + * + * Result format that this find method expects from modules it calls the search() method from: + * + * $result = array( + * 'title' => 'Title of these items, used as the group label except where overridden item "group" property', + * 'url' => 'URL to view all items', // if omitted, one will be provided automatically + * 'total' => 999, // non-paginated total quantity (can be omitted if pagination not supported) + * 'items' => [ + * [ + * // required properties + * 'title' => 'Title of item', + * 'url' => 'URL to view or edit the item', + * // optional properties: + * 'id' => 0, + * 'name' => 'Name of item', + * 'icon' => 'Optional icon name to represent the item, i.e. "gear" or "fa-gear"', + * 'group' => 'Optionally group with other items having this group name, overrides $result[title]', + * 'status' => int, // if item is a Page, status of page using Page::status* constants + * 'summary' => 'Summary or description of item or excerpt of text that matched', // (recommended) + * 'subtitle' => 'Secondary title of item', // (recommended) + * 'modified' => int, // last modified date of item + * ], + * [ ... ], [ ... ], etc. + * ) + * ); + * + * @param array $liveSearch + * @return array Array of matches + * + */ + protected function find(array &$liveSearch) { + + $items = array(); + $user = $this->wire('user'); + $userLanguage = null; + $q = $liveSearch['q']; + $type = $liveSearch['type']; + $foundTypes = array(); + $modulesInfo = array(); + + /** @var Modules $modules */ + $modules = $this->wire('modules'); + + /** @var Languages $languages */ + $languages = $this->wire('languages'); + + if($languages && $liveSearch['language']) { + // change current user to have requested language, temporarily + $language = $languages->get($liveSearch['language']); + if($language && $language->id) { + $userLanguage = $user->language; + $user->language = $language; + } + } + + if($type != 'pages' && $type != 'trash') { + $modulesInfo = $modules->getModuleInfo('*', array('verbose' => true)); + } + + foreach($modulesInfo as $info) { + + if(empty($info['searchable'])) continue; + $name = $info['name']; + $thisType = $info['searchable']; + + if($type != $thisType && in_array($thisType, $this->noSearchTypes)) continue; + if($type && $this->isViewAll && $type != $thisType) continue; + if($type && stripos($thisType, $type) === false) continue; + if(!empty($liveSearch['template']) && !empty($liveSearch['property'])) continue; + if(!$user->isSuperuser() && !$modules->hasPermission($name, $user)) continue; + + $foundTypes[] = $thisType; + $module = null; + $result = array(); + $timer = null; + + try { + /** @var SearchableModule $module */ + $module = $modules->getModule($name, array('noInit' => true)); + if(!$module) continue; + $result = $module->search($q, $liveSearch); // see method phpdoc for $result format + + } catch(\Exception $e) { + // ok + } + + if(!$module || empty($result['items'])) continue; + if(empty($result['total'])) $result['total'] = count($result['items']); + + if(!in_array($thisType, $this->searchTypesOrder)) $this->searchTypesOrder[] = $thisType; + $order = array_search($thisType, $this->searchTypesOrder); + $order = $order * 100; + + $title = empty($result['title']) ? $info['title'] : $result['title']; + $n = $liveSearch['start']; + $item = null; + foreach($result['items'] as $item) { + $n++; + $item = array_merge($this->itemTemplate, $item); + if(empty($item['group'])) $item['group'] = $title; + $item['n'] = "$n/$result[total]"; + $items[$order] = $item; + $order++; + } + if($n && $n < $result['total'] && !$this->isViewAll) { + $url = isset($result['url']) ? $result['url'] : ''; + $items[$order] = $this->makeViewAllItem($liveSearch, $thisType, $item['group'], $result['total'], $url); + } + if($this->isViewAll && $this->pagination && $type) { + $this->pagination->setTotal($result['total']); + $this->pagination->setLimit($liveSearch['limit']); + $this->pagination->setStart($liveSearch['start']); + } + } + + if($type && !count($foundTypes) && !in_array($type, array('pages', 'trash', 'modules'))) { + if(empty($liveSearch['template']) && !count($foundTypes)) { + // if no types matched, and it’s going to skip pages, assume type is a property, and do a pages search + $liveSearch = $this->init(array( + 'q' => $liveSearch['q'], + 'type' => 'pages', + 'property' => $type, + 'operator' => $liveSearch['operator'] + )); + $type = 'pages'; + } + } + + if(empty($type) || $type === 'pages' || $type === 'trash' || $liveSearch['template']) { + // include pages in the search results + if(!in_array('pages', $this->searchTypesOrder)) $this->searchTypesOrder[] = 'pages'; + $order = array_search('pages', $this->searchTypesOrder) * 100; + foreach($this->findPages($liveSearch) as $item) { + $items[$order++] = $item; + } + } + + // use built-in modules search when appropriate + if((empty($type) || $type == 'modules') && $this->wire('user')->isSuperuser()) { + if(!in_array('modules', $this->searchTypesOrder)) $this->searchTypesOrder[] = 'modules'; + $order = array_search('modules', $this->searchTypesOrder) * 100; + foreach($this->findModules($liveSearch, $modulesInfo) as $item) { + $items[$order++] = $item; + } + } + + // add a debug item if requested to + if(!empty($liveSearch['debug'])) { + array_unshift($items, $this->makeDebugItem($liveSearch)); + } + + if($userLanguage) { + // restore original language to user + $user->language = $userLanguage; + } + + ksort($items); + + return $items; + } + + + /** + * Find pages for live search + * + * @param array $liveSearch + * @return array + * + */ + protected function findPages(array &$liveSearch) { + + $user = $this->wire('user'); + $superuser = $user->isSuperuser(); + $pages = $this->wire('pages'); + + // a $pages->find() search will be included in the live search + $selectors = &$liveSearch['selectors']; + $selectors[] = "start=$liveSearch[start], limit=$liveSearch[limit]"; + + if($this->process) { + $repeaterID = $this->process->getRepeatersPageID(); + if($repeaterID) $selectors[] = "has_parent!=$repeaterID"; + } + + if($superuser) { + // superuser only + $selectors[] = "include=all"; + } else if($user->hasPermission('page-edit')) { + // admin search mode and user has some kind of page-edit permission + $selectors[] = "include=unpublished"; + // $selectors[] = "template=$editableTemplates"; + // $selectors[] = "status<" . Page::statusTrash; + } else { + // only show regular, non-hidden, non-unpublished pages + } + + $selector = implode(', ', $selectors); + if($this->process) $selector = $this->process->findReady($selector); + + $titles = array(); + $items = array(); + $matches = array('pages' => array(), 'trash' => array()); + + try { + if(empty($liveSearch['type']) || $liveSearch['type'] == 'pages') { + $items['pages'] = $pages->find("$selector, status<" . Page::statusTrash); + } + } catch(\Exception $e) { + } + try { + if($superuser && (empty($liveSearch['type']) || $liveSearch['type'] == 'trash')) { + $items['trash'] = $pages->find("$selector, status>=" . Page::statusTrash); + } + } catch(\Exception $e) { + } + + foreach($items as $type => $pageItems) { + + $n = $liveSearch['start']; + $total = $pageItems->getTotal(); + $item = array(); + + foreach($pageItems as $page) { + /** @var Page $page */ + if(!$superuser && $page->isUnpublished() && !$page->editable()) continue; + $isAdmin = $page->template == 'admin'; + $title = (string) $page->get('title|name'); + + $item = array( + 'id' => $page->id, + 'name' => $page->name, + 'title' => $title, + 'subtitle' => $page->template->name, + 'summary' => $page->path, + 'url' => !$isAdmin && $page->editable() ? $page->editUrl(array('language' => true)) : $page->url(), + 'icon' => $page->getIcon(), + 'group' => '', + 'n' => (++$n) . '/' . $total, + 'modified' => $page->modified, + 'status' => $page->status, + ); + + if(!isset($titles[$title])) $titles[$title] = 0; + $titles[$title]++; + $item['group'] = $this->labels[$type]; + $matches[$type][] = $item; + } + + if(!empty($item) && $total > count($matches[$type])) { + $matches[$type][] = $this->makeViewAllItem($liveSearch, $type, $item['group'], $total, ''); + } + } + + // merge all the matches together + if(empty($matches['trash'])) { + $matches = $matches['pages']; + } else { + $matches = array_merge($matches['pages'], $matches['trash']); + } + + // if there any colliding titles, add modified date to the subtitle + foreach($titles as $title => $qty) { + if($qty < 2) continue; + foreach($matches as $key => $item) { + if($item['title'] !== $title) continue; + $matches[$key]['subtitle'] .= " (" . wireRelativeTimeStr($item['modified'], true) . ")"; + } + } + + return $matches; + } + + + /** + * Find modules matching query + * + * @param array $liveSearch + * @param array $modulesInfo + * @return array + * + */ + protected function findModules(array &$liveSearch, array &$modulesInfo) { + + $q = $liveSearch['q']; + $groupLabel = $this->labels['modules']; + $items = array(); + $forceMatch = false; + + if($liveSearch['type'] === 'modules' && !empty($liveSearch['property'])) { + // searching for custom module property + $forceMatch = true; + $infos = $this->wire('modules')->findByInfo( + $liveSearch['property'] . $liveSearch['operator'] . + $this->wire('sanitizer')->selectorValue($q), 2 + ); + } else { + // text-matching for all modules + $infos = &$modulesInfo; + } + + foreach($infos as $info) { + $name = $info['name']; + $title = $info['title']; + if(!$forceMatch) { + $searchText = "$name $title $info[summary]"; + if(stripos($searchText, $q) === false) continue; + } + $item = array( + 'id' => $info['id'], + 'name' => $name, + 'title' => $title, + 'subtitle' => $name, + 'summary' => $info['summary'], + 'url' => $this->wire('config')->urls->admin . "module/edit?name=$name", + 'group' => $groupLabel, + ); + $item = array_merge($this->itemTemplate, $item); + $items[] = $item; + } + + $total = count($items); + $n = 0; + foreach($items as $key => $item) { + $n++; + $items[$key]['n'] = "$n/$total"; + } + + return $items; + } + + /** + * Convert items from native live search format (v2) to v1 format + * + * v1 format is used by ProcessWire admin themes. + * + * @param array $items + * @return array + * + */ + protected function convertItemsFormat(array $items) { + + $converted = array(); + $sanitizer = $this->wire('sanitizer'); + + foreach($items as $item) { + $a = array( + 'id' => $item['id'], + 'name' => (string) $item['name'], + 'title' => (string) $item['title'], + 'template_label' => (string) $item['subtitle'], + 'tip' => (string) $item['summary'], + 'editUrl' => (string) $item['url'], + 'type' => (string) $sanitizer->entities($item['group']), + 'icon' => isset($item['icon']) ? $item['icon'] : '', + ); + + if(!empty($item['status'])) { + if($item['status'] & Page::statusUnpublished) $a['unpublished'] = true; + if($item['status'] & Page::statusHidden) $a['hidden'] = true; + if($item['status'] & Page::statusLocked) $a['locked'] = true; + } + + $converted[] = $a; + } + + return $converted; + } + + /** + * Make a search result item that displays debugging info + * + * @param array $liveSearch + * @return array + * + */ + protected function makeDebugItem($liveSearch) { + $liveSearch['user_language'] = $this->wire('user')->language->name; + $summary = print_r($liveSearch, true); + return array_merge($this->itemTemplate, array( + 'id' => 0, + 'name' => 'debug', + 'title' => implode(', ', $liveSearch['selectors']), + 'subtitle' => $liveSearch['q'], + 'summary' => $summary, + 'url' => '#', + 'group' => 'Debug', + )); + } + + /** + * Make a search result item that displays a “view all” link + * + * @param array $liveSearch + * @param string $type + * @param string $group + * @param int $total + * @param string $url If module provides its own view-all URL + * @return array + * + */ + protected function makeViewAllItem(&$liveSearch, $type, $group, $total, $url = '') { + + if(!empty($url)) { + // use provided url + } else if($type == 'pages' || $type == 'trash' || !empty($liveSearch['template'])) { + $url = $this->wire('page')->url(); + $url .= "?q=" . urlencode($liveSearch['q']) . "&live=1"; + if($type == 'trash') $url .= "&trash=1"; + if(!empty($liveSearch['template'])) { + $url .= "&template=" . $liveSearch['template']->name; + } + if(!empty($liveSearch['property'])) { + $url .= "&field=" . urlencode($liveSearch['property']); + } + if(!empty($liveSearch['operator'])) { + $url .= "&operator=" . urlencode($liveSearch['operator']); + } + } else { + $url = $this->wire('page')->url() . 'live/' . + '?q=' . urlencode($liveSearch['q']) . + '&type=' . urlencode($type) . + '&property=' . urlencode($liveSearch['property']) . + '&operator=' . urlencode($liveSearch['operator']); + } + + return array_merge($this->itemTemplate, array( + 'id' => 0, + 'name' => 'view-all', + 'title' => $this->labels['view-all'], + 'subtitle' => sprintf($this->_('%d items'), $total), + 'summary' => '', + 'url' => $url, + 'group' => $group, + )); + } + + /** + * Render “view all” list + * + * @param array $items + * @param string $prefix For CSS classes, default is "pw-search" + * @param string $class Class name for list, default is "list" which translates to "pw-search-list" + * @return string HTML markup + * + */ + protected function ___renderList(array $items, $prefix = 'pw-search', $class = 'list') { + + $pagination = $this->pagination->renderPager(); + $a = array(); + + $out = "\n
        " . $pagination; + + foreach($items as $item) { + $a[] = $this->renderItem($item, $prefix); + } + + $out .= implode('
        ', $a) . $pagination . "\n
        "; + + return $out; + } + + /** + * Render an item for the “view all” list + * + * @param array $item + * @param string $prefix For CSS classes, default is "pw-search" + * @param string $class Class name for item, default is "item" which translates to "pw-search-item" + * @return string HTML markup + * + */ + protected function ___renderItem(array $item, $prefix = 'pw-search', $class = 'item') { + + /** @var Sanitizer $sanitizer */ + $sanitizer = $this->wire('sanitizer'); + + foreach(array('title', 'subtitle', 'summary', 'url') as $key) { + if(isset($item[$key])) { + $item[$key] = $sanitizer->entities($item[$key]); + } else { + $item[$key] = ''; + } + } + + $title = "$item[title] "; + $subtitle = empty($item['subtitle']) ? '' : "
        $item[subtitle] "; + $summary = empty($item['summary']) ? '' : "
        $item[summary] "; + + return "\n\t

        $title $subtitle $summary

        "; + } + +} \ No newline at end of file