├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ └── npmpublish.yml ├── doc └── images │ └── Example.png ├── .vscode └── settings.json ├── postVersion.js ├── .eslintrc ├── LICENSE ├── package.json ├── .gitignore ├── src └── QualtricsGoogleMap.ts ├── tsconfig.json └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: pkmnct 2 | patreon: pkmnct 3 | custom: ['https://paypal.me/pkmnct'] 4 | -------------------------------------------------------------------------------- /doc/images/Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkmnct/qualtrics-google-map-lat-long/HEAD/doc/images/Example.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "eslint.format.enable": true, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | # the Node.js versions to build on 12 | node-version: [13.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | 22 | - name: Install dependencies 23 | run: npm install 24 | 25 | - name: Lint the project 26 | run: npm run lint 27 | 28 | - name: Build the project 29 | run: npm run build 30 | env: 31 | CI: true 32 | -------------------------------------------------------------------------------- /postVersion.js: -------------------------------------------------------------------------------- 1 | // This script ensures that the Readme includes the up-to-date script src 2 | 3 | /* eslint-disable */ 4 | const fs = require("fs"); 5 | 6 | const filename = "README.md"; 7 | const version = process.argv[2]; 8 | const regex = /qualtrics-google-map-lat-long@(.*)\/dist/g; 9 | const replace = `qualtrics-google-map-lat-long@${version}/dist`; 10 | 11 | if (process.argv.length !== 3) { 12 | return console.error("Invalid Arguments"); 13 | } 14 | 15 | fs.readFile(filename, "utf8", (err, data) => { 16 | if (err) { 17 | return console.error(err); 18 | } 19 | const result = data.replace(regex, replace); 20 | 21 | fs.writeFile(filename, result, "utf8", (err) => { 22 | if (err) { 23 | return console.error(err); 24 | } 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/eslint-recommended", 6 | "plugin:@typescript-eslint/recommended" 7 | ], 8 | "parserOptions": { 9 | "ecmaVersion": 2018, 10 | "sourceType": "module" 11 | }, 12 | "ignorePatterns": [ 13 | "dist" 14 | ], 15 | "rules": { 16 | "quotes": ["warn", "single"], 17 | "indent": ["warn", 2, { "SwitchCase": 1 }], 18 | "linebreak-style": ["warn", "unix"], 19 | "semi": ["warn", "always"], 20 | "comma-dangle": ["warn", "always-multiline"], 21 | "eqeqeq": "warn", 22 | "curly": ["warn", "all"], 23 | "brace-style": ["warn"], 24 | "prefer-arrow-callback": ["warn"], 25 | "lines-between-class-members": ["warn", "always", {"exceptAfterSingleLine": true}], 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 12 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: 12 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 George W. Walker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qualtrics-google-map-lat-long", 3 | "version": "2.0.1", 4 | "description": "Embed a customizable Google Map in your Qualtrics survey to collect location data", 5 | "scripts": { 6 | "lint": "eslint src/**.ts", 7 | "lint:fix": "eslint src/**.ts --fix", 8 | "build": "rimraf ./dist && tsc && npm run uglify", 9 | "uglify": "uglifyjs ./dist/QualtricsGoogleMap.js -o ./dist/QualtricsGoogleMap.min.js --compress --mangle --source-map \"content='./dist/QualtricsGoogleMap.js.map',url='QualtricsGoogleMap.min.js.map'\"", 10 | "prepublishOnly": "npm run lint && npm run postversion && npm run build", 11 | "postversion": "cross-var node postVersion.js $npm_package_version" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/pkmnct/qualtrics-google-map-lat-long.git" 16 | }, 17 | "keywords": [ 18 | "Qualtrics", 19 | "Google Maps", 20 | "Maps", 21 | "Survey", 22 | "Lat", 23 | "Long", 24 | "Latitude", 25 | "Longitude", 26 | "Map" 27 | ], 28 | "author": "George W. Walker", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/pkmnct/qualtrics-google-map-lat-long/issues" 32 | }, 33 | "homepage": "https://github.com/pkmnct/qualtrics-google-map-lat-long#readme", 34 | "devDependencies": { 35 | "@types/googlemaps": "^3.39.11", 36 | "@typescript-eslint/eslint-plugin": "^3.7.0", 37 | "@typescript-eslint/parser": "^3.7.0", 38 | "cross-var": "^1.1.0", 39 | "eslint": "^7.5.0", 40 | "rimraf": "^3.0.2", 41 | "typescript": "^3.9.7", 42 | "uglify-js": "^3.10.0" 43 | }, 44 | "files": [ 45 | "dist/*", 46 | "src/*" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore macOS files 2 | .DS_Store 3 | 4 | # Ignore compiled code 5 | dist 6 | 7 | # ------------- Defaults ------------- # 8 | 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | lerna-debug.log* 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | *.lcov 32 | 33 | # nyc test coverage 34 | .nyc_output 35 | 36 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | bower_components 41 | 42 | # node-waf configuration 43 | .lock-wscript 44 | 45 | # Compiled binary addons (https://nodejs.org/api/addons.html) 46 | build/Release 47 | 48 | # Dependency directories 49 | node_modules/ 50 | jspm_packages/ 51 | 52 | # Snowpack dependency directory (https://snowpack.dev/) 53 | web_modules/ 54 | 55 | # TypeScript cache 56 | *.tsbuildinfo 57 | 58 | # Optional npm cache directory 59 | .npm 60 | 61 | # Optional eslint cache 62 | .eslintcache 63 | 64 | # Microbundle cache 65 | .rpt2_cache/ 66 | .rts2_cache_cjs/ 67 | .rts2_cache_es/ 68 | .rts2_cache_umd/ 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variables file 80 | .env 81 | .env.test 82 | 83 | # parcel-bundler cache (https://parceljs.org/) 84 | .cache 85 | .parcel-cache 86 | 87 | # Next.js build output 88 | .next 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # Serverless directories 104 | .serverless/ 105 | 106 | # FuseBox cache 107 | .fusebox/ 108 | 109 | # DynamoDB Local files 110 | .dynamodb/ 111 | 112 | # TernJS port file 113 | .tern-port 114 | 115 | # Stores VSCode versions used for testing VSCode extensions 116 | .vscode-test 117 | 118 | # yarn v2 119 | 120 | .yarn/cache 121 | .yarn/unplugged 122 | .yarn/build-state.yml 123 | .pnp.* -------------------------------------------------------------------------------- /src/QualtricsGoogleMap.ts: -------------------------------------------------------------------------------- 1 | interface Map { 2 | css?: string; 3 | options: google.maps.MapOptions; 4 | markers?: { 5 | autocomplete?: { 6 | enabled: boolean; 7 | label: string; 8 | css?: string; 9 | labelCss?: string; 10 | invalidLocationAlertText: string; 11 | }, 12 | options: google.maps.MarkerOptions; 13 | }[]; 14 | } 15 | 16 | interface Question { 17 | id: string; 18 | container: Element; 19 | map: Map; 20 | } 21 | 22 | const initGoogleMapsQuestion = ( 23 | id: Question['id'], 24 | container: Question['container'], 25 | map: Question['map'], 26 | ): void | Error => { 27 | // Find the dataBox and hide it 28 | const dataBox = document.getElementById(`QR~${id}`) as HTMLInputElement | null; 29 | if (!dataBox) { 30 | return new Error(`Could not find input for question with id ${id}.`); 31 | } 32 | dataBox.style.display = 'none'; 33 | 34 | // Find the QuestionBody to append to 35 | const questionBody = container.querySelector('.QuestionBody') || container; 36 | 37 | // Initialize data storage or load from existing data in field 38 | const value: { [key: number]: google.maps.LatLng } = dataBox.value !== '' ? JSON.parse(dataBox.value) : {}; 39 | 40 | // Function to set the dataBox to a lat/lng 41 | const setLatLng = (key: number, latLng: google.maps.LatLng) => { 42 | value[key] = latLng; 43 | dataBox.value = JSON.stringify(value); 44 | }; 45 | 46 | const styles = document.createElement('style'); 47 | document.head.appendChild(styles); 48 | 49 | // Create the map node 50 | const mapObject = document.createElement('div'); 51 | mapObject.setAttribute('id', `${id}-map`); 52 | if (map.css) { 53 | styles.innerText += `#${id}-map {${map.css}}`; 54 | mapObject.setAttribute('style', map.css); 55 | } else { 56 | styles.innerText += `#${id}-map {height: 300px;}`; 57 | } 58 | questionBody.appendChild(mapObject); 59 | 60 | // Initialize the Google Map 61 | const googleMap = new google.maps.Map(mapObject, map.options); 62 | 63 | // Initialize the Markers 64 | map.markers?.forEach((marker, index) => { 65 | // Create the marker 66 | const mapMarker = new google.maps.Marker({ 67 | ...marker.options, 68 | map: googleMap, 69 | position: index in value ? value[index] : marker.options.position || map.options.center, 70 | }); 71 | 72 | if (marker.autocomplete?.enabled) { 73 | const inputId = `${id}-${index}-locationInput`; 74 | 75 | // Make the label for the autocomplete 76 | const locationLabel = document.createElement('label'); 77 | locationLabel.setAttribute('for', inputId); 78 | locationLabel.setAttribute('id', `${inputId}-label`); 79 | locationLabel.setAttribute('class', 'QuestionText'); 80 | if (marker.autocomplete.labelCss) { 81 | styles.innerText += `#${inputId}-label {${marker.autocomplete.labelCss}}`; 82 | } 83 | locationLabel.innerText = marker.autocomplete.label || marker.options.title || `Marker ${marker.options.label ? marker.options.label : index}`; 84 | questionBody.appendChild(locationLabel); 85 | 86 | // Make the autocomplete 87 | const locationInput = document.createElement('input'); 88 | locationInput.setAttribute('id', inputId); 89 | locationInput.setAttribute('class', 'InputText'); 90 | if (marker.autocomplete.css) { 91 | styles.innerText += `#${id}-${index}-locationInput {${marker.autocomplete.css}}`; 92 | } 93 | questionBody.appendChild(locationInput); 94 | 95 | // Load the places API 96 | const locationAutocomplete = new google.maps.places.Autocomplete(locationInput); 97 | 98 | // Whenever the inputs change, set the locationLatLong and pan the map to the location 99 | google.maps.event.addListener(locationAutocomplete, 'place_changed', () => { 100 | const place = locationAutocomplete.getPlace(); 101 | 102 | if (place.geometry) { 103 | mapMarker.setPosition(place.geometry.location); 104 | googleMap.panTo(place.geometry.location); 105 | setLatLng(index, place.geometry.location); 106 | } else { 107 | alert(marker.autocomplete?.invalidLocationAlertText || 'Invalid Location'); 108 | } 109 | }); 110 | } 111 | 112 | // If there is only one marker, allow setting its position by clicking the map 113 | const draggableMarkerCount = map.markers?.filter(marker => marker.options.draggable).length; 114 | if (draggableMarkerCount === 1) { 115 | // When the map is clicked, move the marker and update stored position 116 | google.maps.event.addListener(googleMap, 'click', event => { 117 | setLatLng(index, event.latLng); 118 | mapMarker.setPosition(event.latLng); 119 | }); 120 | } 121 | 122 | // When the marker is dragged, store the lat/lng where it ends 123 | google.maps.event.addListener(mapMarker, 'dragend', event => { 124 | setLatLng(index, event.latLng); 125 | }); 126 | }); 127 | }; 128 | 129 | // Typescript doesn't allow augmentation of the global scope except in modules, but we need to expose this to the global scope 130 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 131 | // @ts-ignore 132 | window.initGoogleMapsQuestion = initGoogleMapsQuestion; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "none", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qualtrics Google Map 2 | 3 | Embed a customizable Google Map in your Qualtrics survey to collect location data 4 | 5 | ![Screenshot of Example](doc/images/Example.png) 6 | 7 | ## Prerequisites 8 | 9 | ### Qualtrics 10 | 11 | Unfortunately Qualtrics does not allow adding JavaScript to questions on free accounts. In order to use Google Maps in your question, you must have a full account. See _[Trial Accounts](https://www.qualtrics.com/support/survey-platform/managing-your-account/trial-accounts/)_ on Qualtrics' support site for more information. 12 | 13 | ### Google Maps 14 | 15 | You must have a valid Google Maps JavaScript API key. If you want to use the autocomplete functionality, the API key must have access to the Places API as well. See _[Get Maps JavaScript API Key](https://developers.google.com/maps/documentation/javascript/get-api-key)_ and _[Get Places API Key](https://developers.google.com/places/web-service/get-api-key)_ on Google's developer documentation. 16 | 17 | ## Getting Started 18 | 19 | ### Header Script 20 | 21 | The first step is to add the Google Maps API and this script to your survey's header. See _[Adding a Survey Header/Footer](https://www.qualtrics.com/support/survey-platform/survey-module/look-feel/general-look-feel-settings/#AddFooterHeader)_ on Qualtrics' support site. When you get to the Rich Text Editor, click the Source Dialog icon in the toolbar to display HTML. Paste the following at the top of the header: 22 | 23 | ```html 24 | 25 | 26 | ``` 27 | 28 | Make sure to replace the `{YOURKEYHERE}` with your Google Maps API key. 29 | 30 | ### Adding a Map Question 31 | 32 | Once you have the Header Script added, you can create map questions. Start by making a new _Text Entry_ question. Ensure that the text type used is single line. You can treat this question like you would any other (ex. require a response, change the title, etc.). It is recommended that you provide clear instructions to the responder in the question. 33 | 34 | On the left side of the question, click the gear and select _Add JavaScript..._ 35 | 36 | Copy the code from the example below and modify the options to adjust how the map will render. Paste this below the `/*Place your JavaScript here to run when the page loads*/` text in the _addOnload_ section of the _Edit Question JavaScript_ dialog. 37 | 38 | See _[Add JavaScript](https://www.qualtrics.com/support/survey-platform/survey-module/question-options/add-javascript/)_ on Qualtrics' support site for more information. 39 | 40 | #### Option Documentation 41 | 42 | - Map Options are documented on Google's developer documentation. [Center and zoom options are required](https://developers.google.com/maps/documentation/javascript/overview#MapOptions), and there are [many other options you can configure](https://developers.google.com/maps/documentation/javascript/reference/map#MapOptions). 43 | - [Marker Options are documented on Google's developer documentation](https://developers.google.com/maps/documentation/javascript/reference/marker#MarkerOptions) 44 | - Marker Autocomplete Options 45 | - You can enable an autocomplete field to assist responders in finding a location. Responders can enter a location into this field and the map will snap its marker to that location. They can then fine-tune the response by dragging the marker to a specific location (such as a door to a building). 46 | - See the example markers below 47 | - If there is only one marker, you can click on the map to set its location. 48 | 49 | #### Example 50 | 51 | ```js 52 | initGoogleMapsQuestion(this.questionId, this.getQuestionContainer(), { 53 | // Map Options, set these! See Map Options in Option Documentation Section 54 | options: { 55 | center: { 56 | lat: 39.1836, 57 | lng: -96.5717, 58 | }, 59 | zoom: 16, 60 | }, 61 | // Marker Options, set these! 62 | markers: [ 63 | // First Marker 64 | { 65 | // See Marker Options in Option Documentation Section 66 | options: { 67 | title: "Marker 1", 68 | draggable: true, 69 | label: "1", 70 | }, 71 | autocomplete: { 72 | // If true, an autocomplete will show. 73 | enabled: true, 74 | // The label shown for the autocomplete field 75 | label: "Location for Marker 1", 76 | // Styles for the label 77 | labelCss: "padding-left: 0; padding-right: 0;", 78 | // Text to show if an invalid location is selected 79 | invalidLocationAlertText: 80 | "Please choose a location from the search dropdown. If your location doesn't appear in the search, enter a nearby location and move the marker to the correct location.", 81 | }, 82 | }, 83 | // Second Marker 84 | { 85 | // See Marker Options in Option Documentation Section 86 | options: { 87 | title: "This is an example second marker. Rename or delete me.", 88 | draggable: true, 89 | position: { 90 | lat: 39.184, 91 | lng: -96.572, 92 | }, 93 | label: "2", 94 | }, 95 | autocomplete: { 96 | // If true, an autocomplete will show. 97 | enabled: true, 98 | // The label shown for the autocomplete field 99 | label: "Location for Marker 2", 100 | // Styles for the label 101 | labelCss: "padding-left: 0; padding-right: 0;", 102 | // Text to show if an invalid location is selected 103 | invalidLocationAlertText: 104 | "Please choose a location from the search dropdown. If your location doesn't appear in the search, enter a nearby location and move the marker to the correct location.", 105 | }, 106 | }, 107 | // You can add more markers as well 108 | ], 109 | }); 110 | ``` 111 | 112 | Click Save. Test the question before sending out the survey. If you have any issues, see _Troubleshooting_ below. 113 | 114 | ## Data Collected 115 | 116 | The result is collected in the Qualtrics question field as a string representation of a JSON object. Each marker is represented by its index as the key in the object and the latitude and longitude as the value. 117 | 118 | ### Example Single-Marker Question Data 119 | 120 | `{0:{"lat":38.8951,"long":-77.0364}}` 121 | 122 | ### Example Multi-Marker Question Data 123 | 124 | `{0:{"lat":38.8951,"long":-77.0364},1:{"lat":38.8951,"long":-77.0364},2:{"lat":38.8951,"long":-77.0364}}` 125 | 126 | ## Troubleshooting 127 | 128 | ### The map doesn't show after adding it to the question 129 | 130 | The map will only show up in the actual survey, not in the back-end of Qualtrics. Try to preview or take the survey. 131 | 132 | ### The map or autocomplete field search shows "_This page can't load Google Maps correctly_" or "_For development purposes only_" 133 | 134 | This usually indicates an issue with your API key. Make sure you set the API key in the Google Maps script placed in the header. Check that the API key has access to both the Maps JavaScript API, and if you are using the Autocomplete Field, the Places API. If you are still having trouble, follow [Google's API key troubleshooting steps](https://developers.google.com/maps/documentation/javascript/error-messages). 135 | 136 | ### Responses are not saving 137 | 138 | Ensure that the text type used on your form is single line. [See Issue #6](https://github.com/pkmnct/qualtrics-google-map-lat-long/issues/6). 139 | 140 | ### I'm still having problems 141 | 142 | Make sure you are using the latest version of the code. If that doesn't help, see if an [issue](https://github.com/pkmnct/qualtrics-google-map-lat-long/issues) has been created for the problem you are facing already. If not, you can [create a new issue](https://github.com/pkmnct/qualtrics-google-map-lat-long/issues). 143 | 144 | ## Migrating from 1.x 145 | 146 | If you have used older versions of this script in your survey, you have a few options to migrate. Version 2.0 changes the way the data is stored in the text field to support multiple map markers. If you have started collecting survey responses, it may be beneficial for all of the data collected to be in the same format. If that is the case, follow the _I have already collected survey responses_ section below. If you do not mind the survey results mixing data types, or have not started collecting data, see the _I have not started collecting survey responses_ section below. 147 | 148 | ### I have already collected survey responses 149 | 150 | In this case, it is recommended that you continue using [the older version of the script](https://github.com/pkmnct/qualtrics-google-map-lat-long/blob/4e9ab1288e6a030431b0e9eab6db56ba5b5062a2/README.md). This will ensure that all of the data collected is in the same format. If you do not mind the survey results mixing data types, see the _I have not started collecting survey responses_ section below. 151 | 152 | ### I have not started collecting survey responses 153 | 154 | In this case, it is recommended that you update all older questions to use the new script. You can also mix 1.x and 2.x questions, but you must remove the code that loads the Google Maps API in all 1.x questions. This is the last code block pasted, below the _Load the Google Maps API if it is not already loaded_ comment. If you do not want to update questions, you can still use [the older version of the script](https://github.com/pkmnct/qualtrics-google-map-lat-long/blob/4e9ab1288e6a030431b0e9eab6db56ba5b5062a2/README.md). 155 | --------------------------------------------------------------------------------