/************************************************************* * LITWEngine.js.php * * Contains functions for creating standard experiment content. * See www.labinthewild.org/documentation for documentation. * * Dependencies: jQuery * Autocomplete functionality also requires jQueryUI * * Author: Trevor Croxson, based on original code * by Yuechen Zhao, Jun Sup Lee, Norman Zhu, * Jonathan Taratuta-Titus, Nancy Chen, and Samuel Anthony * * Last Modified: November 10, 2015 * * © Copyright 2015 LabintheWild * For questions about this file and permission to use * the code, contact us at info@labinthewild.org *************************************************************/ // TODOs: // protect overwriting of hidden args by testing for leading underscore // demographics footer stuff // ensure English-language data gets recorded in the database // numerical text check when fields are not required questions // revealing module pattern! var LITWEngine = (function() { var version = "0.1", ///////////////////////////////////////////////// // // // Default properties // // // ///////////////////////////////////////////////// properties = { global: { _participantId: 0, _participantCountry: "", _participantCity: "", dataScriptLoc: "../common/include/LITWdata.php", _progression: 1, progressionDescription: "IRB", trackDropouts: true, _aggregateStatistics: {}, _slides: null, _trialNum: 0, _cumulativeTrialNum: 0, _phaseNum: 0, loadingTime: 1500, _results: [], brandHeader: true, _currentSlide: "" }, widgets: { generic: { }, nextButton: { loadingOverride: false, iconPath: "../common/img/next.png", iconActivePath: "../common/img/next_active.png" } }, forms: { generic: { name: undefined, parentElement: undefined, tableName: undefined, stylingClass: "defaultForm", requiredQuestions: [], requiredColor: "#FF0000", headerMessage: "Please fill out the form below:", autocomplete: false, stylingClass: "defaultForm", whyPrompt: "Why?", whyMessage: "The information you provide will help us analyze your results.", requiredMessage: "Fields marked with a * are required.", requiredMessageAll: "Please answer all questions below.", _activeQuestions: [], _activeQuestionTypes: {} }, demographics: { name: "demographics", parentElement: "demographics", tableName: "demographics", stylingClass: "defaultDemoForm", cookieAsk: true, whyCookie: true, requiredQuestions: ["retake", "gender", "age", "country0", "years0", "lang0"], headerMessage: "Please tell us a bit about yourself.", whyMessage: "We need this information for data analysis. Please note that none of the answers are personally identifiable. LabintheWild takes your privacy very seriously. You may email us at info@labinthewild.org for more information." }, comments: { name: "comments", parentElement: "comments", tableName: "comments" } }, formElements: { generic: { name: undefined, options: null, prompt: undefined, style: "dropdown", optionsAsNumbers: false, expansionHeader: "Please add additional information below.", expansionPrompt: ["Next item:"], expandPrompt: "[Add Another]", contractPrompt: "[Delete]", _expansionCount: 0, maxExpansions: 6, _expanded: false, hidden: false }, retake: { name: "retake", style: "dropdown", options: "yesNo", prompt: "Have you taken this test before?", optionsAsNumbers: true }, gender: { name: "gender", style: "dropdown", options: "gender", prompt: "What is your gender?", optionsAsNumbers: true }, multinational: { name: "multinational", style: "dropdown", options: "yesNo", prompt: "Have you lived in more than one country?", expand: "country0", expansionTrigger: "1", optionsAsNumbers: true }, country0: { name: "country0", style: "dropdown", options: "countries", prompt: "Which was the first country where you lived?", expansionHeader: "Please add countries in order from the country you were born in to the most recent.", expansionPrompt: ["First Country:", "Next Country:"], expandPrompt: "[Add Another Country]", contractPrompt: "[Delete]" }, age: { name: "age", style: "dropdown", //valueList: [4, 5, 87, 3], //<-- this is checked first; if it doesn't exist, max and min values are checked minValue: 5, maxValue: 99, //stepValue: 5, //<-- defaults to 1 if not set options: "numericList", prompt: "How old are you?", invalidMessage: "Please enter a number between 5 and 99" }, education: { name: "education", style: "dropdown", options: "education", prompt: "What is the highest level of education you have received or are pursuing?", }, cheating: { name: "cheating", style: "radio", options: "yesNo", expands: "cheatingComments", expansionTrigger: "Yes", }, cheatingComments: { style: "freeText", hidden: true, placeholderText: "You may leave this area blank.", maxExpansions: 1 }, technicalDifficulties: { type: "radio", expandable: true, expansionLinkedTo: "technicalComments", expansionTrigger: "Yes", options: ["Yes", "No"] }, technicalComments: { style: "freeText", defaultValue: "You may leave this area blank." }, generalComments: { style: "freeText", defaultValue: "You may leave this area blank." } }, formOptions: { yesNo: { "No": "No", "Yes": "Yes", }, countries: { "Afghanistan": "Afghanistan", "Aland Islands": "Aland Islands", "Albania": "Albania", "Algeria": "Algeria", "American Samoa": "American Samoa", "Andorra": "Andorra", "Angola": "Angola", "Anguilla": "Anguilla", "Antarctica": "Antarctica", "Antigua And Barbuda": "Antigua And Barbuda", "Argentina": "Argentina", "Armenia": "Armenia", "Aruba": "Aruba", "Australia": "Australia", "Austria": "Austria", "Azerbaijan": "Azerbaijan", "Bahamas": "Bahamas", "Bahrain": "Bahrain", "Bangladesh": "Bangladesh", "Barbados": "Barbados", "Belarus": "Belarus", "Belgium": "Belgium", "Belize": "Belize", "Benin": "Benin", "Bermuda": "Bermuda", "Bhutan": "Bhutan", "Bolivia": "Bolivia", "Bosnia And Herzegovina": "Bosnia And Herzegovina", "Botswana": "Botswana", "Bouvet Island": "Bouvet Island", "Brazil": "Brazil", "British Indian Ocean Territory": "British Indian Ocean Territory", "Brunei Darussalam": "Brunei Darussalam", "Bulgaria": "Bulgaria", "Burkina Faso": "Burkina Faso", "Burundi": "Burundi", "Cambodia": "Cambodia", "Cameroon": "Cameroon", "Canada": "Canada", "Cape Verde": "Cape Verde", "Cayman Islands": "Cayman Islands", "Central African Republic": "Central African Republic", "Chad": "Chad", "Chile": "Chile", "China": "China", "Christmas Island": "Christmas Island", "Cocos (Keeling) Islands": "Cocos (Keeling) Islands", "Colombia": "Colombia", "Comoros": "Comoros", "Congo": "Congo", "The Democratic Republic Of The Congo": "The Democratic Republic Of The Congo", "Cook Islands": "Cook Islands", "Costa Rica": "Costa Rica", "Cote Divoire": "Cote Divoire", "Croatia": "Croatia", "Cuba": "Cuba", "Cyprus": "Cyprus", "Czech Republic": "Czech Republic", "Denmark": "Denmark", "Djibouti": "Djibouti", "Dominica": "Dominica", "Dominican Republic": "Dominican Republic", "Ecuador": "Ecuador", "Egypt": "Egypt", "El Salvador": "El Salvador", "Equatorial Guinea": "Equatorial Guinea", "Eritrea": "Eritrea", "Estonia": "Estonia", "Ethiopia": "Ethiopia", "Falkland Islands (Malvinas)": "Falkland Islands (Malvinas)", "Faroe Islands": "Faroe Islands", "Fiji": "Fiji", "Finland": "Finland", "France": "France", "French Guiana": "French Guiana", "French Polynesia": "French Polynesia", "French Southern Territories": "French Southern Territories", "Gabon": "Gabon", "Gambia": "Gambia", "Georgia": "Georgia", "Germany": "Germany", "Ghana": "Ghana", "Gibraltar": "Gibraltar", "Greece": "Greece", "Greenland": "Greenland", "Grenada": "Grenada", "Guadeloupe": "Guadeloupe", "Guam": "Guam", "Guatemala": "Guatemala", "Guernsey": "Guernsey", "Guinea": "Guinea", "Guinea-bissau": "Guinea-bissau", "Guyana": "Guyana", "Haiti": "Haiti", "Heard Island And Mcdonald Islands": "Heard Island And Mcdonald Islands", "Holy See (Vatican City State)": "Holy See (Vatican City State)", "Honduras": "Honduras", "Hong Kong": "Hong Kong", "Hungary": "Hungary", "Iceland": "Iceland", "India": "India", "Indonesia": "Indonesia", "Iran": "Iran", "Iraq": "Iraq", "Ireland": "Ireland", "Isle Of Man": "Isle Of Man", "Israel": "Israel", "Italy": "Italy", "Jamaica": "Jamaica", "Japan": "Japan", "Jersey": "Jersey", "Jordan": "Jordan", "Kazakhstan": "Kazakhstan", "Kenya": "Kenya", "Kiribati": "Kiribati", "Democratic Peoples Republic of Korea": "Democratic Peoples Republic of Korea", "Republic of Korea": "Republic of Korea", "Kuwait": "Kuwait", "Kyrgyzstan": "Kyrgyzstan", "Lao Peoples Democratic Republic": "Lao Peoples Democratic Republic", "Latvia": "Latvia", "Lebanon": "Lebanon", "Lesotho": "Lesotho", "Liberia": "Liberia", "Libyan Arab Jamahiriya": "Libyan Arab Jamahiriya", "Liechtenstein": "Liechtenstein", "Lithuania": "Lithuania", "Luxembourg": "Luxembourg", "Macao": "Macao", "Macedonia": "Macedonia", "Madagascar": "Madagascar", "Malawi": "Malawi", "Malaysia": "Malaysia", "Maldives": "Maldives", "Mali": "Mali", "Malta": "Malta", "Marshall Islands": "Marshall Islands", "Martinique": "Martinique", "Mauritania": "Mauritania", "Mauritius": "Mauritius", "Mayotte": "Mayotte", "Mexico": "Mexico", "Micronesia": "Micronesia", "Republic of Moldova": "Republic of Moldova", "Monaco": "Monaco", "Mongolia": "Mongolia", "Montenegro": "Montenegro", "Montserrat": "Montserrat", "Morocco": "Morocco", "Mozambique": "Mozambique", "Myanmar": "Myanmar", "Namibia": "Namibia", "Nauru": "Nauru", "Nepal": "Nepal", "Netherlands": "Netherlands", "New Caledonia": "New Caledonia", "New Zealand": "New Zealand", "Nicaragua": "Nicaragua", "Niger": "Niger", "Nigeria": "Nigeria", "Niue": "Niue", "Norfolk Island": "Norfolk Island", "Northern Mariana Islands": "Northern Mariana Islands", "Norway": "Norway", "Oman": "Oman", "Pakistan": "Pakistan", "Palau": "Palau", "Palestinian Territory": "Palestinian Territory", "Panama": "Panama", "Papua New Guinea": "Papua New Guinea", "Paraguay": "Paraguay", "Peru": "Peru", "Philippines": "Philippines", "Pitcairn": "Pitcairn", "Poland": "Poland", "Portugal": "Portugal", "Puerto Rico": "Puerto Rico", "Qatar": "Qatar", "Reunion": "Reunion", "Romania": "Romania", "Russian Federation": "Russian Federation", "Rwanda": "Rwanda", "Saint Helena": "Saint Helena", "Saint Kitts And Nevis": "Saint Kitts And Nevis", "Saint Lucia": "Saint Lucia", "Saint Pierre And Miquelon": "Saint Pierre And Miquelon", "Saint Vincent And The Grenadines": "Saint Vincent And The Grenadines", "Samoa": "Samoa", "San Marino": "San Marino", "Sao Tome And Principe": "Sao Tome And Principe", "Saudi Arabia": "Saudi Arabia", "Senegal": "Senegal", "Serbia": "Serbia", "Seychelles": "Seychelles", "Sierra Leone": "Sierra Leone", "Singapore": "Singapore", "Slovakia": "Slovakia", "Slovenia": "Slovenia", "Solomon Islands": "Solomon Islands", "Somalia": "Somalia", "South Africa": "South Africa", "South Georgia And The South Sandwich Islands": "South Georgia And The South Sandwich Islands", "Spain": "Spain", "Sri Lanka": "Sri Lanka", "Sudan": "Sudan", "Suriname": "Suriname", "Svalbard And Jan Mayen": "Svalbard And Jan Mayen", "Swaziland": "Swaziland", "Sweden": "Sweden", "Switzerland": "Switzerland", "Syrian Arab Republic": "Syrian Arab Republic", "Taiwan": "Taiwan", "Tajikistan": "Tajikistan", "Tanzania": "Tanzania", "Thailand": "Thailand", "Timor-leste": "Timor-leste", "Togo": "Togo", "Tokelau": "Tokelau", "Tonga": "Tonga", "Trinidad And Tobago": "Trinidad And Tobago", "Tunisia": "Tunisia", "Turkey": "Turkey", "Turkmenistan": "Turkmenistan", "Turks And Caicos Islands": "Turks And Caicos Islands", "Tuvalu": "Tuvalu", "Uganda": "Uganda", "Ukraine": "Ukraine", "United Arab Emirates": "United Arab Emirates", "United Kingdom": "United Kingdom", "United States": "United States", "United States Minor Outlying Islands": "United States Minor Outlying Islands", "Uruguay": "Uruguay", "Uzbekistan": "Uzbekistan", "Vanuatu": "Vanuatu", "Venezuela": "Venezuela", "Viet Nam": "Viet Nam", "British Virgin Islands": "British Virgin Islands", "U.S. Virgin Islands": "U.S. Virgin Islands", "Wallis And Futuna": "Wallis And Futuna", "Western Sahara": "Western Sahara", "Yemen": "Yemen", "Zambia": "Zambia", "Zimbabwe": "Zimbabwe" }, languages: { Persian: "فارسی", Indonesian: "Bahasa Indonesia", Catalan: "Català", Czech: "Čeština", Danish: "Dansk", German: "Deutsch", English: "English", Spanish: "Español", French: "Français", Croatian: "Hrvatski", Italian: "Italiano", Lithuanian: "Lietuvių", Hungarian: "Magyar", Dutch: "Nederlands", Norwegian: "Norsk", Polish: "Polski", Portuguese: "Português", Romanian: "Română", Slovak: "Slovenčina", Slovenian: "Slovenščina", Finnish: "Suomi", Swedish: "Svensk", Vietnamese: "Tiếng Việt", Turkish: "Türkçe", Greek: "Ελληνικά", Bulgarian: "български език", Russian: "Русский", Serbian: "Српски", Ukrainian: "Українська", Hebrew: "עברית", Arabic: "اللغة العربية", Standard_Hindi: "हिन्दी", Thai: "ภาษาไทย", Korean: "한국어", Chinese: "中文", Japanese: "日本語", Other: "Other" }, gender: { male: "Male", female: "Female", other: "Other" }, education: { "pre-high school": "pre-high school", "high school": "high school", "college": "college", "graduate school": "graduate school", "professional school": "professional school", "PhD": "PhD", "postdoctoral": "postdoctoral" } } }, ///////////////////////////////////////////////// // // // Methods for creating forms // // // ///////////////////////////////////////////////// /** * Initialize a new form. * @function newForm * @param {string} name - The name of the form. Passing a recognized name will load default properties. * @param {Object} [args] - Arguments object. * @param {string} [args.name] - The name of the form, only required if the form is an unrecognized type. Currently, only 'demographics' is a recognized type. * @param {string} [args.parentElement] - The id of a DOM element into which the form should be inserted. * @param {string} [args.tableName] - The name of the database table where form data is stored, only required if the form is an unrecognized type. * @param {string} [args.headerMessage] - An optionally gettexted message to display at the top of the form. * @param {string} [args.requiredColor] - A valid CSS color or hex value in which to display messages about required questions. * @param {boolean} [args.autocomplete] - Whether or not to turn on autocomplete comboboxes for dropdown elements. * @param {string} [args.stylingClass] - A CSS class which contains styling rules for the form. * @param {string} [args.whyPrompt] - An optionally gettexted clickable prompt to display for the user to learn more information about why this form exists. * @param {string} [args.whyMessage] - An optionally gettexted message to display in an alert box when the user clicks on whyPrompt. * @param {string[]} [args.requiredQuestions] - Array of required questions. * @param {string} [args.requiredMessage] - An optionally gettexted message to display indicating that some form questions (denoted by a *) are required. * @param {string} [args.requiredMessageAll] - An optionally gettexted message to display indicating that all forms questions are required. * @param {string} [args.requiredMessageAll] - An optionally gettexted message to display indicating that all forms questions are required. */ newForm = function() { var formProps = _fetchProps("newForm", "forms", arguments[0], arguments[1]) ._check() ._register() ._val; // because the autocomplete plugin uses JQueryUI, only run it if autocomplete is explicitly enabled if (formProps.autocomplete) _activateAutocomplete(); var html = _cat(_elt("form", { "name": formProps["name"], "class": formProps["stylingClass"] }), _elt("div", { "id": "headerContainer" }), _elt("span", { "id": "formHeader", "class": "spacedRight" }, formProps["headerMessage"]), "", _elt("span", { "id": "whyForm" }, formProps["whyPrompt"]), "", ""); return { /** * Add an individual form element. * @function add * @param {string} name - The name of the form element. Passing a recognized name will load default properties. * @param {Object} [args] - Arguments object. * @param {string} [args.name] - The name of the form, only required if the form element is an unrecognized type. Currently, only 'demographics' is a recognized type. * @param {(string|string[])} [args.options] - The set of options to use for this form element. Default option sets will load default options; an array of options will load that array. * @param {string} [args.style] - The style of the question type, only required if the form element is an unrecognized type. Recognized styles are 'dropdown', 'freeText', 'shortFreeText', and 'numericalFreeText'. * @param {boolean} [args.optionsAsNumbers] - Whether or not to record option values as 0-indexed integers (as opposed to the value displayed in the form elemnent). Defaults to false. * @param {string} [args.expansionHeader] - If this element is expandable, an optionally gettexted header message to display for the expandable elements. * @param {(string|string[])} [args.expandPrompt] - If this element is expandable, an optionally gettexted clickable prompt for creating additional expansion elements. Passing an array will cause prompts in the array to be displayed one after another until the last is reached. * @param {string} [args.contractPrompt] - If this element is expandable, an optionally gettexted clickable propmt for removing a single expansion element. * @param {number} [args.maxExpansions] - If this element is expandable, the maximum number of expansion elements allowed. Defaults to 6. * @param {boolean} [args.hidden] - Whether or not this element is hidden by default. Defaults to false. * @param {string} [args.expands] - If this element triggers another element to expand, the name of the triggered element. * @param {string} [args.expansionTrigger] - If this element triggers another element to expand, the value that will trigger the expansion. Other selected values of this element will cause the expansion element to be removed. */ add: function() { // add to array of active questions formProps._activeQuestions.push([arguments[0], arguments[1]]); return this; }, /** * Render accumulated form elements. * @function render * @param {Function} fn - The name of a function to be called after form data has been validated and submitted. */ render: function(fn) { // loop through all added questions and generate html for them formProps._activeQuestions.forEach(function(question) { var eltProps = _fetchProps("add", "formElements", question[0], question[1]) ._check() ._register() ._val; // add to array of question types, used for validation later formProps._activeQuestionTypes[eltProps.name] = { "name": eltProps.name, "style": eltProps.style, "minValue": eltProps.minValue, "maxValue": eltProps.maxValue }; // attach a listener to handle clicks on expansion/contraction prompts for this element $("body").on("click", "#" + eltProps.name + "ExpansionContainer .expansionPrompt", function(e) { _handleExpansion(e, eltProps, formProps); }); // build a container for this element html += _cat(_elt("div", { "class": "formElement" + (eltProps.hidden ? " hidden" : ""), "id": eltProps.name + "Container" }), _buildFormElement(eltProps, formProps), ""); }); html += ""; // record progression to demographics page _recordDropoutProgression(1, "demographics"); showNextButton(function() { _processForm(formProps, fn) }, { "loadingOverride": true }); $("#" + formProps.parentElement).append(html); $("#whyForm").on("click", function() { alert(formProps.whyMessage); }); if (formProps._activeQuestions.length == formProps.requiredQuestions.length) { $("#headerContainer").append(_elt("div", { "id": "requiredMessageAll", "style": "color: " + formProps.requiredColor }, formProps.requiredMessageAll) + ""); } else { $("#headerContainer").append(_elt("div", { "id": "requiredMessage", "style": "color: " + formProps.requiredColor }, formProps.requiredMessage) + ""); } if (formProps.autocomplete) $("select").combobox(); } } }, /** * Build the HTML for a single form element. * @function _buildFormElement * @param {Object} eltProps - The working properties of the element to be built. * @param {Object} formProps - The working properties of the form into which the element is being added. * @returns {string} The assembled HTML. */ _buildFormElement = function(eltProps, formProps) { // maps form element styles to input markup var inputTypes = { "dropdown": ["select", ""], "freeText": ["textarea rows='4' cols='50'", ""], "shortFreeText": ["input type='text'", ""], "numericalFreeText": ["input type='text'", ""], "radio": ["input type='radio'", ""], "numericList": ["select", ""] }; //remove a prior contract prompt if it exists $("[data-role='contract']").remove(); // assemble the html for a single form line var html = _cat(((_inArray(eltProps.name, formProps.requiredQuestions) && formProps._activeQuestions.length != formProps.requiredQuestions.length) ? _elt("span", { "class": "promptText", "style": "color: " + formProps.requiredColor }, "* ") : ""), "", _elt("span", { "class": "promptText spacedRight" }, ((eltProps._expanded) ? _getLabel(eltProps._expansionCount - 1, eltProps.expansionPrompt) : eltProps.prompt)), "", _elt(inputTypes[eltProps.style][0], { "id": eltProps.name }), _buildOptions(eltProps), inputTypes[eltProps.style][1], ((eltProps._expanded && eltProps.maxExpansions > 1) ? _elt("span", { "class": "hoverable linkBlue expansionPrompt", "data-role": ((eltProps._expansionCount == 1) ? "expand" : "contract") }, ((eltProps._expansionCount == 1) ? eltProps.expandPrompt : eltProps.contractPrompt)) : "")); // if this is an element that triggers an expansion, attach a listener for the trigger event if (eltProps.expand) { $("body").on("change", "#" + eltProps.name , function() { // get a live copy of the element to be expanded var targetProps = properties.formElements[eltProps.expand]; var baseElement = targetProps.name.match(/([A-Za-z_]+)\d$/)[1]; // add an expansion element if maxExpansions has not been rearched if ($(this).val() == eltProps.expansionTrigger) { // give a sequential name to the expansion element based on the current expansion count targetProps.name = baseElement + targetProps._expansionCount++; targetProps._expanded = true; $("#" + targetProps.name + "Container").after( _cat(_elt("div", { "class": "expansionContainer", "id": targetProps.name + "ExpansionContainer" }), _elt("div", { "class": "expansionHeader" }, targetProps.expansionHeader), "", _elt("div", { "class": "formElement expansionElement", "id": targetProps.name + "Container" }), _buildFormElement(targetProps, formProps), "") ).remove(); // if expansion is cancelled } else if (targetProps._expanded == true && $(this).val() != eltProps.expansionTrigger) { // reset appropriate properties of the base element targetProps._expanded = false; targetProps._expansionCount = 0; targetProps.name = baseElement + "0"; // restore the base element $("#" + targetProps.name + "ExpansionContainer").replaceWith(_elt("div", { "class": "formElement" + (targetProps.hidden ? " hidden" : ""), "id": targetProps.name + "Container" }) + ""), $("#" + targetProps.name + "Container").html(_buildFormElement(targetProps, formProps)); } }); } //console.log("working html:", html); return html; }, /** * Handle the expansion or contraction of an expandable element, either by adding an additional expansion element or by removing an expansion element. * @function _handleExpansion * @param {Object} e - The click event object. * @param {Object} eltProps - The working properties of the element that is being expanded or contracted. * @param {Object} formProps - The working properties of the form in which the element is located. */ _handleExpansion = function(e, eltProps, formProps) { var action = $(e.target).attr("data-role"); var baseElement = eltProps.name.match(/([A-Za-z_]+)\d$/)[1]; if (action == "expand") { if (eltProps._expansionCount < eltProps.maxExpansions) { eltProps.name = baseElement + eltProps._expansionCount++; $("#" + baseElement + "0ExpansionContainer").append(_cat( _elt("div", { "class": "formElement expansionElement", "id": eltProps.name + "Container" }), _buildFormElement(eltProps, formProps), "") ); } } else if (action == "contract") { $("#" + baseElement + --eltProps._expansionCount + "Container").remove(); if (eltProps._expansionCount >= 2) $("#" + baseElement + (eltProps._expansionCount - 1) + "Container").append(_elt("span", { "class": "hoverable linkBlue expansionPrompt", "data-role": "contract" }, eltProps.contractPrompt), ""); } }, /** * Build the HTML for a for element's options. * @function _buildOptions * @param {Object} eltProps - The working properties of the element for which to build options. * @returns {string} - The options HTML. */ _buildOptions = function(eltProps) { // if this element isn't a dropdown, nothing to do // TODO: handle radio buttons if (eltProps.style != "dropdown") return; var optionsHtml = "", count = 0, options = {}; if (eltProps.options == "numericList") { for (var i = eltProps.minValue; i <= eltProps.maxValue; i += (eltProps.stepValue || 1)) { options[i] = i; } // if eltProps.options is an object, assume user has passed in a custom options set } else if (typeof eltProps.options === "object") { options = eltProps.options; } else { options = properties.formOptions[eltProps.options]; } // add a blank option optionsHtml += ""; for (var option in options) { // use the keys of option sets as the values to record; use the (possibly translated) values of option sets as the values to display optionsHtml += _cat(""); count++; } return optionsHtml; }, /** * Validate a form. * @function _validateForm * @param {Object} formProps - The working properties of the form to validate. * @param {Object} responses - Object of user-supplied form responses. * @returns {boolean} - Whether or not the form is valid, based on the requiredQuestions array. */ _validateForm = function(formProps, responses) { var invalidQuestions = []; // reset all borders $(".formElement").css("border", "1pt solid transparent"); for (var response in responses) { if (!formProps._activeQuestionTypes[response]) continue; var questionType = formProps._activeQuestionTypes[response]; // assume that a single blank space or an empty string constitutes no response if (((responses[response] == " " || responses[response] == "") || (questionType.style == "numericalFreeText" && (isNaN(responses[response]) || responses[response] < questionType.minValue || responses[response] > questionType.maxValue))) && _inArray(response, formProps.requiredQuestions)) { invalidQuestions.push(response); // add animation and highlighting $(_cat("#", response, "Container")).animate({backgroundColor: '#FFB2B2'}, 350) .animate({backgroundColor: '#FFFFFF'}, 350); $(_cat("#", response, "Container")).css("border", "1pt solid #FFB2B2"); } } return (invalidQuestions.length == 0 ? true : false); }, /** * Gather user-supplied responses and submit form data (if form is valid). * @function _processForm * @param {Object} formProps - The working properties of the form to validate. * @param {Function} fn - Function to call once form data has been submitted. */ _processForm = function(formProps, fn) { var responses = {}; // in this selector, we need to be careful to not select the hidden inputs that the combobox plugin creates $("#" + formProps.parentElement + " :input:not(.custom-combobox-input)").each(function() { responses[$(this).attr("id")] = $(this).val(); }); if (_validateForm(formProps, responses)) { responses.participantId = properties.global._participantId; responses.tableName = formProps.tableName; responses.action = "formData"; $.ajax({ type: "POST", url: properties.global.dataScriptLoc, data: responses }); _loadingTransition("demographics"); // hide next button here to prevent further input hideNextButton(); setTimeout(function() { fn(); }, properties.global.loadingTime); } }, /** * Activates the function that adds autocomplete combobox functionality to dropdown form elements. NOTE: Requires JQueryUI. * @function _activateAutocomplete */ _activateAutocomplete = function() { // combobox code due to https://jqueryui.com/autocomplete/#combobox (function( $ ) { $.widget( "custom.combobox", { _create: function() { this.wrapper = $( "" ) .addClass( "custom-combobox" ) .insertAfter( this.element ); this.element.hide(); this._createAutocomplete(); this._createShowAllButton(); }, _createAutocomplete: function() { var selected = this.element.children( ":selected" ), value = selected.val() ? selected.text() : ""; // create a little extra space between combobox elements since they take up more room than regular dropdowns this.wrapper.css("margin-top", "3px"); this.input = $( "" ) .appendTo( this.wrapper ) .val( value ) .attr( "title", "" ) .addClass( "custom-combobox-input ui-widget ui-widget-content ui-state-default ui-corner-left" ) .autocomplete({ delay: 0, minLength: 0, source: $.proxy( this, "_source" ) }) .tooltip({ tooltipClass: "ui-state-highlight" }) // need to import existing element parameters here for expansion elements to work correctly .attr("name", $(this.element).attr("name")) .attr("onchange", $(this.element).attr("onchange")) .attr("id", $(this.element).attr("id")) .data("previousVal", ""); // show all menu options when text entry field is clicked $(this.input).click(function() { $(this).autocomplete( "search", "" ); }); this._on( this.input, { autocompleteselect: function( event, ui ) { ui.item.option.selected = true; this._trigger( "select", event, { item: ui.item.option }); //console.log("ui.item.option.value:", ui.item.option.value); // trigger onchange event here // need to test if input value has indeed changed... if ($(this.input).data("previousVal") != ui.item.option.value) { $(this.input).data("previousVal", ui.item.option.value); this.input.trigger("onchange"); } }, autocompletechange: "_removeIfInvalid", }); }, _createShowAllButton: function() { var input = this.input, wasOpen = false; $( "" ) .attr( "tabIndex", -1 ) .appendTo( this.wrapper ) .button({ icons: { primary: "ui-icon-triangle-1-s" }, text: false }) .removeClass( "ui-corner-all" ) .addClass( "custom-combobox-toggle ui-corner-right" ) .mousedown(function() { wasOpen = input.autocomplete( "widget" ).is( ":visible" ); }) .click(function() { input.focus(); // Close if already visible if ( wasOpen ) { return; } // Pass empty string as value to search for, displaying all results input.autocomplete( "search", "" ); }); }, _source: function( request, response ) { var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), "i" ); response( this.element.children( "option" ).map(function() { var text = $( this ).text(); if ( this.value && ( !request.term || matcher.test(text) ) ) return { label: text, value: text, option: this }; }) ); }, _removeIfInvalid: function( event, ui ) { // Selected an item, nothing to do if ( ui.item ) { return; } // Search for a match (case-insensitive) var value = this.input.val(), valueLowerCase = value.toLowerCase(), valid = false; this.element.children( "option" ).each(function() { if ( $( this ).text().toLowerCase() === valueLowerCase ) { this.selected = valid = true; return false; } }); // Found a match, nothing to do if ( valid ) { // need to make sure we trigger the onchange event here in case user tab-switches out of the field without directly selecting a dropdown option this.input.trigger("onchange"); return; } // Remove invalid value this.input .val( "" ) //.attr( "title", value + " didn't match any item" ) //.tooltip( "open" ); this.element.val( "" ); this._delay(function() { this.input.tooltip( "close" ).attr( "title", "" ); }, 2500 ); this.input.autocomplete( "instance" ).term = ""; }, _destroy: function() { this.wrapper.remove(); this.element.show(); } }); })( jQuery ); }, /** * Generate and insert standard comments form into a div with id 'comments'. NOTE: Eventually this functionality will be merged with the general newForm method. * @function showCommentsPage * @param {Function} fn - Function to call when comments data has been submitted. */ showCommentsPage = function(fn) { var blankMsg = "You may leave this area blank."; $("#comments").html(_cat( "

", "Thank you for your participation!", "

", "Before you continue to your results, please let us know what you thought of the test!", "

", "Do you have any comments for the researcher? Questions, Suggestions, or Concerns?", "


", "Did you encounter any technical difficulties during this study? If yes, how?  ", "  


", "Did you cheat or in any way provide false information? If yes, how?  ", "  



", "You can also email us at info@labinthewild.org" )); $(".comments_box").on("blur", function() { $(this).html(blankMsg); }) .on("focus", function() { $(this).html(""); }); $("input[type=radio]").on("click", function() { var target = $(this).attr("name"); $("textarea[name=" + target + "]").css("display", ($(this).filter(":checked").val() == "yes") ? "block" : "none"); }); showNextButton(_submitCommentData(fn, blankMsg), { "loadingOverride": true }); showSlide("comments"); }, /** * Submit data from comments form to the server. NOTE: Eventually this functionality will be merged with the general newForm method. * @function _submitCommentData * @param {Function} fn - Function to call when comments data has been submitted. * @param {string} blankMsg - Optionally gettexted default string to insert into text boxes. */ _submitCommentData = function(fn, blankMsg) { return function() { var data = {}; data.general = ($("#general").val() == blankMsg) ? "" : $("#general").val(); //console.log($("input[name=cheating]:checked").val()); //console.log($("input[name=technical]:checked").val()); if ($("input[name=cheating]:checked").val() == "yes") data.cheating = ($("#cheating").val() == blankMsg) ? "Yes" : $("#cheating").val(); else data.cheating = ""; if ($("input[name=technical]:checked").val() == "yes") data.technical = ($("#technical").val() == blankMsg) ? "Yes" : $("#technical").val(); else data.technical = ""; data.participantId = properties.global._participantId; data.action = "commentsData"; $.ajax({ type: "POST", url: properties.global.dataScriptLoc, data: data }).done(function(result) { //console.log("Result: " + result); }); _loadingTransition(); setTimeout(function() { fn(); }, properties.global.loadingTime); } }, //////////////////////////////////////////////////////////// // // // Methods for recording participant city and country // // // //////////////////////////////////////////////////////////// /** * Retrieve a unique participant id, user country, and user city. * @function _getParticipantInfo * @param {Object} args - Argument object. * @param {boolean} args.ipCountry - Whether or not to fetch user country from MaxMind. * @param {boolean} args.ipCity - Whether or not to fetch user city from MaxMind. */ _getParticipantInfo = function(args) { var args = args || {}; args.ipCountry = true; args.ipCity = true; var _litw_locale = window.litw_locale || null; $.ajax({ type: "POST", url: properties.global.dataScriptLoc, data: { "action": "participantInfo", "condition": "", "locale": _litw_locale } }).done(function(result) { result = JSON.parse(result); properties.global._participantId = result.participantId; properties.global._participantCountry = result.ipCountry; properties.global._participantCity = result.ipCity; _recordDropoutProgression(1, "IRB"); if (args.callback) args.callback(); }); }, ///////////////////////////////////////////////////////////// // // // Methods for accessing aggregate experiment statistics // // // ///////////////////////////////////////////////////////////// // calls the relevant method in LITWdata.php and retrieves aggregate stats from a database table with standard structure // TODO: categories that have no data in them. These categories will simply not appear in the results, but should be handled as count = 0, mean = NaN, etc. // TODO: add user-supplied WHERE clause to limit data returned /** * Retrieve aggregate statistics from the database. * @function _fetchStatistics */ _fetchStatistics = function() { $.ajax({ type: "POST", url: properties.global.dataScriptLoc, data: { action: "getStatistics" } }).done(function(results, textStatus) { results = JSON.parse(results); // loop through all rows from the stats table in the database for (var i = 0; i < results.length; i++) { var categoryName = results[i]["category"]; // create an object for each demographic category listed in the database, and store statistics within these objects properties.global._aggregateStatistics[categoryName] = {}; for (var key in results[i]) { if (results[i].hasOwnProperty(key) && key != "category") { properties.global._aggregateStatistics[categoryName][key] = results[i][key]; } } } //console.log("fetched stats:", properties.global._aggregateStatistics); }); }, /** * Return previously fetched aggregate statistics. * @function getStatistics * @returns {Object} - The previously fetched statistics. */ getStatistics = function() { return properties.global._aggregateStatistics; }, // this is a temporary function for testing purposes... setStatistics = function() { $.ajax({ type: "POST", url: properties.global.dataScriptLoc, data: { action: "setStatistics" } }).done(function(results) { //console.log(results); }); }, //////////////////////////////////////////////////////////// // // // utility functions // // // //////////////////////////////////////////////////////////// /** * Shuffle the order of elements in an array (Fisher-Yates algorithm). * @function shuffleArray * @param {Array} array - Array of elements to be shuffled. * @returns {Array} The shuffled array. */ shuffleArray = function(array) { var m = array.length, t, i; while (m) { i = Math.floor(Math.random() * m--); t = array[m]; array[m] = array[i]; array[i] = t; } return array; }, /** * Tests whether an item is found in an array. * @function _inArray * @param {*} item - Item to search for. * @param {Array} array - The array to search in. * @returns {boolean} */ _inArray = function(item, array) { for (var i = 0; i < array.length; i++) { if (array[i] == item) return true; } return false; }, /** * Creates an HTML string of given types with given attributes and content. Does not close the tag. * @function _elt * @param {string} type - Name of the HTML tag to create. * @param {Object} [attributes] - Optional names and values of attributes to add to the tag. * @param {string} [content] - Optional content to insert inside the tag. * @returns {string} The HTML string. */ _elt = function(type, attributes, content) { var attributeString = ""; for (var attrib in attributes) { // <-- there is an API for adding attributes which maybe we should use instead of building a string? attributeString = _cat(attributeString, " ", attrib, "='", attributes[attrib], "'"); } return _cat("<", type, attributeString, ">", (content ? content : "")); }, /** * Loops through a set of args and overwrites corresponding values in a set of default args. Throws an error when undefined default args still exist after this process. * @function _checkArgs * @param {Object} defaultArgs - Object of default args to overwrite. * @param {Object} userArgs - Object of user-supplied args. * @returns {Object} An object of args with desired defaults overwritten. * @throws Will throw an error if undefined default args remain at the end of execution. */ _checkArgs = function(defaultArgs, userArgs) { var missingArgs = []; for (arg in userArgs) { defaultArgs[arg] = userArgs[arg]; if (defaultArgs[arg] === undefined) missingArgs.push(arg); } if (missingArgs.length > 0) throw(_cat("Missing the following required arguments in ", arguments.callee.caller.name, ": ", missingArgs.toString())); return defaultArgs; }, /** * Loops through properties in properties[context][name] and overwrites with any properties found in args. * @function _fetchProps * @param {string} fnName - The name of the calling function (used to generate error messages). * @param {string} context - The name of the relevant properties type. * @param {string} name - The name of the specific element type. * @param {Object} args - Object of specific arguments to overwrite with. * @returns {Object} Object of additional methods to call. */ _fetchProps = function(fnName, context, name, args) { var props = _clone(properties[context]["generic"]); for (var prop in properties[context][name]) { props[prop] = properties[context][name][prop]; } for (var prop in args) { props[prop] = args[prop]; } return { /** * After property overwriting has occurred, checks if any undefined properties remain in properties[context][name]. * @function _check * @throws Will throw an error indicating which properties remain undefined. * @returns {Object} Object of additional methods to call. */ _check: function() { var missingProps = []; for (var prop in props) { if (props[prop] === undefined) missingProps.push(prop); } if (missingProps.length > 0) throw(_cat(fnName, "() is missing the following required arguments: ", missingProps.toString())); return this; }, /** * Writes passed properties to the properties object. * @function _register * @returns {Object} Object of additional methods to call. */ _register: function() { properties[context][name] = props; return this; }, _val: props } }, /** * Clones an object by value, producing a new object with identical values. * @function _clone * @param {Object} obj - The object to clone. * @returns {Object} The cloned object. */ _clone = function(obj) { var copy = {}; for (var key in obj) { if (obj.hasOwnProperty(key)) copy[key] = obj[key]; } return copy; }, // apply parameters in args to target _applyArgs = function(args, target) { for (var arg in args) { if (args.hasOwnProperty(arg)) { target[arg] = args[arg]; } } }, /** * Efficiently concatenates all strings passed. * @function _cat * @returns {string} The concatenated string. */ _cat = function() { var temp = []; for (var i = 0; i < arguments.length; i++) { temp.push(arguments[i]); } return temp.join(""); }, /** * Returns labels from an array of labels such that the final label is always returned if the label count desired exceeds the number of labels available. * @function _getLabel * @param {string[]} labels - Array of labels. * @param {number} count - Current number of labels array. * @returns {string} The label for the relevant count, or, if count exceeds the number of labels, the last label in the labels array. */ _getLabel = function(count, labels) { if (labels[count]) return labels[count]; return labels[labels.length - 1]; }, ///////////////////////////////////////////////// // // // experiment maintenance functions // // // ///////////////////////////////////////////////// /** * Displays the spinning loading icon in the middle of the screen and reduces the opacity of the DOM element with id elt. * @function _loadingTransition * @param {string} elt - The id of the element to fade. */ _loadingTransition = function(elt) { $("#" + elt).css("opacity", "0.3"); $("#ajaxWorking") .html("") .css("display", "inline"); }, /** * Sets the CSS display of the DOM element with id elt to 'none'. * @function hideElement * @param {string} elt - The id of the element to hide. */ hideElement = function(elt) { $("#" + elt).css("display", "none"); }, /** * Sets the CSS display of the DOM element with id elt to 'block'. * @function showElement * @param {string} elt - The id of the element to hide. */ showElement = function(elt) { $("#" + elt).css("display", "block"); }, /** * Displays the next button. * @function showNextButton * @param {Function} fn - The function to call when the next button is pressed. * @param {Object} [props] - Object of properties. * @param {string} [props.iconPath] - Filepath of an icon to use for the next button in place of the deault icon. * @param {string} [props.iconActivePath] - Filepath of an icon to use for the next button in place of the deault active icon. */ showNextButton = function(fn, props) { var props = _fetchProps("showNextButton", "widgets", "nextButton", props) ._check() ._register() ._val; $("#nextButton").html("
"); $("#nextButtonContainer").on("mousedown", function() { $("img", this).attr("src", props.iconActivePath); }) .on("mouseup", function() { $("img", this).attr("src", props.iconPath); }); if (!props.loadingOverride) { $("#nextButtonContainer").on("click", function() { $(this).css("display", "none"); _loadingTransition(properties.global._currentSlide); setTimeout(fn, properties.global.loadingTime); }); } else { $("#nextButtonContainer").on("click", function() { fn(); }); } //if (getKeyboardInput) getKeyboardInput(["space", "right"], fun); //if (disableSelect) disableSelect($("#next_button")); $("#nextButton").css("display", "block"); }, /** * Hides the next button (assumes the next button has id = 'nextButton'.) * @function hideNextButton */ hideNextButton = function() { $("#nextButton").css("display", "none"); }, /** * Increases the trial counter as well as the cumulative trial counter by one. * @function increaseTrialCounter */ increaseTrialCounter = function() { properties.global._trialNum++; properties.global._cumulativeTrialNum++; }, /** * Increases the phase counter by one and resets the trial counter to zero. * @function IncreasePhaseCounter */ increasePhaseCounter = function() { properties.global._trialNum = 0; properties.global._phaseNum++; }, /** * Returns the current phase number. * @function getPhaseNum * @returns {number} The phase number. */ getPhaseNum = function() { return properties.global._phaseNum; }, /** * Returns the current trial number. * @function getTrialNum * @returns {number} The trial number. */ getTrialNum = function() { return properties.global._trialNum; }, /** * Returns the current cumulative trial number. * @function getCumulativeTrialNum * @returns {number} The cumulative trial number. */ getCumulativeTrialNum = function() { return properties.global._cumulativeTrialNum; }, /** * Shows an element on the page with class "slide" and id passed in. All other elements with class "slide" will be hidden. NOTE: code adapted from the zen.js library. * @function showSlide * @param {string} id - The HTML id of the element show. */ showSlide = function(id) { $("#ajaxWorking").css("display", "none"); properties.global._currentSlide = id; var slides = properties.global._slides; if (!slides) slides = $(".slide"); var i = slides.length, changeTo; while (i--) { changeTo = ($(slides[i]).attr("id")) == id ? 'block' : 'none'; if ($(slides[i]).css("style", "display") == changeTo) continue; $(slides[i]).css("display", changeTo); } }, /** * Calls method to get participant id, attaches a beforeunload event listener to handle dropouts, and fetches aggregate experiment statistics if available. * @function initialize */ initialize = function(callback) { _getParticipantInfo({callback: callback}); // attach a listener to record dropouts when the browser window is closed // TODO: alert box confirming the unload $(window).on("beforeunload", function() { var data = { participantId: properties.global._participantId, progression: properties.global._progression, description: properties.global.progressionDescription, dropoutCode: 2, action: "dropoutProgression" } $.ajax({ type: "POST", url: properties.global.dataScriptLoc, data: data, async: false // <-- this turns out to be important. Setting it to false seems to increase the chances of the data actually being recorded }); }); properties.global.aggregateStatistics = _fetchStatistics(); // show header? if (properties.global.brandHeader) { $("#header").html("
 
") .css("display", "block"); } }, // initialize a study, without getting aggregate data, creating the onbeforeunload // callback, or inserting the header initializeLite = function(callback) { _getParticipantInfo({callback: callback}); }, /** * Records one trial's worth of data in an internal variable. NOTE: Currently, the api does not handle the submission of this data to a database. * @function recordResult * @param {Object} trial - Object of trial data. */ recordResult = function(trial) { trial.participantId = properties.global._participantId; properties.global._results.push(trial); }, /** * Get the current participant id. * @function getParticipantId * @returns {number} The participant id. */ getParticipantId = function() { return properties.global._participantId; }, /** * Get an array of colors suitable for use on a graph. * @function getGraphColors * @returns {string[]} The array of hex value colors. */ getGraphColors = function() { return ["#1f77b4", "#d62728", "#ff7f0e", "#2ca02c", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"]; }, /** * Insert the standard results page footer content with desired experiment links. * @function showFooter * @param {string[]} testTitles - Gettexted array of test taglines. Note that these must be gettexted for links to be displayed correctly. * @param {string} divId - The id of the HTML element where footer content will be appended. */ showFooter = function(testTitles, divId) { // insert preliminary footer before we get to sharing buttons var beginShare = "
 
" + "

" + "Thanks again! Thought that was fun?" + "

"; $("#" + divId).append(beginShare); // now begin links to other stuff var links = "

" + "Consider taking some of our other tests:

" + ""; // json info of other tests to link to var tests = $.parseJSON( '[{"title":"How do you collaboratively solve problems?","img":"https:\/\/studies.labinthewild.org\/team-work\/img\/logo1.jpg","desc":"Imagine you are trying to solve a difficult problem at work. Your boss assigns you to a new collaborator, an artificial intelligence (AI), who is going to be helping you solve the problem. How well can you work with this AI? This study takes around 10 minutes to complete.","lang":"en","url":"https:\/\/studies.labinthewild.org\/team-work\/"},{"title":"What is your decision-making style?","img":"images\/decision-making-logo.jpg","desc":"You are making decisions every day. Have you wondered what kind of decision-making styles you have? Take our test and you will learn more about it!","lang":"en","url":"studies\/decision-making\/"},{"title":"What\u2019s your personality?","img":"images\/personality.png","desc":"You will learn about the five main traits of your personality and how you score on them. We will also try to establish the relationship between personality and physical activity goals.","lang":"en","url":"studies\/personality\/"},{"title":"Can you tell the nutritional content of a plate?","img":"https:\/\/ai.labinthewild.org\/nutrition\/images\/thumbnail.png","desc":"Take this study to see if you can accurately tell the nutritional content of a plate. See if you are more accurate than the average! An AI assistant will help you along the way.","lang":"en","url":"https:\/\/va.labinthewild.org\/va"},{"title":"Are you better than an AI in noticing hateful speech?","img":"images\/toxicity-icon.png","desc":"Rate what speech is hateful and we\u0027ll show you how well you detect hateful speech compared to an AI and others.","lang":"en","url":"studies\/toxicity\/"},{"title":"Where are you on the techno-skeptic to techno-utopian scale?","img":"images\/mixed-reality.png","desc":"Tell us how you think future mixed reality technology will affect your personal life, we will show you whether you are more techno-skeptic or techno-utopian.","lang":"en","url":"studies\/techno-utopia"},{"title":"Could you live with an AI and its morals?","img":"images\/culturaldelphii-logo.png","desc":"Tell us your moral judgments on certain situations and we will show you how you compare to others\u0027 and an AI.","lang":"en","url":"https:\/\/culturaldelphii.labinthewild.org\/"},{"title":"Play retro video games and see how you compare to others!","img":"studies\/atari\/atari-logo.png","desc":"Play old arcade video games and see how you do compared to others. You will also help us understand how AI agents can learn from and interact with human players. This experiment can take as little as one minute or as long as you are having fun playing the games.","lang":"en","url":"studies\/atari\/"},{"title":"Bar chart ratios as far as the eyes can see","img":"images\/ratio_estimation.png","desc":"Are different bar chart styles created equal? Find out by taking quick estimates of ratios from bar charts. This test tasks about 5 minutes.","lang":"en","url":"studies\/ratio\/"},{"title":"What percentage of Wikipedia do you know?","img":"studies\/wikipedia\/img\/wikipedia-logo.png","desc":"Take a survey on a few Wikipedia articles to see how well and fast you learn things. This study takes 5 minutes to complete.","lang":"en","url":"studies\/wikipedia"},{"title":"How do you make online connections?","img":"images\/friends-logo.png","desc":"This study will find out the fittest traits for your ideal friend. You will need to answer a few questions for us to generate your ideal friends\u0027 profiles, which you will then need to interact with. Afterwards we will show you our estimates on their traits.","lang":"en","url":"https:\/\/friends.labinthewild.org\/friends\/"},{"title":"Three privacy categories: which one best describes you?","img":"images\/privacyexpectations-logo.jpg","desc":"Tell us about your social media privacy expectations to find out your privacy index and how you compare to others. This test typically takes 15 minutes to complete. ","lang":"en","url":"https:\/\/privacyexpectations.labinthewild.org\/"},{"title":"You\u0027ve got email: Discover what email look will move you!","img":"studies\/newsletters\/img\/newsletter-icon.png","desc":"Tell us your opinion about some real-world email designs and you will learn what looks will tend to make you click! This test takes around 7 minutes.","lang":"en","url":"studies\/newsletters\/"},{"title":"Amazon, Apple, Facebook, Google: Can you tell the difference?","img":"images\/formality-logo.jpg","desc":"Test how well you know how big tech talks. This study takes 5 minutes.","lang":"en","url":"studies\/formality-security\/"},{"title":"COVID-19 dilemmas around the world: how would you decide?","img":"images\/covid_dilemma_logo.png","desc":"Test how your responses to moral dilemmas compare to others. This study takes around 5 minutes.","lang":"en pt de zh","url":"studies\/covid-dilemmas\/index.php"},{"title":"Test your spatial reasoning!","img":"images\/spatial-logo.png","desc":"Test how well you can visualize objects and images in space! The test typically takes 15 minutes to complete.","lang":"en","url":"https:\/\/spatialreasoning.labinthewild.org\/spatial\/"},{"title":"Do you make assumptions about people without knowing it?","img":"images\/aliens-logo.png","desc":"How well do you find patterns? Do you make assumptions about people without knowing it?","lang":"en","url":"https:\/\/aliens.labinthewild.org\/aliens\/"},{"title":"How well can AI understand your speech?","img":"images\/reading-assessment.jpg","desc":"Find out how well AI can understand your speech by reading out loud a few words and sentences! The test takes around 7 minutes.","lang":"en","url":"https:\/\/reading.labinthewild.org"},{"title":"How accurate is your peripheral vision?","img":"images\/virtual-chinrest.jpg","desc":"Are you curious about how accurate your peripheral vision is compared to others? Learn more about your visual perception by doing these fun tasks! This test takes around 8 minutes.","lang":"en","url":"studies\/peripheral-vision\/"},{"title":"What is your reasoning style?","img":"images\/ai_reasoning_logo2.png","desc":"Do you know what your reasoning style is? Interact with two different AIs in a nutrition-related task and find out which reasoning style you understand better. Compare yourself to others! The test typically takes 7 minutes to complete.","lang":"en","url":"http:\/\/ai.labinthewild.org\/nutrition\/?v=v2-a"},{"title":"Do you solve puzzles like a robot?","img":"images\/robot-puzzles-logo.png","desc":"Do you think you\u0027re a puzzle master? Do you think that you can do as well as a computer program? Try our puzzles to find out. They take about 10 minutes.","lang":"en","url":"studies\/litwf-robot-puzzles\/"},{"title":"How fast can you scan websites?","img":"images\/search-world.jpg","desc":"How quickly do you find information on websites? Compare your speed to others! This study takes around 10 minutes.","lang":"en ja","url":"studies\/viz_performance\/"},{"title":"Are you a savvy Airbnb user?","img":"images\/airbnb.png","desc":"Find out how your Airbnb use and preferences compare with others and learn about the use of technology in Airbnbs. This study takes around 10 minutes.","lang":"en","url":"studies\/litwf-airbnb\/"},{"title":"How good are you at data analysis?","img":"images\/analytic_skills_logo.jpg","desc":"Do you wonder how good you are at using different kinds of data? Try to make sense of some charts and see how you compare to others. This will take around 10 minutes.","lang":"en","url":"studies\/litwf-analytic-skills\/index.php"},{"title":"What is your personality?","img":"images\/puzzle_question_mark_brain.png","desc":"In this 10 minute study we will predict your personality traits, like how extroverted you are!","lang":"en","url":"https:\/\/storytelling.labinthewild.org\/template\/index.html"},{"title":"What is your problem solving intelligence?","img":"images\/cog-brain.jpg","desc":"How well do you solve new problems? Test your problem solving abilities! This study will take around 10 minutes.","lang":"en ru zh","url":"studies\/problem_solving\/"},{"title":"What do you see?","img":"images\/graphs-small.png","desc":"How well you can read different types of graphs? Learn what type of graph is best for you, and you can help us invent new graph types. This experiment takes about 10 minutes and requires a laptop or tablet.","lang":"en es","url":"http:\/\/graphs.labinthewild.org\/"},{"title":"Where in the world does your taste live?","img":"images\/aesthetics_around_the_world.jpg","desc":"Judge the visual appeal of website screenshots and we will tell you the country that your taste matches up with.","lang":"en","url":"studies\/aesthetics\/"},{"title":"How good is your nutrition knowledge?","img":"images\/vegetables-200.jpg","desc":"How good is your nutrition gut? Can you tell just by looking at a meal whether it is a significant source of protein, fat, or carbohydrates? This test takes about 10 minutes.","lang":"en","url":"http:\/\/food.labinthewild.org\/study1\/"},{"title":"What is your privacy profile?","img":"images\/privacy-iot-logo-small.png","desc":"Find out how your data sharing behavior compares to others and learn about the Internet of Things. This study takes around 10 minutes.","lang":"en pt","url":"studies\/privacy-iot\/"},{"title":"Multitasking Test","img":"images\/multitasking.png","desc":"How well can you multitask? Compare yourself to others by taking this test! Takes about 10 minutes.","lang":"en","url":"http:\/\/multitasking.labinthewild.org\/multitasking\/"},{"title":"Test your social intelligence!","img":"images\/intelligence2.png","desc":"Test how well you can read emotions of others just by looking at their eyes. This experiment takes around 10 minutes.","lang":"en","url":"http:\/\/socialintelligence.labinthewild.org\/mite\/?"}]' ); // add test info to table for tests we want to display $.each(tests, function (index, t) { // if title is in our array of desired test titles if ($.inArray(t.title, testTitles) !== -1) { links += "" + "" + "" + ""; } }); // finish off the table and officially append to results links += "
" + "" + "" + "

" + t.title + "

" + "

" + t.desc + " " + "Participate now!" + "" + "

" + "
"; $("#" + divId).append(links); // link back to home page and put copyright at bottom var home = "

" + "Or Return to the Wild!" + "

" + "
 
" + "Copyright 2015, LabintheWild." + "
"; $(home).appendTo("#" + divId); }, ///////////////////////////////////////////////// // // // functions for tracking dropouts // // // ///////////////////////////////////////////////// /** * Writes an entry in the dropouts table. * @function _recordDropoutProgression * @param {number} code - Numerical code identifying the current progression state. * @param {string} [description] - A description of this progression state. */ _recordDropoutProgression = function(code, description) { properties.global.progressionDescription = (description || ""); var data = { participantId: properties.global._participantId, progression: properties.global._progression, description: properties.global.progressionDescription, dropoutCode: code, action: "dropoutProgression" } $.ajax({ type: "POST", url: properties.global.dataScriptLoc, data: data }).done(function(response) { properties.global._progression++; }); }, /** * Record a dropout marker. * @function recordProgression * @param {string} [description] - A description of this dropout marker. */ recordProgression = function(description) { if (!description) var description = "unknown"; _recordDropoutProgression(1, description); }, /** * Log current properties for properties[context][item]. * @function logProps * @param {string} context - The context to search in. * @param {string} item - The item to retrieve. */ logProps = function(context, item) { console.log(properties[context][item]); } // public-facing methods return { initialize: initialize, initializeLite: initializeLite, showNextButton: showNextButton, hideNextButton: hideNextButton, showSlide: showSlide, shuffleArray: shuffleArray, increasePhaseCounter: increasePhaseCounter, increaseTrialCounter: increaseTrialCounter, getPhaseNum: getPhaseNum, getTrialNum: getTrialNum, getCumulativeTrialNum: getCumulativeTrialNum, recordResult: recordResult, getParticipantId: getParticipantId, setStatistics: setStatistics, getStatistics: getStatistics, getGraphColors: getGraphColors, showCommentsPage: showCommentsPage, newForm: newForm, logProps: logProps, recordProgression: recordProgression, showFooter: showFooter } })();