/** * ProcessWire Page Auto Completion select widget * * This Inputfield connects the jQuery UI Autocomplete widget with the ProcessWire ProcessPageSearch AJAX API. * * ProcessWire 3.x (development), Copyright 2023 by Ryan Cramer * https://processwire.com * */ var InputfieldPageAutocomplete = { /** * Initialize the given InputfieldPageListSelectMultiple OL by making it sortable * */ init: function(id, url, labelField, searchField, operator) { var $value = $('#' + id); var $ol = $('#' + id + '_items'); var $input = $('#' + id + '_input'); var $icon = $input.parent().find(".InputfieldPageAutocompleteStatus"); var $note = $input.parent().find(".InputfieldPageAutocompleteNote"); var numAdded = 0; // counter that keeps track of quantity items added var numFound = 0; // indicating number of pages matching during last ajax request var disableChars = $input.attr('data-disablechars'); var noList = $input.hasClass('no_list'); function hasDisableChar(str) { if(!disableChars || !disableChars.length) return false; var disable = false; for(var n = 0; n < disableChars.length; n++) { if(str.indexOf(disableChars[n]) > -1) { disable = true; break; } } return disable; } InputfieldPageAutocomplete.setIconPosition($icon, 'left'); if(noList) { // specific to single-item autocompletes, where there is no separate "selected" list $input.attr('data-selectedLabel', $input.val()); var $remove = $input.siblings('.InputfieldPageAutocompleteRemove'); InputfieldPageAutocomplete.setIconPosition($remove, 'right'); $remove.on('click', function() { $value.val('').trigger('change'); $input.val('').attr('placeholder', '').attr('data-selectedLabel', '').trigger('change').trigger('focus'); $input.trigger('keydown'); }); $input.on('change', function() { if($(this).val().length == 0) { $remove.hide(); } else { $remove.show(); } }); $input.on('focus', function() { var val = $value.val(); if(!val.length) return; if(hasDisableChar(val)) return; if($(this).hasClass('added_item')) return; $(this).attr('placeholder', $(this).attr('data-selectedLabel')); $(this).val(''); }).on('blur', function() { setTimeout(function() { }, 200); }); } $icon.on('click', function() { $input.trigger('focus'); }); $icon.attr('data-class', $icon.attr('class')); function isAddAllowed() { var allowed = $('#_' + id.replace('Inputfield_', '') + '_add_items').length > 0; return allowed; } $input.one('focus', function() { InputfieldPageAutocomplete.updateIcons($input.closest('.InputfieldContent')); $input.autocomplete({ minLength: 2, source: function(request, response) { var term = request.term; if(hasDisableChar(term)) { response([]); return; } $icon.attr('class', 'fa fa-fw fa-spin fa-spinner'); if($input.hasClass('and_words') && term.indexOf(' ') > 0) { // AND words mode term = term.replace(/\s+/, ','); } term = encodeURIComponent(term); var ajaxURL = url + '&' + searchField + operator + term; $.getJSON(ajaxURL, function(data) { $icon.attr('class', $icon.attr('data-class')); numFound = data.total; if(data.total > 0) { $icon.attr('class', 'fa fa-fw fa-angle-double-down'); } else if(isAddAllowed()) { $icon.attr('class', 'fa fa-fw fa-plus-circle'); $note.show(); } else { $icon.attr('class', 'fa fa-fw fa-frown-o'); } response($.map(data.matches, function(item) { return { label: item[labelField], value: item[labelField], page_id: item.id } })); }); }, select: function(event, ui) { if(!ui.item) return; var $t = $(this); if($t.hasClass('no_list')) { $t.val(ui.item.label).trigger('change'); $t.attr('data-selectedLabel', ui.item.label); $t.closest('.InputfieldPageAutocomplete') .find('.InputfieldPageAutocompleteData').val(ui.item.page_id).trigger('change'); $t.trigger('blur'); } else { InputfieldPageAutocomplete.pageSelected($ol, ui.item); $t.val('').trigger('focus'); } event.stopPropagation(); return false; }, open: function(event, ui) { var $items = $('.ui-autocomplete.ui-front'); if(!$items.find('a').length) { // newer jQuery UI versions use
rather than , but we prefer to keep $items.find('div').each(function() { $(this).parent().html('' + $(this).html() + ''); }); } } }).on('blur', function() { var $input = $(this); //if(!$input.val().length) $input.val(''); $icon.attr('class', $icon.attr('data-class')); $note.hide(); if($input.hasClass('no_list')) { if($value.val().length || $input.val().length) { if($input.hasClass('allow_any') || $input.hasClass('added_item')) { // allow value to remain } else { $input.val($input.attr('data-selectedLabel')).attr('placeholder', ''); } } else { $input.val('').attr('placeholder', '').attr('data-selectedLabel', ''); } } if($input.hasClass('focus-after-blur')) { $input.removeClass('focus-after-blur'); setTimeout(function() { $input.trigger('focus'); }, 250); } }).on('keyup', function() { $icon.attr('class', $icon.attr('data-class')); }).on('keydown', function(event) { var $addNote; if(event.keyCode == 13) { // prevents enter from submitting the form event.preventDefault(); // instead we add the text entered as a new item // if there is an .InputfieldPageAdd sibling, which indicates support for this if(isAddAllowed()) { if($input.val().trim().length < 1) { $input.trigger('blur'); return false; } numAdded++; // new items have a negative page_id var page = { page_id: (-1 * numAdded), label: $input.val() }; // add it to the list if(noList) { // adding new item while using input as the label $value.val(page.page_id); $("#_" + id.replace('Inputfield_', '') + '_add_items').val(page.label); $input.addClass('added_item').trigger('blur'); $addNote = $note.siblings(".InputfieldPageAutocompleteNoteAdd"); if(!$addNote.length) { $addNote = $("
"); $note.after($addNote); } $addNote.text($note.attr('data-adding') + ' ' + page.label); $addNote.show(); } else { // adding new item to list InputfieldPageAutocomplete.pageSelected($ol, page); $input.val('').trigger('blur').trigger('focus'); } $note.hide(); } else { $(this).addClass('focus-after-blur').trigger('blur'); } return false; } if(numAdded && noList) { // some other key after an item already added, so remove added item info for potential new one $addNote = $note.siblings(".InputfieldPageAutocompleteNoteAdd"); var $addText = $("#_" + id.replace('Inputfield_', '') + '_add_items'); if($addNote.length && $addText.val() != $(this).val()) { // added value has changed $addNote.remove(); $value.val(''); $addText.val(''); $("#_" + id.replace('Inputfield_', '') + '_add_items').val(''); numAdded--; } } }); }); var makeSortable = function($ol) { $ol.sortable({ // items: '.InputfieldPageListSelectMultiple ol > li', axis: 'y', update: function(e, data) { InputfieldPageAutocomplete.rebuildInput($(this)); $ol.trigger('sorted', [ data.item ]); }, start: function(e, data) { data.item.addClass('ui-state-highlight'); }, stop: function(e, data) { data.item.removeClass('ui-state-highlight'); } }); $ol.addClass('InputfieldPageAutocompleteSortable'); }; $('#' + $ol.attr('id')).on('mouseover', '>li', function() { $(this).removeClass('ui-state-default').addClass('ui-state-hover'); makeSortable($ol); }).on('mouseout', '>li', function() { $(this).removeClass('ui-state-hover').addClass('ui-state-default'); }); }, /** * Same as init() but only requires the Inputfield where autocomplete lives * * @param $inputfield * */ initFromInputfield: function($inputfield) { var $a = $inputfield.find(".InputfieldPageAutocompleteData"); if(!$a.length) return; if($a.hasClass('InputfieldPageAutocompleteInit')) return; InputfieldPageAutocomplete.init( $a.attr('id'), $a.attr('data-url'), $a.attr('data-label'), $a.attr('data-search'), $a.attr('data-operator') ); $a.addClass('InputfieldPageAutocompleteInit'); }, /** * Set position of icon within parent element * * @param $icon * @param side Either 'left' or 'right' * */ setIconPosition: function($icon, side) { if($icon.hasClass('PageAutocompleteIconHidden')) { $icon.removeClass('PageAutocompleteIconHidden').show(); } var iconHeight = $icon.height(); if(iconHeight) { var pHeight = $icon.parent().height(); var iconTop = ((pHeight - iconHeight) / 2); $icon.css('top', iconTop + 'px'); if(side == 'left') { $icon.css('left', (iconTop / 2) + 'px'); } else if(side == 'right') { $icon.css('right', (iconTop / 4) + 'px'); } } else { // icon is not visible (in a tab or collapsed field), we'll leave it alone $icon.hide().addClass('PageAutocompleteIconHidden'); } }, /** * Callback function executed when a page is selected from PageList * */ pageSelected: function($ol, page) { var dup = false; $ol.children('li:not(.itemTemplate)').each(function() { var v = parseInt($(this).children('.itemValue').text()); if(v == page.page_id) dup = $(this); }); var $inputText = $('#' + $ol.attr('data-id') + '_input'); $inputText.trigger('blur'); if(dup) { dup.effect('highlight'); return; } var $li = $ol.children(".itemTemplate").clone(); $li.removeClass("itemTemplate"); $li.children('.itemValue').text(page.page_id); $li.children('.itemLabel').text(page.label); $ol.append($li); InputfieldPageAutocomplete.rebuildInput($ol); InputfieldPageAutocomplete.triggerChange($ol); }, /** * Trigger change event * * @param $item Any element within the autocomplete Inputfield * */ triggerChange: function($item) { var $input; if($item.hasClass('InputfieldPageAutocompleteData')) { $input = $item; } else { if(!$item.hasClass('Inputfield')) $item = $item.closest('.Inputfield'); $input = $item.find('.InputfieldPageAutocompleteData') } $input.trigger('change'); }, /** * Rebuild the CSV values present in the hidden input[text] field * */ rebuildInput: function($ol) { var id = $ol.attr('data-id'); var name = $ol.attr('data-name'); //id = id.substring(0, id.lastIndexOf('_')); var $input = $('#' + id); var value = ''; var addValue = ''; var max = parseInt($input.attr('data-max')); var $children = $ol.children(':not(.itemTemplate)'); if(max > 0 && $children.length > max) { while($children.length > max) $children = $children.slice(1); $ol.children(':not(.itemTemplate)').replaceWith($children); } $children.each(function() { var v = parseInt($(this).children('.itemValue').text()); if(v > 0) { value += ',' + v; } else if(v < 0) { value += ',' + v; addValue += $(this).children('.itemLabel').text() + "\n"; } }); $input.val(value); var $addItems = $('#_' + name + '_add_items'); if($addItems.length > 0) $addItems.val(addValue); }, updateIcons: function($target) { // update positions of icons that previously were not calculable var $icons = $target.find('.InputfieldPageAutocompleteStatus'); $icons.each(function() { InputfieldPageAutocomplete.setIconPosition($(this), 'left'); }); $icons = $target.find('.InputfieldPageAutocompleteRemove'); $icons.each(function() { InputfieldPageAutocomplete.setIconPosition($(this), 'right'); }); } }; $(document).ready(function() { $(".InputfieldPageAutocomplete").each(function() { InputfieldPageAutocomplete.initFromInputfield($(this)); }); $(document).on('reloaded', '.InputfieldPageAutocomplete, .InputfieldPage', function() { InputfieldPageAutocomplete.initFromInputfield($(this)); }); $(document).on('click', '.InputfieldPageAutocomplete ol a.itemRemove', function() { // $(".InputfieldPageAutocomplete ol").on('click', 'a.itemRemove', function() { var $li = $(this).parent(); var $ol = $li.parent(); var id = $li.children(".itemValue").text(); $li.remove(); InputfieldPageAutocomplete.rebuildInput($ol); InputfieldPageAutocomplete.triggerChange($ol); return false; }); $(document).on('wiretabclick', function(a, $tab) { // update positions of icons that previously were not calculable InputfieldPageAutocomplete.updateIcons($tab); }); });