/** * PageForms.js * * Javascript utility functions for the Page Forms extension. * * @author Yaron Koren * @author Sanyam Goyal * @author Stephan Gambke * @author Jeffrey Stuckman * @author Harold Solbrig * @author Eugene Mednikov */ /*global wgPageFormsShowOnSelect, wgPageFormsFieldProperties, wgPageFormsCargoFields, wgPageFormsDependentFields, validateAll, alert, mwTinyMCEInit, pf*/ // Activate autocomplete functionality for the specified field ( function ( $, mw ) { /* extending jQuery functions for custom highlighting */ $.ui.autocomplete.prototype._renderItem = function( ul, item) { var delim = this.element[0].delimiter; var term; if ( delim === null ) { term = this.term; } else { term = this.term.split( delim ).pop(); } var re = new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"); // HTML-encode the value's label. var itemLabel = $('
').text(item.label).html(); var loc = itemLabel.search(re); var t; if (loc >= 0) { t = itemLabel.substr(0, loc) + '' + itemLabel.substr(loc, term.length) + '' + itemLabel.substr(loc + term.length); } else { t = itemLabel; } return $( "" ) .data( "item.autocomplete", item ) .append( " " + t + "" ) .appendTo( ul ); }; $.fn.attachAutocomplete = function() { return this.each(function() { // Get all the necessary values from the input's "autocompletesettings" // attribute. This should probably be done as three separate attributes, // instead. var field_string = $(this).attr("autocompletesettings"); if ( typeof field_string === 'undefined' ) { return; } var field_values = field_string.split(','); var delimiter = null; var data_source = field_values[0]; if (field_values[1] === 'list') { delimiter = ","; if (field_values[2] !== null && field_values[2] !== '' && field_values[2] !== undefined) { delimiter = field_values[2]; } } // Modify the delimiter. If it's "\n", change it to an actual // newline - otherwise, add a space to the end. // This doesn't cover the case of a delimiter that's a newline // plus something else, like ".\n" or "\n\n", but as far as we // know no one has yet needed that. if ( delimiter !== null && delimiter !== '' && delimiter !== undefined ) { if ( delimiter === "\\n" ) { delimiter = "\n"; } else { delimiter += " "; } } // Store this value within the object, so that it can be used // during highlighting of the search term as well. this.delimiter = delimiter; /* extending jQuery functions */ $.extend( $.ui.autocomplete, { filter: function(array, term) { var wgPageFormsAutocompleteOnAllChars = mw.config.get( 'wgPageFormsAutocompleteOnAllChars' ); var matcher; if ( wgPageFormsAutocompleteOnAllChars ) { matcher = new RegExp($.ui.autocomplete.escapeRegex(term), "i" ); } else { matcher = new RegExp("\\b" + $.ui.autocomplete.escapeRegex(term), "i" ); } // This may be an associative array instead of a // regular one - grep() requires a regular one. // (Is this "if" check necessary, or useful?) if ( typeof array === 'object' ) { // Unfortunately, Object.values() is // not supported on all browsers. array = Object.keys(array).map(function(key) { return array[key]; }); } return $.grep( array, function(value) { return matcher.test( value.label || value.value || value ); }); } } ); var values = $(this).data('autocompletevalues'); if ( !values ) { var wgPageFormsAutocompleteValues = mw.config.get( 'wgPageFormsAutocompleteValues' ); values = wgPageFormsAutocompleteValues[field_string]; } var split = function (val) { return val.split(delimiter); }; var extractLast = function (term) { return split(term).pop(); }; if (values !== null && values !== undefined) { // Local autocompletion if (delimiter !== null && delimiter !== undefined) { // Autocomplete for multiple values var thisInput = $(this); $(this).autocomplete({ minLength: 0, source: function(request, response) { // We need to re-get the set of values, since // the "values" variable gets overwritten. values = thisInput.data( 'autocompletevalues' ); if ( !values ) { values = wgPageFormsAutocompleteValues[field_string]; } response($.ui.autocomplete.filter(values, extractLast(request.term))); }, focus: function() { // prevent value inserted on focus return false; }, select: function(event, ui) { var terms = split( this.value ); // remove the current input terms.pop(); // add the selected item terms.push( ui.item.value ); // add placeholder to get the comma-and-space at the end terms.push(""); this.value = terms.join(delimiter); return false; } }); } else { // Autocomplete for a single value $(this).autocomplete({ // Unfortunately, Object.values() is // not supported on all browsers. source: ( typeof values === 'object' ) ? Object.keys(values).map(function(key) { return values[key]; }) : values }); } } else { // Remote autocompletion. var myServer = mw.util.wikiScript( 'api' ); var autocomplete_type = $(this).attr("autocompletedatatype"); if ( autocomplete_type === 'cargo field' ) { var table_and_field = data_source.split('|'); myServer += "?action=pfautocomplete&format=json&cargo_table=" + table_and_field[0] + "&cargo_field=" + table_and_field[1]; } else { myServer += "?action=pfautocomplete&format=json&" + autocomplete_type + "=" + data_source; } if (delimiter !== null && delimiter !== undefined) { $(this).autocomplete({ source: function(request, response) { $.getJSON(myServer, { substr: extractLast(request.term) }, function( data ) { response($.map(data.pfautocomplete, function(item) { return { value: item.title }; })); }); }, search: function() { // custom minLength var term = extractLast(this.value); if (term.length < 1) { return false; } }, focus: function() { // prevent value inserted on focus return false; }, select: function(event, ui) { var terms = split( this.value ); // remove the current input terms.pop(); // add the selected item terms.push( ui.item.value ); // add placeholder to get the comma-and-space at the end terms.push(""); this.value = terms.join(delimiter); return false; } } ); } else { $(this).autocomplete({ minLength: 1, source: function(request, response) { $.ajax({ url: myServer, dataType: "json", data: { substr:request.term }, success: function( data ) { response($.map(data.pfautocomplete, function(item) { return { value: item.title }; })); } }); }, open: function() { $(this).removeClass("ui-corner-all").addClass("ui-corner-top"); }, close: function() { $(this).removeClass("ui-corner-top").addClass("ui-corner-all"); } } ); } } }); }; /* * Functions to register/unregister methods for the initialization and * validation of inputs. */ // Initialize data object to hold initialization and validation data function setupPF() { $("#pfForm").data("PageForms",{ initFunctions : [], validationFunctions : [] }); } // Register a validation method // // More than one method may be registered for one input by subsequent calls to // PageForms_registerInputValidation. // // Validation functions and their data are stored in a numbered array // // @param valfunction The validation functions. Must take a string (the input's id) and an object as parameters // @param param The parameter object given to the validation function $.fn.PageForms_registerInputValidation = function(valfunction, param) { if ( ! this.attr("id") ) { return this; } if ( ! $("#pfForm").data("PageForms") ) { setupPF(); } $("#pfForm").data("PageForms").validationFunctions.push({ input : this.attr("id"), valfunction : valfunction, parameters : param }); return this; }; // Register an initialization method // // More than one method may be registered for one input by subsequent calls to // PageForms_registerInputInit. This method also executes the initFunction // if the element referenced by /this/ is not part of a multipleTemplateStarter. // // Initialization functions and their data are stored in a associative array // // @param initFunction The initialization function. Must take a string (the input's id) and an object as parameters // @param param The parameter object given to the initialization function // @param noexecute If set, the initialization method will not be executed here $.fn.PageForms_registerInputInit = function( initFunction, param, noexecute ) { // return if element has no id if ( ! this.attr("id") ) { return this; } // setup data structure if necessary if ( ! $("#pfForm").data("PageForms") ) { setupPF(); } // if no initialization function for this input was registered yet, // create entry if ( ! $("#pfForm").data("PageForms").initFunctions[this.attr("id")] ) { $("#pfForm").data("PageForms").initFunctions[this.attr("id")] = []; } // record initialization function $("#pfForm").data("PageForms").initFunctions[this.attr("id")].push({ initFunction : initFunction, parameters : param }); // execute initialization if input is not part of multipleTemplateStarter // and if not forbidden if ( this.closest(".multipleTemplateStarter").length === 0 && !noexecute) { var input = this; // ensure initFunction is only exectued after doc structure is complete $(function() {initFunction ( input.attr("id"), param );}); } return this; }; // Unregister all validation methods for the element referenced by /this/ $.fn.PageForms_unregisterInputValidation = function() { var pfdata = $("#pfForm").data("PageForms"); if ( this.attr("id") && pfdata ) { // delete every validation method for this input for ( var i = 0; i < pfdata.validationFunctions.length; i++ ) { if ( typeof pfdata.validationFunctions[i] !== 'undefined' && pfdata.validationFunctions[i].input === this.attr("id") ) { delete pfdata.validationFunctions[i]; } } } return this; }; // Unregister all initialization methods for the element referenced by /this/ $.fn.PageForms_unregisterInputInit = function() { if ( this.attr("id") && $("#pfForm").data("PageForms") ) { delete $("#pfForm").data("PageForms").initFunctions[this.attr("id")]; } return this; }; /* * Functions for handling 'show on select' */ // Display a div that would otherwise be hidden by "show on select". function showDiv( div_id, instanceWrapperDiv, initPage ) { var speed = initPage ? 0 : 'fast'; var elem; if ( instanceWrapperDiv !== null ) { elem = $('[data-origID="' + div_id + '"]', instanceWrapperDiv); } else { elem = $('#' + div_id); } elem .addClass('shownByPF') .find(".hiddenByPF") .removeClass('hiddenByPF') .addClass('shownByPF') .find(".disabledByPF") .removeAttr('disabled') .removeClass('disabledByPF'); elem.each( function() { if ( $(this).css('display') === 'none' ) { $(this).slideDown(speed, function() { $(this).fadeTo(speed,1); }); } }); // Now re-show any form elements that are meant to be shown due // to the current value of form inputs in this div that are now // being uncovered. var wgPageFormsShowOnSelect = mw.config.get( 'wgPageFormsShowOnSelect' ); elem.find(".pfShowIfSelected, .pfShowIfChecked").each( function() { var uncoveredInput = $(this); var uncoveredInputID = null; if ( instanceWrapperDiv === null ) { uncoveredInputID = uncoveredInput.attr("id"); } else { uncoveredInputID = uncoveredInput.attr("data-origID"); } var showOnSelectVals = wgPageFormsShowOnSelect[uncoveredInputID]; if ( showOnSelectVals !== undefined ) { var inputVal = uncoveredInput.val(); for ( var i = 0; i < showOnSelectVals.length; i++ ) { var options = showOnSelectVals[i][0]; var div_id2 = showOnSelectVals[i][1]; if ( uncoveredInput.hasClass( 'pfShowIfSelected' ) ) { showDivIfSelected( options, div_id2, inputVal, instanceWrapperDiv, initPage ); } else { uncoveredInput.showDivIfChecked( options, div_id2, instanceWrapperDiv, initPage ); } } } }); } // Hide a div due to "show on select". The CSS class is there so that PF can // ignore the div's contents when the form is submitted. function hideDiv( div_id, instanceWrapperDiv, initPage ) { var speed = initPage ? 0 : 'fast'; var elem; // IDs can't contain spaces, and jQuery won't work with such IDs - if // this one has a space, display an alert. if ( div_id.indexOf( ' ' ) > -1 ) { // TODO - this should probably be a language value, instead of // hardcoded in English. alert( "Warning: this form has \"show on select\" pointing to an invalid element ID (\"" + div_id + "\") - IDs in HTML cannot contain spaces." ); } if ( instanceWrapperDiv !== null ) { elem = instanceWrapperDiv.find('[data-origID=' + div_id + ']'); } else { elem = $('#' + div_id); } // If we're just setting up the page, and this element has already // been marked to be shown by some other input, don't hide it. if ( initPage && elem.hasClass('shownByPF') ) { return; } elem.find("span, div").addClass('hiddenByPF'); elem.each( function() { if ( $(this).css('display') !== 'none' ) { // if 'display' is not 'hidden', but the element is hidden otherwise // (e.g. by having height = 0), just hide it, else animate the hiding if ( $(this).is(':hidden') ) { $(this).hide(); } else { $(this).fadeTo(speed, 0, function() { $(this).slideUp(speed); }); } } }); // Also, recursively hide further elements that are only shown because // inputs within this now-hidden div were checked/selected. var wgPageFormsShowOnSelect = mw.config.get( 'wgPageFormsShowOnSelect' ); elem.find(".pfShowIfSelected, .pfShowIfChecked").each( function() { var showOnSelectVals; if ( instanceWrapperDiv === null ) { showOnSelectVals = wgPageFormsShowOnSelect[$(this).attr("id")]; } else { showOnSelectVals = wgPageFormsShowOnSelect[$(this).attr("data-origID")]; } if ( showOnSelectVals !== undefined ) { for ( var i = 0; i < showOnSelectVals.length; i++ ) { //var options = showOnSelectVals[i][0]; var div_id2 = showOnSelectVals[i][1]; hideDiv( div_id2, instanceWrapperDiv, initPage ); } } }); } // Show this div if the current value is any of the relevant options - // otherwise, hide it. function showDivIfSelected(options, div_id, inputVal, instanceWrapperDiv, initPage) { for ( var i = 0; i < options.length; i++ ) { // If it's a listbox and the user has selected more than one // value, it'll be an array - handle either case. if (($.isArray(inputVal) && $.inArray(options[i], inputVal) >= 0) || (!$.isArray(inputVal) && (inputVal === options[i]))) { showDiv( div_id, instanceWrapperDiv, initPage ); return; } } hideDiv( div_id, instanceWrapperDiv, initPage ); } // Used for handling 'show on select' for the 'dropdown' and 'listbox' inputs. $.fn.showIfSelected = function(partOfMultiple, initPage) { var inputVal = this.val(), wgPageFormsShowOnSelect = mw.config.get( 'wgPageFormsShowOnSelect' ), showOnSelectVals, instanceWrapperDiv; if ( partOfMultiple ) { showOnSelectVals = wgPageFormsShowOnSelect[this.attr("data-origID")]; instanceWrapperDiv = this.closest('.multipleTemplateInstance'); } else { showOnSelectVals = wgPageFormsShowOnSelect[this.attr("id")]; instanceWrapperDiv = null; } if ( showOnSelectVals !== undefined ) { for ( var i = 0; i < showOnSelectVals.length; i++ ) { var options = showOnSelectVals[i][0]; var div_id = showOnSelectVals[i][1]; showDivIfSelected( options, div_id, inputVal, instanceWrapperDiv, initPage ); } } return this; }; // Show this div if any of the relevant selections are checked - // otherwise, hide it. $.fn.showDivIfChecked = function(options, div_id, instanceWrapperDiv, initPage ) { for ( var i = 0; i < options.length; i++ ) { if ($(this).find('[value="' + options[i] + '"]').is(":checked")) { showDiv( div_id, instanceWrapperDiv, initPage ); return this; } } hideDiv( div_id, instanceWrapperDiv, initPage ); return this; }; // Used for handling 'show on select' for the 'checkboxes' and 'radiobutton' // inputs. $.fn.showIfChecked = function(partOfMultiple, initPage) { var wgPageFormsShowOnSelect = mw.config.get( 'wgPageFormsShowOnSelect' ), showOnSelectVals, instanceWrapperDiv, i; if ( partOfMultiple ) { showOnSelectVals = wgPageFormsShowOnSelect[this.attr("data-origID")]; instanceWrapperDiv = this.closest('.multipleTemplateInstance'); } else { showOnSelectVals = wgPageFormsShowOnSelect[this.attr("id")]; instanceWrapperDiv = null; } if ( showOnSelectVals !== undefined ) { for ( i = 0; i < showOnSelectVals.length; i++ ) { var options = showOnSelectVals[i][0]; var div_id = showOnSelectVals[i][1]; this.showDivIfChecked( options, div_id, instanceWrapperDiv, initPage ); } } return this; }; // Used for handling 'show on select' for the 'checkbox' input. $.fn.showIfCheckedCheckbox = function( partOfMultiple, initPage ) { var wgPageFormsShowOnSelect = mw.config.get( 'wgPageFormsShowOnSelect' ), divIDs, instanceWrapperDiv, i; if (partOfMultiple) { divIDs = wgPageFormsShowOnSelect[this.attr("data-origID")]; instanceWrapperDiv = this.closest(".multipleTemplateInstance"); } else { divIDs = wgPageFormsShowOnSelect[this.attr("id")]; instanceWrapperDiv = null; } for ( i = 0; i < divIDs.length; i++ ) { var divID = divIDs[i]; if ($(this).is(":checked")) { showDiv( divID, instanceWrapperDiv, initPage ); } else { hideDiv( divID, instanceWrapperDiv, initPage ); } } return this; }; /* * Validation functions */ // Set the error message for an input. $.fn.setErrorMessage = function(msg, val) { var container = this.find('.pfErrorMessages'); container.html($('or. // Code copied, more or less, from PFTemplateInForm::escapeNonTemplatePipes(). var startAndEndTags = [ [ ' ' ], [ '' ], [ '